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
26 changes: 26 additions & 0 deletions .github/workflows/pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,17 @@ jobs:
# The smoke test drives Chromium, so the browser has to be present. A
# static build passes even when islands fail to hydrate — only this
# proves the diagrams draw.
# The browser download is the slowest step on a cold runner and the only
# variable one (20s usually, minutes when the CDN stalls). The key tracks
# the lockfile, so a dependency bump refreshes it and every other run hits
# the cache; `--with-deps` stays because it is a no-op once the image has
# the system libraries, and Chromium will not launch without them.
- name: Cache Chromium
uses: actions/cache@v6
with:
path: ~/.cache/ms-playwright
key: playwright-${{ runner.os }}-${{ hashFiles('bun.lock') }}

- name: Install Chromium
run: bunx playwright install --with-deps chromium

Expand Down Expand Up @@ -130,5 +141,20 @@ jobs:
# builds. The playground imports `dist/` from the repository, so it cannot
# catch a broken `files` field, a missing `dist/` file, or an `exports`
# subpath that does not resolve.
#
# The run ends on the consumer sites in Chromium: a static build passes even
# when every island fails to hydrate (dev served React's CommonJS
# `react/compiler-runtime` raw), so the browser is the only proof that a
# diagram draws. Same cache key as the smoke job above, so whichever runs
# second downloads nothing.
- name: Cache Chromium
uses: actions/cache@v6
with:
path: ~/.cache/ms-playwright
key: playwright-${{ runner.os }}-${{ hashFiles('bun.lock') }}

- name: Install Chromium
run: bunx playwright install --with-deps chromium

- name: Build a real Starlight site from the packed package
run: bun run test:site
80 changes: 67 additions & 13 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,28 +1,39 @@
## Development

Bun project. The package source is TypeScript in `src/`; `playground/` is a
small Starlight site that consumes it, for developing without a host site. The
Bun project. The package source is TypeScript in `src/`; there are two host
sites. `playground/` is a small Starlight site that consumes it, and
`examples/astro/` is the same thing in **plain Astro** (no Starlight) — both
import the package by name and resolve it through Node's self-reference. The
wiki in `docs/wiki/` is the published documentation — keep it in step with the
code. It is grouped into `guide/` (using the plugin) and `contributing/`
(changing it), each with a `README.md` index, plus `Troubleshooting.md` and
`Roadmap.md` at the root; new pages belong in one of those two folders.

```
bun run build:lib # bundle src/ -> dist/ (+ .d.ts, grammar data)
bun run dev # build:lib, then the playground dev server on :4321
bun run dev # build:lib, then the Starlight playground on :4321
bun run dev:astro # build:lib, then the plain-Astro example on :4322
bun run build # build:lib, then the playground static build
bun run build:astro # build:lib, then the plain-Astro static build
bun run test # vitest: pure helpers, the Langium scanner, the grammar
bun run test:network # + a live check against likec4/likec4
bun run test:site # pack the tarball and build a consumer Starlight site
bun run check # astro check (playground and src/) + tsc for scripts
bun run test:site # pack + install the tarball into a copy of playground/ and
# a copy of examples/astro/, then build both, run the full
# smoke suite against the Starlight copy, and check the
# plain one hydrates in Chromium
bun run check # astro check (both sites and src/) + tsc for scripts
bun run grammar:check # the committed grammar snapshot matches its derivation
```

`dev`, `build`, `check` and `test:site` all run `build:lib` first, because the
playground imports the plugin from `dist/` — the artifact a site installs.
Importing `../src/index.ts` there instead puts the plugin inside the Vite
module runner that loads `astro.config.mjs`, and its dynamic `import()` of
`dev`, `build`, `check` and `test:site` all run `build:lib` first, because both
host sites import the plugin from `dist/` — the artifact a site installs.
Importing `../src/index.ts` there instead puts the plugin inside the Vite module
runner that loads `astro.config.mjs`, and its dynamic `import()` of
`@astrojs/markdown-satteri` then dies with "module runner has been closed".
`examples/astro/astro.config.mjs` imports `@purview-dev/astroc4/integration` and
its page imports `@purview-dev/astroc4/components/LikeC4View.astro`; inside this
repository those resolve the way an installed site resolves them, through
`package.json` `exports`.

The playground scripts `cd playground` first rather than using
`astro … --root playground`: with `--root`, `astro preview` can't find
Expand Down Expand Up @@ -57,6 +68,21 @@ For highlighting, the smoke test's "distinct colours per block" check is the
proof that the grammar reached Expressive Code: the playground has no
`ec.config.mjs` on purpose.

