diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c0de3a..deb1f9f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## 0.2.1 Under development +- feat: add `create()` factories for renderers and Vite configurations, and use them in examples and tests. + ## 0.2.0 August 25, 2026 - docs: add `Next steps` section with links to installation, usage, configuration, and testing guides. diff --git a/README.md b/README.md index b9a8378..0fd7761 100644 --- a/README.md +++ b/README.md @@ -65,13 +65,13 @@ use PHPForge\Vite\Html\HtmlRenderer; use PHPForge\Vite\Vite; $vite = Vite::create( - new DevelopmentConfiguration( + DevelopmentConfiguration::create( devServerUrl: 'http://localhost:5173', ), entrypoints: ['resources/js/app.js'], ); -echo (new HtmlRenderer())->render($vite->resolve()); +echo HtmlRenderer::create()->render($vite->resolve()); ``` ### Production @@ -82,14 +82,14 @@ use PHPForge\Vite\Html\HtmlRenderer; use PHPForge\Vite\Vite; $vite = Vite::create( - new ProductionConfiguration( + ProductionConfiguration::create( manifestPath: '/srv/app/public/build/.vite/manifest.json', assetBaseUrl: '/build', ), entrypoints: ['resources/js/app.js'], ); -echo (new HtmlRenderer())->render($vite->resolve()); +echo HtmlRenderer::create()->render($vite->resolve()); ``` ## Documentation diff --git a/docs/configuration.md b/docs/configuration.md index 099f767..60f562b 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -8,7 +8,7 @@ Configuration objects are immutable and accept only resolved filesystem paths an ```php use PHPForge\Vite\Configuration\DevelopmentConfiguration; -$configuration = new DevelopmentConfiguration( +$configuration = DevelopmentConfiguration::create( devServerUrl: 'http://localhost:5173', includeViteClient: true, inlineModuleProviders: [], @@ -28,7 +28,7 @@ The URL may include a path prefix, but not a query or fragment. ```php use PHPForge\Vite\Configuration\ProductionConfiguration; -$configuration = new ProductionConfiguration( +$configuration = ProductionConfiguration::create( manifestPath: '/srv/app/public/build/.vite/manifest.json', assetBaseUrl: '/build', modulePreload: true, @@ -98,7 +98,7 @@ Resolution does not produce HTML. Use `HtmlRenderer` only when the application w use PHPForge\Vite\Html\HtmlRenderer; use PHPForge\Vite\Html\HtmlRenderOptions; -$html = (new HtmlRenderer())->render( +$html = HtmlRenderer::create()->render( $vite->resolve(), HtmlRenderOptions::create() ->withNonce($nonce) diff --git a/docs/examples.md b/docs/examples.md index b9f5095..d1de6b8 100644 --- a/docs/examples.md +++ b/docs/examples.md @@ -34,10 +34,10 @@ use PHPForge\Vite\Html\HtmlRenderer; use PHPForge\Vite\Vite; $configuration = $isDevelopment - ? new DevelopmentConfiguration( + ? DevelopmentConfiguration::create( devServerUrl: 'http://localhost:5173', ) - : new ProductionConfiguration( + : ProductionConfiguration::create( manifestPath: __DIR__ . '/public/build/.vite/manifest.json', assetBaseUrl: '/build', ); @@ -46,7 +46,7 @@ $vite = Vite::create($configuration, entrypoints: ['resources/js/app.js']); $assets = $vite->resolve(); -echo (new HtmlRenderer())->render($assets); +echo HtmlRenderer::create()->render($assets); ``` The production example assumes `__DIR__` is the absolute project root used by the matching Vite configuration. @@ -90,10 +90,10 @@ $config = [ 'class' => Vite::class, '__construct()' => [ 'configuration' => YII_ENV === 'dev' - ? new DevelopmentConfiguration( + ? DevelopmentConfiguration::create( devServerUrl: 'http://localhost:5173', ) - : new ProductionConfiguration( + : ProductionConfiguration::create( manifestPath: dirname(__DIR__) . '/public/build/.vite/manifest.json', assetBaseUrl: '/build', ), @@ -106,7 +106,7 @@ $config = [ /** @var Vite $vite */ $vite = Yii::$app->get('vite'); -echo (new HtmlRenderer())->render($vite->resolve()); +echo HtmlRenderer::create()->render($vite->resolve()); ``` The `__construct()` entry is Yii2 container syntax. Its values are passed to the framework-independent constructor, and @@ -123,7 +123,7 @@ use Yiisoft\Aliases\Aliases; static function (Aliases $aliases): Vite { return Vite::create( - new ProductionConfiguration( + ProductionConfiguration::create( manifestPath: $aliases->get('@public/build/.vite/manifest.json'), assetBaseUrl: '/build', ), @@ -162,7 +162,7 @@ final class ReactRefreshPreamble implements InlineModuleProviderInterface } } -$configuration = new DevelopmentConfiguration( +$configuration = DevelopmentConfiguration::create( devServerUrl: 'http://localhost:5173', inlineModuleProviders: [new ReactRefreshPreamble()], ); diff --git a/docs/security.md b/docs/security.md index 38ed1c1..d47fec0 100644 --- a/docs/security.md +++ b/docs/security.md @@ -51,7 +51,7 @@ $nonce = base64_encode(random_bytes(18)); header("Content-Security-Policy: script-src 'nonce-{$nonce}' 'strict-dynamic'; object-src 'none'; base-uri 'none'"); -$html = (new HtmlRenderer())->render( +$html = HtmlRenderer::create()->render( $vite->resolve(), HtmlRenderOptions::create()->withNonce($nonce), ); diff --git a/src/Configuration/DevelopmentConfiguration.php b/src/Configuration/DevelopmentConfiguration.php index bd5d749..920c15e 100644 --- a/src/Configuration/DevelopmentConfiguration.php +++ b/src/Configuration/DevelopmentConfiguration.php @@ -42,6 +42,25 @@ public function __construct( $this->inlineModuleProviders = $this->normalizeProviders($inlineModuleProviders); } + /** + * Creates a development-server configuration. + * + * @param string $devServerUrl Absolute HTTP(S) URL of the running Vite development server. + * @param bool $includeViteClient Whether the `@vite/client` module script is emitted. + * @param list $inlineModuleProviders Providers of application-owned inline modules. + * + * @throws ConfigurationException if the development-server URL is invalid, or if a provider is unsupported. + * + * @return self A new development-server configuration. + */ + public static function create( + string $devServerUrl, + bool $includeViteClient = true, + array $inlineModuleProviders = [], + ): self { + return new self($devServerUrl, $includeViteClient, $inlineModuleProviders); + } + /** * Rejects any provider that does not satisfy the contract, and reindexes the survivors as a list. * diff --git a/src/Configuration/ProductionConfiguration.php b/src/Configuration/ProductionConfiguration.php index 7d51775..0e32281 100644 --- a/src/Configuration/ProductionConfiguration.php +++ b/src/Configuration/ProductionConfiguration.php @@ -35,4 +35,23 @@ public function __construct(string $manifestPath, string $assetBaseUrl, public b $this->manifestPath = Path::requireAbsolute($manifestPath, 'manifestPath'); $this->assetBaseUrl = Url::normalizeAssetBaseUrl($assetBaseUrl); } + + /** + * Creates a production-manifest configuration. + * + * @param string $manifestPath Absolute path to the manifest emitted by the Vite build. + * @param string $assetBaseUrl Public base URL of the build output, absolute or relative. + * @param bool $modulePreload Whether `modulepreload` hints are emitted for transitive imports. + * + * @throws ConfigurationException if the manifest path or base URL is invalid. + * + * @return self A new production-manifest configuration. + */ + public static function create( + string $manifestPath, + string $assetBaseUrl, + bool $modulePreload = true, + ): self { + return new self($manifestPath, $assetBaseUrl, $modulePreload); + } } diff --git a/src/Html/HtmlRenderer.php b/src/Html/HtmlRenderer.php index 8460846..d5b0bd0 100644 --- a/src/Html/HtmlRenderer.php +++ b/src/Html/HtmlRenderer.php @@ -37,6 +37,16 @@ final class HtmlRenderer 'type' => true, ]; + /** + * Creates an HTML renderer. + * + * @return self A new HTML renderer. + */ + public static function create(): self + { + return new self(); + } + /** * Renders a collection of neutral assets as HTML5 tags joined by the configured separator. * diff --git a/tests/ConfigurationTest.php b/tests/ConfigurationTest.php index 1578b0e..c479001 100644 --- a/tests/ConfigurationTest.php +++ b/tests/ConfigurationTest.php @@ -32,6 +32,48 @@ public function testAssetValueAcceptsCaseInsensitiveHttpScheme(): void ); } + public function testConfigurationFactoriesCreateConfiguredInstances(): void + { + $manifestPath = __DIR__ . '/Fixture/manifest.json'; + + $provider = new CapturingInlineModuleProviderStub(); + + $development = DevelopmentConfiguration::create( + 'http://localhost:5173', + false, + [$provider], + ); + $production = ProductionConfiguration::create( + $manifestPath, + '/build', + false, + ); + + self::assertSame( + 'http://localhost:5173', + $development->devServerUrl, + 'The development factory must preserve the normalized server URL.', + ); + self::assertFalse( + $development->includeViteClient, + 'The development factory must preserve the client setting.', + ); + self::assertSame( + [$provider], + $development->inlineModuleProviders, + 'The development factory must preserve inline module providers.', + ); + self::assertSame( + $manifestPath, + $production->manifestPath, + 'The production factory must preserve the manifest path.', + ); + self::assertFalse( + $production->modulePreload, + 'The production factory must preserve the preload setting.', + ); + } + public function testCreateReturnsConfiguredViteFacade(): void { $manifestPath = __DIR__ . '/Fixture/manifest.json'; @@ -41,7 +83,7 @@ public function testCreateReturnsConfiguredViteFacade(): void $cachedManifest = $loader->load($manifestPath); $vite = Vite::create( - new ProductionConfiguration($manifestPath, '/build'), + ProductionConfiguration::create($manifestPath, '/build'), ['views/foo.js'], $loader, ); @@ -64,11 +106,22 @@ public function testCreateReturnsConfiguredViteFacade(): void ); } + public function testDevelopmentConfigurationEnablesViteClientByDefault(): void + { + $configuration = new DevelopmentConfiguration('http://localhost:5173'); + + self::assertTrue( + $configuration->includeViteClient, + 'The development configuration must enable the Vite client by default.', + ); + } + public function testDevelopmentConfigurationNormalizesValues(): void { $firstProvider = new CapturingInlineModuleProviderStub(); $secondProvider = new CapturingInlineModuleProviderStub(); - $configuration = new DevelopmentConfiguration( + + $configuration = DevelopmentConfiguration::create( devServerUrl: ' HTTPS://localhost:5173/vite/ ', inlineModuleProviders: [$firstProvider, $secondProvider], ); @@ -89,9 +142,19 @@ public function testDevelopmentConfigurationNormalizesValues(): void ); } + public function testProductionConfigurationEnablesModulePreloadByDefault(): void + { + $configuration = new ProductionConfiguration(__DIR__ . '/Fixture/manifest.json', '/build'); + + self::assertTrue( + $configuration->modulePreload, + 'The production configuration must enable module preloading by default.', + ); + } + public function testProductionConfigurationNormalizesBaseUrl(): void { - $configuration = new ProductionConfiguration( + $configuration = ProductionConfiguration::create( manifestPath: __DIR__ . '/Fixture/manifest.json', assetBaseUrl: ' HTTPS://cdn.example.com/build/ ', ); @@ -115,7 +178,7 @@ public function testThrowConfigurationExceptionForInvalidDevelopmentServerUrl(st $message->getMessage(), ); - new DevelopmentConfiguration($url); + DevelopmentConfiguration::create($url); } public function testThrowConfigurationExceptionForInvalidInlineModuleProvider(): void @@ -125,7 +188,7 @@ public function testThrowConfigurationExceptionForInvalidInlineModuleProvider(): Message::DEVELOPMENT_INLINE_MODULE_PROVIDER_INVALID->getMessage(), ); - new DevelopmentConfiguration('http://localhost:5173', inlineModuleProviders: [new stdClass()]); + DevelopmentConfiguration::create('http://localhost:5173', inlineModuleProviders: [new stdClass()]); } public function testThrowConfigurationExceptionForNonAbsoluteManifestPath(): void @@ -135,7 +198,7 @@ public function testThrowConfigurationExceptionForNonAbsoluteManifestPath(): voi Message::FILESYSTEM_PATH_INVALID->getMessage('manifestPath'), ); - new ProductionConfiguration('@webroot/build/.vite/manifest.json', '/build'); + ProductionConfiguration::create('@webroot/build/.vite/manifest.json', '/build'); } #[DataProviderExternal(ConfigurationProvider::class, 'unsafeAssetUrls')] @@ -157,7 +220,7 @@ public function testThrowConfigurationExceptionForUnsafeProductionBaseUrl(string $message->getMessage(), ); - new ProductionConfiguration(__DIR__ . '/Fixture/manifest.json', $url); + ProductionConfiguration::create(__DIR__ . '/Fixture/manifest.json', $url); } public function testThrowConfigurationExceptionForWhitespaceInlineModuleSource(): void @@ -180,7 +243,7 @@ public function testThrowInvalidEntrypointExceptionForInvalidRelativeSourcePath( $message->getMessage(), ); - new Vite(new DevelopmentConfiguration('http://localhost:5173'), [$entrypoint]); + Vite::create(DevelopmentConfiguration::create('http://localhost:5173'), [$entrypoint]); } public function testThrowInvalidEntrypointExceptionForNonStringEntrypoint(): void @@ -190,6 +253,6 @@ public function testThrowInvalidEntrypointExceptionForNonStringEntrypoint(): voi Message::ENTRYPOINT_TYPE_INVALID->getMessage(), ); - new Vite(new DevelopmentConfiguration('http://localhost:5173'), [123]); + Vite::create(DevelopmentConfiguration::create('http://localhost:5173'), [123]); } } diff --git a/tests/HtmlRendererTest.php b/tests/HtmlRendererTest.php index 89dc5a0..afef848 100644 --- a/tests/HtmlRendererTest.php +++ b/tests/HtmlRendererTest.php @@ -57,7 +57,7 @@ public function testFactoryCreatesDefaultOptions(): void public function testInlineModuleNeutralizesClosingScriptSequence(): void { - $html = (new HtmlRenderer())->render( + $html = HtmlRenderer::create()->render( new AssetCollection([new InlineModule('window.html = "

