tosijs-ui 1.6.6 → 1.6.7
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/dist/doc-browser.d.ts +15 -1
- package/dist/doc-browser.js +80 -31
- package/dist/doc-system/doc-system.d.ts +5 -0
- package/dist/doc-system/doc-system.js +89 -7
- package/dist/doc-system/site/orchestrator.js +20 -7
- package/dist/iife.js +42 -42
- package/dist/iife.js.map +5 -5
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/llms.txt +1 -1
- package/package.json +1 -1
package/dist/doc-browser.d.ts
CHANGED
|
@@ -46,8 +46,12 @@ export interface LinkItem {
|
|
|
46
46
|
* - 'query' (default, legacy): single-page app, links are `?filename`.
|
|
47
47
|
* - 'path': clean per-page URLs (`/slug/`), for the static pre-rendered site
|
|
48
48
|
* driven by <tosi-doc-system>. Requires a real page to exist at each path.
|
|
49
|
+
* - 'memory': self-contained — navigation never touches window.history/location
|
|
50
|
+
* and the instance ignores the page URL. For an embedded/nested browser (a
|
|
51
|
+
* live demo, a floating help panel). Drive it via `initialRoute` +
|
|
52
|
+
* `onRouteChange`, and the returned element's `.navigate(slug)` method.
|
|
49
53
|
*/
|
|
50
|
-
export type DocRoutingMode = 'query' | 'path';
|
|
54
|
+
export type DocRoutingMode = 'query' | 'path' | 'memory';
|
|
51
55
|
export interface DocBrowserOptions {
|
|
52
56
|
docs: Doc[];
|
|
53
57
|
context?: Record<string, any>;
|
|
@@ -56,6 +60,16 @@ export interface DocBrowserOptions {
|
|
|
56
60
|
navSize?: number;
|
|
57
61
|
minSize?: number;
|
|
58
62
|
routing?: DocRoutingMode;
|
|
63
|
+
/**
|
|
64
|
+
* Memory routing only: the slug to show first (instead of the page URL / first
|
|
65
|
+
* doc). Lets a host mount the browser already pointed at a specific doc.
|
|
66
|
+
*/
|
|
67
|
+
initialRoute?: string;
|
|
68
|
+
/**
|
|
69
|
+
* Memory routing only: called with the current doc's slug whenever in-app
|
|
70
|
+
* navigation happens, so a host can reflect it (e.g. to an attribute).
|
|
71
|
+
*/
|
|
72
|
+
onRouteChange?: (slug: string) => void;
|
|
59
73
|
/**
|
|
60
74
|
* Header-bar links. When provided, these replace the legacy `projectLinks` icon
|
|
61
75
|
* set in the header (each renders as an icon if `icon` names a known icon, else
|
package/dist/doc-browser.js
CHANGED
|
@@ -239,20 +239,37 @@ const testIndicatorStyleSpec = {
|
|
|
239
239
|
},
|
|
240
240
|
};
|
|
241
241
|
export function createDocBrowser(options) {
|
|
242
|
-
const { docs, context = {}, projectName = '', projectLinks = {}, navSize = 200, minSize = 600, routing = 'query', navbarLinks, contentElement, } = options;
|
|
242
|
+
const { docs, context = {}, projectName = '', projectLinks = {}, navSize = 200, minSize = 600, routing = 'query', initialRoute, onRouteChange, navbarLinks, contentElement, } = options;
|
|
243
|
+
// Memory routing is fully self-contained: it never reads or writes
|
|
244
|
+
// window.history/location, so an embedded or nested browser can't hijack the
|
|
245
|
+
// host page's URL (or recurse into it).
|
|
246
|
+
const memoryRouting = routing === 'memory';
|
|
243
247
|
// Initialize testStatus on all docs so tosi can track it
|
|
244
248
|
for (const doc of docs) {
|
|
245
249
|
doc.testStatus = undefined;
|
|
246
250
|
}
|
|
247
251
|
// Routing abstraction — keeps the legacy `?filename` SPA behavior the default,
|
|
248
252
|
// while letting <tosi-doc-system> drive clean `/slug/` URLs off the same docs.
|
|
249
|
-
|
|
250
|
-
const
|
|
251
|
-
const
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
253
|
+
// Both path and memory routing key off slugs; only legacy query routing doesn't.
|
|
254
|
+
const slugMap = routing === 'query' ? {} : buildSlugMap(docs);
|
|
255
|
+
const slugFor = (filename) => slugMap[filename] ?? filename;
|
|
256
|
+
const filenameForSlug = (slug) => {
|
|
257
|
+
for (const d of docs)
|
|
258
|
+
if (slugFor(d.filename) === slug)
|
|
259
|
+
return d.filename;
|
|
260
|
+
return '';
|
|
261
|
+
};
|
|
262
|
+
const hrefFor = (filename) => routing === 'query' ? `?${filename}` : pathForSlug(slugFor(filename));
|
|
263
|
+
const filenameFromLocation = () => {
|
|
264
|
+
// Memory routing ignores the page URL entirely — it starts at initialRoute.
|
|
265
|
+
if (memoryRouting)
|
|
266
|
+
return initialRoute ? filenameForSlug(initialRoute) : '';
|
|
267
|
+
return routing === 'path'
|
|
268
|
+
? filenameForPath(document.location.pathname, slugMap)
|
|
269
|
+
: document.location.search !== ''
|
|
270
|
+
? document.location.search.substring(1).split('&')[0]
|
|
271
|
+
: '';
|
|
272
|
+
};
|
|
256
273
|
const docName = filenameFromLocation() || docs[0]?.filename || 'README.md';
|
|
257
274
|
const currentDoc = docs.find((doc) => doc.filename === docName) || docs[0];
|
|
258
275
|
const { app } = tosi({
|
|
@@ -272,10 +289,13 @@ export function createDocBrowser(options) {
|
|
|
272
289
|
let backgroundTestsStarted = false;
|
|
273
290
|
let pagesWithTests = 0;
|
|
274
291
|
let pagesTested = 0;
|
|
275
|
-
// Set up global promise for scriptable browser integration
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
292
|
+
// Set up global promise for scriptable browser integration. A memory-routed
|
|
293
|
+
// (embedded) browser must not clobber the host page's global.
|
|
294
|
+
if (!memoryRouting) {
|
|
295
|
+
window.__docTestResults = new Promise((resolve) => {
|
|
296
|
+
testResultsResolve = resolve;
|
|
297
|
+
});
|
|
298
|
+
}
|
|
279
299
|
const updateDocTestStatus = (filename) => {
|
|
280
300
|
const results = pageTestResults[filename];
|
|
281
301
|
// Callback receives bare object, return is proxy - cast to work with both
|
|
@@ -378,9 +398,13 @@ export function createDocBrowser(options) {
|
|
|
378
398
|
},
|
|
379
399
|
onInput: filterDocs,
|
|
380
400
|
});
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
401
|
+
// Memory routing is decoupled from the page URL, so it ignores browser
|
|
402
|
+
// back/forward (the host owns history, if any).
|
|
403
|
+
if (!memoryRouting) {
|
|
404
|
+
window.addEventListener('popstate', () => {
|
|
405
|
+
navigateTo(filenameFromLocation());
|
|
406
|
+
});
|
|
407
|
+
}
|
|
384
408
|
const headerContent = [
|
|
385
409
|
button({
|
|
386
410
|
class: 'iconic',
|
|
@@ -459,7 +483,12 @@ export function createDocBrowser(options) {
|
|
|
459
483
|
padding: '0 1em',
|
|
460
484
|
overflow: 'hidden',
|
|
461
485
|
});
|
|
462
|
-
|
|
486
|
+
// Adoption is zero-flash hydration of a statically pre-rendered page: it only
|
|
487
|
+
// holds for the page's own path-routed instance, whose static `.doc-content`
|
|
488
|
+
// matches the initial doc. A memory-routed embed's initial doc
|
|
489
|
+
// (initialRoute/docs[0]) has no relation to whatever happens to sit inside the
|
|
490
|
+
// host element (often an empty placeholder), so always render it fresh.
|
|
491
|
+
let adoptInitialContent = contentElement !== undefined && !memoryRouting;
|
|
463
492
|
const showDoc = (doc) => {
|
|
464
493
|
if (adoptInitialContent) {
|
|
465
494
|
adoptInitialContent = false; // leave the pre-rendered HTML untouched
|
|
@@ -482,6 +511,19 @@ export function createDocBrowser(options) {
|
|
|
482
511
|
showDoc(doc);
|
|
483
512
|
refreshNav(true);
|
|
484
513
|
};
|
|
514
|
+
// User-initiated navigation: record the new location (history for path/query,
|
|
515
|
+
// the onRouteChange callback for memory) and then render the doc. Every nav
|
|
516
|
+
// click funnels through here so memory mode never touches window.history.
|
|
517
|
+
const go = (filename) => {
|
|
518
|
+
if (memoryRouting) {
|
|
519
|
+
onRouteChange?.(slugFor(filename));
|
|
520
|
+
}
|
|
521
|
+
else {
|
|
522
|
+
const href = hrefFor(filename);
|
|
523
|
+
window.history.pushState({ href }, '', href);
|
|
524
|
+
}
|
|
525
|
+
navigateTo(filename);
|
|
526
|
+
};
|
|
485
527
|
// ── Hierarchical nav (path routing) ───────────────────────────────────────
|
|
486
528
|
// Build nested <details> from the doc tree; current-highlight, test status,
|
|
487
529
|
// search visibility, and auto-open are applied imperatively by refreshNav.
|
|
@@ -512,12 +554,10 @@ export function createDocBrowser(options) {
|
|
|
512
554
|
// Use the href from the closure, not the event — the click can land on a
|
|
513
555
|
// child of the <a> (the localized label), so event.currentTarget/target
|
|
514
556
|
// isn't reliably the anchor.
|
|
515
|
-
const href = hrefFor(doc.filename);
|
|
516
557
|
const nav = event.target.closest('tosi-sidenav');
|
|
517
558
|
if (nav)
|
|
518
559
|
nav.contentVisible = true;
|
|
519
|
-
|
|
520
|
-
navigateTo(String(doc.filename));
|
|
560
|
+
go(String(doc.filename));
|
|
521
561
|
event.preventDefault();
|
|
522
562
|
const results = pageTestResults[doc.filename];
|
|
523
563
|
if (results && !results.passed) {
|
|
@@ -598,7 +638,7 @@ export function createDocBrowser(options) {
|
|
|
598
638
|
refreshNav(true);
|
|
599
639
|
return root;
|
|
600
640
|
};
|
|
601
|
-
const navContent = routing
|
|
641
|
+
const navContent = routing !== 'query'
|
|
602
642
|
? buildHierarchicalNav()
|
|
603
643
|
: div({
|
|
604
644
|
...navStyle,
|
|
@@ -613,12 +653,10 @@ export function createDocBrowser(options) {
|
|
|
613
653
|
bindDocLink: '^.filename',
|
|
614
654
|
bindTestStatus: '^.testStatus',
|
|
615
655
|
onClick(event) {
|
|
616
|
-
const anchor = event.target;
|
|
617
656
|
const doc = getListItem(event.target);
|
|
618
657
|
const nav = event.target.closest('tosi-sidenav');
|
|
619
658
|
nav.contentVisible = true;
|
|
620
|
-
|
|
621
|
-
navigateTo(String(doc.filename));
|
|
659
|
+
go(String(doc.filename));
|
|
622
660
|
event.preventDefault();
|
|
623
661
|
},
|
|
624
662
|
}, tosiLocalized({ bindText: '^.title' }))));
|
|
@@ -766,9 +804,7 @@ export function createDocBrowser(options) {
|
|
|
766
804
|
// Navigate to the page
|
|
767
805
|
const docObj = app.docs.find((d) => String(d.filename) === filename);
|
|
768
806
|
if (docObj) {
|
|
769
|
-
|
|
770
|
-
window.history.pushState({ href }, '', href);
|
|
771
|
-
navigateTo(filename);
|
|
807
|
+
go(filename);
|
|
772
808
|
// Scroll to failing test after render
|
|
773
809
|
setTimeout(() => {
|
|
774
810
|
const failedExample = document.querySelector('tosi-example.-test-failed');
|
|
@@ -825,9 +861,10 @@ export function createDocBrowser(options) {
|
|
|
825
861
|
lines.unshift(`**Summary: ${totalPassed} passed, ${totalFailed} failed**`, '');
|
|
826
862
|
return lines.join('\n');
|
|
827
863
|
}
|
|
828
|
-
// Detect if running as background test iframe
|
|
864
|
+
// Detect if running as background test iframe (never for an embedded browser —
|
|
865
|
+
// it shares the host page's URL but isn't the test target).
|
|
829
866
|
const searchParams = new URLSearchParams(window.location.search);
|
|
830
|
-
const isTestFrame = searchParams.get('_testMode') === '1';
|
|
867
|
+
const isTestFrame = !memoryRouting && searchParams.get('_testMode') === '1';
|
|
831
868
|
const testFrameFilename = isTestFrame ? filenameFromLocation() : null;
|
|
832
869
|
// Listen for test completion events
|
|
833
870
|
container.addEventListener('testcomplete', ((event) => {
|
|
@@ -960,8 +997,20 @@ export function createDocBrowser(options) {
|
|
|
960
997
|
}
|
|
961
998
|
}
|
|
962
999
|
};
|
|
963
|
-
// Start now if enabled, and watch for toggle
|
|
964
|
-
|
|
965
|
-
|
|
1000
|
+
// Start now if enabled, and watch for toggle. A memory-routed (embedded)
|
|
1001
|
+
// browser never spawns the test-runner iframes (they'd navigate the host site).
|
|
1002
|
+
if (!memoryRouting) {
|
|
1003
|
+
startBackgroundTests();
|
|
1004
|
+
testManager.enabled.observe(startBackgroundTests);
|
|
1005
|
+
}
|
|
1006
|
+
// Memory routing: let the host drive navigation programmatically (by slug) and
|
|
1007
|
+
// read the current slug back, so the browser can live in a floating panel etc.
|
|
1008
|
+
if (memoryRouting) {
|
|
1009
|
+
;
|
|
1010
|
+
container.navigate = (slug) => navigateTo(filenameForSlug(slug) || slug);
|
|
1011
|
+
Object.defineProperty(container, 'currentSlug', {
|
|
1012
|
+
get: () => slugFor(String(app.currentDoc.filename)),
|
|
1013
|
+
});
|
|
1014
|
+
}
|
|
966
1015
|
return container;
|
|
967
1016
|
}
|
|
@@ -5,6 +5,8 @@ export declare class TosiDocSystem extends Component {
|
|
|
5
5
|
docs: string;
|
|
6
6
|
config: string;
|
|
7
7
|
localized: string;
|
|
8
|
+
routing: string;
|
|
9
|
+
route: string;
|
|
8
10
|
accent: string;
|
|
9
11
|
background: string;
|
|
10
12
|
text: string;
|
|
@@ -13,6 +15,9 @@ export declare class TosiDocSystem extends Component {
|
|
|
13
15
|
content: null;
|
|
14
16
|
private corpus?;
|
|
15
17
|
private browser?;
|
|
18
|
+
private appliedRoute;
|
|
19
|
+
private suppressed;
|
|
20
|
+
private nestingDepth;
|
|
16
21
|
private prefs;
|
|
17
22
|
private stylesApplied;
|
|
18
23
|
private applyStyles;
|
|
@@ -31,6 +31,15 @@ language).
|
|
|
31
31
|
- `config` — inline JSON `{ projectName, projectLinks }` baked in at build time.
|
|
32
32
|
- `localized` — URL of a tab-separated translation table; when set, it powers the
|
|
33
33
|
settings menu's language picker.
|
|
34
|
+
- `routing` — `''` (default) drives the page with clean `/slug/` URLs. Set
|
|
35
|
+
`"memory"` for a **self-contained** instance whose navigation never touches the
|
|
36
|
+
page URL or history — for embedding the docs in a dialog, side panel, or
|
|
37
|
+
floating element. (A `<tosi-doc-system>` nested inside another one — e.g. the
|
|
38
|
+
live demo on this page — is forced to memory routing automatically, and a demo
|
|
39
|
+
nested two levels deep renders an inert placeholder instead of recursing.)
|
|
40
|
+
- `route` — memory routing only: the current doc's slug. Set it to navigate the
|
|
41
|
+
embedded browser to a doc; it's reflected back here as the user clicks around,
|
|
42
|
+
so a host can observe/bind it (e.g. to remember where a help panel was left).
|
|
34
43
|
- `accent` / `background` / `text` — base theme colors (most of the palette is
|
|
35
44
|
derived from `accent` via color math).
|
|
36
45
|
|
|
@@ -38,6 +47,13 @@ The element is light-DOM (no shadow root) so the pre-rendered markdown remains t
|
|
|
38
47
|
real, indexable page content until hydration replaces it with the live browser.
|
|
39
48
|
Live-example modules default to the `xinjs` / `xinjsui` IIFE globals; override by
|
|
40
49
|
setting the element's `context` property before it connects.
|
|
50
|
+
|
|
51
|
+
A self-contained, controllable embed (e.g. docs in a floating panel):
|
|
52
|
+
|
|
53
|
+
```html
|
|
54
|
+
<tosi-doc-system docs="/docs.json" routing="memory" route="data-table">
|
|
55
|
+
</tosi-doc-system>
|
|
56
|
+
```
|
|
41
57
|
*/
|
|
42
58
|
/*{ "parent": "Appendices" }*/
|
|
43
59
|
import { Component, StyleSheet, elements, tosi, vars } from 'tosijs';
|
|
@@ -47,7 +63,7 @@ import { docSystemStyleSpec } from './doc-system-styles';
|
|
|
47
63
|
import { icons } from '../icons';
|
|
48
64
|
import { popMenu } from '../menu';
|
|
49
65
|
import { i18n, setLocale, initLocalization } from '../localize';
|
|
50
|
-
const { button } = elements;
|
|
66
|
+
const { button, div } = elements;
|
|
51
67
|
const PREFS_KEY = 'tosi-doc-system-prefs';
|
|
52
68
|
export class TosiDocSystem extends Component {
|
|
53
69
|
static preferredTagName = 'tosi-doc-system';
|
|
@@ -55,6 +71,15 @@ export class TosiDocSystem extends Component {
|
|
|
55
71
|
docs: '/docs.json',
|
|
56
72
|
config: '',
|
|
57
73
|
localized: '',
|
|
74
|
+
// Routing mode. '' (default) → clean `/slug/` URLs that drive the page. Set
|
|
75
|
+
// 'memory' (or 'internal') for a self-contained instance that never touches
|
|
76
|
+
// the page URL — for embedding the docs in a panel, dialog, etc. A nested
|
|
77
|
+
// <tosi-doc-system> (inside a live example) is forced to 'memory' too.
|
|
78
|
+
routing: '',
|
|
79
|
+
// Memory routing only: the current doc slug. Set it to navigate the embedded
|
|
80
|
+
// browser; it's reflected back here as the user clicks around, so a host can
|
|
81
|
+
// bind/observe it (e.g. docs in a floating element).
|
|
82
|
+
route: '',
|
|
58
83
|
// Base theme colors — most of the palette is derived from `accent`.
|
|
59
84
|
accent: '',
|
|
60
85
|
background: '',
|
|
@@ -68,6 +93,27 @@ export class TosiDocSystem extends Component {
|
|
|
68
93
|
// The loaded corpus and the mounted browser; both also act as one-time guards.
|
|
69
94
|
corpus;
|
|
70
95
|
browser;
|
|
96
|
+
// Route-reflection bookkeeping (memory routing): the last slug applied to the
|
|
97
|
+
// browser, so external `route` changes navigate but internal navigation (which
|
|
98
|
+
// writes `route` back) doesn't re-navigate or loop.
|
|
99
|
+
appliedRoute = '';
|
|
100
|
+
suppressed = false;
|
|
101
|
+
// How many <tosi-doc-system> ancestors this element has, crossing shadow
|
|
102
|
+
// boundaries. 0 = the page's own; ≥1 = embedded in another (e.g. the live
|
|
103
|
+
// demo on the doc-system page). Used to force memory routing on nested
|
|
104
|
+
// instances and to stop runaway recursion (a doc page that demos itself).
|
|
105
|
+
nestingDepth() {
|
|
106
|
+
let depth = 0;
|
|
107
|
+
let node = this.parentNode;
|
|
108
|
+
while (node) {
|
|
109
|
+
if (node.nodeType === Node.ELEMENT_NODE &&
|
|
110
|
+
node.tagName === 'TOSI-DOC-SYSTEM') {
|
|
111
|
+
depth++;
|
|
112
|
+
}
|
|
113
|
+
node = node.parentNode || node.host || null;
|
|
114
|
+
}
|
|
115
|
+
return depth;
|
|
116
|
+
}
|
|
71
117
|
// User theme/locale preferences (persisted to localStorage).
|
|
72
118
|
prefs;
|
|
73
119
|
stylesApplied = false;
|
|
@@ -224,6 +270,15 @@ export class TosiDocSystem extends Component {
|
|
|
224
270
|
}
|
|
225
271
|
connectedCallback() {
|
|
226
272
|
super.connectedCallback();
|
|
273
|
+
// A doc page can demo the doc system itself. The page's browser (depth 0)
|
|
274
|
+
// and one embedded demo (depth 1) are fine, but a demo INSIDE that demo
|
|
275
|
+
// (depth ≥ 2) would recurse forever — so render an inert placeholder and
|
|
276
|
+
// stop. (Don't even fetch the corpus.)
|
|
277
|
+
if (this.nestingDepth() >= 2) {
|
|
278
|
+
this.suppressed = true;
|
|
279
|
+
this.replaceChildren(div({ class: 'doc-system-nested' }, '📖 nested doc-system preview (suppressed)'));
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
227
282
|
this.applyStyles();
|
|
228
283
|
this.initPrefs();
|
|
229
284
|
void this.initLocale();
|
|
@@ -253,8 +308,19 @@ export class TosiDocSystem extends Component {
|
|
|
253
308
|
}
|
|
254
309
|
render() {
|
|
255
310
|
super.render();
|
|
256
|
-
if (this.
|
|
311
|
+
if (this.suppressed || this.corpus === undefined)
|
|
312
|
+
return;
|
|
313
|
+
// Already mounted: the only live input is an external `route` change (memory
|
|
314
|
+
// routing), which navigates the embedded browser. The appliedRoute guard
|
|
315
|
+
// stops the browser's own write-back to `route` from re-navigating.
|
|
316
|
+
if (this.browser !== undefined) {
|
|
317
|
+
const navigate = this.browser.navigate;
|
|
318
|
+
if (navigate && this.route && this.route !== this.appliedRoute) {
|
|
319
|
+
this.appliedRoute = this.route;
|
|
320
|
+
navigate(this.route);
|
|
321
|
+
}
|
|
257
322
|
return;
|
|
323
|
+
}
|
|
258
324
|
let config = {};
|
|
259
325
|
if (this.config) {
|
|
260
326
|
try {
|
|
@@ -264,15 +330,28 @@ export class TosiDocSystem extends Component {
|
|
|
264
330
|
console.warn('<tosi-doc-system> ignoring invalid config attribute');
|
|
265
331
|
}
|
|
266
332
|
}
|
|
333
|
+
// Nested instances (a live demo of the doc system) must never drive the page
|
|
334
|
+
// URL — force memory routing. A top-level instance opts in via the attribute.
|
|
335
|
+
const nested = this.nestingDepth() >= 1;
|
|
336
|
+
const memory = nested || this.routing === 'memory' || this.routing === 'internal';
|
|
267
337
|
// Adopt the pre-rendered markdown for the landing page so it is hydrated in
|
|
268
338
|
// place, never re-rendered. createDocBrowser grafts this node into its content
|
|
269
339
|
// area; the static nav list (already crawled) is replaced by the reactive one.
|
|
270
340
|
const contentElement = this.querySelector('.doc-content') || undefined;
|
|
271
341
|
// Header-bar links are declared as a real, crawlable list in the static page.
|
|
272
342
|
const navbarLinks = this.parseLinks('.doc-navbar');
|
|
343
|
+
this.appliedRoute = this.route;
|
|
273
344
|
this.browser = createDocBrowser({
|
|
274
345
|
docs: this.corpus,
|
|
275
|
-
routing: 'path',
|
|
346
|
+
routing: memory ? 'memory' : 'path',
|
|
347
|
+
initialRoute: memory ? this.route || undefined : undefined,
|
|
348
|
+
onRouteChange: memory
|
|
349
|
+
? (slug) => {
|
|
350
|
+
// Record before reflecting so the resulting render() no-ops.
|
|
351
|
+
this.appliedRoute = slug;
|
|
352
|
+
this.route = slug;
|
|
353
|
+
}
|
|
354
|
+
: undefined,
|
|
276
355
|
context: this.context || {
|
|
277
356
|
tosijs: globalThis.xinjs,
|
|
278
357
|
'tosijs-ui': globalThis.xinjsui,
|
|
@@ -282,10 +361,13 @@ export class TosiDocSystem extends Component {
|
|
|
282
361
|
navbarLinks: navbarLinks.length ? navbarLinks : undefined,
|
|
283
362
|
contentElement,
|
|
284
363
|
});
|
|
285
|
-
// Add the settings (theme + language) menu
|
|
286
|
-
|
|
287
|
-
if (
|
|
288
|
-
header
|
|
364
|
+
// Add the settings (theme + language) menu — except in a nested demo, where
|
|
365
|
+
// a gear that retints the whole host page would be surprising.
|
|
366
|
+
if (!nested) {
|
|
367
|
+
const header = this.browser.querySelector('header');
|
|
368
|
+
if (header)
|
|
369
|
+
header.append(this.settingsButton());
|
|
370
|
+
}
|
|
289
371
|
this.replaceChildren(this.browser);
|
|
290
372
|
}
|
|
291
373
|
}
|
|
@@ -19,6 +19,9 @@ import { extractDocs } from './docs';
|
|
|
19
19
|
import { ensureSections } from './sections';
|
|
20
20
|
import { generateLlmsTxt } from './make-llms-txt';
|
|
21
21
|
import { generateSite } from './generate-site';
|
|
22
|
+
// Module specifiers contain regex metacharacters (`/`, `.`, `@`, …), so escape
|
|
23
|
+
// before interpolating into the require-shim detector below.
|
|
24
|
+
const escapeRegExp = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
22
25
|
export async function buildSite(config) {
|
|
23
26
|
const PROJECT_ROOT = './';
|
|
24
27
|
const PUBLIC = path.resolve(PROJECT_ROOT, config.outputDir ?? 'docs');
|
|
@@ -89,13 +92,6 @@ export async function buildSite(config) {
|
|
|
89
92
|
// consumer supplies (e.g. via staticDirs or an absolute URL).
|
|
90
93
|
const scriptName = (config.scriptUrl ?? '/iife.js').replace(/^\//, '');
|
|
91
94
|
if (config.bundleEntry) {
|
|
92
|
-
// IIFE externals become a synchronous require() shim that throws at
|
|
93
|
-
// module-eval ("Dynamic require of … is not supported") — the build still
|
|
94
|
-
// succeeds, so warn rather than fail silently.
|
|
95
|
-
if (config.bundleExternals && config.bundleExternals.length > 0) {
|
|
96
|
-
console.warn(`⚠️ bundleExternals in an IIFE bundle (${config.bundleExternals.join(', ')}): Bun emits a dynamic require() shim that throws at runtime. Load these\n` +
|
|
97
|
-
` via import() (dynamically, so the bundler keeps them async) or an importmap, not a static import.`);
|
|
98
|
-
}
|
|
99
95
|
const result = await Bun.build({
|
|
100
96
|
entrypoints: [config.bundleEntry],
|
|
101
97
|
outdir: DIST,
|
|
@@ -111,6 +107,23 @@ export async function buildSite(config) {
|
|
|
111
107
|
console.error(message);
|
|
112
108
|
return false;
|
|
113
109
|
}
|
|
110
|
+
// Warn only when an external actually compiled to a synchronous require()
|
|
111
|
+
// shim, which throws at module-eval ("Dynamic require of … is not
|
|
112
|
+
// supported"). That only happens for a *static* `import x from 'ext'`. A
|
|
113
|
+
// *dynamic* `import('ext')` is preserved as native `import("ext")` and
|
|
114
|
+
// resolves via the page's importmap — the recommended pattern, so it must
|
|
115
|
+
// stay silent. The config alone can't tell the two apart; the emitted
|
|
116
|
+
// bundle can, so we inspect the actual output.
|
|
117
|
+
if (config.bundleExternals && config.bundleExternals.length > 0) {
|
|
118
|
+
const js = (await Promise.all(result.outputs
|
|
119
|
+
.filter((o) => o.kind === 'entry-point' || o.path.endsWith('.js'))
|
|
120
|
+
.map((o) => o.text()))).join('\n');
|
|
121
|
+
const broken = config.bundleExternals.filter((ext) => new RegExp(`(?:__require|\\brequire)\\(\\s*["'\`]${escapeRegExp(ext)}["'\`]`).test(js));
|
|
122
|
+
if (broken.length > 0) {
|
|
123
|
+
console.warn(`⚠️ bundleExternals compiled to a synchronous require() shim that throws at runtime (${broken.join(', ')}). Reference these via a dynamic import() (kept async by the bundler)\n` +
|
|
124
|
+
` or an importmap, not a static import.`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
114
127
|
await $ `cp ${DIST}/${scriptName} ${PUBLIC}`.text();
|
|
115
128
|
const bundleFile = await Bun.file(`${DIST}/${scriptName}`).arrayBuffer();
|
|
116
129
|
// import.meta is illegal in a classic <script> — if it survived bundling
|