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
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
"milpa/tool-runtime": ">=0.13 <1.0",
"nyholm/psr7": "^1.8",
"nyholm/psr7-server": "^1.1",
"milpa/live-web": ">=0.26.1 <1.0"
"milpa/live-web": ">=0.27 <1.0"
},
"conflict": {
"milpa/agent": "<0.5",
Expand Down
27 changes: 23 additions & 4 deletions src/Plugins/HelloPlugin/Controllers/HomeController.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,10 @@
use Milpa\Live\Components\CodeBlockComponent;
use Milpa\Live\Contracts\Component\ComponentDefinitionInterface;
use Milpa\Live\Contracts\Rendering\ComponentRendererInterface;
use App\Plugins\HelloPlugin\HelloPlugin;
use Milpa\Live\Rendering\BrandMarkHtmlRenderer;
use Milpa\Live\Rendering\CodeBlockHtmlRenderer;
use Milpa\Live\Support\DesignTokens;
use Milpa\Live\ValueObjects\ComponentContext;
use Milpa\Live\ValueObjects\RenderRequest;
use Milpa\Live\ValueObjects\RenderTarget;
Expand Down Expand Up @@ -223,12 +225,30 @@ private function elsewhere(): string
return $html;
}

/**
* The design system's tags, from the authority rather than from typing.
*
* The three filenames used to be spelled here, a fourth time in this family, and the prefix a
* fourth WAY — which is how `/design/` came to exist next to `/webauthn/`, `/admin/assets/` and
* `/live/` without anyone choosing four. {@see HelloPlugin::designPrefix()} owns the prefix and
* {@see DesignTokens::urls()} owns the filenames; this method owns neither
* (greenhouse decisions/0308).
*/
private static function designLinks(): string
{
$design = DesignTokens::urls(HelloPlugin::designPrefix());

return '<link rel="stylesheet" href="' . $design[DesignTokens::TOKENS] . '">'
. '<link rel="stylesheet" href="' . $design[DesignTokens::FONTS] . '">'
. DesignTokens::iconLink($design[DesignTokens::APP_ICON]);
}

