@bongos/core 1.24.49 → 1.24.50
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.bongos-core.json +28 -33
- package/docs/copy-inventory.md +80 -80
- package/docs/copy-registry.json +109 -109
- package/docs/module-api-changelog.md +2 -0
- package/docs/page-inventory.json +5 -4
- package/docs/page-readings.json +57 -57
- package/modules/status-ui/public/explore.html +14 -4
- package/modules/status-ui/public/index.html +25 -125
- package/modules/status-ui/public/index.states.json +3 -3
- package/modules/status-ui/public/status.js +53 -27
- package/modules/status-ui/public/style.css +638 -831
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +6 -0
- package/scripts/status-mirror-sync.js +44 -45
- package/src/bongos/serve-internal.js +8 -7
- package/src/module-api.js +1 -1
- package/tests/status_page_chrome.mjs +195 -0
- package/modules/status-ui/public/cursors/arrow.png +0 -0
- package/modules/status-ui/public/cursors/hand-pointer.png +0 -0
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.24.
|
|
3
|
+
"version": "1.24.50",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.24.
|
|
9
|
+
"version": "1.24.50",
|
|
10
10
|
"license": "AGPL-3.0-or-later",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"express": "^4.21.2",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.24.
|
|
3
|
+
"version": "1.24.50",
|
|
4
4
|
"description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"main": "src/platform-server.js",
|
package/release-notes.json
CHANGED
|
@@ -11265,5 +11265,11 @@
|
|
|
11265
11265
|
"id": "1005088",
|
|
11266
11266
|
"text": "The Cloud Bongos logo and the browser-tab icon now come from one file, so every page, the hall, the welcome screen, the gates and the fundraising page show exactly the same mark, and phones get a proper home-screen icon."
|
|
11267
11267
|
}
|
|
11268
|
+
],
|
|
11269
|
+
"1.24.50": [
|
|
11270
|
+
{
|
|
11271
|
+
"id": "1005089",
|
|
11272
|
+
"text": "The public status page now looks like the rest of Cloud Bongos: same colours, fonts, logo and panels, with a dark mode, and its off-site backup copy is built from the same source so the two always match."
|
|
11273
|
+
}
|
|
11268
11274
|
]
|
|
11269
11275
|
}
|
|
@@ -23,15 +23,19 @@
|
|
|
23
23
|
// mirror, and the publish REFUSES rather than shipping a
|
|
24
24
|
// page whose banner points nowhere.
|
|
25
25
|
//
|
|
26
|
-
// DESIGN: status.js + style.css +
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
//
|
|
26
|
+
// DESIGN: status.js + style.css + the shared sheets and brand files the page
|
|
27
|
+
// links (SHARED_ASSETS, at the same paths) are byte-identical between the server
|
|
28
|
+
// copy and the mirror (#737 parameterized the one differing constant via
|
|
29
|
+
// window.STATUS_API_BASE), so they are PURE COPIES. index.html is built from the
|
|
30
|
+
// SAME functions the server serves it with (task 1005089): the server's brand-token
|
|
31
|
+
// stamp (applyBrandTokens) and its theme splice (themeMarkup — the pack's palette,
|
|
32
|
+
// faces, font links and favicon), so the mirror cannot drift from the live page.
|
|
33
|
+
// On top of that it carries the three off-box additions, injected at stable anchors:
|
|
30
34
|
// 1. <script>window.STATUS_API_BASE = <domains.publicOrigin></script> (head)
|
|
31
35
|
// 2. <div id="offbox-status">…</div> (after <body>)
|
|
32
36
|
// 3. the availability-banner driver <script> (reads domains.statusDataUrl) (before status.js)
|
|
33
37
|
// If any anchor is missing the script THROWS — a silent mis-sync is worse than
|
|
34
|
-
// a failed run.
|
|
38
|
+
// a failed run. tests/status_mirror_sync.mjs holds the mirror equal to the live page.
|
|
35
39
|
//
|
|
36
40
|
// USAGE:
|
|
37
41
|
// node scripts/status-mirror-sync.js --repo-dir <path-to-mirror-checkout> [--push]
|
|
@@ -48,35 +52,25 @@ const { execFileSync } = require('node:child_process');
|
|
|
48
52
|
|
|
49
53
|
const REPO_ROOT = path.resolve(__dirname, '..');
|
|
50
54
|
const SRC = path.join(REPO_ROOT, 'modules', 'status-ui', 'public');
|
|
51
|
-
const { branding, clientBranding
|
|
52
|
-
const {
|
|
53
|
-
|
|
54
|
-
//
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
// the
|
|
58
|
-
//
|
|
59
|
-
|
|
60
|
-
|
|
55
|
+
const { branding, clientBranding } = require('../src/branding');
|
|
56
|
+
const { stampHtml } = require('../src/asset-version');
|
|
57
|
+
// The server's own serving transforms: the mirror is built with them, not with a
|
|
58
|
+
// copy of them, so the off-box page and the live page are one page (task 1005089).
|
|
59
|
+
const { applyBrandTokens, themeMarkup } = require('../src/bongos/serve-internal');
|
|
60
|
+
|
|
61
|
+
// The shared files the page links from other modules, published at the SAME paths
|
|
62
|
+
// so the page's root-absolute hrefs resolve on the mirror's host as on the server.
|
|
63
|
+
const SHARED_ASSETS = [
|
|
64
|
+
'modules/ui-design/public/tokens.css',
|
|
65
|
+
'modules/hall-ui/public/controls.css',
|
|
66
|
+
'modules/ui-design/public/brand',
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
// Stamp instance-identity tokens into the mirror HTML: the server's applyBrandTokens
|
|
70
|
+
// (task 1705, ADR 0062 §3), with every origin INJECTED ('' when absent) so nothing is
|
|
71
|
+
// read ambiently and the transform stays pure and testable.
|
|
61
72
|
function brandTokens(html, brand = {}, publicOrigin = '', buildersOrigin = '') {
|
|
62
|
-
|
|
63
|
-
const product = (brand.identity && brand.identity.productName) || '';
|
|
64
|
-
const tagline = (brand.copy && brand.copy.tagline) || '';
|
|
65
|
-
const cur = (brand.currency && brand.currency.label) || 'credits';
|
|
66
|
-
const Cur = cur ? cur.charAt(0).toUpperCase() + cur.slice(1) : cur;
|
|
67
|
-
const company = (brand.identity && brand.identity.company) || {};
|
|
68
|
-
const esc = escapeHtml; // the one server escaper (task 1004876)
|
|
69
|
-
return applyBrandConditionals(html, { companyName: company.name, companySite: company.site, companyTagline: company.tagline })
|
|
70
|
-
.split('{{worldName}}').join(esc(world))
|
|
71
|
-
.split('{{productName}}').join(esc(product))
|
|
72
|
-
.split('{{Currency}}').join(esc(Cur))
|
|
73
|
-
.split('{{currency}}').join(esc(cur))
|
|
74
|
-
.split('{{tagline}}').join(esc(tagline))
|
|
75
|
-
.split('{{companyName}}').join(esc(company.name))
|
|
76
|
-
.split('{{companySite}}').join(esc(company.site))
|
|
77
|
-
.split('{{companyTagline}}').join(esc(company.tagline))
|
|
78
|
-
.split('{{publicOrigin}}').join(esc(publicOrigin || ''))
|
|
79
|
-
.split('{{buildersOrigin}}').join(esc(buildersOrigin || publicOrigin || ''));
|
|
73
|
+
return applyBrandTokens(html, brand || {}, publicOrigin || '', buildersOrigin || publicOrigin || '', '', '', '');
|
|
80
74
|
}
|
|
81
75
|
|
|
82
76
|
// ---- the three off-box index.html injections -------------------------------
|
|
@@ -152,7 +146,9 @@ function buildMirrorIndex({ srcHtml, apiBase, statusDataUrl, brand }) {
|
|
|
152
146
|
if (html.includes('offbox-status')) {
|
|
153
147
|
throw new Error('modules/status-ui/public/index.html already contains offbox markup — the source should be the droplet (no-banner) copy. Aborting.');
|
|
154
148
|
}
|
|
155
|
-
|
|
149
|
+
// The off-box globals, then the pack's theme exactly as the server splices it —
|
|
150
|
+
// last in <head>, after the page's own sheets, so the pack wins here as it does live.
|
|
151
|
+
html = injectOnce(html, '</head>', headInject(apiBase) + brandingInject(brand) + themeMarkup((brand || {}).theme), 'before');
|
|
156
152
|
html = injectOnce(html, '<body>', BANNER_DIV, 'after');
|
|
157
153
|
// Insert the driver right before the main status.js script tag.
|
|
158
154
|
const scriptAnchor = '<script src="/status.js';
|
|
@@ -188,17 +184,11 @@ if (require.main === module) {
|
|
|
188
184
|
fs.copyFileSync(path.join(SRC, rel), path.join(repoDir, rel));
|
|
189
185
|
console.error(` copied ${rel}`);
|
|
190
186
|
};
|
|
191
|
-
const copyDir = (rel) => {
|
|
192
|
-
const from = path.join(SRC, rel), to = path.join(repoDir, rel);
|
|
193
|
-
if (!fs.existsSync(from)) return;
|
|
194
|
-
fs.rmSync(to, { recursive: true, force: true });
|
|
195
|
-
fs.cpSync(from, to, { recursive: true });
|
|
196
|
-
console.error(` copied ${rel}/`);
|
|
197
|
-
};
|
|
198
187
|
|
|
199
188
|
console.error('[status-mirror-sync] building mirror from modules/status-ui/public/ …');
|
|
200
189
|
const indexHtml = buildMirrorIndex({
|
|
201
|
-
|
|
190
|
+
// the content-hash ?v= stamp the server serves the index with (#521)
|
|
191
|
+
srcHtml: stampHtml(fs.readFileSync(path.join(SRC, 'index.html'), 'utf8'), { assetDir: SRC, urlPrefix: '/' }),
|
|
202
192
|
apiBase: b.domains.publicOrigin,
|
|
203
193
|
statusDataUrl: b.domains.statusDataUrl,
|
|
204
194
|
brand: clientBranding(),
|
|
@@ -207,12 +197,21 @@ if (require.main === module) {
|
|
|
207
197
|
console.error(' wrote index.html (with off-box injections)');
|
|
208
198
|
copyFile('status.js');
|
|
209
199
|
copyFile('style.css');
|
|
210
|
-
|
|
200
|
+
// the retired bronze cursors (task 1005089) leave the mirror too
|
|
201
|
+
fs.rmSync(path.join(repoDir, 'cursors'), { recursive: true, force: true });
|
|
202
|
+
for (const rel of SHARED_ASSETS) {
|
|
203
|
+
const from = path.join(REPO_ROOT, rel), to = path.join(repoDir, rel);
|
|
204
|
+
fs.rmSync(to, { recursive: true, force: true });
|
|
205
|
+
fs.mkdirSync(path.dirname(to), { recursive: true });
|
|
206
|
+
fs.cpSync(from, to, { recursive: true });
|
|
207
|
+
console.error(` copied ${rel}`);
|
|
208
|
+
}
|
|
211
209
|
|
|
212
210
|
// ---- commit + (optionally) push -------------------------------------------
|
|
213
211
|
|
|
214
212
|
const git = (args) => execFileSync('git', ['-C', repoDir, ...args], { encoding: 'utf8' });
|
|
215
|
-
git(['add', 'index.html', 'status.js', 'style.css', '
|
|
213
|
+
git(['add', '-A', '--', 'index.html', 'status.js', 'style.css', 'modules']);
|
|
214
|
+
git(['rm', '-r', '-q', '--cached', '--ignore-unmatch', 'cursors']);
|
|
216
215
|
const status = git(['status', '--porcelain']);
|
|
217
216
|
if (!status.trim()) {
|
|
218
217
|
console.error('[status-mirror-sync] mirror already up to date — nothing to publish.');
|
|
@@ -228,4 +227,4 @@ if (require.main === module) {
|
|
|
228
227
|
console.error(`[status-mirror-sync] published. The Pages host rebuilds ${b.domains.statusOrigin || 'the mirror'} in ~1 min.`);
|
|
229
228
|
}
|
|
230
229
|
|
|
231
|
-
module.exports = { buildMirrorIndex, injectOnce, brandTokens };
|
|
230
|
+
module.exports = { buildMirrorIndex, injectOnce, brandTokens, SHARED_ASSETS };
|
|
@@ -835,15 +835,16 @@ function mountInternalSurfaces(app) {
|
|
|
835
835
|
const surface = surfaceOf(req);
|
|
836
836
|
if (!surface) return next();
|
|
837
837
|
// /api/* already handled above; everything else on a surface host serves from
|
|
838
|
-
// the module's static dir.
|
|
839
|
-
//
|
|
840
|
-
// static handler.
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
838
|
+
// the module's static dir. Each page ("/" is the index, "/explore[.html]" its
|
|
839
|
+
// sibling; task 1005089) comes from the stamped + branded in-memory copy (asset
|
|
840
|
+
// ?v= = content hash); everything else falls through to the on-disk static handler.
|
|
841
|
+
// The map's keys are fixed at boot (the dir's own .html files), so a slug can only name a shipped page.
|
|
842
|
+
const pm = /^\/(?:([a-z0-9-]+)(?:\.html)?)?$/i.exec(req.path);
|
|
843
|
+
const page = pm && `${pm[1] || 'index'}.html`;
|
|
844
|
+
if ((req.method === 'GET' || req.method === 'HEAD') && page && surface.html[page]) {
|
|
844
845
|
res.set('Cache-Control', 'public, max-age=60');
|
|
845
846
|
res.type('html');
|
|
846
|
-
return res.send(surface.html[
|
|
847
|
+
return res.send(surface.html[page]);
|
|
847
848
|
}
|
|
848
849
|
return surface.static(req, res, next);
|
|
849
850
|
});
|
package/src/module-api.js
CHANGED
|
@@ -81,7 +81,7 @@ const { sleep } = require('./sleep');
|
|
|
81
81
|
// MAJOR (see allowBoxScope below): passes the request through untouched.
|
|
82
82
|
function deprecatedNoopMiddleware(_req, _res, next) { next(); }
|
|
83
83
|
|
|
84
|
-
const CORE_VERSION = '1.24.
|
|
84
|
+
const CORE_VERSION = '1.24.50'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
85
85
|
|
|
86
86
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
87
87
|
// Usage: const log = api.logger('discord'); log.info('mounted');
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
// tests/status_page_chrome.mjs — the status page on the chrome world (task 1005089,
|
|
2
|
+
// goal 1000138; DESIGN.md "Naming": the owner moved the status page onto this world,
|
|
3
|
+
// keeping its fixed status vocabulary as the hall does).
|
|
4
|
+
//
|
|
5
|
+
// What this pins, DB-free and browser-free:
|
|
6
|
+
// * nothing of the retired Greek ledger look survives in the surface: no parchment
|
|
7
|
+
// colours, no Cinzel / EB Garamond, no bronze cursors, no amphora or laurel
|
|
8
|
+
// sprite, no double-ruled frame;
|
|
9
|
+
// * every page title carries the product name from the branding pack;
|
|
10
|
+
// * the sheet declares no page :root, writes its dark tier twice byte-identical,
|
|
11
|
+
// and carries the hall's status vocabulary value for value;
|
|
12
|
+
// * explore is served through the branding injection, so it loads the pack's faces;
|
|
13
|
+
// * the off-box mirror IS the live page: the server's response for "/" on the
|
|
14
|
+
// status host, and the mirror's index built from the same source, are equal once
|
|
15
|
+
// each side's own injections are taken out — and every file the page links is one
|
|
16
|
+
// the mirror publishes.
|
|
17
|
+
//
|
|
18
|
+
// Run: node --test tests/status_page_chrome.mjs
|
|
19
|
+
|
|
20
|
+
import { test } from 'node:test';
|
|
21
|
+
import assert from 'node:assert/strict';
|
|
22
|
+
import fs from 'node:fs';
|
|
23
|
+
import http from 'node:http';
|
|
24
|
+
import path from 'node:path';
|
|
25
|
+
import { createRequire } from 'node:module';
|
|
26
|
+
import { fileURLToPath } from 'node:url';
|
|
27
|
+
|
|
28
|
+
const require = createRequire(import.meta.url);
|
|
29
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
30
|
+
const DIR = path.join(ROOT, 'modules', 'status-ui', 'public');
|
|
31
|
+
const read = (...p) => fs.readFileSync(path.join(ROOT, ...p), 'utf8').replace(/\r\n/g, '\n');
|
|
32
|
+
const decomment = (s) => s.replace(/\/\*[\s\S]*?\*\//g, '');
|
|
33
|
+
const CSS = read('modules', 'status-ui', 'public', 'style.css');
|
|
34
|
+
const CODE = decomment(CSS);
|
|
35
|
+
|
|
36
|
+
function surfaceFiles(dir = DIR, out = []) {
|
|
37
|
+
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
38
|
+
const p = path.join(dir, e.name);
|
|
39
|
+
if (e.isDirectory()) surfaceFiles(p, out);
|
|
40
|
+
else if (/\.(html|css|js)$/.test(e.name)) out.push(p);
|
|
41
|
+
}
|
|
42
|
+
return out;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// The retired look, as literals. The parchment hexes are the old sheet's :root.
|
|
46
|
+
const RETIRED = [
|
|
47
|
+
/Cinzel/i, /Garamond/i, /cursors\//, /hand-pointer\.png/, /sigil-amphora/, /orn-(laurel|column|coin|scroll|map|diamond)/,
|
|
48
|
+
/#ece0c2|#f6ecd2|#ddc99c|#e3d0a4|#2a2110|#6e5b3a|#735f3d|#8b6f2c|#b89a55|#4a7d5c|#2f5a3f|#2d6e7e|#a85530/i,
|
|
49
|
+
/rgba\(\s*(139, 111, 44|60, 40, 10|42, 33, 16|243, 230, 198)/,
|
|
50
|
+
/upper-roman/,
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
test('nothing of the Greek ledger look is left in the status surface', () => {
|
|
54
|
+
const hits = [];
|
|
55
|
+
for (const f of surfaceFiles()) {
|
|
56
|
+
const text = fs.readFileSync(f, 'utf8');
|
|
57
|
+
for (const re of RETIRED) if (re.test(text)) hits.push(`${path.relative(ROOT, f)}: ${re}`);
|
|
58
|
+
}
|
|
59
|
+
assert.deepEqual(hits, []);
|
|
60
|
+
assert.equal(fs.existsSync(path.join(DIR, 'cursors')), false, 'the bronze cursors are gone');
|
|
61
|
+
// no double-ruled frame: a panel is one hairline, no inset rule, no shadow
|
|
62
|
+
const panel = CODE.match(/\n\.tablet \{([^}]*)\}/);
|
|
63
|
+
assert.ok(panel, 'the panel rule exists');
|
|
64
|
+
assert.match(panel[1], /border: var\(--line\) solid var\(--rule-soft\);/);
|
|
65
|
+
assert.match(panel[1], /border-radius: var\(--ds-radius-panel\);/);
|
|
66
|
+
assert.doesNotMatch(panel[1], /box-shadow/);
|
|
67
|
+
assert.doesNotMatch(CODE, /\.tablet::(before|after)/, 'no carved corner ornaments');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test('every page title carries the product name from the pack', () => {
|
|
71
|
+
const pages = fs.readdirSync(DIR).filter((f) => f.endsWith('.html'));
|
|
72
|
+
assert.ok(pages.length >= 2);
|
|
73
|
+
for (const p of pages) {
|
|
74
|
+
const title = fs.readFileSync(path.join(DIR, p), 'utf8').match(/<title>([^<]*)<\/title>/);
|
|
75
|
+
assert.ok(title, `${p} has a title`);
|
|
76
|
+
assert.match(title[1], /\{\{productName\}\}/, `${p}: the title names the product`);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('the page draws the shared mark and favicon, and names no face of its own', () => {
|
|
81
|
+
const html = read('modules', 'status-ui', 'public', 'index.html');
|
|
82
|
+
assert.ok(html.includes('<use href="/modules/ui-design/public/brand/mark.svg#mark"/>'), 'the top bar draws the one mark by reference');
|
|
83
|
+
assert.ok(html.includes('<link rel="icon" href="/modules/ui-design/public/brand/favicon.svg"'));
|
|
84
|
+
assert.doesNotMatch(html, /fonts\.googleapis\.com/, 'the faces come from the pack splice, not the page');
|
|
85
|
+
assert.ok(html.indexOf('/modules/hall-ui/public/controls.css') > html.indexOf('/modules/ui-design/public/tokens.css'), 'the shared controls follow the scales');
|
|
86
|
+
assert.ok(html.indexOf('/modules/hall-ui/public/controls.css') < html.indexOf('href="/style.css'), 'and precede the page sheet');
|
|
87
|
+
// exactly one header (the top bar), one main, one h1
|
|
88
|
+
assert.equal((html.match(/<header\b/g) || []).length, 1);
|
|
89
|
+
assert.equal((html.match(/<main\b/g) || []).length, 1);
|
|
90
|
+
assert.equal((html.match(/<h1\b/g) || []).length, 1);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test('the sheet declares no page :root, and its dark tier is written twice, byte-identical', () => {
|
|
94
|
+
assert.doesNotMatch(CODE, /(^|[\s,}]):root\b/, 'no :root — the pack splices the only one');
|
|
95
|
+
assert.match(CODE, /:where\(html\) \{/, 'the fallback contract tier sits at zero specificity');
|
|
96
|
+
const switchBody = CODE.match(/\nhtml\[data-theme="dark"\] \{([^}]*)\}/);
|
|
97
|
+
const mediaBody = CODE.match(/@media \(prefers-color-scheme: dark\) \{\s*html:not\(\[data-theme\]\) \{([^}]*)\}/);
|
|
98
|
+
assert.ok(switchBody && mediaBody, 'both dark twins exist');
|
|
99
|
+
const norm = (s) => s.split('\n').map((l) => l.trim()).filter(Boolean).join('\n');
|
|
100
|
+
assert.equal(norm(mediaBody[1]), norm(switchBody[1]));
|
|
101
|
+
// the dark tier re-pins no accent (the Unpinned Accent Rule)
|
|
102
|
+
assert.doesNotMatch(switchBody[1], /--(accent|accent-deep|sea|terracotta):/);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test('the status vocabulary is the hall\'s, value for value, in both modes', () => {
|
|
106
|
+
const hall = decomment(read('modules', 'hall-ui', 'public', 'style.css'));
|
|
107
|
+
const NAMES = ['--ok', '--ok-weak', '--warn', '--warn-weak', '--danger', '--danger-weak', '--info', '--info-weak'];
|
|
108
|
+
const valuesIn = (body) => Object.fromEntries(NAMES.map((n) => {
|
|
109
|
+
const m = body.match(new RegExp(`\\n\\s*${n}:\\s*([^;]+);`));
|
|
110
|
+
assert.ok(m, `${n} is declared`);
|
|
111
|
+
return [n, m[1].trim()];
|
|
112
|
+
}));
|
|
113
|
+
const lightOf = (src, open) => { const at = src.indexOf(open); assert.ok(at > -1, open); return src.slice(at, src.indexOf('}', at)); };
|
|
114
|
+
assert.deepEqual(valuesIn(lightOf(CODE, '\nhtml {')), valuesIn(lightOf(hall, '\n:root {')));
|
|
115
|
+
assert.deepEqual(valuesIn(lightOf(CODE, '\nhtml[data-theme="dark"] {')), valuesIn(lightOf(hall, '\nhtml[data-theme="dark"] {')));
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// ── served ────────────────────────────────────────────────────────────────────
|
|
119
|
+
// node:http, not fetch: fetch silently drops a Host override (tests/module_asset_subdomain.mjs).
|
|
120
|
+
function get(port, p, host) {
|
|
121
|
+
return new Promise((resolve, reject) => {
|
|
122
|
+
const req = http.request({ host: '127.0.0.1', port, path: p, headers: { Host: host } }, (res) => {
|
|
123
|
+
let body = '';
|
|
124
|
+
res.setEncoding('utf8');
|
|
125
|
+
res.on('data', (c) => { body += c; });
|
|
126
|
+
res.on('end', () => resolve({ status: res.statusCode, body }));
|
|
127
|
+
});
|
|
128
|
+
req.on('error', reject);
|
|
129
|
+
req.end();
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
async function withServer(fn) {
|
|
133
|
+
const express = require('express');
|
|
134
|
+
const { mountInternalSurfaces } = require('../src/bongos/serve-internal.js');
|
|
135
|
+
const app = express();
|
|
136
|
+
mountInternalSurfaces(app);
|
|
137
|
+
const server = await new Promise((resolve) => { const s = app.listen(0, () => resolve(s)); });
|
|
138
|
+
try { return await fn(server.address().port); } finally { await new Promise((r) => server.close(r)); }
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
test('explore is served through the branding injection: the pack\'s faces, the product name', async () => {
|
|
142
|
+
const { clientBranding } = require('../src/branding.js');
|
|
143
|
+
const brand = clientBranding();
|
|
144
|
+
await withServer(async (port) => {
|
|
145
|
+
for (const p of ['/explore', '/explore.html']) {
|
|
146
|
+
const r = await get(port, p, 'status.example.com');
|
|
147
|
+
assert.equal(r.status, 200, p);
|
|
148
|
+
// the href is attribute-escaped on the way in (themeMarkup), so & arrives as &
|
|
149
|
+
for (const href of brand.theme.fonts.links) assert.ok(r.body.includes(`<link rel="stylesheet" href="${href.replace(/&/g, '&')}">`), `${p} loads the pack's faces`);
|
|
150
|
+
assert.ok(r.body.includes(`<title>Explore — projects on the map · ${brand.identity.productName}</title>`), `${p} names the product`);
|
|
151
|
+
assert.doesNotMatch(r.body, /\{\{productName\}\}/);
|
|
152
|
+
}
|
|
153
|
+
// an asset still comes off disk
|
|
154
|
+
assert.equal((await get(port, '/explore.js', 'status.example.com')).status, 200);
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test('the off-box mirror is the live page, and publishes every file the page links', async () => {
|
|
159
|
+
const mirror = require('../scripts/status-mirror-sync.js');
|
|
160
|
+
const { stampHtml } = require('../src/asset-version.js');
|
|
161
|
+
const { clientBranding, branding } = require('../src/branding.js');
|
|
162
|
+
const brand = clientBranding();
|
|
163
|
+
const src = stampHtml(read('modules', 'status-ui', 'public', 'index.html'), { assetDir: DIR, urlPrefix: '/' });
|
|
164
|
+
const apiBase = branding().domains.publicOrigin || 'https://origin.example.test';
|
|
165
|
+
const built = mirror.buildMirrorIndex({ srcHtml: src, apiBase, statusDataUrl: 'https://data.example.test/status.json', brand });
|
|
166
|
+
|
|
167
|
+
const live = await withServer(async (port) => (await get(port, '/', 'status.example.com')).body);
|
|
168
|
+
assert.ok(live.includes(brand.theme.fonts.links[0].replace(/&/g, '&')), 'the live page splices the pack');
|
|
169
|
+
|
|
170
|
+
// each side minus its own injections: the server's client bundle + globals, and the
|
|
171
|
+
// mirror's three off-box additions + its globals
|
|
172
|
+
const liveCore = live
|
|
173
|
+
.replace(/<script src="\/lib\/bongos-client\.js[^"]*"><\/script>/, '')
|
|
174
|
+
.replace(/<script>window\.__BRANDING__=[\s\S]*?<\/script>/, '');
|
|
175
|
+
const mirrorCore = built
|
|
176
|
+
.replace(/ {2}<!-- #737: off-box mirror runs cross-origin against the droplet\. -->\n {2}<script>window\.STATUS_API_BASE = [^\n]*<\/script>\n {2}<script>window\.__BRANDING__ = [^\n]*<\/script>\n/, '')
|
|
177
|
+
.replace(/\n<!-- Off-box availability banner \(ADR 0029\)[\s\S]*?<div id="offbox-status" role="status" aria-live="polite"><\/div>\n/, '')
|
|
178
|
+
.replace(/<!-- Off-box availability banner driver \(ADR 0029\)[\s\S]*?<\/script>\n/, '');
|
|
179
|
+
// the origin token is the one value the two resolve from different places on a
|
|
180
|
+
// checkout with no domains; normalise it before comparing
|
|
181
|
+
const norm = (s) => s.split(apiBase).join('{{origin}}');
|
|
182
|
+
assert.equal(norm(mirrorCore), norm(liveCore));
|
|
183
|
+
|
|
184
|
+
// every root-absolute file the page links is published: its own files are copied
|
|
185
|
+
// from the surface dir, the shared ones at the same path
|
|
186
|
+
const refs = [...src.matchAll(/\b(?:href|src)="(\/[^"?#]+)/g)].map((m) => m[1]).filter((u) => !u.startsWith('//'));
|
|
187
|
+
assert.ok(refs.length >= 5);
|
|
188
|
+
for (const u of refs) {
|
|
189
|
+
const rel = u.slice(1).replace(/#.*$/, '');
|
|
190
|
+
const own = ['status.js', 'style.css'].includes(rel);
|
|
191
|
+
const shared = mirror.SHARED_ASSETS.some((a) => rel === a || rel.startsWith(`${a}/`));
|
|
192
|
+
assert.ok(own || shared, `the mirror publishes ${u}`);
|
|
193
|
+
assert.ok(fs.existsSync(path.join(own ? DIR : ROOT, rel)), `${u} exists to publish`);
|
|
194
|
+
}
|
|
195
|
+
});
|
|
Binary file
|
|
Binary file
|