WORK-240
Setting up your dashboard 0 entities found · 9/32 branches scanned
ID:WORK-240Status:done

Wire site-tokens CSS through the Astro adapter

Inject the CSS produced by composeSiteTokensCss(site, configDir) into pages rendered by @refrakt-md/astro, so that theme.tokens, theme.modes, theme.presets, and site.tints configured in refrakt.config.json actually take effect on Astro sites. Currently the Astro integration imports the theme package's bare CSS via injectScript('page-ssr', import '${themePackage}';) and never reads the site-level overrides — silently dropping every preset / token / mode / tint declared in config.

Astro runs on Vite, so the SvelteKit pattern of a Vite virtual module transplants cleanly.

Priority:highComplexity:moderateMilestone:v0.14.4Source:SPEC-058
claude/update-adapters-5CJgQ View source

Criteria completion

Criteria completion: 5 of 7 (71%) checked; history from May 21 to May 210%25%50%75%100%May 21May 21
Branches 1
claude/update-adapters-5CJgQ current done
main done
History 2
  1. ba2b92e
    • ☑ `@refrakt-md/astro` defines a Vite virtual module `virtual:refrakt/site-tokens.css` whose `load` hook returns the output of `composeSiteTokensCss(activeSite, configDir)` from {% ref "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
    • ☑ 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 {% ref "SPEC-048" /%} and {% ref "SPEC-056" /%}
    by bjornolofandersson
  2. 804df99
    Created (ready)by bjornolofandersson

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:

  1. Resolves virtual:refrakt/site-tokens.css\0virtual:refrakt/site-tokens.css in resolveId
  2. Returns the cached CSS string in load
  3. 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-239composeSiteTokensCss 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 modify
  • packages/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.