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.