openvibers/openvibe.shared Commits
4.138.0: 38 KB off most first visits; every remaining sibling file by content hash (#326)
Changes
@@@ -1,5 +1,19 @@# Changelog++## 4.138.0 — 2026-11-07++++- **38 KB off most first visits** (the owner, 11-07: "make things faster and more optimized"):++ - the footer's "shipped" line (`shipped.js`, 12 KB) is fetched only when the footer comes near the screen;++ - brand logos (`brands.js`, 26 KB, the footer's social links) wait until a logo is near the screen, or opens++ from a fold;++ - most visits never scroll that far.++- **The rest of the files loaded from beside Shared's scripts go by content hash** (4.137.0 did the bar's):++ - `shipped.js` from the footer and from `frame.js`'s updates pages;++ - `brands.js` from the glyphs, through their build;++ - `route-transition.js` from Boost.++ - `scripts/sibling-revs.js` writes FOOTER_REV and BOOST_REV; `scripts/build-glyphs.js` writes BRANDS_REV.++ - Run the glyph build first, then sibling-revs.++## 4.137.0 — 2026-11-07- **Releases reach phones at once.** The bar loaded the menu (`search.js`), the Dock, presence, panels, notifications,@@@ -2,7 +2,7 @@> Versioned UI, the OpenVibe Frame (the navbar, footer and "shipped" views every site sits in), SEO, legal and release-client packages every OpenVibe site renders.--**Status:** alpha, v4.137.0 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployed++**Status:** alpha, v4.138.0 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployedconsumer installs a tagged release and none keeps a vendored copy([docs/migration-plan.md](docs/migration-plan.md) is done); each repository's `package.json` namesthe tag it pins.@@@ -4,13 +4,14 @@"code": true,"wave": "W2","package": "openvibe-shared",-- "version": "4.137.0",++ "version": "4.138.0","plan": "OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026)","deployed": false,"deployment": "none of its own: a package released as git tags; OpenVibe.Network serves /shared/* from its pinned tag, and a site can serve its own pinned copy (openvibe-shared/serve)","contracts": "openvibe-contracts v0.312.0 (devDependency: the release manifest contract)","sdk": "openvibe-sdk v0.94.0","features": [++ "the footer's shipped line and brand logos load when the footer nears the screen (38 KB off most first visits); the footer, Boost and the glyphs load their files by content hash (4.138.0)","every script the bar, the Dock and the notifications load goes by its content hash (?v=), so a release reaches every open page at once instead of up to a day later; one menu button on a phone (4.137.0)","the Dock's Agent panel reads the timeline again (the token is resolved when a call is made), and the Dock can open at a step: OpenVibeNavbar.openDock('agent', { task }) marks it and brings it into view (4.134.0)","the OpenVibe Frame: navbar, footer, panels, account switcher, notification UI with realtime",@@@ -112,13 +112,15 @@});}++ // route-transition.js's content hash (scripts/sibling-revs.js writes it), so a release gives it a new URL (4.138.0).++ const BOOST_REV = { 'route-transition.js': '776674802ce8' };function fx(mainEl) {if (o.fx === false) return Promise.resolve(null);if (root.OVRouteFx) return Promise.resolve(root.OVRouteFx);if (!selfSrc) return Promise.resolve(null);return new Promise((done) => {const t = document.createElement('script');-- t.src = selfSrc.replace(/boost\.js(\?.*)?$/, 'route-transition.js');++ t.src = selfSrc.replace(/boost\.js(\?.*)?$/, 'route-transition.js') + (BOOST_REV['route-transition.js'] ? `?v=${BOOST_REV['route-transition.js']}` : '');t.onload = () => done(root.OVRouteFx || null); t.onerror = () => done(null);document.head.appendChild(t);});@@@ -526,7 +526,7 @@}try { if (typeof window !== 'undefined' && typeof window.ovMarkMount === 'function') window.ovMarkMount(_mounted); } catch { /* */ }fillAccount({ ..._cfg, variant });-- if (_cfg.shipped !== false) loadShipped();++ if (_cfg.shipped !== false) shippedWhenNear(_mounted);watchPulse();}@@@ -585,6 +585,16 @@// shipped.js mounts every [data-ov-shipped] element when it loads; loaded once, lazily, after the footer.let _shippedLoading = false;++ // shipped.js's content hash (scripts/sibling-revs.js writes it), and the footer's "shipped" line is fetched only when the++ // footer comes near the screen (4.138.0): most visits never scroll there, and it is 12 KB.++ const FOOTER_REV = { 'shipped.js': 'ee1a121fe4f7' };++ let _shippedSeen = null;++ function shippedWhenNear(el) {++ if (typeof IntersectionObserver !== 'function' || !el || !el.getBoundingClientRect) { loadShipped(); return; }++ if (_shippedSeen) return;++ _shippedSeen = new IntersectionObserver((es) => { if (es.some((e) => e.isIntersecting)) { _shippedSeen.disconnect(); _shippedSeen = null; loadShipped(); } }, { rootMargin: '900px 0px' });++ _shippedSeen.observe(el);++ }function loadShipped() {if (typeof document === 'undefined') return;if (root.OpenVibeShipped && typeof root.OpenVibeShipped.scan === 'function') { try { root.OpenVibeShipped.scan(document); } catch { /* */ } return; }@@@ -593,7 +603,7 @@const sc = document.createElement('script');let src = `${NETWORK_URL}/shared/shipped.js`;try { if (OWN_SRC) src = new URL('shipped.js', OWN_SRC).href; } catch { /* */ }-- sc.src = src;++ sc.src = src + (FOOTER_REV['shipped.js'] ? `?v=${FOOTER_REV['shipped.js']}` : '');sc.defer = true;sc.setAttribute('data-ov-shipped-src', '');(document.head || document.documentElement).appendChild(sc);@@@ -46,8 +46,17 @@ function updatesBody({ service, siteName = null, base = NETWORK } = {}) {+ `</section>`;}++// shipped.js by its content hash (4.138.0), so an updates page never runs a copy an edge kept from before a release.++let _shippedRev = null;++function shippedRev() {++ if (_shippedRev === null) {++ try { _shippedRev = require('./assets').hashAsset(require('fs').readFileSync(require('path').join(__dirname, 'shipped.js'))); } catch { _shippedRev = ''; }++ }++ return _shippedRev;++}function shippedScript({ base = NETWORK } = {}) {-- return `<script src="${esc(String(base).replace(/\/+$/, ''))}/shared/shipped.js" defer></script>`;++ const v = shippedRev();++ return `<script src="${esc(String(base).replace(/\/+$/, ''))}/shared/shipped.js${v ? `?v=${v}` : ''}" defer></script>`;}module.exports = { footer: (cfg) => footerModule.ssr(cfg), noscriptNav, shipped, updatesBody, shippedScript };@@@ -513,7 +513,7 @@const NAV_ICONS_REV = 'd9a4febbbad7'; // sha256(nav-icons.js), first 12 hex: its ?v= (cacheable forever)// END generated// The files loaded from beside this one, by content (scripts/sibling-revs.js): a release changes their URLs.-- const SIBLING_REV = { 'ov-mark.js': '1e91c9c0f347', 'ov-glyphs.js': '715972edd968', 'ov-brand.woff2': '3b4e6a8a5878', 'ov-look.js': '2a6c53a7b176', 'ov-fx.js': '46fce1e2b8db', 'ov-labels.js': 'a30b178391f5', 'ov-forms.js': '068ce1b1c4d5', 'ov-forms.css': '98cf0f28bb68', 'ov-share.js': '7d4affbb5093', 'ov-share.css': 'cbd18e6ce991', 'search.js': '08f0452e6a79', 'dock.js': 'e13066db0aac', 'presence.js': '1b675c37d888', 'panels.js': '7d8267ca8035', 'notification-ui.js': '3ceb5266e7b8', 'history.js': '1e6598f78ba4', 'site-switcher.js': '292bea111aa8', 'appbar.js': '1edf7532f228', 'release-watch.js': '25f02a3f0684', 'ov-icons.js': 'ba3981246bc0', 'sso-client.js': '52439265b980' };++ const SIBLING_REV = { 'ov-mark.js': '1e91c9c0f347', 'ov-glyphs.js': '1a0d8ce6909f', 'ov-brand.woff2': '3b4e6a8a5878', 'ov-look.js': '2a6c53a7b176', 'ov-fx.js': '46fce1e2b8db', 'ov-labels.js': 'a30b178391f5', 'ov-forms.js': '068ce1b1c4d5', 'ov-forms.css': '98cf0f28bb68', 'ov-share.js': '7d4affbb5093', 'ov-share.css': 'cbd18e6ce991', 'search.js': '08f0452e6a79', 'dock.js': 'e13066db0aac', 'presence.js': '1b675c37d888', 'panels.js': '7d8267ca8035', 'notification-ui.js': '3ceb5266e7b8', 'history.js': '1e6598f78ba4', 'site-switcher.js': '292bea111aa8', 'appbar.js': '1edf7532f228', 'release-watch.js': '25f02a3f0684', 'ov-icons.js': 'ba3981246bc0', 'sso-client.js': '52439265b980' };// The navbar's icons are filled paths (no Font Awesome CSS needed): the ones above ship here, the rest// (nav-icons.js) load after first paint when a link, chip or menu row names one, into an empty <svg> of their// size. Anything unknown, or nav-icons.js failing, falls back to <i class="fa-solid">.@@@ -53,6 +53,17 @@const SRC = (document.currentScript && document.currentScript.src) || '';let brandState = 0; // 0 idle, 1 loading, 2 ready, 3 unavailableconst waiting = new Set();++ const BRANDS_REV = '0da501c80e60';++ // A logo far below the fold (a footer's social links) waits until it comes near the screen (4.138.0): most visits++ // never get there, and brands.js is 26 KB. One that is in view, or hidden in a closed fold that opens, loads then.++ let brandSeen = null;++ function wantBrands(el) {++ if (brandState || typeof IntersectionObserver !== 'function' || !el.getBoundingClientRect) { loadBrands(); return; }++ const r = el.getBoundingClientRect();++ if ((r.width || r.height) && r.bottom > -800 && r.top < (window.innerHeight || 0) + 800) { loadBrands(); return; }++ brandSeen = brandSeen || new IntersectionObserver((es) => { if (es.some((e) => e.isIntersecting)) { brandSeen.disconnect(); brandSeen = null; loadBrands(); } }, { rootMargin: '800px 0px' });++ brandSeen.observe(el);++ }function loadBrands() {if (brandState) return;if (window.OVBrands) { brandState = 2; return; }@@@ -61,7 +72,7 @@const base = SRC ? SRC.split(/[?#]/)[0] : '';const dir = base && /ov-glyphs[.]js$/.test(base) ? base.slice(0, base.lastIndexOf('/') + 1) : 'https://openvibe.network/shared/';const sc = document.createElement('script');-- sc.src = dir + 'brands.js';++ sc.src = dir + 'brands.js' + (BRANDS_REV ? '?v=' + BRANDS_REV : '');sc.async = true;sc.onload = () => { brandState = window.OVBrands ? 2 : 3; waiting.forEach((el) => drawBrand(el)); waiting.clear(); };sc.onerror = () => { brandState = 3; waiting.clear(); };@@@ -73,7 +84,7 @@if (!fa) return;const slug = FB[fa];if (el.dataset.ovg === 'brand:' + slug) return;-- if (!window.OVBrands) { if (brandState !== 3) { waiting.add(el); loadBrands(); } return; }++ if (!window.OVBrands) { if (brandState !== 3) { waiting.add(el); wantBrands(el); } return; }const b = window.OVBrands.get(slug);if (!b || (!el.dataset.ovg && el.textContent.trim())) return;el.classList.add('ovg', 'ovg-brand');@@@ -1,6 +1,6 @@{"name": "openvibe-shared",-- "version": "4.137.0",++ "version": "4.138.0","description": "Shared navbar, footer, themes, icons, legal pages, SEO, auth and analytics for every OpenVibe site","main": "index.js","bin": {@@@ -92,6 +92,8 @@ const CSS = [].join('');const brands = require('../brands');++// brands.js's content hash, so ov-glyphs.js fetches it by a URL that changes with each release (4.138.0).++const brandsRev = require('../assets').hashAsset(fs.readFileSync(path.join(__dirname, '..', 'brands.js')));const out = `/* Generated by scripts/build-glyphs.js from glyphs.js: edit those, then run it. *//*** openvibe-shared/ov-glyphs.js — the OpenVibe glyphs in place of Font Awesome (plan L32). A page that loads it keeps its@@@ -147,6 +149,17 @@ const out = `/* Generated by scripts/build-glyphs.js from glyphs.js: edit those,const SRC = (document.currentScript && document.currentScript.src) || '';let brandState = 0; // 0 idle, 1 loading, 2 ready, 3 unavailableconst waiting = new Set();++ const BRANDS_REV = '${brandsRev}';++ // A logo far below the fold (a footer's social links) waits until it comes near the screen (4.138.0): most visits++ // never get there, and brands.js is 26 KB. One that is in view, or hidden in a closed fold that opens, loads then.++ let brandSeen = null;++ function wantBrands(el) {++ if (brandState || typeof IntersectionObserver !== 'function' || !el.getBoundingClientRect) { loadBrands(); return; }++ const r = el.getBoundingClientRect();++ if ((r.width || r.height) && r.bottom > -800 && r.top < (window.innerHeight || 0) + 800) { loadBrands(); return; }++ brandSeen = brandSeen || new IntersectionObserver((es) => { if (es.some((e) => e.isIntersecting)) { brandSeen.disconnect(); brandSeen = null; loadBrands(); } }, { rootMargin: '800px 0px' });++ brandSeen.observe(el);++ }function loadBrands() {if (brandState) return;if (window.OVBrands) { brandState = 2; return; }@@@ -155,7 +168,7 @@ const out = `/* Generated by scripts/build-glyphs.js from glyphs.js: edit those,const base = SRC ? SRC.split(/[?#]/)[0] : '';const dir = base && /ov-glyphs[.]js$/.test(base) ? base.slice(0, base.lastIndexOf('/') + 1) : 'https://openvibe.network/shared/';const sc = document.createElement('script');-- sc.src = dir + 'brands.js';++ sc.src = dir + 'brands.js' + (BRANDS_REV ? '?v=' + BRANDS_REV : '');sc.async = true;sc.onload = () => { brandState = window.OVBrands ? 2 : 3; waiting.forEach((el) => drawBrand(el)); waiting.clear(); };sc.onerror = () => { brandState = 3; waiting.clear(); };@@@ -167,7 +180,7 @@ const out = `/* Generated by scripts/build-glyphs.js from glyphs.js: edit those,if (!fa) return;const slug = FB[fa];if (el.dataset.ovg === 'brand:' + slug) return;-- if (!window.OVBrands) { if (brandState !== 3) { waiting.add(el); loadBrands(); } return; }++ if (!window.OVBrands) { if (brandState !== 3) { waiting.add(el); wantBrands(el); } return; }const b = window.OVBrands.get(slug);if (!b || (!el.dataset.ovg && el.textContent.trim())) return;el.classList.add('ovg', 'ovg-brand');@@@ -16,15 +16,16 @@ const FILES = ['ov-mark.js', 'ov-glyphs.js', 'ov-brand.woff2', 'ov-look.js', 'ov'search.js', 'dock.js', 'presence.js', 'panels.js', 'notification-ui.js', 'history.js', 'site-switcher.js', 'appbar.js', 'release-watch.js', 'ov-icons.js', 'sso-client.js'];// The files dock.js and notification-ui.js load from beside themselves: their own tables, in their own files.const OWN = { 'dock.js': { name: 'DOCK_REV', files: ['dock-vibe.js', 'action-center.js', 'dock-timeline.js', 'dock-agent.js', 'dock-timer.js'] },-- 'notification-ui.js': { name: 'NOTIFY_REV', files: ['notification-live.js'] } };++ 'notification-ui.js': { name: 'NOTIFY_REV', files: ['notification-live.js'] },++ 'footer.js': { name: 'FOOTER_REV', files: ['shipped.js'] }, 'boost.js': { name: 'BOOST_REV', files: ['route-transition.js'] } };const table = (name, files) => ` const ${name} = { ${files.map((f) => `'${f}': '${hashAsset(fs.readFileSync(path.join(ROOT, f)))}'`).join(', ')} };`;function writeOwn() {for (const [file, { name, files }] of Object.entries(OWN)) {const p = path.join(ROOT, file);const src = fs.readFileSync(p, 'utf8');-- const re = new RegExp(`^ {4}const ${name} = \\{[^\\n]*\\};$`, 'm');++ const re = new RegExp(`^( *)const ${name} = \\{[^\\n]*\\};$`, 'm');if (!re.test(src)) throw new Error(`${file} has no ${name} line to update`);-- fs.writeFileSync(p, src.replace(re, table(name, files)));++ fs.writeFileSync(p, src.replace(re, (m, indent) => indent + table(name, files).trimStart()));}}const revs = () => Object.fromEntries(FILES.map((f) => [f, hashAsset(fs.readFileSync(path.join(ROOT, f)))]));@@@ -16,7 +16,8 @@ const body = frame.updatesBody({ service: 'wiki' });assert.ok(body.includes('<h1>What shipped on OpenVibe.Wiki</h1>'));assert.ok(body.includes('data-ov-shipped="log" data-service="wiki"'));assert.ok(body.includes('https://openvibe.network/updates?site=wiki'), 'the no-JavaScript fallback is the network log');--assert.strictEqual(frame.shippedScript(), '<script src="https://openvibe.network/shared/shipped.js" defer></script>');++// By its content hash (4.138.0), so an updates page never runs a copy an edge kept from before a release.++assert.match(frame.shippedScript(), /^<script src="https:\/\/openvibe\.network\/shared\/shipped\.js\?v=[0-9a-f]{12}" defer><\/script>$/);const foot = frame.footer({ service: 'wiki', variant: 'full', updates: '/updates' });assert.ok(foot.includes('data-ov-shipped="latest"') && foot.includes('href="/updates"'));@@@ -23,4 +23,13 @@ for (const [file, { name, files }] of Object.entries(OWN)) {}const dock = fs.readFileSync(path.join(__dirname, '..', 'dock.js'), 'utf8');assert.ok(dock.includes("sc.src = sibling(src) + (DOCK_REV[src] ? `?v=${DOCK_REV[src]}` : '');"), 'the Dock loads its siblings by hash');--console.log('sibling revs: every script the navbar, the Dock and the notifications load goes by its content hash');++// 4.138.0: the footer, Boost and the glyphs load theirs by hash too; the footer's shipped.js and the glyphs' brands.js++// wait until the footer (or a logo) comes near the screen.++const footer = fs.readFileSync(path.join(__dirname, '..', 'footer.js'), 'utf8');++assert.ok(/if \(_cfg\.shipped !== false\) shippedWhenNear\(_mounted\);/.test(footer), 'the footer fetches shipped.js when it nears the screen');++assert.ok(footer.includes("sc.src = src + (FOOTER_REV['shipped.js'] ? `?v=${FOOTER_REV['shipped.js']}` : '');"), 'and by its hash');++const glyphsOut = fs.readFileSync(path.join(__dirname, '..', 'ov-glyphs.js'), 'utf8');++assert.ok(/const BRANDS_REV = '[0-9a-f]{12}';/.test(glyphsOut) && glyphsOut.includes('waiting.add(el); wantBrands(el);'), 'brands.js by hash, when a logo nears the screen');++const boost = fs.readFileSync(path.join(__dirname, '..', 'boost.js'), 'utf8');++assert.ok(/BOOST_REV\['route-transition\.js'\]/.test(boost), 'route-transition.js by hash');++console.log('sibling revs: every script the navbar, the Dock, the notifications, the footer, Boost and the glyphs load goes by its content hash');
@@ -1,5 +1,19 @@
@@ -1,5 +1,19 @@
# Changelog
# Changelog
+## 4.138.0 — 2026-11-07
+
+- **38 KB off most first visits** (the owner, 11-07: "make things faster and more optimized"):
+ - the footer's "shipped" line (`shipped.js`, 12 KB) is fetched only when the footer comes near the screen;
+ - brand logos (`brands.js`, 26 KB, the footer's social links) wait until a logo is near the screen, or opens
+ from a fold;
+ - most visits never scroll that far.
+- **The rest of the files loaded from beside Shared's scripts go by content hash** (4.137.0 did the bar's):
+ - `shipped.js` from the footer and from `frame.js`'s updates pages;
+ - `brands.js` from the glyphs, through their build;
+ - `route-transition.js` from Boost.
+ - `scripts/sibling-revs.js` writes FOOTER_REV and BOOST_REV; `scripts/build-glyphs.js` writes BRANDS_REV.
+ - Run the glyph build first, then sibling-revs.
+
## 4.137.0 — 2026-11-07
## 4.137.0 — 2026-11-07
- **Releases reach phones at once.** The bar loaded the menu (`search.js`), the Dock, presence, panels, notifications,
- **Releases reach phones at once.** The bar loaded the menu (`search.js`), the Dock, presence, panels, notifications,
@@ -2,7 +2,7 @@
@@ -2,7 +2,7 @@
> Versioned UI, the OpenVibe Frame (the navbar, footer and "shipped" views every site sits in), SEO, legal and release-client packages every OpenVibe site renders.
> Versioned UI, the OpenVibe Frame (the navbar, footer and "shipped" views every site sits in), SEO, legal and release-client packages every OpenVibe site renders.
-**Status:** alpha, v4.137.0 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployed
+**Status:** alpha, v4.138.0 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployed
consumer installs a tagged release and none keeps a vendored copy
consumer installs a tagged release and none keeps a vendored copy
([docs/migration-plan.md](docs/migration-plan.md) is done); each repository's `package.json` names
([docs/migration-plan.md](docs/migration-plan.md) is done); each repository's `package.json` names
the tag it pins.
the tag it pins.
@@ -4,13 +4,14 @@
@@ -4,13 +4,14 @@
"code": true,
"code": true,
"wave": "W2",
"wave": "W2",
"package": "openvibe-shared",
"package": "openvibe-shared",
- "version": "4.137.0",
+ "version": "4.138.0",
"plan": "OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026)",
"plan": "OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026)",
"deployed": false,
"deployed": false,
"deployment": "none of its own: a package released as git tags; OpenVibe.Network serves /shared/* from its pinned tag, and a site can serve its own pinned copy (openvibe-shared/serve)",
"deployment": "none of its own: a package released as git tags; OpenVibe.Network serves /shared/* from its pinned tag, and a site can serve its own pinned copy (openvibe-shared/serve)",
"contracts": "openvibe-contracts v0.312.0 (devDependency: the release manifest contract)",
"contracts": "openvibe-contracts v0.312.0 (devDependency: the release manifest contract)",
"sdk": "openvibe-sdk v0.94.0",
"sdk": "openvibe-sdk v0.94.0",
"features": [
"features": [
+ "the footer's shipped line and brand logos load when the footer nears the screen (38 KB off most first visits); the footer, Boost and the glyphs load their files by content hash (4.138.0)",
"every script the bar, the Dock and the notifications load goes by its content hash (?v=), so a release reaches every open page at once instead of up to a day later; one menu button on a phone (4.137.0)",
"every script the bar, the Dock and the notifications load goes by its content hash (?v=), so a release reaches every open page at once instead of up to a day later; one menu button on a phone (4.137.0)",
"the Dock's Agent panel reads the timeline again (the token is resolved when a call is made), and the Dock can open at a step: OpenVibeNavbar.openDock('agent', { task }) marks it and brings it into view (4.134.0)",
"the Dock's Agent panel reads the timeline again (the token is resolved when a call is made), and the Dock can open at a step: OpenVibeNavbar.openDock('agent', { task }) marks it and brings it into view (4.134.0)",
"the OpenVibe Frame: navbar, footer, panels, account switcher, notification UI with realtime",
"the OpenVibe Frame: navbar, footer, panels, account switcher, notification UI with realtime",
@@ -112,13 +112,15 @@
@@ -112,13 +112,15 @@
});
});
}
}
+ // route-transition.js's content hash (scripts/sibling-revs.js writes it), so a release gives it a new URL (4.138.0).
+ const BOOST_REV = { 'route-transition.js': '776674802ce8' };
function fx(mainEl) {
function fx(mainEl) {
if (o.fx === false) return Promise.resolve(null);
if (o.fx === false) return Promise.resolve(null);
if (root.OVRouteFx) return Promise.resolve(root.OVRouteFx);
if (root.OVRouteFx) return Promise.resolve(root.OVRouteFx);
if (!selfSrc) return Promise.resolve(null);
if (!selfSrc) return Promise.resolve(null);
return new Promise((done) => {
return new Promise((done) => {
const t = document.createElement('script');
const t = document.createElement('script');
- t.src = selfSrc.replace(/boost\.js(\?.*)?$/, 'route-transition.js');
+ t.src = selfSrc.replace(/boost\.js(\?.*)?$/, 'route-transition.js') + (BOOST_REV['route-transition.js'] ? `?v=${BOOST_REV['route-transition.js']}` : '');
t.onload = () => done(root.OVRouteFx || null); t.onerror = () => done(null);
t.onload = () => done(root.OVRouteFx || null); t.onerror = () => done(null);
document.head.appendChild(t);
document.head.appendChild(t);
});
});
@@ -526,7 +526,7 @@
@@ -526,7 +526,7 @@
}
}
try { if (typeof window !== 'undefined' && typeof window.ovMarkMount === 'function') window.ovMarkMount(_mounted); } catch { /* */ }
try { if (typeof window !== 'undefined' && typeof window.ovMarkMount === 'function') window.ovMarkMount(_mounted); } catch { /* */ }
fillAccount({ ..._cfg, variant });
fillAccount({ ..._cfg, variant });
- if (_cfg.shipped !== false) loadShipped();
+ if (_cfg.shipped !== false) shippedWhenNear(_mounted);
watchPulse();
watchPulse();
}
}
@@ -585,6 +585,16 @@
@@ -585,6 +585,16 @@
// shipped.js mounts every [data-ov-shipped] element when it loads; loaded once, lazily, after the footer.
// shipped.js mounts every [data-ov-shipped] element when it loads; loaded once, lazily, after the footer.
let _shippedLoading = false;
let _shippedLoading = false;
+ // shipped.js's content hash (scripts/sibling-revs.js writes it), and the footer's "shipped" line is fetched only when the
+ // footer comes near the screen (4.138.0): most visits never scroll there, and it is 12 KB.
+ const FOOTER_REV = { 'shipped.js': 'ee1a121fe4f7' };
+ let _shippedSeen = null;
+ function shippedWhenNear(el) {
+ if (typeof IntersectionObserver !== 'function' || !el || !el.getBoundingClientRect) { loadShipped(); return; }
+ if (_shippedSeen) return;
+ _shippedSeen = new IntersectionObserver((es) => { if (es.some((e) => e.isIntersecting)) { _shippedSeen.disconnect(); _shippedSeen = null; loadShipped(); } }, { rootMargin: '900px 0px' });
+ _shippedSeen.observe(el);
+ }
function loadShipped() {
function loadShipped() {
if (typeof document === 'undefined') return;
if (typeof document === 'undefined') return;
if (root.OpenVibeShipped && typeof root.OpenVibeShipped.scan === 'function') { try { root.OpenVibeShipped.scan(document); } catch { /* */ } return; }
if (root.OpenVibeShipped && typeof root.OpenVibeShipped.scan === 'function') { try { root.OpenVibeShipped.scan(document); } catch { /* */ } return; }
@@ -593,7 +603,7 @@
@@ -593,7 +603,7 @@
const sc = document.createElement('script');
const sc = document.createElement('script');
let src = `${NETWORK_URL}/shared/shipped.js`;
let src = `${NETWORK_URL}/shared/shipped.js`;
try { if (OWN_SRC) src = new URL('shipped.js', OWN_SRC).href; } catch { /* */ }
try { if (OWN_SRC) src = new URL('shipped.js', OWN_SRC).href; } catch { /* */ }
- sc.src = src;
+ sc.src = src + (FOOTER_REV['shipped.js'] ? `?v=${FOOTER_REV['shipped.js']}` : '');
sc.defer = true;
sc.defer = true;
sc.setAttribute('data-ov-shipped-src', '');
sc.setAttribute('data-ov-shipped-src', '');
(document.head || document.documentElement).appendChild(sc);
(document.head || document.documentElement).appendChild(sc);
@@ -46,8 +46,17 @@ function updatesBody({ service, siteName = null, base = NETWORK } = {}) {
@@ -46,8 +46,17 @@ function updatesBody({ service, siteName = null, base = NETWORK } = {}) {
+ `</section>`;
+ `</section>`;
}
}
+// shipped.js by its content hash (4.138.0), so an updates page never runs a copy an edge kept from before a release.
+let _shippedRev = null;
+function shippedRev() {
+ if (_shippedRev === null) {
+ try { _shippedRev = require('./assets').hashAsset(require('fs').readFileSync(require('path').join(__dirname, 'shipped.js'))); } catch { _shippedRev = ''; }
+ }
+ return _shippedRev;
+}
function shippedScript({ base = NETWORK } = {}) {
function shippedScript({ base = NETWORK } = {}) {
- return `<script src="${esc(String(base).replace(/\/+$/, ''))}/shared/shipped.js" defer></script>`;
+ const v = shippedRev();
+ return `<script src="${esc(String(base).replace(/\/+$/, ''))}/shared/shipped.js${v ? `?v=${v}` : ''}" defer></script>`;
}
}
module.exports = { footer: (cfg) => footerModule.ssr(cfg), noscriptNav, shipped, updatesBody, shippedScript };
module.exports = { footer: (cfg) => footerModule.ssr(cfg), noscriptNav, shipped, updatesBody, shippedScript };
@@ -513,7 +513,7 @@
@@ -513,7 +513,7 @@
const NAV_ICONS_REV = 'd9a4febbbad7'; // sha256(nav-icons.js), first 12 hex: its ?v= (cacheable forever)
const NAV_ICONS_REV = 'd9a4febbbad7'; // sha256(nav-icons.js), first 12 hex: its ?v= (cacheable forever)
// END generated
// END generated
// The files loaded from beside this one, by content (scripts/sibling-revs.js): a release changes their URLs.
// The files loaded from beside this one, by content (scripts/sibling-revs.js): a release changes their URLs.
- const SIBLING_REV = { 'ov-mark.js': '1e91c9c0f347', 'ov-glyphs.js': '715972edd968', 'ov-brand.woff2': '3b4e6a8a5878', 'ov-look.js': '2a6c53a7b176', 'ov-fx.js': '46fce1e2b8db', 'ov-labels.js': 'a30b178391f5', 'ov-forms.js': '068ce1b1c4d5', 'ov-forms.css': '98cf0f28bb68', 'ov-share.js': '7d4affbb5093', 'ov-share.css': 'cbd18e6ce991', 'search.js': '08f0452e6a79', 'dock.js': 'e13066db0aac', 'presence.js': '1b675c37d888', 'panels.js': '7d8267ca8035', 'notification-ui.js': '3ceb5266e7b8', 'history.js': '1e6598f78ba4', 'site-switcher.js': '292bea111aa8', 'appbar.js': '1edf7532f228', 'release-watch.js': '25f02a3f0684', 'ov-icons.js': 'ba3981246bc0', 'sso-client.js': '52439265b980' };
+ const SIBLING_REV = { 'ov-mark.js': '1e91c9c0f347', 'ov-glyphs.js': '1a0d8ce6909f', 'ov-brand.woff2': '3b4e6a8a5878', 'ov-look.js': '2a6c53a7b176', 'ov-fx.js': '46fce1e2b8db', 'ov-labels.js': 'a30b178391f5', 'ov-forms.js': '068ce1b1c4d5', 'ov-forms.css': '98cf0f28bb68', 'ov-share.js': '7d4affbb5093', 'ov-share.css': 'cbd18e6ce991', 'search.js': '08f0452e6a79', 'dock.js': 'e13066db0aac', 'presence.js': '1b675c37d888', 'panels.js': '7d8267ca8035', 'notification-ui.js': '3ceb5266e7b8', 'history.js': '1e6598f78ba4', 'site-switcher.js': '292bea111aa8', 'appbar.js': '1edf7532f228', 'release-watch.js': '25f02a3f0684', 'ov-icons.js': 'ba3981246bc0', 'sso-client.js': '52439265b980' };
// The navbar's icons are filled paths (no Font Awesome CSS needed): the ones above ship here, the rest
// The navbar's icons are filled paths (no Font Awesome CSS needed): the ones above ship here, the rest
// (nav-icons.js) load after first paint when a link, chip or menu row names one, into an empty <svg> of their
// (nav-icons.js) load after first paint when a link, chip or menu row names one, into an empty <svg> of their
// size. Anything unknown, or nav-icons.js failing, falls back to <i class="fa-solid">.
// size. Anything unknown, or nav-icons.js failing, falls back to <i class="fa-solid">.
@@ -53,6 +53,17 @@
@@ -53,6 +53,17 @@
const SRC = (document.currentScript && document.currentScript.src) || '';
const SRC = (document.currentScript && document.currentScript.src) || '';
let brandState = 0; // 0 idle, 1 loading, 2 ready, 3 unavailable
let brandState = 0; // 0 idle, 1 loading, 2 ready, 3 unavailable
const waiting = new Set();
const waiting = new Set();
+ const BRANDS_REV = '0da501c80e60';
+ // A logo far below the fold (a footer's social links) waits until it comes near the screen (4.138.0): most visits
+ // never get there, and brands.js is 26 KB. One that is in view, or hidden in a closed fold that opens, loads then.
+ let brandSeen = null;
+ function wantBrands(el) {
+ if (brandState || typeof IntersectionObserver !== 'function' || !el.getBoundingClientRect) { loadBrands(); return; }
+ const r = el.getBoundingClientRect();
+ if ((r.width || r.height) && r.bottom > -800 && r.top < (window.innerHeight || 0) + 800) { loadBrands(); return; }
+ brandSeen = brandSeen || new IntersectionObserver((es) => { if (es.some((e) => e.isIntersecting)) { brandSeen.disconnect(); brandSeen = null; loadBrands(); } }, { rootMargin: '800px 0px' });
+ brandSeen.observe(el);
+ }
function loadBrands() {
function loadBrands() {
if (brandState) return;
if (brandState) return;
if (window.OVBrands) { brandState = 2; return; }
if (window.OVBrands) { brandState = 2; return; }
@@ -61,7 +72,7 @@
@@ -61,7 +72,7 @@
const base = SRC ? SRC.split(/[?#]/)[0] : '';
const base = SRC ? SRC.split(/[?#]/)[0] : '';
const dir = base && /ov-glyphs[.]js$/.test(base) ? base.slice(0, base.lastIndexOf('/') + 1) : 'https://openvibe.network/shared/';
const dir = base && /ov-glyphs[.]js$/.test(base) ? base.slice(0, base.lastIndexOf('/') + 1) : 'https://openvibe.network/shared/';
const sc = document.createElement('script');
const sc = document.createElement('script');
- sc.src = dir + 'brands.js';
+ sc.src = dir + 'brands.js' + (BRANDS_REV ? '?v=' + BRANDS_REV : '');
sc.async = true;
sc.async = true;
sc.onload = () => { brandState = window.OVBrands ? 2 : 3; waiting.forEach((el) => drawBrand(el)); waiting.clear(); };
sc.onload = () => { brandState = window.OVBrands ? 2 : 3; waiting.forEach((el) => drawBrand(el)); waiting.clear(); };
sc.onerror = () => { brandState = 3; waiting.clear(); };
sc.onerror = () => { brandState = 3; waiting.clear(); };
@@ -73,7 +84,7 @@
@@ -73,7 +84,7 @@
if (!fa) return;
if (!fa) return;
const slug = FB[fa];
const slug = FB[fa];
if (el.dataset.ovg === 'brand:' + slug) return;
if (el.dataset.ovg === 'brand:' + slug) return;
- if (!window.OVBrands) { if (brandState !== 3) { waiting.add(el); loadBrands(); } return; }
+ if (!window.OVBrands) { if (brandState !== 3) { waiting.add(el); wantBrands(el); } return; }
const b = window.OVBrands.get(slug);
const b = window.OVBrands.get(slug);
if (!b || (!el.dataset.ovg && el.textContent.trim())) return;
if (!b || (!el.dataset.ovg && el.textContent.trim())) return;
el.classList.add('ovg', 'ovg-brand');
el.classList.add('ovg', 'ovg-brand');
@@ -1,6 +1,6 @@
@@ -1,6 +1,6 @@
{
{
"name": "openvibe-shared",
"name": "openvibe-shared",
- "version": "4.137.0",
+ "version": "4.138.0",
"description": "Shared navbar, footer, themes, icons, legal pages, SEO, auth and analytics for every OpenVibe site",
"description": "Shared navbar, footer, themes, icons, legal pages, SEO, auth and analytics for every OpenVibe site",
"main": "index.js",
"main": "index.js",
"bin": {
"bin": {
@@ -92,6 +92,8 @@ const CSS = [
@@ -92,6 +92,8 @@ const CSS = [
].join('');
].join('');
const brands = require('../brands');
const brands = require('../brands');
+// brands.js's content hash, so ov-glyphs.js fetches it by a URL that changes with each release (4.138.0).
+const brandsRev = require('../assets').hashAsset(fs.readFileSync(path.join(__dirname, '..', 'brands.js')));
const out = `/* Generated by scripts/build-glyphs.js from glyphs.js: edit those, then run it. */
const out = `/* Generated by scripts/build-glyphs.js from glyphs.js: edit those, then run it. */
/**
/**
* openvibe-shared/ov-glyphs.js — the OpenVibe glyphs in place of Font Awesome (plan L32). A page that loads it keeps its
* openvibe-shared/ov-glyphs.js — the OpenVibe glyphs in place of Font Awesome (plan L32). A page that loads it keeps its
@@ -147,6 +149,17 @@ const out = `/* Generated by scripts/build-glyphs.js from glyphs.js: edit those,
@@ -147,6 +149,17 @@ const out = `/* Generated by scripts/build-glyphs.js from glyphs.js: edit those,
const SRC = (document.currentScript && document.currentScript.src) || '';
const SRC = (document.currentScript && document.currentScript.src) || '';
let brandState = 0; // 0 idle, 1 loading, 2 ready, 3 unavailable
let brandState = 0; // 0 idle, 1 loading, 2 ready, 3 unavailable
const waiting = new Set();
const waiting = new Set();
+ const BRANDS_REV = '${brandsRev}';
+ // A logo far below the fold (a footer's social links) waits until it comes near the screen (4.138.0): most visits
+ // never get there, and brands.js is 26 KB. One that is in view, or hidden in a closed fold that opens, loads then.
+ let brandSeen = null;
+ function wantBrands(el) {
+ if (brandState || typeof IntersectionObserver !== 'function' || !el.getBoundingClientRect) { loadBrands(); return; }
+ const r = el.getBoundingClientRect();
+ if ((r.width || r.height) && r.bottom > -800 && r.top < (window.innerHeight || 0) + 800) { loadBrands(); return; }
+ brandSeen = brandSeen || new IntersectionObserver((es) => { if (es.some((e) => e.isIntersecting)) { brandSeen.disconnect(); brandSeen = null; loadBrands(); } }, { rootMargin: '800px 0px' });
+ brandSeen.observe(el);
+ }
function loadBrands() {
function loadBrands() {
if (brandState) return;
if (brandState) return;
if (window.OVBrands) { brandState = 2; return; }
if (window.OVBrands) { brandState = 2; return; }
@@ -155,7 +168,7 @@ const out = `/* Generated by scripts/build-glyphs.js from glyphs.js: edit those,
@@ -155,7 +168,7 @@ const out = `/* Generated by scripts/build-glyphs.js from glyphs.js: edit those,
const base = SRC ? SRC.split(/[?#]/)[0] : '';
const base = SRC ? SRC.split(/[?#]/)[0] : '';
const dir = base && /ov-glyphs[.]js$/.test(base) ? base.slice(0, base.lastIndexOf('/') + 1) : 'https://openvibe.network/shared/';
const dir = base && /ov-glyphs[.]js$/.test(base) ? base.slice(0, base.lastIndexOf('/') + 1) : 'https://openvibe.network/shared/';
const sc = document.createElement('script');
const sc = document.createElement('script');
- sc.src = dir + 'brands.js';
+ sc.src = dir + 'brands.js' + (BRANDS_REV ? '?v=' + BRANDS_REV : '');
sc.async = true;
sc.async = true;
sc.onload = () => { brandState = window.OVBrands ? 2 : 3; waiting.forEach((el) => drawBrand(el)); waiting.clear(); };
sc.onload = () => { brandState = window.OVBrands ? 2 : 3; waiting.forEach((el) => drawBrand(el)); waiting.clear(); };
sc.onerror = () => { brandState = 3; waiting.clear(); };
sc.onerror = () => { brandState = 3; waiting.clear(); };
@@ -167,7 +180,7 @@ const out = `/* Generated by scripts/build-glyphs.js from glyphs.js: edit those,
@@ -167,7 +180,7 @@ const out = `/* Generated by scripts/build-glyphs.js from glyphs.js: edit those,
if (!fa) return;
if (!fa) return;
const slug = FB[fa];
const slug = FB[fa];
if (el.dataset.ovg === 'brand:' + slug) return;
if (el.dataset.ovg === 'brand:' + slug) return;
- if (!window.OVBrands) { if (brandState !== 3) { waiting.add(el); loadBrands(); } return; }
+ if (!window.OVBrands) { if (brandState !== 3) { waiting.add(el); wantBrands(el); } return; }
const b = window.OVBrands.get(slug);
const b = window.OVBrands.get(slug);
if (!b || (!el.dataset.ovg && el.textContent.trim())) return;
if (!b || (!el.dataset.ovg && el.textContent.trim())) return;
el.classList.add('ovg', 'ovg-brand');
el.classList.add('ovg', 'ovg-brand');
@@ -16,15 +16,16 @@ const FILES = ['ov-mark.js', 'ov-glyphs.js', 'ov-brand.woff2', 'ov-look.js', 'ov
@@ -16,15 +16,16 @@ const FILES = ['ov-mark.js', 'ov-glyphs.js', 'ov-brand.woff2', 'ov-look.js', 'ov
'search.js', 'dock.js', 'presence.js', 'panels.js', 'notification-ui.js', 'history.js', 'site-switcher.js', 'appbar.js', 'release-watch.js', 'ov-icons.js', 'sso-client.js'];
'search.js', 'dock.js', 'presence.js', 'panels.js', 'notification-ui.js', 'history.js', 'site-switcher.js', 'appbar.js', 'release-watch.js', 'ov-icons.js', 'sso-client.js'];
// The files dock.js and notification-ui.js load from beside themselves: their own tables, in their own files.
// The files dock.js and notification-ui.js load from beside themselves: their own tables, in their own files.
const OWN = { 'dock.js': { name: 'DOCK_REV', files: ['dock-vibe.js', 'action-center.js', 'dock-timeline.js', 'dock-agent.js', 'dock-timer.js'] },
const OWN = { 'dock.js': { name: 'DOCK_REV', files: ['dock-vibe.js', 'action-center.js', 'dock-timeline.js', 'dock-agent.js', 'dock-timer.js'] },
- 'notification-ui.js': { name: 'NOTIFY_REV', files: ['notification-live.js'] } };
+ 'notification-ui.js': { name: 'NOTIFY_REV', files: ['notification-live.js'] },
+ 'footer.js': { name: 'FOOTER_REV', files: ['shipped.js'] }, 'boost.js': { name: 'BOOST_REV', files: ['route-transition.js'] } };
const table = (name, files) => ` const ${name} = { ${files.map((f) => `'${f}': '${hashAsset(fs.readFileSync(path.join(ROOT, f)))}'`).join(', ')} };`;
const table = (name, files) => ` const ${name} = { ${files.map((f) => `'${f}': '${hashAsset(fs.readFileSync(path.join(ROOT, f)))}'`).join(', ')} };`;
function writeOwn() {
function writeOwn() {
for (const [file, { name, files }] of Object.entries(OWN)) {
for (const [file, { name, files }] of Object.entries(OWN)) {
const p = path.join(ROOT, file);
const p = path.join(ROOT, file);
const src = fs.readFileSync(p, 'utf8');
const src = fs.readFileSync(p, 'utf8');
- const re = new RegExp(`^ {4}const ${name} = \\{[^\\n]*\\};$`, 'm');
+ const re = new RegExp(`^( *)const ${name} = \\{[^\\n]*\\};$`, 'm');
if (!re.test(src)) throw new Error(`${file} has no ${name} line to update`);
if (!re.test(src)) throw new Error(`${file} has no ${name} line to update`);
- fs.writeFileSync(p, src.replace(re, table(name, files)));
+ fs.writeFileSync(p, src.replace(re, (m, indent) => indent + table(name, files).trimStart()));
}
}
}
}
const revs = () => Object.fromEntries(FILES.map((f) => [f, hashAsset(fs.readFileSync(path.join(ROOT, f)))]));
const revs = () => Object.fromEntries(FILES.map((f) => [f, hashAsset(fs.readFileSync(path.join(ROOT, f)))]));
@@ -16,7 +16,8 @@ const body = frame.updatesBody({ service: 'wiki' });
@@ -16,7 +16,8 @@ const body = frame.updatesBody({ service: 'wiki' });
assert.ok(body.includes('<h1>What shipped on OpenVibe.Wiki</h1>'));
assert.ok(body.includes('<h1>What shipped on OpenVibe.Wiki</h1>'));
assert.ok(body.includes('data-ov-shipped="log" data-service="wiki"'));
assert.ok(body.includes('data-ov-shipped="log" data-service="wiki"'));
assert.ok(body.includes('https://openvibe.network/updates?site=wiki'), 'the no-JavaScript fallback is the network log');
assert.ok(body.includes('https://openvibe.network/updates?site=wiki'), 'the no-JavaScript fallback is the network log');
-assert.strictEqual(frame.shippedScript(), '<script src="https://openvibe.network/shared/shipped.js" defer></script>');
+// By its content hash (4.138.0), so an updates page never runs a copy an edge kept from before a release.
+assert.match(frame.shippedScript(), /^<script src="https:\/\/openvibe\.network\/shared\/shipped\.js\?v=[0-9a-f]{12}" defer><\/script>$/);
const foot = frame.footer({ service: 'wiki', variant: 'full', updates: '/updates' });
const foot = frame.footer({ service: 'wiki', variant: 'full', updates: '/updates' });
assert.ok(foot.includes('data-ov-shipped="latest"') && foot.includes('href="/updates"'));
assert.ok(foot.includes('data-ov-shipped="latest"') && foot.includes('href="/updates"'));
@@ -23,4 +23,13 @@ for (const [file, { name, files }] of Object.entries(OWN)) {
@@ -23,4 +23,13 @@ for (const [file, { name, files }] of Object.entries(OWN)) {
}
}
const dock = fs.readFileSync(path.join(__dirname, '..', 'dock.js'), 'utf8');
const dock = fs.readFileSync(path.join(__dirname, '..', 'dock.js'), 'utf8');
assert.ok(dock.includes("sc.src = sibling(src) + (DOCK_REV[src] ? `?v=${DOCK_REV[src]}` : '');"), 'the Dock loads its siblings by hash');
assert.ok(dock.includes("sc.src = sibling(src) + (DOCK_REV[src] ? `?v=${DOCK_REV[src]}` : '');"), 'the Dock loads its siblings by hash');
-console.log('sibling revs: every script the navbar, the Dock and the notifications load goes by its content hash');
+// 4.138.0: the footer, Boost and the glyphs load theirs by hash too; the footer's shipped.js and the glyphs' brands.js
+// wait until the footer (or a logo) comes near the screen.
+const footer = fs.readFileSync(path.join(__dirname, '..', 'footer.js'), 'utf8');
+assert.ok(/if \(_cfg\.shipped !== false\) shippedWhenNear\(_mounted\);/.test(footer), 'the footer fetches shipped.js when it nears the screen');
+assert.ok(footer.includes("sc.src = src + (FOOTER_REV['shipped.js'] ? `?v=${FOOTER_REV['shipped.js']}` : '');"), 'and by its hash');
+const glyphsOut = fs.readFileSync(path.join(__dirname, '..', 'ov-glyphs.js'), 'utf8');
+assert.ok(/const BRANDS_REV = '[0-9a-f]{12}';/.test(glyphsOut) && glyphsOut.includes('waiting.add(el); wantBrands(el);'), 'brands.js by hash, when a logo nears the screen');
+const boost = fs.readFileSync(path.join(__dirname, '..', 'boost.js'), 'utf8');
+assert.ok(/BOOST_REV\['route-transition\.js'\]/.test(boost), 'route-transition.js by hash');
+console.log('sibling revs: every script the navbar, the Dock, the notifications, the footer, Boost and the glyphs load goes by its content hash');