private function html(): string
{
$assets = self::assets();

return \str_replace(
['__GREETING__', '__MARK__', '__DOOR__', '__WAYS_OUT__', '__ELSEWHERE__', '__STYLES__', '__SCRIPTS__'],
['__GREETING__', '__MARK__', '__DOOR__', '__WAYS_OUT__', '__ELSEWHERE__', '__DESIGN__', '__STYLES__', '__SCRIPTS__'],
[
htmlspecialchars($this->greeting, \ENT_QUOTES, 'UTF-8'),
// READY, not `sown`: the mark reports what the surface is doing, and this page has
Expand All @@ -238,6 +258,7 @@ private function html(): string
self::door(),
self::waysOut(),
$this->elsewhere(),
self::designLinks(),
$assets->styleTag(),
$assets->scriptTag(),
],
Expand All @@ -248,9 +269,7 @@ private function html(): string
<meta charset="utf-8">
<title>Milpa is running</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="/design/milpa-tokens.css">
<link rel="stylesheet" href="/design/milpa-fonts.css">
<link rel="icon" type="image/svg+xml" href="/design/milpa-app-icon.svg">
__DESIGN__
__STYLES__
<style>
/* LAYOUT AND WORDS ONLY — no colour of its own, and nothing that a component owns.
Expand Down
20 changes: 18 additions & 2 deletions src/Plugins/HelloPlugin/HelloPlugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,9 @@ public function uninstall(): void
{
}

/** This house's design-system mount point — see {@see designPrefix()}. */
private const string DESIGN_PREFIX = '/design';

public function enable(): void
{
}
Expand All @@ -105,6 +108,19 @@ public function disable(): void
{
}

/**
* Where this house mounts the design system — the one place the prefix is typed.
*
* The five files under it come from {@see \Milpa\Live\Support\DesignTokens::urls()} rather than from typing, and the
* two route PATTERNS below (`{file}`, `{face}`) are this house's shape, which no authority can
* produce for it. What is shared is the prefix and the filenames; the shape stays local
* (greenhouse decisions/0308).
*/
public static function designPrefix(): string
{
return self::DESIGN_PREFIX;
}

/** @return list<Route> */
public function routes(): array
{
Expand All @@ -120,13 +136,13 @@ public function routes(): array
// and two of them were unreadable. A page that can link the tokens never has to invent a
// colour — see {@see DesignController} (greenhouse decisions/0298).
new Route(
path: '/design/{file}',
path: self::DESIGN_PREFIX . '/{file}',
methods: HttpMethod::GET,
name: 'design.file',
handler: new HandlerReference(DesignController::class, 'file'),
),
new Route(
path: '/design/fonts/{face}',
path: self::DESIGN_PREFIX . '/fonts/{face}',
methods: HttpMethod::GET,
name: 'design.face',
handler: new HandlerReference(DesignController::class, 'face'),
Expand Down
10 changes: 9 additions & 1 deletion tests/Console/ApplicationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -464,6 +464,14 @@ public function testRepairWithoutAPackageSaysHowItIsUsed(): void
$texto = (string) ob_get_clean();

self::assertSame(1, $codigo);
self::assertStringContainsString('uso: coa repair', $texto);
// 🚨 THE SHAPE, NOT THE SENTENCE. This asserted `uso: coa repair` — a dependency's exact copy,
// in the language it happened to be written in. `milpa/app-runtime` translated that line and
// CI went red here while this suite stayed green locally, because the lock pinned v0.151.0 and
// CI's `composer require --dev` re-resolved to v0.151.4. A package behind the registry does
// not break the run; it makes the suite lie (greenhouse decisions/0308).
//
// What this test actually cares about is that a bare `repair` REFUSES and names the argument
// it wanted. The wording is app-runtime's to choose and to internationalize.
self::assertStringContainsString('repair <', $texto, 'it names the argument it was missing');
}
}
123 changes: 123 additions & 0 deletions tests/Plugins/TheHouseDerivesItsDesignUrlsTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
<?php

/**
* This file is part of milpa/framework.
*
* (c) Rodrigo Vicente - TeamX Agency — https://teamx.agency <hola@teamx.agency>
*
* @license Apache-2.0
*
* @link https://github.com/getmilpa/framework
*/

declare(strict_types=1);

namespace App\Tests\Plugins;

use App\Plugins\HelloPlugin\HelloPlugin;
use Milpa\Container\DIContainer;
use Milpa\Live\Support\DesignTokens;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\TestCase;

/**
* 🚨 THIS HOUSE DERIVES ITS DESIGN-SYSTEM URLS; IT TYPES THE PREFIX ONCE AND NOTHING ELSE.
*
* `decisions/0243` stopped the tokens being COPIED — three packages carried identical copies that had
* drifted, all three missing `--space-32`, and now the file ships in `milpa/live-web`. What it did not
* stop was the TYPING, and this house is the proof of the cost: `/design/` was invented here in the
* same afternoon that `/webauthn/`, `/admin/assets/` and `/live/` already existed, by someone who
* could not see the other three from where they were writing (greenhouse decisions/0308).
*
* A prefix per host is not the defect — it is a decision written where it is made
* ({@see DesignTokens::iconLink()}: «each serves this file from its own asset route, with its own
* cache policy»), because a plugin whose pages work the moment it is installed cannot depend on
* another plugin's routes being mounted. Four prefixes nobody chose is the defect.
*/
#[CoversClass(HelloPlugin::class)]
final class TheHouseDerivesItsDesignUrlsTest extends TestCase
{
/**
* 🚨 NO STRING IN `src/` SPELLS A DESIGN-SYSTEM FILENAME OR REPEATS THE PREFIX.
*
* A guard, not an example. The three `<link>`s that used to live in the page's `<head>` were the
* fourth copy of those filenames in this family, and the page could not have known: nothing it
* imported mentioned the other three. What a test can see, a reader cannot.
*/
public function testNoStringInTheSourceSpellsADesignUrl(): void
{
$offenders = [];

foreach (self::phpFiles(\dirname(__DIR__, 2) . '/src') as $file) {
foreach (explode("\n", (string) file_get_contents($file)) as $n => $line) {
$trimmed = ltrim($line);
// Comments and docblocks name these URLs to explain them; that is prose, not a link.
if ($trimmed === '' || str_starts_with($trimmed, '*') || str_starts_with($trimmed, '//') || str_starts_with($trimmed, '/*')) {
continue;
}
// The one exemption: something has to say where this house mounts them, and it says it once.
if (str_contains($line, 'DESIGN_PREFIX = ')) {
continue;
}
$spellsAFile = preg_match('#[\'"][^\'"]*milpa-(tokens|fonts|wordmark|wordmark-light|app-icon)\.(css|svg)#', $line) === 1;
$repeatsThePrefix = preg_match("#['\"]/design#", $line) === 1;
if ($spellsAFile || $repeatsThePrefix) {
$offenders[] = basename($file) . ':' . ($n + 1) . ' ' . trim($line);
}
}
}

self::assertSame([], $offenders, "derive these from DesignTokens::urls(HelloPlugin::designPrefix()) instead:\n" . implode("\n", $offenders));
}

/**
* The routes this house declares for the system sit under the prefix it declares.
*
* The route PATTERNS (`{file}`, `{face}`) are this house's own shape and no authority can produce
* them for it — one catch-all here, five named routes in the passkey door, and both are right for
* their host. What moved is the prefix and the filenames; the shape stayed local.
*/
public function testTheDesignRoutesSitUnderTheDeclaredPrefix(): void
{
$paths = [];
foreach ((new HelloPlugin(new DIContainer()))->routes() as $route) {
if (str_contains($route->path, 'design') || str_contains($route->path, 'fonts')) {
$paths[] = $route->path;
}
}

self::assertSame([
HelloPlugin::designPrefix() . '/{file}',
HelloPlugin::designPrefix() . '/fonts/{face}',
], $paths);

// And the URLs the page links resolve INTO those patterns — the stylesheet's own relative
// `url('fonts/…')` is why the second one exists at all.
$urls = DesignTokens::urls(HelloPlugin::designPrefix());
self::assertSame(HelloPlugin::designPrefix() . '/' . DesignTokens::TOKENS, $urls[DesignTokens::TOKENS]);

$faces = array_diff_key($urls, array_flip([
DesignTokens::TOKENS, DesignTokens::FONTS,
DesignTokens::WORDMARK, DesignTokens::WORDMARK_LIGHT, DesignTokens::APP_ICON,
]));
self::assertNotSame([], $faces, 'a stylesheet whose faces are not served renders as missing type, not as an error');
foreach ($faces as $url) {
self::assertStringStartsWith(HelloPlugin::designPrefix() . '/fonts/', $url);
}
}

/**
* @return list<string>
*/
private static function phpFiles(string $root): array
{
$found = [];
foreach (new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($root)) as $file) {
if ($file instanceof \SplFileInfo && $file->getExtension() === 'php') {
$found[] = $file->getPathname();
}
}

return $found;
}
}
Loading