Subdomain apps get /embed-test: every web plugin in the host's tree, embedded as JavaScript
This commit is contained in:
parent
0856980d58
commit
51cd0e3628
2 changed files with 141 additions and 0 deletions
|
|
@ -90,6 +90,13 @@ if (Object.keys(SUBDOMAINS).length) {
|
|||
const html = readFileSync(leaf, 'utf8').replace(/<head([^>]*)>/i, `<head$1><base href="/dx-maps/${target}/">`)
|
||||
return new Response(html, { headers: { ...headers, 'content-type': 'text/html; charset=utf-8', 'cache-control': 'no-cache' } })
|
||||
}
|
||||
// /embed-test: every web plugin in this host's tree, embedded as JavaScript (deployment/cloud/embed-test.html,
|
||||
// read per request from the pulled checkout, so the page follows the branch without a restart).
|
||||
if (url.pathname === '/embed-test' || url.pathname === '/embed-test/') {
|
||||
const page = '/app/repo/deployment/cloud/embed-test.html'
|
||||
if (!existsSync(page)) return text('not found', 404)
|
||||
return new Response(readFileSync(page, 'utf8'), { headers: { ...headers, 'content-type': 'text/html; charset=utf-8', 'cache-control': 'no-cache' } })
|
||||
}
|
||||
const f = normalize(join(CDN, decodeURIComponent(url.pathname)))
|
||||
if (!f.startsWith(CDN + '/dx-maps/')) return text('not found', 404)
|
||||
const file = existsSync(f) && statSync(f).isDirectory() ? join(f, 'index.html') : f
|
||||
|
|
|
|||
134
deployment/cloud/embed-test.html
Normal file
134
deployment/cloud/embed-test.html
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
<!doctype html>
|
||||
<!--
|
||||
/embed-test on a subdomain app (for example research.<host>/embed-test): proves that EVERY dx-maps plugin built
|
||||
for the web embeds as JavaScript (no iframe), straight from this host's published tree (/dx-maps/). admin.mjs
|
||||
serves this file on the subdomain server. It is the same test as DX-life's /embed-test page:
|
||||
- the list is read at runtime from /dx-maps/manifest.json, never typed here;
|
||||
- section 1 is every profile whose stage is "production", section 2 every other one;
|
||||
- a profile whose `targets` leaves out "web" is skipped.
|
||||
A card mounts its element when it scrolls into view and removes it when it scrolls far away (a browser allows
|
||||
only a handful of WebGL contexts at once).
|
||||
?only=<bundle>/<profile> (comma-separated, or a bundle name) shows just those cards.
|
||||
?lang=<locale> sets the language (Arabic, Persian, Urdu and Hebrew lay out right to left).
|
||||
-->
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="robots" content="noindex">
|
||||
<title>Embed test</title>
|
||||
<style>
|
||||
body { font-family: system-ui, sans-serif; margin: 0 auto; max-width: 1200px; padding: 16px; color: #1f2620; }
|
||||
.embed-card { border: 1px solid #d1d4cd; border-radius: 10px; overflow: hidden; margin: 16px 0; }
|
||||
.embed-card__head { display: flex; flex-wrap: wrap; gap: 8px 12px; align-items: baseline; padding: 10px 12px; background: #f3f3f1; }
|
||||
.embed-card__state { margin-inline-start: auto; font-size: 12px; color: #6d766e; }
|
||||
.embed-card__desc { margin: 0; padding: 6px 12px; font-size: 13px; color: #3b463d; }
|
||||
.embed-card__error { margin: 0; padding: 6px 12px; color: #a0302a; }
|
||||
.embed-card__host { position: relative; height: 560px; }
|
||||
.embed-card__host > * { display: block; width: 100%; height: 100%; }
|
||||
@media (max-width: 600px) { .embed-card__host { height: 480px; } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Embed test</h1>
|
||||
<p data-embed-status>Reading the manifest…</p>
|
||||
<div id="cards"></div>
|
||||
<script>
|
||||
(async () => {
|
||||
const q = new URLSearchParams(location.search)
|
||||
const lang = (q.get('lang') || 'en').trim()
|
||||
document.documentElement.lang = lang
|
||||
if (/^(ar|fa|ur|he)\b/.test(lang)) document.documentElement.dir = 'rtl'
|
||||
const base = `${location.origin}/dx-maps`
|
||||
const status = document.querySelector('[data-embed-status]')
|
||||
const resolveUrl = (ref) => /^(https?:)?\/\//.test(ref) ? ref : ref.startsWith('/') ? new URL(ref, base).href : `${base}/${ref.replace(/^\.\//, '')}`
|
||||
const scripts = new Map()
|
||||
const loadScript = (src, integrity) => {
|
||||
if (!scripts.has(src)) scripts.set(src, new Promise((resolve, reject) => {
|
||||
const s = document.createElement('script')
|
||||
s.src = src
|
||||
if (integrity) { s.integrity = integrity; s.crossOrigin = 'anonymous' }
|
||||
s.onload = () => resolve()
|
||||
s.onerror = () => reject(new Error(`the script did not load: ${src}`))
|
||||
document.head.appendChild(s)
|
||||
}))
|
||||
return scripts.get(src)
|
||||
}
|
||||
let manifest
|
||||
try {
|
||||
const r = await fetch(`${base}/manifest.json`, { cache: 'no-cache' })
|
||||
if (!r.ok) throw new Error(`HTTP ${r.status}`)
|
||||
manifest = await r.json()
|
||||
} catch (e) {
|
||||
status.textContent = `The manifest did not load from ${base}/manifest.json (${e.message}).`
|
||||
return
|
||||
}
|
||||
const only = (q.get('only') || '').split(',').map((s) => s.trim()).filter(Boolean)
|
||||
const out = []
|
||||
for (const b of manifest.bundles || []) {
|
||||
const defined = b.elements || []
|
||||
for (const p of b.profiles || []) {
|
||||
const targets = p.targets || b.targets
|
||||
if (Array.isArray(targets) && !targets.includes('web')) continue
|
||||
const key = `${b.bundle}/${p.profile}`
|
||||
if (only.length && !only.includes(key) && !only.includes(b.bundle)) continue
|
||||
// the tag the bundle actually defines (a profile's own element name can name one it does not)
|
||||
const tag = p.element && (!defined.length || defined.includes(p.element)) ? p.element : defined[0] || p.element
|
||||
out.push({ key, bundle: b.bundle, profile: p.profile, tag, script: resolveUrl(b.scriptUrl || `${b.bundle}/${b.bundle}.js`),
|
||||
integrity: b.integrity || '', description: p.description || '', production: (p.stage || b.stage) === 'production', state: 'idle' })
|
||||
}
|
||||
}
|
||||
const cards = [...out.filter((c) => c.production), ...out.filter((c) => !c.production)]
|
||||
const prod = out.filter((c) => c.production).length
|
||||
status.textContent = `${out.length} web plugins from ${base}/manifest.json (${prod} production, ${out.length - prod} other), each embedded as JavaScript.`
|
||||
|
||||
const root = document.getElementById('cards')
|
||||
const el = (tag, attrs = {}, text) => { const n = document.createElement(tag); for (const [k, v] of Object.entries(attrs)) n.setAttribute(k, v); if (text) n.textContent = text; return n }
|
||||
cards.forEach((c, i) => {
|
||||
if (i === 0 && c.production) root.appendChild(el('h2', { 'data-embed-section': 'production' }, '1. Production'))
|
||||
if (!c.production && (i === 0 || cards[i - 1].production)) root.appendChild(el('h2', { 'data-embed-section': 'other' }, '2. Every other web plugin'))
|
||||
const sec = el('section', { class: 'embed-card', 'data-card': c.key, 'data-state': 'idle', 'data-production': String(c.production), 'data-locale': '' })
|
||||
const head = el('header', { class: 'embed-card__head' })
|
||||
head.append(el('strong', {}, `${c.bundle} / ${c.profile}`), el('code', {}, `<${c.tag}>`), el('span', { class: 'embed-card__state' }, 'idle'))
|
||||
sec.appendChild(head)
|
||||
if (c.description) sec.appendChild(el('p', { class: 'embed-card__desc' }, c.description))
|
||||
c.host = el('div', { class: 'embed-card__host' })
|
||||
sec.appendChild(c.host)
|
||||
c.section = sec
|
||||
root.appendChild(sec)
|
||||
})
|
||||
const setState = (c, s, err) => {
|
||||
c.state = s; c.section.dataset.state = s; c.section.querySelector('.embed-card__state').textContent = s
|
||||
if (err) { const p = el('p', { class: 'embed-card__error', role: 'alert' }, err); c.section.insertBefore(p, c.host) }
|
||||
}
|
||||
const mount = async (c) => {
|
||||
if (c.state === 'mounted' || c.state === 'loading') return
|
||||
setState(c, 'loading')
|
||||
try {
|
||||
await loadScript(c.script, c.integrity)
|
||||
if (!customElements.get(c.tag)) throw new Error(`<${c.tag}> was not defined by its bundle`)
|
||||
const node = document.createElement(c.tag)
|
||||
// shells (the Research Gallery, Plugin Studio) find their child maps through the tree's manifest
|
||||
node.setAttribute('tree-base', base)
|
||||
node.setAttribute('profile-config', JSON.stringify({ profile: c.profile, assetsBase: `${base}/assets`, locale: lang, provider: 'maplibre' }))
|
||||
node.setAttribute('language', lang)
|
||||
c.section.dataset.locale = lang
|
||||
c.host.replaceChildren(node)
|
||||
setState(c, 'mounted')
|
||||
} catch (e) {
|
||||
setState(c, 'error', String(e && e.message || e))
|
||||
}
|
||||
}
|
||||
const unmount = (c) => { if (c.state !== 'mounted') return; c.host.replaceChildren(); setState(c, 'idle') }
|
||||
const observer = new IntersectionObserver((entries) => {
|
||||
for (const en of entries) {
|
||||
const c = cards.find((x) => x.key === en.target.dataset.card)
|
||||
if (!c) continue
|
||||
if (en.isIntersecting) mount(c); else unmount(c)
|
||||
}
|
||||
}, { rootMargin: '600px 0px' })
|
||||
cards.forEach((c) => observer.observe(c.section))
|
||||
})()
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
Reference in a new issue