tailwind2-10 Exercise 2: The Same Kit in Astro, and Proving Both Builds Match
================================================================================
Setup (folder "astro", next to "kit"): npm install astro @astrojs/react react react-dom tailwindcss @tailwindcss/vite @types/react @types/react-dom ; add .gitignore (node_modules, dist). The page imports the kit's App from the sibling folder.
--- astro.config.mjs ---
import { defineConfig } from 'astro/config';
import react from '@astrojs/react';
import tailwindcss from '@tailwindcss/vite';
export default defineConfig({
integrations: [react()],
vite: { plugins: [tailwindcss()] },
});
--- src/pages/index.astro ---
---
import { App } from '../../../kit/src/docs/App';
---
Kit docs (Astro)
First build (kit/src/ui/tokens.css had NO @source line):
npx astro build exit 0, no warning
CSS 6,396 B; matches for bg-primary 0, outline-ring 0, state=active 0, rounded-control 0, starting 0 <- the page is unstyled
Fix: the first lines of the SHARED stylesheet kit/src/ui/tokens.css now read
@import 'tailwindcss';
/* The kit's own files, wherever the app that imports this stylesheet lives */
@source '../';
(the path is relative to the stylesheet: kit/src/)
Second build: exit 0. CSS 15,092 B; bg-primary 3, outline-ring 1, state=active 2, rounded-control 1, starting 2.
The kit's own build is unchanged (14.06 kB CSS).
Behaviour: I ran the same keyboard / contrast / dialog / persistence scripts against dist/ of the Astro build (cd astro; node ../tests/test-behaviour.mjs and test-contrast.mjs): every line was identical to the Vite results (tab order, ArrowRight and
End on the tabs, dialog focus in and out, aria-invalid wiring, reduced motion, persisted theme), and only the four CONTROL lines failed.
--- tests/shots.mjs (screenshots of both builds in four themes, 900 x 1100, with two controls) ---
import { withPage } from '../../c4/cdp.mjs';
import http from 'node:http'; import fs from 'node:fs'; import path from 'node:path';
import { PNG } from 'pngjs'; import pixelmatch from 'pixelmatch';
const serve = (dir, port) => http.createServer((q, r) => { const f = path.join(dir, q.url === '/' ? 'index.html' : q.url.split('?')[0]); fs.readFile(f, (e, d) => { if (e) { r.statusCode = 404; r.end(); return; } r.setHeader('content-type', f.endsWith('.js') ? 'text/javascript' : f.endsWith('.css') ? 'text/css' : 'text/html'); r.end(d); }); }).listen(port);
const kitDist = path.resolve('../kit/dist'), astroDist = path.resolve('../astro/dist');
const s1 = serve(kitDist, 5421), s2 = serve(astroDist, 5422);
fs.mkdirSync('shots', { recursive: true });
async function shot(port, brand, mode, name) {
await withPage(`http://localhost:${port}/`, async (p) => {
await p.ev(`localStorage.setItem('brand', '${brand}'); localStorage.setItem('mode', '${mode}')`);
await p.viewport(900, 1100);
await p.reload(); await p.sleep(500);
const r = await p.send('Page.captureScreenshot', { format: 'png' });
fs.writeFileSync(`shots/${name}.png`, Buffer.from(r.data, 'base64'));
});
}
const diff = (a, b) => { const A = PNG.sync.read(fs.readFileSync(`shots/${a}.png`)), B = PNG.sync.read(fs.readFileSync(`shots/${b}.png`)); if (A.width !== B.width || A.height !== B.height) return `size differs ${A.width}x${A.height} vs ${B.width}x${B.height}`; return pixelmatch(A.data, B.data, null, A.width, A.height, { threshold: 0.1 }) + ' of ' + A.width * A.height; };
for (const brand of ['ocean', 'sunset']) for (const mode of ['light', 'dark']) {
const k = `${brand}-${mode}`;
await shot(5421, brand, mode, `vite-${k}`);
await shot(5421, brand, mode, `vite-${k}-again`);
await shot(5422, brand, mode, `astro-${k}`);
console.log(k.padEnd(13), '| vite vs vite (same build, second run):', diff(`vite-${k}`, `vite-${k}-again`), '| vite vs astro:', diff(`vite-${k}`, `astro-${k}`));
}
// the four themes must differ from each other
console.log('ocean-light vs ocean-dark (vite):', diff('vite-ocean-light', 'vite-ocean-dark'), '| ocean-light vs sunset-light:', diff('vite-ocean-light', 'vite-sunset-light'));
s1.close(); s2.close(); process.exit(0);
Results:
ocean-light | vite vs vite (same build, second run): 0 of 990000 | vite vs astro: 0 of 990000
ocean-dark | vite vs vite (same build, second run): 0 of 990000 | vite vs astro: 0 of 990000
sunset-light | vite vs vite (same build, second run): 0 of 990000 | vite vs astro: 0 of 990000
sunset-dark | vite vs vite (same build, second run): 0 of 990000 | vite vs astro: 0 of 990000
ocean-light vs ocean-dark (vite): 981557 of 990000 | ocean-light vs sunset-light: 15535 of 990000
WHY THIS WORKS AS AN ANSWER
---------------------------
The first build is the silent failure from chapter 5: the scanner starts at the Astro project, the kit's files are outside it, so no kit class was found; the build exited 0 and the page had no styling. Putting the @source line in the shared
stylesheet fixes it for every app that imports the kit, because the path is relative to that file. The screenshot comparison has two controls: the same build twice gives 0 differing pixels (so the comparison is stable) and different
themes give large differences (so it can see change); against those, 0 pixels between Vite and Astro means the two wirings render the same page. This is Chrome on one machine; other browsers and machines will differ in text rendering.
If you use Next.js instead, the same @source line applies (chapter 5 covers its PostCSS setup); I did not run that variant.