Dev server: serve assets from data/ and under the tree base, so a fresh clone's plugins get their data
Some checks are pending
build-gate / build (push) Waiting to run

Opening every plugin in the studio under the dev server on a fresh
worktree, 13 of them mounted with empty data: every table, boundary and
people-groups file 404'd. Two causes.

- The dev asset lookup read public/assets (a mirror generate-tree
  writes, absent on a fresh clone) and plugin-designs/*/data, but never
  data/, the tracked source that mirror is copied from. It now falls
  back to data/<rel>, which is what DX gets when they clone.
- Plugins nested in the studio resolve assets against the tree base the
  studio hands them (/__plugin-gallery/), as they do against <tree>/assets
  in the built tree, but only /assets/ was served. Both prefixes are
  served now.

After: the same sweep shows the data 404s gone from all 13.
This commit is contained in:
doxa-maps-88655c70 2026-09-25 03:27:21 +00:00
parent 8415e8e112
commit bf7e0e6464

View file

@ -6,7 +6,7 @@
* galleryModel(discoverSpaRegistry())
* GET /__plugin-gallery/manifest.json that model, manifest-shaped (the page fetches it live)
* GET /__plugin-gallery/registry.json the discovery registry, machine-readable (smoke tools)
* GET /assets/<rel> shared asset root (see resolveDevAsset)
* GET /assets/<rel> (and /__plugin-gallery/assets/<rel>) shared asset root (see resolveDevAsset)
* Wire-up: plugins: [vue(), cssInjectedByJsPlugin(), autoSpaDevPlugin(token)] in vite.config.js.
* Verify: bun run smoke:gallery (internal/plugin-gallery-smoke.mjs — every element must boot)
*/
@ -26,7 +26,7 @@ import { readBuildConfig, renderGallery } from '../generate-gallery.mjs'
// resources-map, cities-map, poster walls, tables) fetch `assetsBase + '/tables/*.json'`
// and `'/boundaries/*.geojson'`. publicDir is false ON PURPOSE (a bundle build must never
// inline public/), so dev has to serve the asset tree explicitly, the way the built site
// does: public/assets first (exactly what generate-tree ships), else the per-bundle source
// does: public/assets first (exactly what generate-tree ships), else the tracked data/ source, else the per-bundle source
// of truth plugin-designs/<bundle>/data/<rel> when public/ has been cleaned.
const DEV_ASSET_MIME = {
'.json': 'application/json; charset=utf-8', '.geojson': 'application/geo+json; charset=utf-8',
@ -39,6 +39,12 @@ export function resolveDevAsset(rel, root = REPO_ROOT) {
const isFile = (p) => existsSync(p) && statSync(p).isFile()
const shipped = resolve(root, 'public', 'assets', rel)
if (isFile(shipped)) return shipped
// data/<rel> is the TRACKED source of truth public/assets mirrors (see .gitignore): generate-tree
// writes the mirror, so on a fresh clone — which is what DX receives — public/assets holds almost
// nothing and every table, boundary and people-groups file 404'd under the dev server until the
// first full build. Measured 2026-09-25 across the studio: 13 plugins mounted with empty data.
const source = resolve(root, 'data', rel)
if (isFile(source)) return source
const profiles = resolve(root, 'plugin-designs')
for (const name of readdirSync(profiles)) {
if (name.startsWith('_') || name.startsWith('.')) continue
@ -232,12 +238,17 @@ export function autoSpaDevPlugin(token = '') {
// /assets/* — see resolveDevAsset. A miss is a REAL 404, never Vite's HTML
// fallback: HTML-for-JSON made three maps die silently at JSON.parse before
// they ever created a map, with no console error and no error card.
if (path.startsWith('/assets/')) {
// <tree>/assets/* too: a plugin nested in a cap-5 shell resolves its assets against the tree base
// the shell hands it (plugin-studio passes /__plugin-gallery/), exactly as it would against
// <tree>/assets in the built tree. Only /assets/ was served, so every nested data table 404'd.
const assetPrefix = path.startsWith('/assets/') ? '/assets/'
: path.startsWith('/__plugin-gallery/assets/') ? '/__plugin-gallery/assets/' : ''
if (assetPrefix) {
let rel = ''
try { rel = decodeURIComponent(path.slice('/assets/'.length)) } catch { rel = '' }
try { rel = decodeURIComponent(path.slice(assetPrefix.length)) } catch { rel = '' }
if (!rel || rel.split('/').includes('..')) { res.statusCode = 400; return res.end('bad asset path') }
const file = resolveDevAsset(rel)
if (!file) return json(res, { error: 'asset not found', asset: rel, looked: ['public/assets/', 'plugin-designs/*/data/'] }, 404)
if (!file) return json(res, { error: 'asset not found', asset: rel, looked: ['public/assets/', 'data/', 'plugin-designs/*/data/'] }, 404)
res.statusCode = 200
res.setHeader('Content-Type', DEV_ASSET_MIME[extname(file).toLowerCase()] || 'application/octet-stream')
res.setHeader('Cache-Control', 'no-cache')