├── .docs ├── README.md └── debug_panel.png ├── .editorconfig ├── LICENSE.md ├── Makefile ├── README.md ├── composer.json └── src ├── AssetLocator.php ├── AssetNameResolver ├── AssetNameResolverInterface.php ├── CannotResolveAssetNameException.php ├── DebuggerAwareAssetNameResolver.php ├── IdentityAssetNameResolver.php ├── ManifestAssetNameResolver.php └── StaticAssetNameResolver.php ├── BasePath ├── BasePathProvider.php └── NetteHttpBasePathProvider.php ├── BuildDirectoryProvider.php ├── DI └── WebpackExtension.php ├── Debugging ├── WebpackPanel.php └── templates │ ├── WebpackPanel.panel.phtml │ └── WebpackPanel.tab.phtml ├── DevServer ├── DevServer.php └── Http │ ├── Client.php │ ├── CurlClient.php │ └── MockClient.php ├── Latte ├── WebpackExtension.php ├── WebpackMacros.php └── WebpackNode.php ├── Manifest ├── CannotLoadManifestException.php ├── ManifestLoader.php ├── ManifestMapper.php └── Mapper │ ├── AssetsWebpackPluginMapper.php │ └── WebpackManifestPluginMapper.php └── PublicPathProvider.php /.docs/README.md: -------------------------------------------------------------------------------- 1 | # Webpack 2 | 3 | ## Contents 4 | 5 | - [Setup](#setup) 6 | - [Usage](#usage) 7 | 8 | ## Setup 9 | 10 | ```bash 11 | composer require contributte/webpack 12 | ``` 13 | 14 | ## Usage 15 | 16 | Register the extension in your config file, and configure it. The two `build` options are mandatory: 17 | 18 | ```neon 19 | extensions: 20 | webpack: Contributte\Webpack\DI\WebpackExtension(%debugMode%, %consoleMode%) 21 | 22 | webpack: 23 | build: 24 | directory: %wwwDir%/dist 25 | publicPath: dist/ 26 | ``` 27 | 28 | 29 | Now you can use the `{webpack}` macro in your templates. It automatically expands the provided asset name to the full path as configured: 30 | 31 | ```latte 32 | 33 | ``` 34 | 35 | 36 | ### webpack-dev-server integration 37 | 38 | You might want to use the Webpack's [dev server](https://www.npmjs.com/package/webpack-dev-server) to facilitate the development of client-side assets. But maybe once you're done with the client-side, you would like to build the back-end without having to start up the dev server. 39 | 40 | This package effectively solves this problem: it automatically serves assets from the dev server if available (i.e. it responds within a specified timeout), and falls back to the build directory otherwise. All you have to do is configure the dev server URL. The dev server is enabled automatically in debug mode; you can override this setting via `enabled` option: 41 | 42 | ```neon 43 | webpack: 44 | devServer: 45 | enabled: %debugMode% # default 46 | url: http://localhost:3000 47 | timeout: 0.1 # (seconds) default 48 | ``` 49 | 50 | #### Ignored assets 51 | 52 | You can also configure a set of asset names that should be ignored (i.e. resolved to an empty data URI) if the dev-server is available. This can be helpful e.g. if you use [`style-loader`](https://www.npmjs.com/package/style-loader) in development which does not emit any CSS files. 53 | 54 | ```neon 55 | webpack: 56 | devServer: 57 | ignoredAssets: 58 | - main.css 59 | ``` 60 | 61 | #### Public URL (e.g. Docker usage) 62 | 63 | Dev-server might have different URLs for different access points. For example, when running in Docker Compose setup, the Nette application accesses it via the internal Docker network, while you access it in the browser via the exposed port. For this, you can set up a different `publicUrl`. 64 | 65 | ```neon 66 | webpack: 67 | devServer: 68 | url: http://webpack-dev-server:3000 # URL over internal Docker network 69 | publicUrl: http://localhost:3030 # exposed port from the dev-server container 70 | ``` 71 | 72 | 73 | ### Asset resolvers and manifest file 74 | 75 | You might want to include the Webpack's asset hash in its file name for assets caching (and automatic cache busting in new releases) in the user agent. But how do you reference the asset files in your code if their names are dynamic? 76 | 77 | This package comes to the rescue. You can employ the [`webpack-manifest-plugin`](https://www.npmjs.com/package/webpack-manifest-plugin) or some similar plugin (see below) to produce a manifest file, and then configure the adapter to use it: 78 | 79 | ```neon 80 | webpack: 81 | manifest: 82 | name: manifest.json 83 | ``` 84 | 85 | This way, you can keep using the original asset names, and they get expanded automatically following the resolutions from the manifest file. 86 | 87 | This package automatically optimizes this in production environment by loading the manifest file in compile time. 88 | 89 | 90 | #### Manifest mappers 91 | 92 | By default, the manifest loader supports the aforementioned `webpack-manifest-plugin`. If you use a different plugin that produces the manifest in a different format, you can implement and configure a mapper for it. This package comes bundled with a mapper for the [`assets-webpack-plugin`](https://www.npmjs.com/package/assets-webpack-plugin): 93 | 94 | ```neon 95 | webpack: 96 | manifest: 97 | name: manifest.json 98 | mapper: Contributte\Webpack\Manifest\Mapper\AssetsWebpackPluginMapper 99 | ``` 100 | 101 | You can also implement your own mapper, simply extend `Contributte\Webpack\Manifest\ManifestMapper` and implement its `map()` method. It takes the parsed JSON content of the manifest file and is expected to return a flat array mapping asset names to file names. 102 | 103 | 104 | 105 | #### Manifest loading timeout 106 | 107 | You can specify a timeout for manifest loading from webpack-dev-server. The timeout defaults to 1 second. 108 | 109 | ```neon 110 | webpack: 111 | manifest: 112 | name: manifest.json 113 | timeout: 0.5 114 | ``` 115 | 116 | 117 | ### Debugger 118 | 119 | In development environment, this package registers its own debug bar panel into Tracy, giving you the overview of 120 | 121 | - what assets have been resolved and how; 122 | - the path from where the assets are served; 123 | - whether the dev server is enabled and available. 124 | 125 | ![Debug bar panel](debug_panel.png) 126 | -------------------------------------------------------------------------------- /.docs/debug_panel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/contributte/webpack/3f4900b457853d8341cd6d352cb9ca86469236c6/.docs/debug_panel.png -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | indent_style = tab 11 | indent_size = tab 12 | tab_width = 4 13 | 14 | [{*.json, *.yaml, *.yml, *.md}] 15 | indent_style = space 16 | indent_size = 2 17 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017, Jiří Pudil 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 5 | 6 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 7 | 8 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 9 | 10 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 11 | 12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 13 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: install qa cs csf phpstan tests coverage-clover coverage-html 2 | 3 | install: 4 | composer update 5 | 6 | qa: phpstan cs 7 | 8 | cs: 9 | vendor/bin/ecs check src tests 10 | 11 | csf: 12 | vendor/bin/ecs check src tests --fix 13 | 14 | phpstan: 15 | vendor/bin/phpstan analyze -l 8 src 16 | 17 | tests: 18 | vendor/bin/tester -C tests 19 | 20 | coverage-clover: 21 | vendor/bin/tester -C --coverage coverage.xml --coverage-src src tests 22 | 23 | coverage-html: 24 | vendor/bin/tester -C --coverage coverage.hzml --coverage-src src tests 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![](https://heatbadger.now.sh/github/readme/contributte/webpack/) 2 | 3 |

