From cb47702f6312a7fda947d89d6484fe7fc5ea4c56 Mon Sep 17 00:00:00 2001 From: rrr63 Date: Mon, 27 Apr 2026 14:51:47 +0200 Subject: [PATCH 1/6] support htmx --- .../Commands/FrontendInstallCommand.php | 37 +++- .../Commands/FrontendUninstallCommand.php | 29 +++ .../stubs/frontend/controller/htmx-test.stub | 17 ++ .../stubs/frontend/entries/bootstrap.stub | 9 +- .../Commands/stubs/frontend/entries/htmx.stub | 6 + .../stubs/frontend/views/welcome/htmx.stub | 171 ++++++++++++++++++ 6 files changed, 258 insertions(+), 11 deletions(-) create mode 100644 src/Phaseolies/Console/Commands/stubs/frontend/controller/htmx-test.stub create mode 100644 src/Phaseolies/Console/Commands/stubs/frontend/entries/htmx.stub create mode 100644 src/Phaseolies/Console/Commands/stubs/frontend/views/welcome/htmx.stub diff --git a/src/Phaseolies/Console/Commands/FrontendInstallCommand.php b/src/Phaseolies/Console/Commands/FrontendInstallCommand.php index d6b0765..8c98dc9 100644 --- a/src/Phaseolies/Console/Commands/FrontendInstallCommand.php +++ b/src/Phaseolies/Console/Commands/FrontendInstallCommand.php @@ -9,6 +9,19 @@ class FrontendInstallCommand extends Command { + private const BOOTSTRAP_VENDOR_IMPORT = "import 'bootstrap/dist/js/bootstrap.bundle.min.js';\n"; + + private const TYPESCRIPT_DECLARATION = <<<'TYPESCRIPT' +declare global { + interface Window { + __DOPPAR_FRONTEND__?: Record & { + csrfToken?: string | null; + headers?: Record; + }; + } +} + +TYPESCRIPT; use InteractsWithFrontendScaffoldState; /** @@ -46,7 +59,7 @@ public function handle(): int $framework = strtolower($this->choice( 'Which client framework do you want to use?', - ['Vanilla', 'React', 'Vue', 'Svelte'], + ['Vanilla', 'React', 'Vue', 'Svelte', 'htmx'], 0 )); @@ -162,6 +175,10 @@ protected function writeFrontendFiles(array $options, array &$state): void $files[base_path('tsconfig.json')] = $this->tsconfig($framework); } + if ($framework === 'htmx') { + $files[base_path('app/Http/Controllers/HtmxTestController.php')] = $this->getFrontendStubContent('controller/htmx-test.stub'); + } + foreach ($this->frameworkComponentFiles($framework, $typescript) as $path => $contents) { $files[$path] = $contents; } @@ -320,7 +337,7 @@ protected function entryFilename(string $framework, bool $typescript): string return $typescript ? 'main.tsx' : 'main.jsx'; } - if (in_array($framework, ['vue', 'svelte'], true)) { + if (in_array($framework, ['vue', 'svelte', 'htmx'], true)) { return $typescript ? 'main.ts' : 'main.js'; } @@ -362,6 +379,10 @@ protected function packageJson(string $framework, string $cssStack, bool $typesc $dependencies['bootstrap'] = '^5.3.3'; } + if ($framework === 'htmx') { + $dependencies['htmx.org'] = '^2.0.10'; + } + if ($cssStack === 'tailwind') { $devDependencies['postcss'] = '^8.4.49'; $devDependencies['tailwindcss'] = '^4.0.0'; @@ -480,11 +501,16 @@ protected function frameworkComponentFiles(string $framework, bool $typescript): protected function bootstrapFile(string $cssStack, bool $typescript): string { $bootstrapVendorImport = $cssStack === 'bootstrap' - ? "import 'bootstrap/dist/js/bootstrap.bundle.min.js';\n" + ? self::BOOTSTRAP_VENDOR_IMPORT + : ''; + + $typescriptDeclaration = $typescript + ? self::TYPESCRIPT_DECLARATION : ''; return $this->renderFrontendStub('entries/bootstrap.stub', [ 'bootstrapVendorImport' => $bootstrapVendorImport, + 'typescriptDeclaration' => $typescriptDeclaration, ]); } @@ -554,6 +580,10 @@ protected function usesClientFramework(string $framework): bool */ protected function welcomeView(string $framework, bool $typescript): string { + if ($framework === 'htmx') { + return $this->getFrontendStubContent('views/welcome/htmx.stub'); + } + if ($this->usesClientFramework($framework)) { return $this->getFrontendStubContent('views/welcome/client.stub'); } @@ -599,6 +629,7 @@ protected function entryStubName(string $framework, bool $typescript): string 'react' => $typescript ? 'react.ts.stub' : 'react.js.stub', 'vue' => 'vue.stub', 'svelte' => $typescript ? 'svelte.ts.stub' : 'svelte.js.stub', + 'htmx' => 'htmx.stub', default => 'vanilla.stub', }; } diff --git a/src/Phaseolies/Console/Commands/FrontendUninstallCommand.php b/src/Phaseolies/Console/Commands/FrontendUninstallCommand.php index b348549..4714d4c 100644 --- a/src/Phaseolies/Console/Commands/FrontendUninstallCommand.php +++ b/src/Phaseolies/Console/Commands/FrontendUninstallCommand.php @@ -40,6 +40,8 @@ public function handle(): int $removeNodeModules = (bool) $this->option('clean-node-modules'); $state = $this->loadFrontendScaffoldState(); + $this->removeHtmxController(); + if ($state === null) { $removedArtifacts = $this->performLegacyFrontendCleanup($removeNodeModules); @@ -150,6 +152,19 @@ protected function performLegacyFrontendCleanup(bool $removeNodeModules = false) base_path('client/js/App.svelte'), ]; + $controllerFiles = [ + base_path('app/Http/Controllers/HtmxTestController.php'), + ]; + + foreach ($controllerFiles as $path) { + if (!is_file($path)) { + continue; + } + + unlink($path); + $removedArtifacts++; + } + foreach ($clientFiles as $path) { if (!is_file($path)) { continue; @@ -319,6 +334,7 @@ protected function isGeneratedPackageJson(string $contents): bool 'react-dom', 'vue', 'svelte', + 'htmx.org', 'bootstrap', 'postcss', 'tailwindcss', @@ -399,4 +415,17 @@ protected function isGeneratedPostcssConfig(string $contents): bool return str_contains($contents, "export default") && str_contains($contents, "'@tailwindcss/postcss': {}"); } + + /** + * Remove the htmx test controller if it exists. + * + * @return void + */ + protected function removeHtmxController(): void + { + $htmxController = base_path('app/Http/Controllers/HtmxTestController.php'); + if (file_exists($htmxController)) { + unlink($htmxController); + } + } } diff --git a/src/Phaseolies/Console/Commands/stubs/frontend/controller/htmx-test.stub b/src/Phaseolies/Console/Commands/stubs/frontend/controller/htmx-test.stub new file mode 100644 index 0000000..4b17742 --- /dev/null +++ b/src/Phaseolies/Console/Commands/stubs/frontend/controller/htmx-test.stub @@ -0,0 +1,17 @@ + & { - csrfToken?: string | null; - headers?: Record; - }; - } -} +{{ typescriptDeclaration }} diff --git a/src/Phaseolies/Console/Commands/stubs/frontend/entries/htmx.stub b/src/Phaseolies/Console/Commands/stubs/frontend/entries/htmx.stub new file mode 100644 index 0000000..20ce9b0 --- /dev/null +++ b/src/Phaseolies/Console/Commands/stubs/frontend/entries/htmx.stub @@ -0,0 +1,6 @@ +import '../css/app.css'; +{{ bootstrapImport }} +import 'htmx.org'; + +document.documentElement.dataset.clientReady = 'true'; +console.info('Doppar client booted successfully.'); diff --git a/src/Phaseolies/Console/Commands/stubs/frontend/views/welcome/htmx.stub b/src/Phaseolies/Console/Commands/stubs/frontend/views/welcome/htmx.stub new file mode 100644 index 0000000..8af5090 --- /dev/null +++ b/src/Phaseolies/Console/Commands/stubs/frontend/views/welcome/htmx.stub @@ -0,0 +1,171 @@ + + + + + + Doppar - HTMX Demo + + + #vite('resources/client/js/main.js') + + + +
+ Logo +

