diff --git a/app/Commands/ServeCommand.php b/app/Commands/ServeCommand.php index 6039d56a..8c66c04d 100644 --- a/app/Commands/ServeCommand.php +++ b/app/Commands/ServeCommand.php @@ -7,6 +7,7 @@ use Phar; use App\Launcher\Project; use App\Launcher\RuntimeManager; +use App\Support\BundledStylesheet; use Illuminate\Support\Facades\File; use Illuminate\Support\Facades\Process; use Hyde\RealtimeCompiler\Console\Commands\ServeCommand as BaseServeCommand; @@ -49,24 +50,23 @@ protected function runServerProcess(string $command): void /** The script the built-in server runs for every request. */ protected function getExecutablePath(): string { - $default = parent::getExecutablePath(); - - if (File::exists($default)) { - // A source checkout has the realtime compiler on disk already. - return $default; - } - return $this->createServerScript(); } protected function getEnvironmentVariables(): array { - return array_merge(parent::getEnvironmentVariables(), [ + $environment = array_merge(parent::getEnvironmentVariables(), [ 'HYDE_AUTOLOAD_PATH' => $this->resourcePath('vendor/autoload.php'), 'HYDE_BOOTSTRAP_PATH' => $this->resourcePath('app/bootstrap.php'), 'HYDE_WORKING_DIR' => $this->laravel->basePath(), 'HYDE_TEMP_DIR' => $this->temporaryDirectory(), ]); + + if ($this->laravel->make(Project::class)->isPortable()) { + $environment['HYDE_BUNDLED_STYLESHEET'] = BundledStylesheet::path(); + } + + return $environment; } /** @@ -81,6 +81,7 @@ protected function createServerScript(): string $path = $this->temporaryDirectory().'/bin/server.php'; $server = var_export($this->resourcePath('vendor/hyde/realtime-compiler/bin/server.php'), true); + $autoload = var_export($this->resourcePath('vendor/autoload.php'), true); File::ensureDirectoryExists(dirname($path)); @@ -90,7 +91,22 @@ protected function createServerScript(): string // Runs the realtime compiler out of the Hyde application archive. // Generated by `hyde serve`; safe to delete. - return require $server; + require $autoload; + + if (getenv('HYDE_BUNDLED_STYLESHEET') !== false + && \App\Support\BundledStylesheet::servesRequest( + (string) (\$_SERVER['REQUEST_URI'] ?? '/'), + (string) (getenv('HYDE_WORKING_DIR') ?: getcwd()), + (string) (getenv('HYDE_SERVER_MEDIA_DIRECTORY') ?: '_media'), + (string) (getenv('HYDE_SERVER_MEDIA_OUTPUT_DIRECTORY') ?: 'media'), + )) { + header('Content-Type: text/css'); + echo \App\Support\BundledStylesheet::contents((string) getenv('HYDE_BUNDLED_STYLESHEET')); + + return; + } + + require $server; PHP); return $path; diff --git a/app/Support/BundledStylesheet.php b/app/Support/BundledStylesheet.php index 64709b08..7846ba70 100644 --- a/app/Support/BundledStylesheet.php +++ b/app/Support/BundledStylesheet.php @@ -9,6 +9,12 @@ use function dirname; use function file_exists; +use function file_get_contents; +use function is_file; +use function parse_url; +use function rtrim; +use function str_replace; +use function trim; /** Locates the production stylesheet carried by the CLI application. */ final class BundledStylesheet @@ -29,4 +35,44 @@ public static function path(): string throw new RuntimeException('The bundled Hyde app.css stylesheet is missing. Rebuild the CLI or sync the Hyde develop checkout.'); } + + /** The virtual source path represented by the fallback. */ + public static function sourcePath(string $mediaDirectory): string + { + return trim($mediaDirectory, '/\\').'/'.RuntimeManager::STYLESHEET_FILE; + } + + /** The URL path at which the fallback is published by Hyde. */ + public static function outputPath(string $mediaOutputDirectory): string + { + return trim($mediaOutputDirectory, '/\\').'/'.RuntimeManager::STYLESHEET_FILE; + } + + /** + * Whether a server request should be answered by the bundled fallback. + * + * This is intentionally independent of `_media`: the configured media directory is + * the source of truth, and an existing source file always remains authoritative. + */ + public static function servesRequest( + string $requestUri, + string $projectRoot, + string $mediaDirectory, + string $mediaOutputDirectory, + ): bool { + $requestPath = (string) (parse_url($requestUri, PHP_URL_PATH) ?: $requestUri); + $requestPath = trim(str_replace('\\', '/', $requestPath), '/'); + + if ($requestPath !== self::outputPath($mediaOutputDirectory)) { + return false; + } + + return ! is_file(rtrim($projectRoot, '/\\').'/'.self::sourcePath($mediaDirectory)); + } + + /** Read the bundled bytes for the virtual stylesheet. */ + public static function contents(?string $path = null): string + { + return (string) file_get_contents($path ?: self::path()); + } } diff --git a/bin/build-manual.php b/bin/build-manual.php index fb4b2501..5c249b9b 100644 --- a/bin/build-manual.php +++ b/bin/build-manual.php @@ -50,8 +50,9 @@ $theme = get_theme_key(get_default_ansi_theme()); $template = get_template(); $version = parse_version(trim(hyde_exec('--version --no-ansi', true))); + $portableSection = portable_manual_html(); - $data = compact(['themes', 'themeSelector', 'theme', 'entries', 'template', 'version']); + $data = compact(['themes', 'themeSelector', 'theme', 'entries', 'template', 'version', 'portableSection']); $manual = view($template, $data); @@ -65,9 +66,74 @@ task('building|built', 'Markdown manual', function (): void { $md = hyde_exec('list --format=md --no-ansi', true); - file_put_contents('docs/manual/manual.md', $md); + file_put_contents('docs/manual/manual.md', portable_manual_markdown()."\n\n".$md); }); +/** The conceptual part of the manual is maintained here beside the generated commands. */ +function portable_manual_markdown(): string +{ + return <<<'MD' +## Portable sites + +A Portable project can be content and configuration only: + +``` +_pages/ +_posts/ +_media/ +_static/ +hyde.yml +``` + +It does not need a local PHP or Composer installation. Choose Portable when you want the +smallest, easiest-to-copy site and do not need Composer addons or custom PHP dependencies. +Choose a Composer project when you need those extensions; it uses the dependencies declared by +the project and keeps its own asset behavior. + +### Default styling + +The standalone Hyde executable includes Hyde's production `app.css`. A fresh Portable site is +therefore styled even though `_media/app.css` does not exist. Both `hyde build` and `hyde serve` +use this bundled default, so the standard site works offline without Tailwind Play CDN, Vite, +Node, npm, or the Hyde stylesheet CDN. + +### Custom styling and media directories + +Create `_media/app.css` to override the bundled stylesheet. Hyde serves or builds the user file, +does not expose the bundled replacement, and never modifies or overwrites the source file. If +the media directory is configured as `assets`, the corresponding source and virtual stylesheet +path is `assets/app.css`, and the served/generated URL is the configured media output path. +MD; +} + +function portable_manual_html(): string +{ + return <<<'HTML' +
+

Portable sites

+

A Portable project can consist only of:

+
_pages/
+    _posts/
+    _media/
+    _static/
+    hyde.yml
+

It needs no local PHP or Composer installation. Choose Portable for a content-only site; + choose a Composer project when you need addons or custom PHP dependencies. Composer projects + use the dependencies and asset behavior declared by the project.

+

Default styling

+

The standalone executable includes Hyde's production app.css. A fresh + Portable site is styled even though _media/app.css does not exist. Both + hyde build and hyde serve use this bundled default, offline and + without Tailwind Play CDN, Vite, Node, npm, or the Hyde stylesheet CDN.

+

Custom styling

+

Creating _media/app.css overrides the bundled default. The CLI never changes + or overwrites the user file. When the media directory is configured as assets, + the virtual stylesheet is assets/app.css and its served/generated URL follows + the configured media output path.

+
+ HTML; +} + /** Execute a command in the Hyde CLI and return the output. */ function hyde_exec(string $command, bool $cache = false): string { @@ -327,7 +393,7 @@ function get_template(): string -
{{ entries }}
+
{{ portableSection }}{{ entries }}