├── 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 | 8 | 9 | 10 | 11 | {% endfor %} 12 |
{{ bundle }}{{ path }}
13 |

Templates

14 | 15 | 16 | {% for path in collector.get('templates') %} 17 | 18 | 19 | 20 | 21 | {% endfor %} 22 |
{{ path }}
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 | 7 | 8 | 9 | 10 | 11 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 |
Compile reason{{ collector.get('tracker.reason') }}
Analyze time 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 |
Compile time{{ collector.get('compiler.performance.compiler') }} ms
Total time{{ collector.get('compiler.performance.total') }} ms
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*(.*)<\/script>\s*$/s', $str, $matches)) { 161 | return $matches[2]; 162 | } 163 | 164 | if (preg_match('/^\s*(.*)<\/style>\s*$/s', $str, $matches)) { 165 | return $matches[2]; 166 | } 167 | 168 | return $str; 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /src/Bundle/DependencyInjection/Configuration.php: -------------------------------------------------------------------------------- 1 | bundles = $bundles; 30 | $this->plugins = $plugins; 31 | } 32 | 33 | /** 34 | * {@inheritdoc} 35 | */ 36 | public function getConfigTreeBuilder() 37 | { 38 | $tree_builder = $this->createTreeBuilder(); 39 | $root_node = $this->retrieveRootNode($tree_builder); 40 | $children = $root_node->children(); 41 | 42 | $root_node->fixXmlConfig('bundle'); 43 | 44 | $this->addNodeJSConfiguration($children); 45 | $this->addParentConfiguration($children); 46 | $this->addBundleConfiguration($children); 47 | $this->addLoaderConfiguration($children); 48 | $this->addPluginConfiguration($children); 49 | 50 | $children 51 | ->integerNode('compile_timeout') 52 | ->defaultValue(self::DEFAULT_COMPILE_TIMEOUT_SECONDS) 53 | ->end(); 54 | 55 | $children->end(); 56 | 57 | return $tree_builder; 58 | } 59 | 60 | /** 61 | * Adds node-js specific configuration to the tree builder. 62 | * 63 | * @param NodeBuilder $node 64 | */ 65 | private function addNodeJSConfiguration(NodeBuilder $node): void 66 | { 67 | $node 68 | ->arrayNode('node') 69 | ->addDefaultsIfNotSet() 70 | ->children() 71 | ->arrayNode('binary') 72 | ->addDefaultsIfNotSet() 73 | ->beforeNormalization() 74 | ->ifString() 75 | ->then(function ($value) { 76 | return [ 77 | 'win32' => $value, 78 | 'win64' => $value, 79 | 'linux_x32' => $value, 80 | 'linux_x64' => $value, 81 | 'darwin' => $value, 82 | 'fallback' => $value, 83 | ]; 84 | }) 85 | ->end() 86 | ->children() 87 | ->scalarNode('win32')->defaultValue('node')->end() 88 | ->scalarNode('win64')->defaultValue('node')->end() 89 | ->scalarNode('linux_x32')->defaultValue('node')->end() 90 | ->scalarNode('linux_x64')->defaultValue('node')->end() 91 | ->scalarNode('darwin')->defaultValue('node')->end() 92 | ->scalarNode('fallback')->defaultValue('node')->end() 93 | ->end() 94 | ->end() 95 | ->scalarNode('npm_packages_path')->defaultNull()->end() 96 | ->scalarNode('node_modules_path')->defaultNull()->end() 97 | ->end(); 98 | } 99 | 100 | /** 101 | * Adds generic configuration to the tree builder in the parent (root) node. 102 | * 103 | * @param NodeBuilder $node 104 | */ 105 | private function addParentConfiguration(NodeBuilder $node): void 106 | { 107 | $this->applyConfigurationFromClass(ConfigInterface::class, $node); 108 | } 109 | 110 | /** 111 | * Adds bundle configuration to the tree builder. 112 | * 113 | * @param NodeBuilder $node 114 | */ 115 | private function addBundleConfiguration(NodeBuilder $node): void 116 | { 117 | $node 118 | ->arrayNode('bundles') 119 | ->defaultValue($this->bundles) 120 | ->prototype('scalar') 121 | ->validate() 122 | ->ifNotInArray($this->bundles) 123 | ->thenInvalid('%s is not a valid bundle.') 124 | ->end() 125 | ->end(); 126 | } 127 | 128 | /** 129 | * @param NodeBuilder $node 130 | */ 131 | private function addPluginConfiguration(NodeBuilder $node): void 132 | { 133 | $children = $node 134 | ->arrayNode('plugins') 135 | ->addDefaultsIfNotSet() 136 | ->children(); 137 | 138 | $this->applyConfigurationFromClass(PluginInterface::class, $children); 139 | $children->end(); 140 | } 141 | 142 | /** 143 | * Adds loader configuration to the tree builder. 144 | * 145 | * @param NodeBuilder $node 146 | */ 147 | private function addLoaderConfiguration(NodeBuilder $node): void 148 | { 149 | $children = $node 150 | ->arrayNode('loaders') 151 | ->addDefaultsIfNotSet() 152 | ->children(); 153 | 154 | $this->applyConfigurationFromClass(LoaderInterface::class, $children); 155 | $children->end(); 156 | } 157 | 158 | /** 159 | * @param string $interface 160 | * @param NodeBuilder $node_builder 161 | */ 162 | private function applyConfigurationFromClass($interface, NodeBuilder $node_builder): void 163 | { 164 | foreach ($this->plugins as $name => $class_name) { 165 | // Only accept plugins of type PluginInterface. 166 | if (false === \in_array($interface, class_implements($class_name), false)) { 167 | continue; 168 | } 169 | 170 | /** @var ConfigExtensionInterface $class_name */ 171 | $class_name::applyConfiguration($node_builder); 172 | } 173 | } 174 | 175 | private function createTreeBuilder(): TreeBuilder 176 | { 177 | if (Kernel::VERSION_ID >= 40200) { 178 | return new TreeBuilder(self::CONFIG_ROOT); 179 | } 180 | 181 | if (Kernel::VERSION_ID >= 30300 && Kernel::VERSION_ID < 40200) { 182 | return new TreeBuilder(); 183 | } 184 | 185 | throw new \RuntimeException('This bundle can only be used by Symfony 3.3 and up.'); 186 | } 187 | 188 | private function retrieveRootNode(TreeBuilder $tree_builder): NodeDefinition 189 | { 190 | if (Kernel::VERSION_ID >= 40200) { 191 | return $tree_builder->getRootNode(); 192 | } 193 | 194 | if (Kernel::VERSION_ID >= 30300 && Kernel::VERSION_ID < 40200) { 195 | return $tree_builder->root(self::CONFIG_ROOT); 196 | } 197 | 198 | throw new \RuntimeException('This bundle can only be used by Symfony 3.3 and up.'); 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /src/Component/Asset/Tracker.php: -------------------------------------------------------------------------------- 1 | profiler = $profiler; 105 | $this->finder = $finder; 106 | $this->root_dir = $root_dir; 107 | $this->asset_dir = $asset_dir; 108 | $this->output_dir = $output_dir; 109 | $this->bundle_paths = $bundle_paths; 110 | } 111 | 112 | /** 113 | * Add a path to the list of tracked paths (this can be both dir's or files). 114 | * 115 | * @param string $path the path to track. 116 | * @return Tracker this instance. 117 | */ 118 | public function addPath($path): Tracker 119 | { 120 | if (empty($path) || false === ($real_path = realpath($path))) { 121 | throw new FileNotFoundException(null, 0, null, $path); 122 | } 123 | $this->paths[] = $real_path; 124 | 125 | return $this; 126 | } 127 | 128 | /** 129 | * Returns true if the cache is outdated. 130 | * 131 | * @return bool true, cache is outdated of non exsitant. 132 | */ 133 | public function isOutdated(): bool 134 | { 135 | $this->boot(); 136 | 137 | $compiled_tracked_files = new TrackedFiles([$this->output_dir]); 138 | $current_tracked_files = new TrackedFiles($this->paths); 139 | 140 | if ($current_tracked_files->modifiedAfter($compiled_tracked_files)) { 141 | $this->profiler->set('tracker.reason', 'One of the tracked files has been modified.'); 142 | return true; 143 | } 144 | 145 | $this->profiler->set('tracker.reason', false); 146 | 147 | return false; 148 | } 149 | 150 | /** 151 | * Get the tracked aliases 152 | * 153 | * @return array the tracked aliases. 154 | */ 155 | public function getAliases(): array 156 | { 157 | return $this->aliases; 158 | } 159 | 160 | /** 161 | * Returns a list of twig templates that are being tracked. 162 | * 163 | * @return string[] list of twig templates. 164 | */ 165 | public function getTemplates(): array 166 | { 167 | $this->boot(); 168 | 169 | return $this->templates; 170 | } 171 | 172 | /** 173 | * Runtime initialize this tracker. 174 | */ 175 | private function boot(): void 176 | { 177 | if ($this->booted) { 178 | return; 179 | } 180 | 181 | $this->booted = true; 182 | 183 | foreach ($this->finder->findAllTemplates() as $reference) { 184 | $this->addTemplate($reference); 185 | } 186 | 187 | foreach (array_keys($this->bundle_paths) as $name) { 188 | if (false !== ($resolved_path = $this->resolveResourcePath('@' . $name))) { 189 | $this->aliases['@' . $name] = $resolved_path; 190 | $this->addPath($resolved_path); 191 | } 192 | } 193 | 194 | $this->profiler->set('bundles', $this->aliases); 195 | $this->profiler->set('templates', $this->templates); 196 | } 197 | 198 | /** 199 | * Find the full path to a requested path, this can be bundle configurations like @BundleName/ 200 | * 201 | * @param string $path the path to resolv. 202 | * @return string|false the full path to the requested resource or false if not found. 203 | */ 204 | private function resolvePath($path) 205 | { 206 | // Find and replace the @BundleName with the absolute path to the bundle. 207 | $matches = []; 208 | preg_match('/(^|\/)@(\w+)/', $path, $matches); 209 | if (isset($matches[0], $matches[2], $this->bundle_paths[$matches[2]])) { 210 | return realpath(str_replace($matches[0], $this->bundle_paths[$matches[2]], $path)); 211 | } 212 | 213 | // The path doesn't contain a bundle name. In this case it must exist in %kernel.root_dir%/Resources/views 214 | $path = $this->root_dir . DIRECTORY_SEPARATOR . 'Resources' . DIRECTORY_SEPARATOR . $path; 215 | if (file_exists($path)) { 216 | return $path; 217 | } 218 | 219 | return false; 220 | } 221 | 222 | /** 223 | * Find the full path to a requested resource, this can be bundle configurations like @BundleName/resource.twig 224 | * 225 | * @param string $path the path resolve 226 | * @return string|false the full path to the requested resource or false if not found. 227 | */ 228 | public function resolveResourcePath($path) 229 | { 230 | $matches = []; 231 | preg_match('/@(\w+)/', $path, $matches); 232 | if (isset($matches[0], $matches[1])) { 233 | if (false === isset($this->bundle_paths[$matches[1]])) { 234 | return false; 235 | } 236 | 237 | return realpath(str_replace( 238 | $matches[0], 239 | $this->bundle_paths[$matches[1]] . DIRECTORY_SEPARATOR . trim($this->asset_dir, "\\/"), 240 | $path 241 | )); 242 | } 243 | 244 | return $path; 245 | } 246 | 247 | /** 248 | * Adds twig templates to the tracker. 249 | * 250 | * @param TemplateReferenceInterface $reference the reference to the twig template to be added. 251 | * @return Tracker this instance 252 | */ 253 | private function addTemplate(TemplateReferenceInterface $reference): Tracker 254 | { 255 | if ($reference->get('engine') !== 'twig') { 256 | return $this; 257 | } 258 | 259 | if (false !== ($path = $this->resolvePath($reference->getPath()))) { 260 | $this->templates[] = $path; 261 | 262 | return $this->addPath($path); 263 | } 264 | 265 | return $this; 266 | } 267 | } 268 | --------------------------------------------------------------------------------