Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 29 additions & 12 deletions src/Phaseolies/Console/Commands/FrontendInstallCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,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
Expand All @@ -68,6 +73,7 @@ public function handle(): int
'cssStack' => $cssStack,
'framework' => $framework,
'typescript' => $typescript,
'htmx' => $htmx,
'installDependencies' => $installDependencies,
'packageManager' => $packageManager,
'force' => (bool) $this->option('force'),
Expand Down Expand Up @@ -118,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);
}
Expand All @@ -141,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, $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),
Expand Down Expand Up @@ -335,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 = [
Expand All @@ -362,6 +371,10 @@ protected function packageJson(string $framework, string $cssStack, bool $typesc
$dependencies['bootstrap'] = '^5.3.3';
}

if ($htmx) {
$dependencies['htmx.org'] = '^2.0.9';
}

if ($cssStack === 'tailwind') {
$devDependencies['postcss'] = '^8.4.49';
$devDependencies['tailwindcss'] = '^4.0.0';
Expand Down Expand Up @@ -458,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'),
Expand All @@ -477,16 +490,20 @@ 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, bool $typescript, bool $htmx): string
{
$bootstrapVendorImport = $cssStack === 'bootstrap'
? "import 'bootstrap/dist/js/bootstrap.bundle.min.js';\n"
: '';
$htmxImport = $htmx
? "import 'htmx.org';\n"
: '';

return $this->renderFrontendStub(
'entries/' . ($typescript ? 'bootstrap.ts.stub' : 'bootstrap.js.stub'),
[
'bootstrapVendorImport' => $bootstrapVendorImport,
'htmxImport' => $htmxImport,
]
);
}
Expand Down
7 changes: 4 additions & 3 deletions src/Phaseolies/Console/Commands/FrontendUninstallCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -320,6 +320,7 @@ protected function isGeneratedPackageJson(string $contents): bool
'vue',
'svelte',
'bootstrap',
'htmx.org',
'postcss',
'tailwindcss',
'@tailwindcss/postcss',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{{ bootstrapVendorImport }}const csrfToken = document
{{ bootstrapVendorImport }}{{ htmxImport }}const csrfToken = document
.querySelector('meta[name="csrf-token"]')
?.getAttribute('content');

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{{ bootstrapVendorImport }}const csrfToken = document
{{ bootstrapVendorImport }}{{ htmxImport }}const csrfToken = document
.querySelector('meta[name="csrf-token"]')
?.getAttribute('content');

Expand Down
54 changes: 51 additions & 3 deletions tests/Console/FrontendInstallCommandTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -97,14 +97,15 @@ public function testClientBootstrapExposesCsrfHeaderFromMetaToken(): void
$command = new FrontendInstallCommand();
$method = new \ReflectionMethod($command, 'bootstrapFile');

$bootstrap = $method->invoke($command, 'bootstrap', true);
$bootstrap = $method->invoke($command, 'bootstrap', true, false);

$this->assertStringContainsString("meta[name=\"csrf-token\"]", $bootstrap);
$this->assertStringContainsString("headers: csrfToken ? { 'X-CSRF-TOKEN': csrfToken } : {}", $bootstrap);
$this->assertStringNotContainsString('window.fetch', $bootstrap);
$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);
}

Expand All @@ -113,25 +114,45 @@ public function testJavascriptBootstrapStubAvoidsTypescriptOnlySyntax(): void
$command = new FrontendInstallCommand();
$method = new \ReflectionMethod($command, 'bootstrapFile');

$bootstrap = $method->invoke($command, 'bootstrap', false);
$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
{
$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.9"', $packageJson);
}

public function testTailwindCssIncludesExplicitSourceDirectives(): void
{
$command = new FrontendInstallCommand();
Expand Down Expand Up @@ -248,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.9"
},
"devDependencies": {
"vite": "^7.0.0"
}
}
JSON;

$this->assertTrue($method->invoke($command, $generated));
}

private function normalizePath(string $path): string
{
return str_replace(['\\', '/'], DIRECTORY_SEPARATOR, $path);
Expand Down
Loading