diff --git a/.agents/skills/build-pipeline/SKILL.md b/.agents/skills/build-pipeline/SKILL.md index 08f7509fa..85ba2fb56 100644 --- a/.agents/skills/build-pipeline/SKILL.md +++ b/.agents/skills/build-pipeline/SKILL.md @@ -19,7 +19,7 @@ Astro only renders pages. Every stylesheet, script, font and image on a page arr | Directory | Written by | Notes | | --- | --- | --- | | `core/dist/` | `@tabler/core` (`css:dev`, `js:build`, `copy`) | the framework: css, js, fonts, img, vendored libs | -| `preview/tmp-assets/` | `preview` `assets` (`build-css.ts` + vite/terser) | demo css/js, isolated from Astro's output | +| `preview/tmp-assets/` | `preview` `assets` / `watch:css` (`build-css.ts` + vite/terser) | demo css/js, isolated from Astro's output | | `docs/tmp-assets/css/` | `docs` `css` / `watch:css` (build-css, `--no-prefix`) | docs stylesheets | | `/public/` | **`copy-assets` only** | fully generated, wiped and rebuilt on every Astro start | | `/dist/` | `astro build` | the shipped site | @@ -32,10 +32,10 @@ An Astro integration, configured per package in `astro.config.mjs`: - On `astro:config:done` it **deletes `public/`** and rebuilds it from the `copies` manifest (core dist, the package's `tmp-assets`, `shared/static`, favicons). Running it twice must never accumulate content — that is why it starts from a clean directory. - It is skipped when the Astro command is `sync`, so `astro check` (and CI's type-check job) does not need built workspace assets. -- In dev it watches `syncDirs` and copies changed files into `public/`, then sends one coalesced `full-reload` (250 ms window, `.map` files ride along with their source file). `reloadDirs` are directories a watcher already writes into directly — those only get the reload. +- In dev it watches `syncDirs` and copies changed files into `public/`, then sends one coalesced `full-reload` (250 ms window, `.map` files ride along with their source file). - `allowDestinationFallback` keeps the existing copy when the source vanishes mid-copy — `@tabler/core` cleaning `dist/` while `turbo dev` starts the dependents. -**Rule for watchers:** because `public/` is wiped at startup, a watcher writing straight into it (preview's `watch:css` / `watch:js`) only works when `dev:prepare` has produced the same output into `tmp-assets` first, so the startup copy seeds it. The safer arrangement is docs': the watcher writes into `tmp-assets/css` and `copy-assets` syncs it to `public/css`. Prefer that for anything new. +**Rule for watchers:** `copy-assets` is the only writer of `public/`. It wipes the directory at startup, and the wipe can race a watcher that writes there directly — that was the `ENOENT` in #3006. Every watcher (preview's and docs' `watch:css`) writes into `tmp-assets/css`, and `copy-assets` syncs it into `public/` via `syncDirs`. Do the same for anything new. ## 3. Ordering @@ -56,7 +56,7 @@ Work down this list; each step is cheap: 1. **404 on `/dist/css/tabler.css` or `/preview/css/demo.css`** — the source was never built. `pnpm --dir core run dev:prepare`, then `pnpm --dir preview run dev:prepare`. 2. **Assets vanished after restarting dev** — something wrote into `public/` that `copy-assets` does not know about; it was wiped at startup. Add it to the manifest, or write it into `tmp-assets` instead. -3. **CSS/JS edits do not appear** — the watcher is not running (started `astro dev` alone instead of the package's `dev` script), or its output dir is not in `syncDirs`/`reloadDirs`. +3. **CSS/JS edits do not appear** — the watcher is not running (started `astro dev` alone instead of the package's `dev` script), or its output dir is not in `syncDirs`. 4. **Stale content that survives a rebuild** — `pnpm --dir run clean`, then `dev:prepare`. 5. **`dist/` grows between identical builds** — an output dir is nested inside another copy step (see the first trap). 6. **CI type-check fails on missing assets** — something made `copy-assets` run for the `sync` command; keep the early return. diff --git a/.build/copy-assets.ts b/.build/copy-assets.ts index da09dcc99..0b2f8ecb9 100644 --- a/.build/copy-assets.ts +++ b/.build/copy-assets.ts @@ -58,11 +58,9 @@ interface CopyAssetsOptions { copies: CopyEntry[] /** generated source dirs to live-sync into publicDir while `astro dev` runs */ syncDirs?: { from: string; to: string }[] - /** dirs written to directly by watchers — already in place, only trigger a browser reload */ - reloadDirs?: string[] } -export function copyAssets({ repo, publicDir, copies, syncDirs = [], reloadDirs = [] }: CopyAssetsOptions): Integration { +export function copyAssets({ repo, publicDir, copies, syncDirs = [] }: CopyAssetsOptions): Integration { let command: string function rebuildPublicDir(logger: Logger) { @@ -124,7 +122,7 @@ export function copyAssets({ repo, publicDir, copies, syncDirs = [], reloadDirs // dirs during `pnpm run dev` (e.g. core/dist by core's watchers) would // never reach the served public/ copy. Watch them, sync changed files // into public/, and trigger a browser reload. - server.watcher.add([...syncDirs.map((dir) => dir.from), ...reloadDirs]) + server.watcher.add(syncDirs.map((dir) => dir.from)) let reloadTimer: ReturnType | undefined const scheduleReload = (file: string) => { // Source maps piggyback on their css/js file's reload. build-css.ts @@ -146,7 +144,6 @@ export function copyAssets({ repo, publicDir, copies, syncDirs = [], reloadDirs scheduleReload(file) return } - if (reloadDirs.some((dir) => file.startsWith(dir + sep))) scheduleReload(file) } server.watcher.on('add', sync) server.watcher.on('change', sync) diff --git a/.changeset/preview-watch-css-tmp-assets.md b/.changeset/preview-watch-css-tmp-assets.md new file mode 100644 index 000000000..8eb809650 --- /dev/null +++ b/.changeset/preview-watch-css-tmp-assets.md @@ -0,0 +1,5 @@ +--- +"@tabler/preview": patch +--- + +Fixed a dev-server `ENOENT` race: preview's `watch:css` now writes to `tmp-assets/css` and `copy-assets` syncs it into `public/`. diff --git a/preview/astro.config.mjs b/preview/astro.config.mjs index 428d01c26..fae13d7af 100644 --- a/preview/astro.config.mjs +++ b/preview/astro.config.mjs @@ -136,7 +136,11 @@ export default defineConfig({ }, { // demo css built by this package's sass pipeline, from tmp-assets/ - // (not dist/, which is Astro's own build output — see copy-assets.ts). + // (the `css` and `watch:css` scripts both write there). Not dist/, + // which is Astro's own build output — see copy-assets.ts — and not + // public/, which copy-assets wipes on every restart: anything a + // watcher writes straight into public/ is lost, or worse, racing + // with the wipe (#3006). from: path('./tmp-assets'), to: path('./public/preview'), label: '@tabler/preview', @@ -156,11 +160,9 @@ export default defineConfig({ // reads from — same directory. syncDirs: [ { from: path('../core/dist'), to: path('./public/dist') }, + { from: path('./tmp-assets'), to: path('./public/preview') }, { from: path('../shared/static'), to: path('./public/static') }, ], - // watch:css writes straight into public/preview — the file is already - // in place, but Astro does not reload the browser on public/ changes. - reloadDirs: [path('./public/preview')], }), mdx(), dropPlaygrounds(), diff --git a/preview/package.json b/preview/package.json index a37f00a45..18fe6fdb1 100644 --- a/preview/package.json +++ b/preview/package.json @@ -9,7 +9,7 @@ "scripts": { "assets": "shx rm -rf tmp-assets && pnpm run css", "css": "tsx ../.build/build-css.ts scss tmp-assets/css --minify", - "watch:css": "nodemon --watch scss/ --ext scss --exec \"tsx ../.build/build-css.ts scss public/preview/css\"", + "watch:css": "nodemon --watch scss/ --ext scss --exec \"tsx ../.build/build-css.ts scss tmp-assets/css\"", "watch": "pnpm run watch:css", "dev": "concurrently -n astro,css -c blue,magenta \"astro dev\" \"pnpm run watch:css\"", "dev:prepare": "pnpm run assets",