Welcome to Doppar

+
+ +
+

HTMX Demo

+ +
+ Click the button to test HTMX... +
+
+ +
+

[[ trans('messages.welcome', ['version' => 'v' . Application::VERSION]) ]]

+

Craft Fast-Loading PHP Application

+
+
+ +

Starter kits

+

To give you a head start building your new Doppar application, we are happy to offer application starter kits.

+
+ +

System Architecture

+

Doppar follows the Model-View-Controller (MVC) architectural pattern, a widely accepted standard in web application development.

+
+ +

Routing

+

With support for route prefix grouping, named routes, throttle route, middleware assignment, and RESTful resource routing, Doppar gives developers full control over how requests are handled.

+
+ +

Authentication

+

Doppar simplifies this process by providing built-in tools and scaffolding to help you implement user authentication quickly and securely.

+
+
+ + + From cedd3812b8d58b8e7b0dcef8061a3e07570bd2ca Mon Sep 17 00:00:00 2001 From: rrr63 Date: Mon, 27 Apr 2026 15:36:14 +0200 Subject: [PATCH 2/6] clean PR --- .../Commands/FrontendInstallCommand.php | 20 +- .../Commands/FrontendUninstallCommand.php | 27 --- .../stubs/frontend/controller/htmx-test.stub | 17 -- .../stubs/frontend/entries/bootstrap.stub | 2 +- .../Commands/stubs/frontend/entries/htmx.stub | 6 - .../stubs/frontend/views/welcome/htmx.stub | 171 ------------------ 6 files changed, 10 insertions(+), 233 deletions(-) delete mode 100644 src/Phaseolies/Console/Commands/stubs/frontend/controller/htmx-test.stub delete mode 100644 src/Phaseolies/Console/Commands/stubs/frontend/entries/htmx.stub delete mode 100644 src/Phaseolies/Console/Commands/stubs/frontend/views/welcome/htmx.stub diff --git a/src/Phaseolies/Console/Commands/FrontendInstallCommand.php b/src/Phaseolies/Console/Commands/FrontendInstallCommand.php index 8c98dc9..8d8fd37 100644 --- a/src/Phaseolies/Console/Commands/FrontendInstallCommand.php +++ b/src/Phaseolies/Console/Commands/FrontendInstallCommand.php @@ -11,6 +11,8 @@ class FrontendInstallCommand extends Command { private const BOOTSTRAP_VENDOR_IMPORT = "import 'bootstrap/dist/js/bootstrap.bundle.min.js';\n"; + private const HTMX_VENDOR_IMPORT = "import 'htmx.org';\n"; + private const TYPESCRIPT_DECLARATION = <<<'TYPESCRIPT' declare global { interface Window { @@ -161,7 +163,7 @@ protected function writeFrontendFiles(array $options, array &$state): void base_path('package.json') => $this->packageJson($framework, $cssStack, $typescript), base_path('vite.config.js') => $this->viteConfig($framework, $typescript), client_path('css/app.css') => $this->clientCss($cssStack), - client_path('js/' . $this->bootstrapFilename($typescript)) => $this->bootstrapFile($cssStack, $typescript), + client_path('js/' . $this->bootstrapFilename($typescript)) => $this->bootstrapFile($cssStack, $framework, $typescript), client_path('js/' . $this->entryFilename($framework, $typescript)) => $this->entryFile($framework, $cssStack, $typescript), base_path('resources/views/layouts/app.odo.php') => $this->appLayoutView($framework, $typescript), base_path('resources/views/welcome.odo.php') => $this->welcomeView($framework, $typescript), @@ -175,10 +177,6 @@ protected function writeFrontendFiles(array $options, array &$state): void $files[base_path('tsconfig.json')] = $this->tsconfig($framework); } - if ($framework === 'htmx') { - $files[base_path('app/Http/Controllers/HtmxTestController.php')] = $this->getFrontendStubContent('controller/htmx-test.stub'); - } - foreach ($this->frameworkComponentFiles($framework, $typescript) as $path => $contents) { $files[$path] = $contents; } @@ -498,18 +496,23 @@ protected function frameworkComponentFiles(string $framework, bool $typescript): * @param bool $typescript * @return string */ - protected function bootstrapFile(string $cssStack, bool $typescript): string + protected function bootstrapFile(string $cssStack, string $framework, bool $typescript): string { $bootstrapVendorImport = $cssStack === 'bootstrap' ? self::BOOTSTRAP_VENDOR_IMPORT : ''; + $htmxVendorImport = $framework === 'htmx' + ? self::HTMX_VENDOR_IMPORT + : ''; + $typescriptDeclaration = $typescript ? self::TYPESCRIPT_DECLARATION : ''; return $this->renderFrontendStub('entries/bootstrap.stub', [ 'bootstrapVendorImport' => $bootstrapVendorImport, + 'htmxVendorImport' => $htmxVendorImport, 'typescriptDeclaration' => $typescriptDeclaration, ]); } @@ -580,10 +583,6 @@ protected function usesClientFramework(string $framework): bool */ protected function welcomeView(string $framework, bool $typescript): string { - if ($framework === 'htmx') { - return $this->getFrontendStubContent('views/welcome/htmx.stub'); - } - if ($this->usesClientFramework($framework)) { return $this->getFrontendStubContent('views/welcome/client.stub'); } @@ -629,7 +628,6 @@ protected function entryStubName(string $framework, bool $typescript): string 'react' => $typescript ? 'react.ts.stub' : 'react.js.stub', 'vue' => 'vue.stub', 'svelte' => $typescript ? 'svelte.ts.stub' : 'svelte.js.stub', - 'htmx' => 'htmx.stub', default => 'vanilla.stub', }; } diff --git a/src/Phaseolies/Console/Commands/FrontendUninstallCommand.php b/src/Phaseolies/Console/Commands/FrontendUninstallCommand.php index 4714d4c..8bf6878 100644 --- a/src/Phaseolies/Console/Commands/FrontendUninstallCommand.php +++ b/src/Phaseolies/Console/Commands/FrontendUninstallCommand.php @@ -40,8 +40,6 @@ public function handle(): int $removeNodeModules = (bool) $this->option('clean-node-modules'); $state = $this->loadFrontendScaffoldState(); - $this->removeHtmxController(); - if ($state === null) { $removedArtifacts = $this->performLegacyFrontendCleanup($removeNodeModules); @@ -152,19 +150,6 @@ protected function performLegacyFrontendCleanup(bool $removeNodeModules = false) base_path('client/js/App.svelte'), ]; - $controllerFiles = [ - base_path('app/Http/Controllers/HtmxTestController.php'), - ]; - - foreach ($controllerFiles as $path) { - if (!is_file($path)) { - continue; - } - - unlink($path); - $removedArtifacts++; - } - foreach ($clientFiles as $path) { if (!is_file($path)) { continue; @@ -416,16 +401,4 @@ protected function isGeneratedPostcssConfig(string $contents): bool && str_contains($contents, "'@tailwindcss/postcss': {}"); } - /** - * Remove the htmx test controller if it exists. - * - * @return void - */ - protected function removeHtmxController(): void - { - $htmxController = base_path('app/Http/Controllers/HtmxTestController.php'); - if (file_exists($htmxController)) { - unlink($htmxController); - } - } } diff --git a/src/Phaseolies/Console/Commands/stubs/frontend/controller/htmx-test.stub b/src/Phaseolies/Console/Commands/stubs/frontend/controller/htmx-test.stub deleted file mode 100644 index 4b17742..0000000 --- a/src/Phaseolies/Console/Commands/stubs/frontend/controller/htmx-test.stub +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - Doppar - HTMX Demo - - - #vite('resources/client/js/main.js') - - - -
- Logo -

Welcome to Doppar

-
- -
-

HTMX Demo

- -
- Click the button to test HTMX... -
-
- -
-

[[ trans('messages.welcome', ['version' => 'v' . Application::VERSION]) ]]

-

Craft Fast-Loading PHP Application

-
-
- -

Starter kits

-

To give you a head start building your new Doppar application, we are happy to offer application starter kits.

-
- -

System Architecture

-

Doppar follows the Model-View-Controller (MVC) architectural pattern, a widely accepted standard in web application development.

-
- -

Routing

-

With support for route prefix grouping, named routes, throttle route, middleware assignment, and RESTful resource routing, Doppar gives developers full control over how requests are handled.

-
- -

Authentication

-

Doppar simplifies this process by providing built-in tools and scaffolding to help you implement user authentication quickly and securely.

-
-
- - - From bf586ec81ad1d4c75297fe3a6505357f2d883021 Mon Sep 17 00:00:00 2001 From: rrr63 Date: Mon, 27 Apr 2026 15:41:05 +0200 Subject: [PATCH 3/6] ci fix --- tests/Console/FrontendInstallCommandTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Console/FrontendInstallCommandTest.php b/tests/Console/FrontendInstallCommandTest.php index 3d29c0d..2935ac3 100644 --- a/tests/Console/FrontendInstallCommandTest.php +++ b/tests/Console/FrontendInstallCommandTest.php @@ -97,7 +97,7 @@ public function testClientBootstrapExposesCsrfHeaderFromMetaToken(): void $command = new FrontendInstallCommand(); $method = new \ReflectionMethod($command, 'bootstrapFile'); - $bootstrap = $method->invoke($command, 'bootstrap', true); + $bootstrap = $method->invoke($command, 'bootstrap', 'vanilla', true); $this->assertStringContainsString("meta[name=\"csrf-token\"]", $bootstrap); $this->assertStringContainsString("headers: csrfToken ? { 'X-CSRF-TOKEN': csrfToken } : {}", $bootstrap); From 4e7b3fbf8d60e77f0a916124bba98661981d5c7f Mon Sep 17 00:00:00 2001 From: rrr63 Date: Mon, 27 Apr 2026 15:43:33 +0200 Subject: [PATCH 4/6] doc --- src/Phaseolies/Console/Commands/FrontendInstallCommand.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Phaseolies/Console/Commands/FrontendInstallCommand.php b/src/Phaseolies/Console/Commands/FrontendInstallCommand.php index 8d8fd37..aa39812 100644 --- a/src/Phaseolies/Console/Commands/FrontendInstallCommand.php +++ b/src/Phaseolies/Console/Commands/FrontendInstallCommand.php @@ -493,6 +493,7 @@ protected function frameworkComponentFiles(string $framework, bool $typescript): * Generate the shared client bootstrap file. * * @param string $cssStack + * @param string $framework * @param bool $typescript * @return string */ From e4749262c72888017f6fada97e9d6d4df90aa60e Mon Sep 17 00:00:00 2001 From: Arif Hoque Date: Tue, 28 Apr 2026 11:03:01 +0600 Subject: [PATCH 5/6] code refactored: --- .../Commands/FrontendInstallCommand.php | 80 ++++++++----------- .../Commands/FrontendUninstallCommand.php | 9 +-- .../{bootstrap.stub => bootstrap.js.stub} | 4 +- .../stubs/frontend/entries/bootstrap.ts.stub | 18 +++++ tests/Console/FrontendInstallCommandTest.php | 66 ++++++++++++++- 5 files changed, 122 insertions(+), 55 deletions(-) rename src/Phaseolies/Console/Commands/stubs/frontend/entries/{bootstrap.stub => bootstrap.js.stub} (70%) create mode 100644 src/Phaseolies/Console/Commands/stubs/frontend/entries/bootstrap.ts.stub diff --git a/src/Phaseolies/Console/Commands/FrontendInstallCommand.php b/src/Phaseolies/Console/Commands/FrontendInstallCommand.php index aa39812..2519573 100644 --- a/src/Phaseolies/Console/Commands/FrontendInstallCommand.php +++ b/src/Phaseolies/Console/Commands/FrontendInstallCommand.php @@ -9,21 +9,6 @@ class FrontendInstallCommand extends Command { - private const BOOTSTRAP_VENDOR_IMPORT = "import 'bootstrap/dist/js/bootstrap.bundle.min.js';\n"; - - private const HTMX_VENDOR_IMPORT = "import 'htmx.org';\n"; - - private const TYPESCRIPT_DECLARATION = <<<'TYPESCRIPT' -declare global { - interface Window { - __DOPPAR_FRONTEND__?: Record & { - csrfToken?: string | null; - headers?: Record; - }; - } -} - -TYPESCRIPT; use InteractsWithFrontendScaffoldState; /** @@ -61,7 +46,7 @@ public function handle(): int $framework = strtolower($this->choice( 'Which client framework do you want to use?', - ['Vanilla', 'React', 'Vue', 'Svelte', 'htmx'], + ['Vanilla', 'React', 'Vue', 'Svelte'], 0 )); @@ -70,6 +55,11 @@ public function handle(): int in_array($framework, ['react', 'vue', 'svelte'], true) ); + $htmx = $this->confirm( + 'Do you want HTMX support?', + false + ); + $installDependencies = (bool) ($this->option('install') ?: $this->confirm( 'Do you want to install frontend dependencies now?', false @@ -83,6 +73,7 @@ public function handle(): int 'cssStack' => $cssStack, 'framework' => $framework, 'typescript' => $typescript, + 'htmx' => $htmx, 'installDependencies' => $installDependencies, 'packageManager' => $packageManager, 'force' => (bool) $this->option('force'), @@ -133,13 +124,15 @@ protected function renderInstallationWelcome(): void */ protected function ensureClientDirectories(): void { - foreach ([ - client_path(), - client_path('css'), - client_path('js'), - resource_path('views/layouts'), - storage_path('framework'), - ] as $directory) { + foreach ( + [ + client_path(), + client_path('css'), + client_path('js'), + resource_path('views/layouts'), + storage_path('framework'), + ] as $directory + ) { if (!is_dir($directory)) { mkdir($directory, 0777, true); } @@ -156,14 +149,15 @@ protected function writeFrontendFiles(array $options, array &$state): void { $framework = $options['framework']; $typescript = $options['typescript']; + $htmx = $options['htmx']; $cssStack = $options['cssStack']; $force = $options['force']; $files = [ - base_path('package.json') => $this->packageJson($framework, $cssStack, $typescript), + base_path('package.json') => $this->packageJson($framework, $cssStack, $typescript, $htmx), base_path('vite.config.js') => $this->viteConfig($framework, $typescript), client_path('css/app.css') => $this->clientCss($cssStack), - client_path('js/' . $this->bootstrapFilename($typescript)) => $this->bootstrapFile($cssStack, $framework, $typescript), + client_path('js/' . $this->bootstrapFilename($typescript)) => $this->bootstrapFile($cssStack, $typescript, $htmx), client_path('js/' . $this->entryFilename($framework, $typescript)) => $this->entryFile($framework, $cssStack, $typescript), base_path('resources/views/layouts/app.odo.php') => $this->appLayoutView($framework, $typescript), base_path('resources/views/welcome.odo.php') => $this->welcomeView($framework, $typescript), @@ -335,7 +329,7 @@ protected function entryFilename(string $framework, bool $typescript): string return $typescript ? 'main.tsx' : 'main.jsx'; } - if (in_array($framework, ['vue', 'svelte', 'htmx'], true)) { + if (in_array($framework, ['vue', 'svelte'], true)) { return $typescript ? 'main.ts' : 'main.js'; } @@ -350,7 +344,7 @@ protected function entryFilename(string $framework, bool $typescript): string * @param bool $typescript * @return string */ - protected function packageJson(string $framework, string $cssStack, bool $typescript): string + protected function packageJson(string $framework, string $cssStack, bool $typescript, bool $htmx): string { $dependencies = []; $devDependencies = [ @@ -377,8 +371,8 @@ protected function packageJson(string $framework, string $cssStack, bool $typesc $dependencies['bootstrap'] = '^5.3.3'; } - if ($framework === 'htmx') { - $dependencies['htmx.org'] = '^2.0.10'; + if ($htmx) { + $dependencies['htmx.org'] = '^2.0.9'; } if ($cssStack === 'tailwind') { @@ -477,7 +471,7 @@ protected function frameworkComponentFiles(string $framework, bool $typescript): return match ($framework) { 'react' => [ client_path('js/' . ($typescript ? 'App.tsx' : 'App.jsx')) => - $this->getFrontendStubContent('components/' . ($typescript ? 'react.tsx.stub' : 'react.jsx.stub')), + $this->getFrontendStubContent('components/' . ($typescript ? 'react.tsx.stub' : 'react.jsx.stub')), ], 'vue' => [ client_path('js/App.vue') => $this->getFrontendStubContent('components/vue.stub'), @@ -493,29 +487,25 @@ protected function frameworkComponentFiles(string $framework, bool $typescript): * Generate the shared client bootstrap file. * * @param string $cssStack - * @param string $framework * @param bool $typescript * @return string */ - protected function bootstrapFile(string $cssStack, string $framework, bool $typescript): string + protected function bootstrapFile(string $cssStack, bool $typescript, bool $htmx): string { $bootstrapVendorImport = $cssStack === 'bootstrap' - ? self::BOOTSTRAP_VENDOR_IMPORT - : ''; - - $htmxVendorImport = $framework === 'htmx' - ? self::HTMX_VENDOR_IMPORT + ? "import 'bootstrap/dist/js/bootstrap.bundle.min.js';\n" : ''; - - $typescriptDeclaration = $typescript - ? self::TYPESCRIPT_DECLARATION + $htmxImport = $htmx + ? "import 'htmx.org';\n" : ''; - return $this->renderFrontendStub('entries/bootstrap.stub', [ - 'bootstrapVendorImport' => $bootstrapVendorImport, - 'htmxVendorImport' => $htmxVendorImport, - 'typescriptDeclaration' => $typescriptDeclaration, - ]); + return $this->renderFrontendStub( + 'entries/' . ($typescript ? 'bootstrap.ts.stub' : 'bootstrap.js.stub'), + [ + 'bootstrapVendorImport' => $bootstrapVendorImport, + 'htmxImport' => $htmxImport, + ] + ); } /** diff --git a/src/Phaseolies/Console/Commands/FrontendUninstallCommand.php b/src/Phaseolies/Console/Commands/FrontendUninstallCommand.php index 8bf6878..a51f98b 100644 --- a/src/Phaseolies/Console/Commands/FrontendUninstallCommand.php +++ b/src/Phaseolies/Console/Commands/FrontendUninstallCommand.php @@ -87,9 +87,9 @@ protected function performLegacyFrontendCleanup(bool $removeNodeModules = false) && $this->isGeneratedPackageJson((string) file_get_contents($packageJsonPath)); $generatedRootFiles = [ - base_path('vite.config.js') => fn (string $contents): bool => $this->isGeneratedViteConfig($contents), - base_path('tsconfig.json') => fn (string $contents): bool => $this->isGeneratedTsConfig($contents), - base_path('postcss.config.js') => fn (string $contents): bool => $this->isGeneratedPostcssConfig($contents), + base_path('vite.config.js') => fn(string $contents): bool => $this->isGeneratedViteConfig($contents), + base_path('tsconfig.json') => fn(string $contents): bool => $this->isGeneratedTsConfig($contents), + base_path('postcss.config.js') => fn(string $contents): bool => $this->isGeneratedPostcssConfig($contents), ]; foreach ($generatedRootFiles as $path => $detector) { @@ -319,8 +319,8 @@ protected function isGeneratedPackageJson(string $contents): bool 'react-dom', 'vue', 'svelte', - 'htmx.org', 'bootstrap', + 'htmx.org', 'postcss', 'tailwindcss', '@tailwindcss/postcss', @@ -400,5 +400,4 @@ protected function isGeneratedPostcssConfig(string $contents): bool return str_contains($contents, "export default") && str_contains($contents, "'@tailwindcss/postcss': {}"); } - } diff --git a/src/Phaseolies/Console/Commands/stubs/frontend/entries/bootstrap.stub b/src/Phaseolies/Console/Commands/stubs/frontend/entries/bootstrap.js.stub similarity index 70% rename from src/Phaseolies/Console/Commands/stubs/frontend/entries/bootstrap.stub rename to src/Phaseolies/Console/Commands/stubs/frontend/entries/bootstrap.js.stub index e9aa0bf..851c639 100644 --- a/src/Phaseolies/Console/Commands/stubs/frontend/entries/bootstrap.stub +++ b/src/Phaseolies/Console/Commands/stubs/frontend/entries/bootstrap.js.stub @@ -1,4 +1,4 @@ -{{ bootstrapVendorImport }}const csrfToken = document +{{ bootstrapVendorImport }}{{ htmxImport }}const csrfToken = document .querySelector('meta[name="csrf-token"]') ?.getAttribute('content'); @@ -7,5 +7,3 @@ window.__DOPPAR_FRONTEND__ = { csrfToken: csrfToken ?? null, headers: csrfToken ? { 'X-CSRF-TOKEN': csrfToken } : {}, }; -{{ htmxVendorImport }} -{{ typescriptDeclaration }} diff --git a/src/Phaseolies/Console/Commands/stubs/frontend/entries/bootstrap.ts.stub b/src/Phaseolies/Console/Commands/stubs/frontend/entries/bootstrap.ts.stub new file mode 100644 index 0000000..244ffb3 --- /dev/null +++ b/src/Phaseolies/Console/Commands/stubs/frontend/entries/bootstrap.ts.stub @@ -0,0 +1,18 @@ +{{ bootstrapVendorImport }}{{ htmxImport }}const csrfToken = document + .querySelector('meta[name="csrf-token"]') + ?.getAttribute('content'); + +window.__DOPPAR_FRONTEND__ = { + ...(window.__DOPPAR_FRONTEND__ ?? {}), + csrfToken: csrfToken ?? null, + headers: csrfToken ? { 'X-CSRF-TOKEN': csrfToken } : {}, +}; + +declare global { + interface Window { + __DOPPAR_FRONTEND__?: Record & { + csrfToken?: string | null; + headers?: Record; + }; + } +} diff --git a/tests/Console/FrontendInstallCommandTest.php b/tests/Console/FrontendInstallCommandTest.php index 2935ac3..43bcfba 100644 --- a/tests/Console/FrontendInstallCommandTest.php +++ b/tests/Console/FrontendInstallCommandTest.php @@ -97,7 +97,7 @@ public function testClientBootstrapExposesCsrfHeaderFromMetaToken(): void $command = new FrontendInstallCommand(); $method = new \ReflectionMethod($command, 'bootstrapFile'); - $bootstrap = $method->invoke($command, 'bootstrap', 'vanilla', true); + $bootstrap = $method->invoke($command, 'bootstrap', true, false); $this->assertStringContainsString("meta[name=\"csrf-token\"]", $bootstrap); $this->assertStringContainsString("headers: csrfToken ? { 'X-CSRF-TOKEN': csrfToken } : {}", $bootstrap); @@ -105,6 +105,31 @@ public function testClientBootstrapExposesCsrfHeaderFromMetaToken(): void $this->assertStringNotContainsString('XMLHttpRequest.prototype', $bootstrap); $this->assertStringNotContainsString('X-Requested-With', $bootstrap); $this->assertStringContainsString("import 'bootstrap/dist/js/bootstrap.bundle.min.js';", $bootstrap); + $this->assertStringNotContainsString("import 'htmx.org';", $bootstrap); + $this->assertStringContainsString('declare global', $bootstrap); + } + + public function testJavascriptBootstrapStubAvoidsTypescriptOnlySyntax(): void + { + $command = new FrontendInstallCommand(); + $method = new \ReflectionMethod($command, 'bootstrapFile'); + + $bootstrap = $method->invoke($command, 'bootstrap', false, false); + + $this->assertStringContainsString("meta[name=\"csrf-token\"]", $bootstrap); + $this->assertStringContainsString("headers: csrfToken ? { 'X-CSRF-TOKEN': csrfToken } : {}", $bootstrap); + $this->assertStringContainsString("import 'bootstrap/dist/js/bootstrap.bundle.min.js';", $bootstrap); + $this->assertStringNotContainsString('declare global', $bootstrap); + } + + public function testBootstrapStubCanImportHtmxSupport(): void + { + $command = new FrontendInstallCommand(); + $method = new \ReflectionMethod($command, 'bootstrapFile'); + + $bootstrap = $method->invoke($command, 'none', false, true); + + $this->assertStringContainsString("import 'htmx.org';", $bootstrap); } public function testVuePackageJsonUsesViteSevenCompatiblePluginVersion(): void @@ -112,12 +137,22 @@ public function testVuePackageJsonUsesViteSevenCompatiblePluginVersion(): void $command = new FrontendInstallCommand(); $method = new \ReflectionMethod($command, 'packageJson'); - $packageJson = $method->invoke($command, 'vue', 'tailwind', true); + $packageJson = $method->invoke($command, 'vue', 'tailwind', true, false); $this->assertStringContainsString('"vite": "^7.0.0"', $packageJson); $this->assertStringContainsString('"@vitejs/plugin-vue": "^6.0.0"', $packageJson); } + public function testPackageJsonCanIncludeHtmxDependency(): void + { + $command = new FrontendInstallCommand(); + $method = new \ReflectionMethod($command, 'packageJson'); + + $packageJson = $method->invoke($command, 'vanilla', 'none', false, true); + + $this->assertStringContainsString('"htmx.org": "^2.0.4"', $packageJson); + } + public function testTailwindCssIncludesExplicitSourceDirectives(): void { $command = new FrontendInstallCommand(); @@ -234,6 +269,33 @@ public function testFrontendUninstallDetectsGeneratedPackageJsonTemplate(): void $this->assertFalse($method->invoke($command, $custom)); } + public function testFrontendUninstallDetectsGeneratedPackageJsonTemplateWithHtmx(): void + { + $command = new FrontendUninstallCommand(); + $method = new \ReflectionMethod($command, 'isGeneratedPackageJson'); + + $generated = <<<'JSON' +{ + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "bootstrap": "^5.3.3", + "htmx.org": "^2.0.4" + }, + "devDependencies": { + "vite": "^7.0.0" + } +} +JSON; + + $this->assertTrue($method->invoke($command, $generated)); + } + private function normalizePath(string $path): string { return str_replace(['\\', '/'], DIRECTORY_SEPARATOR, $path); From 920d8e94eb064256428ebbf0d23b58e2b69153a3 Mon Sep 17 00:00:00 2001 From: Arif Hoque Date: Tue, 28 Apr 2026 11:12:44 +0600 Subject: [PATCH 6/6] htmx version updated in test file: --- tests/Console/FrontendInstallCommandTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Console/FrontendInstallCommandTest.php b/tests/Console/FrontendInstallCommandTest.php index 43bcfba..5ebf6db 100644 --- a/tests/Console/FrontendInstallCommandTest.php +++ b/tests/Console/FrontendInstallCommandTest.php @@ -150,7 +150,7 @@ public function testPackageJsonCanIncludeHtmxDependency(): void $packageJson = $method->invoke($command, 'vanilla', 'none', false, true); - $this->assertStringContainsString('"htmx.org": "^2.0.4"', $packageJson); + $this->assertStringContainsString('"htmx.org": "^2.0.9"', $packageJson); } public function testTailwindCssIncludesExplicitSourceDirectives(): void @@ -285,7 +285,7 @@ public function testFrontendUninstallDetectsGeneratedPackageJsonTemplateWithHtmx }, "dependencies": { "bootstrap": "^5.3.3", - "htmx.org": "^2.0.4" + "htmx.org": "^2.0.9" }, "devDependencies": { "vite": "^7.0.0"