tosijs-ui 1.7.1 → 1.7.3
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/CHANGELOG.md +59 -0
- package/dist/code-editor-cm.js +5 -1
- package/dist/color-input.d.ts +0 -1
- package/dist/color-input.js +24 -10
- package/dist/doc-browser.js +4 -2
- package/dist/doc-system/doc-system-styles.d.ts +2 -0
- package/dist/doc-system/doc-system-styles.js +15 -1
- package/dist/doc-system/site/check-examples-cli.js +5 -3
- package/dist/doc-system/site/check-examples.d.ts +13 -0
- package/dist/doc-system/site/check-examples.js +26 -10
- package/dist/doc-system/site/dev-server.d.ts +11 -0
- package/dist/doc-system/site/dev-server.js +15 -2
- package/dist/doc-system/site/generate-site.d.ts +18 -0
- package/dist/doc-system/site/generate-site.js +42 -10
- package/dist/doc-system/site/orchestrator.js +15 -2
- package/dist/doc-system/site/site-config.d.ts +10 -3
- package/dist/form.d.ts +0 -1
- package/dist/form.js +35 -21
- package/dist/hydrate/{ajs-language-h1x0ydap.js → ajs-language-qm5w38ae.js} +3 -3
- package/dist/hydrate/{ajs-language-h1x0ydap.js.map → ajs-language-qm5w38ae.js.map} +1 -1
- package/dist/hydrate/{code-editor-cm-2hgj9002.js → code-editor-cm-95n31m4f.js} +8 -8
- package/dist/hydrate/{code-editor-cm-2hgj9002.js.map → code-editor-cm-95n31m4f.js.map} +3 -3
- package/dist/hydrate/hydrate.js +82 -82
- package/dist/hydrate/hydrate.js.map +15 -14
- package/dist/hydrate/{index-nsmyq9mv.js → index-0ps8jzdn.js} +3 -3
- package/dist/hydrate/{index-nsmyq9mv.js.map → index-0ps8jzdn.js.map} +1 -1
- package/dist/hydrate/{index-iife-64z9yngn.js → index-iife-415dypjp.js} +3 -3
- package/dist/hydrate/{index-iife-64z9yngn.js.map → index-iife-415dypjp.js.map} +1 -1
- package/dist/hydrate/index-iife-kwhe0yt8.js +27 -0
- package/dist/hydrate/{index-iife-5wzyvyzm.js.map → index-iife-kwhe0yt8.js.map} +2 -2
- package/dist/hydrate/{index-iife-x2h4f6fc.js → index-iife-ype8sch3.js} +3 -3
- package/dist/hydrate/{index-iife-x2h4f6fc.js.map → index-iife-ype8sch3.js.map} +1 -1
- package/dist/hydrate/{tjs-import-resolver-akt3k27z.js → tjs-import-resolver-s4z0w296.js} +2 -2
- package/dist/iife.js +114 -114
- package/dist/iife.js.map +16 -15
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/live-example/code-transform.d.ts +14 -0
- package/dist/live-example/code-transform.js +20 -2
- package/dist/live-example/component.d.ts +32 -52
- package/dist/live-example/component.js +44 -51
- package/dist/live-example/styles.d.ts +30 -51
- package/dist/live-example/styles.js +65 -50
- package/dist/live-example/types.d.ts +2 -2
- package/dist/pocket-bar.d.ts +99 -0
- package/dist/pocket-bar.js +298 -0
- package/dist/pop-float.d.ts +1 -1
- package/dist/pop-float.js +13 -7
- package/dist/segmented.d.ts +0 -1
- package/dist/segmented.js +38 -20
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/llms.txt +3 -3
- package/package.json +3 -3
- package/dist/bp-loader.d.ts +0 -1
- package/dist/bp-loader.js +0 -26
- package/dist/hydrate/index-iife-5wzyvyzm.js +0 -27
- /package/dist/hydrate/{tjs-import-resolver-akt3k27z.js.map → tjs-import-resolver-s4z0w296.js.map} +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,64 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.7.3
|
|
4
|
+
|
|
5
|
+
- **New `<tosi-pocket-bar>` component.** A "pocket buttonbar": a single translucent icon at
|
|
6
|
+
rest that expands into a bar of slotted icon-button controls on hover/tap, then collapses
|
|
7
|
+
again. `direction` (the full `FloatPosition` vocabulary, `auto` by default) determines both
|
|
8
|
+
layout and growth direction; the handle stays put and toggles the bar; touch counts as hover.
|
|
9
|
+
Built as a thin wrapper around `positionFloat`.
|
|
10
|
+
- **The live-example toolbar is now a pocket bar**, pinned to the top-right of the preview (so
|
|
11
|
+
it never sits over the code editors in side-by-side mode). The collapsed handle carries the
|
|
12
|
+
example's pass/fail color, "run tests" is a checkbox that greys when off, and the bar stays
|
|
13
|
+
visible when the example is maximized.
|
|
14
|
+
- **Build system is fragility-tolerant.** An *unsupported import* in a live example (a bare
|
|
15
|
+
specifier the runtime can't resolve) now downgrades that block to display-only with a warning
|
|
16
|
+
instead of failing the whole build; genuine syntax errors still fail the build.
|
|
17
|
+
- **Removed a render-skip anti-pattern** from `<tosi-segmented>`, `<tosi-color>`, and
|
|
18
|
+
`<tosi-form>`. They previously depended on `render()` being *skipped* after interaction via a
|
|
19
|
+
`valueChanged` flag — fragile timing that broke under tosijs's parts changes. Renders are now
|
|
20
|
+
idempotent: they skip only *provably-redundant* writes by comparing values (colors compared
|
|
21
|
+
**parsed**, so a `#rrggbb ↔ rgba()` round-trip no longer clobbers the caret/selection in the
|
|
22
|
+
field being edited).
|
|
23
|
+
- **Code editor background is now a neutral off-white** (off-black in dark mode) instead of a
|
|
24
|
+
brand-tinted color — both the default `<tosi-code>` and live examples.
|
|
25
|
+
- **Fix: nav flickered on refresh / reflowed on hydration.** The static no-JS nav relied on the
|
|
26
|
+
browser's native `<details>` collapse (`::details-content { content-visibility: hidden }`),
|
|
27
|
+
which Chromium applies lazily and non-deterministically on first paint — a closed section
|
|
28
|
+
could render at full height for a frame before snapping shut. Closed sections now collapse
|
|
29
|
+
synchronously via `display: none` (still opening on a no-JS `<summary>` toggle). Enforced by
|
|
30
|
+
`tests/hydration.pw.ts`.
|
|
31
|
+
- **Moved the doc-browser test-counter widget to the bottom-left** so it no longer sits under
|
|
32
|
+
the haltija dev overlay.
|
|
33
|
+
- **tosijs bumped to `^1.7.8`**, which fixes the `this.parts` proxy crossing into nested
|
|
34
|
+
component instances (tosijs#20 — surfaced by the self-hosting doc demo, where clicking "edit"
|
|
35
|
+
on one example opened the editor in the wrong one) and change-handler value staleness
|
|
36
|
+
(tosijs#21).
|
|
37
|
+
|
|
38
|
+
## 1.7.2
|
|
39
|
+
|
|
40
|
+
- **Fix: doc sites are now mount-point-agnostic** (issue #25). The build baked `basePath` into
|
|
41
|
+
every functional URL (nav/content links, `scriptUrl`, `stylesUrl`, favicon, `docsUrl`), so a
|
|
42
|
+
build was locked to one mount — adding a custom domain to a GitHub project page flipped the
|
|
43
|
+
live site to the root while `docs/` still pointed at `/repo`, 404ing every asset into a bare,
|
|
44
|
+
unstyled shell with no warning. Functional URLs are now emitted **relative to each page**, so
|
|
45
|
+
one build works at `/repo`, a custom-domain root, or a moved mount with no rebuild. *Metadata*
|
|
46
|
+
URLs (`canonical`, `og:url`, `og:image`, `sitemap.xml`) stay absolute via `baseUrl`+`basePath`
|
|
47
|
+
— so `basePath` now only affects metadata, never the page's assets. (The hydrated SPA's own
|
|
48
|
+
nav/`pushState` and `__TJS_LOCAL_BASE` remain root-relative — tracked in #16.)
|
|
49
|
+
- **Fix: the haltija dev channel loaded once per background-test iframe.** The doc-browser's
|
|
50
|
+
background test runner loads every page-with-tests in a hidden iframe, each served the injected
|
|
51
|
+
dev-channel loader — so `dev.js` was imported N times into throwaway frames. The loader is now
|
|
52
|
+
gated to the top window (`self===top`).
|
|
53
|
+
- **Live examples get a rounded, padded card.** `.preview` gains `padding` (breathing room around
|
|
54
|
+
rendered content) and its first child's top margin is dropped; the inset border moves to the
|
|
55
|
+
example's `:host` with a `border-radius`, so the whole example (preview + editors) reads as one
|
|
56
|
+
bordered card. CodeMirror tooltips render fixed-positioned so autocomplete isn't clipped by the
|
|
57
|
+
card's `overflow: hidden`.
|
|
58
|
+
- **Removed the "blueprint loading" doc page.** It documented tosijs's own
|
|
59
|
+
`<xin-loader>`/`<xin-blueprint>` elements (no tosijs-ui component), so it belongs in tosijs's
|
|
60
|
+
docs. The generic `blueprint` icon stays.
|
|
61
|
+
|
|
3
62
|
## 1.7.1
|
|
4
63
|
|
|
5
64
|
- **Fix: components looked wrong in the doc site's dark mode** (e.g. `<tosi-table>` stayed
|
package/dist/code-editor-cm.js
CHANGED
|
@@ -10,7 +10,7 @@ Languages here are the always-available set (js/ts/css/html/markdown). First-cla
|
|
|
10
10
|
tjs (highlighting + runtime-value autocomplete via tjs-lang's editors/codemirror) is
|
|
11
11
|
layered in separately and only when tjs-lang is installed.
|
|
12
12
|
*/
|
|
13
|
-
import { EditorView, keymap, lineNumbers, highlightActiveLineGutter, highlightSpecialChars, drawSelection, dropCursor, rectangularSelection, crosshairCursor, highlightActiveLine, } from '@codemirror/view';
|
|
13
|
+
import { EditorView, keymap, lineNumbers, highlightActiveLineGutter, highlightSpecialChars, drawSelection, dropCursor, rectangularSelection, crosshairCursor, highlightActiveLine, tooltips, } from '@codemirror/view';
|
|
14
14
|
import { EditorState, Compartment, Annotation, Transaction, } from '@codemirror/state';
|
|
15
15
|
import { indentWithTab, history, defaultKeymap, historyKeymap, undo, redo, undoDepth, redoDepth, } from '@codemirror/commands';
|
|
16
16
|
import { indentUnit, foldGutter, foldKeymap, indentOnInput, syntaxHighlighting, defaultHighlightStyle, bracketMatching, } from '@codemirror/language';
|
|
@@ -129,6 +129,10 @@ const editorTheme = EditorView.theme({
|
|
|
129
129
|
function baseExtensions() {
|
|
130
130
|
return [
|
|
131
131
|
editorTheme,
|
|
132
|
+
// Render autocomplete / hover tooltips in a fixed layer so they escape the
|
|
133
|
+
// live-example's `:host { overflow: hidden }` (the rounded card clip) instead
|
|
134
|
+
// of being cut off at the editor's bottom edge in a short example.
|
|
135
|
+
tooltips({ position: 'fixed' }),
|
|
132
136
|
lineNumbers(),
|
|
133
137
|
highlightActiveLineGutter(),
|
|
134
138
|
highlightSpecialChars(),
|
package/dist/color-input.d.ts
CHANGED
package/dist/color-input.js
CHANGED
|
@@ -71,7 +71,6 @@ class ColorInput extends Component {
|
|
|
71
71
|
}),
|
|
72
72
|
input({ title: 'css color spec', part: 'css' }),
|
|
73
73
|
];
|
|
74
|
-
valueChanged = false;
|
|
75
74
|
update = (event) => {
|
|
76
75
|
const { rgb, alpha, css } = this.parts;
|
|
77
76
|
if (event.type === 'input') {
|
|
@@ -86,7 +85,6 @@ class ColorInput extends Component {
|
|
|
86
85
|
}
|
|
87
86
|
rgb.style.opacity = String(this.color.a);
|
|
88
87
|
this.value = this.color.rgba;
|
|
89
|
-
this.valueChanged = true;
|
|
90
88
|
};
|
|
91
89
|
connectedCallback() {
|
|
92
90
|
super.connectedCallback();
|
|
@@ -96,16 +94,32 @@ class ColorInput extends Component {
|
|
|
96
94
|
css.addEventListener('change', this.update);
|
|
97
95
|
}
|
|
98
96
|
render() {
|
|
99
|
-
if (this.valueChanged) {
|
|
100
|
-
this.valueChanged = false;
|
|
101
|
-
return;
|
|
102
|
-
}
|
|
103
97
|
const { rgb, alpha, css } = this.parts;
|
|
104
98
|
this.color = Color.fromCss(this.value);
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
99
|
+
const rgba = this.color.rgba;
|
|
100
|
+
const alphaStr = String(this.color.a);
|
|
101
|
+
// Idempotent: only WRITE an input whose value doesn't already represent this
|
|
102
|
+
// colour. Compare the PARSED colour, not the string — `#rrggbb` and `rgba(...)`
|
|
103
|
+
// are the same colour, so a string compare (or a blind write) would clobber the
|
|
104
|
+
// caret / selection and re-round-trip the user's format on the field they're
|
|
105
|
+
// editing, every render. Skipping provably-redundant work is right; a `valueChanged`
|
|
106
|
+
// timing flag to decide it was the fragile part (broke under tosijs 1.7.7).
|
|
107
|
+
const sameColor = (v) => {
|
|
108
|
+
try {
|
|
109
|
+
return Color.fromCss(v).rgba === rgba;
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
if (!sameColor(rgb.value))
|
|
116
|
+
rgb.value = this.color.html.substring(0, 7);
|
|
117
|
+
if (rgb.style.opacity !== alphaStr)
|
|
118
|
+
rgb.style.opacity = alphaStr;
|
|
119
|
+
if (alpha.value !== alphaStr)
|
|
120
|
+
alpha.value = alphaStr;
|
|
121
|
+
if (!sameColor(css.value))
|
|
122
|
+
css.value = this.color.html;
|
|
109
123
|
}
|
|
110
124
|
}
|
|
111
125
|
export const colorInput = ColorInput.elementCreator();
|
package/dist/doc-browser.js
CHANGED
|
@@ -206,12 +206,14 @@ const testIndicatorStyleSpec = {
|
|
|
206
206
|
background: testColor.fail,
|
|
207
207
|
animation: 'test-pulse 2s ease-in-out infinite',
|
|
208
208
|
},
|
|
209
|
-
// Floating widget - position and colors only, inherits button structure
|
|
209
|
+
// Floating widget - position and colors only, inherits button structure.
|
|
210
|
+
// Bottom-LEFT: bottom-right is where the injected haltija dev widget sits, and it
|
|
211
|
+
// was overlaying this.
|
|
210
212
|
'.test-widget': {
|
|
211
213
|
_testBg: testColor.running,
|
|
212
214
|
position: 'fixed',
|
|
213
215
|
bottom: vars.spacing,
|
|
214
|
-
|
|
216
|
+
left: vars.spacing,
|
|
215
217
|
zIndex: '1000',
|
|
216
218
|
background: vars.testBg,
|
|
217
219
|
color: 'white',
|
|
@@ -8,6 +8,8 @@ export interface DocSystemTheme {
|
|
|
8
8
|
text?: string;
|
|
9
9
|
buttonBg?: string;
|
|
10
10
|
inputBg?: string;
|
|
11
|
+
/** code-editor surface (`--code-bg`); neutral off-white by default, not brand-tinted. */
|
|
12
|
+
codeBg?: string;
|
|
11
13
|
}
|
|
12
14
|
/** Compute the full set of `:root` color variables from a few base colors. */
|
|
13
15
|
export declare function docSystemColors(theme?: DocSystemTheme): XinStyleRule;
|
|
@@ -34,7 +34,9 @@ export function docSystemColors(theme = {}) {
|
|
|
34
34
|
_placeholderColor: brandColor.opacity(0.4),
|
|
35
35
|
_brandTextColor: brandColor.rotate(30).brighten(0.9),
|
|
36
36
|
_insetBg: brandColor.rotate(45).brighten(0.8),
|
|
37
|
-
|
|
37
|
+
// Neutral off-white (not brand-tinted); dark mode inverts its luminance to
|
|
38
|
+
// off-black, so the editor surface stays a clean neutral in both themes.
|
|
39
|
+
_codeBg: theme.codeBg ?? '#fdfdfd',
|
|
38
40
|
_linkColor: brandColor.rotate(-30).darken(0.5),
|
|
39
41
|
_shadowColor: '#0004',
|
|
40
42
|
_menuBg: '#fafafa',
|
|
@@ -476,6 +478,18 @@ export function docSystemStyleSpec(theme = {}) {
|
|
|
476
478
|
'.doc-nav details > ul': {
|
|
477
479
|
paddingLeft: vars.spacing,
|
|
478
480
|
},
|
|
481
|
+
// Collapse a closed section's list SYNCHRONOUSLY. The UA collapses <details>
|
|
482
|
+
// via `::details-content { content-visibility: hidden }`, which is applied
|
|
483
|
+
// lazily and — for this static tree — non-deterministically on first paint:
|
|
484
|
+
// sometimes the closed section is still laid out at full height for a frame
|
|
485
|
+
// before it snaps shut. That frame is a visible flicker on refresh, and it made
|
|
486
|
+
// the "nav does not move on hydration" test measure a 36px row that hydration
|
|
487
|
+
// then collapses to 0. An explicit display:none collapses it in the first layout
|
|
488
|
+
// pass with no content-visibility race, and still opens on a no-JS <summary>
|
|
489
|
+
// toggle (the UA adds [open], so this rule stops matching).
|
|
490
|
+
'.doc-nav details:not([open]) > ul': {
|
|
491
|
+
display: 'none',
|
|
492
|
+
},
|
|
479
493
|
// <summary> is the section header (its triangle toggles; the link navigates).
|
|
480
494
|
'.doc-nav summary': {
|
|
481
495
|
cursor: 'pointer',
|
|
@@ -20,8 +20,9 @@ the bundle step, the ePub step, and generate-css; see the Bun.build note in
|
|
|
20
20
|
orchestrator.ts.
|
|
21
21
|
|
|
22
22
|
bun check-examples-cli.ts <docs.json> [contextKey ...]
|
|
23
|
-
→ writes
|
|
24
|
-
found (a problem is data, not a crash — only a real failure exits
|
|
23
|
+
→ writes { problems, warnings, bakes } as JSON to stdout; exit 0 whether or not
|
|
24
|
+
problems were found (a problem is data, not a crash — only a real failure exits
|
|
25
|
+
non-zero). The parent fails the build on `problems`, warns on `warnings`.
|
|
25
26
|
|
|
26
27
|
Build-time only. Never import this from browser code.
|
|
27
28
|
*/
|
|
@@ -36,7 +37,7 @@ const contextKeys = process.argv.slice(3);
|
|
|
36
37
|
// non-context imports validate as dynamic `<prefix><spec>` imports rather than failing.
|
|
37
38
|
const importPrefix = process.env.TOSI_IMPORT_PREFIX || undefined;
|
|
38
39
|
const corpus = JSON.parse(await Bun.file(docsJson).text());
|
|
39
|
-
const { problems, bakes } = await checkExamples(corpus, {
|
|
40
|
+
const { problems, warnings, bakes } = await checkExamples(corpus, {
|
|
40
41
|
...(contextKeys.length ? { contextKeys } : {}),
|
|
41
42
|
...(importPrefix ? { importPrefix } : {}),
|
|
42
43
|
});
|
|
@@ -45,6 +46,7 @@ const { problems, bakes } = await checkExamples(corpus, {
|
|
|
45
46
|
// Maps can't JSON-roundtrip, so bakes go over as [filename, [source, {dialect,js}][]][].
|
|
46
47
|
process.stdout.write(JSON.stringify({
|
|
47
48
|
problems,
|
|
49
|
+
warnings,
|
|
48
50
|
bakes: Array.from(bakes.entries()).map(([filename, docBakes]) => [
|
|
49
51
|
filename,
|
|
50
52
|
Array.from(docBakes.entries()),
|
|
@@ -8,7 +8,20 @@ export interface ExampleProblem {
|
|
|
8
8
|
snippet: string;
|
|
9
9
|
}
|
|
10
10
|
export interface ExampleCheck {
|
|
11
|
+
/**
|
|
12
|
+
* Blocks that failed to build for a real reason (a tjs/TS syntax error, a
|
|
13
|
+
* transpile failure). These FAIL the build — broken code shouldn't ship.
|
|
14
|
+
*/
|
|
11
15
|
problems: ExampleProblem[];
|
|
16
|
+
/**
|
|
17
|
+
* Blocks that reference a package the environment can't provide (a non-context
|
|
18
|
+
* import, no import-resolver). The code isn't broken, it just can't run here —
|
|
19
|
+
* almost always illustrative code that should be tagged `typescript` (display-
|
|
20
|
+
* only) rather than `ts`. These WARN and are treated as display-only; they do
|
|
21
|
+
* NOT fail the build. (Enable `importResolver`, or tag the block display-only,
|
|
22
|
+
* to silence the warning.)
|
|
23
|
+
*/
|
|
24
|
+
warnings: ExampleProblem[];
|
|
12
25
|
/**
|
|
13
26
|
* Build-time transpiled JS for `tjs` blocks, grouped by doc filename, each keyed
|
|
14
27
|
* by exact source text. The renderer embeds a doc's bakes as hidden scripts (so
|
|
@@ -4,17 +4,25 @@ Build-time transpile check for live examples.
|
|
|
4
4
|
Every executable code block in the corpus (`js` / `tjs` / `ts` / `test`) is put
|
|
5
5
|
through the SAME front half of the runtime pipeline the live-example component
|
|
6
6
|
uses — `rewriteImports` → the tjs-lang transform → `new AsyncFunction(...)` — so a
|
|
7
|
-
block that can't build
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
7
|
+
block that can't build is caught *at authoring time*, on every page, instead of
|
|
8
|
+
silently rendering an error only when someone opens that page. The two outcomes
|
|
9
|
+
are graded differently (this is the point):
|
|
10
|
+
|
|
11
|
+
- a real **syntax / transpile error** is broken code → a `problem` that FAILS
|
|
12
|
+
the build;
|
|
13
|
+
- an **unsupported import** (a non-context package, no import-resolver) is fine
|
|
14
|
+
code the doc environment just can't run → a `warning`: the block is treated as
|
|
15
|
+
display-only and the build survives. It's almost always illustrative code that
|
|
16
|
+
should have been tagged `typescript` instead of `ts` — a mistagging shouldn't
|
|
17
|
+
take the whole build down with it.
|
|
18
|
+
|
|
19
|
+
`html` / `css` and display-only languages (`typescript`, `json`, …) are not
|
|
20
|
+
executed, so they're skipped.
|
|
13
21
|
|
|
14
22
|
Build-time only (bun). Never import from browser code.
|
|
15
23
|
*/
|
|
16
24
|
import { marked } from 'marked';
|
|
17
|
-
import { rewriteImports, AsyncFunction, loadTransform, } from '../../live-example/code-transform';
|
|
25
|
+
import { rewriteImports, AsyncFunction, loadTransform, UnsupportedImportError, } from '../../live-example/code-transform';
|
|
18
26
|
// The default live-example context (matches the IIFE globals the pages provide).
|
|
19
27
|
// A project that sets a custom `context` on its <tosi-doc-system> can pass its
|
|
20
28
|
// own keys; these are the tosijs-ui defaults.
|
|
@@ -58,6 +66,7 @@ function collectCodeTokens(text) {
|
|
|
58
66
|
export async function checkExamples(docs, opts = {}) {
|
|
59
67
|
const contextKeys = opts.contextKeys ?? DEFAULT_CONTEXT_KEYS;
|
|
60
68
|
const problems = [];
|
|
69
|
+
const warnings = [];
|
|
61
70
|
const bakes = new Map();
|
|
62
71
|
for (const doc of docs) {
|
|
63
72
|
for (const block of collectCodeTokens(doc.text)) {
|
|
@@ -97,17 +106,24 @@ export async function checkExamples(docs, opts = {}) {
|
|
|
97
106
|
}
|
|
98
107
|
}
|
|
99
108
|
catch (err) {
|
|
100
|
-
|
|
109
|
+
const entry = {
|
|
101
110
|
filename: doc.filename,
|
|
102
111
|
title: doc.title,
|
|
103
112
|
lang: block.lang,
|
|
104
113
|
error: err.message || String(err),
|
|
105
114
|
snippet: block.text.split('\n').slice(0, 3).join('\n'),
|
|
106
|
-
}
|
|
115
|
+
};
|
|
116
|
+
// An unsupported import means the block references deps the environment
|
|
117
|
+
// can't provide — not broken code. Warn (display-only), don't fail the
|
|
118
|
+
// build. A real syntax/transpile error is a genuine problem.
|
|
119
|
+
if (err instanceof UnsupportedImportError)
|
|
120
|
+
warnings.push(entry);
|
|
121
|
+
else
|
|
122
|
+
problems.push(entry);
|
|
107
123
|
}
|
|
108
124
|
}
|
|
109
125
|
}
|
|
110
|
-
return { problems, bakes };
|
|
126
|
+
return { problems, warnings, bakes };
|
|
111
127
|
}
|
|
112
128
|
/** Format problems for a build log. */
|
|
113
129
|
export function formatExampleProblems(problems) {
|
|
@@ -28,6 +28,17 @@ export declare function resolveIdleMs(configHours: number | undefined, envHours:
|
|
|
28
28
|
* off. Only an explicit non-positive number disables the ceiling.
|
|
29
29
|
*/
|
|
30
30
|
export declare function resolveLimitMb(configMb: number | undefined, envMb: string | undefined): number;
|
|
31
|
+
/**
|
|
32
|
+
* The localhost-gated haltija dev-channel loader injected into served HTML at
|
|
33
|
+
* serve time (never bundled, never in the built output).
|
|
34
|
+
*
|
|
35
|
+
* `self===top` keeps it in the TOP window only. The doc-browser's background test
|
|
36
|
+
* runner loads every page-with-tests in a hidden iframe (`?_testMode=1`), each
|
|
37
|
+
* served this same HTML — without the guard, haltija's `dev.js` gets imported
|
|
38
|
+
* once per test page (N redundant loads in throwaway frames). An agent only ever
|
|
39
|
+
* drives the top page, so nested frames never need the channel.
|
|
40
|
+
*/
|
|
41
|
+
export declare function haltijaLoaderSnippet(httpsPort: number): string;
|
|
31
42
|
export declare function devServer(config: SiteConfig, opts?: {
|
|
32
43
|
test?: boolean;
|
|
33
44
|
build?: () => unknown | Promise<unknown>;
|
|
@@ -78,6 +78,20 @@ export function resolveLimitMb(configMb, envMb) {
|
|
|
78
78
|
const mb = Number.isFinite(raw) ? raw : DEFAULT_LIMIT_MB;
|
|
79
79
|
return mb > 0 ? mb : 0;
|
|
80
80
|
}
|
|
81
|
+
/**
|
|
82
|
+
* The localhost-gated haltija dev-channel loader injected into served HTML at
|
|
83
|
+
* serve time (never bundled, never in the built output).
|
|
84
|
+
*
|
|
85
|
+
* `self===top` keeps it in the TOP window only. The doc-browser's background test
|
|
86
|
+
* runner loads every page-with-tests in a hidden iframe (`?_testMode=1`), each
|
|
87
|
+
* served this same HTML — without the guard, haltija's `dev.js` gets imported
|
|
88
|
+
* once per test page (N redundant loads in throwaway frames). An agent only ever
|
|
89
|
+
* drives the top page, so nested frames never need the channel.
|
|
90
|
+
*/
|
|
91
|
+
export function haltijaLoaderSnippet(httpsPort) {
|
|
92
|
+
return (`<script>self===top&&/^localhost$|^127\\./.test(location.hostname)` +
|
|
93
|
+
`&&import('https://localhost:${httpsPort}/dev.js')</script>`);
|
|
94
|
+
}
|
|
81
95
|
/**
|
|
82
96
|
* Reclaim the port we are about to bind, from the process LISTENING on it.
|
|
83
97
|
*
|
|
@@ -259,8 +273,7 @@ export async function devServer(config, opts = {}) {
|
|
|
259
273
|
(config.haltijaDev === true ||
|
|
260
274
|
process.env.HALTIJA_DEV === '1' ||
|
|
261
275
|
process.env.HALTIJA_DEV === 'true');
|
|
262
|
-
const HALTIJA_SNIPPET =
|
|
263
|
-
`&&import('https://localhost:${HALTIJA_HTTPS_PORT}/dev.js')</script>`;
|
|
276
|
+
const HALTIJA_SNIPPET = haltijaLoaderSnippet(HALTIJA_HTTPS_PORT);
|
|
264
277
|
let testReportResolve;
|
|
265
278
|
// Source read/write for in-browser "edit page source" (config.editableSources).
|
|
266
279
|
// Local dev only — your machine, your files — so the lone guard is correctness:
|
|
@@ -56,4 +56,22 @@ export interface GenerateSiteConfig {
|
|
|
56
56
|
/** extra lines injected into every <head> (favicon, analytics, etc.) */
|
|
57
57
|
headExtra?: string;
|
|
58
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* Depth of a page below the site root: 0 for the root index (served at the mount
|
|
61
|
+
* root), 1 for every `/slug/` page. The generator only ever emits a flat `/slug/`
|
|
62
|
+
* tree (see `pathForSlug`), so a non-root page is always exactly one directory deep.
|
|
63
|
+
*/
|
|
64
|
+
export declare function pageDepth(slug: string): number;
|
|
65
|
+
/**
|
|
66
|
+
* Rewrite a root-relative *functional* URL (asset ref, nav / content link) to be
|
|
67
|
+
* relative to a page at `depth`, so ONE build works at ANY mount point — a GitHub
|
|
68
|
+
* project page under `/repo`, a custom-domain root, or a moved mount — with no
|
|
69
|
+
* `basePath` rebuild. Relative URLs resolve against wherever the page is actually
|
|
70
|
+
* served, so `basePath` is deliberately NOT applied here: a page at `/repo/x/`
|
|
71
|
+
* gets `../styles.css` → `/repo/styles.css`, the same page at `/x/` gets it at
|
|
72
|
+
* `/styles.css`. External (`https://…`, `//…`) and already-relative refs pass
|
|
73
|
+
* through untouched. (Metadata URLs still use `withBase` — they need the absolute
|
|
74
|
+
* origin.) See issue #25; the runtime/SPA-navigation half is issue #16.
|
|
75
|
+
*/
|
|
76
|
+
export declare function relativeUrl(depth: number, p: string): string;
|
|
59
77
|
export declare function generateSite(config: GenerateSiteConfig): Promise<number>;
|
|
@@ -27,11 +27,11 @@ const escapeText = (s) => s.replace(/&/g, '&').replace(/</g, '<').replace
|
|
|
27
27
|
* <details> on the path to `currentFilename` opened. `currentFilename` gets
|
|
28
28
|
* aria-current. Crawlable + no-JS: every node is a real <a>.
|
|
29
29
|
*/
|
|
30
|
-
function navHtml(docs, slugMap, currentFilename,
|
|
30
|
+
function navHtml(docs, slugMap, currentFilename, depth) {
|
|
31
31
|
const roots = buildNavTree(docs, slugMap);
|
|
32
32
|
const open = navOpenPath(roots, currentFilename);
|
|
33
33
|
const renderNode = (node, indent) => {
|
|
34
|
-
const href =
|
|
34
|
+
const href = relativeUrl(depth, pathForSlug(node.slug));
|
|
35
35
|
const current = node.doc.filename === currentFilename;
|
|
36
36
|
// `doc-link` is what the HYDRATED nav emits, and the shared nav CSS is written
|
|
37
37
|
// against it. Emitting a bare <a> here forced a second, hand-copied rule set under
|
|
@@ -78,16 +78,48 @@ function absUrl(baseUrl, pathOrUrl) {
|
|
|
78
78
|
/**
|
|
79
79
|
* Prefix a root-relative path with basePath. No-op for '/', empty, protocol-
|
|
80
80
|
* relative (`//…`), or absolute (`https://…`) URLs.
|
|
81
|
+
*
|
|
82
|
+
* Used ONLY for *metadata* URLs now (canonical, og:url, og:image, sitemap) — they
|
|
83
|
+
* legitimately need the real absolute origin+path. *Functional* URLs go through
|
|
84
|
+
* `relativeUrl` instead, so the artifact is mount-agnostic. See `relativeUrl`.
|
|
81
85
|
*/
|
|
82
86
|
function withBase(basePath, p) {
|
|
83
87
|
if (!p || !basePath || basePath === '/' || /^(https?:)?\/\//.test(p))
|
|
84
88
|
return p;
|
|
85
89
|
return basePath.replace(/\/$/, '') + (p.startsWith('/') ? p : '/' + p);
|
|
86
90
|
}
|
|
91
|
+
/**
|
|
92
|
+
* Depth of a page below the site root: 0 for the root index (served at the mount
|
|
93
|
+
* root), 1 for every `/slug/` page. The generator only ever emits a flat `/slug/`
|
|
94
|
+
* tree (see `pathForSlug`), so a non-root page is always exactly one directory deep.
|
|
95
|
+
*/
|
|
96
|
+
export function pageDepth(slug) {
|
|
97
|
+
return slug === '' ? 0 : 1;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Rewrite a root-relative *functional* URL (asset ref, nav / content link) to be
|
|
101
|
+
* relative to a page at `depth`, so ONE build works at ANY mount point — a GitHub
|
|
102
|
+
* project page under `/repo`, a custom-domain root, or a moved mount — with no
|
|
103
|
+
* `basePath` rebuild. Relative URLs resolve against wherever the page is actually
|
|
104
|
+
* served, so `basePath` is deliberately NOT applied here: a page at `/repo/x/`
|
|
105
|
+
* gets `../styles.css` → `/repo/styles.css`, the same page at `/x/` gets it at
|
|
106
|
+
* `/styles.css`. External (`https://…`, `//…`) and already-relative refs pass
|
|
107
|
+
* through untouched. (Metadata URLs still use `withBase` — they need the absolute
|
|
108
|
+
* origin.) See issue #25; the runtime/SPA-navigation half is issue #16.
|
|
109
|
+
*/
|
|
110
|
+
export function relativeUrl(depth, p) {
|
|
111
|
+
if (!p || /^(https?:)?\/\//.test(p) || !p.startsWith('/'))
|
|
112
|
+
return p;
|
|
113
|
+
const rel = '../'.repeat(depth) + p.slice(1);
|
|
114
|
+
return rel === '' ? './' : rel;
|
|
115
|
+
}
|
|
87
116
|
function pageHtml(doc, config, slugMap, configAttr) {
|
|
88
117
|
const { projectName = '', baseUrl = '', lang = 'en', favicon = '/favicon.svg', docsUrl = '/docs.json', scriptUrl = '/iife.js', hydrateUrl, stylesUrl = '/doc-system.css', localizedUrl = '/localized-strings.txt', basePath, headExtra = '', bakes, } = config;
|
|
118
|
+
// Functional URLs are emitted relative to THIS page's depth so the build is
|
|
119
|
+
// mount-agnostic (issue #25); metadata URLs below stay absolute via withBase.
|
|
120
|
+
const depth = pageDepth(slugMap[doc.filename] ?? '');
|
|
89
121
|
const localizedAttr = config.localizedStrings
|
|
90
|
-
? ` localized="${escapeAttr(
|
|
122
|
+
? ` localized="${escapeAttr(relativeUrl(depth, localizedUrl))}"`
|
|
91
123
|
: '';
|
|
92
124
|
// ONE rule, shared with the doc-browser's hydration path — see doc-title.ts. Writing
|
|
93
125
|
// it twice is what made the title change on hydration (issue #6).
|
|
@@ -103,9 +135,9 @@ function pageHtml(doc, config, slugMap, configAttr) {
|
|
|
103
135
|
// static HTML is correct for no-JS readers and crawlers (the doc-browser also
|
|
104
136
|
// does this client-side after hydration).
|
|
105
137
|
const body = rewriteDocLinks(renderDocMarkdown(doc.text, { bakes: bakes?.get(doc.filename) }), (filename) => slugMap[filename] !== undefined
|
|
106
|
-
?
|
|
138
|
+
? relativeUrl(depth, pathForSlug(slugMap[filename]))
|
|
107
139
|
: null);
|
|
108
|
-
const nav = navHtml(config.docs, slugMap, doc.filename,
|
|
140
|
+
const nav = navHtml(config.docs, slugMap, doc.filename, depth);
|
|
109
141
|
const navbar = linkListHtml('doc-navbar', config.navbarLinks);
|
|
110
142
|
const jsonLd = baseUrl
|
|
111
143
|
? jsonLdScript({
|
|
@@ -126,7 +158,7 @@ function pageHtml(doc, config, slugMap, configAttr) {
|
|
|
126
158
|
' <link rel="preconnect" href="https://fonts.googleapis.com" />',
|
|
127
159
|
' <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />',
|
|
128
160
|
// Burned-in theme: styles the page with no JS and with zero flash on hydration.
|
|
129
|
-
` <link rel="stylesheet" href="${escapeAttr(
|
|
161
|
+
` <link rel="stylesheet" href="${escapeAttr(relativeUrl(depth, stylesUrl))}" data-tosi-doc-system />`,
|
|
130
162
|
` <title>${escapeText(title)}</title>`,
|
|
131
163
|
description
|
|
132
164
|
? ` <meta name="description" content="${escapeAttr(description)}" />`
|
|
@@ -155,7 +187,7 @@ function pageHtml(doc, config, slugMap, configAttr) {
|
|
|
155
187
|
? ` <meta name="twitter:image" content="${escapeAttr(imageAbs)}" />`
|
|
156
188
|
: '',
|
|
157
189
|
jsonLd,
|
|
158
|
-
` <link rel="icon" href="${escapeAttr(
|
|
190
|
+
` <link rel="icon" href="${escapeAttr(relativeUrl(depth, favicon))}" />`,
|
|
159
191
|
headExtra,
|
|
160
192
|
]
|
|
161
193
|
.filter(Boolean)
|
|
@@ -189,7 +221,7 @@ function pageHtml(doc, config, slugMap, configAttr) {
|
|
|
189
221
|
${head}
|
|
190
222
|
</head>
|
|
191
223
|
<body>
|
|
192
|
-
<tosi-doc-system docs="${escapeAttr(
|
|
224
|
+
<tosi-doc-system docs="${escapeAttr(relativeUrl(depth, docsUrl))}" config="${configAttr}"${localizedAttr}>
|
|
193
225
|
<article class="doc-content">
|
|
194
226
|
${body}
|
|
195
227
|
</article>
|
|
@@ -197,8 +229,8 @@ ${nav}
|
|
|
197
229
|
${navbar}
|
|
198
230
|
</tosi-doc-system>
|
|
199
231
|
${hydrateUrl
|
|
200
|
-
? `<script type="module" src="${escapeAttr(
|
|
201
|
-
: `<script src="${escapeAttr(
|
|
232
|
+
? `<script type="module" src="${escapeAttr(relativeUrl(depth, hydrateUrl))}"></script>`
|
|
233
|
+
: `<script src="${escapeAttr(relativeUrl(depth, scriptUrl))}"></script>`}
|
|
202
234
|
</body>
|
|
203
235
|
</html>
|
|
204
236
|
`;
|
|
@@ -132,6 +132,7 @@ async function checkExamplesInChild(docsJson, importPrefix) {
|
|
|
132
132
|
const payload = JSON.parse(out);
|
|
133
133
|
return {
|
|
134
134
|
problems: payload.problems,
|
|
135
|
+
warnings: payload.warnings ?? [],
|
|
135
136
|
bakes: new Map(payload.bakes.map(([file, e]) => [file, new Map(e)])),
|
|
136
137
|
};
|
|
137
138
|
}
|
|
@@ -239,14 +240,26 @@ export async function buildSite(config) {
|
|
|
239
240
|
: undefined;
|
|
240
241
|
let exampleBakes;
|
|
241
242
|
if (config.checkExamples !== false) {
|
|
242
|
-
const { problems, bakes } = await checkExamplesInChild(DOCS_JSON, resolverPrefix);
|
|
243
|
+
const { problems, warnings, bakes } = await checkExamplesInChild(DOCS_JSON, resolverPrefix);
|
|
243
244
|
exampleBakes = bakes;
|
|
245
|
+
// Unsupported imports don't fail the build — the code isn't broken, it just
|
|
246
|
+
// can't run in the doc environment (almost always illustrative code that
|
|
247
|
+
// should be tagged `typescript`). Warn loudly, treat the block as display-only,
|
|
248
|
+
// and keep going. Only real syntax/transpile errors below are fatal.
|
|
249
|
+
if (warnings.length) {
|
|
250
|
+
console.warn(`⚠️ doc-site build: ${warnings.length} live example block(s) can't run ` +
|
|
251
|
+
`here and were left as display-only:\n\n` +
|
|
252
|
+
formatExampleProblems(warnings) +
|
|
253
|
+
`\n\nTag each with a display-only language like \`typescript\` (instead of` +
|
|
254
|
+
` \`js\`/\`ts\`) to silence this, or enable \`importResolver\` to import` +
|
|
255
|
+
` other packages.\n`);
|
|
256
|
+
}
|
|
244
257
|
if (problems.length) {
|
|
245
258
|
throw new Error(`doc-site build: ${problems.length} live example(s) failed to build:\n\n` +
|
|
246
259
|
formatExampleProblems(problems) +
|
|
247
260
|
`\n\nFix the code, or — if a block is illustrative and not meant to run —` +
|
|
248
261
|
` tag it with a display-only language like \`typescript\` instead of` +
|
|
249
|
-
` \`js\`/\`ts\`. (Disable with checkExamples: false.)`);
|
|
262
|
+
` \`js\`/\`ts\`. (Disable all checking with checkExamples: false.)`);
|
|
250
263
|
}
|
|
251
264
|
}
|
|
252
265
|
// Copy static-asset dirs into the web root.
|
|
@@ -88,9 +88,16 @@ export interface SiteConfig {
|
|
|
88
88
|
domain?: string;
|
|
89
89
|
/**
|
|
90
90
|
* URL prefix the site is served under, default '/'. Set to '/<repo>' for a
|
|
91
|
-
* GitHub project page without a custom domain
|
|
92
|
-
*
|
|
93
|
-
*
|
|
91
|
+
* GitHub project page without a custom domain.
|
|
92
|
+
*
|
|
93
|
+
* As of the mount-agnostic build (issue #25), `basePath` only affects
|
|
94
|
+
* *metadata* URLs — `canonical`, `og:url`, `og:image`, and `sitemap.xml` —
|
|
95
|
+
* which need the real absolute served path for SEO. *Functional* URLs (nav /
|
|
96
|
+
* content links, `scriptUrl`, `stylesUrl`, favicon, `docsUrl`) are emitted
|
|
97
|
+
* **relative to each page**, so one build works at `/repo`, at a custom-domain
|
|
98
|
+
* root, or a moved mount with no rebuild. You still want `basePath` correct so
|
|
99
|
+
* search engines see canonical URLs at the real path; getting it wrong now only
|
|
100
|
+
* mis-states metadata, it no longer 404s the page's assets.
|
|
94
101
|
*/
|
|
95
102
|
basePath?: string;
|
|
96
103
|
/**
|