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
14 changes: 7 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,13 @@ Run the complete source, build, export, size, and route smoke gate:
./scripts/verify-production.sh
```

The gate derives the exact GoSX and gotreesitter versions from `go.mod`, rejects local engine
overrides, builds the browser parser and all 206 lazy grammar assets from Go, validates every `.gsx` source, runs the Go tests,
creates a clean `gosx build --prod` artifact, runs `gosx export`, reports runtime size, boots
`dist/run.sh`, and requests every documentation route. Its Chrome check types a unique private
marker into the editor and proves that gotreesitter parses it without any source-bearing request;
it then proves an internal route change does not issue a document request. The `gosx` binary on
`PATH` must exactly match the version in `go.mod`.
The gate derives the exact GoSX and gotreesitter versions from `go.mod`. It rejects local engine
overrides. It builds the playground, all 206 lazy grammar assets, and the authoring engine from Go.
It validates every `.gsx` source and runs the Go tests. It creates a clean `gosx build --prod`
artifact, runs `gosx export`, reports runtime size, boots `dist/run.sh`, and requests every route.
It checks every internal link and validates the authoring asset surface. Its Chrome check proves
that playground source never enters a request. It also proves that internal navigation does not
request a new document. The `gosx` binary on `PATH` must exactly match the version in `go.mod`.

Browser performance budgets are optional because they require a local Chrome installation:

Expand Down
2 changes: 1 addition & 1 deletion app/authoring/page.server.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,11 +78,11 @@ func init() {
docsapp.RegisterStaticDocsPage(
"Grammar Authoring",
"Inherit a base grammar and author a delta of added/overridden rules in the browser — grammargen merges and compiles the extended grammar live.",
"/authoring",
route.FileModuleOptions{
Load: loadAuthoring,
Metadata: func(ctx *route.RouteContext, page route.FilePage, data any) (server.Metadata, error) {
return server.Metadata{
Title: server.Title{Absolute: "Grammar Authoring — gotreesitter"},
Links: []server.LinkTag{{
Rel: "stylesheet",
Href: docsapp.PublicAssetURL("authoring/authoring.css"),
Expand Down
10 changes: 5 additions & 5 deletions app/changelog/page.server.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ func init() {
docsapp.RegisterStaticDocsPage(
"Changelog",
"Explore every GoTreeSitter release, current unreleased work, upgrade impact, and source evidence.",
"/changelog",
route.FileModuleOptions{
Load: loadChangelog,
Metadata: func(ctx *route.RouteContext, page route.FilePage, data any) (server.Metadata, error) {
Expand All @@ -45,10 +46,9 @@ func changelogMetadata() server.Metadata {
const (
title = "Changelog — GoTreeSitter"
description = "Explore GoTreeSitter releases, current work, upgrade impact, and source evidence."
siteURL = "https://gotreesitter.m31labs.dev"
)
image := server.MediaAsset{
URL: siteURL + docsapp.PublicAssetURL("social/changelog.png"),
URL: docsapp.SiteURL + docsapp.PublicAssetURL("social/changelog.png"),
Width: 1200,
Height: 630,
Alt: "GoTreeSitter Changelog with an abstract syntax-tree release timeline.",
Expand All @@ -57,11 +57,11 @@ func changelogMetadata() server.Metadata {
return server.Metadata{
Title: server.Title{Absolute: title},
Description: description,
MetadataBase: siteURL,
Alternates: &server.Alternates{Canonical: siteURL + "/changelog"},
MetadataBase: docsapp.SiteURL,
Alternates: &server.Alternates{Canonical: docsapp.SiteURL + "/changelog"},
OpenGraph: &server.OpenGraph{
Type: "website",
URL: siteURL + "/changelog",
URL: docsapp.SiteURL + "/changelog",
SiteName: "GoTreeSitter",
Title: title,
Description: description,
Expand Down
10 changes: 6 additions & 4 deletions app/docs/__slug/page.server.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,16 +62,18 @@ func init() {
"title": title,
"description": doc.Frontmatter["description"],
"content": node,
"slug": slug,
}, nil
},
Metadata: func(ctx *route.RouteContext, page route.FilePage, data any) (server.Metadata, error) {
values, _ := data.(map[string]any)
title, _ := values["title"].(string)
description, _ := values["description"].(string)
return server.Metadata{
Title: server.Title{Default: title},
Description: description,
}, nil
slug, _ := values["slug"].(string)
meta := docsapp.CanonicalMetadata("/docs/" + slug)
meta.Title = server.Title{Default: title + " | GoTreeSitter Docs"}
meta.Description = description
return meta, nil
},
},
)
Expand Down
30 changes: 22 additions & 8 deletions app/layout.gsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,24 +12,24 @@ func Layout() Node {
<a href="#docs-main" class="skip-link">Skip to content</a>
<header class="topbar">
<a href="/" class="brand" data-gosx-link="true">
<span class="blk">
<span class="blk" aria-hidden="true">
<i></i>
<i></i>
<i></i>
<i></i>
</span>
gotreesitter
</a>
<span class="ver mono">{gtsVersion}</span>
<span class="ver mono" aria-hidden="true">{gtsVersion}</span>
<span class="tspacer"></span>
<span class="status">
<i></i>
<i aria-hidden="true"></i>
{gtsVersion}
· 206/206 curated parity
</span>
<a class="ghlink" href="/changelog" data-gosx-link="true">Changelog</a>
<a class="ghlink" href="/playground" data-gosx-link="true">Playground</a>
<a class="ghlink" href="/authoring" data-gosx-link="true">Authoring</a>
<TopNavLink href="/changelog" label="Changelog"></TopNavLink>
<TopNavLink href="/playground" label="Playground"></TopNavLink>
<TopNavLink href="/authoring" label="Authoring"></TopNavLink>
<a
class="ghlink"
href="https://github.com/odvcencio/gotreesitter"
Expand All @@ -52,22 +52,36 @@ func Layout() Node {
</div>
}

// TopNavLink exposes the active tool route to sighted and assistive users.
func TopNavLink(props any) Node {
return <>
<If when={request.path == props.Href}>
<a class="ghlink" href={props.Href} aria-current="page" data-gosx-link="true">
{props.Label}
</a>
</If>
<If when={request.path != props.Href}>
<a class="ghlink" href={props.Href} data-gosx-link="true">{props.Label}</a>
</If>
</>
}

// DocsNavLink renders one sidebar `<li class="navitem">` entry using the
// public/docs.css `.navitem`/`.ndot` contract and a real file-route link.
func DocsNavLink(props any) Node {
return <>
<If when={props.Active}>
<li class="navitem on">
<a href={props.Href} class="nav-anchor" aria-current="page" data-gosx-link="true">
<span class={"ndot " + props.Color}></span>
<span class={"ndot " + props.Color} aria-hidden="true"></span>
{props.Label}
</a>
</li>
</If>
<If when={props.Active == false}>
<li class="navitem">
<a href={props.Href} class="nav-anchor" data-gosx-link="true">
<span class={"ndot " + props.Color}></span>
<span class={"ndot " + props.Color} aria-hidden="true"></span>
{props.Label}
</a>
</li>
Expand Down
17 changes: 14 additions & 3 deletions app/modules.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@ package docs

import (
"log"
"strings"

"m31labs.dev/gosx/route"
"m31labs.dev/gosx/server"
)

const SiteURL = "https://gotreesitter.m31labs.dev"

var docsPublicAssetURL func(string) string

func BindPublicAssetURL(fn func(string) string) {
Expand All @@ -20,6 +23,14 @@ func PublicAssetURL(path string) string {
return server.AssetURL(path)
}

func CanonicalMetadata(path string) server.Metadata {
path = "/" + strings.TrimLeft(strings.TrimSpace(path), "/")
return server.Metadata{
MetadataBase: SiteURL,
Alternates: &server.Alternates{Canonical: SiteURL + path},
}
}

func RegisterDocsPage(title, description string, opts route.FileModuleOptions) {
metadata := opts.Metadata
opts.Metadata = func(ctx *route.RouteContext, page route.FilePage, data any) (server.Metadata, error) {
Expand All @@ -41,13 +52,13 @@ func RegisterDocsPage(title, description string, opts route.FileModuleOptions) {
}
}

func RegisterStaticDocsPage(title, description string, opts route.FileModuleOptions) {
func RegisterStaticDocsPage(title, description, canonicalPath string, opts route.FileModuleOptions) {
metaMetadata := opts.Metadata
opts.Metadata = func(ctx *route.RouteContext, page route.FilePage, data any) (server.Metadata, error) {
meta := server.Metadata{
meta := mergeDocsMetadata(server.Metadata{
Title: server.Title{Default: title + " | GoTreeSitter Docs"},
Description: description,
}
}, CanonicalMetadata(canonicalPath))
if metaMetadata == nil {
return meta, nil
}
Expand Down
12 changes: 12 additions & 0 deletions app/modules_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,15 @@ func TestMergeDocsMetadataPreservesStructuredSocialMetadata(t *testing.T) {
t.Fatalf("base metadata changed unexpectedly: %#v", merged)
}
}

func TestCanonicalMetadataUsesProductionOrigin(t *testing.T) {
for _, path := range []string{"changelog", "/changelog"} {
meta := CanonicalMetadata(path)
if meta.MetadataBase != SiteURL {
t.Fatalf("metadata base = %q, want %q", meta.MetadataBase, SiteURL)
}
if meta.Alternates == nil || meta.Alternates.Canonical != SiteURL+"/changelog" {
t.Fatalf("canonical metadata for %q = %#v", path, meta.Alternates)
}
}
}
1 change: 1 addition & 0 deletions app/page.server.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ func init() {
RegisterStaticDocsPage(
"Overview",
"A pure-Go, byte-exact reimplementation of tree-sitter — no CGo, no C toolchain, 206 grammars built in.",
"/",
route.FileModuleOptions{
Bindings: func(ctx *route.RouteContext, page route.FilePage, data any) route.FileTemplateBindings {
return route.FileTemplateBindings{
Expand Down
2 changes: 1 addition & 1 deletion app/playground/page.server.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,11 @@ func init() {
docsapp.RegisterStaticDocsPage(
"Playground",
"Parse source and run tree-sitter queries locally in a GoSX-managed WebAssembly engine.",
"/playground",
route.FileModuleOptions{
Load: loadPlayground,
Metadata: func(ctx *route.RouteContext, page route.FilePage, data any) (server.Metadata, error) {
return server.Metadata{
Title: server.Title{Absolute: "Playground — gotreesitter"},
Links: []server.LinkTag{{
Rel: "stylesheet",
Href: docsapp.PublicAssetURL("playground/playground.css"),
Expand Down
2 changes: 1 addition & 1 deletion content/docs/authoring-languages.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ The relevant functions are all public:
|---|---|---|
| `grammargen.ImportGrammarJSON(data []byte) (*Grammar, error)` | `grammargen/import_grammarjson.go` | Parse a resolved `grammar.json` (the output of `tree-sitter generate`) into the grammar IR. Rules, extras, conflicts, externals, inline, word, precedences, reserved sets, supertypes are all imported. |
| `grammargen.GenerateLanguage(g *Grammar) (*gotreesitter.Language, error)` | `grammargen/encode.go` | Compile the IR into runtime parse tables. |
| `grammargen.GenerateLanguageAndBlob(g *Grammar) (*gotreesitter.Language, []byte, error)` | `grammargen/encode.go` | Same, plus the serialized gob+gzip blob in one pass. `...WithContext` variants exist for cancellation. |
| `grammargen.GenerateLanguageAndBlob(g *Grammar) (*gotreesitter.Language, []byte, error)` | `grammargen/encode.go` | Same, plus a serialized language blob in one pass. Blobs without `LargeStateGotos` keep the legacy gob+gzip format. Map-bearing blobs use a deterministic versioned envelope. Load either form with `gotreesitter.LoadLanguage`. `...WithContext` variants exist for cancellation. |
| `grammargen.EmitGrammarGo(g *Grammar, pkgName, funcName string) ([]byte, error)` | `grammargen/emit_grammar_go.go` | Emit Go DSL source that reconstructs the grammar — useful for vendoring the grammar as reviewable Go code instead of JSON. |
| `gotreesitter.LoadLanguage(data []byte) (*Language, error)` | `load_language.go` | Deserialize a blob at runtime. The only function needed to load a pre-compiled grammar — no grammargen import, no registry. |
| `grammars.LoadLanguage(name string, data []byte)` | `grammars/embedded_loader.go` | Like the above, but also attaches any external scanner / external lex-state tables registered for `name` in the `grammars` registry. |
Expand Down
2 changes: 2 additions & 0 deletions public/docs.css
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ a{color:var(--blue);text-decoration:none;font-weight:600}a:hover{color:var(--vio
.tnav{display:flex;gap:6px;align-items:center}
.ghlink{font:600 13px 'JetBrains Mono',monospace;color:var(--ink);background:var(--paper);padding:7px 12px;border-radius:2px;border:2px solid var(--paper)}
.ghlink:hover{background:var(--yellow);color:var(--ink)}
.ghlink[aria-current="page"]{background:var(--color-yellow);color:var(--color-ink)}
:where(a,button,input,select,textarea,summary):focus-visible{outline:3px solid var(--color-blue);outline-offset:3px}
.newpill{font:600 11px 'JetBrains Mono',monospace;color:var(--ink);background:var(--yellow);padding:5px 9px;border-radius:2px;text-transform:uppercase;letter-spacing:.5px}
.body{display:flex;align-items:flex-start;gap:0}
.sidebar{position:sticky;top:58px;align-self:flex-start;width:250px;flex:none;height:calc(100vh - 58px);overflow-y:auto;padding:20px 16px 40px;border-right:4px solid var(--ink);background:var(--paper)}
Expand Down
50 changes: 41 additions & 9 deletions scripts/verify-production.sh
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ fail() {
exit 1
}

for command in go gosx tinygo curl grep sed awk mktemp find; do
for command in git go gosx tinygo curl grep sed awk mktemp find sort; do
command -v "$command" >/dev/null 2>&1 || fail "required command not found: $command"
done

Expand Down Expand Up @@ -43,10 +43,9 @@ gosx_cli_version="$(gosx version 2>&1)"
grep -Fq "GoSX ${gosx_version#v}" README.md || fail "README GoSX requirement does not match go.mod"
grep -Fq "m31labs.dev/gosx/cmd/gosx@$gosx_version" README.md || fail "README GoSX install command does not match go.mod"

# Application behavior must be authored in Go/GoSX. Framework-generated
# runtime assets in dist are allowed; repository-authored JS is not.
if find . -path './.git' -prune -o -path './dist' -prune -o -path './build' -prune \
-o -type f \( -name '*.js' -o -name '*.jsx' -o -name '*.ts' -o -name '*.tsx' \) -print -quit | grep -q .; then
# Application behavior must be authored in Go/GoSX. Ignore generated files
# through the repository's normal ignore rules. Reject tracked or new source.
if git ls-files --cached --others --exclude-standard -- '*.js' '*.jsx' '*.ts' '*.tsx' | grep -q .; then
fail "application-authored JavaScript remains in the repository"
fi
if grep -R -n --include='*.gsx' '<script' app; then
Expand All @@ -55,6 +54,7 @@ fi

rm -rf build dist
go run ./cmd/build-playground-wasm
go run ./cmd/build-authoring-wasm
while IFS= read -r source; do
gosx check "$source"
done < <(find app -name '*.gsx' -type f | sort)
Expand All @@ -74,6 +74,7 @@ port="${VERIFY_PORT:-18080}"
base="http://127.0.0.1:${port}"
server_log="$(mktemp)"
page_body="$(mktemp)"
route_pages="$(mktemp -d)"
server_pid=""

cleanup() {
Expand All @@ -82,6 +83,7 @@ cleanup() {
wait "$server_pid" 2>/dev/null || true
fi
rm -f "$server_log" "$page_body"
rm -rf "$route_pages"
}
trap cleanup EXIT

Expand All @@ -99,20 +101,36 @@ for _ in $(seq 1 60); do
done
curl --silent --fail "$base/healthz" | grep -q '"ok":true'

routes=("/" "/playground")
routes=("/" "/playground" "/authoring")
routes+=("/changelog")
for source in content/docs/*.md; do
routes+=("/docs/$(basename "$source" .md)")
done
for route in "${routes[@]}"; do
curl --silent --show-error --fail --output /dev/null "$base$route"
for index in "${!routes[@]}"; do
route="${routes[$index]}"
route_page="$route_pages/$index.html"
curl --silent --show-error --fail --output "$route_page" "$base$route"
grep -Fq 'rel="canonical"' "$route_page" || fail "canonical metadata missing from $route"
grep -Fq "href=\"https://gotreesitter.m31labs.dev$route\"" "$route_page" ||
fail "canonical metadata does not match $route"
done

while IFS= read -r href; do
[[ -n "$href" ]] || continue
curl --silent --show-error --fail --output /dev/null "$base$href" ||
fail "internal link does not resolve: $href"
done < <(
grep -rhoE 'href="/[^"]*"' "$route_pages" |
sed -e 's/^href="//' -e 's/"$//' -e 's/&amp;/\&/g' -e 's/#.*$//' |
sort -u
)

curl --silent --fail "$base/" | grep -q '5.53×' || fail "landing headline metric (5.53× geomean) missing from /"
curl --silent --fail "$base/docs/performance" | grep -q '5.526× C' || fail "performance geomean (5.526× C) missing from /docs/performance"
curl --silent --show-error --fail --output "$page_body" "$base/changelog"
grep -q 'History you can interrogate' "$page_body" || fail "changelog hero missing from /changelog"
grep -q 'v0.47.1' "$page_body" || fail "v0.47.1 missing from /changelog"
grep -q 'href="/changelog" aria-current="page"' "$page_body" || fail "active changelog navigation state is missing"

languages_html="$(curl --silent --show-error --fail "$base/docs/languages")"
lang_search_url="$(grep -oE '/gosx/islands/LangSearch\.json\?v=[0-9a-f]{12}' <<<"$languages_html" | head -n 1)"
Expand All @@ -134,8 +152,22 @@ curl --silent --show-error --fail --head "$base$wasm_url" | grep -Fqi 'content-t

PLAYGROUND_BASE_URL="$base" go run ./cmd/verify-playground-browser

curl --silent --show-error --fail --output "$page_body" "$base/authoring"
grep -q 'data-gosx-engine="GotreesitterAuthoring"' "$page_body" || fail "authoring GoSX engine mount is missing"
authoring_wasm_url="$(grep -oE '/authoring/authoring\.wasm\?v=[0-9a-f]{12}' "$page_body" | head -n 1)"
[[ -n "$authoring_wasm_url" ]] || fail "content-versioned authoring WASM URL is missing"
curl --silent --show-error --fail --head "$base$authoring_wasm_url" |
grep -Fqi 'content-type: application/wasm' || fail "authoring engine is not served as application/wasm"
authoring_worker_url="$(grep -oE '/authoring/authoring-worker\.js\?v=[0-9a-f]{12}' "$page_body" | head -n 1)"
[[ -n "$authoring_worker_url" ]] || fail "content-versioned authoring worker URL is missing"
curl --silent --show-error --fail --output /dev/null "$base$authoring_worker_url"
authoring_bases_url="$(grep -oE '/authoring/bases/index\.json\?v=[0-9a-f]{12}' "$page_body" | head -n 1)"
[[ -n "$authoring_bases_url" ]] || fail "content-versioned authoring base index URL is missing"
curl --silent --show-error --fail "$base$authoring_bases_url" | grep -Eq '"name"[[:space:]]*:[[:space:]]*"calc"' ||
fail "authoring base index does not include calc"

if [[ "${RUN_BROWSER_PERF:-0}" == "1" ]]; then
for route in / /docs/getting-started /docs/performance /playground; do
for route in / /docs/getting-started /docs/performance /playground /authoring; do
gosx perf --coverage --budget perf-budget.json --budget-profile docs "$base$route"
done
fi
Expand Down
Loading