tosijs-ui 1.17.0-beta.2 → 1.17.0-beta.4
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 +36 -1
- package/dist/doc-browser.js +11 -1
- package/dist/doc-system/site/a11y-audit-cli.js +65 -21
- package/dist/doc-system/site/a11y-audit.d.ts +8 -0
- package/dist/doc-system/site/a11y-audit.js +5 -2
- package/dist/doc-system/site/build-peers.d.ts +6 -0
- package/dist/doc-system/site/build-peers.js +43 -0
- package/dist/doc-system/site/docs.d.ts +4 -0
- package/dist/doc-system/site/docs.js +89 -5
- package/dist/doc-system/site/orchestrator.js +25 -5
- package/dist/iife.js +3 -3
- package/dist/iife.js.map +4 -4
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/llms.txt +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,41 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 1.17.0-beta.
|
|
3
|
+
## 1.17.0-beta.4
|
|
4
|
+
|
|
5
|
+
> Prerelease, published under the `beta` dist-tag: install with `tosijs-ui@beta`. `latest` stays
|
|
6
|
+
> on 1.16.41. Fixes from Learn 3D and from tosijs-native's first site build.
|
|
7
|
+
|
|
8
|
+
- **Every doc page can be scrolled by keyboard, and has a main landmark (#3575).** A page of
|
|
9
|
+
plain prose had nothing to tab to inside its scrolling content, so a keyboard user could not
|
|
10
|
+
scroll it, which axe reports as `scrollable-region-focusable` (Learn 3D: 71 of 126 pages). The
|
|
11
|
+
content scroller is now focusable, and it is the page's main landmark, which the doc site did not
|
|
12
|
+
have at all: a screen reader's "jump to main content" now works. A doc browser embedded in
|
|
13
|
+
another page stays focusable without claiming that page's main landmark.
|
|
14
|
+
- **`buildSite` says it needs `happy-dom` before it starts (#3600).** Without it, the build died
|
|
15
|
+
partway through with "Cannot find package 'happy-dom'" from a file inside tosijs-ui. It now
|
|
16
|
+
stops before writing anything and prints the install command. `happy-dom` stays an optional
|
|
17
|
+
peer: a project that only uses the components never needs it.
|
|
18
|
+
- **TJS's own doc comments, `/# … #/`, become documentation (#3601).** In `.tjs` and `.ajs`
|
|
19
|
+
files they are read in source order with any `/*#` blocks. tjs-lang finds them, so its grammar
|
|
20
|
+
applies exactly; that needs a tjs-lang that exports `findDocCommentSpans` (requested). With an
|
|
21
|
+
older one, the build names each file whose `/#` comments it could not read, instead of
|
|
22
|
+
dropping the page silently. `.ajs` files are now scraped for `/*#` blocks too.
|
|
23
|
+
|
|
24
|
+
### 1.17.0-beta.3 (prerelease detail)
|
|
25
|
+
|
|
26
|
+
> Prerelease, published under the `beta` dist-tag: install with `tosijs-ui@beta`. `latest` stays
|
|
27
|
+
> on 1.16.41. beta.2 was staged and never published; its notes follow.
|
|
28
|
+
|
|
29
|
+
- **The axe audit waits for a page marked `aria-busy`, and one slow page no longer ends it.** A
|
|
30
|
+
page that is still loading (a 3D scene fetching large models, say) marks an element
|
|
31
|
+
`aria-busy="true"`, the standard ARIA signal, and is waited for up to 30 seconds; other pages
|
|
32
|
+
are audited once they have stopped adding elements, or after five seconds. A page that never
|
|
33
|
+
loads, or is still busy at 30 seconds, is listed as not fully checked, and the build makes no
|
|
34
|
+
claim about it. Before, each page load waited for the network to go idle, so a page that kept
|
|
35
|
+
downloading for 30 seconds, or that polled, threw and ended the audit of every page. The doc
|
|
36
|
+
site reference shows a component setting and clearing `aria-busy`.
|
|
37
|
+
|
|
38
|
+
### 1.17.0-beta.2 (prerelease detail)
|
|
4
39
|
|
|
5
40
|
> Prerelease, published under the `beta` dist-tag: install with `tosijs-ui@beta`. `latest` stays
|
|
6
41
|
> on 1.16.41. Fixes from Learn 3D's first run of beta.1.
|
package/dist/doc-browser.js
CHANGED
|
@@ -1699,7 +1699,17 @@ export function createDocBrowser(options) {
|
|
|
1699
1699
|
// The sidenav has just introduced itself; give it the layout the doc asked for.
|
|
1700
1700
|
applyFullScreen();
|
|
1701
1701
|
},
|
|
1702
|
-
}, navContent,
|
|
1702
|
+
}, navContent,
|
|
1703
|
+
/*
|
|
1704
|
+
The page's main landmark, and focusable: a page of plain prose has nothing else to tab
|
|
1705
|
+
to, so a keyboard user could not scroll it at all (WCAG 2.1.1; axe
|
|
1706
|
+
scrollable-region-focusable, #3575). A role, not a <main>, so styles written against the
|
|
1707
|
+
div still apply. An embedded browser (`memory` routing) is part of someone else's page,
|
|
1708
|
+
so it does not claim that page's main landmark.
|
|
1709
|
+
*/
|
|
1710
|
+
div({
|
|
1711
|
+
...(routing === 'memory' ? {} : { role: 'main' }),
|
|
1712
|
+
tabindex: 0,
|
|
1703
1713
|
style: {
|
|
1704
1714
|
position: 'relative',
|
|
1705
1715
|
overflowY: 'scroll',
|
|
@@ -10,7 +10,7 @@ tosijs-ui. When either is missing, or Chromium is not installed, it says which a
|
|
|
10
10
|
NOT_RUN_EXIT, so the build can say the audit did not run instead of reporting a pass.
|
|
11
11
|
|
|
12
12
|
bun a11y-audit-cli.ts <payload.json> # payload = { outputDir, basePath, pages }
|
|
13
|
-
stdout: one JSON line, { violations: AxeFinding[] }
|
|
13
|
+
stdout: one JSON line, { violations: AxeFinding[], unsettled: { page, reason }[] }
|
|
14
14
|
|
|
15
15
|
Build-time only. Never import this from browser code.
|
|
16
16
|
*/
|
|
@@ -19,7 +19,7 @@ import { createRequire } from 'module';
|
|
|
19
19
|
import { existsSync } from 'fs';
|
|
20
20
|
import { NOT_RUN_EXIT, AXE_TAGS } from './a11y-audit.js';
|
|
21
21
|
const payloadPath = process.argv[2];
|
|
22
|
-
const { outputDir, basePath, pages } = JSON.parse(await Bun.file(payloadPath).text());
|
|
22
|
+
const { outputDir, basePath, pages, slowPageMs } = JSON.parse(await Bun.file(payloadPath).text());
|
|
23
23
|
const notRun = (why) => {
|
|
24
24
|
console.error(why);
|
|
25
25
|
process.exit(NOT_RUN_EXIT);
|
|
@@ -69,7 +69,12 @@ Pages are audited a few at a time, each in its own tab: one at a time took Learn
|
|
|
69
69
|
to ~73s (#3585). Results are kept in page order, so the report reads the same either way.
|
|
70
70
|
*/
|
|
71
71
|
const CONCURRENCY = 4;
|
|
72
|
+
/** How long a page may take to fire `load`, and to finish being `aria-busy`. */
|
|
73
|
+
const SLOW_PAGE_MS = slowPageMs ?? 30_000;
|
|
74
|
+
/** How long an un-busy page may keep adding elements before it is audited as it stands. */
|
|
75
|
+
const QUIET_CAP_MS = 5_000;
|
|
72
76
|
const perPage = [];
|
|
77
|
+
const unsettled = [];
|
|
73
78
|
try {
|
|
74
79
|
const context = await browser.newContext();
|
|
75
80
|
/*
|
|
@@ -83,9 +88,32 @@ try {
|
|
|
83
88
|
while (next < pages.length) {
|
|
84
89
|
const index = next++;
|
|
85
90
|
const pagePath = pages[index];
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
91
|
+
/*
|
|
92
|
+
One slow or broken page must not end the audit of every other page, and must not pass
|
|
93
|
+
unremarked either: it is listed as not fully checked.
|
|
94
|
+
*/
|
|
95
|
+
try {
|
|
96
|
+
await page.goto(`http://localhost:${server.port}${prefix}${pagePath}`, {
|
|
97
|
+
waitUntil: 'load',
|
|
98
|
+
timeout: SLOW_PAGE_MS,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
const message = error.message.split('\n')[0];
|
|
103
|
+
if (!/timeout/i.test(message)) {
|
|
104
|
+
unsettled.push({ page: pagePath, reason: `not audited: ${message}` });
|
|
105
|
+
perPage[index] = [];
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
unsettled.push({
|
|
109
|
+
page: pagePath,
|
|
110
|
+
reason: `did not finish loading in ${SLOW_PAGE_MS / 1000}s; audited as it stood`,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
// Best effort: a page that polls or streams never goes idle, and that is not a defect.
|
|
114
|
+
await page
|
|
115
|
+
.waitForLoadState('networkidle', { timeout: 10_000 })
|
|
116
|
+
.catch(() => undefined);
|
|
89
117
|
// Audit the page a reader uses: hydrated when the bundle loads, static if it does not.
|
|
90
118
|
if (await page.$('tosi-doc-system'))
|
|
91
119
|
await page
|
|
@@ -94,26 +122,42 @@ try {
|
|
|
94
122
|
})
|
|
95
123
|
.catch(() => undefined);
|
|
96
124
|
/*
|
|
97
|
-
Then until
|
|
98
|
-
|
|
99
|
-
|
|
125
|
+
Then until the page has settled: no element added or removed for half a second, and
|
|
126
|
+
nothing marked `aria-busy="true"`. A fixed pause audited a busy machine's pages
|
|
127
|
+
half-built. A page that is still loading (a 3D scene fetching its models, say) says so with
|
|
128
|
+
aria-busy, which screen readers also announce, and is waited for up to SLOW_PAGE_MS; one
|
|
129
|
+
that merely keeps changing is audited after QUIET_CAP_MS.
|
|
100
130
|
*/
|
|
101
|
-
await page.evaluate(() => new Promise((resolve) => {
|
|
102
|
-
|
|
103
|
-
|
|
131
|
+
const settled = await page.evaluate(({ quietCap, busyCap }) => new Promise((resolve) => {
|
|
132
|
+
const start = performance.now();
|
|
133
|
+
let last = start;
|
|
104
134
|
const observer = new MutationObserver(() => {
|
|
105
|
-
|
|
106
|
-
timer = setTimeout(done, 500);
|
|
135
|
+
last = performance.now();
|
|
107
136
|
});
|
|
108
|
-
function done() {
|
|
109
|
-
observer.disconnect();
|
|
110
|
-
clearTimeout(timer);
|
|
111
|
-
clearTimeout(deadline);
|
|
112
|
-
resolve();
|
|
113
|
-
}
|
|
114
137
|
// Elements arriving, not attributes: an animation changes a style forever.
|
|
115
138
|
observer.observe(document, { subtree: true, childList: true });
|
|
116
|
-
|
|
139
|
+
const tick = () => {
|
|
140
|
+
const now = performance.now();
|
|
141
|
+
const busy = !!document.querySelector('[aria-busy="true"]');
|
|
142
|
+
const outcome = now - last >= 500 && !busy
|
|
143
|
+
? 'settled'
|
|
144
|
+
: !busy && now - start >= quietCap
|
|
145
|
+
? 'changing'
|
|
146
|
+
: now - start >= busyCap
|
|
147
|
+
? 'busy'
|
|
148
|
+
: '';
|
|
149
|
+
if (!outcome)
|
|
150
|
+
return void setTimeout(tick, 100);
|
|
151
|
+
observer.disconnect();
|
|
152
|
+
resolve(outcome);
|
|
153
|
+
};
|
|
154
|
+
tick();
|
|
155
|
+
}), { quietCap: QUIET_CAP_MS, busyCap: SLOW_PAGE_MS });
|
|
156
|
+
if (settled === 'busy')
|
|
157
|
+
unsettled.push({
|
|
158
|
+
page: pagePath,
|
|
159
|
+
reason: `still aria-busy after ${SLOW_PAGE_MS / 1000}s; audited as it stood`,
|
|
160
|
+
});
|
|
117
161
|
await page.addScriptTag({ path: axePath });
|
|
118
162
|
const found = await page.evaluate(async (tags) => {
|
|
119
163
|
const result = await window.axe.run(document, {
|
|
@@ -136,4 +180,4 @@ finally {
|
|
|
136
180
|
server.stop(true);
|
|
137
181
|
}
|
|
138
182
|
const violations = perPage.flat();
|
|
139
|
-
console.log(JSON.stringify({ violations }));
|
|
183
|
+
console.log(JSON.stringify({ violations, unsettled }));
|
|
@@ -11,9 +11,15 @@ export interface AxeFinding {
|
|
|
11
11
|
/** a CSS selector for each element that fails the rule */
|
|
12
12
|
targets: string[];
|
|
13
13
|
}
|
|
14
|
+
/** A page the audit could not check fully: it never loaded, or never stopped being busy. */
|
|
15
|
+
export interface UnsettledPage {
|
|
16
|
+
page: string;
|
|
17
|
+
reason: string;
|
|
18
|
+
}
|
|
14
19
|
export type AxeAuditResult = {
|
|
15
20
|
ran: true;
|
|
16
21
|
violations: AxeFinding[];
|
|
22
|
+
unsettled: UnsettledPage[];
|
|
17
23
|
} | {
|
|
18
24
|
ran: false;
|
|
19
25
|
reason: string;
|
|
@@ -25,6 +31,8 @@ export declare function auditBuiltSite(opts: {
|
|
|
25
31
|
pages: string[];
|
|
26
32
|
/** the project Playwright and axe-core are resolved from (default: the working directory) */
|
|
27
33
|
projectDir?: string;
|
|
34
|
+
/** how long a page may take to load, or stay `aria-busy="true"` (default 30s) */
|
|
35
|
+
slowPageMs?: number;
|
|
28
36
|
}): Promise<AxeAuditResult>;
|
|
29
37
|
/** The report: one line per rule per page, worst impact first. */
|
|
30
38
|
export declare function formatAxeFindings(violations: AxeFinding[]): string;
|
|
@@ -25,6 +25,7 @@ export async function auditBuiltSite(opts) {
|
|
|
25
25
|
outputDir: path.resolve(opts.outputDir),
|
|
26
26
|
basePath: opts.basePath ?? '',
|
|
27
27
|
pages: opts.pages,
|
|
28
|
+
slowPageMs: opts.slowPageMs,
|
|
28
29
|
}));
|
|
29
30
|
// `--no-install`: without it Bun resolves a package the project does not have from its global
|
|
30
31
|
// cache, so "not installed" would never be said and the audit would use a Playwright nobody chose.
|
|
@@ -42,8 +43,10 @@ export async function auditBuiltSite(opts) {
|
|
|
42
43
|
return { ran: false, reason: stderr.trim() };
|
|
43
44
|
const line = stdout.trim().split('\n').pop() ?? '';
|
|
44
45
|
try {
|
|
45
|
-
if (code === 0)
|
|
46
|
-
|
|
46
|
+
if (code === 0) {
|
|
47
|
+
const { violations, unsettled = [] } = JSON.parse(line);
|
|
48
|
+
return { ran: true, violations, unsettled };
|
|
49
|
+
}
|
|
47
50
|
}
|
|
48
51
|
catch {
|
|
49
52
|
// fall through: a run that printed no result did not run
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The install command for each build peer that cannot be resolved, or none.
|
|
3
|
+
* `resolve` is injectable for tests.
|
|
4
|
+
*/
|
|
5
|
+
export declare function missingBuildPeers(moduleDir: string, resolve?: (name: string, from: string) => string): string[];
|
|
6
|
+
export declare function missingBuildPeersMessage(missing: string[]): string;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Optional peers a site BUILD cannot do without (#3600).
|
|
3
|
+
|
|
4
|
+
`happy-dom` is an optional peer of tosijs-ui because a project that only uses the components
|
|
5
|
+
never needs it, and npm installs a required peer whether or not it is used. But `buildSite`
|
|
6
|
+
does need it: the theme stylesheet is generated with tosijs imported under a DOM shim. Without
|
|
7
|
+
it the build died several steps in with "Cannot find package 'happy-dom'" from a file inside
|
|
8
|
+
tosijs-ui, which says nothing about whose job it is to install it. So the build checks first,
|
|
9
|
+
and says what to run.
|
|
10
|
+
*/
|
|
11
|
+
import { readFileSync } from 'fs';
|
|
12
|
+
import * as path from 'path';
|
|
13
|
+
import { tosijsUiDir } from './codemirror-copies.js';
|
|
14
|
+
/** What a site build requires, resolved from tosijs-ui's own location (where it is imported). */
|
|
15
|
+
const BUILD_PEERS = ['happy-dom'];
|
|
16
|
+
/**
|
|
17
|
+
* The install command for each build peer that cannot be resolved, or none.
|
|
18
|
+
* `resolve` is injectable for tests.
|
|
19
|
+
*/
|
|
20
|
+
export function missingBuildPeers(moduleDir, resolve = Bun.resolveSync) {
|
|
21
|
+
let ranges = {};
|
|
22
|
+
try {
|
|
23
|
+
const pkg = JSON.parse(readFileSync(path.join(tosijsUiDir(moduleDir), 'package.json'), 'utf8'));
|
|
24
|
+
ranges = pkg.peerDependencies ?? {};
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
// no manifest found: name the package without a range
|
|
28
|
+
}
|
|
29
|
+
return BUILD_PEERS.filter((name) => {
|
|
30
|
+
try {
|
|
31
|
+
resolve(name, moduleDir);
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
}).map((name) => (ranges[name] ? `${name}@${ranges[name]}` : name));
|
|
38
|
+
}
|
|
39
|
+
export function missingBuildPeersMessage(missing) {
|
|
40
|
+
return (`\n🛑 Building a site needs ${missing.join(', ')}, an optional peer of tosijs-ui ` +
|
|
41
|
+
`(a project that only uses the components does not).\n` +
|
|
42
|
+
` bun add -d ${missing.map((m) => `'${m}'`).join(' ')}\n`);
|
|
43
|
+
}
|
|
@@ -143,5 +143,9 @@ export declare function parseFrontmatter(content: string): {
|
|
|
143
143
|
body: string;
|
|
144
144
|
};
|
|
145
145
|
export declare const SCRAPED_SOURCE_EXTENSIONS: string[];
|
|
146
|
+
type DocSpans = (source: string) => Array<[number, number]>;
|
|
147
|
+
/** Tests only: stand in for tjs-lang's scanner (`undefined` restores the lookup). */
|
|
148
|
+
export declare function setTjsDocCommentScanner(scan: DocSpans | null | undefined): void;
|
|
146
149
|
export declare function extractDocs(options: ExtractDocsOptions): Doc[];
|
|
147
150
|
export declare function saveDocsJSON(docs: Doc[], outputPath: string): void;
|
|
151
|
+
export {};
|
|
@@ -30,8 +30,13 @@ whatever metadata the doc declares (`pin`, `order`, `parent`, `layout`, `descrip
|
|
|
30
30
|
## What is collected
|
|
31
31
|
|
|
32
32
|
- **`.md` files**, whole. YAML frontmatter is read and removed.
|
|
33
|
-
- **`/*#` … `*/` blocks** in `.ts`, `.tjs`, `.js` and `.css` files, as markdown. The
|
|
34
|
-
is the page title. A block counts only when `/*#` starts a line.
|
|
33
|
+
- **`/*#` … `*/` blocks** in `.ts`, `.tjs`, `.ajs`, `.js` and `.css` files, as markdown. The
|
|
34
|
+
first heading is the page title. A block counts only when `/*#` starts a line.
|
|
35
|
+
- **TJS doc comments, `/#` … `#/`**, in `.tjs` and `.ajs` files, in source order with any `/*#`
|
|
36
|
+
blocks. They are found by tjs-lang itself, so they follow its grammar exactly: multi-line, and
|
|
37
|
+
a doc comment nested inside one is quoted as part of it. This needs a tjs-lang that exports
|
|
38
|
+
`findDocCommentSpans`; with an older one, the build names each file whose `/#` comments it
|
|
39
|
+
could not read.
|
|
35
40
|
- **Files whose name starts with `_`** are skipped: use the prefix for drafts and fragments.
|
|
36
41
|
|
|
37
42
|
Metadata blocks (`<!--{ … }-->` in markdown, `/*{ … }*/` in code), nav order, sections and
|
|
@@ -40,6 +45,7 @@ which code fences run live are described under [Doc format](/doc-site-system/#do
|
|
|
40
45
|
/*{ "parent": "Documentation System", "order": 60, "description": "extractDocs() collects markdown files and doc comments from source code into one docs.json corpus, the input to the doc browser and static site." }*/
|
|
41
46
|
// TODO CLI options
|
|
42
47
|
import * as fs from 'fs';
|
|
48
|
+
import { createRequire } from 'module';
|
|
43
49
|
import * as path from 'path';
|
|
44
50
|
import { truncationWarnings, formatTruncationWarnings, } from './truncated-doc.js';
|
|
45
51
|
import { pinnedSort } from '../nav-tree.js';
|
|
@@ -230,9 +236,47 @@ ends it — the same trap tosijs-ui#70 records for doc blocks, hit again while w
|
|
|
230
236
|
|
|
231
237
|
See `warnUnscrapedDocBlocks` below: a missing extension should never again be invisible.
|
|
232
238
|
*/
|
|
233
|
-
export const SCRAPED_SOURCE_EXTENSIONS = ['.ts', '.js', '.tjs', '.css'];
|
|
239
|
+
export const SCRAPED_SOURCE_EXTENSIONS = ['.ts', '.js', '.tjs', '.ajs', '.css'];
|
|
234
240
|
/** Files that look documented but are not scraped — reported once at the end of a walk. */
|
|
235
241
|
const unscrapedDocBlocks = [];
|
|
242
|
+
/*
|
|
243
|
+
TJS's own doc comment, `/# … #/` (#3601): multi-line, nestable, and the convention in `.tjs` and
|
|
244
|
+
`.ajs` sources. Its grammar belongs to tjs-lang, which finds the spans over a literal-masked view
|
|
245
|
+
(a raw scan reads a regex `/#/` and a later string `"#/"` as one comment). So this does not keep a
|
|
246
|
+
second copy: it asks the installed tjs-lang, and when that tjs-lang does not export the scanner,
|
|
247
|
+
the build names the files whose doc comments it could not read rather than dropping them.
|
|
248
|
+
*/
|
|
249
|
+
const TJS_DOC_EXTENSIONS = ['.tjs', '.ajs'];
|
|
250
|
+
// An opener at the start of a line that its own line does not close: a single-line `/# x #/` is
|
|
251
|
+
// a regex, never a doc comment.
|
|
252
|
+
const TJS_DOC_OPENER = /^[ \t]*\/#(?![^\n]*#\/)/m;
|
|
253
|
+
let tjsDocSpans;
|
|
254
|
+
function tjsDocCommentSpans() {
|
|
255
|
+
if (tjsDocSpans !== undefined)
|
|
256
|
+
return tjsDocSpans;
|
|
257
|
+
tjsDocSpans = null;
|
|
258
|
+
// The project's tjs-lang, the one its .tjs files are written for.
|
|
259
|
+
const projectModule = createRequire(path.resolve('package.json'));
|
|
260
|
+
for (const entry of ['tjs-lang/lang', 'tjs-lang']) {
|
|
261
|
+
try {
|
|
262
|
+
const found = projectModule(entry).findDocCommentSpans;
|
|
263
|
+
if (typeof found === 'function') {
|
|
264
|
+
tjsDocSpans = found;
|
|
265
|
+
break;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
catch {
|
|
269
|
+
// not installed, or no such entry: try the next
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return tjsDocSpans;
|
|
273
|
+
}
|
|
274
|
+
/** Tests only: stand in for tjs-lang's scanner (`undefined` restores the lookup). */
|
|
275
|
+
export function setTjsDocCommentScanner(scan) {
|
|
276
|
+
tjsDocSpans = scan;
|
|
277
|
+
}
|
|
278
|
+
/** `.tjs`/`.ajs` files with doc comments the installed tjs-lang could not read. */
|
|
279
|
+
const unreadTjsDocComments = [];
|
|
236
280
|
/**
|
|
237
281
|
* Does an UNSCRAPED file appear to carry a doc block?
|
|
238
282
|
*
|
|
@@ -385,9 +429,41 @@ walked) {
|
|
|
385
429
|
// before the slash). This keeps a `/*#` that appears inside a // comment,
|
|
386
430
|
// a string, or otherwise mid-line from being scraped as a spurious doc.
|
|
387
431
|
// m[1] is the block without its leading indentation.
|
|
388
|
-
const
|
|
432
|
+
const blocks = [
|
|
433
|
+
...content.matchAll(/^[ \t]*(\/\*#[\s\S]+?\*\/)/gm),
|
|
434
|
+
].map((m) => ({
|
|
435
|
+
at: m.index + m[0].length - m[1].length,
|
|
436
|
+
block: m[1],
|
|
437
|
+
}));
|
|
438
|
+
const docs = blocks.map(({ block }) => block.substring(3, block.length - 2).trim());
|
|
439
|
+
// TJS doc comments, in source order with any `/*#` blocks.
|
|
440
|
+
if (TJS_DOC_EXTENSIONS.includes(path.extname(file)) &&
|
|
441
|
+
TJS_DOC_OPENER.test(content)) {
|
|
442
|
+
const scan = tjsDocCommentSpans();
|
|
443
|
+
if (!scan) {
|
|
444
|
+
unreadTjsDocComments.push(path.relative(process.cwd(), filePath));
|
|
445
|
+
}
|
|
446
|
+
else {
|
|
447
|
+
const spans = scan(content);
|
|
448
|
+
// Outermost only: a nested doc comment is being quoted, not published.
|
|
449
|
+
const outer = spans.filter(([a, b]) => !spans.some(([c, d]) => c < a && b <= d));
|
|
450
|
+
const all = [
|
|
451
|
+
...blocks.map(({ at }, i) => ({ at, text: docs[i] })),
|
|
452
|
+
...outer.map(([a, b]) => ({
|
|
453
|
+
at: a,
|
|
454
|
+
// The span may or may not include its delimiters; the text never does.
|
|
455
|
+
text: content
|
|
456
|
+
.slice(a, b)
|
|
457
|
+
.replace(/^\/#/, '')
|
|
458
|
+
.replace(/#\/$/, '')
|
|
459
|
+
.trim(),
|
|
460
|
+
})),
|
|
461
|
+
].sort((x, y) => x.at - y.at);
|
|
462
|
+
docs.splice(0, docs.length, ...all.map((d) => d.text));
|
|
463
|
+
}
|
|
464
|
+
}
|
|
389
465
|
if (docs.length) {
|
|
390
|
-
const markdown = docs
|
|
466
|
+
const markdown = docs;
|
|
391
467
|
// A doc block that ended early drops the rest of the documentation silently, and
|
|
392
468
|
// turns the code after it back into source — which surfaces as parse errors
|
|
393
469
|
// pointing at prose rather than at the delimiter responsible (#70).
|
|
@@ -444,6 +520,14 @@ walked) {
|
|
|
444
520
|
can act on without going looking. The fix is nearly always one entry in
|
|
445
521
|
SCRAPED_SOURCE_EXTENSIONS.
|
|
446
522
|
*/
|
|
523
|
+
if (unreadTjsDocComments.length) {
|
|
524
|
+
console.warn(`\n⚠️ ${unreadTjsDocComments.length} file(s) have TJS doc comments (/# … #/) that this ` +
|
|
525
|
+
`build cannot read, so they have no page from them:\n` +
|
|
526
|
+
unreadTjsDocComments.map((f) => ` ${f}`).join('\n') +
|
|
527
|
+
`\n Reading them needs findDocCommentSpans from tjs-lang, which the installed ` +
|
|
528
|
+
`tjs-lang does not export. Until it does, write those blocks as /*# … */.\n`);
|
|
529
|
+
unreadTjsDocComments.length = 0;
|
|
530
|
+
}
|
|
447
531
|
if (unscrapedDocBlocks.length) {
|
|
448
532
|
const list = unscrapedDocBlocks.map((f) => ` ${f}`).join('\n');
|
|
449
533
|
console.warn(`\n⚠️ ${unscrapedDocBlocks.length} file(s) look documented but are not scraped, so ` +
|
|
@@ -31,6 +31,7 @@ export function registerSiteDialects(config) {
|
|
|
31
31
|
import { namedBooks, partitionByBook, DEFAULT_BOOK } from '../book-target.js';
|
|
32
32
|
import { EPUB_IMAGE_EXIT, attachCovers, listEpubVolumes, printCoverManifestUrl, VOLUME_MANIFEST, renderEpubDownloads, } from './epub-volumes.js';
|
|
33
33
|
import { buildSlugMap, pathForSlug } from '../routing.js';
|
|
34
|
+
import { missingBuildPeers, missingBuildPeersMessage } from './build-peers.js';
|
|
34
35
|
import { assetStampInputs, computeAssetStamp, missingStampInputWarning, } from './asset-stamp.js';
|
|
35
36
|
import { existsSync, mkdirSync, readdirSync, readFileSync, unlinkSync, } from 'fs';
|
|
36
37
|
import { tmpdir } from 'os';
|
|
@@ -416,6 +417,12 @@ export async function buildSite(config, opts = {}) {
|
|
|
416
417
|
}))) {
|
|
417
418
|
return false;
|
|
418
419
|
}
|
|
420
|
+
// A peer the build needs, checked before anything is deleted or written (#3600).
|
|
421
|
+
const missingPeers = missingBuildPeers(import.meta.dir);
|
|
422
|
+
if (missingPeers.length) {
|
|
423
|
+
console.error(missingBuildPeersMessage(missingPeers));
|
|
424
|
+
return false;
|
|
425
|
+
}
|
|
419
426
|
// Dependency-audit gate. Runs on the initial build only — the dev server runs its
|
|
420
427
|
// own synchronous audit just before binding the port, and `opts.skipAudit` keeps
|
|
421
428
|
// watch rebuilds off the network. Fails the build (before the destructive rm -rf)
|
|
@@ -1706,12 +1713,25 @@ export async function buildSite(config, opts = {}) {
|
|
|
1706
1713
|
console.warn(`\n⚠️ Accessibility: the axe audit did not run, so this build makes no claim\n` +
|
|
1707
1714
|
` about what it would find. ${audit.reason}\n`);
|
|
1708
1715
|
}
|
|
1709
|
-
else if (audit.violations.length) {
|
|
1710
|
-
misconfigured(config.strict, `\n⚠️ Accessibility: axe found ${audit.violations.length} WCAG 2.2 AA failure(s) in the built pages:\n` +
|
|
1711
|
-
`${formatAxeFindings(audit.violations)}\n`);
|
|
1712
|
-
}
|
|
1713
1716
|
else {
|
|
1714
|
-
|
|
1717
|
+
// A page the audit could not check fully is said, never counted as clean.
|
|
1718
|
+
if (audit.unsettled.length)
|
|
1719
|
+
console.warn(`\n⚠️ Accessibility: ${audit.unsettled.length} page(s) were not fully checked, so ` +
|
|
1720
|
+
`this build makes no claim about them. A page that is still loading should mark ` +
|
|
1721
|
+
`itself aria-busy="true"; the audit waits up to 30s for it:\n` +
|
|
1722
|
+
audit.unsettled
|
|
1723
|
+
.map((u) => ` ${u.page} ${u.reason}`)
|
|
1724
|
+
.join('\n') +
|
|
1725
|
+
'\n');
|
|
1726
|
+
const checked = pages.length - audit.unsettled.length;
|
|
1727
|
+
if (audit.violations.length)
|
|
1728
|
+
misconfigured(config.strict, `\n⚠️ Accessibility: axe found ${audit.violations.length} WCAG 2.2 AA failure(s) in the built pages:\n` +
|
|
1729
|
+
`${formatAxeFindings(audit.violations)}\n`);
|
|
1730
|
+
else
|
|
1731
|
+
console.log(`♿ axe: no WCAG 2.2 AA failures in ${checked} fully checked page(s)` +
|
|
1732
|
+
(audit.unsettled.length
|
|
1733
|
+
? ` (${audit.unsettled.length} not fully checked, above)`
|
|
1734
|
+
: ''));
|
|
1715
1735
|
}
|
|
1716
1736
|
}
|
|
1717
1737
|
console.timeEnd('build');
|