The dev server's manifest carries stage, heavy and widget, so Rudder's corner mounts under bun run dev
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions

rega (7a7ca67b): on the dev server the corner widget never mounted, because /__plugin-gallery/manifest.json
built its profiles without the .meta.json fields only generate-tree read. Both now read them through one
module, internal/profile-meta.mjs (stage, heavy, widget per profile; bundle stage = production if any
profile says so), so the dev Studio and the built Studio cannot disagree. Verified: the dev model lists the
same 7 production bundles and rudder-voice:bottom-right; the built manifest is unchanged.
This commit is contained in:
Doxa Maps 2026-09-27 09:29:42 +00:00
parent 17763cdccc
commit e53495dfcf
3 changed files with 41 additions and 25 deletions

View file

@ -44,6 +44,7 @@ import { createHash } from 'node:crypto'
// without a separate tierMap. Previously the manifest carried no `type`, so a manifest-fed
// consumer (plugin-studio) mis-tiered every profile as cap-1 (single). Single source of truth.
import { detectProfileType } from './plugin-gallery/discover.mjs'
import { profileMetaFields, bundleStage } from './profile-meta.mjs'
import { TREE_NAME as CONFIGURED_TREE_NAME } from './_tree-name.mjs'
import { syncAppShells } from './sync-app-shells.mjs'
@ -301,25 +302,6 @@ function profileReachesAMap(dir, file) {
}
return false
}
/**
* STAGE and HEAVY, read from <profile>.meta.json and carried into manifest.json (the Driver, 2026-09-27:
* a public-but-unfinished plugin shows under EXPERIMENTAL in the Plugin Studio and the gallery until it works
* as a production map). Both surfaces read the MANIFEST, so they cannot disagree, and neither keeps a list.
* "stage": "production" | "experimental" default experimental; a bundle is production if ANY of its
* profiles says so (a bundle is one folder in the studio)
* "heavy": true the element holds live WebGL contexts, so a host must destroy
* it when it leaves the screen (Plugin Studio's HEAVY rows)
*/
function metaStageHeavy(dir, profileName) {
try {
const meta = JSON.parse(readFileSync(join(dir, `${profileName}.meta.json`), 'utf8'))
const corner = meta?.widget?.corner
return { stage: meta?.stage === 'production' ? 'production' : 'experimental', heavy: meta?.heavy === true,
// "widget": { "corner": "bottom-right" } — a corner widget the Plugin Studio mounts once, fixed, not in a pane
widget: ['bottom-right', 'bottom-left', 'top-right', 'top-left'].includes(corner) ? { corner } : null }
} catch { return { stage: 'experimental', heavy: false, widget: null } }
}
function metaIsMap(dir, profileName) {
try {
const meta = JSON.parse(readFileSync(join(dir, `${profileName}.meta.json`), 'utf8'))
@ -1305,12 +1287,9 @@ function scanBundles() {
? composesAMapLeaf(dir, profileName, file)
: (metaIsMap(dir, profileName) ?? profileReachesAMap(dir, file))
const { stage: profileStage, heavy: profileHeavy, widget: profileWidget } = metaStageHeavy(dir, profileName)
return {
profile: profileName,
stage: profileStage,
...(profileHeavy ? { heavy: true } : {}),
...(profileWidget ? { widget: profileWidget } : {}),
...profileMetaFields(dir, profileName), // stage / heavy / widget, shared with the dev manifest
file,
description: description || fileTag || '',
element: tag,
@ -1364,7 +1343,7 @@ function scanBundles() {
integrity,
elements: bundleElements,
...(composesUnique.length ? { composes: composesUnique } : {}),
stage: profiles.some((p) => p.stage === 'production') ? 'production' : 'experimental',
stage: bundleStage(profiles),
profiles,
})
}

View file

@ -16,6 +16,7 @@ import { readdirSync, statSync, existsSync, readFileSync } from 'node:fs'
import { REPO_ROOT, discoverSpaRegistry } from './discover.mjs'
import { readBuildConfig, renderGallery } from '../generate-gallery.mjs'
import { TREE_NAME } from '../_tree-name.mjs'
import { profileMetaFields, bundleStage } from '../profile-meta.mjs'
// ─── DEV plugin gallery ──────────────────────────────────────────────────────
// `bun x vite` (dev) serves the plugin gallery at `/` — the same page `bun run build`
@ -115,6 +116,7 @@ export function galleryModel(reg) {
const p = {
profile, file: `${profile}.vue`, description: '', element: b.tag,
type: typeOf.get(profile) || 'single',
...profileMetaFields(resolve(REPO_ROOT, 'plugin-designs', b.bundle), profile), // same as the built manifest
iframeUrl: link(b.bundle, profile), scriptUrl,
embed: {
iframe: iframe(`./${b.bundle}/${profile}/index.html`),
@ -135,7 +137,7 @@ export function galleryModel(reg) {
// to drop them, so under the dev server — which is what the Driver's laptop runs — the studio's
// Composed-of panel and composition trees were silently empty for every composite.
const composes = [...new Set([...presets.keys()].flatMap((profile) => readComposes(b.bundle, profile)))]
return { bundle: b.bundle, scriptUrl, integrity: '', elements: [b.tag], ...(composes.length ? { composes } : {}), profiles }
return { bundle: b.bundle, scriptUrl, integrity: '', elements: [b.tag], stage: bundleStage(profiles), ...(composes.length ? { composes } : {}), profiles }
})
return {
schema: 'doxa-maps-manifest@1', generatedFrom: 'plugin-designs/*/*.vue (dev discovery)', embedBase: '.',

35
internal/profile-meta.mjs Normal file
View file

@ -0,0 +1,35 @@
/**
* profile-meta.mjs — the per-profile fields a host reads from <profile>.meta.json, in ONE place.
*
* generate-tree (the built manifest.json) and the dev server's /__plugin-gallery/manifest.json both call
* this, so the dev Studio and the built Studio cannot disagree (rega: the corner widget never mounted in
* dev because only the built manifest carried "widget").
* stage "production" | "experimental" default experimental
* heavy true when the element holds live WebGL contexts
* widget { corner } for a corner widget the Plugin Studio mounts once, fixed
*/
import { readFileSync } from 'node:fs'
import { join } from 'node:path'
const CORNERS = ['bottom-right', 'bottom-left', 'top-right', 'top-left']
export function readProfileMeta(dir, profileName) {
try {
const meta = JSON.parse(readFileSync(join(dir, `${profileName}.meta.json`), 'utf8'))
const corner = meta?.widget?.corner
return {
stage: meta?.stage === 'production' ? 'production' : 'experimental',
heavy: meta?.heavy === true,
widget: CORNERS.includes(corner) ? { corner } : null,
}
} catch { return { stage: 'experimental', heavy: false, widget: null } }
}
/** The optional manifest keys for one profile (stage always; heavy / widget only when set). */
export function profileMetaFields(dir, profileName) {
const m = readProfileMeta(dir, profileName)
return { stage: m.stage, ...(m.heavy ? { heavy: true } : {}), ...(m.widget ? { widget: m.widget } : {}) }
}
/** A bundle is production if ANY of its profiles says so. */
export const bundleStage = (profiles) => (profiles.some((p) => p.stage === 'production') ? 'production' : 'experimental')