tosijs-ui 1.16.14 → 1.16.16

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 CHANGED
@@ -1,5 +1,84 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.16.16
4
+
5
+ For tjs-lang's book (GitHub #216 item 3 and #210 item 5), plus one change to `llms.txt`.
6
+ Both tjs-lang items are feature-detected, so nothing changes until your tjs-lang has them.
7
+
8
+ - **A console line shows its own value, and the JavaScript it became.** A dialect's
9
+ transform may now return `prelude` and `body` beside `code`: its runtime setup, and what
10
+ the source itself became. When it does (tjs-lang 0.14.0-rc.5 and later), a line typed at
11
+ an example's console is evaluated for its own value, so a declaration shows `undefined`
12
+ where it used to show the setup's last assignment, and a line that needs both the setup
13
+ and `await` shows what it resolved to. The input also gains a small **JS** button that
14
+ shows the JavaScript for that line. A transform that returns only `code` behaves as
15
+ before, with no button.
16
+ - **`tjs` and `ts` examples may be written as modules.** `export` is removed from `tjs` and
17
+ `ts` examples before they run, by tjs-lang's own parser (`stripExports`, from
18
+ 0.14.0-rc.4), and the build's example check does the same. An example runs as a function
19
+ body, where `export` is a syntax error, so with an older tjs-lang it fails as it always
20
+ has. `js` examples are unchanged.
21
+ - **The tjs-lang peer range admits the 0.14 release candidates** (`^0.13.1 || ^0.14.0-rc.5`).
22
+ It read `^0.14.0`, which excludes every prerelease, so `npm install` refused the one
23
+ tjs-lang that has these features. Run here with 0.14.0-rc.5 installed: the browser lane
24
+ on Chromium, and the new tests of both features on all three browsers.
25
+ - **`registerDialect` documents what a transform should return.** `testRunner` has been
26
+ honoured since 1.16.8 but was not in the type or the docs: a dialect that returns only
27
+ `code` has its inline tests run from their raw text, with JavaScript's semantics. The
28
+ `transform` type now names `testRunner`, `prelude` and `body`.
29
+ - **An example re-run while it was still loading could leave its page never finishing its
30
+ tests.** A run replaced by a newer one (tests turned on, or an edit, while the first run
31
+ was still awaiting a fetch) went on to run its test block against a preview no longer in
32
+ the page. A test that waits for something to render then waited for ever and the example
33
+ stayed marked as running, so the page was reported as not finished after 45 seconds. A
34
+ replaced run now stops before its tests, and one replaced while its tests are running
35
+ no longer writes its results over the newer run's. Seen on the data-table page off
36
+ localhost.
37
+ - **`llms.txt` no longer carries a paragraph about the dev server.** A site with
38
+ `haltijaDev` set used to tell readers of its `llms.txt` to run `bun start` and drive the
39
+ page with the `hj` CLI. That file is for whoever consumes the library, and those readers
40
+ have no repo to run. If you relied on it to brief an agent working in your project, put
41
+ the same note in your `CLAUDE.md` or `AGENTS.md`.
42
+
43
+ The tjs-lang this package pins for sites that load it from a CDN is still 0.13.13, which has
44
+ neither feature. A site that serves its own tjs-lang (the installed peer, copied
45
+ same-origin by the build) gets both as soon as that is 0.14.0-rc.5.
46
+
47
+ `LlmsTxtMeta.haltijaDev` is still accepted and now ignored (deprecated). `dist/iife.js`
48
+ grows by about 0.4 kB gzip.
49
+
50
+ ## 1.16.15
51
+
52
+ For projects that build their own docs bundle, from tosijs's upgrade to 1.16.13 (board
53
+ #3248). Two failures that left a green build over a broken site now say so.
54
+
55
+ - **`restampSite(config)`, from `tosijs-ui/site`.** If your site has no `bundleEntry` and you
56
+ overwrite `/iife.js` after `buildSite`, call it afterwards. It checks that the file you
57
+ wrote registers `<tosi-doc-system>`, and `<tosi-example>` when the corpus has live
58
+ examples, and then re-stamps the script and stylesheet `?v=` in every page from the files
59
+ actually on disk (the stamp `buildSite` wrote described the bundle it replaced, so a
60
+ returning visitor could run a cached library against new pages). A bundle whose entry
61
+ imports only the root `tosijs-ui` registers neither element since 1.14.0, and until now
62
+ nothing noticed: pages served, no header, no nav, no examples, no doc tests. A warning by
63
+ default. Under `strict: true` it throws before writing anything. It also throws when
64
+ there is nothing to re-stamp, on a `bundleEntry` site, and when it cannot read the corpus.
65
+ - **The build reports two copies of CodeMirror.** After an upgrade a lockfile can keep an
66
+ older hoisted `@codemirror/state` while the newer one nests under
67
+ `node_modules/tosijs-ui/`; every `<tosi-code>` is then blank and nothing fails.
68
+ `buildSite` now asks the module resolver whether the packages the editor is built from
69
+ agree, and names both copies and the command that fixes it
70
+ (`bun update @codemirror/state @codemirror/view`). A warning by default. **Under `strict: true` it fails
71
+ `buildSite` on a site with a `bundleEntry`, and fails `restampSite`**, which is new in a
72
+ patch: those are the two places a bundle is built from the install, and such a build was
73
+ already producing blank editors. Otherwise `buildSite` says it once and carries on. At runtime `<tosi-code>` logs the same explanation beside CodeMirror's own
74
+ "Unrecognized extension value" error. Cost: `iife.js` +176 bytes gzip.
75
+ - **Fixed: `llms.txt` told agents to try `/carousel/index.md` on every site.** That is a
76
+ tosijs-ui page. The example is now a page of the site being built.
77
+ - **The "relative imports without a file extension" warning says what it read.** It is
78
+ about the `.js` files tsc emitted, at the moment it emitted them. A project that replaces
79
+ those with bundles afterwards could not find the files it named; the warning now says so
80
+ and points at `"emitDeclarationOnly": true`.
81
+
3
82
  ## 1.16.14
