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
34 changes: 25 additions & 9 deletions app/Commands/ServeCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}

/**
Expand All @@ -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));

Expand All @@ -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;
Expand Down
46 changes: 46 additions & 0 deletions app/Support/BundledStylesheet.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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());
}
}
73 changes: 69 additions & 4 deletions bin/build-manual.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -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'
<section>
<h2>Portable sites</h2>
<p>A Portable project can consist only of:</p>
<pre>_pages/
_posts/
_media/
_static/
hyde.yml</pre>
<p>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.</p>
<h3>Default styling</h3>
<p>The standalone executable includes Hyde's production <code>app.css</code>. A fresh
Portable site is styled even though <code>_media/app.css</code> does not exist. Both
<code>hyde build</code> and <code>hyde serve</code> use this bundled default, offline and
without Tailwind Play CDN, Vite, Node, npm, or the Hyde stylesheet CDN.</p>
<h3>Custom styling</h3>
<p>Creating <code>_media/app.css</code> overrides the bundled default. The CLI never changes
or overwrites the user file. When the media directory is configured as <code>assets</code>,
the virtual stylesheet is <code>assets/app.css</code> and its served/generated URL follows
the configured media output path.</p>
</section>
HTML;
}

/** Execute a command in the Hyde CLI and return the output. */
function hyde_exec(string $command, bool $cache = false): string
{
Expand Down Expand Up @@ -327,7 +393,7 @@ function get_template(): string
</a>
</menu>
</nav>
<main>{{ entries }}</main>
<main>{{ portableSection }}{{ entries }}</main>
<footer>
<p>
Manual for the <a href="https://hydephp.github.io/cli?ref=manual">HydePHP CLI</a> - Version {{ version }}
Expand Down Expand Up @@ -358,4 +424,3 @@ function view(string $template, array $data): string

return $template;
}

39 changes: 31 additions & 8 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,11 @@ A Portable project cannot load Composer addons. There is no hybrid autoloading,
local `vendor/autoload.php` is never merged into the embedded dependency graph.

Portable projects remain content/configuration-only. The executable supplies the production
Hyde stylesheet as a read-only fallback during a build when `_media/app.css` is absent; it is
transferred to the normal compiled `media/app.css` location. `hyde new` does not create the
source stylesheet, and a user-provided file always takes precedence.
Hyde stylesheet as a read-only fallback during a build or preview when the configured media
directory does not contain `app.css`; it is transferred to the normal compiled media location
for a build and exposed at that location by the preview server. `hyde new` does not create the
source stylesheet, and a user-provided file always takes precedence. This keeps the source tree
content-only while making a fresh site look like a normal Hyde site offline.

### Composer project

Expand Down Expand Up @@ -189,11 +191,32 @@ hyde = micro.sfx ++ hyde.phar

The production Hyde stylesheet is copied from the v3 develop checkout into `runtime/app.css`
while the archive is assembled. In Portable mode the embedded application exposes that file
through a read-only media overlay only when the project has no `_media/app.css`. The framework
therefore continues to use its normal local `Asset::exists('app.css')` path: user stylesheets
win, the source `_media/` directory is never modified, and the generated site receives the
stylesheet at `media/app.css`. Composer projects are not given this overlay and retain their
own asset behavior.
through a read-only media overlay only when the project has no `app.css` in its configured media
directory. The virtual source path is derived from that configured directory, not from a
`_media/app.css` suffix test. The framework therefore continues to use its normal local
`Asset::exists('app.css')` path: user stylesheets win, the source media directory is never
modified, and the generated site receives the stylesheet at the configured media output path
(normally `media/app.css`). Composer projects are not given this overlay and retain their own
asset behavior.

`hyde build` reaches the fallback through two matching abstractions: `PortableHydeKernel` uses
the Portable filesystem to discover the virtual media file, and the Illuminate filesystem
binding installed by `app/bootstrap.php` reads, hashes, and sizes the bytes from
`runtime/app.css`. No fallback file is written to the project. The same bundled-stylesheet
abstraction also supplies the resource path and virtual source/output paths to `hyde serve`.

Serving has an additional process boundary. The executable is a micro SAPI and cannot run
`php -S`, so `ServeCommand` extracts the bundled PHP CLI and starts it with a generated router
script. That child loads the bundled realtime compiler and is given `HYDE_WORKING_DIR`,
`HYDE_BOOTSTRAP_PATH`, the media source/output directories, and the archive's runtime resource
path. A page request enters the realtime compiler, which loads `app/bootstrap.php`; launcher
detection therefore still classifies the project as Portable and installs the Portable kernel
and filesystem. Media requests are intentionally handled before that application boot, so the
generated router script asks the same stylesheet abstraction whether the request is the virtual
configured `media-output/app.css` path. If the user's source file exists, the normal realtime
compiler path serves it; otherwise the router reads the bundled archive resource. This is why
both page and stylesheet requests work without PHP, Composer, Node, Vite, Tailwind Play CDN, or
network access.

### Why a second PHP is embedded

Expand Down
Loading
Loading