4 | 5 | 6 | 7 | 8 |

9 |

10 | 11 | 12 | 13 | 14 | 15 |

16 | 17 |

18 | Website 🚀 contributte.org | Contact 👨🏻‍💻 f3l1x.io | Twitter 🐦 @contributte 19 |

20 | 21 | ## Usage 22 | 23 | To install the latest version of `contributte/webpack` use [Composer](https://getcomposer.org). 24 | 25 | ```bash 26 | composer require contributte/webpack 27 | ``` 28 | 29 | ## Documentation 30 | 31 | For details on how to use this package, check out our [documentation](.docs). 32 | 33 | 34 | ## Version 35 | 36 | | State | Version | Branch | Nette | PHP | 37 | |-------------|--------------|----------|----------|----------| 38 | | dev | `^2.0.x-dev` | `master` | `3.0+` | `>= 8.1` | 39 | | stable | `^2.0` | `master` | `3.0+` | `>= 8.1` | 40 | 41 | ## Development 42 | 43 | See [how to contribute](https://contributte.org/contributing.html) to this package. 44 | 45 | This package is currently maintaining by these authors. 46 | 47 | 48 | 49 | 50 | 51 | ----- 52 | 53 | Consider to [support](https://contributte.org/partners.html) **contributte** development team. 54 | Also thank you for using this package. 55 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "contributte/webpack", 3 | "description": "Webpack integration for Nette Framework.", 4 | "keywords": [ 5 | "nette", 6 | "webpack" 7 | ], 8 | "type": "library", 9 | "license": "BSD-3-Clause", 10 | "homepage": "https://github.com/contributte/webpack", 11 | "authors": [ 12 | { 13 | "name": "Jiří Pudil", 14 | "email": "me@jiripudil.cz", 15 | "homepage": "https://jiripudil.cz" 16 | } 17 | ], 18 | "support": { 19 | "email": "me@jiripudil.cz", 20 | "issues": "https://github.com/contributte/webpack/issues" 21 | }, 22 | "require": { 23 | "php": ">=8.1", 24 | "ext-curl": "*", 25 | "nette/di": "^3.0" 26 | }, 27 | "require-dev": { 28 | "latte/latte": "^2.4 || ^3.0", 29 | "nette/application": "^3.0", 30 | "nette/bootstrap": "^3.0", 31 | "nette/tester": "^2.0", 32 | "phpstan/phpstan": "^2.0", 33 | "symplify/easy-coding-standard": "^12.0", 34 | "tracy/tracy": "^2.4" 35 | }, 36 | "suggest": { 37 | "tracy/tracy": "to enable asset debugging in the debug bar panel", 38 | "latte/latte": "to enable Latte integration via {webpack} macro" 39 | }, 40 | "autoload": { 41 | "psr-4": { 42 | "Contributte\\Webpack\\": "src/" 43 | } 44 | }, 45 | "prefer-stable": true, 46 | "config": { 47 | "sort-packages": true, 48 | "allow-plugins": { 49 | "dealerdirect/phpcodesniffer-composer-installer": true 50 | } 51 | }, 52 | "extra": { 53 | "branch-alias": { 54 | "dev-master": "2.0.x-dev" 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/AssetLocator.php: -------------------------------------------------------------------------------- 1 | devServer->isAvailable() && \in_array($asset, $this->ignoredAssetNames, true)) { 27 | return 'data:,'; 28 | } 29 | 30 | $assetName = $this->assetResolver->resolveAssetName($asset); 31 | 32 | if ($this->isAbsoluteUrl($assetName)) { 33 | return $assetName; 34 | } 35 | 36 | return \rtrim($path, '/') . '/' . \ltrim($assetName, '/'); 37 | } 38 | 39 | public function locateInPublicPath(string $asset): string 40 | { 41 | return $this->locateInPath($this->publicPathProvider->getPublicPath(), $asset); 42 | } 43 | 44 | public function locateInBuildDirectory(string $asset): string 45 | { 46 | return $this->locateInPath($this->directoryProvider->getBuildDirectory(), $asset); 47 | } 48 | 49 | private function isAbsoluteUrl(string $url): bool 50 | { 51 | return \str_contains($url, '://') || \str_starts_with($url, '//'); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/AssetNameResolver/AssetNameResolverInterface.php: -------------------------------------------------------------------------------- 1 | */ 10 | private array $resolvedAssets = []; 11 | 12 | public function __construct( 13 | private readonly AssetNameResolverInterface $inner, 14 | ) { 15 | } 16 | 17 | public function resolveAssetName(string $asset): string 18 | { 19 | $resolved = $this->inner->resolveAssetName($asset); 20 | $this->resolvedAssets[] = [$asset, $resolved]; 21 | return $resolved; 22 | } 23 | 24 | /** 25 | * @return array 26 | */ 27 | public function getResolvedAssets(): array 28 | { 29 | return $this->resolvedAssets; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/AssetNameResolver/IdentityAssetNameResolver.php: -------------------------------------------------------------------------------- 1 | |null */ 13 | private ?array $manifestCache = null; 14 | 15 | public function __construct( 16 | private readonly string $manifestName, 17 | private readonly ManifestLoader $loader, 18 | ) { 19 | } 20 | 21 | public function resolveAssetName(string $asset): string 22 | { 23 | if ($this->manifestCache === null) { 24 | try { 25 | $this->manifestCache = $this->loader->loadManifest($this->manifestName); 26 | } catch (CannotLoadManifestException $e) { 27 | throw new CannotResolveAssetNameException('Failed to load manifest file.', 0, $e); 28 | } 29 | } 30 | 31 | if (!isset($this->manifestCache[$asset])) { 32 | throw new CannotResolveAssetNameException(\sprintf( 33 | "Asset '%s' was not found in the manifest file '%s'", 34 | $asset, 35 | $this->loader->getManifestPath($this->manifestName) 36 | )); 37 | } 38 | 39 | return $this->manifestCache[$asset]; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/AssetNameResolver/StaticAssetNameResolver.php: -------------------------------------------------------------------------------- 1 | $resolutions 11 | */ 12 | public function __construct( 13 | private readonly array $resolutions, 14 | ) { 15 | } 16 | 17 | public function resolveAssetName(string $asset): string 18 | { 19 | if (!isset($this->resolutions[$asset])) { 20 | throw new CannotResolveAssetNameException(\sprintf( 21 | "Asset '%s' was not found in the resolutions array", 22 | $asset 23 | )); 24 | } 25 | 26 | return $this->resolutions[$asset]; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/BasePath/BasePathProvider.php: -------------------------------------------------------------------------------- 1 | httpRequest->getUrl()->getBasePath(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/BuildDirectoryProvider.php: -------------------------------------------------------------------------------- 1 | devServer->isAvailable() 23 | ? $this->devServer->getInternalUrl() 24 | : $this->directory; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/DI/WebpackExtension.php: -------------------------------------------------------------------------------- 1 | $config 32 | */ 33 | final class WebpackExtension extends CompilerExtension 34 | { 35 | private readonly bool $debugMode; 36 | 37 | private readonly bool $consoleMode; 38 | 39 | public function __construct(bool $debugMode, ?bool $consoleMode = null) 40 | { 41 | $this->debugMode = $debugMode; 42 | $this->consoleMode = $consoleMode ?? \PHP_SAPI === 'cli'; 43 | } 44 | 45 | public function getConfigSchema(): Schema 46 | { 47 | return Expect::structure([ 48 | 'debugger' => Expect::bool($this->debugMode), 49 | 'macros' => Expect::bool(\interface_exists(LatteFactory::class)), 50 | 'devServer' => Expect::structure([ 51 | 'enabled' => Expect::bool($this->debugMode), 52 | 'url' => Expect::string()->nullable()->dynamic(), 53 | 'publicUrl' => Expect::string()->nullable()->dynamic(), 54 | 'timeout' => Expect::anyOf(Expect::float(), Expect::int())->default(0.1), 55 | 'ignoredAssets' => Expect::listOf(Expect::string())->default([]), 56 | ])->castTo('array') 57 | ->assert( 58 | static fn (array $devServer): bool => !$devServer['enabled'] || $devServer['url'] !== null, 59 | "The 'webpack › devServer › url' expects to be string, null given." 60 | ), 61 | 'build' => Expect::structure([ 62 | 'directory' => Expect::string()->required(), 63 | 'publicPath' => Expect::string()->required(), 64 | ])->castTo('array'), 65 | 'manifest' => Expect::structure([ 66 | 'name' => Expect::string()->nullable(), 67 | 'optimize' => Expect::bool(!$this->debugMode && (!$this->consoleMode || (bool) \getenv('CONTRIBUTTE_WEBPACK_OPTIMIZE_MANIFEST'))), 68 | 'mapper' => Expect::anyOf(Expect::string(), Expect::type(Statement::class))->default(WebpackManifestPluginMapper::class), 69 | 'timeout' => Expect::anyOf(Expect::float(), Expect::int())->default(1), 70 | ])->castTo('array'), 71 | ])->castTo('array'); 72 | } 73 | 74 | public function loadConfiguration(): void 75 | { 76 | $builder = $this->getContainerBuilder(); 77 | 78 | $basePathProvider = $builder->addDefinition($this->prefix('pathProvider.basePathProvider'), new ServiceDefinition()) 79 | ->setType(BasePathProvider::class) 80 | ->setFactory(NetteHttpBasePathProvider::class) 81 | ->setAutowired(false); 82 | 83 | $builder->addDefinition($this->prefix('pathProvider'), new ServiceDefinition()) 84 | ->setFactory(PublicPathProvider::class, [$this->config['build']['publicPath'], $basePathProvider]); 85 | 86 | $builder->addDefinition($this->prefix('buildDirProvider'), new ServiceDefinition()) 87 | ->setFactory(BuildDirectoryProvider::class, [$this->config['build']['directory']]); 88 | 89 | $builder->addDefinition($this->prefix('devServer'), new ServiceDefinition()) 90 | ->setFactory(DevServer::class, [ 91 | $this->config['devServer']['enabled'], 92 | $this->config['devServer']['url'] ?? '', 93 | $this->config['devServer']['publicUrl'], 94 | $this->config['devServer']['timeout'], 95 | new Statement(CurlClient::class), 96 | ]); 97 | 98 | $assetLocator = $builder->addDefinition($this->prefix('assetLocator'), new ServiceDefinition()) 99 | ->setFactory(AssetLocator::class, [ 100 | 'ignoredAssetNames' => $this->config['devServer']['ignoredAssets'], 101 | ]); 102 | 103 | $assetResolver = $this->setupAssetResolver($this->config); 104 | 105 | if ($this->config['debugger']) { 106 | $assetResolver->setAutowired(false); 107 | $builder->addDefinition($this->prefix('assetResolver.debug'), new ServiceDefinition()) 108 | ->setFactory(AssetNameResolver\DebuggerAwareAssetNameResolver::class, [$assetResolver]); 109 | } 110 | 111 | // latte macro 112 | if ($this->config['macros']) { 113 | try { 114 | $latteFactory = $builder->getDefinitionByType(LatteFactory::class); 115 | \assert($latteFactory instanceof FactoryDefinition); 116 | 117 | $definition = $latteFactory->getResultDefinition(); 118 | 119 | // @phpstan-ignore-next-line latte 2 compatibility 120 | if (\version_compare(Engine::VERSION, '3', '<')) { 121 | $definition->addSetup('?->addProvider(?, ?)', ['@self', 'webpackAssetLocator', $assetLocator]); 122 | $definition->addSetup('?->onCompile[] = function ($engine) { Contributte\Webpack\Latte\WebpackMacros::install($engine->getCompiler()); }', ['@self']); 123 | } else { 124 | $definition->addSetup('addExtension', [new Statement(\Contributte\Webpack\Latte\WebpackExtension::class)]); 125 | } 126 | } catch (MissingServiceException $e) { 127 | // ignore 128 | } 129 | } 130 | } 131 | 132 | public function beforeCompile(): void 133 | { 134 | $builder = $this->getContainerBuilder(); 135 | 136 | if ($this->config['debugger'] && \interface_exists(Tracy\IBarPanel::class)) { 137 | $definition = $builder->getDefinition($this->prefix('pathProvider')); 138 | \assert($definition instanceof ServiceDefinition); 139 | 140 | $definition->addSetup('@Tracy\Bar::addPanel', [ 141 | new Statement(WebpackPanel::class) 142 | ]); 143 | } 144 | } 145 | 146 | /** 147 | * @param array $config 148 | */ 149 | private function setupAssetResolver(array $config): ServiceDefinition 150 | { 151 | $builder = $this->getContainerBuilder(); 152 | 153 | $assetResolver = $builder->addDefinition($this->prefix('assetResolver'), new ServiceDefinition()) 154 | ->setType(AssetNameResolver\AssetNameResolverInterface::class); 155 | 156 | if ($config['manifest']['name'] !== null) { 157 | if (!$config['manifest']['optimize']) { 158 | $loader = $builder->addDefinition($this->prefix('manifestLoader'), new ServiceDefinition()) 159 | ->setFactory(ManifestLoader::class, [ 160 | 'manifestMapper' => new Statement($config['manifest']['mapper']), 161 | 'timeout' => $config['manifest']['timeout'], 162 | ]) 163 | ->setAutowired(false); 164 | 165 | $assetResolver->setFactory(AssetNameResolver\ManifestAssetNameResolver::class, [ 166 | $config['manifest']['name'], 167 | $loader 168 | ]); 169 | } else { 170 | $devServerInstance = new DevServer(false, '', '', 0.0, new CurlClient()); 171 | 172 | /** @var ManifestMapper $mapperInstance */ 173 | $mapperInstance = new $config['manifest']['mapper'](); 174 | 175 | $directoryProviderInstance = new BuildDirectoryProvider($config['build']['directory'], $devServerInstance); 176 | $loaderInstance = new ManifestLoader($directoryProviderInstance, $mapperInstance, $config['manifest']['timeout']); 177 | $manifestCache = $loaderInstance->loadManifest($config['manifest']['name']); 178 | 179 | $assetResolver->setFactory(AssetNameResolver\StaticAssetNameResolver::class, [$manifestCache]); 180 | 181 | // add dependency so that container is recompiled if manifest changes 182 | $manifestPath = $loaderInstance->getManifestPath($config['manifest']['name']); 183 | $this->compiler->addDependencies([$manifestPath]); 184 | } 185 | } else { 186 | $assetResolver->setFactory(AssetNameResolver\IdentityAssetNameResolver::class); 187 | } 188 | 189 | return $assetResolver; 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /src/Debugging/WebpackPanel.php: -------------------------------------------------------------------------------- 1 | devServer; 26 | $assets = $this->assetResolver->getResolvedAssets(); 27 | require __DIR__ . '/templates/WebpackPanel.tab.phtml'; 28 | return (string) \ob_get_clean(); 29 | } 30 | 31 | public function getPanel(): string 32 | { 33 | \ob_start(function (): void { 34 | }); 35 | $devServer = $this->devServer; 36 | $path = $this->pathProvider->getPublicPath(); 37 | $assets = $this->assetResolver->getResolvedAssets(); 38 | require __DIR__ . '/templates/WebpackPanel.panel.phtml'; 39 | return (string) \ob_get_clean(); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Debugging/templates/WebpackPanel.panel.phtml: -------------------------------------------------------------------------------- 1 | 9 | 10 | 42 | 43 |
44 |