4
83
 
5
84
  More reach for curated schema forms (#221): into arrays and unions, grouped into sections,
@@ -197,27 +197,44 @@ export function createCmEditor(parent, opts = {}) {
197
197
  const readonly = new Compartment();
198
198
  const highlight = new Compartment();
199
199
  const langExt = opts.languageExtension ?? standardLanguageBundle(opts.mode ?? 'javascript');
200
- const view = new EditorView({
201
- parent,
202
- root: opts.root,
203
- state: EditorState.create({
204
- doc: opts.value ?? '',
205
- extensions: [
206
- baseExtensions(),
207
- highlight.of(highlightFor(isDarkMode())),
208
- language.of(langExt),
209
- readonly.of(EditorState.readOnly.of(!!opts.readOnly)),
210
- EditorView.updateListener.of((u) => {
211
- if (!u.docChanged || !opts.onChange)
212
- return;
213
- // A programmatic `value` set is not a change the USER made. See `programmatic`.
214
- if (u.transactions.some((t) => t.annotation(programmatic)))
215
- return;
216
- opts.onChange(u.state.doc.toString());
217
- }),
218
- ],
219
- }),
220
- });
200
+ let view;
201
+ try {
202
+ view = new EditorView({
203
+ parent,
204
+ root: opts.root,
205
+ state: EditorState.create({
206
+ doc: opts.value ?? '',
207
+ extensions: [
208
+ baseExtensions(),
209
+ highlight.of(highlightFor(isDarkMode())),
210
+ language.of(langExt),
211
+ readonly.of(EditorState.readOnly.of(!!opts.readOnly)),
212
+ EditorView.updateListener.of((u) => {
213
+ if (!u.docChanged || !opts.onChange)
214
+ return;
215
+ // A programmatic `value` set is not a change the USER made. See `programmatic`.
216
+ if (u.transactions.some((t) => t.annotation(programmatic)))
217
+ return;
218
+ opts.onChange(u.state.doc.toString());
219
+ }),
220
+ ],
221
+ }),
222
+ });
223
+ }
224
+ catch (error) {
225
+ /*
226
+ CodeMirror recognises extensions by identity, so a second copy of `@codemirror/state`
227
+ in the install makes it reject its own language packages. The editor is then blank and
228
+ the message names an internal; say what happened and what fixes it (board #3248).
229
+ */
230
+ if (/Unrecognized extension value/.test(String(error?.message))) {
231
+ console.error('<tosi-code>: CodeMirror rejected an extension, which means two copies of ' +
232
+ '@codemirror/state (or @codemirror/view) are loaded. Every CodeMirror package ' +
233
+ 'must share one. Run `bun update @codemirror/state @codemirror/view` (or ' +
234
+ '`npm dedupe`), then rebuild.');
235
+ }
236
+ throw error;
237
+ }
221
238
  // Re-apply the right highlight palette when the doc-system toggles dark mode
222
239
  // (it flips the `darkmode` class on <body>).
223
240
  let dark = isDarkMode();
@@ -22,6 +22,11 @@ export interface AssetStampResult {
22
22
  * keeps executing the bundle it cached hours ago — through edits, through reloads.
23
23
  */
24
24
  export declare function computeAssetStamp(paths: string[], fallback: string): Promise<AssetStampResult>;
25
+ /**
26
+ * The assets the stamp covers, in hashing order. ONE list, for `buildSite` and for
27
+ * `restampSite`: two would let a re-stamp hash a different set and never match a build.
28
+ */
29
+ export declare function assetStampInputs(publicDir: string, scriptName: string, hydrateName?: string): string[];
25
30
  /**
26
31
  * The message for a named-but-missing input. Separate so the test asserts the real string
27
32
  * rather than a paraphrase of it.
@@ -53,6 +53,17 @@ export async function computeAssetStamp(paths, fallback) {
53
53
  usedFallback: !sawAny,
54
54
  };
55
55
  }
56
+ /**
57
+ * The assets the stamp covers, in hashing order. ONE list, for `buildSite` and for
58
+ * `restampSite`: two would let a re-stamp hash a different set and never match a build.
59
+ */
60
+ export function assetStampInputs(publicDir, scriptName, hydrateName) {
61
+ return [
62
+ hydrateName && `${publicDir}/${hydrateName}`,
63
+ `${publicDir}/${scriptName}`,
64
+ `${publicDir}/doc-system.css`,
65
+ ].filter(Boolean);
66
+ }
56
67
  /**
57
68
  * The message for a named-but-missing input. Separate so the test asserts the real string
58
69
  * rather than a paraphrase of it.
@@ -22,7 +22,7 @@ executed, so they're skipped.
22
22
  Build-time only (bun). Never import from browser code.
23
23
  */
24
24
  import { collectCodeTokens } from '../code-fences.js';
25
- import { rewriteImports, AsyncFunction, loadTransform, transformAvailable, UnsupportedImportError, } from '../../live-example/code-transform.js';
25
+ import { rewriteImports, AsyncFunction, loadTransform, transformAvailable, UnsupportedImportError, withoutExports, } from '../../live-example/code-transform.js';
26
26
  import { isLiveFence } from '../example-policy.js';
27
27
  // The default live-example context (matches the IIFE globals the pages provide).
28
28
  // A project that sets a custom `context` on its <tosi-doc-system> can pass its
@@ -130,7 +130,9 @@ export async function checkExamples(docs, opts = {}) {
130
130
  // We only need to validate that the source builds, not reproduce tjs
131
131
  // lowering exactly.
132
132
  tsTranspiler ??= new Bun.Transpiler({ loader: 'ts' });
133
- js = tsTranspiler.transformSync(rewritten);
133
+ // The page removes `export` from a `ts` example when its tjs-lang can; so does
134
+ // this, or a module-style example the page runs would fail the build.
135
+ js = await withoutExports(tsTranspiler.transformSync(rewritten));
134
136
  }
135
137
  else if (dialect === 'tjs') {
136
138
  const transform = await loadTransform('tjs');
@@ -0,0 +1,13 @@
1
+ declare global {
2
+ var Bun: any;
3
+ }
4
+ /**
5
+ * For each package that must be shared, the distinct copies reachable from tosijs-ui, from
6
+ * every `@codemirror/*` package it depends on, and from tjs-lang's editor extension. More than one path for a name is
7
+ * the defect. `uiDir` is where tosijs-ui itself is installed.
8
+ */
9
+ export declare function codemirrorCopies(root: string, uiDir: string): Record<string, string[]>;
10
+ /** What to say about `codemirrorCopies`' answer, or null when each package has one copy. */
11
+ export declare function duplicateCodemirrorWarning(copies: Record<string, string[]>): string | null;
12
+ /** tosijs-ui's own package directory, found from this module's location. */
13
+ export declare function tosijsUiDir(moduleDir: string): string;
@@ -0,0 +1,115 @@
1
+ /*
2
+ Two copies of CodeMirror in one install (board #3248, item 2; the same failure as #131).
3
+
4
+ Every CodeMirror package must share ONE `@codemirror/state` (and one `@codemirror/view`):
5
+ extensions are recognised by identity, so a language package holding a different copy hands
6
+ the editor values it rejects ("Unrecognized extension value in extension set"), and every
7
+ `<tosi-code>` renders blank while the build and the tests stay green.
8
+
9
+ It arrives through an ordinary upgrade. A lockfile that pinned an older hoisted
10
+ `@codemirror/state` keeps it, the package manager nests the newer one tosijs-ui now needs
11
+ under `node_modules/tosijs-ui/`, and the `lang-*` packages go on resolving the hoisted copy.
12
+
13
+ Build-time only: it asks the module resolver, which is the thing that decides what loads.
14
+ */
15
+ import * as path from 'path';
16
+ import { existsSync, readFileSync, realpathSync } from 'fs';
17
+ const SHARED = ['@codemirror/state', '@codemirror/view'];
18
+ /** The package directory a resolved file belongs to (the one holding its package.json). */
19
+ function packageDirOf(file) {
20
+ let dir = path.dirname(file);
21
+ while (dir !== path.dirname(dir)) {
22
+ if (existsSync(path.join(dir, 'package.json')))
23
+ return dir;
24
+ dir = path.dirname(dir);
25
+ }
26
+ return path.dirname(file);
27
+ }
28
+ function resolveFrom(specifier, from) {
29
+ try {
30
+ return realpathSync(Bun.resolveSync(specifier, from));
31
+ }
32
+ catch {
33
+ return null;
34
+ }
35
+ }
36
+ /**
37
+ * For each package that must be shared, the distinct copies reachable from tosijs-ui, from
38
+ * every `@codemirror/*` package it depends on, and from tjs-lang's editor extension. More than one path for a name is
39
+ * the defect. `uiDir` is where tosijs-ui itself is installed.
40
+ */
41
+ export function codemirrorCopies(root, uiDir) {
42
+ let deps = [];
43
+ try {
44
+ const pkg = JSON.parse(readFileSync(path.join(uiDir, 'package.json'), 'utf8'));
45
+ deps = Object.keys(pkg.dependencies ?? {}).filter((name) => name.startsWith('@codemirror/'));
46
+ }
47
+ catch {
48
+ // no package.json to read: only the two direct resolutions are compared
49
+ }
50
+ // Where each CodeMirror package tosijs-ui uses actually lives: each resolves its OWN
51
+ // dependencies from there, which is where a second copy shows up.
52
+ /*
53
+ NOT the project root. A project may hold its own, unrelated `@codemirror/state` for an
54
+ editor of its own, beside a consistent set under tosijs-ui: that is two copies in the
55
+ install and no defect. The defect is the packages the editor is BUILT FROM disagreeing,
56
+ and the upgrade case shows up there, since the hoisted `lang-*` resolve the hoisted copy.
57
+ */
58
+ const origins = new Set([uiDir]);
59
+ for (const dep of deps) {
60
+ const resolved = resolveFrom(dep, uiDir);
61
+ if (resolved)
62
+ origins.add(packageDirOf(resolved));
63
+ }
64
+ // tjs-lang's CodeMirror extension is bundled into the same editor (see bundle-guard.ts).
65
+ const tjs = resolveFrom('tjs-lang/editors/codemirror', root);
66
+ if (tjs)
67
+ origins.add(packageDirOf(tjs));
68
+ const copies = {};
69
+ for (const name of SHARED) {
70
+ const found = new Set();
71
+ for (const origin of origins) {
72
+ const resolved = resolveFrom(name, origin);
73
+ if (resolved)
74
+ found.add(packageDirOf(resolved));
75
+ }
76
+ copies[name] = [...found].sort();
77
+ }
78
+ return copies;
79
+ }
80
+ /** What to say about `codemirrorCopies`' answer, or null when each package has one copy. */
81
+ export function duplicateCodemirrorWarning(copies) {
82
+ const doubled = Object.entries(copies).filter(([, dirs]) => dirs.length > 1);
83
+ if (doubled.length === 0)
84
+ return null;
85
+ return (`\n⚠️ This install has more than one copy of a CodeMirror package that must be shared:\n` +
86
+ doubled
87
+ .map(([name, dirs]) => ` ${name}\n` + dirs.map((dir) => ` ${dir}\n`).join(''))
88
+ .join('') +
89
+ ` CodeMirror recognises extensions by identity, so in a bundle built from this install\n` +
90
+ ` every <tosi-code> renders blank ("Unrecognized extension value in extension set")\n` +
91
+ ` and nothing fails. It usually follows an upgrade: the lockfile kept an older hoisted\n` +
92
+ ` copy. Bring them back to one:\n` +
93
+ ` bun update ${doubled
94
+ .map(([name]) => name)
95
+ .join(' ')} (npm: npm dedupe)\n`);
96
+ }
97
+ /** tosijs-ui's own package directory, found from this module's location. */
98
+ export function tosijsUiDir(moduleDir) {
99
+ let dir = moduleDir;
100
+ while (dir !== path.dirname(dir)) {
101
+ const file = path.join(dir, 'package.json');
102
+ if (existsSync(file)) {
103
+ try {
104
+ if (JSON.parse(readFileSync(file, 'utf8')).name === 'tosijs-ui') {
105
+ return dir;
106
+ }
107
+ }
108
+ catch {
109
+ // unreadable: keep walking up
110
+ }
111
+ }
112
+ dir = path.dirname(dir);
113
+ }
114
+ return moduleDir;
115
+ }
@@ -1,6 +1,8 @@
1
1
  export { defineSiteConfig } from './site-config.js';
2
2
  export type { SiteConfig, SiteHost, LibraryBuildContext, } from './site-config.js';
3
3
  export { buildSite } from './orchestrator.js';
4
+ export { restampSite } from './restamp.js';
5
+ export type { RestampResult } from './restamp.js';
4
6
  export { SiteMisconfiguredError } from './build-warnings.js';
5
7
  export { testReportVerdict } from './test-report.js';
6
8
  export type { TestReportLike, TestReportVerdict } from './test-report.js';
@@ -5,6 +5,9 @@ browser code. See ./README is at ../doc-site-system.md and the repo docs.
5
5
  */
6
6
  export { defineSiteConfig } from './site-config.js';
7
7
  export { buildSite } from './orchestrator.js';
8
+ // For a project that replaces the hydration bundle after `buildSite`: re-stamp the pages
9
+ // from what is served, and check it registers the doc system (board #3248).
10
+ export { restampSite } from './restamp.js';
8
11
  // Thrown by `buildSite` under `strict: true` — exported so a caller can tell a refused
9
12
  // misconfiguration from a crash (`catch (e) { if (e instanceof SiteMisconfiguredError) … }`).
10
13
  export { SiteMisconfiguredError } from './build-warnings.js';
@@ -30,14 +30,9 @@ export interface LlmsTxtMeta {
30
30
  /** optional framing line(s) under the description */
31
31
  tagline?: string;
32
32
  /**
33
- * Mirrors `SiteConfig.haltijaDev`. When set, `llms.txt` tells an agent it can DRIVE the
34
- * running page rather than reason about it from source — which is the single most useful
35
- * thing an agent can know about a project it is working on, and was previously discoverable
36
- * only by reading the doc-site-system page in full.
37
- *
38
- * `'tunnel'` also carries that meaning — it is `true` plus a remote path — so the note is
39
- * written on truthiness, not on `=== true`. Getting that wrong would silently drop the note
40
- * for exactly the projects that enabled the MORE capable mode.
33
+ * @deprecated Ignored since 1.16.16. It used to add a paragraph about driving the dev
34
+ * server with the `hj` CLI; `llms.txt` is for whoever consumes the library, so that note
35
+ * belongs in the project's own agent instructions. Kept so existing callers still compile.
41
36
  */
42
37
  haltijaDev?: boolean | 'tunnel';
43
38
  /**
@@ -56,6 +51,13 @@ export interface LlmsTxtMeta {
56
51
  * docs and needs no `dist/` library output.
57
52
  */
58
53
  export declare function entriesFromCorpus(corpus: CorpusDoc[], meta: LlmsTxtMeta): LlmsEntry[];
54
+ /**
55
+ * A real page of THIS site to show the `index.md` rule with. It was hard-coded to
56
+ * `/carousel/index.md`, a tosijs-ui page, so every other project's llms.txt pointed agents
57
+ * at a URL it does not have (board #3248). The first page that has a markdown copy and is
58
+ * not the site root.
59
+ */
60
+ export declare function markdownExample(corpus: CorpusDoc[], meta: LlmsTxtMeta): string | undefined;
59
61
  /**
60
62
  * Write an `llms.txt` index. Pass the extracted `corpus` (the build does) to
61
63
  * index every doc by its rendered URL; omit it to fall back to the legacy
@@ -93,6 +93,25 @@ function entriesFromSrcScan() {
93
93
  }
94
94
  return entries.sort((a, b) => a.title.localeCompare(b.title));
95
95
  }
96
+ /**
97
+ * A real page of THIS site to show the `index.md` rule with. It was hard-coded to
98
+ * `/carousel/index.md`, a tosijs-ui page, so every other project's llms.txt pointed agents
99
+ * at a URL it does not have (board #3248). The first page that has a markdown copy and is
100
+ * not the site root.
101
+ */
102
+ export function markdownExample(corpus, meta) {
103
+ const slugMap = buildSlugMap(corpus);
104
+ for (const doc of corpus) {
105
+ if (!doc.title || doc.hidden || doc.noindex) {
106
+ continue;
107
+ }
108
+ const pagePath = pathForSlug(slugMap[doc.filename]);
109
+ if (pagePath === '/')
110
+ continue;
111
+ return `${withBase(meta.basePath, pagePath)}index.md`;
112
+ }
113
+ return undefined;
114
+ }
96
115
  /**
97
116
  * Write an `llms.txt` index. Pass the extracted `corpus` (the build does) to
98
117
  * index every doc by its rendered URL; omit it to fall back to the legacy
@@ -128,28 +147,14 @@ export function generateLlmsTxt(outputPath, meta = {}, corpus) {
128
147
  '',
129
148
  ]
130
149
  : [];
131
- /*
132
- Only advertise browser control when the project has actually opted in. Telling an agent to
133
- drive a page that has no dev channel sends it after a capability that will not answer, and
134
- a wrong affordance costs more than a missing one.
135
- */
136
- const haltijaNote = meta.haltijaDev
137
- ? [
138
- 'Running this project locally (`bun start`), the dev server injects a',
139
- 'localhost-gated dev channel, so an agent can DRIVE the live page with the `hj`',
140
- 'CLI — `hj navigate`, `hj eval`, `hj tree` — instead of inferring behaviour from',
141
- 'source. Injected at serve time only, never in the built output. Note that no',
142
- 'requestAnimationFrame runs under `hj eval`, and tosijs renders are rAF-driven, so',
143
- 'use it to inspect STATE; a painted-output check needs a real test lane.',
144
- '',
145
- ]
146
- : [];
147
150
  // Only for a corpus-driven index: the legacy scan links to dist/ files, not pages.
151
+ const example = corpus ? markdownExample(corpus, meta) : undefined;
148
152
  const markdownNote = corpus && meta.markdownPages !== false
149
153
  ? [
150
- 'Pages below are also available as plain markdown: append `index.md` to the URL',
151
- '(for example `/carousel/index.md`). Prefer that if you only need the text. A page',
152
- 'marked noindex has no markdown copy.',
154
+ 'Pages below are also available as plain markdown: append `index.md` to the URL' +
155
+ (example ? '' : '.'),
156
+ ...(example ? [`(for example \`${example}\`).`] : []),
157
+ 'Prefer that if you only need the text. A page marked noindex has no markdown copy.',
153
158
  '',
154
159
  ]
155
160
  : [];
@@ -178,7 +183,6 @@ export function generateLlmsTxt(outputPath, meta = {}, corpus) {
178
183
  '',
179
184
  ...markdownNote,
180
185
  ...liveExampleNote,
181
- ...haltijaNote,
182
186
  '## Pages',
183
187
  '',
184
188
  ];
@@ -1,3 +1,6 @@
1
+ export declare function registerSiteDialects(config: {
2
+ dialects?: readonly string[];
3
+ }): void;
1
4
  import type { SiteConfig } from './site-config.js';
2
5
  declare global {
3
6
  var Bun: any;
@@ -21,7 +21,8 @@ import * as path from 'path';
21
21
  * that asks "is this fence live?" calls it first rather than depending on build order: the
22
22
  * misconfiguration checks run long before the page generator, which used to be the only caller.
23
23
  */
24
- function registerSiteDialects(config) {
24
+ let codemirrorWarned = false;
25
+ export function registerSiteDialects(config) {
25
26
  for (const name of normalizeDialectNames(config.dialects)) {
26
27
  if (!BUILT_IN_DIALECTS.has(name))
27
28
  registerLiveLanguage(name);
@@ -30,7 +31,7 @@ function registerSiteDialects(config) {
30
31
  import { namedBooks, partitionByBook, DEFAULT_BOOK } from '../book-target.js';
31
32
  import { attachCovers, listEpubVolumes, printCoverManifestUrl, VOLUME_MANIFEST, renderEpubDownloads, } from './epub-volumes.js';
32
33
  import { buildSlugMap } from '../routing.js';
33
- import { computeAssetStamp, missingStampInputWarning } from './asset-stamp.js';
34
+ import { assetStampInputs, computeAssetStamp, missingStampInputWarning, } from './asset-stamp.js';
34
35
  import { existsSync, mkdirSync, readdirSync, readFileSync, unlinkSync, } from 'fs';
35
36
  import { tmpdir } from 'os';
36
37
  import { $, spawn } from 'bun';
@@ -43,10 +44,12 @@ import { generateLlmsTxt } from './make-llms-txt.js';
43
44
  import { DEFAULT_DOC_IGNORES } from './docs.js';
44
45
  import { writesOutsideOutputDir } from './host-preset.js';
45
46
  import { generateSite } from './generate-site.js';
47
+ import { servedBundleProblem } from './restamp.js';
48
+ import { codemirrorCopies, duplicateCodemirrorWarning, tosijsUiDir, } from './codemirror-copies.js';
46
49
  import { findOutputDirOverlap, resolveBundleDir } from './output-guard.js';
47
50
  import { acquireBuildLock, describeHolder } from './build-lock.js';
48
51
  import { sourcemapWarning } from './sourcemap-check.js';
49
- import { basePathDoubling, bundleRegistrations, firebasePublicMismatch, } from './host-preset.js';
52
+ import { basePathDoubling, firebasePublicMismatch } from './host-preset.js';
50
53
  import { preflight } from './preflight.js';
51
54
  import { auditDependencies, reportAudit } from './audit-guard.js';
52
55
  import { gatherBuildStamp, hashOutput, stampToWrite, } from './build-stamp.js';
@@ -452,6 +455,28 @@ export async function buildSite(config, opts = {}) {
452
455
  const PUBLIC = path.resolve(PROJECT_ROOT, config.outputDir ?? 'docs');
453
456
  const DIST = path.resolve(PROJECT_ROOT, 'dist');
454
457
  /*
458
+ Two copies of CodeMirror blank every <tosi-code>, and nothing else says so (#3248). Asked
459
+ first, of the module resolver: it costs a couple of dozen resolutions, and a site built
460
+ on that install is broken however the rest of the build goes.
461
+ */
462
+ {
463
+ const doubled = duplicateCodemirrorWarning(codemirrorCopies(process.cwd(), tosijsUiDir(import.meta.dir)));
464
+ /*
465
+ Fatal under `strict` only where buildSite itself bundles the editor (`bundleEntry`).
466
+ Without one the site ships tosijs-ui's prebuilt bundle, which the install cannot
467
+ break, so the copies matter only to a bundle the project builds for itself, and
468
+ `restampSite` is where that project gets a failing build. Otherwise said once per
469
+ process: a watch rebuild has nothing new to add.
470
+ */
471
+ if (doubled) {
472
+ if (config.bundleEntry && config.strict)
473
+ misconfigured(true, doubled);
474
+ else if (!codemirrorWarned)
475
+ console.warn(doubled);
476
+ codemirrorWarned = true;
477
+ }
478
+ }
479
+ /*
455
480
  Where the hydration bundle is BUILT. Defaults to the site output, not to `dist` (#69).
456
481
 
457
482
  It used to be built into `dist` unconditionally — the same directory `emitLibrary` /
@@ -848,9 +873,15 @@ export async function buildSite(config, opts = {}) {
848
873
  existsSync(DIST)) {
849
874
  const bad = extensionlessImports(DIST);
850
875
  if (bad.length) {
851
- console.warn(`⚠️ dist/ has ${bad.length} relative import(s) without a file extension — Node rejects them ` +
852
- `(ERR_MODULE_NOT_FOUND), even though Bun and bundlers resolve them. Use \`libraryBundle\` ` +
853
- `instead of ${config.libraryTsconfig ? 'libraryTsconfig' : 'emitLibrary'} (tosijs-ui#169). First: ${bad[0]}`);
876
+ console.warn(`⚠️ The .js files tsc just emitted into dist/ have ${bad.length} relative import(s) without a ` +
877
+ `file extension — Node rejects them (ERR_MODULE_NOT_FOUND), even though Bun and bundlers ` +
878
+ `resolve them. Use \`libraryBundle\` instead of ${config.libraryTsconfig ? 'libraryTsconfig' : 'emitLibrary'} (tosijs-ui#169). First: ${bad[0]}\n` +
879
+ // A project that publishes bundles it builds afterwards sees this about files
880
+ // it then replaces, and went looking for them in its final dist/ (board #3248).
881
+ ` If a later step replaces these files with bundles, they are not what you publish: ` +
882
+ (config.libraryTsconfig
883
+ ? `set "emitDeclarationOnly": true in ${config.libraryTsconfig} so tsc emits only the .d.ts files.`
884
+ : `use \`libraryTsconfig\` with "emitDeclarationOnly": true so tsc emits only the .d.ts files.`));
854
885
  }
855
886
  }
856
887
  // The hydration bundle. If bundleEntry is set we build it (IIFE), else pages
@@ -1145,23 +1176,10 @@ export async function buildSite(config, opts = {}) {
1145
1176
  for (const f of await Array.fromAsync(new Bun.Glob('**/*.js').scan({ cwd: PUBLIC, absolute: true }))) {
1146
1177
  bundled += await Bun.file(f).text();
1147
1178
  }
1148
- const reg = bundleRegistrations(bundled);
1149
- if (!reg.docSystem) {
1150
- misconfigured(config.strict, `\n⚠️ ${config.bundleEntry} does not register <tosi-doc-system>, so every page\n` +
1151
- ` will render its prerendered markup and nothing else — no header, no nav,\n` +
1152
- ` no menu, no live examples, and no error in the console.\n\n` +
1153
- ` bundleEntry REPLACES tosijs-ui's bundle; it does not extend it. Add:\n` +
1154
- ` import 'tosijs-ui/doc-browser'\n` +
1155
- (docsHaveExamples && !reg.liveExample
1156
- ? ` import 'tosijs-ui/live-example' // this corpus has live examples\n`
1157
- : '') +
1158
- ` to ${config.bundleEntry}.\n`);
1159
- }
1160
- else if (docsHaveExamples && !reg.liveExample) {
1161
- misconfigured(config.strict, `\n⚠️ This corpus has executable fences but ${config.bundleEntry} does not\n` +
1162
- ` register <tosi-example>. Add \`import 'tosijs-ui/live-example'\` — the\n` +
1163
- ` code blocks will render as plain text otherwise.\n`);
1164
- }
1179
+ // ONE wording and one rule, shared with restampSite (restamp.ts).
1180
+ const problem = servedBundleProblem(bundled, String(config.bundleEntry), docsHaveExamples, String(config.bundleEntry));
1181
+ if (problem)
1182
+ misconfigured(config.strict, problem);
1165
1183
  }
1166
1184
  // Report the always-loaded weight (entry, not the lazy editor chunks) so a
1167
1185
  // regression that pulls CodeMirror back into the entry is visible.
@@ -1206,7 +1224,6 @@ export async function buildSite(config, opts = {}) {
1206
1224
  baseUrl: config.baseUrl,
1207
1225
  basePath: config.basePath,
1208
1226
  projectLinks: config.projectLinks,
1209
- haltijaDev: config.haltijaDev,
1210
1227
  markdownPages: config.markdownPages,
1211
1228
  liveExamples: config.liveExamples,
1212
1229
  }, corpus);
@@ -1425,11 +1442,7 @@ export async function buildSite(config, opts = {}) {
1425
1442
  written, so hashing the directory would hash the previous build. The bundle and the
1426
1443
  stylesheet exist by now, and they are exactly what a stale cache serves.
1427
1444
  */
1428
- const stampInputs = [
1429
- hydrateName && `${PUBLIC}/${hydrateName}`,
1430
- `${PUBLIC}/${scriptName}`,
1431
- `${PUBLIC}/doc-system.css`,
1432
- ].filter(Boolean);
1445
+ const stampInputs = assetStampInputs(PUBLIC, scriptName, hydrateName);
1433
1446
  /*
1434
1447
  Always a CONTENT HASH — the `opts.lock ? version : hash` branch is gone (major M3).
1435
1448
 
@@ -0,0 +1,29 @@
1
+ import type { SiteConfig } from './site-config.js';
2
+ export interface RestampResult {
3
+ /** the stamp now in every page */
4
+ stamp: string;
5
+ /** the stamp `buildSite` had written */
6
+ previous: string;
7
+ /** pages that carried a stamp */
8
+ pages: number;
9
+ /** pages whose stamp changed (0 when the bundle was not actually replaced) */
10
+ changed: number;
11
+ }
12
+ /** The hydration entry `buildSite` emitted for a `bundleEntry`, if there is one. */
13
+ export declare function hydrateEntryIn(publicDir: string): string | undefined;
14
+ /**
15
+ * What is wrong with the scripts the pages hydrate from, or null. ONE wording and one rule,
16
+ * for `buildSite` (asked of a `bundleEntry`'s output) and `restampSite` (asked of a file the
17
+ * project wrote itself). `subject` names what was read; `fix` says where the import goes.
18
+ */
19
+ export declare function servedBundleProblem(source: string, subject: string, docsHaveExamples: boolean, fix?: string): string | null;
20
+ /**
21
+ * Re-stamp a built site from the assets actually on disk, and check the hydration bundle.
22
+ * Call it after replacing `/iife.js` (or whatever `scriptUrl` names) in `outputDir`.
23
+ *
24
+ * Throws when no page carries a stamp (the build did not run, or `outputDir` is wrong): a
25
+ * restamp that silently did nothing is the failure it exists to prevent. A bundle that does
26
+ * not register the doc system is a warning, and fails under `strict: true`, as in
27
+ * `buildSite`.
28
+ */
29
+ export declare function restampSite(config: SiteConfig): Promise<RestampResult>;