";')]), ); @@ -83,7 +83,7 @@ public function testNonceIsAppliedToEveryGeneratedTag(): void new ModulePreload('/build/vendor.js'), ], ); - $html = (new HtmlRenderer())->render( + $html = HtmlRenderer::create()->render( $assets, HtmlRenderOptions::create()->withNonce('c2VjdXJlLW5vbmNl'), ); @@ -336,7 +336,7 @@ public function testRendererEscapesUrlsAndCustomAttributes(): void HTML, - (new HtmlRenderer())->render(new AssetCollection([$script]), $options), + HtmlRenderer::create()->render(new AssetCollection([$script]), $options), 'URLs, values, booleans, and omitted attributes must be encoded safely.', ); } @@ -358,7 +358,7 @@ public function testRendererPreservesCollectionOrder(): void HTML, - (new HtmlRenderer())->render($assets), + HtmlRenderer::create()->render($assets), 'Rendered tags must follow collection order.', ); } @@ -366,7 +366,7 @@ public function testRendererPreservesCollectionOrder(): void public function testRendererSupportsCustomSeparator(): void { $assets = new AssetCollection([new ModuleScript('/one.js'), new ModuleScript('/two.js')]); - $html = (new HtmlRenderer())->render($assets, HtmlRenderOptions::create()->withSeparator('')); + $html = HtmlRenderer::create()->render($assets, HtmlRenderOptions::create()->withSeparator('')); self::assertSame( <<withModuleScriptAttributes($attributes); - (new HtmlRenderer())->render(new AssetCollection([new ModuleScript('/app.js')]), $options); + HtmlRenderer::create()->render(new AssetCollection([new ModuleScript('/app.js')]), $options); } public function testThrowHtmlRenderingExceptionForUnsupportedAssetAttributes(): void diff --git a/tests/ViteDevelopmentTest.php b/tests/ViteDevelopmentTest.php index 3358e36..1cf3d60 100644 --- a/tests/ViteDevelopmentTest.php +++ b/tests/ViteDevelopmentTest.php @@ -21,8 +21,9 @@ final class ViteDevelopmentTest extends TestCase public function testDevelopmentAssetsUseDocumentedOrder(): void { $provider = new CapturingInlineModuleProviderStub(); - $vite = new Vite( - new DevelopmentConfiguration( + + $vite = Vite::create( + DevelopmentConfiguration::create( devServerUrl: 'http://localhost:5173/', inlineModuleProviders: [$provider], ), @@ -85,8 +86,8 @@ public function testDevelopmentAssetsUseDocumentedOrder(): void public function testDevelopmentCanOmitViteClient(): void { - $vite = new Vite( - new DevelopmentConfiguration( + $vite = Vite::create( + DevelopmentConfiguration::create( devServerUrl: 'http://localhost:5173', includeViteClient: false, ), @@ -109,8 +110,8 @@ public function testDevelopmentCanOmitViteClient(): void public function testDevelopmentServerPathPrefixIsPreserved(): void { - $vite = new Vite( - new DevelopmentConfiguration( + $vite = Vite::create( + DevelopmentConfiguration::create( devServerUrl: 'https://assets.example.com/vite/', ), entrypoints: ['resources/js/app.js'], @@ -131,7 +132,7 @@ public function testDevelopmentServerPathPrefixIsPreserved(): void public function testResolveAcceptsStringOverrideAndNormalizesLeadingSlash(): void { - $vite = new Vite(new DevelopmentConfiguration('http://localhost:5173')); + $vite = Vite::create(DevelopmentConfiguration::create('http://localhost:5173')); $scripts = $vite->resolve('/resources/js/app.js')->moduleScripts(); @@ -147,7 +148,7 @@ public function testResolveAcceptsStringOverrideAndNormalizesLeadingSlash(): voi public function testResolveDeduplicatesOverrideEntrypoints(): void { - $vite = new Vite(new DevelopmentConfiguration('http://localhost:5173')); + $vite = Vite::create(DevelopmentConfiguration::create('http://localhost:5173')); $scripts = $vite->resolve(['resources/js/app.js', '/resources/js/app.js'])->moduleScripts(); @@ -160,7 +161,7 @@ public function testResolveDeduplicatesOverrideEntrypoints(): void public function testThrowInvalidEntrypointExceptionWhenNoEntrypointIsConfigured(): void { - $vite = new Vite(new DevelopmentConfiguration('http://localhost:5173')); + $vite = Vite::create(DevelopmentConfiguration::create('http://localhost:5173')); $this->expectException(InvalidEntrypointException::class); $this->expectExceptionMessage( diff --git a/tests/ViteProductionTest.php b/tests/ViteProductionTest.php index b0fc1b7..cc64983 100644 --- a/tests/ViteProductionTest.php +++ b/tests/ViteProductionTest.php @@ -20,12 +20,14 @@ final class ViteProductionTest extends TestCase { public function testAbsoluteBaseUrlProducesCdnAssetUrls(): void { - $configuration = new ProductionConfiguration( + $configuration = ProductionConfiguration::create( manifestPath: __DIR__ . '/Fixture/css-entrypoint-manifest.json', assetBaseUrl: 'https://cdn.example.com/build/', ); - $stylesheets = (new Vite($configuration, ['resources/css/app.css']))->resolve()->stylesheets(); + $stylesheets = Vite::create($configuration, ['resources/css/app.css']) + ->resolve() + ->stylesheets(); self::assertSame( ['https://cdn.example.com/build/assets/app-abc123.css'], @@ -52,10 +54,13 @@ public function testCircularImportDoesNotPreloadRootEntrypoint(): void public function testClearManifestCacheDiscardsTheProductionManifest(): void { $manifestPath = __DIR__ . '/Fixture/manifest.json'; + $loader = new ManifestLoader(); + $cachedManifest = $loader->load($manifestPath); - $vite = new Vite( - new ProductionConfiguration($manifestPath, '/build'), + + $vite = Vite::create( + ProductionConfiguration::create($manifestPath, '/build'), ['views/foo.js'], $loader, ); @@ -93,12 +98,14 @@ public function testDynamicImportsAreNotIncludedInInitialAssets(): void public function testEmptyBaseUrlProducesRelativeAssetUrls(): void { - $configuration = new ProductionConfiguration( + $configuration = ProductionConfiguration::create( manifestPath: __DIR__ . '/Fixture/css-entrypoint-manifest.json', assetBaseUrl: '', ); - $stylesheets = (new Vite($configuration, ['resources/css/app.css']))->resolve()->stylesheets(); + $stylesheets = Vite::create($configuration, ['resources/css/app.css']) + ->resolve() + ->stylesheets(); self::assertSame( ['assets/app-abc123.css'], @@ -171,13 +178,14 @@ public function testLeadingSlashAssetPathsAreNormalized(): void public function testModulePreloadCanBeDisabled(): void { - $configuration = new ProductionConfiguration( + $configuration = ProductionConfiguration::create( manifestPath: __DIR__ . '/Fixture/manifest.json', assetBaseUrl: '/build', modulePreload: false, ); - $assets = (new Vite($configuration, ['views/foo.js']))->resolve(); + $assets = Vite::create($configuration, ['views/foo.js']) + ->resolve(); self::assertSame( [], @@ -244,11 +252,13 @@ public function testResolveAcceptsStringOverride(): void public function testRootBaseUrlProducesRootRelativeAssetUrls(): void { - $configuration = new ProductionConfiguration( + $configuration = ProductionConfiguration::create( manifestPath: __DIR__ . '/Fixture/css-entrypoint-manifest.json', assetBaseUrl: '/', ); - $stylesheets = (new Vite($configuration, ['resources/css/app.css']))->resolve()->stylesheets(); + $stylesheets = Vite::create($configuration, ['resources/css/app.css']) + ->resolve() + ->stylesheets(); self::assertSame( ['/assets/app-abc123.css'], @@ -317,8 +327,8 @@ private function describe(AssetCollection $collection): array */ private function vite(string $fixture, array $entrypoints): Vite { - return new Vite( - new ProductionConfiguration( + return Vite::create( + ProductionConfiguration::create( manifestPath: __DIR__ . '/Fixture/' . $fixture, assetBaseUrl: '/build', ),