tosijs-ui 1.16.14 → 1.16.15

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,37 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.16.15
4
+
5
+ For projects that build their own docs bundle, from tosijs's upgrade to 1.16.13 (board
6
+ #3248). Two failures that left a green build over a broken site now say so.
7
+
8
+ - **`restampSite(config)`, from `tosijs-ui/site`.** If your site has no `bundleEntry` and you
9
+ overwrite `/iife.js` after `buildSite`, call it afterwards. It checks that the file you
10
+ wrote registers `<tosi-doc-system>`, and `<tosi-example>` when the corpus has live
11
+ examples, and then re-stamps the script and stylesheet `?v=` in every page from the files
12
+ actually on disk (the stamp `buildSite` wrote described the bundle it replaced, so a
13
+ returning visitor could run a cached library against new pages). A bundle whose entry
14
+ imports only the root `tosijs-ui` registers neither element since 1.14.0, and until now
15
+ nothing noticed: pages served, no header, no nav, no examples, no doc tests. A warning by
16
+ default. Under `strict: true` it throws before writing anything. It also throws when
17
+ there is nothing to re-stamp, on a `bundleEntry` site, and when it cannot read the corpus.
18
+ - **The build reports two copies of CodeMirror.** After an upgrade a lockfile can keep an
19
+ older hoisted `@codemirror/state` while the newer one nests under
20
+ `node_modules/tosijs-ui/`; every `<tosi-code>` is then blank and nothing fails.
21
+ `buildSite` now asks the module resolver whether the packages the editor is built from
22
+ agree, and names both copies and the command that fixes it
23
+ (`bun update @codemirror/state @codemirror/view`). A warning by default. **Under `strict: true` it fails
24
+ `buildSite` on a site with a `bundleEntry`, and fails `restampSite`**, which is new in a
25
+ patch: those are the two places a bundle is built from the install, and such a build was
26
+ already producing blank editors. Otherwise `buildSite` says it once and carries on. At runtime `<tosi-code>` logs the same explanation beside CodeMirror's own
27
+ "Unrecognized extension value" error. Cost: `iife.js` +176 bytes gzip.
28
+ - **Fixed: `llms.txt` told agents to try `/carousel/index.md` on every site.** That is a
29
+ tosijs-ui page. The example is now a page of the site being built.
30
+ - **The "relative imports without a file extension" warning says what it read.** It is
31
+ about the `.js` files tsc emitted, at the moment it emitted them. A project that replaces
32
+ those with bundles afterwards could not find the files it named; the warning now says so
33
+ and points at `"emitDeclarationOnly": true`.
34
+
3
35
  ## 1.16.14
4
36
 
5
37
  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.
@@ -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';
@@ -56,6 +56,13 @@ export interface LlmsTxtMeta {
56
56
  * docs and needs no `dist/` library output.
57
57
  */
58
58
  export declare function entriesFromCorpus(corpus: CorpusDoc[], meta: LlmsTxtMeta): LlmsEntry[];
59
+ /**
60
+ * A real page of THIS site to show the `index.md` rule with. It was hard-coded to
61
+ * `/carousel/index.md`, a tosijs-ui page, so every other project's llms.txt pointed agents
62
+ * at a URL it does not have (board #3248). The first page that has a markdown copy and is
63
+ * not the site root.
64
+ */
65
+ export declare function markdownExample(corpus: CorpusDoc[], meta: LlmsTxtMeta): string | undefined;
59
66
  /**
60
67
  * Write an `llms.txt` index. Pass the extracted `corpus` (the build does) to
61
68
  * 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
@@ -145,11 +164,13 @@ export function generateLlmsTxt(outputPath, meta = {}, corpus) {
145
164
  ]
146
165
  : [];
147
166
  // Only for a corpus-driven index: the legacy scan links to dist/ files, not pages.
167
+ const example = corpus ? markdownExample(corpus, meta) : undefined;
148
168
  const markdownNote = corpus && meta.markdownPages !== false
149
169
  ? [
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.',
170
+ 'Pages below are also available as plain markdown: append `index.md` to the URL' +
171
+ (example ? '' : '.'),
172
+ ...(example ? [`(for example \`${example}\`).`] : []),
173
+ 'Prefer that if you only need the text. A page marked noindex has no markdown copy.',
153
174
  '',
154
175
  ]
155
176
  : [];
@@ -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.
@@ -1425,11 +1443,7 @@ export async function buildSite(config, opts = {}) {
1425
1443
  written, so hashing the directory would hash the previous build. The bundle and the
1426
1444
  stylesheet exist by now, and they are exactly what a stale cache serves.
1427
1445
  */
1428
- const stampInputs = [
1429
- hydrateName && `${PUBLIC}/${hydrateName}`,
1430
- `${PUBLIC}/${scriptName}`,
1431
- `${PUBLIC}/doc-system.css`,
1432
- ].filter(Boolean);
1446
+ const stampInputs = assetStampInputs(PUBLIC, scriptName, hydrateName);
1433
1447
  /*
1434
1448
  Always a CONTENT HASH — the `opts.lock ? version : hash` branch is gone (major M3).
1435
1449
 
@@ -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>;