The plain-Astro path has its own two proofs: `bun run check` runs `astro check`
over `examples/astro` (where `App.Locals` has no `t`, so a Starlight-ism in the
components fails there first), and `bun run dev:astro` plus
`bun scripts/hydration.ts http://localhost:4322` shows the island drawing with
the fallback palette. `bun run test:site` covers both host sites from the tarball,
and runs the **full smoke suite** against the packed copy of the playground — the
same assertions the local loop uses, but on the installed package rather than
`dist/` next to the repo.

Hydration has its own dev-only failure mode: Vite served React's CommonJS
`react/compiler-runtime` raw, and every island threw "does not provide an export
named 'c'" while `astro build` stayed green. `src/index.ts` now adds it to
`vite.optimizeDeps.include`; `bun run test:site` (Chromium) and
`bun scripts/hydration.ts <url>` are what catch a regression, not the build.

For the model, `bun x likec4 export json playground/src/likec4 -o .tmp-model.json`
reports node/edge counts per view in ~2s; a view that lays out to 0 nodes
(e.g. a deployment view that only includes `node.**`) shows up there first.
Expand All @@ -79,10 +105,38 @@ reports node/edge counts per view in ~2s; a view that lays out to 0 nodes
`root`, and `updateConfig({ expressiveCode })` to reach Expressive Code.
`updateConfig` merges shallowly, so merge existing options yourself —
`withLikeC4Language` does.
- **No Tailwind, no host aliases.** Components are styled with plain CSS over
Starlight's `--sl-color-*` tokens. A host's Tailwind doesn't scan this
package, so utility classes would silently be unstyled. `--sl-color-white`
is semantic (highest-contrast text), not literal white.
- **Integration-time output goes through Astro's logger**, never `console`: the
logger honours the CLI log level and prefixes the message with the
integration's name. `registerFences` forks it to `likec4-fences` so the label
matches what `Troubleshooting.md` quotes. `console` belongs to browser code
(`render-icon.tsx`, `ErrorBoundary.tsx`) and to `scripts/`.
- **`keywords` is release metadata, not decoration.** Astro's integrations
library indexes npm by keyword: `astro-integration` (and `withastro`) put the
package in the library, and a category keyword (`ui`) decides where. The
listing reads `name`, `description`, `repository` and `homepage`. See
[Repository CI/CD](docs/wiki/contributing/Repository-CI-CD.md#astros-integrations-library).
- **The integration adds one thing to a site's config: `@astrojs/react`.** The
islands need a React renderer, so `ensureReactRenderer` pushes it through
`updateConfig({ integrations })` when it is missing (Astro runs hooks of
integrations appended during `astro:config:setup`). MDX and Expressive Code are
deliberately left alone — they change the host's content types and code blocks.
Related: ordering between `astroc4()` and `astroExpressiveCode()` does not
matter (Astro applies both mdast plugins and `markdown.rehypePlugins`);
`examples/astro` lists Expressive Code first so that stays true.
- **No Tailwind, no host aliases.** Components are styled with plain CSS over a
`--likec4-*` layer that defaults to Starlight's `--sl-color-*` tokens, with
scheme-neutral fallbacks for plain Astro (an undefined `var()` with no fallback
is invalid at computed-value time, so every token needs one). A host's Tailwind
doesn't scan this package, so utility classes would silently be unstyled.
`--sl-color-white` is semantic (highest-contrast text), not literal white.
- **Two entry points, one implementation.** `src/index.ts` is the Starlight
plugin and `src/integration.ts` is the `AstroIntegration`; the plugin calls the
integration. Anything both hosts need belongs in `integration.ts`, never in the
plugin — and `Astro.locals.t` is read as `unknown` because `App.Locals` is
empty outside Starlight. The plugin object also exposes the integration's
`astro:config:setup`, because `astro add` writes `integrations: [astroc4()]`
from the default export; the two hook sets are disjoint, so carrying both is
safe (and it warns when a Starlight site ends up with the wrong one).
- **Strings go through `src/translations.ts`.** Astro components translate
with `getTranslator(Astro.locals.t)` and pass islands plain strings
(`IslandLabels`) — `t()` doesn't exist in the browser. Placeholders are
Expand Down
122 changes: 121 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,78 @@ All notable changes to this project are documented here. The format follows
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project
adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]
## [1.0.0-prerelease.5] - 2026-11-05

### Added

