golden-path-check: walk the develop-a-plugin-and-see-it-live path end to end
Some checks are pending
build-gate / build (push) Waiting to run
Some checks are pending
build-gate / build (push) Waiting to run
The north star is that an admin agent can author a plugin, build it, publish it and see it live. Every step was documented and individually verified, but never walked in one pass by following the doc as written. This plugin is that walk, authored from the documented three-file minimum. It stays useful afterwards: it reads the manifest.json it was published beside and reports what the catalog actually contains -- bundle and profile counts, how many profiles describe themselves, and whether it is listed. If you can see it answering, the path worked. Deliberately dependency-free: no map engine, no token, no data API, so it can mount when everything else is broken, which is when someone reaches for it. The walk found a real defect in my own plugin, which is the point of doing it. The first version wrapped the element in ProfileLoader (copied from a MAP bundle) without createPinia. ProfileLoader builds three stores in setup(), so setup threw -- and a thrown setup removes the subtree silently. Build green, manifest listed it, nothing complained. Only smoke:embeds caught it: "shadow DOM nearly empty (1 nodes)". That is exactly what Step 3b exists for and why the doc puts it before publish. The fix is the cap-1 pattern doxa-vision-card already uses: mount the root SFC directly. ProfileLoader earns its cost for a map plugin; it is the wrong wrapper for an element that draws no tiles and takes no token. Verified: 17 -> 18 bundles, 26 -> 27 profiles, auto-discovered with no config edit; renders 19 shadow nodes reporting live catalog state; embed suite 41/41.
This commit is contained in:
parent
e2c5c2ec49
commit
e7eeb4d02a
3 changed files with 257 additions and 0 deletions
169
plugin-designs/golden-path-check/golden-path-check.vue
Normal file
169
plugin-designs/golden-path-check/golden-path-check.vue
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
<script setup>
|
||||
/**
|
||||
* golden-path-check.vue — the capability-1 single-element SELF-TEST of the golden path.
|
||||
*
|
||||
* @element golden-path-check
|
||||
* @description A live self-test of the develop-a-plugin-and-see-it-live path: it reads the built
|
||||
* manifest.json it was published beside and reports what the catalog actually contains — bundle
|
||||
* and profile counts, how many profiles describe themselves, and whether this very plugin is
|
||||
* listed. If you can see it answering, the whole path worked.
|
||||
*
|
||||
* WHY THIS EXISTS. The north star is that an admin agent can author a plugin, build, publish and
|
||||
* SEE IT LIVE. That path was documented and its parts were individually verified, but never walked
|
||||
* end-to-end in one pass by following the doc as written. This plugin IS that walk: authoring it
|
||||
* from the documented 3-file minimum, building it, and finding it in the live catalog exercises
|
||||
* every step. It then stays useful, because it reports the catalog's real state to anyone who
|
||||
* opens it — the first thing an inheritor can click to confirm their own build reached the world.
|
||||
*
|
||||
* DELIBERATELY DEPENDENCY-FREE. No map engine, no Mapbox token, no data API, no shared library
|
||||
* component. A cap-1 element is the right tier for a self-test: it must be able to mount when
|
||||
* everything else is broken, which is exactly when someone reaches for it. Material light only
|
||||
* (#f3f3f1 / #fff / #3b463d / #92b195 / #d1d4cd / #6d766e), per the standing palette rule.
|
||||
*
|
||||
* TREE-RELATIVE BY CONSTRUCTION. It resolves manifest.json from its own document URL rather than
|
||||
* any baked host, so it answers correctly wherever the tree is dropped — CDN, a foreign subpath,
|
||||
* or file://. That is the same position-independence check:drop enforces for every other bundle.
|
||||
*/
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
|
||||
/**
|
||||
* `tree-base` is a plain ATTRIBUTE, not an injection. This element mounts its own SFC (no
|
||||
* ProfileLoader — see index.js), so there is no provide('profileConfig') above it. Taking a prop
|
||||
* instead keeps the element self-sufficient: it works when embedded directly by a host page, and
|
||||
* it cannot break because something upstream failed to provide a key.
|
||||
*/
|
||||
const props = defineProps({
|
||||
treeBase: { type: String, default: '' },
|
||||
})
|
||||
|
||||
const state = ref('loading') // loading | ok | error
|
||||
const error = ref('')
|
||||
const manifest = ref(null)
|
||||
|
||||
/**
|
||||
* Where is the tree root? The leaf page for any profile is served at
|
||||
* <tree>/<bundle>/<profile>/index.html
|
||||
* so the manifest sits three levels up. A host embedding the element directly can override with
|
||||
* the `tree-base` ATTRIBUTE (<golden-path-check tree-base="https://host/doxa-maps">). Falls back
|
||||
* to deriving it from location.pathname so a plain drop still resolves.
|
||||
*/
|
||||
const treeBase = computed(() => {
|
||||
if (props.treeBase) return props.treeBase.replace(/\/+$/, '')
|
||||
if (typeof location === 'undefined') return '.'
|
||||
const parts = location.pathname.split('/').filter(Boolean)
|
||||
// drop index.html + <profile> + <bundle> when they are present
|
||||
const trimmed = parts.slice(0, Math.max(0, parts.length - 3))
|
||||
return `${location.origin}/${trimmed.join('/')}`.replace(/\/+$/, '')
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const res = await fetch(`${treeBase.value}/manifest.json`, { cache: 'no-store' })
|
||||
if (!res.ok) throw new Error(`manifest.json → HTTP ${res.status}`)
|
||||
manifest.value = await res.json()
|
||||
state.value = 'ok'
|
||||
} catch (e) {
|
||||
error.value = String(e && e.message ? e.message : e)
|
||||
state.value = 'error'
|
||||
}
|
||||
})
|
||||
|
||||
const bundles = computed(() => (manifest.value?.bundles || []))
|
||||
const profiles = computed(() => bundles.value.flatMap(b => b.profiles || []))
|
||||
const described = computed(() =>
|
||||
profiles.value.filter(p => String(p.description || '').trim().length > 0).length)
|
||||
const selfListed = computed(() =>
|
||||
bundles.value.some(b => (b.name || b.bundle) === 'golden-path-check'))
|
||||
|
||||
const rows = computed(() => [
|
||||
{ label: 'bundles in the catalog', value: String(bundles.value.length) },
|
||||
{ label: 'profiles in the catalog', value: String(profiles.value.length) },
|
||||
{ label: 'profiles that describe themselves',
|
||||
value: `${described.value} / ${profiles.value.length}` },
|
||||
{ label: 'this plugin is listed', value: selfListed.value ? 'yes' : 'no' },
|
||||
])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="gpc">
|
||||
<h2 class="gpc-h">Golden path — live self-test</h2>
|
||||
|
||||
<p v-if="state === 'loading'" class="gpc-note">Reading the catalog manifest…</p>
|
||||
|
||||
<p v-else-if="state === 'error'" class="gpc-note gpc-err">
|
||||
Could not read <code>manifest.json</code> beside this page.<br>
|
||||
<small>{{ error }}</small>
|
||||
</p>
|
||||
|
||||
<template v-else>
|
||||
<dl class="gpc-rows">
|
||||
<div v-for="r in rows" :key="r.label" class="gpc-row">
|
||||
<dt>{{ r.label }}</dt>
|
||||
<dd>{{ r.value }}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
<p class="gpc-note">
|
||||
This element was authored in <code>plugin-designs/</code>, built with
|
||||
<code>bun run build</code>, and published to the catalog. It is reading the manifest it
|
||||
shipped beside — so if these numbers are here, the whole path worked.
|
||||
</p>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
/* Material LIGHT only — no dark mode, no prefers-color-scheme:dark (standing palette rule). */
|
||||
:host { display: block; }
|
||||
|
||||
.gpc {
|
||||
background: #f3f3f1;
|
||||
color: #3b463d;
|
||||
border: 1px solid #d1d4cd;
|
||||
border-radius: 10px;
|
||||
padding: 18px 20px;
|
||||
font: 14px/1.5 -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
|
||||
}
|
||||
|
||||
.gpc-h {
|
||||
margin: 0 0 14px;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
.gpc-rows { margin: 0; }
|
||||
|
||||
.gpc-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
padding: 9px 0;
|
||||
border-bottom: 1px solid #d1d4cd;
|
||||
}
|
||||
|
||||
.gpc-row:last-child { border-bottom: 0; }
|
||||
|
||||
.gpc-row dt { color: #6d766e; }
|
||||
|
||||
.gpc-row dd {
|
||||
margin: 0;
|
||||
font-weight: 700;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.gpc-note {
|
||||
margin: 14px 0 0;
|
||||
color: #6d766e;
|
||||
font-size: 12.5px;
|
||||
}
|
||||
|
||||
.gpc-err { color: #3b463d; }
|
||||
|
||||
.gpc code {
|
||||
background: #fff;
|
||||
border: 1px solid #d1d4cd;
|
||||
border-radius: 4px;
|
||||
padding: 1px 5px;
|
||||
font: 12px/1 ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||
}
|
||||
</style>
|
||||
47
plugin-designs/golden-path-check/index.html
Normal file
47
plugin-designs/golden-path-check/index.html
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>doxa-maps · golden-path-check staging</title>
|
||||
<link rel="icon" href="data:,">
|
||||
|
||||
<!-- NO map peer script here, on purpose. This is a capability-1 element that draws no tiles,
|
||||
so it must mount with no Mapbox engine and no token — which is the point of a self-test:
|
||||
it has to work when everything else is broken. -->
|
||||
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
/* Material LIGHT only (standing palette rule): #f3f3f1 surface, #fff, #3b463d text,
|
||||
#92b195 accent, #d1d4cd border, #6d766e muted. No dark mode. */
|
||||
html, body { background: #f3f3f1; color: #3b463d; min-height: 100%;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif; }
|
||||
.wrap { max-width: 720px; margin: 0 auto; padding: 32px 20px; }
|
||||
h1 { font-size: 22px; font-weight: 700; letter-spacing: -0.02em; }
|
||||
.sub { color: #6d766e; font-size: 13px; margin: 6px 0 24px; }
|
||||
.slot { background: #fff; border: 1px solid #d1d4cd; border-radius: 12px; overflow: hidden; }
|
||||
.slot .label { font: 12px/1 ui-monospace, SFMono-Regular, Menlo, monospace; color: #6d766e;
|
||||
padding: 8px 12px; background: #f3f3f1; border-bottom: 1px solid #d1d4cd; }
|
||||
.frame { padding: 16px; }
|
||||
golden-path-check { display: block; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<h1>golden-path-check · staging</h1>
|
||||
<p class="sub">
|
||||
The capability-1 self-test of the develop-a-plugin-and-see-it-live path. It reads the
|
||||
<code>manifest.json</code> it was published beside and reports what the catalog contains.
|
||||
</p>
|
||||
|
||||
<div class="slot">
|
||||
<div class="label"><golden-path-check></div>
|
||||
<div class="frame">
|
||||
<golden-path-check></golden-path-check>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="module" src="./index.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
41
plugin-designs/golden-path-check/index.js
Normal file
41
plugin-designs/golden-path-check/index.js
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
/**
|
||||
* plugin-designs/golden-path-check/index.js
|
||||
*
|
||||
* Bundle entry for the GOLDEN PATH CHECK — the capability-1 self-test of the
|
||||
* develop-a-plugin-and-see-it-live path.
|
||||
*
|
||||
* Usage from a host page:
|
||||
* <script src="/doxa-maps/golden-path-check/golden-path-check.js"></script>
|
||||
* <golden-path-check></golden-path-check>
|
||||
*
|
||||
* Registers ONLY its own tag (one-shot-define safety: never re-register another bundle's tag,
|
||||
* never import another plugin's components).
|
||||
*
|
||||
* ★ MOUNTS THE ROOT SFC DIRECTLY — no ProfileLoader. This is the cap-1 pattern, and it is the
|
||||
* SAME choice doxa-vision-card makes. It is also a defect this plugin HIT and fixed, which is
|
||||
* worth recording because it is the first thing a new plugin author will get wrong:
|
||||
*
|
||||
* The first version wrapped this element in ProfileLoader (copying all-people-groups, a MAP
|
||||
* bundle) but omitted `app.use(createPinia())`. ProfileLoader creates uiStore/mapStore/
|
||||
* dataStore in its setup(), so without Pinia its setup THREW — and a thrown setup() removes
|
||||
* the subtree SILENTLY. The build stayed green, the manifest listed the bundle, and only
|
||||
* `bun run smoke:embeds` caught it: "shadow DOM nearly empty (1 nodes)". That is exactly the
|
||||
* failure Step 3b of the golden path exists to catch, and why the doc puts it BEFORE publish.
|
||||
*
|
||||
* The fix is not "add Pinia". ProfileLoader exists to parse profile-config, resolve a profile
|
||||
* .vue, and wire a token / dataSource / colorSet / stores — a MAP plugin's needs. This element
|
||||
* draws no tiles, fetches no API and takes no token, so routing it through the loader would
|
||||
* add three stores and a config parser to a component that wants none of them. A cap-1 element
|
||||
* mounts its own SFC; reach for ProfileLoader when you actually need what it provides.
|
||||
*/
|
||||
|
||||
import { defineCustomElement } from 'vue'
|
||||
import GoldenPathCheck from './golden-path-check.vue'
|
||||
|
||||
const GoldenPathCheckElement = defineCustomElement(GoldenPathCheck)
|
||||
|
||||
if (typeof customElements !== 'undefined' && !customElements.get('golden-path-check')) {
|
||||
customElements.define('golden-path-check', GoldenPathCheckElement)
|
||||
}
|
||||
|
||||
export default GoldenPathCheckElement
|
||||
Loading…
Reference in a new issue