Assemble the site's pages in one place, with the design system at /design-system/
Human
Prompt
Task 1 of Fiesta part 3: internal/www with the design system mounted at /design-system/, the -site flag, the gate change and the stand-in landing page.
From this commit's own message. The prompt ledger has no entries for it — which is what a squashed, rebased or amended commit looks like, and what every commit looks like in a repo without a ledger.
Summary
internal/www.Pages renders the gallery for /design-system/, copies site/static/ and writes the landing page; fiesta site takes -site instead of -pages and builds through www.Pages and www.FS; site/index.html is deleted and the placeholder's words move into www as the stand-in landing page; internal/www@2 joins BROWSER_PKGS and the suite step becomes 50-suite@12; site/FOLLOW-UPS.md lists what Fiesta 1.1 should take in from site/static/.
Agent notes
The www browser slices run the package's untagged tests under -tags browser until Task 4 adds browser tests; hack/gotest.sh lists untagged tests under the tag, so neither slice is empty. internal/site/site.go still says -pages in one comment; it is outside this task's files.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
9 files changed,
+386
−50
.amadan/ci.d/{50-suite@10 => 50-suite@12}+0 −0Makefile+10 −8cmd/fiesta/main.go+24 −14cmd/fiesta/release_test.go+16 −12internal/www/fs.go+18 −0internal/www/www.go+128 −0internal/www/www_test.go+180 −0site/FOLLOW-UPS.md+10 −0site/index.html+0 −16
CI passed — run details
CI log
=== gofmt ===
=== no-rastrillo ===
go test -run '^(TestFiestaNeverImportsRastrillo|TestThePublicPackagesLinkNoBrowser)$' -count=1 .
ok amadan.net/rastrillo/fiesta 0.108s
=== size ===
go test ./internal/build -run '^TestTheOneLinkIsUnder25KiBGzipped$' -count=1 -v
=== RUN TestTheOneLinkIsUnder25KiBGzipped
build_test.go:138: fiesta-plain.css: 152856 bytes, 22303 bytes gzipped (21.8 KiB of the 25.0 KiB limit)
build_test.go:138: fiesta-signal.css: 152951 bytes, 22414 bytes gzipped (21.9 KiB of the 25.0 KiB limit)
build_test.go:138: fiesta.css: 155770 bytes, 23018 bytes gzipped (22.5 KiB of the 25.0 KiB limit)
--- PASS: TestTheOneLinkIsUnder25KiBGzipped (0.02s)
PASS
ok amadan.net/rastrillo/fiesta/internal/build 0.022s
=== suite ===
--- shard 0 of 12 ---
==> shard 0 of 12: make root
go build ./...
go vet ./...
FIESTA_TEST_REQUIRE_NODE=1 FIESTA_TEST_REQUIRE_GIT=1 go test -count=1 ./...
ok amadan.net/rastrillo/fiesta 1.495s
ok amadan.net/rastrillo/fiesta/check 4.398s
ok amadan.net/rastrillo/fiesta/cmd/fiesta 5.523s
ok amadan.net/rastrillo/fiesta/example 0.302s
ok amadan.net/rastrillo/fiesta/internal/axe 0.134s
ok amadan.net/rastrillo/fiesta/internal/build 1.472s
ok amadan.net/rastrillo/fiesta/internal/codeview 0.023s
ok amadan.net/rastrillo/fiesta/internal/gallery 2.277s
? amadan.net/rastrillo/fiesta/internal/harness [no test files]
ok amadan.net/rastrillo/fiesta/internal/markup 0.017s
ok amadan.net/rastrillo/fiesta/internal/nodetest 0.383s
ok amadan.net/rastrillo/fiesta/internal/site 3.158s
ok amadan.net/rastrillo/fiesta/internal/tokens 0.016s
ok amadan.net/rastrillo/fiesta/internal/www 2.541s
--- shard 1 of 12 ---
==> shard 1 of 12: make staticcheck
go run honnef.co/go/tools/cmd/staticcheck@v0.8.2 -tags browser ./...
--- shard 2 of 12 ---
==> shard 2 of 12: make govulncheck
go run golang.org/x/vuln/cmd/govulncheck@v1.8.0 ./...
=== Symbol Results ===
No vulnerabilities found.
Your code is affected by 0 vulnerabilities.
This scan also found 2 vulnerabilities in packages you import and 5
vulnerabilities in modules you require, but your code doesn't appear to call
these vulnerabilities.
Use '-show verbose' for more details.
--- shard 3 of 12 ---
==> shard 3 of 12: make gitleaks
go run github.com/zricethezav/gitleaks/v8@v8.30.1 git --no-banner --redact --log-opts='--full-history --diff-filter=tuxdb HEAD' .
[90m10:05AM[0m [32mINF[0m [1mUnknown SCM platform. Use --platform to include links in findings.[0m [36mhost=[0mamadan.net
[90m10:05AM[0m [32mINF[0m [1m1 commits scanned.[0m
[90m10:05AM[0m [32mINF[0m [1mscanned ~5484677 bytes (5.48 MB) in 3.52s[0m
[90m10:05AM[0m [32mINF[0m [1mno leaks found[0m
--- shard 4 of 12 ---
==> shard 4 of 12: make browser.internal.harness
./hack/gotest.sh browser 'internal.harness'
gotest: on DOCS-T1, driving the harness's own choice (/usr/bin/chromium)
ok amadan.net/rastrillo/fiesta/internal/harness 15.652s
--- shard 5 of 12 ---
==> shard 5 of 12: make browser.check~1of3
./hack/gotest.sh browser 'check~1of3'
gotest: on DOCS-T1, driving the harness's own choice (/usr/bin/chromium)
gotest: slice 1 of 3 of ./check/ runs 95 of 286 tests
ok amadan.net/rastrillo/fiesta/check 219.786s
--- shard 6 of 12 ---
==> shard 6 of 12: make browser.check~2of3
./hack/gotest.sh browser 'check~2of3'
gotest: on DOCS-T1, driving the harness's own choice (/usr/bin/chromium)
gotest: slice 2 of 3 of ./check/ runs 95 of 286 tests
ok amadan.net/rastrillo/fiesta/check 244.866s
--- shard 7 of 12 ---
==> shard 7 of 12: make browser.check~3of3
./hack/gotest.sh browser 'check~3of3'
gotest: on DOCS-T1, driving the harness's own choice (/usr/bin/chromium)
gotest: slice 3 of 3 of ./check/ runs 96 of 286 tests
ok amadan.net/rastrillo/fiesta/check 238.964s
--- shard 8 of 12 ---
==> shard 8 of 12: make browser.internal.gallery~1of2
./hack/gotest.sh browser 'internal.gallery~1of2'
gotest: on DOCS-T1, driving the harness's own choice (/usr/bin/chromium)
gotest: slice 1 of 2 of ./internal/gallery/ runs 1 of 13 tests
ok amadan.net/rastrillo/fiesta/internal/gallery 74.524s
--- shard 9 of 12 ---
==> shard 9 of 12: make browser.internal.gallery~2of2
./hack/gotest.sh browser 'internal.gallery~2of2'
gotest: on DOCS-T1, driving the harness's own choice (/usr/bin/chromium)
gotest: slice 2 of 2 of ./internal/gallery/ runs 12 of 13 tests
ok amadan.net/rastrillo/fiesta/internal/gallery 23.662s
--- shard 10 of 12 ---
==> shard 10 of 12: make browser.internal.www~1of2
./hack/gotest.sh browser 'internal.www~1of2'
gotest: on DOCS-T1, driving the harness's own choice (/usr/bin/chromium)
gotest: slice 1 of 2 of ./internal/www/ runs 3 of 5 tests
ok amadan.net/rastrillo/fiesta/internal/www 2.561s
--- shard 11 of 12 ---
==> shard 11 of 12: make browser.internal.www~2of2
./hack/gotest.sh browser 'internal.www~2of2'
gotest: on DOCS-T1, driving the harness's own choice (/usr/bin/chromium)
gotest: slice 2 of 2 of ./internal/www/ runs 2 of 5 tests
ok amadan.net/rastrillo/fiesta/internal/www 0.946s
Intent
diff --git a/.amadan/ci.d/50-suite@10 b/.amadan/ci.d/50-suite@12| similarity index 100% |
| rename from .amadan/ci.d/50-suite@10 |
| rename to .amadan/ci.d/50-suite@12 |
diff --git a/Makefile b/Makefile| index 935dd8d..57b1c8f 100644 |
| --- a/Makefile |
| +++ b/Makefile |
| @@ -54,7 +54,7 @@ ci: gofmt no-rastrillo size suite |
| # check/drives@2 would become browser.check.drives~1of2 and ~2of2. |
| # The tasks that add a browser package append here and rename |
| # 50-suite@N to match. |
| -BROWSER_PKGS := internal/harness check@3 internal/gallery@2 |
| +BROWSER_PKGS := internal/harness check@3 internal/gallery@2 internal/www@2 |
| go-roles = $(foreach p,$(2),$(if $(findstring @,$(p)),$(call go-slices,$(1),$(subst /,.,$(word 1,$(subst @, ,$(p)))),$(word 2,$(subst @, ,$(p)))),$(1).$(subst /,.,$(p)))) |
| go-slices = $(foreach k,$(shell seq 1 $(3)),$(1).$(2)~$(k)of$(3)) |
| BROWSER_ROLES := $(call go-roles,browser,$(BROWSER_PKGS)) |
| @@ -188,14 +188,16 @@ browser.%: | $(BIN)/tmp |
| ./hack/gotest.sh browser '$*' |
| # site assembles fiesta.rastrillo.org into .build/site/public from the |
| -# committed releases and the pages: each release under its exact |
| -# version, _redirects sending /M/* to the newest M.y.z, and site/ at |
| -# the root. .build/site/public is the tree to ship; the command's own |
| -# .fiesta stamp sits above it in .build/site, so nothing hidden is in |
| -# the shipped tree. Nothing in ci depends on this target, since the |
| -# releases are committed and checked there. |
| +# committed releases and the site source: each release under its exact |
| +# version, _redirects sending /M/* to the newest M.y.z, and at the root |
| +# the pages internal/www assembles from site/, the landing page, the |
| +# design system under /design-system/ and site/static/. The tree is |
| +# .build/site/public; the command's own .fiesta stamp sits above it in |
| +# .build/site, so nothing hidden is in the shipped tree. Nothing in ci |
| +# depends on this target, since the releases are committed and checked |
| +# there. |
| site: | $(BIN)/tmp |
| - go run ./cmd/fiesta site -releases releases -pages site -out .build/site |
| + go run ./cmd/fiesta site -releases releases -site site -out .build/site |
| root no-rastrillo size staticcheck govulncheck gitleaks: | $(BIN)/tmp |
diff --git a/cmd/fiesta/main.go b/cmd/fiesta/main.go| index 053a511..1c50a1f 100644 |
| --- a/cmd/fiesta/main.go |
| +++ b/cmd/fiesta/main.go |
| @@ -29,13 +29,16 @@ |
| // an unpublished candidate may be rebuilt until it is tagged. See |
| // release. |
| // |
| -// fiesta site -releases DIR -pages DIR -out DIR |
| +// fiesta site -releases DIR -site DIR -out DIR |
| // |
| -// assembles the site from the releases and the pages, exactly what |
| -// internal/site.Build returns, into DIR/public/: each release under |
| -// its exact version, _redirects sending /M/* to the newest M.y.z, and |
| -// the pages at the root. DIR/public/ is the tree to ship, and the |
| -// stamp sits above it in DIR, so nothing hidden is ever deployed. |
| +// assembles the site from the releases and the site source, exactly |
| +// what internal/site.Build returns, into DIR/public/: each release |
| +// under its exact version, _redirects sending /M/* to the newest |
| +// M.y.z, and the pages at the root. The pages are what internal/www |
| +// assembles from the site source: the landing page, the design system |
| +// under /design-system/, and the source's static/ files. DIR/public/ |
| +// is the tree to ship, and the stamp sits above it in DIR, so nothing |
| +// hidden is ever deployed. |
| // |
| // Each command owns the directory it writes. It empties it before it |
| // writes, so what is there afterwards is the output and only the |
| @@ -61,10 +64,11 @@ import ( |
| "amadan.net/rastrillo/fiesta/internal/build" |
| "amadan.net/rastrillo/fiesta/internal/gallery" |
| "amadan.net/rastrillo/fiesta/internal/site" |
| + "amadan.net/rastrillo/fiesta/internal/www" |
| ) |
| const usage = "usage: fiesta build -out DIR | fiesta gallery -out DIR [-mount /path] | " + |
| - "fiesta release -version X.Y.Z [-releases DIR] [-candidate] | fiesta site -releases DIR -pages DIR -out DIR" |
| + "fiesta release -version X.Y.Z [-releases DIR] [-candidate] | fiesta site -releases DIR -site DIR -out DIR" |
| func main() { |
| if err := run(os.Args[1:], os.Stderr); err != nil { |
| @@ -159,17 +163,17 @@ func releaseCmd(args []string, stderr io.Writer) error { |
| func siteCmd(args []string, stderr io.Writer) error { |
| flags := flag.NewFlagSet("site", flag.ContinueOnError) |
| releases := flags.String("releases", "", "the directory of committed releases, one X.Y.Z/ each") |
| - pages := flags.String("pages", "", "the directory of pages for the site's root") |
| + siteDir := flags.String("site", "", "the site source: its static/ is copied to the site's root") |
| out := flags.String("out", "", "directory to write the site into") |
| if ok, err := parse(flags, args, stderr); !ok { |
| return err |
| } |
| - for _, f := range []struct{ name, value string }{{"-releases", *releases}, {"-pages", *pages}, {"-out", *out}} { |
| + for _, f := range []struct{ name, value string }{{"-releases", *releases}, {"-site", *siteDir}, {"-out", *out}} { |
| if f.value == "" { |
| return errors.New(f.name + " is required\n" + usage) |
| } |
| } |
| - return writeSite(*releases, *pages, *out) |
| + return writeSite(*releases, *siteDir, *out) |
| } |
| // write builds the release and puts it under out/<version>/, a |
| @@ -200,10 +204,16 @@ func writeGallery(out, mount string) error { |
| // keeps the command's own mark out too. |
| const shipDir = "public" |
| -// writeSite assembles the site from releases and pages and puts it in |
| -// out/public/, after claiming out. |
| -func writeSite(releases, pages, out string) error { |
| - files, err := site.Build(os.DirFS(releases), os.DirFS(pages)) |
| +// writeSite assembles the pages from the site source, then the site |
| +// from the releases and those pages, and puts it in out/public/, after |
| +// claiming out. A fault in the site source is reported on its own, |
| +// before the releases are read. |
| +func writeSite(releases, siteDir, out string) error { |
| + pages, err := www.Pages(fiesta.FS(), os.DirFS(siteDir)) |
| + if err != nil { |
| + return err |
| + } |
| + files, err := site.Build(os.DirFS(releases), www.FS(pages)) |
| if err != nil { |
| return err |
| } |
diff --git a/cmd/fiesta/release_test.go b/cmd/fiesta/release_test.go| index da9fa3a..9bef951 100644 |
| --- a/cmd/fiesta/release_test.go |
| +++ b/cmd/fiesta/release_test.go |
| @@ -13,6 +13,7 @@ import ( |
| "amadan.net/rastrillo/fiesta" |
| "amadan.net/rastrillo/fiesta/internal/build" |
| "amadan.net/rastrillo/fiesta/internal/site" |
| + "amadan.net/rastrillo/fiesta/internal/www" |
| ) |
| // requireGitEnv names the environment variable that turns a missing |
| @@ -355,30 +356,33 @@ func TestAnUnreachableOriginRefusesACandidate(t *testing.T) { |
| untouched(t, dir, "an unreachable origin") |
| } |
| -// TestSiteWritesWhatBuildReturns: `fiesta site -releases R -pages P |
| -// -out DIR` writes site.Build's tree into DIR/public, file for file, |
| -// stamps DIR itself as its own, and leaves nothing hidden in the tree |
| -// to ship. |
| +// TestSiteWritesWhatBuildReturns is a preservation test: `fiesta site |
| +// -releases R -site S -out DIR` writes site.Build's tree into |
| +// DIR/public, file for file, stamps DIR itself as its own, and leaves |
| +// nothing hidden in the tree to ship. The pages are what www.Pages |
| +// assembles from S, so the expected tree is computed the same way. |
| func TestSiteWritesWhatBuildReturns(t *testing.T) { |
| root := t.TempDir() |
| releases := filepath.Join(root, "releases") |
| if err := fiestaRelease(t, releases, fiesta.Version, false); err != nil { |
| t.Fatal(err) |
| } |
| - pages := filepath.Join(root, "pages") |
| - if err := os.MkdirAll(filepath.Join(pages, "themes"), 0o755); err != nil { |
| + siteDir := filepath.Join(root, "site") |
| + if err := os.MkdirAll(filepath.Join(siteDir, "static"), 0o755); err != nil { |
| t.Fatal(err) |
| } |
| - for name, text := range map[string]string{"index.html": "<h1>Fiesta</h1>\n", "themes/index.html": "<h1>Themes</h1>\n"} { |
| - if err := os.WriteFile(filepath.Join(pages, filepath.FromSlash(name)), []byte(text), 0o644); err != nil { |
| - t.Fatal(err) |
| - } |
| + if err := os.WriteFile(filepath.Join(siteDir, "static", "x.css"), []byte("body{margin:0}\n"), 0o644); err != nil { |
| + t.Fatal(err) |
| } |
| out := filepath.Join(root, "out", "site") |
| - if err := run([]string{"site", "-releases", releases, "-pages", pages, "-out", out}, io.Discard); err != nil { |
| + if err := run([]string{"site", "-releases", releases, "-site", siteDir, "-out", out}, io.Discard); err != nil { |
| t.Fatalf("fiesta site: %v", err) |
| } |
| - want, err := site.Build(os.DirFS(releases), os.DirFS(pages)) |
| + pages, err := www.Pages(fiesta.FS(), os.DirFS(siteDir)) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + want, err := site.Build(os.DirFS(releases), www.FS(pages)) |
| if err != nil { |
| t.Fatal(err) |
| } |
diff --git a/internal/www/fs.go b/internal/www/fs.go| new file mode 100644 |
| index 0000000..8dae1fe |
| --- /dev/null |
| +++ b/internal/www/fs.go |
| @@ -0,0 +1,18 @@ |
| +package www |
| + |
| +import ( |
| + "io/fs" |
| + "testing/fstest" |
| +) |
| + |
| +// FS presents a tree Pages returned as a file system, so site.Build |
| +// can take it where it takes a directory. Every entry is a regular |
| +// file with mode 0644, the mode cmd/fiesta writes files with, and |
| +// the directories between are implied by the paths. |
| +func FS(m map[string][]byte) fs.FS { |
| + out := make(fstest.MapFS, len(m)) |
| + for name, data := range m { |
| + out[name] = &fstest.MapFile{Data: data, Mode: 0o644} |
| + } |
| + return out |
| +} |
diff --git a/internal/www/www.go b/internal/www/www.go| new file mode 100644 |
| index 0000000..d06f924 |
| --- /dev/null |
| +++ b/internal/www/www.go |
| @@ -0,0 +1,128 @@ |
| +// Package www assembles the pages of fiesta.rastrillo.org: the landing |
| +// page at the root, the design system under Mount, and the site's own |
| +// static files. It is the half of the site that is not a release; |
| +// internal/site puts the two together. |
| +// |
| +// Pages returns the whole tree in memory, keyed by path relative to |
| +// the site root, and it is deterministic: the same tree from the same |
| +// source every time. |
| +// |
| +// index.html the landing page |
| +// design-system/... internal/gallery's tree, rendered for |
| +// Mount, so every URL in it is absolute |
| +// under /design-system/ |
| +// static/... the site's own files, copied from the |
| +// site source as they are |
| +package www |
| + |
| +import ( |
| + "fmt" |
| + "io/fs" |
| + "strings" |
| + |
| + "amadan.net/rastrillo/fiesta/internal/gallery" |
| +) |
| + |
| +// Mount is the URL path the design system is served from. The gallery |
| +// writes every URL it owns as an absolute path under it, so the tree |
| +// cannot be served anywhere else. |
| +const Mount = "/design-system" |
| + |
| +// staticDir is the directory under the site source whose files are |
| +// copied to the site at the same path. |
| +const staticDir = "static" |
| + |
| +// landing is the root page until the real one is built: the |
| +// placeholder the site went live with, in the same words. |
| +const landing = `<!doctype html> |
| +<html lang="en"> |
| +<head> |
| +<meta charset="utf-8"> |
| +<meta name="viewport" content="width=device-width, initial-scale=1"> |
| +<title>Fiesta</title> |
| +<link rel="stylesheet" href="/1/fiesta.css"> |
| +</head> |
| +<body> |
| +<main rst-page> |
| +<h1>Celebrate your new design system</h1> |
| +<p>One stylesheet and done. JS optional. <25KiB gzipped.</p> |
| +<pre style="white-space: pre-wrap; overflow-wrap: anywhere"><code><link rel="stylesheet" href="https://fiesta.rastrillo.org/1/fiesta.css"></code></pre> |
| +</main> |
| +</body> |
| +</html> |
| +` |
| + |
| +// hidden reports whether any segment of a slash path starts with a |
| +// dot: an editor's swap file, a Finder's .DS_Store, a stamp. |
| +func hidden(p string) bool { |
| + for _, seg := range strings.Split(p, "/") { |
| + if strings.HasPrefix(seg, ".") { |
| + return true |
| + } |
| + } |
| + return false |
| +} |
| + |
| +// Pages assembles the site's pages. src is fiesta.FS() or any tree |
| +// laid out the same way, and is what the design system renders from. |
| +// siteDir is the site source, the repository's site/ directory: its |
| +// static/ is copied whole, and the rest of it is the site's own |
| +// business, not published. A hidden file or directory anywhere in |
| +// siteDir is refused by name rather than skipped: the directory is |
| +// committed, so a dotfile there is a mistake someone should see. |
| +// |
| +// Each path is written once. The parts cannot overlap today, since |
| +// each writes under a directory of its own, but a part that grew into |
| +// another's would otherwise win or lose by map order. |
| +func Pages(src fs.FS, siteDir fs.FS) (map[string][]byte, error) { |
| + out := map[string][]byte{} |
| + put := func(name string, data []byte) error { |
| + if _, dup := out[name]; dup { |
| + return fmt.Errorf("www: %s is written twice", name) |
| + } |
| + out[name] = data |
| + return nil |
| + } |
| + |
| + ds, err := gallery.Render(src, gallery.Options{Mount: Mount}) |
| + if err != nil { |
| + return nil, fmt.Errorf("www: %w", err) |
| + } |
| + prefix := strings.TrimPrefix(Mount, "/") + "/" |
| + for name, data := range ds { |
| + if err := put(prefix+name, data); err != nil { |
| + return nil, err |
| + } |
| + } |
| + |
| + err = fs.WalkDir(siteDir, ".", func(p string, d fs.DirEntry, err error) error { |
| + if err != nil { |
| + return fmt.Errorf("www: reading the site source: %w", err) |
| + } |
| + if p == "." { |
| + return nil |
| + } |
| + if hidden(p) { |
| + return fmt.Errorf("www: %s is hidden, and a hidden file is never published; remove it from the site source", p) |
| + } |
| + if d.IsDir() || (p != staticDir && !strings.HasPrefix(p, staticDir+"/")) { |
| + return nil |
| + } |
| + if !d.Type().IsRegular() { |
| + return fmt.Errorf("www: %s is not a regular file", p) |
| + } |
| + data, err := fs.ReadFile(siteDir, p) |
| + if err != nil { |
| + return fmt.Errorf("www: reading %s: %w", p, err) |
| + } |
| + return put(p, data) |
| + }) |
| + if err != nil { |
| + return nil, err |
| + } |
| + |
| + if err := put("index.html", []byte(landing)); err != nil { |
| + return nil, err |
| + } |
| + return out, nil |
| +} |
diff --git a/internal/www/www_test.go b/internal/www/www_test.go| new file mode 100644 |
| index 0000000..44d4940 |
| --- /dev/null |
| +++ b/internal/www/www_test.go |
| @@ -0,0 +1,180 @@ |
| +package www_test |
| + |
| +import ( |
| + "bytes" |
| + "crypto/sha256" |
| + "encoding/hex" |
| + "sort" |
| + "strings" |
| + "testing" |
| + "testing/fstest" |
| + |
| + xhtml "golang.org/x/net/html" |
| + |
| + "amadan.net/rastrillo/fiesta" |
| + "amadan.net/rastrillo/fiesta/internal/site" |
| + "amadan.net/rastrillo/fiesta/internal/www" |
| +) |
| + |
| +// siteOf is a site source tree from path to content. |
| +func siteOf(files map[string]string) fstest.MapFS { |
| + m := fstest.MapFS{} |
| + for name, text := range files { |
| + m[name] = &fstest.MapFile{Data: []byte(text)} |
| + } |
| + return m |
| +} |
| + |
| +// pages runs Pages over Fiesta's own tree and siteDir, failed loudly. |
| +func pages(t *testing.T, siteDir fstest.MapFS) map[string][]byte { |
| + t.Helper() |
| + out, err := www.Pages(fiesta.FS(), siteDir) |
| + if err != nil { |
| + t.Fatalf("Pages: %v", err) |
| + } |
| + return out |
| +} |
| + |
| +// urlsOf reads every href and src on doc the way a browser would, |
| +// whatever the element, so a URL the gallery writes under its mount |
| +// and one a sample wrote are both seen. |
| +func urlsOf(t *testing.T, doc []byte) []string { |
| + t.Helper() |
| + var out []string |
| + z := xhtml.NewTokenizer(bytes.NewReader(doc)) |
| + for { |
| + switch z.Next() { |
| + case xhtml.ErrorToken: |
| + return out |
| + case xhtml.StartTagToken, xhtml.SelfClosingTagToken: |
| + _, more := z.TagName() |
| + for more { |
| + var k, v []byte |
| + k, v, more = z.TagAttr() |
| + if key := string(k); key == "href" || key == "src" { |
| + out = append(out, string(v)) |
| + } |
| + } |
| + } |
| + } |
| +} |
| + |
| +// TestPagesHasARootAndTheDesignSystem: the tree has a landing page at |
| +// the root and the design system's index under design-system/, and |
| +// neither is empty. |
| +func TestPagesHasARootAndTheDesignSystem(t *testing.T) { |
| + out := pages(t, siteOf(nil)) |
| + for _, name := range []string{"index.html", "design-system/index.html"} { |
| + if len(out[name]) == 0 { |
| + t.Errorf("Pages wrote no %s", name) |
| + } |
| + } |
| +} |
| + |
| +// TestTheDesignSystemIsMounted: on the design system's index and on |
| +// one example page, every root-relative href and src starts with |
| +// /design-system/, so the gallery served there asks for nothing from |
| +// the site's root. |
| +func TestTheDesignSystemIsMounted(t *testing.T) { |
| + out := pages(t, siteOf(nil)) |
| + for _, name := range []string{"design-system/index.html", "design-system/day/button.html"} { |
| + doc, ok := out[name] |
| + if !ok { |
| + t.Errorf("Pages wrote no %s", name) |
| + continue |
| + } |
| + rooted := 0 |
| + for _, u := range urlsOf(t, doc) { |
| + if !strings.HasPrefix(u, "/") { |
| + continue |
| + } |
| + rooted++ |
| + if !strings.HasPrefix(u, www.Mount+"/") { |
| + t.Errorf("%s links %s, which is not under %s/", name, u, www.Mount) |
| + } |
| + } |
| + if rooted == 0 { |
| + t.Errorf("%s has no root-relative URL at all; the check above read nothing", name) |
| + } |
| + } |
| +} |
| + |
| +// TestAHiddenSiteFileIsRefused: a hidden file under the site source |
| +// fails the build, and the error names it, rather than being skipped |
| +// or published. |
| +func TestAHiddenSiteFileIsRefused(t *testing.T) { |
| + _, err := www.Pages(fiesta.FS(), siteOf(map[string]string{ |
| + "static/x.css": "body{}\n", |
| + "static/.DS_Store": "\x00", |
| + })) |
| + if err == nil { |
| + t.Fatal("Pages accepted static/.DS_Store") |
| + } |
| + if !strings.Contains(err.Error(), "static/.DS_Store") { |
| + t.Errorf("the error does not name the hidden file: %v", err) |
| + } |
| +} |
| + |
| +// TestStaticFilesAreCopied: a file under static/ in the site source is |
| +// in the tree at the same path, byte for byte. |
| +func TestStaticFilesAreCopied(t *testing.T) { |
| + want := "/* the site's own */\nbody{margin:0}\n" |
| + out := pages(t, siteOf(map[string]string{"static/x.css": want})) |
| + if got := string(out["static/x.css"]); got != want { |
| + t.Errorf("static/x.css is %q, want %q", got, want) |
| + } |
| +} |
| + |
| +// sumsOf is SHA256SUMS as build.Dist writes it: one `<hex> <path>` |
| +// line per file, sorted by path, the sums file itself left out. |
| +func sumsOf(files map[string][]byte) []byte { |
| + names := make([]string, 0, len(files)) |
| + for name := range files { |
| + if name != "SHA256SUMS" { |
| + names = append(names, name) |
| + } |
| + } |
| + sort.Strings(names) |
| + var b bytes.Buffer |
| + for _, name := range names { |
| + sum := sha256.Sum256(files[name]) |
| + b.WriteString(hex.EncodeToString(sum[:]) + " " + name + "\n") |
| + } |
| + return b.Bytes() |
| +} |
| + |
| +// releases is a releases tree holding one small, checksummed 1.0.0. |
| +func releases() fstest.MapFS { |
| + files := map[string][]byte{ |
| + "fiesta.css": []byte("/*! Fiesta 1.0.0 */\nbody{margin:0}\n"), |
| + "LICENSE": []byte("MPL-2.0\n"), |
| + } |
| + files["SHA256SUMS"] = sumsOf(files) |
| + m := fstest.MapFS{} |
| + for name, data := range files { |
| + m["1.0.0/"+name] = &fstest.MapFile{Data: data} |
| + } |
| + return m |
| +} |
| + |
| +// TestSiteBuildAcceptsThePages is a preservation test: site.Build |
| +// takes FS(Pages(...)) as its pages beside a checksummed 1.0.0/, and |
| +// what Pages wrote is in the site at the same paths beside the release. |
| +func TestSiteBuildAcceptsThePages(t *testing.T) { |
| + out := pages(t, siteOf(map[string]string{"static/x.css": "body{}\n"})) |
| + built, err := site.Build(releases(), www.FS(out)) |
| + if err != nil { |
| + t.Fatalf("site.Build refused the pages: %v", err) |
| + } |
| + for name, data := range out { |
| + if !bytes.Equal(built[name], data) { |
| + t.Errorf("%s in the site differs from Pages, or is missing", name) |
| + } |
| + } |
| + if _, ok := built["1.0.0/fiesta.css"]; !ok { |
| + t.Error("the site lost the release") |
| + } |
| + if got := string(built["_redirects"]); got != "/1/* /1.0.0/:splat 200\n" { |
| + t.Errorf("_redirects is %q", got) |
| + } |
| +} |
diff --git a/site/FOLLOW-UPS.md b/site/FOLLOW-UPS.md| new file mode 100644 |
| index 0000000..fea88a6 |
| --- /dev/null |
| +++ b/site/FOLLOW-UPS.md |
| @@ -0,0 +1,10 @@ |
| +# Follow-ups for Fiesta 1.1 |
| + |
| +Fiesta is frozen while the site is built. Anything the site needs that |
| +Fiesta lacks lives in `site/static/`, with a comment saying so, and is |
| +listed here for Fiesta 1.1 to take in. |
| + |
| +- A copyable code box. |
| +- An embeddable sidebar. |
| +- Clipboard behaviour. |
| +- `rst-seg-tabs` for buttons. |
diff --git a/site/index.html b/site/index.html| deleted file mode 100644 |
| index adb3391..0000000 |
| --- a/site/index.html |
| +++ /dev/null |
| @@ -1,16 +0,0 @@ |
| -<!doctype html> |
| -<html lang="en"> |
| -<head> |
| -<meta charset="utf-8"> |
| -<meta name="viewport" content="width=device-width, initial-scale=1"> |
| -<title>Fiesta</title> |
| -<link rel="stylesheet" href="/1/fiesta.css"> |
| -</head> |
| -<body> |
| -<main rst-page> |
| -<h1>Celebrate your new design system</h1> |
| -<p>One stylesheet and done. JS optional. <25KiB gzipped.</p> |
| -<pre style="white-space: pre-wrap; overflow-wrap: anywhere"><code><link rel="stylesheet" href="https://fiesta.rastrillo.org/1/fiesta.css"></code></pre> |
| -</main> |
| -</body> |
| -</html> |