diff --git a/.config/lefthook.yml b/.config/lefthook.yml index 08ce932..04495a9 100644 --- a/.config/lefthook.yml +++ b/.config/lefthook.yml @@ -1,4 +1,4 @@ commit-msg: jobs: - name: conventional commits - run: npx commitlint --edit {1} + run: bun x commitlint --edit {1} diff --git a/.gitignore b/.gitignore index 1b50b86..ca2047a 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ playground/.astro/ .env.local .DS_Store !scripts/* +*.tgz diff --git a/AGENTS.md b/AGENTS.md index f90f15c..e218e7b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -6,8 +6,8 @@ sites. `playground/` is a small Starlight site that consumes it, and 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. +(changing it), each with a `README.md` index, plus `Troubleshooting.md` at the +root; new pages belong in one of those two folders. ``` bun run build:lib # bundle src/ -> dist/ (+ .d.ts, grammar data) @@ -26,11 +26,11 @@ bun run grammar:check # the committed grammar snapshot matches its derivation ``` `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 +host sites import the plugin by package name, which resolves to `dist/` — the +artifact a site installs. Importing `../src/index.ts` there instead puts the +plugin inside the Vite module runner that loads `astro.config.ts`, 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 +`examples/astro/astro.config.ts` 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`. @@ -66,20 +66,21 @@ running: `bun scripts/smoke.ts http://localhost:4321`. The static build passes even when islands fail to hydrate — only the smoke test proves they draw nodes. 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. +Expressive Code config 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, +the fallback palette (`test:site` also asserts the example's `--likec4-accent` +override reaches the shell). `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 +named 'c'" while `astro build` stayed green. `src/integration.ts` adds it to `vite.optimizeDeps.include`; `bun run test:site` (Chromium) and `bun scripts/hydration.ts ` are what catch a regression, not the build. @@ -107,8 +108,8 @@ reports node/edge counts per view in ~2s; a view that lays out to 0 nodes `withLikeC4Language` does. - **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 + integration's name (`[@purview-dev/astroc4]` from the Starlight plugin, which + is the label `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 @@ -123,12 +124,21 @@ reports node/edge counts per view in ~2s; a view that lays out to 0 nodes 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. +- **No Tailwind, no host aliases.** Components are styled with plain CSS. A + host's Tailwind doesn't scan this package, so utility classes would silently + be unstyled. +- **`--likec4-*` tokens are read, never declared, by components.** Each reads + them into a private `--_likec4-*` with the fallback chain + (`var(--likec4-border, var(--sl-color-gray-5, rgba(…)))`), so a site's value on + any ancestor inherits through instead of losing to the scoped selector. + Every chain ends in a scheme-neutral value (an undefined `var()` with no + fallback is invalid). `tokens.test.ts` enforces it; `.md` fence placeholders in + `src/fences.ts` use the same chains inline. `--sl-color-white` is semantic + (highest-contrast text), not literal white. +- **Project configs are read like LikeC4 reads them** (`src/projects.ts`): every + name in LikeC4's `ConfigFilenames` (a `satisfies` check fails the type-check if + LikeC4 changes the list), JSON names via JSON5, JS/TS via `bundle-require` with + `likec4/config` stubbed, and `name` defaulting to the folder name. - **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 @@ -142,7 +152,7 @@ reports node/edge counts per view in ~2s; a view that lays out to 0 nodes (`IslandLabels`) — `t()` doesn't exist in the browser. Placeholders are `{name}`, filled by `format()`, never i18next's `{{name}}`. - **The fence plugin is Sätteri mdast, not remark**, appended to the site's - existing processor in `src/index.ts` so Starlight's and other + existing processor in `src/integration.ts` so Starlight's and other integrations' plugins survive. - **Don't import `likec4` (the Node API) at module scope** in anything `src/index.ts` loads — see the comments in `src/fences.ts`. @@ -151,11 +161,17 @@ reports node/edge counts per view in ~2s; a view that lays out to 0 nodes - **`client:only` fallbacks need `slot="fallback"`**, or they're never painted. - Imports the fence writes into pages use the package name (`@purview-dev/astroc4/components/…`), because they resolve from the site, not from here. -- **Everything is TypeScript; `dist/` is built, never committed.** `bun run - build:lib` bundles the Node entry points with Bun (`.d.ts` from `tsc`) and +- **Everything is TypeScript, configs included (`astro.config.ts`); `dist/` is + built, never committed.** Relative imports use `.ts` specifiers. `bun run + build:lib` bundles the Node entry points with Bun (`.d.ts` from `tsc`, with + `.ts` specifiers rewritten to `.js`) and copies the two grammar data files next to the bundle, where `import.meta.url` finds them. `exports` points the two entry points and the grammar JSON at `dist/`, and the components at `src/components/`: `.astro` and `.tsx` are compiled by the consuming site, which is also why - `src/index.ts` sets `ssr.noExternal`. There are no hand-written `.d.ts` files - — the old ones shadowed the implementation's own types. + `src/integration.ts` sets `ssr.noExternal`. There are no hand-written `.d.ts` + files — the old ones shadowed the implementation's own types. Expressive Code + only reads a config named `ec.config.mjs`, so `examples/astro` passes its + options inline instead. +- **Comments only where required**: a non-obvious why (a workaround, a + load-bearing order, a platform quirk). No file/path headers, no narration. diff --git a/CHANGELOG.md b/CHANGELOG.md index 09ddb02..bc52411 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,87 @@ 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). +## [1.0.0-prerelease.6] - 2026-11-09 + +### Added + +- **Fences work in plain `.md`.** A ` ```likec4 ` fence in a `.md` page — which + cannot hold a component — is compiled and laid out at build time like one in + `.mdx`, written as a placeholder carrying the model, and mounted in the browser + by a page script the integration injects. The script loads the diagram code + only on a page that has a placeholder. Replaces the "fences only render as + diagrams in .mdx files" warning; `FenceOptions.reporter` and `mdx` are + deprecated and unused. +- **`imageAliases` just work.** A project's own `likec4.config.json` can declare + `"@": "../../.assets/images"` and its model can say `icon @/azure/entra.svg` + with no change for astroc4. Every icon LikeC4 compiles to a local file — + through an alias or a path relative to the `.c4` file — is mapped to + `/_astroc4//` (`@brand/…` adds a `brand/` segment, `..` becomes + `__up`), served from the original file in `astro dev`, copied into the build + output, and rendered (SVGs inline, by LikeC4). Files under `public/` keep their + public URL. No build-machine `file://` path reaches the client bundle any more, + for any icon, and nothing is written into the site's source tree. +- **Named projects.** `projects: { architecture: '../../docs/architecture/likec4' }` + compiles LikeC4 projects from anywhere — a sibling folder, another checkout, an + absolute path — alongside the ones discovered under the site root. They are + linked into `.astro/astroc4/workspace` (LikeC4 ignores anything under + `node_modules`), and edits reload in `astro dev`. +- **``** names a configured or discovered project, and + **``** renders one `.c4` file from `src/`, + compiled at build time. Fences take `project="…"` and `file="…"` (relative to + the page) too. +- A missing icon, a missing or config-less project folder, two projects with the + same name, and two icons served at the same URL each fail the build, naming + the files involved. +- **Every LikeC4 config file name.** Projects are found by `.likec4rc`, + `.likec4.config.json` and `likec4.config.json` (read as JSON5) and by + `likec4.config.{ts,mts,cts,js,mjs,cjs}` (bundled with esbuild, with + `likec4/config` stubbed so an external project need not resolve it), as LikeC4 + reads them. A config without `name` is named after its folder, as in LikeC4. + Adds `bundle-require`, `esbuild` and `json5` as dependencies; LikeC4 already + depends on all three. +- **Empty fences check `project` at build time**, in `.md` as well as `.mdx`: an + unknown name, or none on a site with several projects, fails the build instead + of showing an error box in the browser. +- **`.md` fences get the shell options**: `title` defaults to "Diagram", and + `collapsible`/`defaultOpen` work as in `.mdx`. + +### Fixed + +- **`--likec4-*` colour overrides work.** Components declared the tokens on + their own elements in scoped styles, so a site's `.likec4-shell { --likec4-… }` + lost on specificity and a value on an ancestor never reached the frame, badge + or placeholder. They now only read the public tokens (through private + `--_likec4-*` properties carrying the fallbacks), so a value set on `:root` or + any ancestor applies everywhere, `.md` fences included. +- **A site running under Bun without React no longer breaks Vite's optimizer.** + The `react/compiler-runtime` check used `createRequire`, which under Bun also + finds packages in Bun's global cache, so the entry was added for a package + Vite could not find. It now looks in `node_modules`, as Vite does. +- `bun run test:site` passes on Windows when `TEMP` is an 8.3 short path: the + plain-Astro dev server 404'd its own `before-hydration.js` because Vite + resolved the site to its long path. + +### Changed + +- TypeScript throughout: `astro.config.ts` in the playground and the example, + `.ts` relative import specifiers (rewritten to `.js` in the published + declarations), and `examples/astro` passes `likec4Language` to + `expressiveCode()` instead of an `ec.config.mjs`. Code comments trimmed to the + ones that explain a non-obvious reason. + +- **Breaking: no implicit "first project".** With more than one project, + `` and empty fences need `project`; leaving it out, or naming an + unknown project, now fails the build listing the names, instead of rendering + whichever project was found first (or an error box in the browser). + `projectId` still works as a deprecated alias of `project`. A site that sets + `vitePlugin.workspace` or `languageServices` keeps the old in-browser + resolution, and `projects` is ignored there with a warning. +- New translation key `likec4.error.projectRequired`. +- **A fence without `view="…"` shows the first view its source declares.** It + showed LikeC4's automatic `index` landscape instead, which LikeC4 always lists + first — so a snippet's own `view x of system { … }` never appeared. + ## [1.0.0-prerelease.5] - 2026-11-05 ### Added diff --git a/README.md b/README.md index e92cbd0..8f6d2b3 100644 --- a/README.md +++ b/README.md @@ -6,13 +6,17 @@ Interactive [LikeC4](https://likec4.dev) architecture diagrams for Astro and Starlight. - `` — render a view from the site's `.c4` model. -- ` ```likec4 ` fences in `.mdx` — either inline DSL, compiled and laid out at - build time, or an empty fence with `view="…"` pointing at the model. -- LikeC4 syntax highlighting for Expressive Code (`.c4` files, ` ```c4 `), - derived at build time from the Langium grammar in - [likec4/likec4](https://github.com/likec4/likec4) — no `ec.config.mjs` under - Starlight. -- Every UI string translatable through Starlight's i18n. +- ` ```likec4 ` fences in `.md` and `.mdx` — inline DSL or a `.c4` file, compiled + and laid out at build time, or an empty fence with `view="…"` pointing at the + model. +- Projects from anywhere: discovered under the site root, or named in the + options, with any LikeC4 config file (`likec4.config.json`, `.likec4rc`, + `likec4.config.ts`, …) and their `imageAliases` unchanged. +- LikeC4 syntax highlighting for Expressive Code (` ```c4 `), derived at build + time from the Langium grammar in [likec4/likec4](https://github.com/likec4/likec4) + — no Expressive Code config under Starlight. +- Every UI string translatable through Starlight's i18n, and every colour a + `--likec4-*` token a site can set. It works in **plain Astro** as well: `@purview-dev/astroc4/integration` is an `AstroIntegration` that adds the same model compiler and fence transform. See @@ -25,44 +29,36 @@ Full documentation lives in [`docs/wiki`](docs/wiki/README.md), grouped into ## Install ```sh -bun add @purview-dev/astroc4@prerelease # 1.0.0-prerelease.N +bun add @purview-dev/astroc4 ``` -Published to npm under the `prerelease` dist-tag, so `bun add @purview-dev/astroc4` keeps -resolving the last stable release. Astroc4 declares its peer dependencies, so -Bun and npm install them with it; under pnpm add them explicitly (`astro`, +Only prereleases (`1.0.0-prerelease.N`) are published so far; `latest` and +`prerelease` both point at the newest one, and `@prerelease` will keep tracking +prereleases once a stable version ships. Astroc4 declares its peer dependencies, +so Bun and npm install them with it; under pnpm add them explicitly (`astro`, `@astrojs/react`, `@astrojs/markdown-satteri`, `likec4`, `@likec4/core`, `react`, `react-dom`, `shiki`, and `@astrojs/starlight` if you use the plugin — Starlight is an optional peer, because plain Astro does not need it). `astro add @purview-dev/astroc4` wires the **plain-Astro** shape for you: it -installs the package (and the non-optional peers) and writes -`integrations: [astroc4()]`, which works because the default export implements -the Astro integration protocol too. In a Starlight site the CLI cannot know -better — it writes the same thing — so move that call into -`starlight({ plugins: [astroc4()] })`; left in `integrations`, the diagrams still -render but the translated UI and LikeC4 highlighting do not, and the plugin warns -about it. - -(`astro`'s integrations library indexes npm by keyword, and this package carries -`astro-integration` with `ui` for a category.) - -Working on astroc4 itself? `bun add @purview-dev/astroc4@file:../path/to/astroc4` copies the -package into `node_modules`, so run `bun install` in the site again after -changing the plugin. +installs the package and writes `integrations: [astroc4()]`, which works because +the default export implements the Astro integration protocol too. In a Starlight +site move that call into `starlight({ plugins: [astroc4()] })`; left in +`integrations`, the diagrams still render but the translated UI and LikeC4 +highlighting do not, and the plugin warns about it. ## Use ### With Starlight -```js -// astro.config.mjs +```ts +// astro.config.ts +import { defineConfig } from 'astro/config'; import starlight from '@astrojs/starlight'; -import react from '@astrojs/react'; import astroc4 from '@purview-dev/astroc4'; export default defineConfig({ - integrations: [starlight({ plugins: [astroc4()] }), react()], + integrations: [starlight({ title: 'My docs', plugins: [astroc4()] })], }); ``` @@ -72,66 +68,100 @@ import LikeC4View from '@purview-dev/astroc4/components/LikeC4View.astro'; ``` +`@astrojs/react` is added for you when the site has no React renderer (the +islands are `client:only="react"`); list `react()` yourself if you prefer it +explicit. + ### With Astro (no Starlight) -```js -// astro.config.mjs +```ts +// astro.config.ts import { defineConfig } from 'astro/config'; import mdx from '@astrojs/mdx'; import expressiveCode from 'astro-expressive-code'; import astroc4 from '@purview-dev/astroc4/integration'; +import { likec4Language } from '@purview-dev/astroc4/expressive-code'; export default defineConfig({ - // astroExpressiveCode() must come before mdx() — its own requirement. - integrations: [astroc4(), expressiveCode(), mdx()], + // Expressive Code must come before mdx() — its own requirement. + integrations: [expressiveCode({ shiki: { langs: [likec4Language] } }), astroc4(), mdx()], }); ``` -The page is the same `` import. Two things the site supplies: -`@astrojs/mdx` (` ```likec4 ` fences only become diagrams in `.mdx`), and -`astro-expressive-code` plus an `ec.config.mjs` that registers `likec4Language` -when you want ` ```c4 ` highlighted — Starlight has an `expressiveCode` option for -the plugin to fill in, Astro does not. +The site supplies `@astrojs/mdx` (for `` and fences in `.mdx`; +fences in plain `.md` work without it) and, for highlighting, +`astro-expressive-code` given `likec4Language` — Starlight has an +`expressiveCode` option for the plugin to fill in, Astro does not. +[`examples/astro`](examples/astro) is a working site with exactly this config +(`bun run dev:astro`). + +### Projects + +A diagram comes from one of three places: + +- **A discovered project** — every folder under the site root with a LikeC4 + config file. All of LikeC4's names are read, the way LikeC4 reads them: + `.likec4rc`, `.likec4.config.json` and `likec4.config.json` as JSON5, + `likec4.config.{ts,mts,cts,js,mjs,cjs}` bundled with esbuild. The project is + named by its config's `name`, or after its folder. +- **A named project** — `projects: { : }` adds LikeC4 projects + from anywhere: a sibling folder, another checkout, an absolute path. The key is + the name pages use. The project's own config, model and icons are used exactly + as they are. +- **A single file** — ``, or a fence's + `file="…"`, compiled at build time like an inline fence. + +```ts +// src/likec4/likec4.config.ts +import { defineConfig } from 'likec4/config'; + +export default defineConfig({ name: 'docs', imageAliases: { '@': './images' } }); +``` + +```ts +starlight({ + plugins: [astroc4({ projects: { architecture: '../../docs/architecture/likec4' } })], +}); +``` -`react()` is **not** on that list: `astroc4()` adds `@astrojs/react` when the -site has no React renderer, because the islands are `client:only="react"` and -nothing renders without one. [Plain Astro](docs/wiki/guide/Plain-Astro.md) has -the full walkthrough, and [`examples/astro`](examples/astro) is a working site -with exactly this config (`bun run dev:astro`). +```mdx + +``` -`astro add @purview-dev/astroc4` writes the `astroc4()` line for you in this -shape; you still add the other integrations (`astro add mdx -astro-expressive-code`) and the `ec.config.mjs`. +With exactly one project, `project` may be left out. With more than one, +leaving it out fails the build and lists the names — for `` and +empty fences, in `.md` and `.mdx` alike. An unknown name fails the build the same +way. Two projects with the same name, or whose configs declare the same `name`, +fail the build naming both folders. -The plugin registers `LikeC4VitePlugin`, which compiles every -`likec4.config.json` project under the site root. Keep exactly the projects you -mean to render there: the first one found is the default for `` -without a `projectId`, so a stray `.c4` project (a vendored example, say) can -silently replace your own. +Named projects are compiled through links in `.astro/astroc4/workspace` (Astro's +cache folder, which Astro sites already ignore); edits to their `.c4` files +reload in `astro dev` like any other. ### Options | Option | Default | | | --- | --- | --- | -| `vitePlugin` | `{}` | Passed to `LikeC4VitePlugin`, e.g. `{ workspace: 'diagrams' }`. | +| `projects` | `{}` | Named LikeC4 project folders, relative to the site root or absolute. See above. | +| `vitePlugin` | `{}` | Passed to `LikeC4VitePlugin`. Setting `workspace` or `languageServices` takes discovery over: `projects` is then ignored (with a warning). | | `fences` | `true` | Turn ` ```likec4 ` fences into diagrams. | -| `highlighting` | `true` | Highlight LikeC4 source. Starlight only — a plain Astro site uses `ec.config.mjs`. See below. | +| `highlighting` | `true` | Highlight LikeC4 source. Starlight only — a plain Astro site passes `likec4Language` to Expressive Code. See below. | ### Syntax highlighting Under Starlight the plugin reads `packages/language-server/src/like-c4.langium` from `likec4/likec4`, derives a TextMate grammar from it, and registers that with -the site's Expressive Code — so ` ```c4 ` fences, ` ```likec4 ` fences and +the site's Expressive Code — so ` ```c4 ` fences, ` ```likec4 ` source and `` all highlight with no configuration. Highlighting follows the language: when upstream adds a shape or a keyword, the next build picks it up. -```js +```ts starlight({ plugins: [ astroc4({ highlighting: { - ref: 'v1.59.4', // branch, tag or commit SHA; default 'main' + ref: 'v1.60.0', // branch, tag or commit SHA; default 'main' }, }), ], @@ -147,11 +177,12 @@ starlight({ | `cacheTtl` | `3600` | Seconds a cached fetch stays fresh. | | `refresh` | `false` | Ignore the cache and refetch. | | `cacheDir` | `node_modules/.cache/astroc4` | Where to cache. | -| `baseUrl` | `raw.githubusercontent.com` | Raw host to read from. | +| `baseUrl` | `https://raw.githubusercontent.com` | Raw host to read from. | | `timeout` | `15000` | Request timeout in milliseconds. | -`ASTROC4_REF` and `ASTROC4_OFFLINE=1` override `ref` and `offline`, which keeps -CI configuration out of the config file. +`ASTROC4_REF` overrides `ref`, and `ASTROC4_OFFLINE=1` stands in for `offline` +when the config does not set it, which keeps CI configuration out of the config +file. The grammar is resolved while Astro loads its config, so `astro dev`, `astro build`, `astro check` and `astro sync` all fail on a `ref` that does not @@ -160,36 +191,48 @@ LikeC4 grammar — the same way any other build error does. Air-gapped builds us `offline: true`; builds that must not hard-fail use `onError: 'warn'`, which falls back to the last fetched grammar and then to the bundled snapshot. -Fetched grammars are cached per `repo@ref`, so switching `ref` never thrashes -and repeated builds do not touch the network. - -A site that already has an `ec.config.mjs` keeps working, and can pin the -bundled grammar there instead — which is also what a plain Astro site does: - -```js -// ec.config.mjs -import { defineEcConfig } from '@astrojs/starlight/expressive-code'; -import { likec4Language } from '@purview-dev/astroc4/expressive-code'; - -export default defineEcConfig({ shiki: { langs: [likec4Language] } }); -``` - -Expressive Code concatenates `shiki.langs` when it merges an `ec.config.mjs` -with the integration options, so its own languages and the plugin's coexist. +A Starlight site that already has an `ec.config.mjs` keeps working: Expressive +Code concatenates `shiki.langs` when it merges that file with the plugin's +options, so its own languages and LikeC4 coexist. ### `` props | Prop | Type | Default | | --- | --- | --- | | `viewId` | `string` | required | -| `projectId` | `string` | first project | +| `project` | `string` | the only project; required when there are several | +| `file` | `string` | a `.c4` file relative to `src/`, instead of a project | +| `projectId` | `string` | deprecated alias of `project` | | `title` / `description` | `string` | translated defaults | | `height` | `number` | `480` | | `collapsible` / `defaultOpen` | `boolean` | `true` | +`file` must name a `.c4` file under the site's `src/` folder; anything else +fails the build. A single file has no config, so it has no `imageAliases`: use a +pack, a URL or a `public/` icon there. + ### Fence options -` ```likec4 view="…" title="…" description="…" height=420 collapsible=false defaultOpen=false ` +` ```likec4 view="…" project="…" file="…" title="…" description="…" height=420 collapsible=false defaultOpen=false ` + +`file` is relative to the Markdown page and must stay inside the site. Fences in +`.md` and `.mdx` take the same options; in `.md` the UI strings are English. + +### Styling + +Set any token on an ancestor — `:root` works on any site: + +```css +:root { + --likec4-border: #d4d4d8; + --likec4-accent: #0046be; +} +``` + +`--likec4-border`, `-surface`, `-surface-alt`, `-text`, `-text-muted`, +`-accent`, `-skeleton`, `-danger`, `-danger-soft` and `-danger-text`. Unset, +they fall back to Starlight's `--sl-color-*`, then to neutral greys. See +[Components](docs/wiki/guide/Components.md#styling). ### Translating @@ -199,10 +242,21 @@ because some are filled in by the browser, not by i18next. ## Icons -Use a bundled pack (`azure:`, `aws:`, `gcp:`, `tech:`, `bootstrap:`), an -absolute URL, or a file under the site's `public/` referenced by a path -relative to the `.c4` file. Don't use LikeC4's `imageAliases`: the compiler -bakes a build-machine `file://` path into the client bundle. +Every form LikeC4 accepts works, in discovered and named projects alike: + +- a bundled pack (`azure:`, `aws:`, `gcp:`, `tech:`, `bootstrap:`, `md:`); +- an absolute URL; +- `imageAliases` from the project's config — `"@": "../../.assets/images"` and + `icon @/azure/entra.svg` — with no change to the project; +- a path relative to the `.c4` file, inside or outside the project. + +Local files are rendered by LikeC4 itself (SVGs inline) and also served at +`/_astroc4//` — `@/azure/entra.svg` becomes +`/_astroc4/architecture/azure/entra.svg`, a named alias `@brand` adds a `brand/` +segment — and copied into the build output. Files under `public/` keep their +public URL. No `file://` path from the build machine reaches the bundle, and an +icon that does not exist fails the build. Nothing is written into your source +tree. ## Troubleshooting @@ -215,7 +269,10 @@ architecture diagram…" — open the page in `astro dev`, or run | --- | --- | --- | | `The requested module '/node_modules/react/compiler-runtime.js' does not provide an export named 'c'` in the console, diagrams stuck on the loading placeholder | `likec4/react` (1.59.4 and later) is compiled with the React Compiler and imports `c` from React 19's CommonJS `react/compiler-runtime`; in dev Vite serves it unoptimized, so the island never mounts. Production builds bundle it, which is why the build still passes. | Fixed in `1.0.0-prerelease.5`: the plugin adds `react/compiler-runtime` to `vite.optimizeDeps.include`. On an older version, or to confirm by hand: `vite: { optimizeDeps: { include: ['react/compiler-runtime'] } }`. | | Build fails with `could not fetch the LikeC4 grammar from https://raw.githubusercontent.com/…` | The grammar is resolved while the config loads, and the machine cannot reach GitHub. | `highlighting: { offline: true }` (bundled snapshot) or `onError: 'warn'`; `ASTROC4_OFFLINE=1` in CI. | -| `` shows "Unknown LikeC4 projectId" | Every `.c4` project under the site root is compiled and the first is the default, so a vendored model can take over. | Keep only the projects you render, pass `projectId`, or point `vitePlugin.workspace` at one directory. | +| Build fails with `Several LikeC4 projects exist (…); pass project="…"` | The site has more than one project (discovered or named) and a `` or fence names none. | Pass `project=""`; the message lists the names. | +| Build fails with `two LikeC4 projects are named "…"` | A named project and a discovered one, or two folders, share a name or a config `name`. | Rename one, or name the configured project differently. | +| Build fails with `…likec4.config… could not be read` | The config does not parse (JSON5) or does not bundle (JS/TS). | Fix the error after the path; a JS/TS config's imports other than `likec4/config` must resolve from its folder. | +| A diagram shows "Unknown LikeC4 projectId" | Only on a site that sets `vitePlugin.workspace` itself, where astroc4 does not know the projects. | Pass a `project` the workspace contains. | | `Cannot find module …/@purview-dev/astroc4/dist/index.js` | That version shipped without its build output (only `1.0.0-prerelease.3` did). | Move to `1.0.0-prerelease.4` or later. | Everything else — highlighting that produces one colour, fences left as code, @@ -228,12 +285,12 @@ Windows module-runner crashes — is in bun install bun run dev # Starlight playground at http://localhost:4321 (English + French) bun run dev:astro # plain-Astro example at http://localhost:4322 -bun run test # hermetic unit suite: Langium parsing, grammar, fences, plugin -bun run test:network # the same, plus a check against the live likec4/likec4 +bun run test # hermetic unit suite: config loading, grammar, fences, plugin +bun run test:network # the live check against likec4/likec4 bun run test:site # pack the tarball, install it into copies of playground/ and # examples/astro/, build both, run the full smoke suite against # the Starlight copy, and check the plain one hydrates -bun run check # astro check over playground/, examples/astro/ and src/ +bun run check # astro check over playground/, examples/astro/ and src/, tsc over scripts/ bun run grammar:check # fail if the committed grammar snapshot is stale bun run build && bun run preview bun run build:astro # static build of the plain-Astro example @@ -241,6 +298,8 @@ bun scripts/smoke.ts http://localhost:4321 # headless: hydration, icons, highl bun scripts/hydration.ts http://localhost:4321 # headless: one diagram, nothing else ``` +The source is TypeScript throughout, configs included (`astro.config.ts`), with +`.ts` relative imports; `bun run build:lib` bundles it into `dist/`. `playground/` is the Starlight development site; `examples/astro/` is the plain-Astro one, and `test:site` copies it so the checked-in example is what the suite builds. Both import the package by name — Node's self-reference resolves @@ -249,16 +308,17 @@ it from the repository, which *is* the package. `test:site` and the two browser scripts need Chromium (`bunx playwright install chromium`). -`Justfile` wraps each of those (`just verify` runs what the PR workflow runs). -`src/grammar/likec4.tmLanguage.json` and `src/grammar/like-c4.langium` are -generated — `bun run grammar:sync [--ref ]` refreshes them from upstream, -and never by hand. - -CI lives in `.github/workflows`: `pr.yml` runs the organisation gate plus the -unit suite, the browser smoke test and the packed-consumer build; `release.yml` -publishes to npm with **trusted publishing** — an OIDC token, no stored npm -secret — under the `prerelease` dist-tag for prerelease versions, then tags the -release. See [`docs/wiki/contributing/Repository-CI-CD.md`](docs/wiki/contributing/Repository-CI-CD.md). +`Justfile` wraps each of those (`just verify` runs the PR gate's chain plus +`test:site`). `src/grammar/likec4.tmLanguage.json` and +`src/grammar/like-c4.langium` are generated — `bun run grammar:sync [--ref ]` +refreshes them from upstream, and never by hand. + +CI lives in `.github/workflows`: `pr.yml` runs the organisation gate (grammar +snapshot, type-check, build, unit suite), the live grammar check, the browser +smoke test and the packed-consumer build; `release.yml` publishes to npm with +**trusted publishing** — an OIDC token, no stored npm secret — under the +`prerelease` dist-tag for prerelease versions, then tags the release. See +[`docs/wiki/contributing/Repository-CI-CD.md`](docs/wiki/contributing/Repository-CI-CD.md). Open the folder in VS Code and install the recommended **LikeC4** extension for the language server: diagnostics, go-to-definition, hover and a live diff --git a/bun.lock b/bun.lock index 5c79dc9..38c1014 100644 --- a/bun.lock +++ b/bun.lock @@ -4,6 +4,11 @@ "workspaces": { "": { "name": "astroc4", + "dependencies": { + "bundle-require": "^5.1.0", + "esbuild": "^0.28.2", + "json5": "^2.2.3", + }, "devDependencies": { "@astrojs/check": "^0.9.12", "@astrojs/markdown-satteri": "^0.4.6", @@ -627,6 +632,8 @@ "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], + "jsonc-parser": ["jsonc-parser@3.3.1", "", {}, "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ=="], "kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="], diff --git a/docs/wiki/README.md b/docs/wiki/README.md index faa014b..ce05808 100644 --- a/docs/wiki/README.md +++ b/docs/wiki/README.md @@ -7,8 +7,9 @@ Interactive [LikeC4](https://likec4.dev) architecture diagrams for bun add @purview-dev/astroc4 ``` -```js -// astro.config.mjs +```ts +// astro.config.ts +import { defineConfig } from 'astro/config'; import starlight from '@astrojs/starlight'; import react from '@astrojs/react'; import astroc4 from '@purview-dev/astroc4'; @@ -29,19 +30,20 @@ plugin, ` ```likec4 ` fences become diagrams, and LikeC4 source is highlighted b Expressive Code with no `ec.config.mjs`. It also works without Starlight. `@purview-dev/astroc4/integration` is a plain -`AstroIntegration` that adds the same model compiler and fence transform; the -site supplies `@astrojs/react`, `@astrojs/mdx` and — only for highlighting — -`astro-expressive-code` with an `ec.config.mjs`. See +`AstroIntegration` that adds the same model compiler and fence transform, and +adds `@astrojs/react` when the site has not. The site supplies `@astrojs/mdx` +(only for components and fences in `.mdx`) and, only for highlighting, +`astro-expressive-code` given `likec4Language`. See [Plain Astro](guide/Plain-Astro.md). ## What it gives you | | | | --- | --- | -| `` | Render a view from the site's `.c4` model, as a hydrated island. | +| `` | Render a view from the site's `.c4` model, as a hydrated island. Projects are found by any LikeC4 config file, JSON or JS/TS. | | ` ```likec4 ` fences | Inline DSL compiled and laid out **at build time**, or an empty fence with `view="…"`. A typo fails the build. | | Syntax highlighting | A TextMate grammar derived from the Langium grammar in [likec4/likec4](https://github.com/likec4/likec4) at a ref you choose, registered with Expressive Code. | -| Translatable UI | Every string the components render goes through Starlight's i18n. | +| Translatable UI | Every string the Astro components render goes through Starlight's i18n (fences in `.md` pages are English-only). | | Fails like a build | A bad grammar ref, an unreachable repository or an invalid fence fails `dev`, `build`, `check` and `sync`. | ## Documentation @@ -51,7 +53,7 @@ lists its own pages. | | | | --- | --- | -| [guide](guide/README.md) | Using the plugin: getting started, fences, components, highlighting, icons, i18n, options. | +| [guide](guide/README.md) | Using the plugin: getting started, projects, fences, components, highlighting, icons, i18n, options. | | [contributing](contributing/README.md) | Changing it: the module map, the local workflow, CI and releases. | | [Troubleshooting](Troubleshooting.md) | The failures people actually hit. | @@ -60,7 +62,7 @@ four files. ## Requirements -Astro 7, React 19, LikeC4 1.59.4 and Node 22.19 (or Bun 1.4.4). Starlight +Astro 7.4.4, React 19, LikeC4 1.59.4 and Node 22.19 (or Bun 1.4.4). Starlight 0.42.8 is required for the plugin entry point (`@purview-dev/astroc4`), not for `@purview-dev/astroc4/integration`. The peer ranges are declared in `package.json`; the Starlight floor is load-bearing, because the plugin relies on diff --git a/docs/wiki/Troubleshooting.md b/docs/wiki/Troubleshooting.md index 5a2f499..ed6309e 100644 --- a/docs/wiki/Troubleshooting.md +++ b/docs/wiki/Troubleshooting.md @@ -43,8 +43,8 @@ runtime at all. On a release before the fix, or to check the diagnosis by hand, add the entry yourself: -```js -// astro.config.mjs +```ts +// astro.config.ts export default defineConfig({ vite: { optimizeDeps: { include: ['react/compiler-runtime'] }, @@ -74,9 +74,10 @@ bun run test:site # the packed consumer site `hydration.ts` waits for a `[data-likec4-node], .react-flow__node` element (LikeC4 renders into a shadow root, which Playwright locators pierce), fails if the `.likec4-loading` placeholder is still visible, and fails on any console -error. `bun run test:site` runs exactly that against a throwaway Starlight site -built from the tarball, on that site's own dev server, which is the regression -test for this bug. +error. `bun run test:site` runs exactly that against a throwaway plain-Astro site +built from the tarball, on that site's own **dev** server, which is the +regression test for this bug. (Its Starlight copy runs the smoke suite on a +preview server, which serves the bundled build and so cannot see it.) ## The build fails and the log mentions the grammar @@ -104,8 +105,8 @@ because it will fail the same way on every build. Check the tag (`v1.60.0`, not 1. Is `expressiveCode: false` in the Starlight config? The plugin logs a warning and skips highlighting, because there are no code blocks to highlight. -2. Is the fence ` ```c4 ` or ` ```likec4 `? A ` ```likec4 ` fence in **`.mdx`** - becomes a diagram, not code; use `c4` to show source, or ````. +2. Is the fence ` ```c4 ` or ` ```likec4 `? A ` ```likec4 ` fence (in `.md` or + `.mdx`) becomes a diagram, not code; use `c4` to show source, or ````. 3. Look at the HTML, not the exit code: `
` should contain
    several `--0:#…` colour variables. One colour means the grammar never reached
    Shiki.
@@ -119,24 +120,46 @@ You cannot have both: with Expressive Code disabled Starlight renders code with
 Astro's own ``, which reads `markdown.shikiConfig`, not the Expressive Code
 options this plugin sets.
 
-## A fence stayed a code block
+## A fence in a `.md` page shows "Loading diagram…" forever
+
+Since `1.0.0-prerelease.6` a fence in a `.md` page is a placeholder that a script
+astroc4 injects into every page mounts in the browser (a `.md` page cannot hold a
+component). If it never mounts:
+
+1. Check the browser console for a failed `import()` of
+   `@purview-dev/astroc4/components/md-fences`.
+2. Check the integration ran: the page's HTML has a `data-astroc4-fence` element
+   and a page script containing `[data-astroc4-fence]`.
+3. A page rendered outside Astro's pipeline (raw HTML, a custom renderer) does not
+   get the injected script.
+
+
+## Build fails: `Several LikeC4 projects exist (…); pass project="…"`
+
+The site has more than one LikeC4 project — discovered under the site root or
+named in `projects` — and a `` or empty fence names none. Since
+`1.0.0-prerelease.6` there is no "first project found" default, so a vendored
+model can no longer silently replace yours. Add `project=""`; the message
+lists the names. An unknown name fails the same way.
 
-The page is `.md`. Astro only supports components in `.mdx`, so the transform
-leaves the fence alone and logs:
+## Build fails: `…likec4.config… could not be read`
 
 ```
-[likec4-fences] /path/page.md has a ```likec4 fence, but fences only render as diagrams in .mdx files.
+[astroc4] …/likec4.config.ts could not be read: …
 ```
 
-Rename the page, or use `` from an `.astro` layout.
+Astroc4 reads every LikeC4 config file it discovers, to learn the project's name
+and `imageAliases`: JSON names as JSON5, JS/TS names by bundling them with
+esbuild. The message after the path is the parser's or esbuild's. A TS config
+may import `likec4/config` (it is replaced with a stand-in), but anything else it
+imports has to resolve from the config's folder.
 
-## `LikeC4View` shows "Unknown LikeC4 projectId"
+## A diagram shows "Unknown LikeC4 projectId"
 
-The plugin compiles **every** `.c4` project under the site root, and the first one
-found is the default. A vendored example model (a `node_modules` copy, a docs
-sample) can silently become the default. Keep only the projects you mean to
-render, or pass `projectId` explicitly, or point `vitePlugin.workspace` at one
-directory.
+Only on a site that sets `vitePlugin.workspace` (or `languageServices`) itself:
+astroc4 then does not know the projects at build time, and the browser island
+reports the unknown name. Pass a `project` that workspace contains. Everywhere
+else an unknown project fails the build.
 
 ## `Invalid … Could not resolve reference to ElementKind named 'person'`
 
@@ -157,9 +180,9 @@ build, which is the point.
 ## The build crashed on Windows with "Vite module runner has been closed"
 
 That is the race `preloadLikeC4()` and the `require()`-instead-of-`import()`
-comment in `src/fences.mjs` exist to prevent. If it comes back, the preload is no
+comment in `src/fences.ts` exist to prevent. If it comes back, the preload is no
 longer running before the first page render (look at `command === 'build'` in
-`src/index.mjs`).
+`src/integration.ts`).
 
 ## Highlighting changed without a code change
 
@@ -167,10 +190,11 @@ The default ref is `main`, which moves. Pin a tag or a SHA, or set `ASTROC4_REF`
 in CI. The build log always names what it used:
 
 ```
-[astroc4] LikeC4 grammar likec4/likec4@main (da5af77245f9) — highlighting ```c4 and ```likec4 fences
+[@purview-dev/astroc4] LikeC4 grammar likec4/likec4@main (da5af77245f9) — highlighting ```c4 and ```likec4 fences
 ```
 
-`from cache` / `from bundled` in that line tells you which path was taken.
+`from cache` / `from bundled` after the SHA tells you which path was taken;
+nothing there means it was fetched.
 
 ## My theme does not colour LikeC4 source
 
diff --git a/docs/wiki/contributing/Architecture.md b/docs/wiki/contributing/Architecture.md
index 555054d..3d8fc19 100644
--- a/docs/wiki/contributing/Architecture.md
+++ b/docs/wiki/contributing/Architecture.md
@@ -9,10 +9,15 @@ Vite plugin, plus the fence transform) and the highlighting (the grammar).
 src/                                  TypeScript sources — bundled into dist/ by build:lib
   index.ts                  the Starlight plugin: hooks, options, Expressive Code injection
   integration.ts            the AstroIntegration both entry points install
-  fences.ts                 ```likec4 → component; compiles and lays out inline DSL
+  fences.ts                 ```likec4 → component (.mdx) or placeholder (.md); lays out DSL
+  projects.ts               discovered + named projects; reads every LikeC4 config file name
+  workspace.ts              the staging workspace of links; `virtual:astroc4/projects`
+  file-layout.ts            Vite plugin: `` → layouted model at build time
+  icon-plugin.ts            Vite plugin: `file://` icons → `/_astroc4/…`, served and copied
+  icon-assets.ts            icon file → served URL, collision and missing-file checks
   expressive-code.ts        `likec4Language` (bundled) + `loadLikeC4Language(options)`
   translations.ts           the UI strings injected into Starlight's i18n
-  types.d.ts                ambient types for `likec4:*` virtual modules and `t()`
+  types.d.ts                ambient types for `virtual:astroc4/projects`
   grammar/
     langium.ts              Langium text → vocabulary (blocks, keywords, domains, terminals)
     textmate.ts             vocabulary → TextMate JSON
@@ -25,8 +30,9 @@ src/                                  TypeScript sources — bundled into dist/
     DiagramShell.astro      collapsible shell, plain CSS over the --likec4-* tokens
     DiagramFrame.astro      the sized container
     LoadingPlaceholder.astro  pre-hydration placeholder (carries the height)
-    LikeC4ProjectView.tsx   island: loads a project from `likec4:react/`
+    LikeC4ProjectView.tsx   island: loads a project through `likec4:model`'s `loadModel`
     LikeC4InlineView.tsx    island: renders a build-time model dump
+    md-fences.tsx           browser module: mounts the islands into .md fence placeholders
     ErrorBoundary.tsx       translated error UI with retry
     labels.ts               server → island string plumbing, `format()`
     render-icon.tsx         icon rendering, `withImageIcons`
@@ -43,9 +49,9 @@ Two entry points, one implementation:
 
 `index.ts` calls `likec4Integration()` and adds what only Starlight has: the
 `i18n:setup` injection, the `expressiveCode` option, and Starlight's plugin
-context. Everything the diagrams need — the Vite plugin, the fence transform,
-the preload, the `optimizeDeps` fix — lives in `integration.ts`, so the two hosts
-cannot drift.
+context. Everything the diagrams need — the Vite plugins, project resolution, the
+fence transform and its `.md` page script, the preload, the `optimizeDeps` fix,
+the React renderer — lives in `integration.ts`, so the two hosts cannot drift.
 
 The default export carries **both** hook sets: the Starlight plugin hooks and,
 via spread, the integration's `astro:config:setup`. That is what makes
@@ -61,13 +67,19 @@ cannot run).
 
 **A diagram renders in three steps.**
 
-1. *Build, model.* `LikeC4VitePlugin` compiles every `.c4` project into the
-   `likec4:model` / `likec4:icons` / `likec4:projects` virtual modules.
-   `` picks a project there.
-2. *Build, fences.* For a ` ```likec4 ` fence with DSL, `fences.ts` runs
-   `LikeC4.fromSource(source)` and `layoutedModel()`, then hands the resulting
-   `$data` JSON to `` as a prop. This is why layout needs no browser.
-3. *Browser.* `client:only="react"` islands hydrate with `@likec4/core`, render
+1. *Build, model.* `projects.ts` lists the discovered and named projects; with
+   named ones, `workspace.ts` links them all into `.astro/astroc4/workspace`.
+   `LikeC4VitePlugin` compiles that into the `likec4:model` / `likec4:icons` /
+   `likec4:projects` virtual modules, `icon-plugin.ts` rewrites their local
+   icon URLs, and `virtual:astroc4/projects` maps page names to LikeC4 ids.
+   `` resolves its `project` there, on the server.
+2. *Build, fences.* For a ` ```likec4 ` fence with DSL (or `file=`), `fences.ts`
+   runs `LikeC4.fromSource(source)` and `layoutedModel()`, then hands the
+   resulting `$data` JSON to `` as a prop (`.mdx`) or writes it into
+   a placeholder's `data-model` (`.md`). `` gets the same
+   through `file-layout.ts`. This is why layout needs no browser.
+3. *Browser.* `client:only="react"` islands — or, in `.md` pages, `md-fences.tsx`
+   loaded by the injected page script — hydrate with `@likec4/core`, render
    through `likec4/react`, and rehydrate either a project model or the inline
    dump. Nothing is laid out client-side.
 
@@ -81,8 +93,8 @@ Expressive Code integration, which is what makes the injection possible:
 
 | Detail | Why |
 | --- | --- |
-| `likec4` (the Node API) is never imported at module scope | It pulls Langium, chevrotain, an LSP and the WASM layout engine; doing it while `astro.config.mjs` loads triggered a config-reload restart that killed the content-layer transport on a cold start. `fences.ts` loads it lazily. |
-| On Windows, `require('likec4')` rather than `import('likec4')` | Vite's SSR module runner can already be tearing down when a page's fence needs the API, which crashed the build; `require()` goes through Node's resolver instead. |
+| `likec4` (the Node API) is never imported at module scope | It pulls Langium, chevrotain, an LSP and the WASM layout engine; doing it while `astro.config.ts` loads triggered a config-reload restart that killed the content-layer transport on a cold start. `fences.ts` loads it lazily. |
+| `require('likec4')` rather than `import('likec4')` | Vite's SSR module runner intercepts dynamic `import()` and, on Windows, can already be tearing down when a page's fence needs the API, which crashed the build; `require()` goes through Node's resolver instead. It is used on every platform. |
 | `preloadLikeC4()` on `command === 'build'` | Finishes that load before the race exists. Build-only, because eager-loading in dev is what caused the restart. |
 | Fence imports use the package name (`@purview-dev/astroc4/components/…`) | They are written into the *site's* page, so they resolve from the site, not from this package. |
 | The fence plugin is Sätteri mdast, appended to the site's processor | Astro 7 made Sätteri the default; `remarkPlugins` is a legacy option that is silently ignored without `@astrojs/markdown-satteri`. |
@@ -92,8 +104,12 @@ Expressive Code integration, which is what makes the injection possible:
 | `react/compiler-runtime` is added to `vite.optimizeDeps.include` | `likec4/react` is React-Compiler-built and imports `c` from React 19's CommonJS runtime. Unoptimized, dev serves it raw and every island dies with "does not provide an export named 'c'". `astro build` hides it by bundling correctly. Only missing entries are sent, because Astro merges `vite` with Vite's `mergeConfig`, which concatenates arrays. |
 | `@astrojs/react` is added when the site has not | The islands are `client:only="react"`, and without a renderer a site builds and then draws nothing. Pushing it through `updateConfig({ integrations })` works because Astro's config loop reads integrations appended while it runs (`runHookConfigSetup`) — the same mechanism Starlight uses to add Expressive Code. Only that one: MDX and Expressive Code change the host's content types and code blocks, so they stay the site's decision. |
 | Integration ordering is not ours to impose | `astroc4()` and `astroExpressiveCode()` work in either order, because Astro applies the processor's mdast plugins *and* `markdown.rehypePlugins` (Expressive Code's fallback when it runs before a processor exists). The example lists Expressive Code first so that stays true. Only Expressive-Code-before-MDX matters, and Expressive Code enforces it itself. |
-| Integration-time warnings go through Astro's `logger`, never `console` | The logger honours the CLI log level and labels the message with the integration's name; `logger.fork('likec4-fences')` keeps the `[likec4-fences]` label the Troubleshooting page quotes. `console` is only for browser code (`render-icon.tsx`, `ErrorBoundary.tsx`) and `scripts/`. |
-| Colours are a `--likec4-*` layer over `--sl-color-*` | Starlight defines the tokens; plain Astro does not, and an undefined `var()` with no fallback is invalid at computed-value time. Each component declares the tokens it uses with a scheme-neutral fallback (`rgba(128,128,128,…)`, `inherit`), which is why the same components frame a diagram on either host. |
+| Integration-time warnings go through Astro's `logger`, never `console` | The logger honours the CLI log level and labels the message with the integration's name (`[@purview-dev/astroc4]` from the Starlight plugin, `[@purview-dev/astroc4/integration]` from the integration). `console` is only for browser code (`render-icon.tsx`, `ErrorBoundary.tsx`) and `scripts/`. |
+| Fences in `.md` are placeholders plus one injected page script | A `.md` page cannot hold a component. The script runs on every page but only `import()`s `md-fences` when a `[data-astroc4-fence]` exists, and listens for `astro:page-load` for view transitions. |
+| Named projects are linked, not copied, into `.astro/astroc4/workspace` | `LikeC4VitePlugin` compiles one folder. Links (junctions on Windows, which need no elevation) keep edits live; `.astro/` rather than `node_modules/` because LikeC4 ignores `**/node_modules/**`. `icon-assets.ts` `unstage()`s icon paths LikeC4 resolved lexically through the link. |
+| Colours: public `--likec4-*` tokens, read but never declared | Each component reads them into private `--_likec4-*` properties with the fallbacks (`var(--likec4-border, var(--sl-color-gray-5, rgba(…)))`). Declaring the public names on the components' own elements would shadow a site's value from an ancestor, and the scoped selector would out-rank one on the same element. Every chain ends in a scheme-neutral value, because an undefined `var()` with no fallback is invalid at computed-value time. `tokens.test.ts` enforces it. |
+| Config files are read the way LikeC4 reads them | All nine names in `ConfigFilenames` (`satisfies` fails the type-check if LikeC4 changes the list). JSON names via JSON5; JS/TS via `bundle-require` (esbuild), with `likec4/config` replaced by identity functions so an external project need not resolve it; `name` defaults to the folder name. Both loaders are `import()`ed lazily. |
+| Relative imports use `.ts` | `allowImportingTsExtensions`; Bun and Vitest resolve them, and `scripts/build.ts` rewrites them to `.js` in the emitted `.d.ts` (which sit next to `.js` bundles) and fails if one survives. |
 | `keywords` carries `astro-integration`, `withastro` and a category (`ui`) | Astro's [integrations library](https://docs.astro.build/en/guides/integrations/#integrations-library) indexes npm by those keywords and reads `name`, `description`, `repository` and `homepage` from `package.json`. Without a category keyword the listing lands under "Uncategorized". |
 
 ## Tests
@@ -104,4 +120,4 @@ Expressive Code integration, which is what makes the injection possible:
 | Live grammar | `network.test.ts`, opt-in with `ASTROC4_NETWORK=1` |
 | Rendering, hydration, icons, i18n | `scripts/smoke.ts` (Playwright against the playground) |
 | One diagram hydrating, on either host | `scripts/hydration.ts` (Playwright; used by `test:site` against each consumer's dev server) |
-| The shipped package | `scripts/test-site.ts` (packs the tarball, builds a Starlight site and a plain-Astro site — the latter copied from `examples/astro` — then checks hydration on both dev servers) |
+| The shipped package | `scripts/test-site.ts` (packs the tarball, builds a copy of the playground and a copy of `examples/astro` from it, runs the full smoke suite against the playground copy's preview server and `hydration.ts` against the plain copy's dev server, asserts a site's `--likec4-accent` reaches the shell, then rebuilds the plain copy in the `astro add` shape) |
diff --git a/docs/wiki/contributing/Local-Development.md b/docs/wiki/contributing/Local-Development.md
index 5bd2a08..35f7007 100644
--- a/docs/wiki/contributing/Local-Development.md
+++ b/docs/wiki/contributing/Local-Development.md
@@ -9,14 +9,14 @@ bun run dev:astro  # plain-Astro example at http://localhost:4322
 `playground/` is a small Starlight site (English and French) that consumes the
 package through its own `exports`, so it behaves like an installed copy without a
 pack step. `examples/astro/` is the same thing without Starlight — one page, one
-model, `ec.config.mjs` for highlighting — and it is what `test:site` copies and
+model in a `likec4.config.ts` project, `likec4Language` passed to `expressiveCode()` for highlighting — and it is what `test:site` copies and
 builds, so it cannot rot.
 
 ## Scripts
 
 | Script | What it does |
 | --- | --- |
-| `bun run build:lib` | Bundle `src/` into `dist/` (Bun for the JS — three entry points — `tsc` for the `.d.ts`, plus the two grammar data files). |
+| `bun run build:lib` | Bundle `src/` into `dist/` (Bun for the JS — three entry points — `tsc` for the `.d.ts`, with `.ts` specifiers rewritten to `.js`, plus the two grammar data files). |
 | `bun run dev` | `build:lib`, then the playground dev server on port 4321. |
 | `bun run dev:astro` | `build:lib`, then the plain-Astro example on port 4322. |
 | `bun run build` | `build:lib`, then a static build of the playground. |
diff --git a/docs/wiki/contributing/Repository-CI-CD.md b/docs/wiki/contributing/Repository-CI-CD.md
index 194e929..17bcc87 100644
--- a/docs/wiki/contributing/Repository-CI-CD.md
+++ b/docs/wiki/contributing/Repository-CI-CD.md
@@ -11,7 +11,7 @@ here.
 | `Build and test` | Calls `purview-dev/build/.github/workflows/purview-build.yml@main` — the organisation gate. `.config/purview-build.json` declares `ProjectType: Web`, so it installs with Bun, runs the build command below, then the test command. `run-pack: false` and `validate-pack: false`: this repository ships no NuGet package. |
 | `Live grammar` | `bun run test:network` — the only test that talks to likec4/likec4. |
 | `Browser smoke test` | Builds the playground, starts `astro preview`, runs `scripts/smoke.ts` against it with Chromium installed. Uploads `preview.log` on failure. |
-| `Packed consumer site` | `bun run test:site`: pack the tarball, install it into two throwaway sites — a copy of `playground/` and a copy of `examples/astro/` — build both, run the full smoke suite against the Starlight copy on its preview server, and hydration checks on the plain-Astro copy's dev server and the `astro add` config shape. |
+| `Packed consumer site` | `bun run test:site`: pack the tarball, install it into two throwaway sites — a copy of `playground/` and a copy of `examples/astro/` — build both, run the full smoke suite against the Starlight copy on its preview server, a hydration check on the plain-Astro copy's dev server, and a rebuild of the plain copy in the `astro add` config shape. |
 
 ### `.config/purview-build.json`
 
@@ -87,10 +87,11 @@ The step only moves `latest` while `latest` itself is a prerelease, so once
 `1.0.0` ships a `1.0.1-prerelease.1` can never pull the default install back onto
 a prerelease.
 
-It also runs on `workflow_dispatch`, and that is how `latest` first reached
-`1.0.0-prerelease.4`: a dispatch re-runs the check without republishing anything
-(the publish step skips a version already on the registry), and the tag move
-succeeds over OIDC. Both tags now resolve `1.0.0-prerelease.4`.
+It also runs on `workflow_dispatch`, and that is how `latest` first reached a
+prerelease (`1.0.0-prerelease.4`): a dispatch re-runs the check without
+republishing anything (the publish step skips a version already on the
+registry), and the tag move succeeds over OIDC. Since then each publish has moved
+`latest` itself; `npm view @purview-dev/astroc4 dist-tags` shows where both point.
 
 ### The published package is a build artifact
 
@@ -133,7 +134,8 @@ GitHub org name, not the same thing on npm; create it at
  if `npm org ls purview-dev` reports it
 missing). If the organisation name is unavailable, the package can live under
 the personal scope instead: `@kieronlanning/astroc4`, which changes
-`package.json` and the specifiers in `src/index.ts` and `src/fences.ts` only.
+`package.json` and the `PACKAGE_NAME` constants in `src/integration.ts` and
+`src/fences.ts` (plus the documentation and tests that quote the name).
 
 The publish job runs in the GitHub **environment `npm`**. It declares no
 required reviewer, so a merged version bump publishes unattended; the environment
@@ -161,8 +163,8 @@ The CLI cannot grant the dist-tag permission (npm 11.x only exposes
 that one.
 
 Trusted publishing is configured **per package, in the package's settings**, so
-the package has to exist on npm first. `@purview-dev/astroc4` has never been
-published:
+the package has to exist on npm first. For `@purview-dev/astroc4` that was done
+once, as below; the steps are kept for a new package or a re-created one:
 
 1. **Bootstrap the first version once, locally**, from an account that can
    publish to the scope (`npm login`):
@@ -272,9 +274,12 @@ once, after the first publish — not from this repository.
 
 | Workflow job | Locally |
 | --- | --- |
-| `Build and test` | `just verify` — the same chain as `WebBuildCommand` then `WebTestCommand` |
+| `Build and test` | `bun run grammar:check && bun run check && bun run build && bun run test` — `WebBuildCommand` then `WebTestCommand` |
+| All four | `just verify` — the chain above, then `test:site` |
 | `Live grammar` | `bun run test:network` |
 | `Browser smoke test` | `bun run preview` then `bun scripts/smoke.ts http://localhost:4321` |
 | `Packed consumer site` | `bun run test:site` |
 
-`Justfile` wraps each of them.
+`Justfile` wraps each of them. `just verify` skips the live grammar check and
+the separate preview smoke run (`test:site` runs the smoke suite against the
+packed site instead).
diff --git a/docs/wiki/guide/Components.md b/docs/wiki/guide/Components.md
index 30933ad..23b1e6a 100644
--- a/docs/wiki/guide/Components.md
+++ b/docs/wiki/guide/Components.md
@@ -1,7 +1,8 @@
 # Components
 
 Two Astro components are exported. One is for authors; the other exists for the
-fence transform.
+fence transform. A third export, `@purview-dev/astroc4/components/md-fences`, is
+the browser module that mounts fences in `.md` pages; nothing imports it by hand.
 
 ## ``
 
@@ -9,13 +10,16 @@ fence transform.
 import LikeC4View from '@purview-dev/astroc4/components/LikeC4View.astro';
 
 
-
+
+
 ```
 
 | Prop | Type | Default | Meaning |
 | --- | --- | --- | --- |
 | `viewId` | `string` | required | Which view to render. |
-| `projectId` | `string` | the first project found | Which compiled LikeC4 project. |
+| `project` | `string` | the only project; required when there are several | A [named or discovered project](Projects.md). An unknown name fails the build. |
+| `file` | `string` | — | A single `.c4` file relative to `src/` (and under it), compiled at build time, instead of a project. |
+| `projectId` | `string` | — | Deprecated alias of `project`. |
 | `title` | `string` | translated `likec4.view.title` | Shell heading. |
 | `description` | `string` | translated `likec4.view.description` | Shell sub-heading. |
 | `height` | `number` | `480` | Frame height in pixels. |
@@ -52,30 +56,55 @@ unstyled.
 
 Colours come from a small `--likec4-*` layer that defaults to Starlight's design
 tokens and to scheme-neutral fallbacks (translucent greys, `inherit`) when those
-tokens are absent — which is the plain Astro case. Override either layer:
+tokens are absent — which is the plain Astro case:
 
-| Token | Default | Used for |
+| Token | When unset | Used for |
 | --- | --- | --- |
-| `--likec4-border` | `var(--sl-color-gray-5, rgba(128, 128, 128, 0.35))` | Shell, header and frame borders |
+| `--likec4-border` | `var(--sl-color-gray-5, rgba(128, 128, 128, 0.35))` | Shell, header and badge borders |
 | `--likec4-surface` | `var(--sl-color-bg, transparent)` | Header, badge and placeholder background |
 | `--likec4-surface-alt` | `var(--sl-color-bg-nav, rgba(128, 128, 128, 0.06))` | Shell background |
 | `--likec4-text` | `var(--sl-color-white, inherit)` | Shell title |
 | `--likec4-text-muted` | `var(--sl-color-gray-2, rgba(128, 128, 128, 0.95))` | Description, badge, loading label |
 | `--likec4-accent` | `var(--sl-color-text-accent, inherit)` | The hint line |
 | `--likec4-skeleton` | `var(--sl-color-gray-6, rgba(128, 128, 128, 0.18))` | Loading lines |
-| `--likec4-danger`, `--likec4-danger-soft`, `--likec4-danger-text` | Starlight's `--sl-color-red*`, else a red palette | The error boundary |
+| `--likec4-danger`, `--likec4-danger-soft`, `--likec4-danger-text` | Starlight's `--sl-color-red`, `-red-low`, `-red-high`, else a red palette | The error boundary |
+
+### Overriding the colours
+
+Set any token on any ancestor: `:root`, a page wrapper, or `.likec4-shell`
+itself.
+
+```css
+:root {
+	--likec4-border: #d4d4d8;
+	--likec4-surface: #ffffff;
+	--likec4-surface-alt: #f6f6f7;
+	--likec4-text: #18181b;
+	--likec4-text-muted: #52525b;
+	--likec4-accent: #0046be;
+}
+```
+
+The components never declare these tokens; they only read them, through private
+`--_likec4-*` properties that add the fallbacks. So a site's value is inherited
+rather than overridden by the components' scoped styles, and it reaches the
+frame, badge and placeholder as well as the shell. `.md` fences read the same
+tokens.
 
-Or target the classes directly:
+To change anything other than colour, target the classes. Astro scopes component
+styles as a class plus an attribute, so a site rule needs more specificity than
+that, e.g. `html .likec4-shell[class]`:
 
 | Class | Element |
 | --- | --- |
 | `likec4-shell` | The outer `
` | -| `likec4-shell__header` / `__title` / `__description` / `__hint` | The `` and its parts | +| `likec4-shell__header` / `__title` / `__description` / `__hint` | The `` (or `
` when not collapsible) and its parts | | `likec4-shell__body` | The content area | | `likec4-frame` | The sized container (also carries `--likec4-height`) | | `likec4-badge` | The "click a node" badge `` adds | | `likec4-loading` | The pre-hydration placeholder | | `likec4-error` | The error boundary's output | +| `likec4-md-fence` | A `.md` page's fence (inline styles over the same tokens) | `class="not-content"` on the shell is Starlight's opt-out from prose styles; on a plain Astro site it is inert. @@ -89,5 +118,5 @@ them: - client code imports `@likec4/core/model`, never `likec4/model` — the latter is the Node entry point and drags `createRequire` into the browser bundle, which fails at hydration; -- `client:only` fallbacks need `slot="fallback"`, or Starlight parks them in an +- `client:only` fallbacks need `slot="fallback"`, or Astro parks them in an inert `