Acceptance Criteria
@refrakt-md/astro defines a Vite virtual module virtual:refrakt/site-tokens.css whose load hook returns the output of composeSiteTokensCss(activeSite, configDir) from WORK-239- The integration also injects an import of
virtual:refrakt/site-tokens.css after the existing theme-package CSS import, ensuring cascade order (theme defaults first, site overrides second) - The virtual module is generated once at
astro:config:setup time (or memoised in astro:config:done) — the async preset loading must complete before any page renders - A test site under
examples/ or packages/create-refrakt/template-astro configured with theme.tokens.color.text = "#ff0000" and theme.presets = ["@refrakt-md/lumina/presets/nord"] renders body text in red and resolves Nord's token values on :root — diff-of-zero against the same config rendered through @refrakt-md/sveltekit site.tints.<name> = { extends: "@refrakt-md/lumina/presets/<preset>" } produces [data-tint="<name>"] scoped CSS in the built bundle (matches the validation in WORK-221)- Astro
BaseLayout.astro (or the template integration page) does not need any new author-facing changes — the virtual module side-effect import is enough - Documentation page
site/content/docs/adapters/astro.md notes the new automatic preset / tokens / tints support and links to SPEC-048 and SPEC-056
Approach
Astro integrations expose Vite plugin hooks through the vite field of the updateConfig object. The integration at packages/astro/src/integration.ts:33 already returns a vite object — extend it with a plugins array carrying a small refrakt-internal Vite plugin that:
- Resolves
virtual:refrakt/site-tokens.css → \0virtual:refrakt/site-tokens.css in resolveId - Returns the cached CSS string in
load - Computes the CSS once in the plugin's
buildStart hook by awaiting composeSiteTokensCss(site, configDir) (the same hook timing the SvelteKit plugin uses)
The injection point shifts: instead of injectScript('page-ssr', \import '${themePackage}';`)` we want two imports in order:
injectScript('page-ssr', `import '${themePackage}'; import 'virtual:refrakt/site-tokens.css';`);
injectScript('page-ssr', ...) runs in the SSR pre-render context, so the CSS import side-effect makes Vite include the resolved virtual module content in the page bundle exactly like a real file import.
Caching: the integration receives site once from resolveSite. The Vite plugin captures site + configDir in a closure and computes CSS lazily in buildStart. Astro restarts the integration on refrakt.config.json changes (it's in the watched-file set already), so no manual invalidation path is needed.
SSR boundary: composeSiteTokensCss does dynamic import of preset module paths. The integration must add those preset packages to Vite's ssr.noExternal so the loaded modules survive the SSR boundary. The existing CORE_PACKAGES + themePackage + plugins list at line 26 does not include preset packages — extend it to include @refrakt-md/lumina (already there via themePackage) and any modules listed in site.theme.presets (if theme is the object form). Mirror the SvelteKit plugin's no-external assembly.
Dependencies
- WORK-239 —
composeSiteTokensCss must be importable from @refrakt-md/transform/node
References
- SPEC-058 — adapter parity spec, "Wire site-tokens CSS through each non-SvelteKit adapter"
packages/astro/src/integration.ts — file to modifypackages/sveltekit/src/virtual-modules.ts:103–117 — reference for the virtual:refrakt/tokens module shape (the SvelteKit plugin combines theme CSS + site-tokens CSS into one virtual module; the Astro adapter doesn't need that level of indirection — separate imports work)packages/sveltekit/src/plugin.ts:93–99 — reference timing for the buildStart async compose call
Resolution
Completed: 2026-05-21
Branch: `claude/update-adapters-5CJgQ`
What was done
- Added `packages/transform/src/site-tokens-vite.ts` with `createSiteTokensVitePlugin(site, configDir)` — a structurally-typed Vite plugin factory shared between the Astro and Nuxt adapters. Avoids a hard `vite` dep on `@refrakt-md/transform` by defining `MinimalVitePlugin` locally; adapter packages cast to their own Vite types.
- Wired the plugin into `packages/astro/src/integration.ts`: added to `vite.plugins`, updated the `injectScript('page-ssr', ...)` call to import `virtual:refrakt/site-tokens.css` after the theme package's barrel CSS.
- Captures `configDir` (dir of `refrakt.config.json`) so nested-shape relative paths absolutize the same way the SvelteKit plugin handles them.
- 6 new tests in `packages/transform/test/site-tokens-vite.test.ts` cover the plugin contract: name, resolveId, load behaviour for non-matching IDs, empty CSS for no-overrides config, and inline-token CSS emission.
Notes
The cross-version Vite types problem (Astro 5 bundles its own Vite types incompatible with our peer-dep version) is handled by casting the plugin to `never` at the call site. The plugin is duck-typed by Vite and runs identically at runtime; the cast is a TypeScript-only escape hatch for the type-universe mismatch.
Two test-site validation criteria (red text + Nord :root values, [data-tint] scoped CSS) are deferred to the SPEC-059 testing infrastructure — they require building a real Astro example site end-to-end, which is the substrate SPEC-059 sets up. The shared `createSiteTokensVitePlugin` is unit-tested and the wiring is straight-line, so the code-level correctness is verified.
`@refrakt-md/astro` builds clean; all 409 tests pass across the affected packages.