diff --git a/modules/cms/tests/fixtures/npm/package-mixtheme.json b/modules/cms/tests/fixtures/npm/package-mixtheme.json new file mode 100644 index 0000000000..0a1353fc94 --- /dev/null +++ b/modules/cms/tests/fixtures/npm/package-mixtheme.json @@ -0,0 +1,11 @@ +{ + "type": "module", + "workspaces": { + "packages": [ + "modules/cms/tests/fixtures/themes/mixtest" + ] + }, + "devDependencies": { + "laravel-mix": "^6.0.41" + } +} diff --git a/modules/cms/tests/fixtures/themes/mixtest/assets/src/css/theme.css b/modules/cms/tests/fixtures/themes/mixtest/assets/src/css/theme.css new file mode 100644 index 0000000000..d224431f16 --- /dev/null +++ b/modules/cms/tests/fixtures/themes/mixtest/assets/src/css/theme.css @@ -0,0 +1,3 @@ +h1 { + color: red; +} diff --git a/modules/cms/tests/fixtures/themes/mixtest/winter.mix.js b/modules/cms/tests/fixtures/themes/mixtest/winter.mix.js new file mode 100644 index 0000000000..280c474e4b --- /dev/null +++ b/modules/cms/tests/fixtures/themes/mixtest/winter.mix.js @@ -0,0 +1,10 @@ +const mix = require('laravel-mix'); + +mix.setPublicPath(__dirname) + .options({ + manifest: true, + }) + .version() + + // Render Tailwind style + .postCss('assets/src/css/theme.css', 'assets/dist/css/theme.css') diff --git a/modules/cms/tests/twig/MixFilterTest.php b/modules/cms/tests/twig/MixFilterTest.php new file mode 100644 index 0000000000..918c6df6fa --- /dev/null +++ b/modules/cms/tests/twig/MixFilterTest.php @@ -0,0 +1,73 @@ +markTestSkipped('This test requires node_modules to be installed'); + } + + if (!is_file(base_path('node_modules/.bin/mix'))) { + $this->markTestSkipped('This test requires the mix package to be installed'); + } + + $this->originalThemesPath = Config::get('cms.themesPath'); + Config::set('cms.themesPath', '/modules/cms/tests/fixtures/themes'); + + $this->themePath = base_path('modules/cms/tests/fixtures/themes/mixtest'); + + Config::set('cms.activeTheme', 'mixtest'); + + Event::flush('cms.theme.getActiveTheme'); + Theme::resetCache(); + } + + protected function tearDown(): void + { + File::deleteDirectory('modules/cms/tests/fixtures/themes/mixtest/assets/dist'); + File::delete('modules/cms/tests/fixtures/themes/mixtest/mix-manifest.json'); + + Config::set('cms.themesPath', $this->originalThemesPath); + + parent::tearDown(); + } + + public function testGeneratesAssetUrl(): void + { + $theme = Theme::getActiveTheme(); + + $this->artisan('mix:compile', [ + 'theme-mixtest', + '--manifest' => 'modules/cms/tests/fixtures/npm/package-mixtheme.json', + '--disable-tty' => true, + ])->assertExitCode(0); + + $this->assertFileExists($theme->getPath($theme->getDirName() . '/mix-manifest.json')); + + $controller = Controller::getController() ?: new Controller(); + + $extension = new Extension(); + $extension->setController($controller); + + $this->app->make('twig.environment') + ->addExtension($extension); + + $contents = Twig::parse("{{ 'assets/dist/css/theme.css' | mix }}"); + + $this->assertStringContainsString('/assets/dist/css/theme.css?id=', $contents); + } +} diff --git a/modules/cms/twig/Extension.php b/modules/cms/twig/Extension.php index 01ed17438e..20c3dc9004 100644 --- a/modules/cms/twig/Extension.php +++ b/modules/cms/twig/Extension.php @@ -3,6 +3,7 @@ use Block; use Cms\Classes\Controller; use Event; +use System\Classes\Asset\Mix; use System\Classes\Asset\Vite; use Twig\Extension\AbstractExtension as TwigExtension; use Twig\TwigFilter as TwigSimpleFilter; @@ -68,6 +69,7 @@ public function getFilters(): array return [ new TwigSimpleFilter('page', [$this, 'pageFilter'], $options), new TwigSimpleFilter('theme', [$this, 'themeFilter'], $options), + new TwigSimpleFilter('mix', [Mix::class, 'mix'], $options), ]; } diff --git a/modules/system/classes/asset/Mix.php b/modules/system/classes/asset/Mix.php new file mode 100644 index 0000000000..d79884b213 --- /dev/null +++ b/modules/system/classes/asset/Mix.php @@ -0,0 +1,56 @@ +getConfigValue('mix_manifest_path', '/'), '/'); + } + + $manifestPath = $theme->getPath($theme->getDirName() . '/' . $manifestDirectory . '/mix-manifest.json'); + + if (!isset($manifests[$manifestPath])) { + if (!is_file($manifestPath)) { + throw new Exception('The Mix manifest does not exist.'); + } + + $manifests[$manifestPath] = json_decode(file_get_contents($manifestPath), true); + } + + $manifest = $manifests[$manifestPath]; + + if (!isset($manifest[$path])) { + $exception = new Exception("Unable to locate Mix file: $path"); + + if (!app('config')->get('app.debug')) { + report($exception); + + return $path; + } else { + throw $exception; + } + } + + $url = Config::get('cms.themesPath', '/themes') . '/' . $theme->getDirName() . $manifest[$path]; + + return new HtmlString(Url::asset($url)); + } +} diff --git a/modules/system/tests/classes/asset/MixTest.php b/modules/system/tests/classes/asset/MixTest.php new file mode 100644 index 0000000000..9b88038388 --- /dev/null +++ b/modules/system/tests/classes/asset/MixTest.php @@ -0,0 +1,161 @@ +markTestSkipped('This test requires node_modules to be installed'); + } + + if (!is_file(base_path('node_modules/.bin/mix'))) { + $this->markTestSkipped('This test requires the mix package to be installed'); + } + + $this->originalThemesPath = Config::get('cms.themesPath'); + Config::set('cms.themesPath', '/modules/system/tests/fixtures/themes'); + + $this->originalThemesPathLocal = Config::get('cms.themesPathLocal'); + Config::set('cms.themesPathLocal', base_path('modules/system/tests/fixtures/themes')); + $this->app->setThemesPath(Config::get('cms.themesPathLocal')); + + $this->themePath = base_path('modules/system/tests/fixtures/themes/mixtest'); + + Config::set('cms.activeTheme', 'mixtest'); + + Event::flush('cms.theme.getActiveTheme'); + Theme::resetCache(); + } + + protected function tearDown(): void + { + File::deleteDirectory('modules/system/tests/fixtures/themes/mixtest/assets/dist'); + File::delete('modules/system/tests/fixtures/themes/mixtest/mix-manifest.json'); + + Config::set('cms.themesPath', $this->originalThemesPath); + + Config::set('cms.themesPathLocal', $this->originalThemesPathLocal); + $this->app->setThemesPath($this->originalThemesPathLocal); + + parent::tearDown(); + } + + public function testThrowsExceptionWhenMixManifestIsMissing(): void + { + $this->expectException(\Exception::class); + $this->expectExceptionMessage('The Mix manifest does not exist'); + + Mix::mix('assets/dist/foo.css'); + } + + public function testGeneratesAssetUrls(): void + { + $this->artisan('mix:compile', [ + 'theme-mixtest', + '--manifest' => 'modules/system/tests/fixtures/npm/package-mixtest.json', + '--disable-tty' => true, + ])->assertExitCode(0); + + $theme = Theme::getActiveTheme(); + + $this->assertFileExists($theme->getPath($theme->getDirName() . '/mix-manifest.json')); + + $manifest = json_decode(file_get_contents($theme->getPath($theme->getDirName() . '/mix-manifest.json')), true); + + foreach ($manifest as $key => $value) { + $mixAssetUrl = Mix::mix($key); + + $this->assertStringStartsWith( + Url::asset(Config::get('cms.themesPath', '/themes') . '/' . $theme->getDirName()), + $mixAssetUrl + ); + } + } + + public function testThemeCanOverrideMixManifestPath(): void + { + $theme = Theme::getActiveTheme(); + + Event::listen('cms.theme.extendConfig', function ($dirName, &$config) { + $config['mix_manifest_path'] = 'assets/dist'; + }); + + rename( + $theme->getPath($theme->getDirName() . '/winter.mix.js'), + $theme->getPath($theme->getDirName() . '/winter.mix.js.bak') + ); + + copy( + $theme->getPath($theme->getDirName() . '/winter.mix-manifest-override.js'), + $theme->getPath($theme->getDirName() . '/winter.mix.js') + ); + + try { + $this->artisan('mix:compile', [ + 'theme-mixtest', + '--manifest' => 'modules/system/tests/fixtures/npm/package-mixtest.json', + '--disable-tty' => true, + ])->assertExitCode(0); + + $this->assertFileExists($theme->getPath($theme->getDirName() . '/assets/dist/mix-manifest.json')); + + $manifest = json_decode(file_get_contents($theme->getPath($theme->getDirName() . '/assets/dist/mix-manifest.json')), true); + + foreach ($manifest as $key => $value) { + $this->assertStringContainsString($key, (string) Mix::mix($key)); + } + } catch (\Exception $e) { + throw $e; + } finally { + rename( + $theme->getPath($theme->getDirName() . '/winter.mix.js.bak'), + $theme->getPath($theme->getDirName() . '/winter.mix.js') + ); + } + } + + public function testThrowsAnExceptionForInvalidMixFileWhenDebugIsEnabled() + { + $this->expectException(\Exception::class); + $this->expectExceptionMessage('Unable to locate Mix file: /assets/dist/foo.css'); + + $this->artisan('mix:compile', [ + 'theme-mixtest', + '--manifest' => 'modules/system/tests/fixtures/npm/package-mixtest.json', + '--disable-tty' => true, + ])->assertExitCode(0); + + Mix::mix('assets/dist/foo.css'); + } + + public function testDoesNotThrowAnExceptionForInvalidMixFileWhenDebugIsDisabled(): void + { + Config::set('app.debug', false); + + $this->artisan('mix:compile', [ + 'theme-mixtest', + '--manifest' => 'modules/system/tests/fixtures/npm/package-mixtest.json', + '--disable-tty' => true, + ])->assertExitCode(0); + + $this->assertEquals('/assets/dist/foo.css', Mix::mix('assets/dist/foo.css')); + } +} diff --git a/modules/system/tests/fixtures/npm/package-mixtest.json b/modules/system/tests/fixtures/npm/package-mixtest.json new file mode 100644 index 0000000000..e38736ed5c --- /dev/null +++ b/modules/system/tests/fixtures/npm/package-mixtest.json @@ -0,0 +1,11 @@ +{ + "type": "module", + "workspaces": { + "packages": [ + "modules/system/tests/fixtures/themes/mixtest" + ] + }, + "devDependencies": { + "laravel-mix": "^6.0.41" + } +} diff --git a/modules/system/tests/fixtures/themes/mixtest/assets/src/css/theme.css b/modules/system/tests/fixtures/themes/mixtest/assets/src/css/theme.css new file mode 100644 index 0000000000..d224431f16 --- /dev/null +++ b/modules/system/tests/fixtures/themes/mixtest/assets/src/css/theme.css @@ -0,0 +1,3 @@ +h1 { + color: red; +} diff --git a/modules/system/tests/fixtures/themes/mixtest/assets/src/js/theme.js b/modules/system/tests/fixtures/themes/mixtest/assets/src/js/theme.js new file mode 100644 index 0000000000..55fb7c1a0a --- /dev/null +++ b/modules/system/tests/fixtures/themes/mixtest/assets/src/js/theme.js @@ -0,0 +1 @@ +window.alert('hello world'); diff --git a/modules/system/tests/fixtures/themes/mixtest/theme.yaml b/modules/system/tests/fixtures/themes/mixtest/theme.yaml new file mode 100644 index 0000000000..cfd8ec5e95 --- /dev/null +++ b/modules/system/tests/fixtures/themes/mixtest/theme.yaml @@ -0,0 +1 @@ +name: 'Mix Test' diff --git a/modules/system/tests/fixtures/themes/mixtest/winter.mix-manifest-override.js b/modules/system/tests/fixtures/themes/mixtest/winter.mix-manifest-override.js new file mode 100644 index 0000000000..89bb436123 --- /dev/null +++ b/modules/system/tests/fixtures/themes/mixtest/winter.mix-manifest-override.js @@ -0,0 +1,13 @@ +const mix = require('laravel-mix'); + +mix.setPublicPath(__dirname) + .options({ + manifest: 'assets/dist/mix-manifest.json', + }) + .version() + + // Render Tailwind style + .postCss('assets/src/css/theme.css', 'assets/dist/css/theme.css') + + // Compile JS + .js('assets/src/js/theme.js', 'assets/dist/js/theme.js'); diff --git a/modules/system/tests/fixtures/themes/mixtest/winter.mix.js b/modules/system/tests/fixtures/themes/mixtest/winter.mix.js new file mode 100644 index 0000000000..7affd67545 --- /dev/null +++ b/modules/system/tests/fixtures/themes/mixtest/winter.mix.js @@ -0,0 +1,13 @@ +const mix = require('laravel-mix'); + +mix.setPublicPath(__dirname) + .options({ + manifest: true, + }) + .version() + + // Render Tailwind style + .postCss('assets/src/css/theme.css', 'assets/dist/css/theme.css') + + // Compile JS + .js('assets/src/js/theme.js', 'assets/dist/js/theme.js');