- **The integration works in plain Astro, not only Starlight.** The diagrams were
always built on Astro APIs; what was missing was an entry point and three
Starlight assumptions. `@purview-dev/astroc4/integration` is now an
`AstroIntegration` — the same `LikeC4VitePlugin`, fence transform, Windows
preload and `react/compiler-runtime` fix the Starlight plugin installs, because
both call the new `src/integration.ts`:

```js
import astroc4 from '@purview-dev/astroc4/integration';
export default defineConfig({ integrations: [astroc4(), expressiveCode(), mdx(), react()] });
```

Order matters and is documented: `astroc4()` first (Expressive Code needs the
Sätteri processor it installs), `expressiveCode()` before `mdx()`.
- `Astro.locals.t` is read as `unknown`. `App.Locals` is an empty interface
without Starlight's global augmentation, so `Astro.locals.t` is
`TS2339: Property 't' does not exist` for a site that type-checks the
components without Starlight — which `examples/astro` now does, and which is
how the cast is kept honest. The runtime already fell back to English.
- The components' colours moved to a `--likec4-*` layer that defaults to
`--sl-color-*` and to scheme-neutral fallbacks, so a plain Astro page gets a
legible frame instead of borders and backgrounds that resolve to nothing.
- `examples/astro/` is a runnable plain-Astro site (`bun run dev:astro`, port
4322) with a project view, an inline fence and `ec.config.mjs` highlighting;
`docs/wiki/guide/Plain-Astro.md` documents what the site supplies and what it
gives up (translations, the `highlighting` option).
- **`astro add` now wires a working site.** The CLI reads only npm metadata —
the `astro-integration` keyword and the package's default export — and writes
`integrations: [astroc4()]`. The default export therefore carries the Astro
protocol as well: Starlight calls only `config:setup`/`i18n:setup`, Astro
calls only `astro:*`, so one object serves both and the CLI's wiring produces
diagrams instead of a silently inert plugin. On a Starlight site that shape
cannot run the plugin half, so it warns and names
`starlight({ plugins: [astroc4()] })`. `@astrojs/starlight` is now an
optional peer, since a plain Astro site never loads it.
- **The integration supplies the React renderer itself.** The islands are
`client:only="react"`, so a site without `@astrojs/react` built fine and then
drew nothing (Astro's "no renderer" error names React, not this package).
`astroc4()` now adds `@astrojs/react` when `integrations` does not already
list it — Astro runs the hooks of integrations added during
`astro:config:setup`, the mechanism Starlight uses for Expressive Code — and
logs that it did. It adds nothing else on the site's behalf: `@astrojs/mdx`
(a new content type) and `astro-expressive-code` (every code block on the
site) stay the site's decision.
- **The ordering rule was wrong, and is gone.** `astroc4()` and
`astroExpressiveCode()` can be listed in either order: Astro applies a
processor's mdast plugins *and* the site's `markdown.rehypePlugins`, which is
how Expressive Code attaches itself when it runs before a processor exists.
The example site now lists Expressive Code **first** and still builds with
diagrams and `c4` highlighting, so the claim is asserted rather than
asserted-in-prose. The only real rule is `astroExpressiveCode()` before
`mdx()`, which Expressive Code enforces with its own error.
- A fence in a `.md` page no longer says "rename it to `.mdx`" on a site
without `@astrojs/mdx` — advice that cannot work. It says to add MDX first.
- `test:site` now installs the tarball into **copies of both host sites** —
`playground/` and `examples/astro/` — and runs `astro check`, `astro build`,
the **full smoke suite** against the Starlight copy (on its preview server),
a dev-server hydration check against the plain-Astro copy, and a rebuild of
that copy with the exact config `astro add` writes. The smoke suite is the
one that covers hydration, icons, highlighting colours, the loading
placeholder and both locales, so those are now proven against the artifact a
user installs rather than against `dist/` sitting beside the repository.

- **`bun scripts/hydration.ts <url>`** checks one diagram in a real browser: it
waits for a LikeC4 element node, fails while the `.likec4-loading`
placeholder is still visible, and fails on any console error. `test:site`
runs it against the packed consumer site's own dev server, and the smoke test
now asserts the placeholder is gone too.
- **The skills and the guide point at LikeC4's own DSL skill.** Astroc4 covers the
integration — fences, components, highlighting — and not LikeC4's language, so
both `skills/` files name `likec4.dev@likec4-dsl` as a companion and
Expand All @@ -16,8 +84,60 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
`npx skills add https://likec4.dev/` (Agent Skills Discovery protocol), and the
same page documents the MCP server for querying a model in natural language.

### Fixed

- **`<LikeC4View>` hydrates in `astro dev` again.** On a site configured the way
the README says to configure one, every diagram stayed on "Loading
architecture diagram…" while `astro build` exited 0, with one console line:

```
[astro-island] Error hydrating …/LikeC4ProjectView.tsx
SyntaxError: The requested module '/node_modules/react/compiler-runtime.js'
does not provide an export named 'c'
```

`likec4/react` is compiled with the React Compiler and imports `c` from React
19's CommonJS `react/compiler-runtime`. The island's source is in this
package, so dev served that file raw instead of pre-bundling it into an ES
module; `astro build` bundled it correctly, which is why only a browser could
see it. The plugin now adds `react/compiler-runtime` to
`vite.optimizeDeps.include` — only entries the site does not already have,
because Astro merges `vite` with Vite's `mergeConfig`, which concatenates
arrays — so no site needs a workaround.

It applies to **React 19.x** with **LikeC4 1.59.4 and later**, the versions
whose `likec4/react` imports the compiler runtime; the production build was
never affected. `test:site` now starts the packed consumer site's dev server
and asserts in Chromium that the island draws nodes with no console errors;
[Troubleshooting](docs/wiki/Troubleshooting.md#diagrams-stay-blank-while-the-build-passes)
records the error and the manual `optimizeDeps.include` fix older releases
need.

### Changed

- **The package follows Astro's integration conventions from end to end.**
- `keywords` now carries `withastro` (the discoverability keyword Astro's
community npm search filters on) and `ui` (the `CSS + UI` category), because
the [integrations library](https://docs.astro.build/en/guides/integrations/#integrations-library)
only files a package under a category when a category keyword is present —
otherwise it is listed as "Uncategorized". `astro-integration`,
`description`, `repository` and `homepage`, which the library reads, were
already there.
- The warning about a ` ```likec4 ` fence in a `.md` page now goes through
Astro's logger (`logger.fork('likec4-fences').warn(…)`) instead of
`console.warn`, so it respects the CLI log level and keeps the
`[likec4-fences]` label the Troubleshooting page quotes. `likec4Fences()`
takes an optional reporter; `console` remains the fallback for use outside
an Astro config.
- The README and Getting Started describe the two registrations, including the
fact that `astro add` writes the plain-Astro shape and must be moved into
`plugins` under Starlight.

Hooks and config were already the documented shapes — `updateConfig` (never
mutating `config`), `addIntegration` for the nested LikeC4 integration, a
scoped unique integration `name`, `astro` among the `peerDependencies`,
`"type": "module"`, and `exports` with `types` beside each entry point — so
those are unchanged.
- **The wiki is hierarchical.** `docs/wiki/` is now a landing `README.md` plus
`guide/` (using the plugin: getting started, fences, components, highlighting,
icons, i18n, options), `contributing/` (the module map, the local workflow, CI
Expand Down
22 changes: 20 additions & 2 deletions Justfile
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,21 @@ install:
dev:
bun run dev

# Plain-Astro example (no Starlight) at http://localhost:4322
[group('Develop')]
dev-astro:
bun run dev:astro

# Static build of the playground
[group('Build')]
build:
bun run build

# Static build of the plain-Astro example
[group('Build')]
build-astro:
bun run build:astro

# Serve the built playground
[group('Build')]
preview:
Expand All @@ -43,7 +53,9 @@ test:
test-network:
bun run test:network

# Pack the tarball and build a throwaway Starlight site from it
# Pack the tarball, install it into copies of playground/ and examples/astro/,
# then build both, run the full smoke suite against the Starlight copy and check
# the plain-Astro one hydrates in Chromium
[group('Verify')]
test-site:
bun run test:site
Expand All @@ -53,6 +65,11 @@ test-site:
smoke url="http://localhost:4321":
bun scripts/smoke.ts {{url}}

# Check that one diagram hydrates, against a running site (start `just dev` first)
[group('Verify')]
hydration url="http://localhost:4321":
bun scripts/hydration.ts {{url}}

# Fail if the committed grammar snapshot is stale
[group('Grammar')]
grammar-check:
Expand All @@ -69,7 +86,8 @@ likec4-validate:
bun run likec4:validate

# The shared gate's chain (`WebBuildCommand` then `WebTestCommand`), plus the
# packed consumer site — i.e. what a PR must pass.
# packed consumer sites — i.e. what a PR must pass. `test-site` ends in Chromium,
# so run `bunx playwright install chromium` once.
[group('Verify')]
verify: grammar-check check build test test-site
@echo "All checks passed."
Loading
Loading