Editor language support for .axo, plus release and site workflows - #1
Merged
Conversation
matchBrace and splitFrontmatter were private to the compiler, and the editor extension needs exactly the same answers about where a region ends. A second implementation would drift, and the editor would eventually disagree with the build about a file both are reading. The scanner now works in offsets rather than extracted strings, because an editor has to map a token back to a position in the document and a copied substring has forgotten where it came from. matchBrace throws with the offset of the unclosed brace, so a caller can point at it. Published as ./scan, which is a new public entry point and so a new contract — the pinned export list in the test moves deliberately. Compiler behaviour is unchanged: the existing suite passes untouched, which is the whole evidence for that claim.
Feature 014. Six stories, nineteen EARS requirements and seven success criteria, with the plan, the Phase 0 research, and contracts for the scanner and the ambient API table. Two decisions are load-bearing. A TextMate grammar cannot resolve a balanced brace, so colouring is two layers: a declarative grammar for instant baseline colour, corrected by semantic tokens driven by the real scanner. And the editor may report less than the build but never more, which is what keeps a squiggle trustworthy.
A .axo file has been plain text in every editor since the format was
invented, which is a poor start to a loop built on writing components.
Colouring is two layers. The TextMate grammar is declarative, so it
costs no activation and works before any code runs; it handles the
region split, tags, directives and <slot>. It cannot balance a brace,
so semantic tokens driven by the shared scanner mark the true extent of
every expression — that is the case class:list={['a', `x-${y}`]} gets
wrong under a regex.
On top of that: region-aware editing, where comment toggling follows
the embedded-language mapping rather than a single declared style;
completion and hover from one ambient API table, tested to cover
docs/writing-components.md in both directions; and diagnostics for the
two rules the build enforces.
The diagnostics match the compiler's exact client:* list rather than
the wider client:* the spec describes, because client:whatever compiles
today and flagging it would have the editor report a problem the build
does not have. That widening belongs in the compiler.
Everything decidable is a pure function under src/language/; the editor
API appears only in src/adapters/. That is what lets 60 tests run under
node --test with no editor to download.
Go-to-definition on <Pricing> opens components/Pricing.axo. There is no index to keep in sync — the filename IS the component name, so this is a path lookup. A deck component shadows a built-in of the same name, so the deck is searched first and its file is the only result: opening the built-in someone overrode would show them code the build will not run. A tag that resolves to nothing opens nothing and raises nothing, because a lowercase element or a not-yet-written component is not an error. Filesystem access is injected, so the ordering — which IS the requirement — is tested without a fixture tree. Also adds the drift guard FR-019 asks for. It compares behaviour rather than bytes: esbuild rewrites the module on the way into the bundle, so a byte comparison would fail on every build while proving nothing. Both scanners must answer identically across the corpus, and agree that a malformed document is malformed. The language modules now emit as .mjs. They are ESM in a package with no "type" field, since the host bundle has to stay CommonJS, and Node was reparsing and warning on every import.
Tagging `extension-v0.1.0` builds, verifies, packages and publishes to the VS Code Marketplace and Open VSX. Both run, and the second runs even if the first fails — a credential problem should not silently halve a release. If they end up disagreeing the run fails loudly, because a split release is fixed by publishing the same VSIX by hand, not by cutting a new version. Publishing needs VSCE_PAT and OVSX_PAT, which do not exist yet. A preflight step names the missing secret instead of letting vsce fail with something vaguer. Until then, workflow_dispatch packages the VSIX and uploads it without publishing anything. Packaging it for the first time showed the VSIX carried .vscode/ — the launch configs for developing this extension, meaningless to anyone who installs it — and dist/language/, the ESM copies that exist so tests can import modules one at a time, which the host bundle already inlines. It also had no LICENSE despite declaring MIT. Excluding the first two and adding the licence and a README takes it from 13 files to 8, and from 15.77 KB to 11.1 KB. The CI job also never ran the extension's 80 tests: they import the framework's dist/ — the drift guard compares the bundled scanner against it, and the parity tests ask the real compiler whether it rejects a document — so without building the framework first they cannot run at all. Both workflows now do that.
publish.yml ships @ambystech/axo on a published GitHub Release, via npm Trusted Publishing — OIDC rather than a long-lived NPM_TOKEN, with provenance attached automatically. Ported from ambykit, which already runs this. A trusted publisher is configured on a package that exists, though, and this one has never been published: the first release likely needs a manual npm publish to create the name. The payload check nearly shipped broken. `npm pack --dry-run --json` runs `prepare`, and the build prints to stdout, so the JSON never parses — on any platform. It runs with --ignore-scripts now, against the dist/ built two steps earlier. site.yml builds site/ and deploys it to Pages. The landing page is a deck built by Axo, so every push proves what the page claims: the workflow refuses a non-public deck and fails if dist/index.html reaches any external host. If it goes red, the page is lying. Action versions were several majors behind — the last CI run carried a deprecation annotation about exactly that. checkout v4→v7, setup-node v4→v7, upload-artifact v4→v7, the Pages actions to v6/v5/v5, and gh-release v2→v3, across every workflow including the deck template users get scaffolded. Also: a concurrency group so a new push cancels the superseded run, and the npm cache the extension job never had.
axo.ambystech.io has been a claim in package.json with nothing behind it. Now it is six slides in site/, scaffolded by `axo new` and built by `axo build` — the framework's own output, deployed on every push. That is the point of building it this way rather than as a docs site: the page cannot drift from what the framework actually does, because it IS what the framework produces. The workflow checks the built deck reaches no external host, so the offline guarantee on slide two is verified by the thing serving slide two. Writing it found a real edge, too — <Flow> takes at most four <Step>s and the pipeline has five. The build refused, named the file, and said to use <Phases>. Exactly the behaviour slide four describes. The deck depends on the framework by `file:..`, like extension/ does, so it compiles against the working tree rather than a published version that does not exist yet.
The first run failed at configure-pages because Pages is not enabled on the repo yet — but it failed AFTER building the deck and passing both guards, which is the part a pull request actually needs to know. A PR should not require Pages to be configured in order to verify that the deck still compiles and still reaches no external host. Gate the two Pages steps on main, where there is something to deploy.
Two axes, each earning its place. The compiler builds file:// URLs and normalises separators by hand in three modules — the code most likely to break off Linux, and exactly what a Linux-only CI cannot see. And `engines` promises Node >=20 while only 20 was ever tested, though the one CI failure this repo has had was a Node-version difference. Adding the matrix meant fixing what it would have caught immediately. `node --test test/*.test.mjs` depends on the SHELL expanding the glob: POSIX shells do, cmd.exe and PowerShell do not, so on Windows the literal string reaches Node — which can expand it itself, but only from v22. Quoting moves the problem rather than solving it, since then it always reaches Node and v20 fails everywhere instead of just on Windows. There is no form of that command that works on Windows with Node 20. So scripts/test.mjs builds the file list, where neither the shell nor the Node version has an opinion. Both packages use it, which also means `npm test` now works for a Windows contributor on the floor version — it did not before.
esbuild reports REAL paths, with symlinks resolved. macOS makes its temp dir /var/folders/…, a symlink to /private/var/folders/…, so the key written from the unresolved path never matched the path esbuild handed back. The lookup missed, and a broken component was blamed on the generated .jsx in .axo/ rather than the file someone actually wrote — the opposite of what Principle 4 promises. Linux and Windows do not symlink their temp dirs, which is why seven of the eight matrix cells were green and only macOS was red. The matrix found this on its first run. Both ends are resolved now, so they agree on every platform.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Feature 014, plus the workflow gaps it exposed along the way.
What this adds
A VS Code extension (
extension/) giving.axofiles colour, region-aware editing, completion, hover, diagnostics and go-to-definition. Works from one build on VS Code and its forks. 80 tests, no editor required to run them.A shared scanner (
src/scan.ts).matchBraceandsplitFrontmatterwere private to the compiler; the editor needs the same answers, and a second implementation would drift. Now published as./scanand bundled into the extension, with a drift guard asserting the two still agree across the whole built-in corpus.Three workflows.
publish.yml(npm, OIDC Trusted Publishing),site.yml(Pages), andextension-release.yml(both extension registries). Action versions refreshed everywhere — they were several majors behind and the last CI run said so.The landing page (
site/) is now a deck built by Axo, so it cannot drift from what the framework does.Notable
client:*list rather than the wider one FR-012 describes, becauseclient:whatevercompiles today — widening it belongs in the compiler..vscode/and a second copy of the scanner; excluding those took it from 13 files to 8.npm pack --dry-run --jsonrunsprepare, whose build output corrupts the JSON — the publish payload check would have failed on its first real run.Not done
engines.vscodeis provisional until the forks are measured, and neither registry publisher identity exists yet, so US-2 cannot complete.specs/014-editor-language-support/tasks.mdtracks the rest: 44 of 53.Why this PR exists now
None of these workflows has ever executed — the push trigger is
branches: [main]. This givesci.ymlandsite.ymltheir first real run.