├── test
├── Fixture
│ ├── Bundle
│ │ ├── tempfile.txt
│ │ ├── BarBundle
│ │ │ ├── Resources
│ │ │ │ ├── assets
│ │ │ │ │ ├── app.js
│ │ │ │ │ └── asset.js
│ │ │ │ └── views
│ │ │ │ │ ├── base.format.engine
│ │ │ │ │ ├── controller
│ │ │ │ │ └── base.format.engine
│ │ │ │ │ └── this.is.a.template.format.engine
│ │ │ ├── BarBundle.php
│ │ │ ├── Loader
│ │ │ │ └── MockLoader.php
│ │ │ └── DependencyInjection
│ │ │ │ └── BarExtension.php
│ │ └── FooBundle
│ │ │ ├── Resources
│ │ │ ├── public
│ │ │ │ └── public.js
│ │ │ └── views
│ │ │ │ └── foo.html.twig
│ │ │ └── FooBundle.php
│ ├── cache
│ │ └── .gitkeep
│ ├── config
│ │ ├── routing.yml
│ │ └── config.yml
│ ├── Resources
│ │ ├── assets
│ │ │ └── base.js
│ │ └── views
│ │ │ ├── common_id.html.twig
│ │ │ └── template.html.twig
│ ├── node_modules
│ │ └── webpack
│ │ │ └── bin
│ │ │ └── webpack.js
│ ├── TestKernel.php
│ └── Component
│ │ └── Configuration
│ │ └── ConfigGenerator.js
├── Component
│ ├── Asset
│ │ ├── Fixtures
│ │ │ ├── template_parse_error.html.twig
│ │ │ └── template.html.twig
│ │ ├── TemplateReferenceTest.php
│ │ ├── TemplateFinderTest.php
│ │ ├── DumperTest.php
│ │ ├── CompilerTest.php
│ │ ├── CacheGuardTest.php
│ │ ├── TwigParserTest.php
│ │ └── TrackedFilesTest.php
│ ├── Profiler
│ │ └── WebpackDataCollectorTest.php
│ └── Configuration
│ │ ├── CodeBlockTest.php
│ │ ├── Plugin
│ │ ├── DefinePluginTest.php
│ │ └── ProvidePluginTest.php
│ │ ├── Config
│ │ ├── ResolveLoaderConfigTest.php
│ │ ├── ResolveConfigTest.php
│ │ └── OutputConfigTest.php
│ │ ├── Loader
│ │ ├── LessLoaderTest.php
│ │ ├── TypeScriptLoaderTest.php
│ │ ├── CoffeeScriptLoaderTest.php
│ │ ├── BabelLoaderTest.php
│ │ ├── SassLoaderTest.php
│ │ ├── UrlLoaderTest.php
│ │ └── CssLoaderTest.php
│ │ └── ConfigGeneratorTest.php
├── Functional
│ ├── TwigTest.php
│ ├── ConfigGeneratorTest.php
│ ├── DumperTest.php
│ ├── AssetTest.php
│ └── CompileTest.php
├── Bundle
│ ├── Command
│ │ └── CompileCommandTest.php
│ ├── Twig
│ │ ├── Token
│ │ │ └── WebpackTokenParserTest.php
│ │ └── TwigExtensionTest.php
│ ├── CacheWarmer
│ │ └── WebpackCompileCacheWarmerTest.php
│ ├── DependencyInjection
│ │ ├── WebpackExtensionTest.php
│ │ ├── ConfigurationTest.php
│ │ └── WebpackCompilerPassTest.php
│ ├── EventListener
│ │ └── RequestListenerTest.php
│ └── WebpackBundleTest.php
└── AbstractTestCase.php
├── .gitignore
├── src
├── Bundle
│ ├── Resources
│ │ ├── views
│ │ │ └── sections
│ │ │ │ ├── config.html.twig
│ │ │ │ ├── assets.html.twig
│ │ │ │ └── output.html.twig
│ │ └── config
│ │ │ ├── plugins.yml
│ │ │ ├── config.yml
│ │ │ ├── dev.yml
│ │ │ ├── loaders.yml
│ │ │ └── webpack.yml
│ ├── Twig
│ │ ├── Node
│ │ │ ├── WebpackNode.php
│ │ │ └── WebpackInlineNode.php
│ │ ├── TwigExtension.php
│ │ └── Token
│ │ │ └── WebpackTokenParser.php
│ ├── WebpackBundle.php
│ ├── CacheWarmer
│ │ └── WebpackCompileCacheWarmer.php
│ ├── EventListener
│ │ └── RequestListener.php
│ ├── Command
│ │ └── CompileCommand.php
│ └── DependencyInjection
│ │ ├── WebpackExtension.php
│ │ ├── WebpackCompilerPass.php
│ │ └── Configuration.php
└── Component
│ ├── Configuration
│ ├── Config
│ │ ├── ConfigInterface.php
│ │ ├── ResolveLoaderConfig.php
│ │ ├── ResolveConfig.php
│ │ └── OutputConfig.php
│ ├── Loader
│ │ ├── LoaderInterface.php
│ │ ├── BabelLoader.php
│ │ ├── CoffeeScriptLoader.php
│ │ ├── TypeScriptLoader.php
│ │ ├── UrlLoader.php
│ │ ├── LessLoader.php
│ │ ├── CssLoader.php
│ │ └── SassLoader.php
│ ├── Plugin
│ │ ├── PluginInterface.php
│ │ ├── ProvidePlugin.php
│ │ ├── DefinePlugin.php
│ │ └── UglifyJsPlugin.php
│ ├── CodeBlockProviderInterface.php
│ ├── ConfigExtensionInterface.php
│ ├── CodeBlock.php
│ └── ConfigGenerator.php
│ ├── Profiler
│ ├── Profiler.php
│ └── WebpackDataCollector.php
│ └── Asset
│ ├── TemplateReference.php
│ ├── CacheGuard.php
│ ├── TrackedFiles.php
│ ├── Dumper.php
│ ├── TemplateFinder.php
│ ├── Compiler.php
│ ├── TwigParser.php
│ └── Tracker.php
├── phpcs.xml.dist
├── phpunit.xml.dist
├── .travis.yml
├── LICENSE
└── composer.json
/test/Fixture/Bundle/tempfile.txt:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/Fixture/cache/.gitkeep:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/test/Fixture/config/routing.yml:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/Fixture/Resources/assets/base.js:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/Fixture/Bundle/BarBundle/Resources/assets/app.js:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/Fixture/Bundle/BarBundle/Resources/assets/asset.js:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/Fixture/Bundle/FooBundle/Resources/public/public.js:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/Fixture/Bundle/FooBundle/Resources/views/foo.html.twig:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/Fixture/Bundle/BarBundle/Resources/views/base.format.engine:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/Fixture/node_modules/webpack/bin/webpack.js:
--------------------------------------------------------------------------------
1 | // Placeholder
2 |
--------------------------------------------------------------------------------
/test/Fixture/Bundle/BarBundle/Resources/views/controller/base.format.engine:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/Fixture/Bundle/BarBundle/Resources/views/this.is.a.template.format.engine:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/Component/Asset/Fixtures/template_parse_error.html.twig:
--------------------------------------------------------------------------------
1 | I contain parse errors.
2 |
3 | {% set asset = webpack_asset this is not allowed. %}
4 |
--------------------------------------------------------------------------------
/test/Fixture/Resources/views/common_id.html.twig:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /test/Fixture/cache/
2 | /test/Fixture/logs/
3 | /var/
4 |
5 | # PHPUnit
6 | /phpunit.xml
7 | coverage/
8 |
9 | # composer
10 | /vendor/
11 | /composer.phar
12 | composer.lock
13 |
--------------------------------------------------------------------------------
/src/Bundle/Resources/views/sections/config.html.twig:
--------------------------------------------------------------------------------
1 |
2 |
3 | Config information goes here.
4 |
5 |
6 |
--------------------------------------------------------------------------------
/src/Component/Configuration/Config/ConfigInterface.php:
--------------------------------------------------------------------------------
1 |
2 |
3 | test/Fixture/cache/*
4 |
5 |
6 | test/Fixture/TestKernel.php
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/test/Fixture/Bundle/BarBundle/BarBundle.php:
--------------------------------------------------------------------------------
1 | getAttribute('files') as $file) {
20 | $compiler->write('$context["asset"] = "' . $file . '";');
21 | $this->nodes[0]->compile($compiler);
22 | }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/Bundle/Resources/config/plugins.yml:
--------------------------------------------------------------------------------
1 | services:
2 | hostnet_webpack.plugin.define:
3 | class: Hostnet\Component\Webpack\Configuration\Plugin\DefinePlugin
4 | tags:
5 | - { name: "hostnet_webpack.config_extension" }
6 | hostnet_webpack.plugin.provide:
7 | class: Hostnet\Component\Webpack\Configuration\Plugin\ProvidePlugin
8 | tags:
9 | - { name: "hostnet_webpack.config_extension" }
10 | hostnet_webpack.plugin.uglifyjs:
11 | class: Hostnet\Component\Webpack\Configuration\Plugin\UglifyJsPlugin
12 | tags:
13 | - { name: "hostnet_webpack.config_extension" }
14 |
--------------------------------------------------------------------------------
/src/Component/Profiler/Profiler.php:
--------------------------------------------------------------------------------
1 | logs[$key] = $value;
23 | }
24 |
25 | /**
26 | * @param string $id
27 | * @return mixed
28 | */
29 | public function get($id)
30 | {
31 | return $this->logs[$id] ?? null;
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/Bundle/Resources/views/sections/assets.html.twig:
--------------------------------------------------------------------------------
1 |
2 |
Bundles
3 |
4 |
5 | {% for bundle, path in collector.get('bundles') %}
6 |
7 | | {{ bundle }} |
8 | {{ path }} |
9 |
10 |
11 | {% endfor %}
12 |
13 |
Templates
14 |
15 |
16 | {% for path in collector.get('templates') %}
17 |
18 | | {{ path }} |
19 |
20 |
21 | {% endfor %}
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/test/Fixture/Bundle/BarBundle/Loader/MockLoader.php:
--------------------------------------------------------------------------------
1 | code_blocks_called = true;
21 | return [(new CodeBlock())->set(CodeBlock::LOADER, self::BLOCK_CONTENT)];
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/Bundle/Resources/config/config.yml:
--------------------------------------------------------------------------------
1 | # Contains service definitions that extend the application configuration and help generate the final webpack-
2 | # configuration file.
3 | services:
4 | hostnet_webpack.config.output:
5 | class: Hostnet\Component\Webpack\Configuration\Config\OutputConfig
6 | tags:
7 | - { name: "hostnet_webpack.config_extension" }
8 |
9 | hostnet_webpack.config.resolve:
10 | class: Hostnet\Component\Webpack\Configuration\Config\ResolveConfig
11 | tags:
12 | - { name: "hostnet_webpack.config_extension" }
13 |
14 | hostnet_webpack.config.resolve_loader:
15 | class: Hostnet\Component\Webpack\Configuration\Config\ResolveLoaderConfig
16 | tags:
17 | - { name: "hostnet_webpack.config_extension" }
18 |
--------------------------------------------------------------------------------
/src/Bundle/Resources/config/dev.yml:
--------------------------------------------------------------------------------
1 | services:
2 | Hostnet\Bundle\WebpackBundle\EventListener\RequestListener:
3 | arguments:
4 | - "@hostnet_webpack.bridge.asset_cacheguard"
5 | tags:
6 | - { name: "kernel.event_listener", event: "kernel.request", method: "onRequest"}
7 |
8 | Hostnet\Component\Webpack\Profiler\WebpackDataCollector:
9 | public: true
10 | arguments:
11 | - "@hostnet_webpack.bridge.profiler"
12 | tags:
13 | - { name: "data_collector", template: "WebpackBundle::profiler.html.twig", id: "webpack" }
14 |
15 | # BC aliases
16 | hostnet_webpack.bridge.request_listener: '@Hostnet\Bundle\WebpackBundle\EventListener\RequestListener'
17 | hostnet_webpack.bridge.data_collector: '@Hostnet\Component\Webpack\Profiler\WebpackDataCollector'
18 |
--------------------------------------------------------------------------------
/phpunit.xml.dist:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | ./test
15 |
16 |
17 |
18 |
19 | ./src
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/src/Component/Configuration/ConfigExtensionInterface.php:
--------------------------------------------------------------------------------
1 | addCompilerPass(new WebpackCompilerPass());
27 | }
28 |
29 | /**
30 | * {@inheritdoc}
31 | */
32 | public function getContainerExtension()
33 | {
34 | return new WebpackExtension();
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/test/Functional/TwigTest.php:
--------------------------------------------------------------------------------
1 | getContainer()->get('twig');
24 | $html = $twig->render('/common_id.html.twig');
25 |
26 | self::assertRegExp('~src="/compiled/shared\.js\?[0-9]+"~', $html);
27 | self::assertRegExp('~href="/compiled/shared\.css\?[0-9]+"~', $html);
28 |
29 | $twig->render('/template.html.twig');
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/test/Bundle/Command/CompileCommandTest.php:
--------------------------------------------------------------------------------
1 | prophesize(CacheGuard::class);
25 | $guard->rebuild()->shouldBeCalled();
26 |
27 | $compile_command = new CompileCommand($guard->reveal());
28 |
29 | $compile_command->run(new StringInput(''), new NullOutput());
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: php
2 |
3 | sudo: false
4 |
5 | cache:
6 | directories:
7 | - "$HOME/.composer/cache"
8 |
9 | env:
10 | - COMPOSER_FLAGS="--prefer-stable"
11 |
12 | php:
13 | - '7.1'
14 | - '7.2'
15 | - '7.3'
16 | - nightly
17 |
18 | matrix:
19 | include:
20 | - php: 7.1
21 | env: COMPOSER_FLAGS="--prefer-lowest"
22 | - php: 7.1
23 | env: COMPOSER_FLAGS=""
24 | - php: 7.2
25 | env: COMPOSER_FLAGS="--prefer-lowest"
26 | - php: 7.2
27 | env: COMPOSER_FLAGS=""
28 | - php: 7.3
29 | env: COMPOSER_FLAGS="--prefer-lowest"
30 | - php: 7.3
31 | env: COMPOSER_FLAGS=""
32 | allow_failures:
33 | - php: nightly
34 |
35 | before_install:
36 | - phpenv config-rm xdebug.ini
37 | - composer self-update
38 | - if [ "$SYMFONY_VERSION" != "" ]; then composer require --no-update symfony/symfony:${SYMFONY_VERSION}; fi
39 |
40 | install: composer update $COMPOSER_FLAGS --prefer-dist
41 |
42 | script: vendor/bin/phpunit
43 |
--------------------------------------------------------------------------------
/test/Bundle/Twig/Token/WebpackTokenParserTest.php:
--------------------------------------------------------------------------------
1 | prophesize(LoaderInterface::class)->reveal();
21 | $extension = new TwigExtension(
22 | $loader,
23 | __DIR__,
24 | '/compiled',
25 | '/bundles',
26 | '/compiled/shared.js',
27 | '/compiled/shared.css'
28 | );
29 |
30 | $parser = new WebpackTokenParser($extension, $loader);
31 |
32 | self::assertEquals(WebpackTokenParser::TAG_NAME, $parser->getTag());
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/test/Component/Asset/TemplateReferenceTest.php:
--------------------------------------------------------------------------------
1 | getPath());
20 | self::assertSame('AcmeBlogBundle:Admin\Post:index.html.twig', $reference->getLogicalName());
21 |
22 | $reference = new TemplateReference(null, 'Admin\Post', 'index', 'html', 'twig');
23 | self::assertSame('views/Admin/Post/index.html.twig', $reference->getPath());
24 | self::assertSame(':Admin\Post:index.html.twig', $reference->getLogicalName());
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/test/Bundle/CacheWarmer/WebpackCompileCacheWarmerTest.php:
--------------------------------------------------------------------------------
1 | prophesize(CacheGuard::class);
23 | $guard->rebuild()->shouldBeCalled();
24 |
25 | $webpack_compile_cache_warmer = new WebpackCompileCacheWarmer($guard->reveal());
26 |
27 | //Cache warmer is optional...
28 | self::assertTrue($webpack_compile_cache_warmer->isOptional());
29 | $webpack_compile_cache_warmer->warmUp(sys_get_temp_dir());
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/test/Functional/ConfigGeneratorTest.php:
--------------------------------------------------------------------------------
1 | getContainer()->get(MockLoader::class);
21 |
22 | /** @var ConfigGenerator $config_generator */
23 | $config_generator = static::$kernel->getContainer()->get(ConfigGenerator::class);
24 |
25 | $contiguration = $config_generator->getConfiguration();
26 |
27 | self::assertTrue($mock_loader->code_blocks_called);
28 | self::assertStringContainsString(MockLoader::BLOCK_CONTENT, $contiguration);
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/test/Bundle/DependencyInjection/WebpackExtensionTest.php:
--------------------------------------------------------------------------------
1 | getAlias());
20 | }
21 |
22 | /**
23 | * @doesNotPerformAssertions
24 | */
25 | public function testLoadNoConfig(): void
26 | {
27 | $container = new ContainerBuilder();
28 | $extension = new WebpackExtension();
29 |
30 | $container->setParameter('kernel.bundles', []);
31 | $container->setParameter('kernel.environment', 'dev');
32 |
33 | // This should not fail.
34 | $extension->load([], $container);
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/test/Functional/DumperTest.php:
--------------------------------------------------------------------------------
1 | dir = Path::BASE_DIR . '/test/Fixture/cache/bundles';
25 | }
26 |
27 | public function testDump(): void
28 | {
29 | static::bootKernel();
30 |
31 | /** @var Dumper $dumper */
32 | $dumper = static::$kernel->getContainer()->get(Dumper::class);
33 | $dumper->dump();
34 |
35 | self::assertFileExists($this->dir . '/foo/public.js');
36 | }
37 |
38 | protected function tearDown(): void
39 | {
40 | shell_exec("rm -rf $this->dir");
41 |
42 | parent::tearDown();
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/test/Fixture/TestKernel.php:
--------------------------------------------------------------------------------
1 | load(__DIR__ . '/config/config.yml');
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/test/Fixture/Bundle/BarBundle/DependencyInjection/BarExtension.php:
--------------------------------------------------------------------------------
1 | setDefinition(
27 | 'bar.mock_loader',
28 | (new Definition('Hostnet\Fixture\WebpackBundle\Bundle\BarBundle\Loader\MockLoader'))
29 | ->addTag('hostnet_webpack.config_extension')
30 | );
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 Hostnet bv
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
23 |
--------------------------------------------------------------------------------
/test/Component/Profiler/WebpackDataCollectorTest.php:
--------------------------------------------------------------------------------
1 | collect(
24 | $this->getMockBuilder(Request::class)->getMock(),
25 | $this->getMockBuilder(Response::class)->getMock()
26 | );
27 |
28 | self::assertEquals('webpack', $collector->getName());
29 | self::assertNull($collector->get('foobar'));
30 |
31 | $profiler->set('foobar', 'hoi');
32 |
33 | self::assertEquals('hoi', $collector->get('foobar'));
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/Bundle/CacheWarmer/WebpackCompileCacheWarmer.php:
--------------------------------------------------------------------------------
1 | guard = $guard;
32 | }
33 |
34 | /**
35 | * {@inheritdoc}
36 | */
37 | public function warmUp($cache_dir)
38 | {
39 | $this->guard->rebuild();
40 | }
41 |
42 | /**
43 | * {@inheritdoc}
44 | */
45 | public function isOptional()
46 | {
47 | return true;
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/Bundle/EventListener/RequestListener.php:
--------------------------------------------------------------------------------
1 | guard = $guard;
29 | }
30 |
31 | /**
32 | * On Request received check the validity of the webpack cache.
33 | *
34 | * @param GetResponseEvent $event the response to send to te browser, we don't we only ensure the cache is there.
35 | */
36 | public function onRequest(GetResponseEvent $event): void
37 | {
38 | if (! $event->isMasterRequest()) {
39 | return;
40 | }
41 |
42 | $this->guard->rebuild();
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/test/Component/Asset/TemplateFinderTest.php:
--------------------------------------------------------------------------------
1 | prophesize(Kernel::class);
21 | $kernel->getBundle()->shouldNotBeCalled();
22 | $kernel->getBundles()->willReturn(['BaseBundle' => new BarBundle()]);
23 |
24 | $finder = new TemplateFinder($kernel->reveal(), __DIR__ . '/../Fixtures/Resources');
25 |
26 | $templates = array_map(function ($template) {
27 | return $template->getLogicalName();
28 | }, $finder->findAllTemplates());
29 |
30 | self::assertCount(3, $templates);
31 | self::assertContains('BarBundle::base.format.engine', $templates);
32 | self::assertContains('BarBundle::this.is.a.template.format.engine', $templates);
33 | self::assertContains('BarBundle:controller:base.format.engine', $templates);
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/test/Bundle/EventListener/RequestListenerTest.php:
--------------------------------------------------------------------------------
1 | prophesize(GetResponseEvent::class);
21 | $guard = $this->prophesize(CacheGuard::class);
22 |
23 | $guard->rebuild()->shouldNotBeCalled();
24 | $event->isMasterRequest()->willReturn(false);
25 |
26 | (new RequestListener($guard->reveal()))->onRequest($event->reveal());
27 | }
28 |
29 | public function testRequestMasterRequest(): void
30 | {
31 | $event = $this->prophesize(GetResponseEvent::class);
32 | $guard = $this->prophesize(CacheGuard::class);
33 |
34 | $guard->rebuild()->shouldBeCalled();
35 | $event->isMasterRequest()->willReturn(true);
36 |
37 | (new RequestListener($guard->reveal()))->onRequest($event->reveal());
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/Bundle/Command/CompileCommand.php:
--------------------------------------------------------------------------------
1 | guard = $guard;
35 | }
36 |
37 | /**
38 | * Execute the webpack:compile command (basicly forwards the logic to CacheGuard::validate).
39 | *
40 | * {@inheritDoc}
41 | */
42 | protected function execute(InputInterface $input, OutputInterface $output)
43 | {
44 | $this->guard->rebuild();
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/Bundle/Twig/Node/WebpackInlineNode.php:
--------------------------------------------------------------------------------
1 | getAttribute('js_file'))) {
30 | $compiler
31 | ->write('echo ')
32 | ->string('')
33 | ->raw(";\n");
34 | }
35 | if (false !== ($file = $this->getAttribute('css_file'))) {
36 | $compiler
37 | ->write('echo ')
38 | ->string('')
39 | ->raw(";\n");
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/test/Bundle/WebpackBundleTest.php:
--------------------------------------------------------------------------------
1 | getContainerExtension());
23 | $bundle->build($container);
24 |
25 | // Since sf 3.3, there are symfony passes in the list, so we can't assert for only instances of
26 | // WebpackCompilerPass anymore.
27 | self::assertNotEmpty($container->getCompilerPassConfig()->getBeforeOptimizationPasses());
28 |
29 | $found = false;
30 | foreach ($container->getCompilerPassConfig()->getBeforeOptimizationPasses() as $pass) {
31 | if ($pass instanceof WebpackCompilerPass) {
32 | $found = true;
33 | }
34 | }
35 |
36 | self::assertTrue($found);
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/test/Fixture/Resources/views/template.html.twig:
--------------------------------------------------------------------------------
1 | {# Method one: webpack_asset. #}
2 |
3 |
4 |
5 | {# Method two: webpack js #}
6 | {% webpack js '@BarBundle/app.js' '@BarBundle/app.js' %}
7 |
8 | {% endwebpack %}
9 |
10 | {# and webpack css #}
11 | {% webpack css '@BarBundle/app.js' %}
12 |
13 | {% endwebpack %}
14 |
15 | {# and webpack inline with no extension given. It must fallback to "js" by default. #}
16 | {% webpack inline %}
17 |
20 | {% endwebpack %}
21 |
22 | {# Specified an extension. Webpack will also parse the contents by the given type. #}
23 | {% webpack inline js %}
24 | console.log("HENK");
25 | {% endwebpack %}
26 |
27 | {# Parse the contents as it were a LESS file. #}
28 | {% webpack inline less %}
29 | @size: 42px;
30 | * { font-size: @size; }
31 | {% endwebpack %}
32 |
33 | {# Parse the contents as it were a CSS file. Also, feel free to use style tags (for IDE syntax highlighting)... #}
34 | {% webpack inline css %}
35 |
38 | {% endwebpack %}
39 |
--------------------------------------------------------------------------------
/test/Component/Asset/Fixtures/template.html.twig:
--------------------------------------------------------------------------------
1 | {# Method one: webpack_asset. #}
2 |
3 |
4 |
5 |
6 | {# Method two: webpack js #}
7 | {% webpack js '@BarBundle/app2.js' '@BarBundle/app3.js' %}
8 |
9 | {% endwebpack %}
10 |
11 | {# and webpack css #}
12 | {% webpack css '@BarBundle/app4.js' %}
13 |
14 | {% endwebpack %}
15 |
16 | {# and webpack inline with no extension given. It must fallback to "js" by default. #}
17 | {% webpack inline %}
18 |
21 | {% endwebpack %}
22 |
23 | {# Specified an extension. Webpack will also parse the contents by the given type. #}
24 | {% webpack inline js %}
25 | console.log("HENK");
26 | {% endwebpack %}
27 |
28 | {# Parse the contents as it were a LESS file. #}
29 | {% webpack inline less %}
30 | @size: 42px;
31 | * { font-size: @size; }
32 | {% endwebpack %}
33 |
34 | {# Parse the contents as it were a CSS file. Also, feel free to use style tags (for IDE syntax highlighting)... #}
35 | {% webpack inline css %}
36 |
39 | {% endwebpack %}
40 |
--------------------------------------------------------------------------------
/src/Bundle/Resources/views/sections/output.html.twig:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | | Compile reason |
7 | {{ collector.get('tracker.reason') }} |
8 |
9 |
10 | | Analyze time |
11 |
12 | {{ collector.get('compiler.performance.prepare') }} ms (
13 | {{ collector.get('tracker.file_count') }} files,
14 | {{ collector.get('templates')|length }} templates,
15 | {{ collector.get('bundles')|length }} bundles )
16 | |
17 |
18 |
19 | | Compile time |
20 | {{ collector.get('compiler.performance.compiler') }} ms |
21 |
22 |
23 | | Total time |
24 | {{ collector.get('compiler.performance.total') }} ms |
25 |
26 |
27 |
28 |
{{ collector.get('compiler.last_output')|raw }}
29 |
30 |
31 |
--------------------------------------------------------------------------------
/test/AbstractTestCase.php:
--------------------------------------------------------------------------------
1 | = 40200) {
19 | return new TreeBuilder($config_root);
20 | }
21 |
22 | if (Kernel::VERSION_ID >= 30300 && Kernel::VERSION_ID < 40200) {
23 | return new TreeBuilder();
24 | }
25 |
26 | throw new \RuntimeException('This bundle can only be used by Symfony 3.3 and up.');
27 | }
28 |
29 | protected function retrieveRootNode(TreeBuilder $tree_builder, string $config_root): NodeDefinition
30 | {
31 | if (Kernel::VERSION_ID >= 40200) {
32 | return $tree_builder->getRootNode();
33 | }
34 |
35 | if (Kernel::VERSION_ID >= 30300 && Kernel::VERSION_ID < 40200) {
36 | return $tree_builder->root($config_root);
37 | }
38 |
39 | throw new \RuntimeException('This bundle can only be used by Symfony 3.3 and up.');
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/Component/Configuration/Loader/BabelLoader.php:
--------------------------------------------------------------------------------
1 | config = $config['loaders']['babel'] ?? ['enabled' => false];
23 | }
24 |
25 | /**
26 | * {@inheritdoc}
27 | */
28 | public static function applyConfiguration(NodeBuilder $node_builder): void
29 | {
30 | $node_builder
31 | ->arrayNode('babel')
32 | ->canBeDisabled()
33 | ->end();
34 | }
35 |
36 | /**
37 | * {@inheritdoc}
38 | */
39 | public function getCodeBlocks()
40 | {
41 | if (! $this->config['enabled']) {
42 | return [new CodeBlock()];
43 | }
44 |
45 | return [(new CodeBlock())->set(
46 | CodeBlock::LOADER,
47 | '{ test: /\.jsx$/, loader: \'babel-loader?cacheDirectory\' }'
48 | )];
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/Bundle/Resources/config/loaders.yml:
--------------------------------------------------------------------------------
1 | services:
2 | hostnet_webpack.loader.css:
3 | class: Hostnet\Component\Webpack\Configuration\Loader\CssLoader
4 | tags:
5 | - { name: "hostnet_webpack.config_extension" }
6 | hostnet_webpack.loader.url:
7 | class: Hostnet\Component\Webpack\Configuration\Loader\UrlLoader
8 | tags:
9 | - { name: "hostnet_webpack.config_extension" }
10 | hostnet_webpack.loader.less:
11 | class: Hostnet\Component\Webpack\Configuration\Loader\LessLoader
12 | tags:
13 | - { name: "hostnet_webpack.config_extension" }
14 | hostnet_webpack.loader.sass:
15 | class: Hostnet\Component\Webpack\Configuration\Loader\SassLoader
16 | tags:
17 | - { name: "hostnet_webpack.config_extension" }
18 | hostnet_webpack.loader.babel:
19 | class: Hostnet\Component\Webpack\Configuration\Loader\BabelLoader
20 | tags:
21 | - { name: "hostnet_webpack.config_extension" }
22 | hostnet_webpack.loader.typescript:
23 | class: Hostnet\Component\Webpack\Configuration\Loader\TypeScriptLoader
24 | tags:
25 | - { name: "hostnet_webpack.config_extension" }
26 | hostnet_webpack.loader.coffee:
27 | class: Hostnet\Component\Webpack\Configuration\Loader\CoffeeScriptLoader
28 | tags:
29 | - { name: "hostnet_webpack.config_extension" }
30 |
--------------------------------------------------------------------------------
/src/Component/Profiler/WebpackDataCollector.php:
--------------------------------------------------------------------------------
1 | profiler = $profiler;
24 | }
25 |
26 | /**
27 | * {@inheritdoc}
28 | */
29 | public function getName()
30 | {
31 | return Configuration::CONFIG_ROOT;
32 | }
33 |
34 | /**
35 | * {@inheritdoc}
36 | */
37 | public function collect(Request $request, Response $response, \Exception $exception = null)
38 | {
39 | }
40 |
41 | /**
42 | * {@inheritdoc}
43 | */
44 | public function reset()
45 | {
46 | }
47 |
48 | /**
49 | * @param string $id
50 | * @param mixed $default
51 | * @return null|string
52 | */
53 | public function get($id, $default = false): ?string
54 | {
55 | return $this->profiler->get($id, $default);
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/test/Bundle/DependencyInjection/ConfigurationTest.php:
--------------------------------------------------------------------------------
1 | getConfigTreeBuilder();
22 | $final = $tree->buildTree()->finalize([]);
23 |
24 | self::assertArrayHasKey('node', $final);
25 | self::assertArrayHasKey('compile_timeout', $final);
26 |
27 | self::assertArrayHasKey('binary', $final['node']);
28 | self::assertArrayHasKey('win32', $final['node']['binary']);
29 | self::assertArrayHasKey('win64', $final['node']['binary']);
30 | self::assertArrayHasKey('linux_x32', $final['node']['binary']);
31 | self::assertArrayHasKey('linux_x64', $final['node']['binary']);
32 | self::assertArrayHasKey('darwin', $final['node']['binary']);
33 | self::assertArrayHasKey('fallback', $final['node']['binary']);
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "hostnet/webpack-bundle",
3 | "type": "symfony-bundle",
4 | "description": "Integrates Webpack with Symfony",
5 | "license": "MIT",
6 | "abandoned": true,
7 | "require": {
8 | "php": "^7.1.0",
9 | "ext-json": "*",
10 | "monolog/monolog": "~1.25",
11 | "symfony/monolog-bundle": "^4.0.0||^3.1.0",
12 | "symfony/symfony": "^4.3.9||^3.4.36",
13 | "twig/twig": "^2.7.2"
14 | },
15 | "require-dev": {
16 | "hostnet/phpcs-tool": "^8.3",
17 | "phpunit/phpunit": "^7.5.9",
18 | "symfony/phpunit-bridge": "^3.3.2"
19 | },
20 | "conflict": {
21 | "phpdocumentor/type-resolver": "<0.2.1"
22 | },
23 | "autoload": {
24 | "psr-4": {
25 | "Hostnet\\Bundle\\WebpackBundle\\": "src/Bundle",
26 | "Hostnet\\Component\\Webpack\\": "src/Component"
27 | }
28 | },
29 | "autoload-dev": {
30 | "psr-4": {
31 | "Hostnet\\Fixture\\WebpackBundle\\": "test/Fixture",
32 | "Hostnet\\Bundle\\WebpackBundle\\": "test/Bundle",
33 | "Hostnet\\Component\\Webpack\\": "test/Component",
34 | "Hostnet\\Tests\\": "test"
35 | }
36 | },
37 | "archive": {
38 | "exclude": [
39 | "/test"
40 | ]
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/test/Component/Configuration/CodeBlockTest.php:
--------------------------------------------------------------------------------
1 | set(CodeBlock::HEADER, 'foo');
21 | self::assertTrue($block->has(CodeBlock::HEADER));
22 | self::assertEquals('foo', $block->get(CodeBlock::HEADER));
23 | }
24 |
25 | public function testInvalidChunk(): void
26 | {
27 | $this->expectException(\InvalidArgumentException::class);
28 |
29 | (new CodeBlock())->set('foobar', true);
30 | }
31 |
32 | public function testGetInvalid(): void
33 | {
34 | $this->expectException(\InvalidArgumentException::class);
35 |
36 | (new CodeBlock())->get(CodeBlock::HEADER);
37 | }
38 |
39 | public function testDuplicateChunk(): void
40 | {
41 | $block = new CodeBlock();
42 | $block->set(CodeBlock::HEADER, 'foo');
43 |
44 | $this->expectException(\InvalidArgumentException::class);
45 | $this->expectExceptionMessage('The chunk "header" is already in use.');
46 |
47 | $block->set(CodeBlock::HEADER, 'bar');
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/test/Component/Configuration/Plugin/DefinePluginTest.php:
--------------------------------------------------------------------------------
1 | createTreeBuilder('webpack');
25 | $node = $this->retrieveRootNode($tree, 'webpack')->children();
26 |
27 | DefinePlugin::applyConfiguration($node);
28 | $node->end();
29 |
30 | $config = $tree->buildTree()->finalize([]);
31 | }
32 |
33 | public function testGetCodeBlock(): void
34 | {
35 | $config = new DefinePlugin([
36 | 'plugins' => [
37 | 'constants' => [
38 | 'foo' => 'bar',
39 | ],
40 | ],
41 | ]);
42 |
43 | $config->add('bar', 'baz');
44 |
45 | self::assertEquals(
46 | 'new webpack.DefinePlugin({"foo":"bar","bar":"baz"})',
47 | $config->getCodeBlocks()[0]->get(CodeBlock::PLUGIN)
48 | );
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/test/Component/Configuration/Plugin/ProvidePluginTest.php:
--------------------------------------------------------------------------------
1 | createTreeBuilder('webpack');
25 | $node = $this->retrieveRootNode($tree, 'webpack')->children();
26 |
27 | ProvidePlugin::applyConfiguration($node);
28 | $node->end();
29 |
30 | $config = $tree->buildTree()->finalize([]);
31 | }
32 |
33 | public function testGetCodeBlock(): void
34 | {
35 | $config = new ProvidePlugin([
36 | 'plugins' => [
37 | 'provides' => [
38 | '$' => 'jquery',
39 | ],
40 | ],
41 | ]);
42 |
43 | $config->add('jQuery', 'jquery');
44 |
45 | self::assertEquals(
46 | 'new webpack.ProvidePlugin({"$":"jquery","jQuery":"jquery"})',
47 | $config->getCodeBlocks()[0]->get(CodeBlock::PLUGIN)
48 | );
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/test/Component/Configuration/Config/ResolveLoaderConfigTest.php:
--------------------------------------------------------------------------------
1 | createTreeBuilder('webpack');
22 | $node = $this->retrieveRootNode($tree, 'webpack')->children();
23 |
24 | ResolveLoaderConfig::applyConfiguration($node);
25 | $node->end();
26 |
27 | $config = $tree->buildTree()->finalize([]);
28 | self::assertArrayHasKey('resolve_loader', $config);
29 | }
30 |
31 | public function testGetCodeBlock(): void
32 | {
33 | $config = new ResolveLoaderConfig([
34 | 'node' => [
35 | 'node_modules_path' => '/foo/bar',
36 | ],
37 | 'resolve_loader' => [
38 | 'root' => ['/tmp'],
39 | ],
40 | ]);
41 |
42 | self::assertTrue($config->getCodeBlocks()[0]->has(CodeBlock::RESOLVE_LOADER));
43 | self::assertArrayHasKey('root', $config->getCodeBlocks()[0]->get(CodeBlock::RESOLVE_LOADER));
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/test/Component/Configuration/Loader/LessLoaderTest.php:
--------------------------------------------------------------------------------
1 | createTreeBuilder('webpack');
22 | $node = $this->retrieveRootNode($tree, 'webpack')->children();
23 |
24 | LessLoader::applyConfiguration($node);
25 | $node->end();
26 |
27 | $config = $tree->buildTree()->finalize([]);
28 |
29 | self::assertArrayHasKey('less', $config);
30 | self::assertArrayHasKey('enabled', $config['less']);
31 | }
32 |
33 | public function testGetCodeBlockDisabled(): void
34 | {
35 | $config = new LessLoader(['loaders' => ['less' => ['enabled' => false]]]);
36 | $block = $config->getCodeBlocks()[0];
37 |
38 | self::assertFalse($block->has(CodeBlock::LOADER));
39 | }
40 |
41 | public function testGetCodeBlock(): void
42 | {
43 | $config = new LessLoader(['loaders' => ['less' => ['enabled' => true]]]);
44 | $block = $config->getCodeBlocks()[0];
45 |
46 | self::assertTrue($block->has(CodeBlock::LOADER));
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/Component/Configuration/Loader/CoffeeScriptLoader.php:
--------------------------------------------------------------------------------
1 | config = $config;
23 | }
24 |
25 | /**
26 | * {@inheritdoc}
27 | */
28 | public static function applyConfiguration(NodeBuilder $node_builder): void
29 | {
30 | $node_builder
31 | ->arrayNode('coffee')
32 | ->canBeDisabled()
33 | ->addDefaultsIfNotSet()
34 | ->children()
35 | ->scalarNode('loader')->defaultValue('coffee')->end()
36 | ->end()
37 | ->end();
38 | }
39 |
40 | /**
41 | * {@inheritdoc}
42 | */
43 | public function getCodeBlocks()
44 | {
45 | $config = $this->config['loaders']['coffee'];
46 |
47 | if (! $config['enabled']) {
48 | return [new CodeBlock()];
49 | }
50 |
51 | return [(new CodeBlock())->set(
52 | CodeBlock::LOADER,
53 | sprintf("{ test: /\\.coffee/, loader: '%s' }", $config['loader'])
54 | )];
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/test/Component/Asset/DumperTest.php:
--------------------------------------------------------------------------------
1 | fixture_path = realpath(__DIR__ . '/../../Fixture');
31 | $this->dumper = new Dumper(
32 | $this->getMockBuilder(Filesystem::class)->getMock(),
33 | $this->getMockBuilder(LoggerInterface::class)->getMock(),
34 | [
35 | 'FooBundle' => $this->fixture_path . '/Bundle/FooBundle',
36 | 'BarBundle' => $this->fixture_path . '/Bundle/BarBundle',
37 | ],
38 | 'Resources/public',
39 | $this->fixture_path . '/dumper_output'
40 | );
41 |
42 | // Clean out the fixture dumper path before dumping resources.
43 | if (file_exists($this->fixture_path . '/dumper_output')) {
44 | (new Filesystem())->remove($this->fixture_path . '/dumper_output');
45 | }
46 | }
47 |
48 | /**
49 | * @doesNotPerformAssertions
50 | */
51 | public function testDumpDefaults(): void
52 | {
53 | $this->dumper->dump();
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/src/Component/Configuration/Loader/TypeScriptLoader.php:
--------------------------------------------------------------------------------
1 | config = $config;
23 | }
24 |
25 | /**
26 | * {@inheritdoc}
27 | */
28 | public static function applyConfiguration(NodeBuilder $node_builder): void
29 | {
30 | $node_builder
31 | ->arrayNode('typescript')
32 | ->canBeDisabled()
33 | ->addDefaultsIfNotSet()
34 | ->children()
35 | ->scalarNode('loader')->defaultValue('ts')->end()
36 | ->end()
37 | ->end();
38 | }
39 |
40 | /**
41 | * {@inheritdoc}
42 | */
43 | public function getCodeBlocks()
44 | {
45 | $config = $this->config['loaders']['typescript'];
46 |
47 | if (! $config['enabled']) {
48 | return [new CodeBlock()];
49 | }
50 |
51 | return [(new CodeBlock())->set(
52 | CodeBlock::LOADER,
53 | sprintf("{ test: /\\.ts/, loader: '%s' }", $config['loader'])
54 | )];
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/test/Fixture/Component/Configuration/ConfigGenerator.js:
--------------------------------------------------------------------------------
1 | /* Generated by hostnet/webpack-bundle. Do not modify. */
2 | var webpack = require('webpack');
3 |
4 | var a = require("b");
5 | var preLoader1 = require("pre-loader-1");
6 | var preLoader2 = require("pre-loader-2");
7 | var fn_extract_text_plugin_sass = require("extract-text-webpack-plugin");
8 | module.exports = {
9 |
10 | entry : {
11 | "a": "/path/to/a.js",
12 | "b": "/path/to/b.js"
13 | },
14 | output : {
15 | "a": "a",
16 | "b": "b",
17 | "c": "c",
18 | "path": "path/to/output"
19 | },
20 | resolve : {
21 | "root": {
22 | "a": "b",
23 | "b": "c"
24 | },
25 | "alias": {
26 | "a": "b",
27 | "b": "c",
28 | "c": "a"
29 | }
30 | },
31 | resolveLoader : {
32 | "root": "/path/to/node_modules"
33 | },
34 | plugins : [
35 | new webpack.DefinePlugin({"a":"b","b":"c"}),
36 | new webpack.DefinePlugin({"c":"d","d":"e"}),
37 | new fn_extract_text_plugin_sass("testfile", {allChunks: true})
38 | ],
39 | module : {
40 | preLoaders : [
41 | { test: /\.css$/, loader: preLoader1.execute("a", "b") },
42 | { test: /\.less$/, loader: preLoader2.execute("c", "d") }
43 | ],
44 | loaders : [
45 | { test: /\.css$/, loader: "style!some-loader" },
46 | { test: /\.scss$/, loader: fn_extract_text_plugin_sass.extract("css!sass") }
47 | ],
48 | postLoaders : [
49 | { test: /\.inl$/, loader: "style" }
50 | ]
51 | },
52 | sassLoader: {
53 | includePaths: [
54 | 'path1',
55 | 'path2'
56 | ]
57 | }
58 | };
59 |
--------------------------------------------------------------------------------
/test/Component/Configuration/Loader/TypeScriptLoaderTest.php:
--------------------------------------------------------------------------------
1 | createTreeBuilder('webpack');
22 | $node = $this->retrieveRootNode($tree, 'webpack')->children();
23 |
24 | TypeScriptLoader::applyConfiguration($node);
25 | $node->end();
26 |
27 | $config = $tree->buildTree()->finalize([]);
28 |
29 | self::assertArrayHasKey('typescript', $config);
30 | self::assertArrayHasKey('enabled', $config['typescript']);
31 | }
32 |
33 | public function testGetCodeBlockDisabled(): void
34 | {
35 | $config = new TypeScriptLoader(['loaders' => ['typescript' => ['enabled' => false, 'loader' => 'ts']]]);
36 | $block = $config->getCodeBlocks()[0];
37 |
38 | self::assertFalse($block->has(CodeBlock::LOADER));
39 | }
40 |
41 | public function testGetCodeBlock(): void
42 | {
43 | $config = new TypeScriptLoader(['loaders' => ['typescript' => ['enabled' => true, 'loader' => 'ts']]]);
44 | $block = $config->getCodeBlocks()[0];
45 |
46 | self::assertTrue($block->has(CodeBlock::LOADER));
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/test/Component/Configuration/Loader/CoffeeScriptLoaderTest.php:
--------------------------------------------------------------------------------
1 | createTreeBuilder('webpack');
22 | $node = $this->retrieveRootNode($tree, 'webpack')->children();
23 |
24 | CoffeeScriptLoader::applyConfiguration($node);
25 | $node->end();
26 |
27 | $config = $tree->buildTree()->finalize([]);
28 |
29 | self::assertArrayHasKey('coffee', $config);
30 | self::assertArrayHasKey('enabled', $config['coffee']);
31 | }
32 |
33 | public function testGetCodeBlockDisabled(): void
34 | {
35 | $config = new CoffeeScriptLoader(['loaders' => ['coffee' => ['enabled' => false, 'loader' => 'coffee']]]);
36 | $block = $config->getCodeBlocks()[0];
37 |
38 | self::assertFalse($block->has(CodeBlock::LOADER));
39 | }
40 |
41 | public function testGetCodeBlock(): void
42 | {
43 | $config = new CoffeeScriptLoader(['loaders' => ['coffee' => ['enabled' => true, 'loader' => 'coffee']]]);
44 | $block = $config->getCodeBlocks()[0];
45 |
46 | self::assertTrue($block->has(CodeBlock::LOADER));
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/Component/Configuration/Plugin/ProvidePlugin.php:
--------------------------------------------------------------------------------
1 | provides = $config['plugins']['provides'];
28 | }
29 |
30 | /**
31 | * @param string $key
32 | * @param mixed $value
33 | * @return ProvidePlugin
34 | */
35 | public function add($key, $value): ProvidePlugin
36 | {
37 | $this->provides[$key] = $value;
38 |
39 | return $this;
40 | }
41 |
42 | /**
43 | * {@inheritdoc}
44 | */
45 | public static function applyConfiguration(NodeBuilder $node_builder): void
46 | {
47 | $node_builder
48 | ->arrayNode('provides')
49 | ->useAttributeAsKey('name')
50 | ->prototype('scalar')->end()
51 | ->end();
52 | }
53 |
54 | /**
55 | * {@inheritdoc}
56 | */
57 | public function getCodeBlocks()
58 | {
59 | return [(new CodeBlock())
60 | ->set(CodeBlock::PLUGIN, sprintf('new %s(%s)', 'webpack.ProvidePlugin', json_encode($this->provides)))];
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/test/Component/Configuration/Loader/BabelLoaderTest.php:
--------------------------------------------------------------------------------
1 | createTreeBuilder('webpack');
22 | $node = $this->retrieveRootNode($tree, 'webpack')->children();
23 |
24 | BabelLoader::applyConfiguration($node);
25 | $node->end();
26 |
27 | $config = $tree->buildTree()->finalize([]);
28 |
29 | self::assertArrayHasKey('babel', $config);
30 | self::assertArrayHasKey('enabled', $config['babel']);
31 | }
32 |
33 | public function testGetCodeBlockDefault(): void
34 | {
35 | $config = new BabelLoader();
36 | $block = $config->getCodeBlocks()[0];
37 |
38 | self::assertFalse($block->has(CodeBlock::LOADER));
39 | }
40 |
41 | public function testGetCodeBlockDisabled(): void
42 | {
43 | $config = new BabelLoader(['loaders' => ['babel' => ['enabled' => false]]]);
44 | $block = $config->getCodeBlocks()[0];
45 |
46 | self::assertFalse($block->has(CodeBlock::LOADER));
47 | }
48 |
49 | public function testGetCodeBlock(): void
50 | {
51 | $config = new BabelLoader(['loaders' => ['babel' => ['enabled' => true]]]);
52 | $block = $config->getCodeBlocks()[0];
53 |
54 | self::assertTrue($block->has(CodeBlock::LOADER));
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/Component/Configuration/Config/ResolveLoaderConfig.php:
--------------------------------------------------------------------------------
1 | config = $config;
24 |
25 | // Apply node_modules path to resolveLoader.root
26 | if (! empty($config['node']['node_modules_path'])) {
27 | $this->config['resolve_loader']['root'][] = $config['node']['node_modules_path'];
28 | }
29 | }
30 |
31 | /**
32 | * {@inheritdoc}
33 | */
34 | public static function applyConfiguration(NodeBuilder $node_builder): void
35 | {
36 | $node_builder
37 | ->arrayNode('resolve_loader')
38 | ->addDefaultsIfNotSet()
39 | ->children()
40 | ->arrayNode('root')
41 | ->prototype('scalar')->end()
42 | ->end()
43 | ->end();
44 | }
45 |
46 | /**
47 | * {@inheritdoc}
48 | */
49 | public function getCodeBlocks()
50 | {
51 | // Convert keys to camelCase.
52 | $config = [];
53 | foreach ($this->config['resolve_loader'] as $key => $value) {
54 | $config[lcfirst(Container::camelize($key))] = $value;
55 | }
56 |
57 | return [(new CodeBlock())->set(CodeBlock::RESOLVE_LOADER, $config)];
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/src/Component/Configuration/Plugin/DefinePlugin.php:
--------------------------------------------------------------------------------
1 | config = $config;
33 | $this->constants = $config['plugins']['constants'];
34 | }
35 |
36 | /**
37 | * @param string $key
38 | * @param mixed $value
39 | * @return DefinePlugin
40 | */
41 | public function add($key, $value): DefinePlugin
42 | {
43 | $this->constants[$key] = $value;
44 |
45 | return $this;
46 | }
47 |
48 | /**
49 | * {@inheritdoc}
50 | */
51 | public static function applyConfiguration(NodeBuilder $node_builder): void
52 | {
53 | $node_builder
54 | ->arrayNode('constants')
55 | ->useAttributeAsKey('name')
56 | ->prototype('scalar')->end()
57 | ->end();
58 | }
59 |
60 | /**
61 | * {@inheritdoc}
62 | */
63 | public function getCodeBlocks()
64 | {
65 | return [(new CodeBlock())
66 | ->set(CodeBlock::PLUGIN, sprintf('new %s(%s)', 'webpack.DefinePlugin', json_encode($this->constants)))];
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/test/Component/Asset/CompilerTest.php:
--------------------------------------------------------------------------------
1 | profiler = $this->getMockBuilder(Profiler::class)->disableOriginalConstructor()->getMock();
29 | $this->tracker = $this->getMockBuilder(Tracker::class)->disableOriginalConstructor()->getMock();
30 | $this->twig_parser = $this->getMockBuilder(TwigParser::class)->disableOriginalConstructor()->getMock();
31 | $this->generator = $this->getMockBuilder(ConfigGenerator::class)->disableOriginalConstructor()->getMock();
32 | $this->process = $this->getMockBuilder(Process::class)->disableOriginalConstructor()->getMock();
33 | $this->cache_path = realpath(__DIR__ . '/../../Fixture/cache');
34 | }
35 |
36 | public function testCompile(): void
37 | {
38 | $this->tracker->expects($this->once())->method('getTemplates')->willReturn(['foobar']);
39 | $this->tracker->expects($this->once())->method('getAliases')->willReturn(['@AppBundle' => 'foobar']);
40 | $this->twig_parser->expects($this->once())->method('findSplitPoints')->willReturn(['@AppBundle/app.js' => 'a']);
41 |
42 | (new Compiler(
43 | $this->profiler,
44 | $this->tracker,
45 | $this->twig_parser,
46 | $this->generator,
47 | $this->process,
48 | $this->cache_path,
49 | []
50 | ))->compile();
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/Component/Asset/TemplateReference.php:
--------------------------------------------------------------------------------
1 | parameters = [
21 | 'bundle' => $bundle,
22 | 'controller' => $controller,
23 | 'name' => $name,
24 | 'format' => $format,
25 | 'engine' => $engine,
26 | ];
27 | }
28 |
29 | /**
30 | * Returns the path to the template
31 | * - as a path when the template is not part of a bundle
32 | * - as a resource when the template is part of a bundle.
33 | *
34 | * @return string A path to the template or a resource
35 | */
36 | public function getPath(): string
37 | {
38 | $controller = str_replace('\\', '/', $this->get('controller'));
39 |
40 | $name = $this->get('name');
41 | $format = $this->get('format');
42 | $engine = $this->get('engine');
43 |
44 | $path = (empty($controller) ? '' : $controller . '/') . $name . '.' . $format . '.' . $engine;
45 |
46 | return empty($this->parameters['bundle'])
47 | ? 'views/' . $path
48 | : '@' . $this->get('bundle') . '/Resources/views/' . $path;
49 | }
50 |
51 | /**
52 | * {@inheritdoc}
53 | */
54 | public function getLogicalName(): string
55 | {
56 | return sprintf(
57 | '%s:%s:%s.%s.%s',
58 | $this->parameters['bundle'],
59 | $this->parameters['controller'],
60 | $this->parameters['name'],
61 | $this->parameters['format'],
62 | $this->parameters['engine']
63 | );
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/test/Functional/AssetTest.php:
--------------------------------------------------------------------------------
1 | compiled = static::$kernel->getContainer()->getParameter('kernel.root_dir') . '/cache/compiled/';
26 |
27 | if (!file_exists($this->compiled)) {
28 | mkdir($this->compiled);
29 | }
30 | }
31 |
32 | public function testPublicAsset(): void
33 | {
34 | static::bootKernel();
35 |
36 | /** @var TwigExtension $twig_ext */
37 | $twig_ext = static::$kernel->getContainer()->get(TwigExtension::class);
38 |
39 | self::assertEquals('/bundles/henk.png', $twig_ext->webpackPublic('henk.png'));
40 | }
41 |
42 | public function testCompiledAsset(): void
43 | {
44 | /** @var TwigExtension $twig_ext */
45 | $container = static::$kernel->getContainer();
46 | $twig_ext = $container->get(TwigExtension::class);
47 |
48 | self::assertEquals([
49 | 'js' => false,
50 | 'css' => false,
51 | ], $twig_ext->webpackAsset('henk'));
52 |
53 | touch($this->compiled . 'app.henk.js');
54 | touch($this->compiled . 'app.henk.css');
55 |
56 | $resources = $twig_ext->webpackAsset('@App/henk.js');
57 | self::assertStringContainsString('app.henk.js?', (string) $resources['js']);
58 | self::assertStringContainsString('app.henk.css?', (string) $resources['css']);
59 | }
60 |
61 | protected function tearDown(): void
62 | {
63 | shell_exec("rm -rf {$this->compiled}");
64 |
65 | parent::tearDown();
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/test/Bundle/Twig/TwigExtensionTest.php:
--------------------------------------------------------------------------------
1 | prophesize(LoaderInterface::class)->reveal();
20 | $extension = new TwigExtension($loader, __DIR__, '/', '/bundles', '/shared.js', '/shared.css');
21 |
22 | self::assertEquals('webpack', $extension->getName());
23 | self::assertEquals(['js' => false, 'css' => false], $extension->webpackAsset('@AppBundle/app.js'));
24 | self::assertEquals('/shared.js?0', $extension->webpackCommonJs());
25 | self::assertEquals('/shared.css?0', $extension->webpackCommonCss());
26 | }
27 |
28 | /**
29 | * @dataProvider assetProvider
30 | */
31 | public function testAssets($expected, $asset, $web_dir, $dump_path, $public_path): void
32 | {
33 | $loader = $this->prophesize(LoaderInterface::class)->reveal();
34 | $extension = new TwigExtension($loader, $web_dir, $public_path, $dump_path, '', '');
35 | self::assertEquals($expected, $extension->webpackPublic($asset));
36 | }
37 |
38 | public function assetProvider()
39 | {
40 | return [
41 | ['/bundles/img.png', 'img.png', __DIR__ . '/web/', '/bundles/', '/'],
42 | ['/img.png', 'img.png', __DIR__ . 'web/', '/', '/'],
43 | ['/bundles/app/img.png', '@App/img.png', __DIR__ . '/../web/', '/bundles/', '/'],
44 | ['/some/dir/app/img.png', '@App/img.png', __DIR__ . './web/', '/some/dir/', '/'],
45 | ['/bundles/some/img.png', '@SomeBundle/img.png', __DIR__ . '/../web/', '/bundles/', '/'],
46 | ['/bundles/some/test/img.png', '@SomeBundle/test/img.png', './web/', '/bundles/', '/'],
47 | ['/something/else/some/test/img.png', '@SomeBundle/test/img.png', '/web/', '/something/else/', '/'],
48 | ];
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/test/Component/Configuration/Loader/SassLoaderTest.php:
--------------------------------------------------------------------------------
1 | createTreeBuilder('webpack');
20 | $node = $this->retrieveRootNode($tree, 'webpack')->children();
21 |
22 | SassLoader::applyConfiguration($node);
23 | $node->end();
24 |
25 | $config = $tree->buildTree()->finalize([]);
26 |
27 | self::assertArrayHasKey('sass', $config);
28 | self::assertArrayHasKey('enabled', $config['sass']);
29 | }
30 |
31 | public function testGetCodeBlockDisabled(): void
32 | {
33 | $config = new SassLoader(['loaders' => ['sass' => ['enabled' => false]]]);
34 | $block = $config->getCodeBlocks()[0];
35 |
36 | self::assertFalse($block->has(CodeBlock::LOADER));
37 | }
38 |
39 | public function testGetCodeBlock(): void
40 | {
41 | $config = new SassLoader(['loaders' => ['sass' => ['enabled' => true]]]);
42 | $block = $config->getCodeBlocks()[0];
43 |
44 | self::assertTrue($block->has(CodeBlock::LOADER));
45 | }
46 |
47 | public function testGetCodeBlockWithIncludePaths(): void
48 | {
49 | $config = new SassLoader(
50 | [
51 | 'loaders' => [
52 | 'sass' => [
53 | 'enabled' => true,
54 | 'include_paths' => ['path1', 'path2'],
55 | 'filename' => 'testfile',
56 | 'all_chunks' => true,
57 | ],
58 | ],
59 | ]
60 | );
61 | $block = $config->getCodeBlocks()[0];
62 |
63 | self::assertTrue($block->has(CodeBlock::ROOT));
64 | self::assertTrue($block->has(CodeBlock::HEADER));
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/src/Component/Asset/CacheGuard.php:
--------------------------------------------------------------------------------
1 | compiler = $compiler;
55 | $this->dumper = $dumper;
56 | $this->tracker = $tracker;
57 | $this->logger = $logger;
58 | }
59 |
60 | /**
61 | * Rebuild the cache, check to see if it's still valid and rebuild if it's outdated.
62 | */
63 | public function rebuild(): void
64 | {
65 | if ($this->tracker->isOutdated()) {
66 | $this->logger->info('[Webpack 1/2]: Compiling assets.');
67 | $output = $this->compiler->compile();
68 | $this->logger->debug($output);
69 |
70 | $this->logger->info('[Webpack 2/2]: Dumping assets.');
71 | $this->dumper->dump();
72 | } else {
73 | $this->logger->info('[Webpack]: Cache still up-to-date.');
74 | }
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/test/Component/Configuration/Loader/UrlLoaderTest.php:
--------------------------------------------------------------------------------
1 | createTreeBuilder('webpack');
22 | $node = $this->retrieveRootNode($tree, 'webpack')->children();
23 |
24 | UrlLoader::applyConfiguration($node);
25 | $node->end();
26 |
27 | $config = $tree->buildTree()->finalize([]);
28 | self::assertArrayHasKey('url', $config);
29 | self::assertArrayHasKey('font_extensions', $config['url']);
30 | self::assertArrayHasKey('image_extensions', $config['url']);
31 | self::assertArrayHasKey('enabled', $config['url']);
32 | }
33 |
34 | public function testGetCodeBlockDisabled(): void
35 | {
36 | $config = new UrlLoader(['loaders' => ['url' => ['enabled' => false]]]);
37 | $block = $config->getCodeBlocks()[0];
38 |
39 | self::assertFalse($block->has(CodeBlock::LOADER));
40 | }
41 |
42 | public function testGetFontExtensionCodeBlock(): void
43 | {
44 | $config = new UrlLoader([
45 | 'loaders' => ['url' => ['enabled' => true, 'font_extensions' => 'svg,woff', 'limit' => 100]],
46 | ]);
47 | $block = $config->getCodeBlocks()[0];
48 |
49 | self::assertCount(2, $config->getCodeBlocks());
50 | self::assertTrue($block->has(CodeBlock::LOADER));
51 | }
52 |
53 | public function testGetImageExtensionCodeBlock(): void
54 | {
55 | $config = new UrlLoader([
56 | 'loaders' => ['url' => ['enabled' => true, 'image_extensions' => 'png', 'limit' => 100]],
57 | ]);
58 | $block = $config->getCodeBlocks()[0];
59 |
60 | self::assertCount(1, $config->getCodeBlocks());
61 | self::assertTrue($block->has(CodeBlock::LOADER));
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/test/Component/Asset/CacheGuardTest.php:
--------------------------------------------------------------------------------
1 | prophesize(Compiler::class);
23 | $compiler->compile()->willReturn('some debug output');
24 |
25 | $dumper = $this->prophesize(Dumper::class);
26 | $dumper->dump()->shouldBeCalled();
27 |
28 | //Cache is outdated.
29 | $tracker = $this->prophesize(Tracker::class);
30 | $tracker->isOutdated()->willReturn(true);
31 |
32 | //What do we expect for logging
33 | $logger = $this->prophesize(LoggerInterface::class);
34 | $logger->info('[Webpack 1/2]: Compiling assets.')->shouldBeCalled();
35 | $logger->info('[Webpack 2/2]: Dumping assets.')->shouldBeCalled();
36 | $logger->debug('some debug output')->shouldBeCalled();
37 |
38 | $cache_guard = new CacheGuard($compiler->reveal(), $dumper->reveal(), $tracker->reveal(), $logger->reveal());
39 | $cache_guard->rebuild();
40 | }
41 |
42 | /**
43 | * Simple test for the case the cache is not outdated.
44 | */
45 | public function testCacheUpToDate(): void
46 | {
47 | $compiler = $this->prophesize(Compiler::class);
48 | $compiler->compile()->willReturn('some debug output')->shouldNotBeCalled();
49 |
50 | $dumper = $this->prophesize(Dumper::class);
51 | $dumper->dump()->shouldNotBeCalled();
52 |
53 | //Cache is not outdated
54 | $tracker = $this->prophesize(Tracker::class);
55 | $tracker->isOutdated()->willReturn(false);
56 |
57 | //What do we expect for logging
58 | $logger = $this->prophesize(LoggerInterface::class);
59 | $logger->info('[Webpack]: Cache still up-to-date.')->shouldBeCalled();
60 |
61 | $cache_guard = new CacheGuard($compiler->reveal(), $dumper->reveal(), $tracker->reveal(), $logger->reveal());
62 | $cache_guard->rebuild();
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/test/Functional/CompileTest.php:
--------------------------------------------------------------------------------
1 | 'dev', 'debug' => false]);
19 | $collector = static::$kernel->getContainer()->get(WebpackDataCollector::class);
20 |
21 | self::assertInstanceOf(WebpackDataCollector::class, $collector);
22 | }
23 |
24 | public function testMissingCollector(): void
25 | {
26 | $this->expectException(ServiceNotFoundException::class);
27 |
28 | static::bootKernel(['environment' => 'test', 'debug' => false]);
29 | static::$kernel->getContainer()->get(WebpackDataCollector::class);
30 | }
31 |
32 | public function testTrackedTemplates(): void
33 | {
34 | static::bootKernel();
35 |
36 | /** @var Tracker $tracker */
37 | $tracker = static::$kernel->getContainer()->get(Tracker::class);
38 |
39 | $templates = array_map([$this, 'relative'], $tracker->getTemplates());
40 |
41 | self::assertContains('/test/Fixture/Bundle/FooBundle/Resources/views/foo.html.twig', $templates);
42 | self::assertContains('/test/Fixture/Resources/views/template.html.twig', $templates);
43 |
44 | $aliases = $tracker->getAliases();
45 |
46 | self::assertEquals('/test/Fixture/Bundle/BarBundle/Resources/assets', $this->relative($aliases['@BarBundle']));
47 | }
48 |
49 | private function relative($path)
50 | {
51 | return str_replace(
52 | str_replace(
53 | '/test/Fixture',
54 | '',
55 | $this->normalize(
56 | static::$kernel->getContainer()->getParameter('kernel.root_dir')
57 | )
58 | ),
59 | '',
60 | $this->normalize($path)
61 | );
62 | }
63 |
64 | private function normalize($path)
65 | {
66 | return str_replace('\\', '/', $path);
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/src/Component/Asset/TrackedFiles.php:
--------------------------------------------------------------------------------
1 | in() call.
32 | $files = array_filter($paths, 'is_file');
33 |
34 | //Filter out the directories to be used for searching using the Filter class.
35 | $dirs = array_filter($paths, 'is_dir');
36 |
37 | $finder = new Finder();
38 |
39 | //Add the given 'stand-alone-files'
40 | $finder->append($files);
41 |
42 | //Add the Directores recursively
43 | $finder = $finder->in($dirs);
44 |
45 | //Filter out non readable files
46 | $finder = $finder->filter(
47 | function (SplFileInfo $finder) {
48 | return $finder->isReadable();
49 | }
50 | );
51 |
52 | //Loop through all the files and save the latest modification time.
53 | foreach ($finder->files() as $file) {
54 | /**@var $file \SplFileInfo */
55 | if ($this->modification_time < $file->getMTime()) {
56 | $this->modification_time = $file->getMTime();
57 | }
58 | }
59 | }
60 |
61 | /**
62 | * Is one of the Tracked files in this set changed later than the other set.
63 | *
64 | * @param TrackedFiles $other the other set of files to compare to.
65 | * @return bool true if this set if this set is modified after (later) the other set.
66 | */
67 | public function modifiedAfter(TrackedFiles $other): bool
68 | {
69 | return $this->modification_time > $other->modification_time;
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/test/Fixture/config/config.yml:
--------------------------------------------------------------------------------
1 | parameters:
2 | show_warnings_in_uglify: 'true'
3 |
4 | framework:
5 | secret: test
6 | router:
7 | resource: "%kernel.root_dir%/config/routing.yml"
8 |
9 | twig:
10 | strict_variables: false
11 |
12 | webpack:
13 | node:
14 | node_modules_path: "%kernel.root_dir%/node_modules"
15 | output:
16 | path: "%kernel.root_dir%/cache/compiled/"
17 | dump_path: "%kernel.root_dir%/cache/bundles/"
18 | public_path: /compiled/
19 | common_id: shared
20 | loaders:
21 | sass:
22 | filename: '[name].css'
23 | all_chunks: true
24 | less:
25 | filename: '[name].css'
26 | all_chunks: true
27 | css:
28 | filename: '[name].css'
29 | all_chunks: true
30 | url: ~
31 | babel: ~
32 | typescript: ~
33 | coffee: ~
34 | resolve:
35 | alias:
36 | app: "%kernel.root_dir%/Resources/assets"
37 | fake: "%kernel.root_dir%/fake"
38 |
39 | plugins:
40 | constants:
41 | ENVIRONMENT: functional_tests
42 | provides:
43 | '$': 'jquery'
44 | 'jQuery': 'jquery'
45 | uglifyjs:
46 | compress:
47 | sequences: ~
48 | properties: ~
49 | dead_code: ~
50 | drop_debugger: ~
51 | unsafe: ~
52 | conditionals: ~
53 | comparisons: ~
54 | evaluate: ~
55 | booleans: ~
56 | loops: ~
57 | unused: ~
58 | hoist_funs: ~
59 | hoist_vars: ~
60 | if_return: ~
61 | join_vars: ~
62 | cascade: ~
63 | side_effects: ~
64 | warnings: ~
65 | global_defs:
66 | DEBUG: false
67 | test: /\.js($|\?)/i
68 | mangle_except: ~
69 | minimize: ~
70 | monolog:
71 | handlers:
72 | console:
73 | type: console
74 | verbosity_levels:
75 | VERBOSITY_NORMAL: notice
76 |
77 | services:
78 | Hostnet\Fixture\WebpackBundle\Bundle\BarBundle\Loader\MockLoader:
79 | public: true
80 | tags:
81 | - { name: "hostnet_webpack.config_extension" }
82 |
--------------------------------------------------------------------------------
/test/Component/Configuration/Loader/CssLoaderTest.php:
--------------------------------------------------------------------------------
1 | createTreeBuilder('webpack');
22 | $node = $this->retrieveRootNode($tree, 'webpack')->children();
23 |
24 | CssLoader::applyConfiguration($node);
25 | $node->end();
26 |
27 | $config = $tree->buildTree()->finalize([]);
28 |
29 | self::assertArrayHasKey('css', $config);
30 | self::assertArrayHasKey('enabled', $config['css']);
31 | self::assertArrayHasKey('all_chunks', $config['css']);
32 | self::assertArrayHasKey('filename', $config['css']);
33 | }
34 |
35 | public function testGetCodeBlockDisabled(): void
36 | {
37 | $config = new CssLoader(['loaders' => ['css' => ['enabled' => false]]]);
38 |
39 | self::assertFalse($config->getCodeBlocks()[0]->has(CodeBlock::LOADER));
40 | }
41 |
42 | public function testGetCodeBlockEnabledDefaults(): void
43 | {
44 | $configs = (new CssLoader(['loaders' => ['css' => ['enabled' => true]]]))->getCodeBlocks();
45 |
46 | self::assertTrue($configs[0]->has(CodeBlock::LOADER));
47 | self::assertFalse($configs[0]->has(CodeBlock::HEADER));
48 | self::assertFalse($configs[0]->has(CodeBlock::PLUGIN));
49 | }
50 |
51 | public function testGetCodeBlockEnabledCommonsChunk(): void
52 | {
53 | $configs = (new CssLoader([
54 | 'output' => ['common_id' => 'foobar'],
55 | 'loaders' => ['css' => ['enabled' => true, 'filename' => 'blaat', 'all_chunks' => true]],
56 | ]))->getCodeBlocks();
57 |
58 | self::assertTrue($configs[0]->has(CodeBlock::LOADER));
59 | self::assertTrue($configs[0]->has(CodeBlock::HEADER));
60 | self::assertTrue($configs[0]->has(CodeBlock::PLUGIN));
61 | self::assertTrue($configs[1]->has(CodeBlock::PLUGIN));
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/test/Component/Configuration/Config/ResolveConfigTest.php:
--------------------------------------------------------------------------------
1 | createTreeBuilder('webpack');
22 | $node = $this->retrieveRootNode($tree, 'webpack')->children();
23 |
24 | ResolveConfig::applyConfiguration($node);
25 | $node->end();
26 |
27 | $config = $tree->buildTree()->finalize([]);
28 |
29 | self::assertArrayHasKey('resolve', $config);
30 | self::assertArrayHasKey('root', $config['resolve']);
31 | self::assertArrayHasKey('alias', $config['resolve']);
32 | self::assertArrayHasKey('modules_directories', $config['resolve']);
33 | self::assertArrayHasKey('fallback', $config['resolve']);
34 | self::assertArrayHasKey('extensions', $config['resolve']);
35 | }
36 |
37 | public function testGetCodeBlock(): void
38 | {
39 | $config = new ResolveConfig([
40 | 'node' => [
41 | 'node_modules_path' => '/path/to/node_modules',
42 | ],
43 | 'resolve' => [
44 | 'root' => ['foobar.js'],
45 | 'alias' => ['@Common' => 'common'],
46 | 'modules_directories' => [],
47 | ],
48 | ]);
49 | $config->addAlias('/foo/bar', '@FooBar');
50 |
51 | self::assertTrue($config->getCodeBlocks()[0]->has(CodeBlock::RESOLVE));
52 | self::assertArrayHasKey('root', $config->getCodeBlocks()[0]->get(CodeBlock::RESOLVE));
53 | self::assertArrayHasKey('alias', $config->getCodeBlocks()[0]->get(CodeBlock::RESOLVE));
54 | self::assertArrayHasKey('modulesDirectories', $config->getCodeBlocks()[0]->get(CodeBlock::RESOLVE));
55 | self::assertArrayHasKey('@FooBar', $config->getCodeBlocks()[0]->get(CodeBlock::RESOLVE)['alias']);
56 | self::assertArrayHasKey('@Common', $config->getCodeBlocks()[0]->get(CodeBlock::RESOLVE)['alias']);
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/test/Component/Configuration/Config/OutputConfigTest.php:
--------------------------------------------------------------------------------
1 | createTreeBuilder('webpack');
22 | $node = $this->retrieveRootNode($tree, 'webpack')->children();
23 |
24 | OutputConfig::applyConfiguration($node);
25 | $node->end();
26 |
27 | $config = $tree->buildTree()->finalize([]);
28 |
29 | self::assertArrayHasKey('output', $config);
30 | self::assertArrayHasKey('path', $config['output']);
31 | self::assertArrayHasKey('filename', $config['output']);
32 | self::assertArrayHasKey('common_id', $config['output']);
33 | self::assertArrayHasKey('chunk_filename', $config['output']);
34 | self::assertArrayHasKey('source_map_filename', $config['output']);
35 | self::assertArrayHasKey('devtool_module_filename_template', $config['output']);
36 | self::assertArrayHasKey('devtool_fallback_module_filename_template', $config['output']);
37 | self::assertArrayHasKey('devtool_line_to_line', $config['output']);
38 | self::assertArrayHasKey('hot_update_chunk_filename', $config['output']);
39 | self::assertArrayHasKey('hot_update_main_filename', $config['output']);
40 | self::assertArrayHasKey('public_path', $config['output']);
41 | self::assertArrayHasKey('jsonp_function', $config['output']);
42 | self::assertArrayHasKey('hot_update_function', $config['output']);
43 | self::assertArrayHasKey('path_info', $config['output']);
44 | }
45 |
46 | public function testGetCodeBlock(): void
47 | {
48 | $config = new OutputConfig([
49 | 'output' => [
50 | 'filename' => 'foobar.js',
51 | 'common_id' => 'common',
52 | ],
53 | ]);
54 |
55 | self::assertTrue($config->getCodeBlocks()[0]->has(CodeBlock::OUTPUT));
56 | self::assertArrayHasKey('filename', $config->getCodeBlocks()[0]->get(CodeBlock::OUTPUT));
57 | self::assertArrayHasKey('commonId', $config->getCodeBlocks()[0]->get(CodeBlock::OUTPUT));
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/src/Component/Configuration/Loader/UrlLoader.php:
--------------------------------------------------------------------------------
1 | config = $config['loaders']['url'];
23 | }
24 |
25 | /**
26 | * {@inheritdoc}
27 | */
28 | public static function applyConfiguration(NodeBuilder $node_builder): void
29 | {
30 | $node_builder
31 | ->arrayNode('url')
32 | ->canBeDisabled()
33 | ->addDefaultsIfNotSet()
34 | ->children()
35 | ->scalarNode('limit')->defaultValue(1000)->end()
36 | ->scalarNode('font_extensions')->defaultValue('svg,woff,woff2,eot,ttf')->end()
37 | ->scalarNode('image_extensions')->defaultValue('png,gif,jpg,jpeg')->end()
38 | ->end()
39 | ->end();
40 | }
41 |
42 | /**
43 | * {@inheritdoc}
44 | */
45 | public function getCodeBlocks()
46 | {
47 | if (! $this->config['enabled']) {
48 | return [new CodeBlock()];
49 | }
50 |
51 | $limit = $this->config['limit'];
52 | $image_code_block = [];
53 | $font_code_block = [];
54 |
55 | if (isset($this->config['font_extensions'])) {
56 | $font_extensions = explode(',', $this->config['font_extensions']);
57 |
58 | foreach ($font_extensions as $font) {
59 | $font_code_block[] = (new CodeBlock())->set(CodeBlock::LOADER, [sprintf(
60 | '{ test: /\.%s(\?v=\d+\.\d+\.\d+)?$/, loader: \'url-loader?limit=%d&name=[name]-[hash].[ext]\' }',
61 | $font,
62 | $limit
63 | )]);
64 | }
65 | }
66 |
67 | if (isset($this->config['image_extensions'])) {
68 | $image_extensions = str_replace([' ', ','], ['', '|'], $this->config['image_extensions']);
69 |
70 | $image_code_block = [(new CodeBlock())->set(CodeBlock::LOADER, [sprintf(
71 | '{ test: /\.(%s)$/, loader: \'url-loader?limit=%d&name=[name]-[hash].[ext]\' }',
72 | $image_extensions,
73 | $limit
74 | )])];
75 | }
76 |
77 | return array_merge(
78 | $image_code_block,
79 | $font_code_block
80 | );
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/src/Component/Configuration/Loader/LessLoader.php:
--------------------------------------------------------------------------------
1 | config = $config;
23 | }
24 |
25 | /**
26 | * {@inheritdoc}
27 | */
28 | public static function applyConfiguration(NodeBuilder $node_builder): void
29 | {
30 | $node_builder
31 | ->arrayNode('less')
32 | ->canBeDisabled()
33 | ->addDefaultsIfNotSet()
34 | ->children()
35 | ->booleanNode('all_chunks')->defaultTrue()->end()
36 | ->scalarNode('filename')->defaultNull()->end()
37 | ->end()
38 | ->end();
39 | }
40 |
41 | /**
42 | * {@inheritdoc}
43 | */
44 | public function getCodeBlocks()
45 | {
46 | $config = $this->config['loaders']['less'];
47 |
48 | if (! $config['enabled']) {
49 | return [new CodeBlock()];
50 | }
51 |
52 | if (empty($config['filename'])) {
53 | // If the filename is not set, apply inline style tags.
54 | return [(new CodeBlock())->set(CodeBlock::LOADER, '{ test: /\.less$/, loader: \'style!css!less\' }')];
55 | }
56 |
57 | // If a filename is set, apply the ExtractTextPlugin
58 | $fn = 'fn_extract_text_plugin_less';
59 | $code_blocks = [(new CodeBlock())
60 | ->set(CodeBlock::HEADER, 'var ' . $fn . ' = require("extract-text-webpack-plugin");')
61 | ->set(CodeBlock::LOADER, '{ test: /\.less$/, loader: ' . $fn . '.extract("css!less") }')
62 | ->set(CodeBlock::PLUGIN, 'new ' . $fn . '("' . $config['filename'] . '", {' . (
63 | $config['all_chunks'] ? 'allChunks: true' : ''
64 | ) . '})'),
65 | ];
66 |
67 | // If a common_filename is set, apply the CommonsChunkPlugin.
68 | if (! empty($this->config['output']['common_id'])) {
69 | $code_blocks[] = (new CodeBlock())
70 | ->set(CodeBlock::PLUGIN, sprintf(
71 | 'new %s({name: \'%s\', filename: \'%s\'})',
72 | 'webpack.optimize.CommonsChunkPlugin',
73 | $this->config['output']['common_id'],
74 | $this->config['output']['common_id'] . '.js'
75 | ));
76 | }
77 |
78 | return $code_blocks;
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/src/Component/Configuration/Loader/CssLoader.php:
--------------------------------------------------------------------------------
1 | config = $config;
23 | }
24 |
25 | /**
26 | * {@inheritdoc}
27 | */
28 | public static function applyConfiguration(NodeBuilder $node_builder): void
29 | {
30 | $node_builder
31 | ->arrayNode('css')
32 | ->canBeDisabled()
33 | ->addDefaultsIfNotSet()
34 | ->children()
35 | ->booleanNode('all_chunks')->defaultTrue()->end()
36 | ->scalarNode('filename')->defaultNull()->end()
37 | ->end()
38 | ->end();
39 | }
40 |
41 | /**
42 | * {@inheritdoc}
43 | */
44 | public function getCodeBlocks()
45 | {
46 | $config = $this->config['loaders']['css'];
47 |
48 | if (! $config['enabled']) {
49 | return [new CodeBlock()];
50 | }
51 |
52 | if (empty($config['filename'])) {
53 | // If the filename is not set, apply inline style tags.
54 | return [(new CodeBlock())->set(
55 | CodeBlock::LOADER,
56 | '{ test: /\.css$/, loader: \'style-loader!css-loader\' }'
57 | )];
58 | }
59 |
60 | // If a filename is set, apply the ExtractTextPlugin
61 | $fn = 'fn_extract_text_plugin_css';
62 | $code_blocks = [(new CodeBlock())
63 | ->set(CodeBlock::HEADER, 'var ' . $fn . ' = require("extract-text-webpack-plugin");')
64 | ->set(CodeBlock::LOADER, '{ test: /\.css$/, loader: ' . $fn . '.extract("css-loader") }')
65 | ->set(CodeBlock::PLUGIN, 'new ' . $fn . '("' . $config['filename'] . '", {' . (
66 | $config['all_chunks'] ? 'allChunks: true' : ''
67 | ) . '})'),
68 | ];
69 |
70 | // If a common_filename is set, apply the CommonsChunkPlugin.
71 | if (! empty($this->config['output']['common_id'])) {
72 | $code_blocks[] = (new CodeBlock())
73 | ->set(CodeBlock::PLUGIN, sprintf(
74 | 'new %s({name: \'%s\', filename: \'%s\'})',
75 | 'webpack.optimize.CommonsChunkPlugin',
76 | $this->config['output']['common_id'],
77 | $this->config['output']['common_id'] . '.js'
78 | ));
79 | }
80 |
81 | return $code_blocks;
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/src/Component/Configuration/Config/ResolveConfig.php:
--------------------------------------------------------------------------------
1 | config = $config;
24 |
25 | // Apply node_modules path to resolve.root
26 | if (! empty($config['node']['node_modules_path'])) {
27 | $this->config['resolve']['root'][] = $config['node']['node_modules_path'];
28 | }
29 | }
30 |
31 | /**
32 | * @param string $alias
33 | * @param string $path
34 | * @return ResolveConfig
35 | */
36 | public function addAlias($path, $alias = null): ResolveConfig
37 | {
38 | $this->config['resolve']['root'][] = $path;
39 | if ($alias !== null) {
40 | $this->config['resolve']['alias'][$alias] = $path;
41 | }
42 |
43 | return $this;
44 | }
45 |
46 | /**
47 | * {@inheritdoc}
48 | */
49 | public static function applyConfiguration(NodeBuilder $node_builder): void
50 | {
51 | $node_builder
52 | ->arrayNode('resolve')
53 | ->addDefaultsIfNotSet()
54 | ->children()
55 | ->arrayNode('root')
56 | ->requiresAtLeastOneElement()
57 | ->addDefaultChildrenIfNoneSet(0)
58 | ->prototype('scalar')->defaultValue('%kernel.root_dir%/Resources')->end()
59 | ->end()
60 | ->arrayNode('alias')
61 | ->useAttributeAsKey('name')
62 | ->prototype('scalar')->end()
63 | ->end()
64 | ->arrayNode('modules_directories')
65 | ->prototype('scalar')->end()
66 | ->end()
67 | ->arrayNode('fallback')
68 | ->prototype('scalar')->end()
69 | ->end()
70 | ->arrayNode('extensions')
71 | ->defaultValue(['', '.webpack.js', '.web.js', '.js'])
72 | ->prototype('scalar')->end()
73 | ->end()
74 | ->end()
75 | ->end();
76 | }
77 |
78 | /**
79 | * {@inheritdoc}
80 | */
81 | public function getCodeBlocks()
82 | {
83 | // Convert keys to camelCase.
84 | $config = [];
85 | foreach ($this->config['resolve'] as $key => $value) {
86 | $config[lcfirst(Container::camelize($key))] = $value;
87 | }
88 |
89 | return [(new CodeBlock())->set(CodeBlock::RESOLVE, $config)];
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/src/Component/Asset/Dumper.php:
--------------------------------------------------------------------------------
1 | / whenever a resource has been changed.
15 | */
16 | class Dumper
17 | {
18 | private $fs;
19 | private $logger;
20 | private $bundle_paths;
21 | private $public_res_path;
22 | private $output_dir;
23 |
24 | public function __construct(
25 | Filesystem $fs,
26 | LoggerInterface $logger,
27 | array $bundle_paths,
28 | string $public_res_path,
29 | string $output_dir
30 | ) {
31 | $this->fs = $fs;
32 | $this->logger = $logger;
33 | $this->bundle_paths = $bundle_paths;
34 | $this->public_res_path = $public_res_path;
35 | $this->output_dir = $output_dir;
36 | }
37 |
38 | /**
39 | * Iterates through resources and dump all modified resources to the bundle directory in the public dir.
40 | */
41 | public function dump(): void
42 | {
43 | foreach ($this->bundle_paths as $name => $path) {
44 | if (file_exists($path . DIRECTORY_SEPARATOR . $this->public_res_path)) {
45 | $this->dumpBundle($name, $path . DIRECTORY_SEPARATOR . $this->public_res_path);
46 | }
47 | }
48 | }
49 |
50 | /**
51 | * @param string $name
52 | * @param string $path
53 | */
54 | private function dumpBundle($name, $path): void
55 | {
56 | $target_dir = $this->normalize($this->getTargetDir($name));
57 | $path = $this->normalize($path);
58 |
59 | $this->logger->info(sprintf('Dumping public assets for "%s" to "%s".', $name, $target_dir));
60 |
61 | // Always copy on windows.
62 | if (stripos(PHP_OS, 'WIN') === 0) {
63 | $this->fs->mirror($path, $target_dir, null, [
64 | 'override' => true,
65 | 'copy_on_windows' => true,
66 | 'delete' => true,
67 | ]);
68 |
69 | return;
70 | }
71 |
72 | // Create a symlink for non-windows platforms.
73 | try {
74 | $this->fs->symlink($path, $target_dir);
75 | } catch (IOException $e) {
76 | $this->fs->mirror($path, $target_dir);
77 | }
78 | }
79 |
80 | /**
81 | * @param string $name
82 | * @return string
83 | */
84 | private function getTargetDir($name): string
85 | {
86 | if (substr($name, \strlen($name) - 6) === 'Bundle') {
87 | $name = substr($name, 0, -6);
88 | }
89 |
90 | return $this->output_dir . DIRECTORY_SEPARATOR . strtolower($name);
91 | }
92 |
93 | /**
94 | * Makes sure the path is always the OS directory separator.
95 | *
96 | * @param string $path
97 | * @return mixed
98 | */
99 | private function normalize($path)
100 | {
101 | return str_replace(['\\', '/'], DIRECTORY_SEPARATOR, $path);
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/test/Component/Asset/TwigParserTest.php:
--------------------------------------------------------------------------------
1 | tracker = $this->getMockBuilder(Tracker::class)->disableOriginalConstructor()->getMock();
37 | $this->twig = new Environment(new ArrayLoader([]));
38 | $this->cache_dir = sys_get_temp_dir();
39 | }
40 |
41 | public function testParseValid(): void
42 | {
43 | // Call count expectations:
44 | // 1: webpack_asset.js
45 | // 2: webpack_asset.css
46 | // 3: {% webpack js %}
47 | // 4: {% webpack js %}
48 | // 5: {% webpack css %}
49 | // 6: {% webpack inline %}
50 | // 7: {% webpack inline %}
51 | // 8: {% webpack inline less %}
52 | // 9: {% webpack inline css %}
53 | $this->tracker->expects($this->exactly(9))->method('resolveResourcePath')->willReturn('foobar');
54 |
55 | $parser = new TwigParser($this->tracker, $this->twig, $this->cache_dir);
56 | $file = __DIR__ . '/Fixtures/template.html.twig';
57 | $points = ($parser->findSplitPoints($file));
58 |
59 | self::assertCount(8, $points);
60 | self::assertArrayHasKey('@BarBundle/app.js', $points);
61 | self::assertArrayHasKey('@BarBundle/app2.js', $points);
62 | self::assertArrayHasKey('@BarBundle/app3.js', $points);
63 | self::assertArrayHasKey('@BarBundle/app4.js', $points);
64 | self::assertArrayHasKey('cache/' . md5($file . 0) . '.js', $points);
65 | self::assertArrayHasKey('cache/' . md5($file . 1) . '.js', $points);
66 | self::assertArrayHasKey('cache/' . md5($file . 2) . '.less', $points);
67 | self::assertArrayHasKey('cache/' . md5($file . 3) . '.css', $points);
68 |
69 | self::assertContains('foobar', $points);
70 | }
71 |
72 | public function testParseError(): void
73 | {
74 | $this->tracker->expects($this->never())->method('resolveResourcePath');
75 | $parser = new TwigParser($this->tracker, $this->twig, $this->cache_dir);
76 |
77 | $this->expectException(\RuntimeException::class);
78 | $this->expectExceptionMessage('template_parse_error.html.twig at line 3. Expected punctuation "(", got name.');
79 |
80 | $parser->findSplitPoints(__DIR__ . '/Fixtures/template_parse_error.html.twig');
81 | }
82 |
83 | public function testResolveError(): void
84 | {
85 | $this->tracker->expects($this->once())->method('resolveResourcePath')->willReturn(false);
86 | $parser = new TwigParser($this->tracker, $this->twig, $this->cache_dir);
87 |
88 | $this->expectException(\RuntimeException::class);
89 | $this->expectExceptionMessage('at line 3 could not be resolved.');
90 |
91 | $parser->findSplitPoints(__DIR__ . '/Fixtures/template.html.twig');
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/src/Component/Configuration/Config/OutputConfig.php:
--------------------------------------------------------------------------------
1 | config = $config;
24 | }
25 |
26 | /**
27 | * {@inheritdoc}
28 | */
29 | public static function applyConfiguration(NodeBuilder $node_builder): void
30 | {
31 | $node_builder
32 | ->arrayNode('output')
33 | ->validate()
34 | ->ifTrue(function ($c) {
35 | return !preg_match(sprintf(
36 | '~(?.*)%s$~',
37 | rtrim($c['public_path'], '\\/')
38 | ), rtrim($c['path'], '\\/'));
39 | })
40 | ->thenInvalid('webpack.output.public_path must be equal to the end of the webpack.output.path.')
41 | ->end()
42 | ->addDefaultsIfNotSet()
43 | ->children()
44 | ->scalarNode('path')->defaultValue('%kernel.root_dir%/../web/compiled/')->end()
45 | ->scalarNode('dump_path')->defaultValue('%kernel.root_dir%/../web/bundles/')->end()
46 | ->scalarNode('public_path')->defaultValue('/compiled/')->end()
47 | ->scalarNode('filename')->defaultValue('[name].js')->end()
48 | ->scalarNode('common_id')->defaultValue('common')->end()
49 | ->scalarNode('chunk_filename')->defaultValue('[name].[hash].chunk.js')->end()
50 | ->scalarNode('source_map_filename')->defaultValue('[file].sourcemap.js')->end()
51 | ->scalarNode('devtool_module_filename_template')->defaultValue('webpack:///[resource-path]')->end()
52 | ->scalarNode('devtool_fallback_module_filename_template')
53 | ->defaultValue('webpack:///[resourcePath]?[hash]')
54 | ->end()
55 | ->booleanNode('devtool_line_to_line')->defaultFalse()->end()
56 | ->scalarNode('hot_update_chunk_filename')->defaultValue('[id].[hash].hot-update.js')->end()
57 | ->scalarNode('hot_update_main_filename')->defaultValue('[hash].hot-update.json')->end()
58 | ->scalarNode('jsonp_function')->defaultValue('webpackJsonp')->end()
59 | ->scalarNode('hot_update_function')->defaultValue('webpackHotUpdate')->end()
60 | ->booleanNode('path_info')->defaultFalse()->end()
61 | ->end()
62 | ->end();
63 | }
64 |
65 | /**
66 | * {@inheritdoc}
67 | */
68 | public function getCodeBlocks()
69 | {
70 | // Convert keys to camelCase.
71 | $config = [];
72 | foreach ($this->config['output'] as $key => $value) {
73 | $config[lcfirst(Container::camelize($key))] = $value;
74 | }
75 |
76 | return [(new CodeBlock())->set(CodeBlock::OUTPUT, $config)];
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/src/Component/Asset/TemplateFinder.php:
--------------------------------------------------------------------------------
1 | kernel = $kernel;
24 | $this->root_dir = $root_dir;
25 | }
26 |
27 | /**
28 | * @return TemplateReferenceInterface[]
29 | */
30 | public function findAllTemplates(): array
31 | {
32 | if (null !== $this->templates) {
33 | return $this->templates;
34 | }
35 |
36 | $templates = [];
37 |
38 | foreach ($this->kernel->getBundles() as $bundle) {
39 | $templates = array_merge($templates, $this->findTemplatesInBundle($bundle));
40 | }
41 |
42 | $templates = array_merge($templates, $this->findTemplatesInFolder($this->root_dir . '/views'));
43 |
44 | return $this->templates = $templates;
45 | }
46 |
47 | /**
48 | * @param string $directory
49 | *
50 | * @return TemplateReferenceInterface[]
51 | */
52 | private function findTemplatesInFolder(string $directory): array
53 | {
54 | $templates = [];
55 |
56 | if (false === is_dir($directory)) {
57 | return $templates;
58 | }
59 |
60 | /** @var SplFileInfo $file */
61 | foreach ((new Finder())->files()->followLinks()->in($directory) as $file) {
62 | $template = $this->parse($file->getRelativePathname());
63 | if (false !== $template) {
64 | $templates[] = $template;
65 | }
66 | }
67 |
68 | return $templates;
69 | }
70 |
71 | /**
72 | * @param BundleInterface $bundle
73 | *
74 | * @return TemplateReferenceInterface[]
75 | */
76 | private function findTemplatesInBundle(BundleInterface $bundle): array
77 | {
78 | $name = $bundle->getName();
79 | $templates = array_unique(array_merge(
80 | $this->findTemplatesInFolder($bundle->getPath() . '/Resources/views'),
81 | $this->findTemplatesInFolder($this->root_dir . '/' . $name . '/views')
82 | ));
83 |
84 | /** @var TemplateReferenceInterface $template */
85 | foreach ($templates as $i => $template) {
86 | $templates[$i] = $template->set('bundle', $name);
87 | }
88 |
89 | return $templates;
90 | }
91 |
92 | /**
93 | * @param string $file_name
94 | *
95 | * @return TemplateReference|false
96 | */
97 | private function parse(string $file_name)
98 | {
99 | $parts = explode('/', str_replace('\\', '/', $file_name));
100 |
101 | $elements = explode('.', array_pop($parts));
102 | if (3 > \count($elements)) {
103 | return false;
104 | }
105 |
106 | $engine = array_pop($elements);
107 | $format = array_pop($elements);
108 |
109 | return new TemplateReference('', implode('/', $parts), implode('.', $elements), $format, $engine);
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/test/Component/Configuration/ConfigGeneratorTest.php:
--------------------------------------------------------------------------------
1 | addBlock((new CodeBlock())->set(CodeBlock::HEADER, 'var a = require("b");'))
25 | ->addBlock((new CodeBlock())->set(CodeBlock::ENTRY, ['a' => '/path/to/a.js']))
26 | ->addBlock((new CodeBlock())->set(CodeBlock::ENTRY, ['b' => '/path/to/b.js']))
27 | ->addBlock((new CodeBlock())->set(CodeBlock::OUTPUT, ['a' => 'a']))
28 | ->addBlock((new CodeBlock())->set(CodeBlock::OUTPUT, ['b' => 'b', 'c' => 'c']))
29 | ->addBlock((new CodeBlock())->set(CodeBlock::RESOLVE, ['root' => ['a' => 'b']]))
30 | ->addBlock((new CodeBlock())->set(CodeBlock::RESOLVE, ['root' => ['b' => 'c']]))
31 | ->addBlock((new CodeBlock())->set(CodeBlock::RESOLVE, ['alias' => ['a' => 'b', 'b' => 'c']]))
32 | ->addBlock((new CodeBlock())->set(CodeBlock::RESOLVE, ['alias' => ['c' => 'a']]))
33 | ->addBlock((new CodeBlock())->set(CodeBlock::RESOLVE_LOADER, ['root' => '/path/to/node_modules']));
34 |
35 | // Add loaders...
36 | $config->addBlock((new CodeBlock())->set(CodeBlock::LOADER, '{ test: /\.css$/, loader: "style!some-loader" }'));
37 | $config->addBlock((new CodeBlock())->set(CodeBlock::POST_LOADER, '{ test: /\.inl$/, loader: "style" }'));
38 | $config->addBlock(
39 | (new CodeBlock())
40 | ->set(CodeBlock::HEADER, 'var preLoader1 = require("pre-loader-1");')
41 | ->set(CodeBlock::PRE_LOADER, '{ test: /\.css$/, loader: preLoader1.execute("a", "b") }')
42 | );
43 | $config->addBlock(
44 | (new CodeBlock())
45 | ->set(CodeBlock::HEADER, 'var preLoader2 = require("pre-loader-2");')
46 | ->set(CodeBlock::PRE_LOADER, '{ test: /\.less$/, loader: preLoader2.execute("c", "d") }')
47 | );
48 |
49 | // And some plugins
50 | $config->addBlock(
51 | (new DefinePlugin(['plugins' => ['constants' => ['a' => 'b']]]))->add('b', 'c')->getCodeBlocks()[0]
52 | );
53 | $config->addBlock(
54 | (new DefinePlugin(['plugins' => ['constants' => ['c' => 'd']]]))->add('d', 'e')->getCodeBlocks()[0]
55 | );
56 |
57 | // Add extension
58 | $config->addExtension(new OutputConfig(['output' => ['path' => 'path/to/output']]));
59 | $config->addExtension(new SassLoader([
60 | 'loaders' => [
61 | 'sass' => [
62 | 'enabled' => true,
63 | 'include_paths' => ['path1', 'path2'],
64 | 'filename' => 'testfile',
65 | 'all_chunks' => true]],
66 | ]));
67 |
68 | $fixture_file = __DIR__ . '/../../Fixture/Component/Configuration/ConfigGenerator.js';
69 | // file_put_contents($fixture_file, $config->getConfiguration());
70 | $fixture = file_get_contents($fixture_file);
71 |
72 | self::assertEquals(str_replace("\r\n", "\n", $fixture), str_replace("\r\n", "\n", $config->getConfiguration()));
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/src/Component/Configuration/CodeBlock.php:
--------------------------------------------------------------------------------
1 | >
17 | *
18 | * module.exports = {
19 | * entry : {
20 | * // Generated by webpack-bundle
21 | * },
22 | * resolve : {
23 | * <>
24 | * },
25 | * plugins : [
26 | * <>
27 | * ],
28 | * module : {
29 | * preLoaders : [
30 | * <>
31 | * ],
32 | * loaders : [
33 | * <>
34 | * ],
35 | * post_loaders : [
36 | * <>
37 | * ],
38 | * },
39 | * output : {
40 | * << output >>
41 | * }
42 | * << root >>
43 | * };
44 | */
45 | class CodeBlock
46 | {
47 | public const HEADER = 'header';
48 | public const ENTRY = 'entry';
49 | public const RESOLVE = 'resolve';
50 | public const RESOLVE_LOADER = 'resolve_loader';
51 | public const PLUGIN = 'plugin';
52 | public const PRE_LOADER = 'pre_loader';
53 | public const LOADER = 'loader';
54 | public const POST_LOADER = 'post_loader';
55 | public const OUTPUT = 'output';
56 | public const ROOT = 'root';
57 |
58 | // Available types to allow easy validation
59 | private $types = [
60 | 'entry',
61 | 'header',
62 | 'resolve',
63 | 'resolve_loader',
64 | 'plugin',
65 | 'pre_loader',
66 | 'loader',
67 | 'post_loader',
68 | 'output',
69 | 'root',
70 | ];
71 |
72 | // Chunks collection
73 | private $chunks = [];
74 |
75 | /**
76 | * @param string $chunk
77 | * @param mixed $code
78 | * @return CodeBlock
79 | */
80 | public function set($chunk, $code): CodeBlock
81 | {
82 | if (false === \in_array($chunk, $this->types, false)) {
83 | throw new \InvalidArgumentException(sprintf(
84 | 'Invalid insertion point "%s". Available points are: %s.',
85 | $chunk,
86 | implode(', ', $this->types)
87 | ));
88 | }
89 |
90 | if (isset($this->chunks[$chunk])) {
91 | throw new \InvalidArgumentException(sprintf('The chunk "%s" is already in use.', $chunk));
92 | }
93 |
94 | $this->chunks[$chunk] = $code;
95 |
96 | return $this;
97 | }
98 |
99 | /**
100 | * Returns the code associated with the given chunk.
101 | *
102 | * @param string $chunk
103 | * @return mixed
104 | */
105 | public function get($chunk)
106 | {
107 | if (false === isset($this->chunks[$chunk])) {
108 | throw new \InvalidArgumentException(sprintf('This code block does not have a chunk for "%s".', $chunk));
109 | }
110 |
111 | return $this->chunks[$chunk];
112 | }
113 |
114 | /**
115 | * Returns true if this code-block has the given chunk defined.
116 | *
117 | * @param string $chunk
118 | * @return bool
119 | */
120 | public function has($chunk): bool
121 | {
122 | return isset($this->chunks[$chunk]);
123 | }
124 | }
125 |
--------------------------------------------------------------------------------
/src/Component/Configuration/Loader/SassLoader.php:
--------------------------------------------------------------------------------
1 | config = $config;
23 | }
24 |
25 | /**
26 | * {@inheritdoc}
27 | */
28 | public static function applyConfiguration(NodeBuilder $node_builder): void
29 | {
30 | $node_builder
31 | ->arrayNode('sass')
32 | ->canBeDisabled()
33 | ->addDefaultsIfNotSet()
34 | ->children()
35 | ->booleanNode('all_chunks')->defaultTrue()->end()
36 | ->scalarNode('filename')->defaultNull()->end()
37 | ->arrayNode('include_paths')
38 | ->defaultValue([])
39 | ->prototype('scalar')->end()
40 | ->end()
41 | ->end();
42 | }
43 |
44 | /**
45 | * {@inheritdoc}
46 | */
47 | public function getCodeBlocks()
48 | {
49 | $config = $this->config['loaders']['sass'];
50 |
51 | $block = new CodeBlock();
52 |
53 | if (! $config['enabled']) {
54 | return [$block];
55 | }
56 |
57 | $tab1 = str_repeat(' ', 4); // "one tab" spacing for 'pretty' output
58 | $tab2 = str_repeat(' ', 8); // "two tabs" spacing for 'pretty' output
59 |
60 | if (!empty($config['include_paths'])) {
61 | $block->set(CodeBlock::ROOT, 'sassLoader: {' . PHP_EOL .
62 | $tab1 . 'includePaths: [' . PHP_EOL .
63 | $tab2 . '\'' . implode('\',' . PHP_EOL . $tab2 . '\'', $config['include_paths']) . '\'' . PHP_EOL .
64 | $tab1 . ']' . PHP_EOL .
65 | '}');
66 | }
67 |
68 | if (empty($config['filename'])) {
69 | // If the filename is not set, apply inline style tags.
70 | $block->set(CodeBlock::LOADER, '{ test: /\.scss$/, loader: \'style!css!sass\' }');
71 | return [$block];
72 | }
73 |
74 | // If a filename is set, apply the ExtractTextPlugin
75 | $fn = 'fn_extract_text_plugin_sass';
76 | $code_blocks = [(new CodeBlock())
77 | ->set(CodeBlock::HEADER, 'var ' . $fn . ' = require("extract-text-webpack-plugin");')
78 | ->set(CodeBlock::LOADER, '{ test: /\.scss$/, loader: ' . $fn . '.extract("css!sass") }')
79 | ->set(CodeBlock::PLUGIN, 'new ' . $fn . '("' . $config['filename'] . '", {' . (
80 | $config['all_chunks'] ? 'allChunks: true' : ''
81 | ) . '})'),
82 | ];
83 |
84 | if (!empty($config['include_paths'])) {
85 | $code_blocks[0]->set(CodeBlock::ROOT, 'sassLoader: {' . PHP_EOL .
86 | $tab1 . 'includePaths: [' . PHP_EOL .
87 | $tab2 . '\'' . implode('\',' . PHP_EOL . $tab2 . '\'', $config['include_paths']) . '\'' . PHP_EOL .
88 | $tab1 . ']' . PHP_EOL .
89 | '}');
90 | }
91 |
92 | // If a common_filename is set, apply the CommonsChunkPlugin.
93 | if (! empty($this->config['output']['common_id'])) {
94 | $code_blocks[] = (new CodeBlock())
95 | ->set(CodeBlock::PLUGIN, sprintf(
96 | 'new %s({name: \'%s\', filename: \'%s\'})',
97 | 'webpack.optimize.CommonsChunkPlugin',
98 | $this->config['output']['common_id'],
99 | $this->config['output']['common_id'] . '.js'
100 | ));
101 | }
102 |
103 | return $code_blocks;
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/src/Bundle/Resources/config/webpack.yml:
--------------------------------------------------------------------------------
1 | imports:
2 | - { resource: config.yml }
3 | - { resource: loaders.yml }
4 | - { resource: plugins.yml }
5 |
6 | services:
7 | Hostnet\Bundle\WebpackBundle\CacheWarmer\WebpackCompileCacheWarmer:
8 | arguments:
9 | - '@Hostnet\Component\Webpack\Asset\CacheGuard'
10 | tags:
11 | - { name: "kernel.cache_warmer", priority: 10 }
12 |
13 | Hostnet\Bundle\WebpackBundle\Command\CompileCommand:
14 | arguments:
15 | - '@Hostnet\Component\Webpack\Asset\CacheGuard'
16 | tags:
17 | - { name: "console.command" }
18 |
19 | Hostnet\Component\Webpack\Asset\CacheGuard:
20 | arguments:
21 | - '@Hostnet\Component\Webpack\Asset\Compiler'
22 | - '@Hostnet\Component\Webpack\Asset\Dumper'
23 | - '@Hostnet\Component\Webpack\Asset\Tracker'
24 | - '@logger'
25 |
26 | Hostnet\Component\Webpack\Asset\TemplateFinder:
27 | arguments:
28 | - '@kernel'
29 | - '%kernel.root_dir%/Resources'
30 |
31 | Hostnet\Component\Webpack\Asset\Tracker:
32 | public: true
33 | arguments:
34 | - '@Hostnet\Component\Webpack\Profiler\Profiler'
35 | - '@Hostnet\Component\Webpack\Asset\TemplateFinder'
36 | - "%kernel.root_dir%"
37 | - "" # asset_path
38 | - "" # output dir
39 | - [] # bundles
40 |
41 | Hostnet\Component\Webpack\Asset\Dumper:
42 | public: true
43 | arguments:
44 | - '@filesystem'
45 | - '@logger'
46 | - [] # bundles
47 | - "" # "public" dir
48 | - "" # output dir
49 |
50 | Symfony\Component\Process\Process:
51 | public: true
52 | arguments:
53 | - "" # Node binary
54 | - "" # Cache directory
55 |
56 | Hostnet\Component\Webpack\Asset\TwigParser:
57 | arguments:
58 | - '@Hostnet\Component\Webpack\Asset\Tracker'
59 | - '@twig'
60 | - "%kernel.cache_dir%"
61 |
62 | Hostnet\Component\Webpack\Asset\Compiler:
63 | public: true
64 | arguments:
65 | - '@Hostnet\Component\Webpack\Profiler\Profiler'
66 | - '@Hostnet\Component\Webpack\Asset\Tracker'
67 | - '@Hostnet\Component\Webpack\Asset\TwigParser'
68 | - '@Hostnet\Component\Webpack\Configuration\ConfigGenerator'
69 | - '@Symfony\Component\Process\Process'
70 | - "%kernel.cache_dir%"
71 | - [] # bundles
72 | - '@?debug.stopwatch'
73 |
74 | Hostnet\Bundle\WebpackBundle\Twig\TwigExtension:
75 | public: true
76 | arguments:
77 | - '@twig.loader'
78 | - "" # web_path
79 | - "" # public_path
80 | - "" # dump_path
81 | - "" # common_js
82 | - "" # common_css
83 | tags:
84 | - { name: "twig.extension" }
85 |
86 | Hostnet\Component\Webpack\Configuration\ConfigGenerator:
87 | public: true
88 |
89 | Hostnet\Component\Webpack\Profiler\Profiler: ~
90 |
91 | # BC aliases
92 | hostnet_webpack.bridge.twig_extension: '@Hostnet\Bundle\WebpackBundle\Twig\TwigExtension'
93 | hostnet_webpack.bridge.profiler: '@Hostnet\Component\Webpack\Profiler\Profiler'
94 | hostnet_webpack.bridge.asset_compiler: '@Hostnet\Component\Webpack\Asset\Compiler'
95 | hostnet_webpack.bridge.asset_twig_parser: '@Hostnet\Component\Webpack\Asset\TwigParser'
96 | hostnet_webpack.bridge.compiler_process: '@Symfony\Component\Process\Process'
97 | hostnet_webpack.bridge.asset_dumper: '@Hostnet\Component\Webpack\Asset\Dumper'
98 | hostnet_webpack.bridge.cache_warmer: '@Hostnet\Bundle\WebpackBundle\CacheWarmer\WebpackCompileCacheWarmer'
99 | hostnet_webpack.bridge.generate_config_command: '@Hostnet\Bundle\WebpackBundle\Command\CompileCommand'
100 | hostnet_webpack.bridge.config_generator: '@Hostnet\Component\Webpack\Configuration\ConfigGenerator'
101 | hostnet_webpack.bridge.asset_cacheguard: '@Hostnet\Component\Webpack\Asset\CacheGuard'
102 | hostnet_webpack.bridge.asset_tracker: '@Hostnet\Component\Webpack\Asset\Tracker'
103 |
--------------------------------------------------------------------------------
/src/Component/Configuration/ConfigGenerator.php:
--------------------------------------------------------------------------------
1 | getCodeBlocks() as $block) {
23 | $this->addBlock($block);
24 | }
25 |
26 | return $this;
27 | }
28 |
29 | /**
30 | * @param CodeBlock $block
31 | * @return ConfigGenerator
32 | */
33 | public function addBlock(CodeBlock $block): ConfigGenerator
34 | {
35 | $this->blocks[] = $block;
36 |
37 | return $this;
38 | }
39 |
40 | public function getConfiguration()
41 | {
42 | $tab1 = str_repeat(' ', 4); // "one tab" spacing for 'pretty' output
43 | $tab2 = str_repeat(' ', 8); // "two tabs" spacing for 'pretty' output
44 | $code = [];
45 | $code[] = '/* Generated by hostnet/webpack-bundle. Do not modify. */';
46 | $code[] = 'var webpack = require(\'webpack\');';
47 | $code[] = '';
48 |
49 | // Apply headers
50 | $code[] = $this->getChunks(CodeBlock::HEADER);
51 | $code[] = 'module.exports = {';
52 |
53 | // Apply entries (split points)
54 | $code[] = '';
55 | $code[] = 'entry : ' . json_encode(
56 | $this->getChunks(CodeBlock::ENTRY, false, false),
57 | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT
58 | ) . ',';
59 | $code[] = 'output : ' . json_encode(
60 | $this->getChunks(CodeBlock::OUTPUT, false, false),
61 | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT
62 | ) . ',';
63 | $code[] = 'resolve : ' . json_encode(
64 | $this->getChunks(CodeBlock::RESOLVE, false, false),
65 | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT
66 | ) . ',';
67 | $code[] = 'resolveLoader : ' . json_encode(
68 | $this->getChunks(CodeBlock::RESOLVE_LOADER, false, false),
69 | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT
70 | ) . ',';
71 | $code[] = 'plugins : [';
72 | $code[] = $tab1 . $this->getChunks(CodeBlock::PLUGIN, ', ' . PHP_EOL . $tab1, ',' . PHP_EOL . $tab1);
73 | $code[] = '],';
74 | $code[] = 'module : {';
75 | $code[] = $tab1 . 'preLoaders : [';
76 | $code[] = $tab2 . $this->getChunks(CodeBlock::PRE_LOADER, ',' . PHP_EOL . $tab2, ',' . PHP_EOL . $tab2);
77 | $code[] = $tab1 . '],';
78 | $code[] = $tab1 . 'loaders : [';
79 | $code[] = $tab2 . $this->getChunks(CodeBlock::LOADER, ',' . PHP_EOL . $tab2, ',' . PHP_EOL . $tab2);
80 | $code[] = $tab1 . '],';
81 | $code[] = $tab1 . 'postLoaders : [';
82 | $code[] = $tab2 . $this->getChunks(CodeBlock::POST_LOADER, ',' . PHP_EOL . $tab2, ',' . PHP_EOL . $tab2);
83 | $code[] = $tab1 . ']';
84 |
85 | if (!empty($this->getChunks(CodeBlock::ROOT))) {
86 | $code[] = '},';
87 | $code[] = $this->getChunks(CodeBlock::ROOT);
88 | } else {
89 | $code[] = '}';
90 | }
91 |
92 | $code[] = '};';
93 | $code[] = '';
94 |
95 | return implode(PHP_EOL, $code);
96 | }
97 |
98 | /**
99 | * @param string $type
100 | * @param string|bool $delimiter
101 | * @param string|bool $internal_delimiter
102 | *
103 | * @return string|array
104 | */
105 | private function getChunks($type, $delimiter = PHP_EOL, $internal_delimiter = PHP_EOL)
106 | {
107 | $chunks = [];
108 | foreach ($this->blocks as $block) {
109 | if ($block->has($type)) {
110 | if ($internal_delimiter === false) {
111 | $chunks = array_merge_recursive($chunks, $block->get($type));
112 | } else {
113 | $chunks[] = implode($internal_delimiter, (array) $block->get($type));
114 | }
115 | }
116 | }
117 |
118 | return $delimiter === false ? $chunks : implode($delimiter, $chunks);
119 | }
120 | }
121 |
--------------------------------------------------------------------------------
/src/Bundle/DependencyInjection/WebpackExtension.php:
--------------------------------------------------------------------------------
1 | load('webpack.yml');
29 |
30 | // Enable the request listener if we're running in dev.
31 | if ($container->getParameter('kernel.environment') === 'dev') {
32 | $loader->load('dev.yml');
33 | }
34 |
35 | // Retrieve all configuration entities
36 | $config_extension_ids = array_keys($container->findTaggedServiceIds('hostnet_webpack.config_extension'));
37 | $config_definitions = [];
38 |
39 | foreach ($config_extension_ids as $id) {
40 | $config_definitions[$id] = $container->getDefinition($id);
41 | }
42 |
43 | $config = $this->processConfiguration($this->getConfiguration($config, $container), $config);
44 | $container->addResource(new FileResource((new \ReflectionClass(Configuration::class))->getFileName()));
45 |
46 | // Select the correct node binary for the platform we're currently running on.
47 | $config['node']['binary'] = $config['node']['binary'][$this->getPlatformKey()];
48 | $config['node']['node_modules_path'] = ! empty($config['node']['node_modules_path'])
49 | ? $config['node']['node_modules_path']
50 | : getenv('NODE_PATH');
51 |
52 | // Parse application config into the config generator
53 | foreach ($config_definitions as $id => $definition) {
54 | /** @var Definition $definition */
55 | $definition->addArgument($config);
56 | }
57 |
58 | // Pass the configuration to a container parameter for the CompilerPass and profiler to read.
59 | $container->setParameter('hostnet_webpack_config', $config);
60 | }
61 |
62 | /**
63 | * Returns the platform key to take the node binary configuration from.
64 | *
65 | * A little caveat here: This will not give you the actual architecture of the machine, but rather if PHP is running
66 | * in 32 or 64-bit mode. Unfortunately there is no way figuring this out without invoking external system processes.
67 | *
68 | * @codeCoverageIgnore The outcome and coverage of this method solely depends on which platform PHP is running on.
69 | * @return string
70 | */
71 | private function getPlatformKey(): string
72 | {
73 | $platform = PHP_OS;
74 |
75 | if (0 === stripos($platform, 'WIN')) {
76 | return PHP_INT_SIZE === 8 ? 'win64' : 'win32';
77 | }
78 | if (0 === stripos($platform, 'LINUX')) {
79 | return PHP_INT_SIZE === 8 ? 'linux_x64' : 'linux_x32';
80 | }
81 | if (0 === stripos($platform, 'DARWIN')) {
82 | return 'darwin';
83 | }
84 |
85 | return 'fallback';
86 | }
87 |
88 | /**
89 | * {@inheritdoc}
90 | */
91 | public function getConfiguration(array $config, ContainerBuilder $container)
92 | {
93 | $bundles = $container->getParameter('kernel.bundles');
94 | $config_extension_ids = array_keys($container->findTaggedServiceIds('hostnet_webpack.config_extension'));
95 | $config_class_names = [];
96 |
97 | foreach ($config_extension_ids as $id) {
98 | $config_class_names[$id] = $container->getDefinition($id)->getClass();
99 | }
100 |
101 | $configuration = new Configuration(array_keys($bundles), $config_class_names);
102 |
103 | $container->addResource(new FileResource((new \ReflectionClass(\get_class($configuration)))->getFileName()));
104 |
105 | return $configuration;
106 | }
107 |
108 | /**
109 | * {@inheritdoc}
110 | */
111 | public function getNamespace()
112 | {
113 | return 'http://hostnet.nl/schema/dic/webpack';
114 | }
115 | }
116 |
--------------------------------------------------------------------------------
/src/Bundle/Twig/TwigExtension.php:
--------------------------------------------------------------------------------
1 | loader = $loader;
51 | $this->web_dir = $web_dir;
52 | $this->public_path = $public_path;
53 | $this->dump_path = $dump_path;
54 | $this->common_js = $common_js;
55 | $this->common_css = $common_css;
56 | }
57 |
58 | /**
59 | * {@inheritdoc}
60 | */
61 | public function getName()
62 | {
63 | return Configuration::CONFIG_ROOT;
64 | }
65 |
66 | /**
67 | * {@inheritdoc}
68 | */
69 | public function getTokenParsers()
70 | {
71 | return [new WebpackTokenParser($this, $this->loader)];
72 | }
73 |
74 | /**
75 | * {@inheritdoc}
76 | */
77 | public function getFunctions()
78 | {
79 | return [
80 | new TwigFunction('webpack_asset', [$this, 'webpackAsset']),
81 | new TwigFunction('webpack_public', [$this, 'webpackPublic']),
82 | new TwigFunction('webpack_common_js', [$this, 'webpackCommonJs']),
83 | new TwigFunction('webpack_common_css', [$this, 'webpackCommonCss']),
84 | ];
85 | }
86 |
87 | /**
88 | * Returns an array containing a 'js' and 'css' key that refer to the path of the compiled asset from a browser
89 | * perspective.
90 | *
91 | * @param string $asset
92 | * @return array
93 | */
94 | public function webpackAsset($asset): array
95 | {
96 | $asset_id = $this->public_path . '/' . Compiler::getAliasId($asset);
97 | $full_asset_path = $this->web_dir . '/' . $asset_id;
98 |
99 | return [
100 | 'js' => file_exists($full_asset_path . '.js')
101 | ? $asset_id . '.js?' . filemtime($full_asset_path . '.js')
102 | : false,
103 | 'css' => file_exists($full_asset_path . '.css')
104 | ? $asset_id . '.css?' . filemtime($full_asset_path . '.css')
105 | : false,
106 | ];
107 | }
108 |
109 | /**
110 | * Returns the mapped url for the given resource.
111 | *
112 | * For example:
113 | * given url: "@AppBundle/images/foo.png"
114 | * real path: "AppBundle/Resources/public/images/foo.png"
115 | * mapped to: "//app/images/foo.png"
116 | *
117 | * The mapped url is either a symlink or copied asset that resides in the directory.
118 | *
119 | * @param string $url
120 | * @return string
121 | */
122 | public function webpackPublic($url): string
123 | {
124 | $public_dir = '/' . ltrim($this->dump_path, '/');
125 |
126 | $url = preg_replace_callback('/^@(?\w+)/', function ($match) {
127 | $str = $match['bundle'];
128 | if (substr($str, \strlen($str) - 6) === 'Bundle') {
129 | $str = substr($str, 0, -6);
130 | }
131 | return strtolower($str);
132 | }, $url);
133 |
134 | return rtrim($public_dir, '/') . '/' . ltrim($url, '/');
135 | }
136 |
137 | /**
138 | * Example: "/.js".
139 | *
140 | * @return string
141 | */
142 | public function webpackCommonJs(): string
143 | {
144 | $file = $this->web_dir . '/' . $this->common_js;
145 | $modified_time = file_exists($this->web_dir . '/' . $this->common_js) ? filemtime($file) : 0;
146 | return $this->common_js . '?' . $modified_time;
147 | }
148 |
149 | /**
150 | * Example: "/.css".
151 | *
152 | * @return string
153 | */
154 | public function webpackCommonCss(): string
155 | {
156 | $file = $this->web_dir . '/' . $this->common_css;
157 | $modified_time = file_exists($this->web_dir . '/' . $this->common_css) ? filemtime($file) : 0;
158 | return $this->common_css . '?' . $modified_time;
159 | }
160 | }
161 |
--------------------------------------------------------------------------------
/src/Bundle/Twig/Token/WebpackTokenParser.php:
--------------------------------------------------------------------------------
1 | extension = $extension;
51 | $this->loader = $loader;
52 | }
53 |
54 | /**
55 | * {@inheritdoc}
56 | */
57 | public function setParser(Parser $parser)
58 | {
59 | $this->parser = $parser;
60 | }
61 |
62 | /**
63 | * {@inheritdoc}
64 | */
65 | public function getTag()
66 | {
67 | return self::TAG_NAME;
68 | }
69 |
70 | /**
71 | * {@inheritdoc}
72 | */
73 | public function parse(Token $token)
74 | {
75 | $stream = $this->parser->getStream();
76 | $lineno = $stream->getCurrent()->getLine();
77 |
78 | // Export type: "js" or "css"
79 | $export_type = $stream->expect(Token::NAME_TYPE)->getValue();
80 | if (false === \in_array($export_type, ['js', 'css', 'inline'])) {
81 | // This exception will include the template filename by itself.
82 | throw new SyntaxError(sprintf(
83 | 'Expected export type "inline", "js" or "css", got "%s" at line %d.',
84 | $export_type,
85 | $lineno
86 | ));
87 | }
88 |
89 | if ($export_type === 'inline') {
90 | return $this->parseInline($stream, $lineno);
91 | }
92 |
93 | return $this->parseType($stream, $lineno, $export_type);
94 | }
95 |
96 | /**
97 | * @param TokenStream $stream
98 | * @param int $lineno
99 | * @param string $export_type
100 | * @return WebpackNode
101 | */
102 | private function parseType(TokenStream $stream, $lineno, $export_type): WebpackNode
103 | {
104 | $files = [];
105 | while (! $stream->isEOF() && ! $stream->getCurrent()->test(Token::BLOCK_END_TYPE)) {
106 | $asset = $stream->expect(Token::STRING_TYPE)->getValue();
107 |
108 | if (false === ($file = $this->extension->webpackAsset($asset)[$export_type])) {
109 | continue;
110 | }
111 | $files[] = $file;
112 | }
113 |
114 | $stream->expect(Token::BLOCK_END_TYPE);
115 |
116 | $body = $this->parser->subparse(function ($token) {
117 | return $token->test(['end' . $this->getTag()]);
118 | }, true);
119 |
120 | $stream->expect(Token::BLOCK_END_TYPE);
121 |
122 | return new WebpackNode([$body], ['files' => $files], $lineno, $this->getTag());
123 | }
124 |
125 | /**
126 | * @param TokenStream $stream
127 | * @param int $lineno
128 | * @return WebpackInlineNode
129 | */
130 | private function parseInline(TokenStream $stream, $lineno): WebpackInlineNode
131 | {
132 | if ($stream->test(Token::NAME_TYPE)) {
133 | $stream->next();
134 | }
135 |
136 | $stream->expect(Token::BLOCK_END_TYPE);
137 |
138 | $this->parser->subparse(function (Token $token) {
139 | return $token->test(['end' . $this->getTag()]);
140 | }, true);
141 |
142 | $stream->expect(Token::BLOCK_END_TYPE);
143 |
144 | $file = $this->loader->getCacheKey($stream->getSourceContext()->getName());
145 | if (false === isset($this->inline_blocks[$file])) {
146 | $this->inline_blocks[$file] = 0;
147 | }
148 |
149 | $file_name = TwigParser::hashInlineFileName($file, $this->inline_blocks[$file]) . '.js';
150 | $assets = $this->extension->webpackAsset('cache.' . $file_name);
151 |
152 | $this->inline_blocks[$file]++;
153 |
154 | return new WebpackInlineNode(
155 | ['js_file' => $assets['js'], 'css_file' => $assets['css']],
156 | $lineno,
157 | $this->getTag()
158 | );
159 | }
160 | }
161 |
--------------------------------------------------------------------------------
/test/Bundle/DependencyInjection/WebpackCompilerPassTest.php:
--------------------------------------------------------------------------------
1 | getContainerExtension();
37 | $fixture_dir = sprintf('%s/Fixture', \dirname(__DIR__, 2));
38 |
39 | $container->setParameter('kernel.bundles', ['FooBundle' => FooBundle::class, 'BarBundle' => BarBundle::class]);
40 | $container->setParameter('kernel.environment', 'dev');
41 | $container->setParameter('kernel.root_dir', $fixture_dir);
42 | $container->setParameter('kernel.cache_dir', realpath($fixture_dir . '/cache'));
43 | $container->set('kernel', $this->prophesize(Kernel::class)->reveal());
44 | $container->set('filesystem', new Filesystem());
45 | $container->set('twig', $this->prophesize(Environment::class)->reveal());
46 | $container->set('twig.loader', $this->prophesize(FilesystemLoader::class)->reveal());
47 | $container->set('logger', $this->prophesize(LoggerInterface::class)->reveal());
48 |
49 | $code_block_provider = new Definition(CodeBlockProviderInterface::class);
50 | $code_block_provider->addTag('hostnet_webpack.config_extension');
51 | $container->setDefinition('webpack_extension', $code_block_provider);
52 |
53 | $bundle->build($container);
54 |
55 | $extension->load([
56 | 'webpack' => [
57 | 'node' => ['node_modules_path' => $fixture_dir . '/node_modules'],
58 | 'bundles' => ['FooBundle'],
59 | 'resolve' => ['alias' => ['foo' => __DIR__, 'bar' => __DIR__ . '/fake']],
60 | ],
61 | ], $container);
62 | $container->compile();
63 |
64 | self::assertTrue($container->hasDefinition(Compiler::class));
65 | self::assertTrue($container->hasDefinition(Tracker::class));
66 | self::assertTrue($container->hasDefinition(ConfigGenerator::class));
67 | self::assertTrue($container->hasDefinition(Profiler::class));
68 |
69 | $config_generator_definition = $container->getDefinition(ConfigGenerator::class);
70 | self::assertTrue($config_generator_definition->hasMethodCall('addExtension'));
71 |
72 | $method_calls = $container->getDefinition(Tracker::class)->getMethodCalls();
73 | self::assertEquals([['addPath', [__DIR__]]], $method_calls);
74 |
75 | $process_definition = $container->getDefinition(Process::class);
76 | self::assertTrue($process_definition->hasMethodCall('setTimeout'));
77 | self::assertEquals(
78 | Configuration::DEFAULT_COMPILE_TIMEOUT_SECONDS,
79 | $process_definition->getMethodCalls()[0][1][0]
80 | );
81 | }
82 |
83 | public function testLoadNoWebpack(): void
84 | {
85 | $bundle = new WebpackBundle();
86 | $container = new ContainerBuilder();
87 | $extension = $bundle->getContainerExtension();
88 | $fixture_dir = realpath(__DIR__ . '/../../Fixture');
89 |
90 | $container->setParameter('kernel.bundles', ['FooBundle' => FooBundle::class, 'BarBundle' => BarBundle::class]);
91 | $container->setParameter('kernel.environment', 'dev');
92 | $container->setParameter('kernel.root_dir', $fixture_dir);
93 | $container->setParameter('kernel.cache_dir', realpath($fixture_dir . '/cache'));
94 |
95 | $bundle->build($container);
96 |
97 | $extension->load([
98 | 'webpack' => [
99 | 'node' => ['node_modules_path' => $fixture_dir],
100 | 'bundles' => ['FooBundle'],
101 | ],
102 | ], $container);
103 |
104 | $this->expectException(\RuntimeException::class);
105 | $this->expectExceptionMessage('Webpack is not installed in path');
106 |
107 | $container->compile();
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/src/Component/Configuration/Plugin/UglifyJsPlugin.php:
--------------------------------------------------------------------------------
1 | config = $config['plugins']['uglifyjs'] ?? [];
48 | }
49 |
50 | /**
51 | * {@inheritdoc}
52 | */
53 | public static function applyConfiguration(NodeBuilder $node_builder): void
54 | {
55 | $uglify = $node_builder
56 | ->arrayNode('uglifyjs')
57 | ->canBeEnabled()
58 | ->children();
59 |
60 | $compress = $uglify
61 | ->arrayNode('compress')
62 | ->addDefaultsIfNotSet()
63 | ->children();
64 |
65 | foreach (self::$config_map as [$option, $default, $info]) {
66 | $compress
67 | ->booleanNode($option)
68 | ->defaultValue($default)
69 | ->info($info)
70 | ->end();
71 | }
72 |
73 | $compress
74 | ->arrayNode('global_defs')
75 | ->info('global definition')
76 | ->prototype('scalar')
77 | ->end()
78 | ->end();
79 |
80 | $uglify
81 | ->arrayNode('mangle_except')
82 | ->defaultValue(['$super', '$', 'exports', 'require'])
83 | ->info('Variable names to not mangle')
84 | ->prototype('scalar')
85 | ->end();
86 |
87 | $uglify
88 | ->booleanNode('source_map')
89 | ->defaultTrue()
90 | ->info(sprintf(
91 | '%s %s',
92 | 'The plugin uses SourceMaps to map error message locations to modules.',
93 | 'This slows down the compilation'
94 | ))
95 | ->end();
96 |
97 | $uglify
98 | ->scalarNode('test')
99 | ->defaultValue('/\.js($|\?)/i')
100 | ->info('RegExp to filter processed files')
101 | ->end();
102 |
103 | $uglify
104 | ->booleanNode('minimize')
105 | ->defaultTrue()
106 | ->info('Whether to minimize or not')
107 | ->end();
108 | }
109 |
110 | /**
111 | * {@inheritdoc}
112 | */
113 | public function getCodeBlocks()
114 | {
115 | if (empty($this->config) || !$this->config['enabled']) {
116 | return [];
117 | }
118 |
119 | $compress = json_encode($this->config['compress']);
120 | $source_map = json_encode($this->config['source_map']);
121 | $test = $this->config['test'];
122 | $minimize = json_encode($this->config['minimize']);
123 | $mangle = !empty($this->config['mangle_except'])
124 | ? '{except:' . json_encode($this->config['mangle_except']) . '}'
125 | : 'false';
126 |
127 | $config = <<set(CodeBlock::PLUGIN, sprintf('new %s(%s)', 'webpack.optimize.UglifyJsPlugin', $config))];
139 | }
140 | }
141 |
--------------------------------------------------------------------------------
/src/Component/Asset/Compiler.php:
--------------------------------------------------------------------------------
1 | profiler = $profiler;
72 | $this->tracker = $tracker;
73 | $this->twig_parser = $twig_parser;
74 | $this->generator = $generator;
75 | $this->process = $process;
76 | $this->cache_dir = $cache_dir;
77 | $this->bundles = $bundles;
78 | $this->stopwatch = $stopwatch ?? new Stopwatch();
79 | }
80 |
81 | /**
82 | * @return string
83 | */
84 | public function compile(): string
85 | {
86 | $this->stopwatch->start('webpack.total');
87 | $this->stopwatch->start('webpack.prepare');
88 |
89 | // Recompile twig templates where its needed.
90 | $this->addSplitPoints();
91 | $this->addResolveConfig();
92 |
93 | // Write the webpack configuration file.
94 | file_put_contents(
95 | $this->cache_dir . DIRECTORY_SEPARATOR . 'webpack.config.js',
96 | $this->generator->getConfiguration()
97 | );
98 | $this->profiler->set('compiler.performance.prepare', $this->stopwatch->stop('webpack.prepare')->getDuration());
99 | $this->stopwatch->start('webpack.compiler');
100 | $this->process->inheritEnvironmentVariables(true);
101 | $this->process->run();
102 |
103 | $output = $this->process->getOutput() . $this->process->getErrorOutput();
104 | $this->profiler->set('compiler.executed', true);
105 | $this->profiler->set('compiler.last_output', $output);
106 | $this->profiler->set(
107 | 'compiler.successful',
108 | strpos($output, 'Error:') === false &&
109 | strpos($output, 'parse failed') === false
110 | );
111 |
112 | // Finally, write some logging for later use.
113 | file_put_contents($this->cache_dir . DIRECTORY_SEPARATOR . 'webpack.compiler.log', $output);
114 |
115 | $this->profiler->set(
116 | 'compiler.performance.compiler',
117 | $this->stopwatch->stop('webpack.compiler')->getDuration()
118 | );
119 | $this->profiler->set('compiler.performance.total', $this->stopwatch->stop('webpack.total')->getDuration());
120 |
121 | return $output;
122 | }
123 |
124 | /**
125 | * Adds root & alias configuration entries.
126 | */
127 | private function addResolveConfig(): void
128 | {
129 | $aliases = $this->tracker->getAliases();
130 | $this->generator->addBlock(
131 | (new CodeBlock())->set(CodeBlock::RESOLVE, ['alias' => $aliases, 'root' => array_values($aliases)])
132 | );
133 | }
134 |
135 | /**
136 | * Add split points to the 'entry' section of the configuration.
137 | */
138 | private function addSplitPoints(): void
139 | {
140 | $split_points = [];
141 | foreach ($this->tracker->getTemplates() as $template_file) {
142 | $split_points = array_merge($split_points, $this->twig_parser->findSplitPoints($template_file));
143 | }
144 |
145 | foreach ($split_points as $id => $file) {
146 | $this->generator->addBlock((new CodeBlock())->set(CodeBlock::ENTRY, [self::getAliasId($id) => $file]));
147 | }
148 | }
149 |
150 | /**
151 | * Returns the alias id for the given path.
152 | *
153 | * @param string $path
154 | * @return string
155 | */
156 | public static function getAliasId($path): string
157 | {
158 | return str_replace(
159 | ['/', '\\'],
160 | '.',
161 | Container::underscore(ltrim(substr($path, 0, (int) strrpos($path, '.')), '@'))
162 | );
163 | }
164 | }
165 |
--------------------------------------------------------------------------------
/test/Component/Asset/TrackedFilesTest.php:
--------------------------------------------------------------------------------
1 | directory_a = tempnam(sys_get_temp_dir(), 'tracked_files_unittest_a');
42 | unlink($this->directory_a);
43 | mkdir($this->directory_a);
44 |
45 | $this->directory_b = tempnam(sys_get_temp_dir(), 'tracked_files_unittest_b');
46 | unlink($this->directory_b);
47 | mkdir($this->directory_b);
48 | }
49 |
50 | /**
51 | * Ensure the test directories a & b are removed
52 | *
53 | * {@inheritDoc}
54 | */
55 | protected function tearDown(): void
56 | {
57 | $fs = new Filesystem();
58 | $fs->remove($this->directory_a);
59 | $fs->remove($this->directory_b);
60 | }
61 |
62 | /**
63 | * Test the behavior for empty / no directories
64 | */
65 | public function testTrackedFilesEmpty(): void
66 | {
67 | $t1 = new TrackedFiles([$this->directory_a]);
68 | $t2 = new TrackedFiles([$this->directory_b]);
69 |
70 | self::assertFalse($t1->modifiedAfter($t2));
71 | self::assertFalse($t2->modifiedAfter($t1));
72 |
73 | self::assertFalse($t1->modifiedAfter($t1));
74 | self::assertFalse($t2->modifiedAfter($t2));
75 | }
76 |
77 | /**
78 | * What happens when a file is added after 'compilation'
79 | */
80 | public function testAdd(): void
81 | {
82 | $time = time();
83 |
84 | $file = tempnam($this->directory_a, 'tracked_files_unittest_a_file1');
85 | touch($file, $time - 100);
86 | $file = tempnam($this->directory_b, 'tracked_files_unittest_b_file1');
87 | touch($file, $time - 50);
88 | $file = tempnam($this->directory_a, 'tracked_files_unittest_a_file2');
89 | touch($file, $time);
90 |
91 | $t1 = new TrackedFiles([$this->directory_a]);
92 | $t2 = new TrackedFiles([$this->directory_b]);
93 |
94 | self::assertTrue($t1->modifiedAfter($t2));
95 | self::assertFalse($t2->modifiedAfter($t1));
96 | }
97 |
98 | /**
99 | * What happens when a file is removed after 'compilation'
100 | */
101 | public function testDel(): void
102 | {
103 | $time = time();
104 |
105 | $file1 = tempnam($this->directory_a, 'tracked_files_unittest_a_file1');
106 | touch($file1, $time - 100);
107 | $file2 = tempnam($this->directory_b, 'tracked_files_unittest_b_file1');
108 | touch($file2, $time - 50);
109 | $file3 = tempnam($this->directory_a, 'tracked_files_unittest_a_file2');
110 | touch($file3, $time);
111 |
112 | unlink($file3);
113 |
114 | $t1 = new TrackedFiles([$this->directory_a]);
115 | $t2 = new TrackedFiles([$this->directory_b]);
116 |
117 | self::assertFalse($t1->modifiedAfter($t2));
118 | self::assertTrue($t2->modifiedAfter($t1));
119 | }
120 |
121 | /**
122 | * What happens when a file is modified before 'compilation'
123 | */
124 | public function testModifyBefore(): void
125 | {
126 | $time = time();
127 |
128 | $file = tempnam($this->directory_a, 'tracked_files_unittest_a_file1');
129 | touch($file, $time - 100);
130 | $file = tempnam($this->directory_b, 'tracked_files_unittest_b_file1');
131 | touch($file, $time - 50);
132 | $file = tempnam($this->directory_a, 'tracked_files_unittest_a_file2');
133 | touch($file, $time - 200);
134 |
135 | $t1 = new TrackedFiles([$this->directory_a]);
136 | $t2 = new TrackedFiles([$this->directory_b]);
137 |
138 | self::assertFalse($t1->modifiedAfter($t2));
139 | self::assertTrue($t2->modifiedAfter($t1));
140 | }
141 |
142 | /**
143 | * What happens when a file is modified after 'compilation'
144 | */
145 | public function testModifyAfter(): void
146 | {
147 | $time = time();
148 |
149 | $file = tempnam($this->directory_a, 'tracked_files_unittest_a_file1');
150 | touch($file, $time - 100);
151 | $file = tempnam($this->directory_b, 'tracked_files_unittest_b_file1');
152 | touch($file, $time - 50);
153 | $file = tempnam($this->directory_a, 'tracked_files_unittest_a_file2');
154 | touch($file, $time);
155 |
156 | $t1 = new TrackedFiles([$this->directory_a]);
157 | $t2 = new TrackedFiles([$this->directory_b]);
158 |
159 | self::assertTrue($t1->modifiedAfter($t2));
160 | self::assertFalse($t2->modifiedAfter($t1));
161 | }
162 | }
163 |
--------------------------------------------------------------------------------
/src/Bundle/DependencyInjection/WebpackCompilerPass.php:
--------------------------------------------------------------------------------
1 | getDefinition(Tracker::class);
27 | $bundles = $container->getParameter('kernel.bundles');
28 | $config = $container->getParameter('hostnet_webpack_config');
29 | $tracked_bundles = $config['bundles'];
30 | $asset_res_path = 'Resources' . DIRECTORY_SEPARATOR . 'assets';
31 | $public_res_path = 'Resources' . DIRECTORY_SEPARATOR . 'public';
32 | $public_path = rtrim($config['output']['public_path'], '\\/');
33 | $dump_path = rtrim($config['output']['dump_path'], '\\/');
34 | $path = rtrim($config['output']['path'], '\\/');
35 | $web_dir = rtrim(substr($path, 0, \strlen($path) - \strlen($public_path)), '/\\');
36 | $bundle_paths = [];
37 |
38 | // add all configured bundles to the tracker
39 | foreach ($bundles as $name => $class) {
40 | if (false === \in_array($name, $tracked_bundles, false)) {
41 | continue;
42 | }
43 |
44 | $bundle_paths[$name] = realpath(\dirname((new \ReflectionClass($class))->getFileName()));
45 | }
46 |
47 | $asset_tracker->replaceArgument(3, $asset_res_path);
48 | $asset_tracker->replaceArgument(4, $path);
49 | $asset_tracker->replaceArgument(5, $bundle_paths);
50 |
51 | // add all aliases to the tracker
52 | if (isset($config['resolve']['alias']) && \is_array($config['resolve']['alias'])) {
53 | foreach ($config['resolve']['alias'] as $alias_path) {
54 | if (!file_exists($alias_path)) {
55 | continue;
56 | }
57 | $asset_tracker->addMethodCall('addPath', [$alias_path]);
58 | }
59 | }
60 |
61 | // Configure the compiler process.
62 | $env_vars = [
63 | 'PATH' => getenv('PATH'),
64 | 'NODE_PATH' => $config['node']['node_modules_path'],
65 | ];
66 |
67 | $container
68 | ->getDefinition(Dumper::class)
69 | ->replaceArgument(2, $bundle_paths)
70 | ->replaceArgument(3, $public_res_path)
71 | ->replaceArgument(4, $dump_path);
72 |
73 | $container
74 | ->getDefinition(Compiler::class)
75 | ->replaceArgument(6, $config['bundles']);
76 |
77 | $container
78 | ->getDefinition(TwigExtension::class)
79 | ->replaceArgument(1, $web_dir)
80 | ->replaceArgument(2, $public_path)
81 | ->replaceArgument(3, str_replace($web_dir, '', $dump_path))
82 | ->replaceArgument(4, sprintf('%s/%s.js', $public_path, $config['output']['common_id']))
83 | ->replaceArgument(5, sprintf('%s/%s.css', $public_path, $config['output']['common_id']));
84 |
85 | // Ensure webpack is installed in the given (or detected) node_modules directory.
86 | if (false === ($webpack = realpath($config['node']['node_modules_path'] . '/webpack/bin/webpack.js'))) {
87 | throw new \RuntimeException(
88 | sprintf(
89 | 'Webpack is not installed in path "%s".',
90 | $config['node']['node_modules_path']
91 | )
92 | );
93 | }
94 |
95 | $process_definition = $container
96 | ->getDefinition(Process::class)
97 | ->replaceArgument(0, [$config['node']['binary'], $webpack])
98 | ->replaceArgument(1, $container->getParameter('kernel.cache_dir'))
99 | ->addMethodCall('setTimeout', [$config['compile_timeout']]);
100 |
101 | $builder_definition = $container->getDefinition(ConfigGenerator::class);
102 | $config_extension_ids = array_keys($container->findTaggedServiceIds('hostnet_webpack.config_extension'));
103 | foreach ($config_extension_ids as $id) {
104 | $builder_definition->addMethodCall('addExtension', [new Reference($id)]);
105 | }
106 |
107 | // Unfortunately, we need to specify some additional environment variables to pass to the compiler process. We
108 | // need this because there is a big chance that populating the $_ENV variable is disabled on most machines.
109 | // FIXME http://stackoverflow.com/questions/32125810/windows-symfony2-process-crashes-when-passing-env-variables
110 | // @codeCoverageIgnoreStart
111 | if (stripos(PHP_OS, 'WIN') === 0) {
112 | $env_vars['COMSPEC'] = getenv('COMSPEC');
113 | $env_vars['WINDIR'] = getenv('WINDIR');
114 | $env_vars['COMMONPROGRAMW6432'] = getenv('COMMONPROGRAMW6432');
115 | $env_vars['COMPUTERNAME'] = getenv('COMPUTERNAME');
116 | $env_vars['TMP'] = getenv('TMP');
117 |
118 | $process_definition->addMethodCall('setEnhanceWindowsCompatibility', [true]);
119 | // $process_definition->addMethodCall('setEnv', [$env_vars]);
120 | } else {
121 | $process_definition->addMethodCall('setEnv', [$env_vars]);
122 | }
123 | // @codeCoverageIgnoreEnd
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/src/Component/Asset/TwigParser.php:
--------------------------------------------------------------------------------
1 | tracker = $tracker;
27 | $this->twig = $twig;
28 | $this->cache_dir = $cache_dir;
29 | }
30 |
31 | /**
32 | * Consistently calculate file name hashes.
33 | *
34 | * @param $template_file
35 | * @param $block_index
36 | * @return string
37 | */
38 | public static function hashInlineFileName($template_file, $block_index): string
39 | {
40 | // Work around path inconsistencies on Windows/XAMPP.
41 | if (DIRECTORY_SEPARATOR === '\\') {
42 | $template_file = str_replace('\\', '/', $template_file);
43 | }
44 |
45 | return md5($template_file . $block_index);
46 | }
47 |
48 | /**
49 | * Returns an array of split points from the given template file.
50 | *
51 | * @param string $template_file
52 | * @return array
53 | */
54 | public function findSplitPoints($template_file): array
55 | {
56 | $inline_blocks = 0;
57 | $source = new Source(file_get_contents($template_file), $template_file);
58 | $stream = $this->twig->tokenize($source);
59 | $points = [];
60 |
61 | while (! $stream->isEOF() && $token = $stream->next()) {
62 | // {{ webpack_asset(...) }}
63 | if ($token->test(Token::NAME_TYPE, 'webpack_asset')) {
64 | // We found the webpack function!
65 | $asset = $this->getAssetFromStream($template_file, $stream);
66 | $points[$asset] = $this->resolveAssetPath($asset, $template_file, $token);
67 | }
68 |
69 | // {% webpack_javascripts %} and {% webpack_stylesheets %}
70 | if ($token->test(Token::BLOCK_START_TYPE) && $stream->getCurrent()->test(WebpackTokenParser::TAG_NAME)) {
71 | $stream->next();
72 |
73 | if ($stream->getCurrent()->getValue() === 'inline') {
74 | $stream->next();
75 |
76 | $token = $stream->next();
77 | $file_name = self::hashInlineFileName($template_file, $inline_blocks);
78 |
79 | // Are we dealing with a custom extension? If not, fallback to javascript.
80 | $extension = 'js'; // Default
81 | if ($token->test(Token::NAME_TYPE)) {
82 | $extension = $token->getValue();
83 | $stream->next();
84 | }
85 |
86 | file_put_contents(
87 | $this->cache_dir . '/' . $file_name . '.' . $extension,
88 | $this->stripScript($stream->getCurrent()->getValue())
89 | );
90 |
91 | $asset = $file_name . '.' . $extension;
92 | $id = 'cache/' . $asset;
93 | $points[$id] = $this->resolveAssetPath($this->cache_dir . '/' . $asset, $template_file, $token);
94 | $inline_blocks++;
95 | } else {
96 | $stream->next();
97 | while (! $stream->isEOF() && ! $stream->getCurrent()->test(Token::BLOCK_END_TYPE)) {
98 | $asset = $stream->expect(Token::STRING_TYPE)->getValue();
99 | $points[$asset] = $this->resolveAssetPath($asset, $template_file, $token);
100 | }
101 | }
102 | }
103 | }
104 |
105 | return $points;
106 | }
107 |
108 | /**
109 | * @param string $asset
110 | * @param string $template_file
111 | * @param Token $token
112 | * @return string
113 | */
114 | private function resolveAssetPath($asset, $template_file, $token): string
115 | {
116 | if (false === ($asset_path = $this->tracker->resolveResourcePath($asset))) {
117 | throw new \RuntimeException(sprintf(
118 | 'The file "%s" referenced in "%s" at line %d could not be resolved.',
119 | $asset,
120 | $template_file,
121 | $token->getLine()
122 | ));
123 | }
124 |
125 | return $asset_path;
126 | }
127 |
128 | /**
129 | * @param $filename
130 | * @param TokenStream $stream
131 | * @return mixed
132 | */
133 | private function getAssetFromStream($filename, TokenStream $stream)
134 | {
135 | $this->expect($filename, $stream->next(), Token::PUNCTUATION_TYPE, '(');
136 | $token = $stream->next();
137 | $this->expect($filename, $token, Token::STRING_TYPE);
138 | $this->expect($filename, $stream->next(), Token::PUNCTUATION_TYPE, ')');
139 |
140 | return $token->getValue();
141 | }
142 |
143 | private function expect($filename, Token $token, $type, $value = null): void
144 | {
145 | if ($token->getType() !== $type) {
146 | throw new \RuntimeException(sprintf(
147 | 'Parse error in %s at line %d. Expected %s%s, got %s.',
148 | $filename,
149 | $token->getLine(),
150 | Token::typeToEnglish($type),
151 | $value !== null ? ' "' . $value . '"' : '',
152 | Token::typeToEnglish($token->getType())
153 | ));
154 | }
155 | }
156 |
157 | private function stripScript($str)
158 | {
159 | $matches = [];
160 | if (preg_match('/^\s*