Resolved assets:

45 |
46 |
47 |

48 | Dev server is 49 | isAvailable()) { 51 | echo 'available'; 52 | 53 | } elseif ($devServer->isEnabled()) { 54 | echo 'not available, falling back to build directory. Assets may be outdated.'; 55 | 56 | } else { 57 | echo 'disabled'; 58 | } 59 | ?> 60 |

61 | 62 |

Serving assets from

63 | 64 | 1): 66 | ?> 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 81 | 82 | 83 | 84 |
AssetResolution
79 | 80 |
85 | 88 |
89 |
90 |
91 | -------------------------------------------------------------------------------- /src/Debugging/templates/WebpackPanel.tab.phtml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | asset 14 | isEnabled() && ! $devServer->isAvailable()) { 16 | echo '!'; 17 | } 18 | ?> 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/DevServer/DevServer.php: -------------------------------------------------------------------------------- 1 | publicUrl ?? $this->url; 25 | } 26 | 27 | public function getInternalUrl(): string 28 | { 29 | return $this->url; 30 | } 31 | 32 | public function isEnabled(): bool 33 | { 34 | return $this->enabled; 35 | } 36 | 37 | public function isAvailable(): bool 38 | { 39 | if (!$this->isEnabled()) { 40 | return false; 41 | } 42 | 43 | return $this->available ??= $this->httpClient->isAvailable($this->url, $this->timeout); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/DevServer/Http/Client.php: -------------------------------------------------------------------------------- 1 | 'GET', 28 | \CURLOPT_PROTOCOLS => \CURLPROTO_HTTP | \CURLPROTO_HTTPS, 29 | 30 | // no output please 31 | \CURLOPT_RETURNTRANSFER => false, 32 | \CURLOPT_HEADER => false, 33 | \CURLOPT_FILE => \fopen('php://temp', 'w+'), 34 | 35 | // setup timeout; this requires NOSIGNAL for values below 1s 36 | \CURLOPT_TIMEOUT_MS => $timeout * 1000, 37 | \CURLOPT_NOSIGNAL => $timeout < 1 && \PHP_OS_FAMILY !== 'Windows', 38 | 39 | // allow self-signed certificates 40 | \CURLOPT_SSL_VERIFYHOST => 0, 41 | \CURLOPT_SSL_VERIFYPEER => false, 42 | ]); 43 | 44 | \curl_exec($curl); 45 | $error = \curl_error($curl); 46 | 47 | \curl_close($curl); 48 | return $error === ''; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/DevServer/Http/MockClient.php: -------------------------------------------------------------------------------- 1 | isAvailable; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Latte/WebpackExtension.php: -------------------------------------------------------------------------------- 1 | [WebpackNode::class, 'create'], 21 | ]; 22 | } 23 | 24 | public function getProviders(): array 25 | { 26 | return [ 27 | 'webpackAssetLocator' => $this->assetLocator, 28 | ]; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Latte/WebpackMacros.php: -------------------------------------------------------------------------------- 1 | addMacro('webpack', [$me, 'macroWebpackAsset']); 21 | } 22 | 23 | public function macroWebpackAsset(MacroNode $node, PhpWriter $writer): string 24 | { 25 | return $writer->write('echo %escape(%modify($this->global->webpackAssetLocator->locateInPublicPath(%node.word)))'); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Latte/WebpackNode.php: -------------------------------------------------------------------------------- 1 | outputMode = $tag::OutputKeepIndentation; 26 | $tag->expectArguments(); 27 | 28 | $node = new self(); 29 | $node->expression = $tag->parser->parseUnquotedStringOrExpression(); 30 | $node->modifier = $tag->parser->parseModifier(); 31 | $node->modifier->escape = true; 32 | return $node; 33 | } 34 | 35 | public function print(PrintContext $context): string 36 | { 37 | return $context->format( 38 | "echo %modify(\$this->global->webpackAssetLocator->locateInPublicPath(%node)) %line;\n", 39 | $this->modifier, 40 | $this->expression, 41 | $this->position, 42 | ); 43 | } 44 | 45 | /** 46 | * @return \Generator 47 | */ 48 | public function &getIterator(): \Generator 49 | { 50 | yield $this->expression; 51 | yield $this->modifier; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Manifest/CannotLoadManifestException.php: -------------------------------------------------------------------------------- 1 | 24 | */ 25 | public function loadManifest(string $fileName): array 26 | { 27 | $path = $this->getManifestPath($fileName); 28 | 29 | if (\is_file($path)) { 30 | $manifest = @\file_get_contents($path); // @ - errors handled by custom exception 31 | } else { 32 | $ch = \curl_init($path); 33 | 34 | if ($ch === false) { 35 | $manifest = false; 36 | } else { 37 | \curl_setopt_array($ch, [ 38 | \CURLOPT_CUSTOMREQUEST => 'GET', 39 | \CURLOPT_PROTOCOLS => \CURLPROTO_HTTP | \CURLPROTO_HTTPS, 40 | 41 | \CURLOPT_RETURNTRANSFER => true, 42 | \CURLOPT_FAILONERROR => true, 43 | 44 | // setup timeout; this requires NOSIGNAL for values below 1s 45 | \CURLOPT_TIMEOUT_MS => $this->timeout * 1000, 46 | \CURLOPT_NOSIGNAL => $this->timeout < 1 && \PHP_OS_FAMILY !== 'Windows', 47 | 48 | // allow self-signed certificates 49 | \CURLOPT_SSL_VERIFYHOST => 0, 50 | \CURLOPT_SSL_VERIFYPEER => false, 51 | ]); 52 | /** @var string|false $manifest */ 53 | $manifest = \curl_exec($ch); 54 | 55 | if ($manifest === false) { 56 | $errorMessage = \curl_error($ch); 57 | } 58 | 59 | \curl_close($ch); 60 | } 61 | } 62 | 63 | if ($manifest === false) { 64 | throw new CannotLoadManifestException(\sprintf( 65 | "Manifest file '%s' could not be loaded: %s", 66 | $path, 67 | $errorMessage ?? \error_get_last()['message'] ?? 'unknown error', 68 | )); 69 | } 70 | 71 | return $this->manifestMapper->map(\json_decode($manifest, flags: \JSON_THROW_ON_ERROR | \JSON_OBJECT_AS_ARRAY)); 72 | } 73 | 74 | public function getManifestPath(string $fileName): string 75 | { 76 | return $this->directoryProvider->getBuildDirectory() . '/' . $fileName; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/Manifest/ManifestMapper.php: -------------------------------------------------------------------------------- 1 | $manifest 16 | * @return array 17 | */ 18 | abstract public function map(array $manifest): array; 19 | } 20 | -------------------------------------------------------------------------------- /src/Manifest/Mapper/AssetsWebpackPluginMapper.php: -------------------------------------------------------------------------------- 1 | $parts) { 34 | foreach ($parts as $name => $file) { 35 | $result[$main . '.' . $name] = $file; 36 | } 37 | } 38 | return $result; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Manifest/Mapper/WebpackManifestPluginMapper.php: -------------------------------------------------------------------------------- 1 | devServer->isAvailable() 25 | ? $this->devServer->getUrl() 26 | : \rtrim($this->basePathProvider->getBasePath(), '/') . '/' . \trim($this->path, '/'); 27 | } 28 | } 29 | --------------------------------------------------------------------------------