diff --git a/.gitignore b/.gitignore index 6e5293c..1d7efbc 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,9 @@ Icon # --------------- .lock +#Remove the next two line for real project repositories +composer.lock +package-lock.json # Editors # (sensitive workspace files) @@ -49,3 +52,18 @@ Icon /site/config/.license +# Vendor +# --------------- + +/vendor +/node_modules + +# Content +# --------------- + +/content + +# Compiled files +# --------------- + +/public/css \ No newline at end of file diff --git a/composer.json b/composer.json index 92f95bb..8602a6e 100644 --- a/composer.json +++ b/composer.json @@ -6,7 +6,7 @@ "kirby", "cms", "starterkit", - "tailwindcss + "tailwindcss" ], "authors": [ { @@ -23,7 +23,8 @@ }, "require": { "php": "~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0", - "getkirby/cms": "^5.0" + "getkirby/cms": "^5.0", + "ext-pcntl": "*" }, "config": { "allow-plugins": { diff --git a/package.json b/package.json new file mode 100644 index 0000000..2c73ae4 --- /dev/null +++ b/package.json @@ -0,0 +1,18 @@ +{ + "name": "kirby-moin", + "version": "1.0.0", + "description": "Watch and build tailwindcss", + "scripts": { + "watch": "npx @tailwindcss/cli -i ./src/css/tailwind.css -o ./public/css/app.css --content './site/**/*.php' -w", + "build": "npx @tailwindcss/cli -i ./src/css/tailwind.css -o ./public/css/app.css --content './site/**/*.php' -m" + }, + "repository": { + "type": "git", + "url": "ssh://git@deichrakete.space/deichrakete/kirby-moin.git" + }, + "author": "Stephan Plöhn", + "license": "ISC", + "dependencies": { + "tailwindcss": "^4.1.12" + } +} diff --git a/site/commands/serve.php b/site/commands/serve.php new file mode 100644 index 0000000..085dee1 --- /dev/null +++ b/site/commands/serve.php @@ -0,0 +1,208 @@ + 'Run Kirby dev server and Tailwind watcher', + 'command' => static function ($cli): void { + $cwd = getcwd(); + + // Resolve paths + $router = $cwd . '/vendor/getkirby/cms/router.php'; + $docroot = $cli->arg('docroot') ?? 'public'; + $docrootPath = $cwd . '/' . $docroot; + + // Basic checks + if (!is_file($router)) { + $cli->error('Kirby router not found. Make sure dependencies are installed: vendor/getkirby/cms/router.php'); + return; + } + if (!is_dir($docrootPath)) { + $cli->error("Document root not found: {$docrootPath}"); + return; + } + + // Helper function to check if a port is available + $isPortAvailable = function (string $host, int $port): bool { + $socket = @fsockopen($host, $port, $errno, $errstr, 1); + if ($socket) { + fclose($socket); + return false; // Port is occupied + } + return true; // Port is available + }; + + $host = 'localhost'; + $startPort = 8000; + $maxPort = 8100; + $port = $startPort; + + while ($port <= $maxPort) { + if ($isPortAvailable($host, $port)) { + break; + } + $port++; + } + + if ($port > $maxPort) { + $cli->error("No available ports found between {$startPort} and {$maxPort}"); + return; + } + + $cli->info("Using port {$port} for PHP dev server"); + + $phpCmd = sprintf( + 'php -S %s:%s -t %s %s', + escapeshellarg($host), + escapeshellarg($port), + escapeshellarg($docrootPath), + escapeshellarg($router) + ); + + $npmCmd = 'npm run watch'; + + $cli->info("Starting PHP dev server on http://{$host}:{$port} (docroot: {$docroot})"); + + // Helper to start a process with pipes + $start = static function (string $cmd) use ($cwd) { + $descriptorSpec = [ + 0 => ['pipe', 'r'], // stdin + 1 => ['pipe', 'w'], // stdout + 2 => ['pipe', 'w'], // stderr + ]; + + $proc = proc_open($cmd, $descriptorSpec, $pipes, $cwd); + + if (!is_resource($proc)) { + return null; + } + + // Non-blocking I/O + stream_set_blocking($pipes[1], false); + stream_set_blocking($pipes[2], false); + + return [ + 'proc' => $proc, + 'pipes' => $pipes, + 'cmd' => $cmd, + ]; + }; + + $processes = []; + + $phpProc = $start($phpCmd); + if ($phpProc === null) { + $cli->error('Failed to start PHP dev server.'); + return; + } + $processes['php'] = $phpProc; + + $npmProc = $start($npmCmd); + if ($npmProc === null) { + $cli->warning('Could not start npm watcher. Is Node/npm installed and in your PATH?'); + } else { + $processes['npm'] = $npmProc; + } + + // Graceful termination + $terminateAll = static function () use (&$processes, $cli): void { + $cli->line(""); + foreach ($processes as $name => $p) { + // Check if the resource is still valid before calling proc_get_status + if (is_resource($p['proc'])) { + $status = proc_get_status($p['proc']); + if ($status && $status['running']) { + $cli->line("Stopping {$name}..."); + // Try graceful terminate + @proc_terminate($p['proc']); + } + } + + // Close pipes regardless of process status + foreach ($p['pipes'] as $pipe) { + if (is_resource($pipe)) { + @fclose($pipe); + } + } + + // Final close if the resource is still valid + if (is_resource($p['proc'])) { + @proc_close($p['proc']); + } + } + }; + + // Handle Ctrl+C if pcntl is available + if (function_exists('pcntl_async_signals') && function_exists('pcntl_signal')) { + pcntl_async_signals(true); + pcntl_signal(SIGINT, static function () use ($terminateAll) { + $terminateAll(); + // Exit cleanly after terminating children + exit(130); + }); + // Optional: also handle SIGTERM + pcntl_signal(SIGTERM, static function () use ($terminateAll) { + $terminateAll(); + exit(143); + }); + } + + // Ensure cleanup on normal shutdown + register_shutdown_function($terminateAll); + + // Stream outputs until all children exit + $prefixColor = [ + 'php' => fn($s) => "\033[1;34m🐘 [php]\033[0m $s", + 'npm' => fn($s) => "\033[1;32m [npm]\033[0m $s", + ]; + + while (true) { + $running = false; + + foreach ($processes as $name => $p) { + // Check if the resource is still valid + if (!is_resource($p['proc'])) { + continue; + } + + $status = proc_get_status($p['proc']); + if (!$status) { + continue; + } + + $running = $running || $status['running']; + + // Read stdout + if (is_resource($p['pipes'][1])) { + $out = @stream_get_contents($p['pipes'][1]); + if ($out !== false && $out !== '') { + foreach (preg_split('/\R/', rtrim($out)) as $line) { + if ($line !== '') { + $cli->line(($prefixColor[$name])($line)); + } + } + } + } + + // Read stderr + if (is_resource($p['pipes'][2])) { + $err = @stream_get_contents($p['pipes'][2]); + if ($err !== false && $err !== '') { + foreach (preg_split('/\R/', rtrim($err)) as $line) { + if ($line !== '') { + $cli->error(($prefixColor[$name])($line)); + } + } + } + } + } + + if (!$running) { + break; + } + + // Small sleep to avoid busy-waiting + usleep(50_000); + } + + $cli->success('All processes exited.'); + } +]; \ No newline at end of file diff --git a/site/snippets/layouts/main.php b/site/snippets/layouts/main.php new file mode 100644 index 0000000..b643d2f --- /dev/null +++ b/site/snippets/layouts/main.php @@ -0,0 +1,44 @@ + + + + + + + + <?= $page->title() ?> | <?= $site->title() ?> + + + + + +
+

+ + title() ?> + +

+
+ +
+ +
+ + + + + \ No newline at end of file diff --git a/site/templates/default.php b/site/templates/default.php index 74e38ae..6fadabd 100644 --- a/site/templates/default.php +++ b/site/templates/default.php @@ -1 +1,14 @@ + + + + +

title() ?>

+