tosijs-ui 1.6.18 → 1.6.20
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 +45 -0
- package/dist/code-editor-cm.d.ts +30 -0
- package/dist/code-editor-cm.js +132 -0
- package/dist/code-editor.d.ts +28 -9
- package/dist/code-editor.js +68 -62
- package/dist/doc-browser.js +5 -1
- package/dist/doc-system/site/check-examples.d.ts +14 -0
- package/dist/doc-system/site/check-examples.js +97 -0
- package/dist/doc-system/site/orchestrator.js +16 -0
- package/dist/doc-system/site/site-config.d.ts +9 -0
- package/dist/iife.js +137 -109
- package/dist/iife.js.map +31 -8
- package/dist/live-example/component.js +17 -17
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/llms.txt +4 -2
- package/package.json +18 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,50 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.6.20
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- **`import 'tosijs-ui'` no longer resolves to the IIFE under browser bundlers.**
|
|
8
|
+
The `"."` export had a `"browser"` condition pointing at `dist/iife.js` — the
|
|
9
|
+
self-contained CDN/doc-site bundle that inlines tosijs + marked. Browser-targeted
|
|
10
|
+
bundlers (Vite, webpack, esbuild) picked it, so consumers **double-bundled tosijs**
|
|
11
|
+
instead of externalizing the peer, and named imports broke (the IIFE isn't an ESM
|
|
12
|
+
module). Removed the condition: every `import` now resolves to `dist/index.js`
|
|
13
|
+
(ESM, peers externalized). The IIFE stays for CDN `<script>` use and the naive
|
|
14
|
+
doc site — referenced by file path, never via `import 'tosijs-ui'`.
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- **tosijs peer + dev dependency bumped to `^1.6.6`** — picks up a subtle
|
|
19
|
+
component-lifecycle fix.
|
|
20
|
+
|
|
21
|
+
## 1.6.19
|
|
22
|
+
|
|
23
|
+
Doc-system tooling; no component API changes.
|
|
24
|
+
|
|
25
|
+
### Added
|
|
26
|
+
|
|
27
|
+
- **Build-time example transpile check.** Every executable live-example block
|
|
28
|
+
(`js` / `tjs` / `ts` / `test`) across the whole corpus is run through the front
|
|
29
|
+
half of the runtime pipeline — `rewriteImports` → transform → `new
|
|
30
|
+
AsyncFunction` — at build time, **without executing it**, so a block that can't
|
|
31
|
+
build (a syntax/import error, or illustrative code mistakenly tagged with an
|
|
32
|
+
executable language instead of the display-only `typescript`) **fails the build**
|
|
33
|
+
with the offending doc/block named, on every page — including fences hidden in
|
|
34
|
+
blockquotes and lists. TypeScript is transpiled with bun's own transpiler
|
|
35
|
+
(network-free). Opt out with `checkExamples: false`.
|
|
36
|
+
- **"One Source, Every Artifact"** doc page — how one corpus of doc-comments +
|
|
37
|
+
markdown projects into a static SEO site, a self-testing live playground with
|
|
38
|
+
in-browser TypeScript, an ePub/PDF, and an agent-debuggable page. Embeds a live
|
|
39
|
+
`<tosi-doc-system>` (the whole system running inside its own page).
|
|
40
|
+
|
|
41
|
+
### Fixed
|
|
42
|
+
|
|
43
|
+
- **A live example's build/exec failure is now a test failure whether or not it
|
|
44
|
+
defines `test` blocks.** So on any page the browser test runner loads, *all* of
|
|
45
|
+
its examples are checked for explosions — a no-test example that throws is
|
|
46
|
+
reported as a failed test — not just blocks with explicit assertions.
|
|
47
|
+
|
|
3
48
|
## 1.6.18
|
|
4
49
|
|
|
5
50
|
Doc-site tooling; no component API changes.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { EditorView } from '@codemirror/view';
|
|
2
|
+
import { Extension } from '@codemirror/state';
|
|
3
|
+
/** Map a `<tosi-code mode>` value to a CodeMirror language extension. */
|
|
4
|
+
export declare function languageForMode(mode: string): Extension;
|
|
5
|
+
export interface CmHandle {
|
|
6
|
+
view: EditorView;
|
|
7
|
+
getValue(): string;
|
|
8
|
+
setValue(text: string): void;
|
|
9
|
+
setMode(mode: string): void;
|
|
10
|
+
setReadOnly(readOnly: boolean): void;
|
|
11
|
+
undo(): void;
|
|
12
|
+
redo(): void;
|
|
13
|
+
canUndo(): boolean;
|
|
14
|
+
canRedo(): boolean;
|
|
15
|
+
focus(): void;
|
|
16
|
+
refresh(): void;
|
|
17
|
+
destroy(): void;
|
|
18
|
+
}
|
|
19
|
+
export interface CmOptions {
|
|
20
|
+
value?: string;
|
|
21
|
+
mode?: string;
|
|
22
|
+
readOnly?: boolean;
|
|
23
|
+
/** additional language-adjacent extensions (e.g. a tjs language + autocomplete) */
|
|
24
|
+
languageExtension?: Extension;
|
|
25
|
+
onChange?: (value: string) => void;
|
|
26
|
+
/** style-injection root — pass the host's ShadowRoot when mounted in shadow DOM */
|
|
27
|
+
root?: Document | ShadowRoot;
|
|
28
|
+
}
|
|
29
|
+
/** Create a CodeMirror editor mounted in `parent`, returning an editor-agnostic handle. */
|
|
30
|
+
export declare function createCmEditor(parent: HTMLElement, opts?: CmOptions): CmHandle;
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/*
|
|
2
|
+
CodeMirror 6 editor factory — the "heavy" module.
|
|
3
|
+
|
|
4
|
+
`code-editor.ts` imports this **dynamically** (on first `<tosi-code>` connect), so
|
|
5
|
+
CodeMirror only enters a consumer's bundle as a separate lazy chunk when the editor
|
|
6
|
+
is actually used — mirroring the way the old ACE build loaded from a CDN on first
|
|
7
|
+
use. No `<tosi-code>` in the page → no CodeMirror.
|
|
8
|
+
|
|
9
|
+
Languages here are the always-available set (js/ts/css/html/markdown). First-class
|
|
10
|
+
tjs (highlighting + runtime-value autocomplete via tjs-lang's editors/codemirror) is
|
|
11
|
+
layered in separately and only when tjs-lang is installed.
|
|
12
|
+
*/
|
|
13
|
+
import { EditorView, keymap, lineNumbers, highlightActiveLineGutter, highlightSpecialChars, drawSelection, dropCursor, rectangularSelection, crosshairCursor, highlightActiveLine } from '@codemirror/view';
|
|
14
|
+
import { EditorState, Compartment } from '@codemirror/state';
|
|
15
|
+
import { indentWithTab, history, defaultKeymap, historyKeymap, undo, redo, undoDepth, redoDepth } from '@codemirror/commands';
|
|
16
|
+
import { indentUnit, foldGutter, foldKeymap, indentOnInput, syntaxHighlighting, defaultHighlightStyle, bracketMatching } from '@codemirror/language';
|
|
17
|
+
import { closeBrackets, closeBracketsKeymap, autocompletion, completionKeymap } from '@codemirror/autocomplete';
|
|
18
|
+
import { searchKeymap, highlightSelectionMatches } from '@codemirror/search';
|
|
19
|
+
import { lintKeymap } from '@codemirror/lint';
|
|
20
|
+
import { javascript } from '@codemirror/lang-javascript';
|
|
21
|
+
import { css } from '@codemirror/lang-css';
|
|
22
|
+
import { html } from '@codemirror/lang-html';
|
|
23
|
+
import { markdown } from '@codemirror/lang-markdown';
|
|
24
|
+
/** Map a `<tosi-code mode>` value to a CodeMirror language extension. */
|
|
25
|
+
export function languageForMode(mode) {
|
|
26
|
+
switch (mode) {
|
|
27
|
+
case 'js':
|
|
28
|
+
case 'javascript':
|
|
29
|
+
return javascript();
|
|
30
|
+
case 'ts':
|
|
31
|
+
case 'typescript':
|
|
32
|
+
// tjs highlights fine as TypeScript until the tjs-lang language is wired
|
|
33
|
+
// (workstream B); this keeps `mode="tjs"` readable today.
|
|
34
|
+
case 'tjs':
|
|
35
|
+
case 'ajs':
|
|
36
|
+
return javascript({ typescript: true });
|
|
37
|
+
case 'css':
|
|
38
|
+
return css();
|
|
39
|
+
case 'html':
|
|
40
|
+
return html();
|
|
41
|
+
case 'md':
|
|
42
|
+
case 'markdown':
|
|
43
|
+
return markdown();
|
|
44
|
+
default:
|
|
45
|
+
return javascript();
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
// A basicSetup-equivalent without a hard-coded color theme, so the host page's CSS
|
|
49
|
+
// (and dark mode) controls the palette. Line-wrapping mirrors the old ACE `wrap:true`.
|
|
50
|
+
function baseExtensions() {
|
|
51
|
+
return [
|
|
52
|
+
lineNumbers(),
|
|
53
|
+
highlightActiveLineGutter(),
|
|
54
|
+
highlightSpecialChars(),
|
|
55
|
+
history(),
|
|
56
|
+
foldGutter(),
|
|
57
|
+
drawSelection(),
|
|
58
|
+
dropCursor(),
|
|
59
|
+
EditorState.allowMultipleSelections.of(true),
|
|
60
|
+
indentOnInput(),
|
|
61
|
+
indentUnit.of(' '),
|
|
62
|
+
EditorState.tabSize.of(2),
|
|
63
|
+
syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
|
|
64
|
+
bracketMatching(),
|
|
65
|
+
closeBrackets(),
|
|
66
|
+
autocompletion(),
|
|
67
|
+
rectangularSelection(),
|
|
68
|
+
crosshairCursor(),
|
|
69
|
+
highlightActiveLine(),
|
|
70
|
+
highlightSelectionMatches(),
|
|
71
|
+
EditorView.lineWrapping,
|
|
72
|
+
keymap.of([
|
|
73
|
+
indentWithTab,
|
|
74
|
+
...closeBracketsKeymap,
|
|
75
|
+
...defaultKeymap,
|
|
76
|
+
...searchKeymap,
|
|
77
|
+
...historyKeymap,
|
|
78
|
+
...foldKeymap,
|
|
79
|
+
...completionKeymap,
|
|
80
|
+
...lintKeymap,
|
|
81
|
+
]),
|
|
82
|
+
];
|
|
83
|
+
}
|
|
84
|
+
/** Create a CodeMirror editor mounted in `parent`, returning an editor-agnostic handle. */
|
|
85
|
+
export function createCmEditor(parent, opts = {}) {
|
|
86
|
+
const language = new Compartment();
|
|
87
|
+
const readonly = new Compartment();
|
|
88
|
+
const langExt = opts.languageExtension ?? languageForMode(opts.mode ?? 'javascript');
|
|
89
|
+
const view = new EditorView({
|
|
90
|
+
parent,
|
|
91
|
+
root: opts.root,
|
|
92
|
+
state: EditorState.create({
|
|
93
|
+
doc: opts.value ?? '',
|
|
94
|
+
extensions: [
|
|
95
|
+
baseExtensions(),
|
|
96
|
+
language.of(langExt),
|
|
97
|
+
readonly.of(EditorState.readOnly.of(!!opts.readOnly)),
|
|
98
|
+
EditorView.updateListener.of((u) => {
|
|
99
|
+
if (u.docChanged && opts.onChange)
|
|
100
|
+
opts.onChange(u.state.doc.toString());
|
|
101
|
+
}),
|
|
102
|
+
],
|
|
103
|
+
}),
|
|
104
|
+
});
|
|
105
|
+
const handle = {
|
|
106
|
+
view,
|
|
107
|
+
getValue: () => view.state.doc.toString(),
|
|
108
|
+
setValue(text) {
|
|
109
|
+
if (view.state.doc.toString() === text)
|
|
110
|
+
return;
|
|
111
|
+
view.dispatch({
|
|
112
|
+
changes: { from: 0, to: view.state.doc.length, insert: text },
|
|
113
|
+
});
|
|
114
|
+
},
|
|
115
|
+
setMode(mode) {
|
|
116
|
+
view.dispatch({ effects: language.reconfigure(languageForMode(mode)) });
|
|
117
|
+
},
|
|
118
|
+
setReadOnly(ro) {
|
|
119
|
+
view.dispatch({
|
|
120
|
+
effects: readonly.reconfigure(EditorState.readOnly.of(ro)),
|
|
121
|
+
});
|
|
122
|
+
},
|
|
123
|
+
undo: () => void undo(view),
|
|
124
|
+
redo: () => void redo(view),
|
|
125
|
+
canUndo: () => undoDepth(view.state) > 0,
|
|
126
|
+
canRedo: () => redoDepth(view.state) > 0,
|
|
127
|
+
focus: () => view.focus(),
|
|
128
|
+
refresh: () => view.requestMeasure(),
|
|
129
|
+
destroy: () => view.destroy(),
|
|
130
|
+
};
|
|
131
|
+
return handle;
|
|
132
|
+
}
|
package/dist/code-editor.d.ts
CHANGED
|
@@ -1,7 +1,15 @@
|
|
|
1
|
-
import { Component as WebComponent, ElementCreator } from 'tosijs';
|
|
2
|
-
|
|
1
|
+
import { Component as WebComponent, ElementCreator, PartsMap } from 'tosijs';
|
|
2
|
+
import type { CmHandle } from './code-editor-cm';
|
|
3
|
+
interface CodeEditorParts extends PartsMap {
|
|
4
|
+
host: HTMLDivElement;
|
|
5
|
+
}
|
|
6
|
+
export declare class CodeEditor extends WebComponent<CodeEditorParts> {
|
|
3
7
|
static preferredTagName: string;
|
|
4
8
|
private source;
|
|
9
|
+
private _handle;
|
|
10
|
+
private _loadPromise;
|
|
11
|
+
private _appliedMode;
|
|
12
|
+
private _appliedDisabled;
|
|
5
13
|
get value(): string;
|
|
6
14
|
set value(text: string);
|
|
7
15
|
private _original;
|
|
@@ -12,16 +20,16 @@ export declare class CodeEditor extends WebComponent {
|
|
|
12
20
|
showDiff(on: boolean): void;
|
|
13
21
|
static initAttributes: {
|
|
14
22
|
mode: string;
|
|
15
|
-
theme: string;
|
|
16
23
|
disabled: boolean;
|
|
17
24
|
};
|
|
18
25
|
role: string;
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
26
|
+
/** The underlying CodeMirror EditorView (undefined until loaded). */
|
|
27
|
+
get editor(): CmHandle['view'] | undefined;
|
|
28
|
+
undo(): void;
|
|
29
|
+
redo(): void;
|
|
30
|
+
canUndo(): boolean;
|
|
31
|
+
canRedo(): boolean;
|
|
32
|
+
content: () => HTMLDivElement[];
|
|
25
33
|
static shadowStyleSpec: {
|
|
26
34
|
':host': {
|
|
27
35
|
display: string;
|
|
@@ -29,9 +37,20 @@ export declare class CodeEditor extends WebComponent {
|
|
|
29
37
|
width: string;
|
|
30
38
|
height: string;
|
|
31
39
|
};
|
|
40
|
+
'[part="host"]': {
|
|
41
|
+
height: string;
|
|
42
|
+
};
|
|
43
|
+
'.cm-editor': {
|
|
44
|
+
height: string;
|
|
45
|
+
};
|
|
46
|
+
'.cm-scroller': {
|
|
47
|
+
outline: string;
|
|
48
|
+
fontFamily: string;
|
|
49
|
+
};
|
|
32
50
|
};
|
|
33
51
|
onResize(): void;
|
|
34
52
|
connectedCallback(): void;
|
|
35
53
|
render(): void;
|
|
36
54
|
}
|
|
37
55
|
export declare const codeEditor: ElementCreator<CodeEditor>;
|
|
56
|
+
export {};
|
package/dist/code-editor.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/*#
|
|
2
2
|
# code
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
A [CodeMirror 6](https://codemirror.net/) wrapper.
|
|
5
5
|
|
|
6
6
|
Sometimes, it's nice to be able to just toss a code-editor in a web-page.
|
|
7
7
|
|
|
8
|
-
`<tosi-code>`'s `value` is the code it contains. Its `mode` attribute sets the
|
|
9
|
-
|
|
8
|
+
`<tosi-code>`'s `value` is the code it contains. Its `mode` attribute sets the
|
|
9
|
+
language (`javascript`, `typescript`, `tjs`, `css`, `html`, `markdown`).
|
|
10
10
|
|
|
11
11
|
```html
|
|
12
12
|
<tosi-code style="width: 100%; height: 100%" mode="css">
|
|
@@ -16,51 +16,33 @@ body {
|
|
|
16
16
|
</tosi-code>
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
-
The `<tosi-code>` element has an `editor` property that gives you its
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
The `<tosi-code>` element has an `editor` property that gives you its CodeMirror
|
|
20
|
+
[`EditorView`](https://codemirror.net/docs/ref/#view.EditorView), and
|
|
21
|
+
`undo()` / `redo()` / `canUndo()` / `canRedo()` methods for history control.
|
|
22
|
+
|
|
23
|
+
CodeMirror is loaded lazily on first use — a page with no `<tosi-code>` bundles
|
|
24
|
+
none of it.
|
|
22
25
|
*/
|
|
23
26
|
/*{ "parent": "Components" }*/
|
|
24
|
-
import { Component as WebComponent } from 'tosijs';
|
|
25
|
-
import { scriptTag } from './via-tag';
|
|
27
|
+
import { Component as WebComponent, elements } from 'tosijs';
|
|
26
28
|
import { tosiDiff } from './diff';
|
|
27
|
-
const
|
|
28
|
-
const DEFAULT_THEME = 'ace/theme/tomorrow';
|
|
29
|
-
const getAce = async () => {
|
|
30
|
-
const { ace } = await scriptTag(`${ACE_BASE_URL}ace.min.js`);
|
|
31
|
-
return ace;
|
|
32
|
-
};
|
|
33
|
-
const makeCodeEditor = async (codeElement, mode = 'html', options = {}, theme = DEFAULT_THEME) => {
|
|
34
|
-
const ace = await getAce();
|
|
35
|
-
ace.config.set('basePath', ACE_BASE_URL);
|
|
36
|
-
const editor = ace.edit(codeElement, {
|
|
37
|
-
mode: `ace/mode/${mode}`,
|
|
38
|
-
tabSize: 2,
|
|
39
|
-
useSoftTabs: true,
|
|
40
|
-
useWorker: false,
|
|
41
|
-
// Wrap wide lines instead of scrolling horizontally; indentedSoftWrap keeps
|
|
42
|
-
// wrapped continuations aligned under the line's indent.
|
|
43
|
-
wrap: true,
|
|
44
|
-
indentedSoftWrap: true,
|
|
45
|
-
...options,
|
|
46
|
-
});
|
|
47
|
-
editor.setTheme(theme);
|
|
48
|
-
return { ace, editor };
|
|
49
|
-
};
|
|
29
|
+
const { div } = elements;
|
|
50
30
|
export class CodeEditor extends WebComponent {
|
|
51
31
|
static preferredTagName = 'tosi-code';
|
|
52
32
|
source = '';
|
|
33
|
+
_handle;
|
|
34
|
+
_loadPromise;
|
|
35
|
+
_appliedMode = '';
|
|
36
|
+
_appliedDisabled;
|
|
53
37
|
get value() {
|
|
54
|
-
return this.
|
|
38
|
+
return this._handle ? this._handle.getValue() : this.source;
|
|
55
39
|
}
|
|
56
40
|
set value(text) {
|
|
57
|
-
if (this.
|
|
58
|
-
this.
|
|
41
|
+
if (this._handle) {
|
|
42
|
+
this._handle.setValue(text);
|
|
59
43
|
}
|
|
60
44
|
else {
|
|
61
|
-
this.
|
|
62
|
-
this.editor.clearSelection();
|
|
63
|
-
this.editor.session.getUndoManager().reset();
|
|
45
|
+
this.source = text;
|
|
64
46
|
}
|
|
65
47
|
}
|
|
66
48
|
// Baseline for `showDiff()` — the version to diff the current `value` against.
|
|
@@ -72,9 +54,9 @@ export class CodeEditor extends WebComponent {
|
|
|
72
54
|
set original(text) {
|
|
73
55
|
this._original = text;
|
|
74
56
|
}
|
|
75
|
-
// Diff overlay —
|
|
76
|
-
//
|
|
77
|
-
// editor
|
|
57
|
+
// Diff overlay — built only on the editor's public surface (`value` + `original`)
|
|
58
|
+
// and the tosi-diff component, never the underlying editor's API, so it is
|
|
59
|
+
// editor-agnostic (this is why it survived the Ace → CodeMirror swap untouched).
|
|
78
60
|
diffOverlay;
|
|
79
61
|
get showingDiff() {
|
|
80
62
|
return this.diffOverlay !== undefined && !this.diffOverlay.hidden;
|
|
@@ -103,20 +85,27 @@ export class CodeEditor extends WebComponent {
|
|
|
103
85
|
}
|
|
104
86
|
static initAttributes = {
|
|
105
87
|
mode: 'javascript',
|
|
106
|
-
theme: DEFAULT_THEME,
|
|
107
88
|
disabled: false,
|
|
108
89
|
};
|
|
109
90
|
role = 'code editor';
|
|
110
|
-
|
|
111
|
-
_editor;
|
|
112
|
-
_editorPromise;
|
|
113
|
-
options = {};
|
|
114
|
-
get ace() {
|
|
115
|
-
return this._ace;
|
|
116
|
-
}
|
|
91
|
+
/** The underlying CodeMirror EditorView (undefined until loaded). */
|
|
117
92
|
get editor() {
|
|
118
|
-
return this.
|
|
93
|
+
return this._handle?.view;
|
|
94
|
+
}
|
|
95
|
+
// History control — so consumers use these instead of reaching into `editor`.
|
|
96
|
+
undo() {
|
|
97
|
+
this._handle?.undo();
|
|
98
|
+
}
|
|
99
|
+
redo() {
|
|
100
|
+
this._handle?.redo();
|
|
101
|
+
}
|
|
102
|
+
canUndo() {
|
|
103
|
+
return this._handle?.canUndo() ?? false;
|
|
119
104
|
}
|
|
105
|
+
canRedo() {
|
|
106
|
+
return this._handle?.canRedo() ?? false;
|
|
107
|
+
}
|
|
108
|
+
content = () => [div({ part: 'host' })];
|
|
120
109
|
static shadowStyleSpec = {
|
|
121
110
|
':host': {
|
|
122
111
|
display: 'block',
|
|
@@ -124,32 +113,49 @@ export class CodeEditor extends WebComponent {
|
|
|
124
113
|
width: '100%',
|
|
125
114
|
height: '100%',
|
|
126
115
|
},
|
|
116
|
+
'[part="host"]': { height: '100%' },
|
|
117
|
+
'.cm-editor': { height: '100%' },
|
|
118
|
+
'.cm-scroller': {
|
|
119
|
+
outline: 'none',
|
|
120
|
+
fontFamily: "Menlo, Monaco, Consolas, 'Courier New', monospace",
|
|
121
|
+
},
|
|
127
122
|
};
|
|
128
123
|
onResize() {
|
|
129
|
-
|
|
130
|
-
this.editor.resize(true);
|
|
131
|
-
}
|
|
124
|
+
this._handle?.refresh();
|
|
132
125
|
}
|
|
133
126
|
connectedCallback() {
|
|
134
127
|
super.connectedCallback();
|
|
135
128
|
if (this.source === '') {
|
|
136
129
|
this.value = this.textContent !== null ? this.textContent.trim() : '';
|
|
137
130
|
}
|
|
138
|
-
if (this.
|
|
139
|
-
|
|
140
|
-
this.
|
|
141
|
-
this.
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
131
|
+
if (this._loadPromise === undefined) {
|
|
132
|
+
// Lazy chunk — CodeMirror only enters the bundle here, on first editor use.
|
|
133
|
+
this._loadPromise = import('./code-editor-cm').then(({ createCmEditor }) => {
|
|
134
|
+
const handle = createCmEditor(this.parts.host, {
|
|
135
|
+
value: this.source,
|
|
136
|
+
mode: this.mode,
|
|
137
|
+
readOnly: this.disabled,
|
|
138
|
+
root: this.shadowRoot ?? undefined,
|
|
139
|
+
onChange: (value) => this.dispatchEvent(new CustomEvent('change', { detail: { value } })),
|
|
140
|
+
});
|
|
141
|
+
this._handle = handle;
|
|
142
|
+
this._appliedMode = this.mode;
|
|
143
|
+
this._appliedDisabled = this.disabled;
|
|
144
|
+
return handle;
|
|
146
145
|
});
|
|
147
146
|
}
|
|
148
147
|
}
|
|
149
148
|
render() {
|
|
150
149
|
super.render();
|
|
151
|
-
if (this.
|
|
152
|
-
this.
|
|
150
|
+
if (this._handle) {
|
|
151
|
+
if (this.disabled !== this._appliedDisabled) {
|
|
152
|
+
this._handle.setReadOnly(this.disabled);
|
|
153
|
+
this._appliedDisabled = this.disabled;
|
|
154
|
+
}
|
|
155
|
+
if (this.mode !== this._appliedMode) {
|
|
156
|
+
this._handle.setMode(this.mode);
|
|
157
|
+
this._appliedMode = this.mode;
|
|
158
|
+
}
|
|
153
159
|
}
|
|
154
160
|
}
|
|
155
161
|
}
|
package/dist/doc-browser.js
CHANGED
|
@@ -1250,7 +1250,11 @@ export function createDocBrowser(options) {
|
|
|
1250
1250
|
if (isTestFrame)
|
|
1251
1251
|
return; // Don't run background tests in test iframe
|
|
1252
1252
|
backgroundTestsStarted = true;
|
|
1253
|
-
// Find all docs that have test blocks
|
|
1253
|
+
// Find all docs that have test blocks. (Pages with examples but no explicit
|
|
1254
|
+
// tests are covered cheaply at BUILD time by the transpile check —
|
|
1255
|
+
// src/doc-system/site/check-examples.ts — without loading them in a browser,
|
|
1256
|
+
// so the runner doesn't have to render every heavy page just to notice a
|
|
1257
|
+
// build error.)
|
|
1254
1258
|
const docsWithTests = docs.filter((doc) => doc.text.includes('```test'));
|
|
1255
1259
|
pagesWithTests = docsWithTests.length;
|
|
1256
1260
|
if (pagesWithTests > 0) {
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Doc } from './docs';
|
|
2
|
+
export interface ExampleProblem {
|
|
3
|
+
filename: string;
|
|
4
|
+
title: string;
|
|
5
|
+
lang: string;
|
|
6
|
+
error: string;
|
|
7
|
+
snippet: string;
|
|
8
|
+
}
|
|
9
|
+
/** Transpile-check every executable block in the corpus. Returns the problems. */
|
|
10
|
+
export declare function checkExamples(docs: Doc[], opts?: {
|
|
11
|
+
contextKeys?: string[];
|
|
12
|
+
}): Promise<ExampleProblem[]>;
|
|
13
|
+
/** Format problems for a build log. */
|
|
14
|
+
export declare function formatExampleProblems(problems: ExampleProblem[]): string;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Build-time transpile check for live examples.
|
|
3
|
+
|
|
4
|
+
Every executable code block in the corpus (`js` / `tjs` / `ts` / `test`) is put
|
|
5
|
+
through the SAME front half of the runtime pipeline the live-example component
|
|
6
|
+
uses — `rewriteImports` → the tjs-lang transform → `new AsyncFunction(...)` — so a
|
|
7
|
+
block that can't build (a tjs/TS syntax error, an unsupported import, or
|
|
8
|
+
illustrative config mistakenly tagged with an executable language like `ts`
|
|
9
|
+
instead of the display-only `typescript`) fails the build *at authoring time*,
|
|
10
|
+
on every page, instead of silently rendering an error only when someone opens
|
|
11
|
+
that page. `html` / `css` and display-only languages (`typescript`, `json`, …)
|
|
12
|
+
are not executed, so they're skipped.
|
|
13
|
+
|
|
14
|
+
Build-time only (bun). Never import from browser code.
|
|
15
|
+
*/
|
|
16
|
+
import { marked } from 'marked';
|
|
17
|
+
import { rewriteImports, AsyncFunction, loadTransform } from '../../live-example/code-transform';
|
|
18
|
+
// The default live-example context (matches the IIFE globals the pages provide).
|
|
19
|
+
// A project that sets a custom `context` on its <tosi-doc-system> can pass its
|
|
20
|
+
// own keys; these are the tosijs-ui defaults.
|
|
21
|
+
const DEFAULT_CONTEXT_KEYS = ['tosijs', 'tosijs-ui'];
|
|
22
|
+
const EXECUTABLE = new Set(['js', 'tjs', 'ts', 'test']);
|
|
23
|
+
/** The bare dialect from a fence info string ('js#my-id' → 'js'); '' if none. */
|
|
24
|
+
function dialectOf(info) {
|
|
25
|
+
return (info ?? '').match(/^[a-z]+/)?.[0] ?? '';
|
|
26
|
+
}
|
|
27
|
+
/** Collect every fenced code block in a doc (recursing into lists/quotes). */
|
|
28
|
+
function collectCodeTokens(text) {
|
|
29
|
+
const out = [];
|
|
30
|
+
const walk = (tokens) => {
|
|
31
|
+
for (const t of tokens) {
|
|
32
|
+
if (t.type === 'code')
|
|
33
|
+
out.push({ lang: dialectOf(t.lang), text: t.text });
|
|
34
|
+
if (Array.isArray(t.tokens))
|
|
35
|
+
walk(t.tokens);
|
|
36
|
+
if (Array.isArray(t.items))
|
|
37
|
+
walk(t.items); // list items
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
walk(marked.lexer(text));
|
|
41
|
+
return out;
|
|
42
|
+
}
|
|
43
|
+
/** Transpile-check every executable block in the corpus. Returns the problems. */
|
|
44
|
+
export async function checkExamples(docs, opts = {}) {
|
|
45
|
+
const contextKeys = opts.contextKeys ?? DEFAULT_CONTEXT_KEYS;
|
|
46
|
+
const problems = [];
|
|
47
|
+
for (const doc of docs) {
|
|
48
|
+
for (const block of collectCodeTokens(doc.text)) {
|
|
49
|
+
if (!EXECUTABLE.has(block.lang))
|
|
50
|
+
continue;
|
|
51
|
+
// `test` blocks are conventional JS/TS, transpiled as plain js.
|
|
52
|
+
const dialect = block.lang === 'test' ? 'js' : block.lang;
|
|
53
|
+
try {
|
|
54
|
+
const rewritten = rewriteImports(block.text, contextKeys);
|
|
55
|
+
let js;
|
|
56
|
+
if (dialect === 'ts') {
|
|
57
|
+
// Use bun's own transpiler — network-free (the runtime `ts` path
|
|
58
|
+
// fetches the TypeScript compiler from a CDN, which can't run here).
|
|
59
|
+
// We only need to validate that the source builds, not reproduce tjs
|
|
60
|
+
// lowering exactly.
|
|
61
|
+
js = new Bun.Transpiler({ loader: 'ts' }).transformSync(rewritten);
|
|
62
|
+
}
|
|
63
|
+
else if (dialect === 'tjs') {
|
|
64
|
+
const transform = await loadTransform('tjs');
|
|
65
|
+
js = (await transform(rewritten, { transforms: ['typescript'] })).code;
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
js = rewritten; // `js` / `test` are already JS
|
|
69
|
+
}
|
|
70
|
+
// Syntax-validate the way the component does before running it.
|
|
71
|
+
// eslint-disable-next-line no-new
|
|
72
|
+
new AsyncFunction(js);
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
problems.push({
|
|
76
|
+
filename: doc.filename,
|
|
77
|
+
title: doc.title,
|
|
78
|
+
lang: block.lang,
|
|
79
|
+
error: err.message || String(err),
|
|
80
|
+
snippet: block.text.split('\n').slice(0, 3).join('\n'),
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return problems;
|
|
86
|
+
}
|
|
87
|
+
/** Format problems for a build log. */
|
|
88
|
+
export function formatExampleProblems(problems) {
|
|
89
|
+
return problems
|
|
90
|
+
.map((p) => ` ✗ ${p.filename} (${p.lang}) — ${p.title}\n` +
|
|
91
|
+
` ${p.error}\n` +
|
|
92
|
+
p.snippet
|
|
93
|
+
.split('\n')
|
|
94
|
+
.map((l) => ` | ${l}`)
|
|
95
|
+
.join('\n'))
|
|
96
|
+
.join('\n\n');
|
|
97
|
+
}
|
|
@@ -16,6 +16,7 @@ import { existsSync, mkdirSync } from 'fs';
|
|
|
16
16
|
import { gzipSync } from 'zlib';
|
|
17
17
|
import { $ } from 'bun';
|
|
18
18
|
import { extractDocs } from './docs';
|
|
19
|
+
import { checkExamples, formatExampleProblems } from './check-examples';
|
|
19
20
|
import { ensureSections } from './sections';
|
|
20
21
|
import { generateLlmsTxt } from './make-llms-txt';
|
|
21
22
|
import { generateSite } from './generate-site';
|
|
@@ -68,6 +69,21 @@ export async function buildSite(config) {
|
|
|
68
69
|
sectionsDir: config.sectionsDir ?? 'src/docs',
|
|
69
70
|
reExtract: extract,
|
|
70
71
|
});
|
|
72
|
+
// Fail fast on any live example that can't build — a real syntax/import error,
|
|
73
|
+
// or illustrative code mistakenly tagged executable (`js`/`ts`/…) instead of
|
|
74
|
+
// display-only `typescript`. Runs on the whole corpus, so it catches breakage
|
|
75
|
+
// on pages the browser test never navigates to.
|
|
76
|
+
if (config.checkExamples !== false) {
|
|
77
|
+
const corpus = JSON.parse(await Bun.file(DOCS_JSON).text());
|
|
78
|
+
const problems = await checkExamples(corpus);
|
|
79
|
+
if (problems.length) {
|
|
80
|
+
throw new Error(`doc-site build: ${problems.length} live example(s) failed to build:\n\n` +
|
|
81
|
+
formatExampleProblems(problems) +
|
|
82
|
+
`\n\nFix the code, or — if a block is illustrative and not meant to run —` +
|
|
83
|
+
` tag it with a display-only language like \`typescript\` instead of` +
|
|
84
|
+
` \`js\`/\`ts\`. (Disable with checkExamples: false.)`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
71
87
|
// Copy static-asset dirs into the web root.
|
|
72
88
|
const staticDirs = config.staticDirs ?? (existsSync('demo/static') ? ['demo/static'] : ['static']);
|
|
73
89
|
for (const dir of staticDirs) {
|
|
@@ -143,6 +143,15 @@ export interface SiteConfig {
|
|
|
143
143
|
* `sort: 'filename'` gives a folder of chapters natural order with no metadata.
|
|
144
144
|
*/
|
|
145
145
|
book?: BookManifest;
|
|
146
|
+
/**
|
|
147
|
+
* Fail the build when a live example can't transpile — a real syntax/import
|
|
148
|
+
* error, or illustrative code mistakenly tagged with an executable language
|
|
149
|
+
* (`js`/`ts`/`tjs`/`test`) instead of the display-only `typescript`. Catches it
|
|
150
|
+
* at build time, on every page, instead of only when someone opens that page.
|
|
151
|
+
* Default true; assumes the default `tosijs` / `tosijs-ui` example context —
|
|
152
|
+
* set false if your examples import from a custom `context` the check can't see.
|
|
153
|
+
*/
|
|
154
|
+
checkExamples?: boolean;
|
|
146
155
|
/** served web-root output dir, default 'docs' */
|
|
147
156
|
outputDir?: string;
|
|
148
157
|
/** dev-server port, default 8787 */
|