shelving 1.269.0 → 1.271.0
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/extract/FileExtractor.d.ts +6 -4
- package/extract/FileExtractor.js +3 -3
- package/extract/TypescriptExtractor.d.ts +5 -4
- package/extract/TypescriptExtractor.js +186 -74
- package/markup/rule/fenced.d.ts +1 -1
- package/markup/rule/fenced.js +1 -1
- package/package.json +4 -5
- package/ui/block/Row.d.ts +2 -1
- package/ui/block/Row.tsx +2 -2
- package/ui/misc/Catcher.d.ts +0 -1
- package/ui/misc/Catcher.tsx +0 -1
|
@@ -29,15 +29,17 @@ export declare class FileExtractor extends Extractor<BunFile, TreeElement> {
|
|
|
29
29
|
* Build the file element props from the extracted content.
|
|
30
30
|
* - `name` is the basename without extension (e.g. `"array"`) — display-ready, used by menus, cards, and URL paths.
|
|
31
31
|
* - Override to parse `text` into richer elements (content/children/description) and to set
|
|
32
|
-
* `title` if a confident title is available.
|
|
32
|
+
* `title` if a confident title is available. Overrides may be async (e.g. `TypescriptExtractor` parses via the native compiler server) — `extract()` awaits the result either way.
|
|
33
33
|
*
|
|
34
34
|
* @param name The basename of the file without extension (e.g. `"array"`).
|
|
35
35
|
* @param content The raw text content of the file.
|
|
36
|
-
* @returns The element props, always including a `name`; the base implementation stores `content` verbatim.
|
|
36
|
+
* @returns The element props (or a promise resolving to them), always including a `name`; the base implementation stores `content` verbatim.
|
|
37
37
|
* @example extractProps("notes", "Some text") // { name: "notes", content: "Some text" }
|
|
38
38
|
* @see https://shelving.cc/extract/FileExtractor/extractProps
|
|
39
39
|
*/
|
|
40
|
-
extractProps(name: string, content: string): Partial<TreeElementProps> & {
|
|
40
|
+
extractProps(name: string, content: string): (Partial<TreeElementProps> & {
|
|
41
41
|
name: string;
|
|
42
|
-
}
|
|
42
|
+
}) | Promise<Partial<TreeElementProps> & {
|
|
43
|
+
name: string;
|
|
44
|
+
}>;
|
|
43
45
|
}
|
package/extract/FileExtractor.js
CHANGED
|
@@ -32,7 +32,7 @@ export class FileExtractor extends Extractor {
|
|
|
32
32
|
const filename = splitPath(source).at(-1) ?? "unnamed";
|
|
33
33
|
const [base = filename] = splitFileExtension(filename);
|
|
34
34
|
const text = await file.text();
|
|
35
|
-
const props = { ...this.extractProps(base, text), source };
|
|
35
|
+
const props = { ...(await this.extractProps(base, text)), source };
|
|
36
36
|
return {
|
|
37
37
|
type: "tree-element",
|
|
38
38
|
key: filename,
|
|
@@ -43,11 +43,11 @@ export class FileExtractor extends Extractor {
|
|
|
43
43
|
* Build the file element props from the extracted content.
|
|
44
44
|
* - `name` is the basename without extension (e.g. `"array"`) — display-ready, used by menus, cards, and URL paths.
|
|
45
45
|
* - Override to parse `text` into richer elements (content/children/description) and to set
|
|
46
|
-
* `title` if a confident title is available.
|
|
46
|
+
* `title` if a confident title is available. Overrides may be async (e.g. `TypescriptExtractor` parses via the native compiler server) — `extract()` awaits the result either way.
|
|
47
47
|
*
|
|
48
48
|
* @param name The basename of the file without extension (e.g. `"array"`).
|
|
49
49
|
* @param content The raw text content of the file.
|
|
50
|
-
* @returns The element props, always including a `name`; the base implementation stores `content` verbatim.
|
|
50
|
+
* @returns The element props (or a promise resolving to them), always including a `name`; the base implementation stores `content` verbatim.
|
|
51
51
|
* @example extractProps("notes", "Some text") // { name: "notes", content: "Some text" }
|
|
52
52
|
* @see https://shelving.cc/extract/FileExtractor/extractProps
|
|
53
53
|
*/
|
|
@@ -2,7 +2,8 @@ import type { TreeElementProps } from "../util/tree.js";
|
|
|
2
2
|
import { FileExtractor } from "./FileExtractor.js";
|
|
3
3
|
/**
|
|
4
4
|
* File extractor that parses a TypeScript source file into a tree element.
|
|
5
|
-
* - Uses the TypeScript compiler API to parse the AST.
|
|
5
|
+
* - Uses the native TypeScript compiler's API (`typescript/unstable/async` — TypeScript 7+) to parse the AST.
|
|
6
|
+
* - Parsing runs on a shared native compiler server, spawned on demand into a virtual filesystem and closed again after a short idle — callers never manage its lifecycle.
|
|
6
7
|
* - Extracts exported, public, non-`_`-prefixed declarations as `tree-documentation` children.
|
|
7
8
|
* - Overloaded declarations sharing a name are merged into a single `tree-documentation` with multiple `signatures`. When a function (or constructor) is overloaded, the implementation declaration (the "base definition") is dropped entirely — only the overload signatures are documented.
|
|
8
9
|
* - A `@param name {Type}` (or `@returns {Type}`) whose `{Type}` is given is canonical: it supersedes the inferred type from the base definition and any overloads. Multiple `@param name` tags for one parameter each emit a row, letting a single parameter be documented as several typed variants.
|
|
@@ -18,13 +19,13 @@ import { FileExtractor } from "./FileExtractor.js";
|
|
|
18
19
|
* - Keys are the raw declared `name` (case-preserving) so case-distinct exports like `Collection` and `COLLECTION` stay separate.
|
|
19
20
|
* - The file element itself has no `title` — a TS source file has no confident title source; renderers fall back to `name`.
|
|
20
21
|
*
|
|
21
|
-
* @example const element = new TypescriptExtractor().extractProps("string.ts", sourceText);
|
|
22
|
+
* @example const element = await new TypescriptExtractor().extractProps("string.ts", sourceText);
|
|
22
23
|
*
|
|
23
24
|
* @see https://shelving.cc/extract/TypescriptExtractor
|
|
24
25
|
*/
|
|
25
26
|
export declare class TypescriptExtractor extends FileExtractor {
|
|
26
27
|
/** Parses the TypeScript source into one `tree-documentation` child per exported public declaration. */
|
|
27
|
-
extractProps(name: string, text: string): Partial<TreeElementProps> & {
|
|
28
|
+
extractProps(name: string, text: string): Promise<Partial<TreeElementProps> & {
|
|
28
29
|
name: string;
|
|
29
|
-
}
|
|
30
|
+
}>;
|
|
30
31
|
}
|
|
@@ -1,9 +1,13 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { getTextOfJSDocComment, isArrayBindingPattern, isBindingElement, isClassDeclaration, isConstructorDeclaration, isEnumDeclaration, isFunctionDeclaration, isGetAccessorDeclaration, isIdentifier, isInterfaceDeclaration, isJSDoc, isJSDocParameterTag, isJSDocReturnTag, isJSDocSeeTag, isJSDocThrowsTag, isJSDocTypeExpression, isMethodDeclaration, isMethodSignatureDeclaration, isObjectBindingPattern, isPropertyDeclaration, isPropertySignatureDeclaration, isSetAccessorDeclaration, isTypeAliasDeclaration, isTypeLiteralNode, isTypeReferenceNode, isVariableStatement, SyntaxKind, } from "typescript/unstable/ast";
|
|
2
|
+
import { API } from "typescript/unstable/async";
|
|
3
|
+
import { ValueError } from "../error/ValueError.js";
|
|
4
|
+
import { BLACKHOLE } from "../util/function.js";
|
|
2
5
|
import { FileExtractor } from "./FileExtractor.js";
|
|
3
6
|
import { extractMarkdownProps } from "./MarkupExtractor.js";
|
|
4
7
|
/**
|
|
5
8
|
* File extractor that parses a TypeScript source file into a tree element.
|
|
6
|
-
* - Uses the TypeScript compiler API to parse the AST.
|
|
9
|
+
* - Uses the native TypeScript compiler's API (`typescript/unstable/async` — TypeScript 7+) to parse the AST.
|
|
10
|
+
* - Parsing runs on a shared native compiler server, spawned on demand into a virtual filesystem and closed again after a short idle — callers never manage its lifecycle.
|
|
7
11
|
* - Extracts exported, public, non-`_`-prefixed declarations as `tree-documentation` children.
|
|
8
12
|
* - Overloaded declarations sharing a name are merged into a single `tree-documentation` with multiple `signatures`. When a function (or constructor) is overloaded, the implementation declaration (the "base definition") is dropped entirely — only the overload signatures are documented.
|
|
9
13
|
* - A `@param name {Type}` (or `@returns {Type}`) whose `{Type}` is given is canonical: it supersedes the inferred type from the base definition and any overloads. Multiple `@param name` tags for one parameter each emit a row, letting a single parameter be documented as several typed variants.
|
|
@@ -19,25 +23,25 @@ import { extractMarkdownProps } from "./MarkupExtractor.js";
|
|
|
19
23
|
* - Keys are the raw declared `name` (case-preserving) so case-distinct exports like `Collection` and `COLLECTION` stay separate.
|
|
20
24
|
* - The file element itself has no `title` — a TS source file has no confident title source; renderers fall back to `name`.
|
|
21
25
|
*
|
|
22
|
-
* @example const element = new TypescriptExtractor().extractProps("string.ts", sourceText);
|
|
26
|
+
* @example const element = await new TypescriptExtractor().extractProps("string.ts", sourceText);
|
|
23
27
|
*
|
|
24
28
|
* @see https://shelving.cc/extract/TypescriptExtractor
|
|
25
29
|
*/
|
|
26
30
|
export class TypescriptExtractor extends FileExtractor {
|
|
27
31
|
/** Parses the TypeScript source into one `tree-documentation` child per exported public declaration. */
|
|
28
|
-
extractProps(name, text) {
|
|
29
|
-
const source =
|
|
32
|
+
async extractProps(name, text) {
|
|
33
|
+
const source = await _parseSourceFile(text);
|
|
30
34
|
// Names with one or more overload signatures (bodyless function declarations). When a function is overloaded, the
|
|
31
35
|
// implementation declaration (the one with a body — the "base definition") is dropped entirely; only the overloads are documented.
|
|
32
36
|
const overloaded = new Set();
|
|
33
37
|
for (const statement of source.statements)
|
|
34
|
-
if (
|
|
38
|
+
if (isFunctionDeclaration(statement) && !statement.body && statement.name)
|
|
35
39
|
overloaded.add(statement.name.text);
|
|
36
40
|
// Collect elements by key, merging overloads (same name) by appending signatures.
|
|
37
41
|
const byKey = new Map();
|
|
38
42
|
for (const statement of source.statements) {
|
|
39
43
|
// Skip the implementation of an overloaded function — overloads supersede the base definition.
|
|
40
|
-
if (
|
|
44
|
+
if (isFunctionDeclaration(statement) && statement.body && statement.name && overloaded.has(statement.name.text))
|
|
41
45
|
continue;
|
|
42
46
|
const element = _extractStatement(statement, source);
|
|
43
47
|
if (!element)
|
|
@@ -50,6 +54,114 @@ export class TypescriptExtractor extends FileExtractor {
|
|
|
50
54
|
return { name, children: Array.from(byKey.values()) };
|
|
51
55
|
}
|
|
52
56
|
}
|
|
57
|
+
// Constants.
|
|
58
|
+
const _DIR = "/shelving-typescript-extractor";
|
|
59
|
+
const _TSCONFIG_PATH = `${_DIR}/tsconfig.json`;
|
|
60
|
+
const _TSCONFIG_JSON = JSON.stringify({ compilerOptions: { noEmit: true }, include: ["**/*.ts"] });
|
|
61
|
+
const _IDLE_CLOSE_MS = 500;
|
|
62
|
+
let _server;
|
|
63
|
+
let _queue = Promise.resolve();
|
|
64
|
+
let _timer;
|
|
65
|
+
let _count = 0;
|
|
66
|
+
let _previous;
|
|
67
|
+
/** Is a path inside the virtual directory the compiler server is rooted in? */
|
|
68
|
+
function _isVirtual(path) {
|
|
69
|
+
return path === _DIR || path.startsWith(`${_DIR}/`);
|
|
70
|
+
}
|
|
71
|
+
/** Get the shared compiler server, spawning it on demand with a virtual filesystem rooted at `_DIR`. */
|
|
72
|
+
function _getServer() {
|
|
73
|
+
if (_server)
|
|
74
|
+
return _server;
|
|
75
|
+
const files = new Map([[_TSCONFIG_PATH, _TSCONFIG_JSON]]);
|
|
76
|
+
// Paths outside the virtual directory return `undefined` so the server falls back to the real filesystem (e.g. bundled libs).
|
|
77
|
+
const fs = {
|
|
78
|
+
fileExists: f => (_isVirtual(f) ? files.has(f) : undefined),
|
|
79
|
+
readFile: f => (_isVirtual(f) ? (files.get(f) ?? null) : undefined),
|
|
80
|
+
directoryExists: d => (d === _DIR ? true : _isVirtual(d) ? false : undefined),
|
|
81
|
+
getAccessibleEntries: d => d === _DIR
|
|
82
|
+
? { files: Array.from(files.keys(), f => f.slice(_DIR.length + 1)), directories: [] }
|
|
83
|
+
: _isVirtual(d)
|
|
84
|
+
? { files: [], directories: [] }
|
|
85
|
+
: undefined,
|
|
86
|
+
realpath: p => (_isVirtual(p) ? p : undefined),
|
|
87
|
+
};
|
|
88
|
+
_server = { api: new API({ cwd: _DIR, fs }), files };
|
|
89
|
+
return _server;
|
|
90
|
+
}
|
|
91
|
+
/** Close the shared compiler server after a short idle, so the process can exit without callers managing the lifecycle. */
|
|
92
|
+
function _scheduleClose() {
|
|
93
|
+
if (_timer)
|
|
94
|
+
clearTimeout(_timer);
|
|
95
|
+
_timer = setTimeout(() => {
|
|
96
|
+
const server = _server;
|
|
97
|
+
_server = undefined;
|
|
98
|
+
_previous = undefined;
|
|
99
|
+
_timer = undefined;
|
|
100
|
+
try {
|
|
101
|
+
server?.api.close();
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
// Closing can reject in-flight bookkeeping requests — harmless on shutdown.
|
|
105
|
+
}
|
|
106
|
+
}, _IDLE_CLOSE_MS);
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Parse TypeScript source text into a `SourceFile` via the shared native compiler server.
|
|
110
|
+
* - Each call writes the text as a fresh virtual file, snapshots the project, and materialises the file's AST.
|
|
111
|
+
* - Calls are serialised through a queue so concurrent extractions don't interleave snapshot updates.
|
|
112
|
+
*
|
|
113
|
+
* @throws `ValueError` If the server fails to produce a source file for the text.
|
|
114
|
+
*/
|
|
115
|
+
function _parseSourceFile(text) {
|
|
116
|
+
const task = async () => {
|
|
117
|
+
if (_timer)
|
|
118
|
+
clearTimeout(_timer);
|
|
119
|
+
const { api, files } = _getServer();
|
|
120
|
+
const path = `${_DIR}/${_count++}.ts`;
|
|
121
|
+
files.set(path, text);
|
|
122
|
+
// Drop the previous parse's file so the virtual project stays a single file (keeps snapshots cheap).
|
|
123
|
+
const deleted = _previous;
|
|
124
|
+
if (deleted)
|
|
125
|
+
files.delete(deleted);
|
|
126
|
+
_previous = path;
|
|
127
|
+
const snapshot = await api.updateSnapshot({
|
|
128
|
+
openProjects: [_TSCONFIG_PATH],
|
|
129
|
+
fileChanges: { created: [path], ...(deleted ? { deleted: [deleted] } : {}) },
|
|
130
|
+
});
|
|
131
|
+
try {
|
|
132
|
+
const source = await snapshot.getProject(_TSCONFIG_PATH)?.program.getSourceFile(path);
|
|
133
|
+
if (!source)
|
|
134
|
+
throw new ValueError("Unable to parse TypeScript source", { received: text });
|
|
135
|
+
return source;
|
|
136
|
+
}
|
|
137
|
+
finally {
|
|
138
|
+
snapshot.dispose();
|
|
139
|
+
_scheduleClose();
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
const result = _queue.then(task, task);
|
|
143
|
+
_queue = result.catch(BLACKHOLE);
|
|
144
|
+
return result;
|
|
145
|
+
}
|
|
146
|
+
/** Read a node's modifiers (excluding decorators), or `undefined` when it has none. */
|
|
147
|
+
function _getModifiers(node) {
|
|
148
|
+
const modifiers = node.modifiers;
|
|
149
|
+
const filtered = modifiers?.filter((m) => m.kind !== SyntaxKind.Decorator);
|
|
150
|
+
return filtered?.length ? filtered : undefined;
|
|
151
|
+
}
|
|
152
|
+
/** Get a member's declared identifier name, or `undefined` for computed / private / missing names. */
|
|
153
|
+
function _getMemberName(member) {
|
|
154
|
+
// Class/type member bases don't surface `name` structurally — read it off the node and narrow.
|
|
155
|
+
const { name } = member;
|
|
156
|
+
return name && isIdentifier(name) ? name.text : undefined;
|
|
157
|
+
}
|
|
158
|
+
/** Get the text of a JSDoc tag's `{Type}` expression — unwraps a `JSDocTypeExpression` to its inner type. */
|
|
159
|
+
function _getJSDocTypeText(typeExpression, source) {
|
|
160
|
+
if (!typeExpression)
|
|
161
|
+
return;
|
|
162
|
+
const type = isJSDocTypeExpression(typeExpression) ? typeExpression.type : typeExpression;
|
|
163
|
+
return type.getText(source);
|
|
164
|
+
}
|
|
53
165
|
/** Merge a newly-extracted overload into the existing documentation element with the same key. */
|
|
54
166
|
function _mergeOverloads(existing, next) {
|
|
55
167
|
const a = existing.props;
|
|
@@ -146,7 +258,7 @@ function _extractStatement(statement, source) {
|
|
|
146
258
|
}
|
|
147
259
|
/** Extract the `extends` (single base type) and `implements` (interface list) heritage from a class or interface declaration, as full type text including any generic arguments (e.g. `AbstractStore<string>`, `Omit<StringSchemaOptions, "value">`). */
|
|
148
260
|
function _getHeritage(statement, source) {
|
|
149
|
-
if (!
|
|
261
|
+
if (!isClassDeclaration(statement) && !isInterfaceDeclaration(statement))
|
|
150
262
|
return;
|
|
151
263
|
let extendsName;
|
|
152
264
|
const implementsNames = [];
|
|
@@ -154,9 +266,9 @@ function _getHeritage(statement, source) {
|
|
|
154
266
|
// Full text — keep generic arguments (`Foo<T>`) and wrappers (`Omit<…>`) intact; render-time lookup trims them to the bare name to resolve a link.
|
|
155
267
|
const names = clause.types.map(t => t.getText(source));
|
|
156
268
|
// `extends` keeps the first base type; an interface extending several still surfaces its primary base.
|
|
157
|
-
if (clause.token ===
|
|
269
|
+
if (clause.token === SyntaxKind.ExtendsKeyword)
|
|
158
270
|
extendsName ??= names[0];
|
|
159
|
-
else if (clause.token ===
|
|
271
|
+
else if (clause.token === SyntaxKind.ImplementsKeyword)
|
|
160
272
|
implementsNames.push(...names);
|
|
161
273
|
}
|
|
162
274
|
if (!extendsName && !implementsNames.length)
|
|
@@ -170,13 +282,13 @@ function _getHeritage(statement, source) {
|
|
|
170
282
|
* - Order-preserving and de-duplicated. Unresolved names (builtins like `Record`, externals) simply stay as plain text at render time.
|
|
171
283
|
*/
|
|
172
284
|
function _getReferencedTypes(statement, source) {
|
|
173
|
-
if (!
|
|
285
|
+
if (!isTypeAliasDeclaration(statement))
|
|
174
286
|
return;
|
|
175
287
|
const generics = new Set(statement.typeParameters?.map(t => t.name.text) ?? []);
|
|
176
288
|
const names = [];
|
|
177
289
|
const seen = new Set();
|
|
178
290
|
const visit = (node) => {
|
|
179
|
-
if (
|
|
291
|
+
if (isTypeReferenceNode(node)) {
|
|
180
292
|
const name = node.typeName.getText(source);
|
|
181
293
|
if (!generics.has(name) && !seen.has(name)) {
|
|
182
294
|
seen.add(name);
|
|
@@ -196,44 +308,45 @@ function _getReferencedTypes(statement, source) {
|
|
|
196
308
|
* - Getters/setters fold into a single entry per name (the getter's type wins for a get/set pair).
|
|
197
309
|
*/
|
|
198
310
|
function _getProperties(statement, source) {
|
|
199
|
-
const members =
|
|
311
|
+
const members = isInterfaceDeclaration(statement) || isClassDeclaration(statement)
|
|
200
312
|
? statement.members
|
|
201
|
-
:
|
|
313
|
+
: isTypeAliasDeclaration(statement) && isTypeLiteralNode(statement.type)
|
|
202
314
|
? statement.type.members
|
|
203
315
|
: undefined;
|
|
204
316
|
if (!members)
|
|
205
317
|
return;
|
|
206
318
|
const properties = [];
|
|
207
319
|
for (const member of members) {
|
|
208
|
-
const name =
|
|
320
|
+
const name = _getMemberName(member);
|
|
209
321
|
if (!name || name.startsWith("_"))
|
|
210
322
|
continue;
|
|
211
|
-
const modifiers =
|
|
323
|
+
const modifiers = _getModifiers(member);
|
|
212
324
|
// Skip private/protected (not public API), `override` (documented on the base class), and `declare` (ambient re-declarations).
|
|
213
|
-
if (modifiers?.some(m => m.kind ===
|
|
325
|
+
if (modifiers?.some(m => m.kind === SyntaxKind.PrivateKeyword || m.kind === SyntaxKind.ProtectedKeyword))
|
|
214
326
|
continue;
|
|
215
|
-
if (modifiers?.some(m => m.kind ===
|
|
327
|
+
if (modifiers?.some(m => m.kind === SyntaxKind.OverrideKeyword || m.kind === SyntaxKind.DeclareKeyword))
|
|
216
328
|
continue;
|
|
217
329
|
const jsDoc = _getJSDoc(member, source);
|
|
218
|
-
if (
|
|
330
|
+
if (isPropertySignatureDeclaration(member) || isPropertyDeclaration(member)) {
|
|
219
331
|
// A class field's initializer is its default; otherwise fall back to an explicit `@default` tag.
|
|
220
|
-
const def = (
|
|
332
|
+
const def = (isPropertyDeclaration(member) ? member.initializer?.getText(source) : undefined) ?? jsDoc?.default;
|
|
221
333
|
properties.push({
|
|
222
334
|
name,
|
|
223
335
|
type: member.type?.getText(source),
|
|
224
336
|
description: jsDoc?.description,
|
|
225
|
-
|
|
337
|
+
// Optionality is a `?` postfix token (the postfix slot also carries `!` definite-assignment, which isn't optionality).
|
|
338
|
+
optional: member.postfixToken?.kind === SyntaxKind.QuestionToken,
|
|
226
339
|
default: def,
|
|
227
|
-
readonly: modifiers?.some(m => m.kind ===
|
|
340
|
+
readonly: modifiers?.some(m => m.kind === SyntaxKind.ReadonlyKeyword) || undefined,
|
|
228
341
|
});
|
|
229
342
|
}
|
|
230
|
-
else if (
|
|
231
|
-
const type =
|
|
343
|
+
else if (isGetAccessorDeclaration(member) || isSetAccessorDeclaration(member)) {
|
|
344
|
+
const type = isGetAccessorDeclaration(member) ? member.type?.getText(source) : member.parameters[0]?.type?.getText(source);
|
|
232
345
|
const index = properties.findIndex(p => p.name === name);
|
|
233
346
|
const found = index >= 0 ? properties[index] : undefined;
|
|
234
347
|
// Fold a get/set pair into one entry: the getter's declared type wins, and the matching setter clears read-only.
|
|
235
348
|
if (found) {
|
|
236
|
-
properties[index] = { ...found, type:
|
|
349
|
+
properties[index] = { ...found, type: isGetAccessorDeclaration(member) && type ? type : found.type, readonly: undefined };
|
|
237
350
|
}
|
|
238
351
|
else {
|
|
239
352
|
// A lone getter is read-only until a setter is seen; a lone setter is writable.
|
|
@@ -243,7 +356,7 @@ function _getProperties(statement, source) {
|
|
|
243
356
|
description: jsDoc?.description,
|
|
244
357
|
optional: false,
|
|
245
358
|
default: jsDoc?.default,
|
|
246
|
-
readonly:
|
|
359
|
+
readonly: isGetAccessorDeclaration(member) || undefined,
|
|
247
360
|
});
|
|
248
361
|
}
|
|
249
362
|
}
|
|
@@ -264,59 +377,58 @@ function _buildJSDocContent(description, unhandled) {
|
|
|
264
377
|
}
|
|
265
378
|
/** Check if a statement has an `export` modifier. */
|
|
266
379
|
function _isExported(statement) {
|
|
267
|
-
|
|
268
|
-
return !!modifiers?.some(m => m.kind === ts.SyntaxKind.ExportKeyword);
|
|
380
|
+
return !!_getModifiers(statement)?.some(m => m.kind === SyntaxKind.ExportKeyword);
|
|
269
381
|
}
|
|
270
382
|
/** Get the declared name of a statement. */
|
|
271
383
|
function _getStatementName(statement) {
|
|
272
|
-
if (
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
384
|
+
if (isFunctionDeclaration(statement) ||
|
|
385
|
+
isClassDeclaration(statement) ||
|
|
386
|
+
isInterfaceDeclaration(statement) ||
|
|
387
|
+
isTypeAliasDeclaration(statement) ||
|
|
388
|
+
isEnumDeclaration(statement)) {
|
|
277
389
|
return statement.name?.text;
|
|
278
390
|
}
|
|
279
|
-
if (
|
|
391
|
+
if (isVariableStatement(statement)) {
|
|
280
392
|
const declaration = statement.declarationList.declarations[0];
|
|
281
|
-
if (declaration &&
|
|
393
|
+
if (declaration && isIdentifier(declaration.name))
|
|
282
394
|
return declaration.name.text;
|
|
283
395
|
}
|
|
284
396
|
}
|
|
285
397
|
/** Map a statement to its documentation kind. */
|
|
286
398
|
function _getKind(statement) {
|
|
287
|
-
if (
|
|
399
|
+
if (isFunctionDeclaration(statement))
|
|
288
400
|
return "function";
|
|
289
|
-
if (
|
|
401
|
+
if (isClassDeclaration(statement))
|
|
290
402
|
return "class";
|
|
291
|
-
if (
|
|
403
|
+
if (isInterfaceDeclaration(statement))
|
|
292
404
|
return "interface";
|
|
293
|
-
if (
|
|
405
|
+
if (isTypeAliasDeclaration(statement))
|
|
294
406
|
return "type";
|
|
295
|
-
if (
|
|
407
|
+
if (isVariableStatement(statement))
|
|
296
408
|
return "constant";
|
|
297
409
|
}
|
|
298
410
|
/** Get the text signature(s) of a statement — complete, name-prefixed declarations usable as headings. */
|
|
299
411
|
function _getSignatures(statement, source, name) {
|
|
300
|
-
if (
|
|
412
|
+
if (isFunctionDeclaration(statement)) {
|
|
301
413
|
const params = statement.parameters.map(p => p.getText(source)).join(", ");
|
|
302
414
|
const ret = statement.type ? statement.type.getText(source) : "void";
|
|
303
415
|
return [`${name}(${params}): ${ret}`];
|
|
304
416
|
}
|
|
305
|
-
if (
|
|
417
|
+
if (isClassDeclaration(statement)) {
|
|
306
418
|
// Synthesise `new ClassName<…>(…)` constructor signatures so a class page reads like a function's.
|
|
307
419
|
return _getConstructorSignatures(statement, source, name);
|
|
308
420
|
}
|
|
309
|
-
if (
|
|
421
|
+
if (isInterfaceDeclaration(statement)) {
|
|
310
422
|
// Emit a pretty-printed `{ member; member }` block — the same shape a `type` object body produces, distinguished only by the `kind` badge.
|
|
311
423
|
return [_formatObjectSignature(statement.members, source)];
|
|
312
424
|
}
|
|
313
|
-
if (
|
|
425
|
+
if (isTypeAliasDeclaration(statement)) {
|
|
314
426
|
// Pretty-print object-literal aliases multi-line like an interface; emit other bodies (`string | null`, mapped types, …) verbatim. The alias name is already the page title.
|
|
315
|
-
if (
|
|
427
|
+
if (isTypeLiteralNode(statement.type))
|
|
316
428
|
return [_formatObjectSignature(statement.type.members, source)];
|
|
317
429
|
return [statement.type.getText(source)];
|
|
318
430
|
}
|
|
319
|
-
if (
|
|
431
|
+
if (isVariableStatement(statement)) {
|
|
320
432
|
const declaration = statement.declarationList.declarations[0];
|
|
321
433
|
if (declaration?.type)
|
|
322
434
|
return [`${name}: ${declaration.type.getText(source)}`];
|
|
@@ -338,7 +450,7 @@ function _getTypeParamNames(statement, source) {
|
|
|
338
450
|
}
|
|
339
451
|
/** Get a class's constructor declarations in source order — when overloaded, only the overload signatures (the implementation, i.e. the base definition, is dropped). */
|
|
340
452
|
function _getConstructors(statement) {
|
|
341
|
-
const constructors = statement.members.filter(
|
|
453
|
+
const constructors = statement.members.filter(isConstructorDeclaration);
|
|
342
454
|
// When overload signatures exist, drop the implementation constructor (the one with a body) — overloads supersede the base definition.
|
|
343
455
|
return constructors.some(c => !c.body) ? constructors.filter(c => !c.body) : constructors;
|
|
344
456
|
}
|
|
@@ -357,9 +469,9 @@ function _getConstructorSignatures(statement, source, name) {
|
|
|
357
469
|
}
|
|
358
470
|
/** Extract parameters from a function or class declaration, enriched with JSDoc `@param` descriptions. */
|
|
359
471
|
function _getParams(statement, source, jsDocParams) {
|
|
360
|
-
if (
|
|
472
|
+
if (isClassDeclaration(statement))
|
|
361
473
|
return _getConstructorParams(statement, source, jsDocParams);
|
|
362
|
-
if (!
|
|
474
|
+
if (!isFunctionDeclaration(statement))
|
|
363
475
|
return;
|
|
364
476
|
const params = _buildParams(statement.parameters, source, jsDocParams);
|
|
365
477
|
return params.length ? params : undefined;
|
|
@@ -377,7 +489,7 @@ function _buildParams(parameters, source, primaryJsDocParams, fallbackJsDocParam
|
|
|
377
489
|
// Identifier (non-destructured) parameter names — used to spot "orphan" `@param` tags that name a destructured bag.
|
|
378
490
|
const named = new Set();
|
|
379
491
|
for (const p of parameters)
|
|
380
|
-
if (
|
|
492
|
+
if (isIdentifier(p.name))
|
|
381
493
|
named.add(p.name.getText(source));
|
|
382
494
|
// Top-level `@param` names not matching any identifier parameter, in declared order (constructor's own first, then class-level), de-duplicated — these let an author name a destructured param (`@param options`). Sub-tags (`options.min`) are excluded.
|
|
383
495
|
const orphans = [];
|
|
@@ -386,7 +498,7 @@ function _buildParams(parameters, source, primaryJsDocParams, fallbackJsDocParam
|
|
|
386
498
|
orphans.push(d.name);
|
|
387
499
|
let orphan = 0;
|
|
388
500
|
for (const p of parameters) {
|
|
389
|
-
const name =
|
|
501
|
+
const name = isIdentifier(p.name) ? p.name.getText(source) : (orphans[orphan++] ?? _getBindingName(p, source));
|
|
390
502
|
const type = p.type?.getText(source);
|
|
391
503
|
const optional = !!p.questionToken || !!p.initializer;
|
|
392
504
|
const def = p.initializer?.getText(source);
|
|
@@ -413,9 +525,9 @@ function _buildParams(parameters, source, primaryJsDocParams, fallbackJsDocParam
|
|
|
413
525
|
*/
|
|
414
526
|
function _getBindingName(p, source) {
|
|
415
527
|
const { name } = p;
|
|
416
|
-
if (
|
|
528
|
+
if (isObjectBindingPattern(name) || isArrayBindingPattern(name))
|
|
417
529
|
for (const el of name.elements)
|
|
418
|
-
if (
|
|
530
|
+
if (isBindingElement(el) && el.dotDotDotToken && el.name && isIdentifier(el.name))
|
|
419
531
|
return el.name.text;
|
|
420
532
|
return p.type?.getText(source).replace(/<.*/s, "").endsWith("Props") ? "props" : "options";
|
|
421
533
|
}
|
|
@@ -436,12 +548,12 @@ function _getConstructorParams(statement, source, classJsDocParams) {
|
|
|
436
548
|
}
|
|
437
549
|
/** Extract return entries — combines the signature return type with any `@returns` descriptions. */
|
|
438
550
|
function _getReturns(statement, source, jsDocReturns, name) {
|
|
439
|
-
if (
|
|
551
|
+
if (isClassDeclaration(statement)) {
|
|
440
552
|
// A constructor returns an instance of the class, including its generics (e.g. `ChoiceSchema<T>`).
|
|
441
553
|
const type = `${name}${_getTypeParamNames(statement, source)}`;
|
|
442
554
|
return [{ type, description: jsDocReturns?.[0]?.description }];
|
|
443
555
|
}
|
|
444
|
-
if (!
|
|
556
|
+
if (!isFunctionDeclaration(statement))
|
|
445
557
|
return jsDocReturns;
|
|
446
558
|
const type = statement.type?.getText(source);
|
|
447
559
|
if (jsDocReturns?.length) {
|
|
@@ -463,31 +575,31 @@ function _getReturns(statement, source, jsDocReturns, name) {
|
|
|
463
575
|
* - `static` methods are labelled `static method` so the docs site groups them in their own section, separate from instance `method`s, and their rendered signature is prefixed with the `static ` keyword.
|
|
464
576
|
*/
|
|
465
577
|
function _getClassMembers(statement, source, className) {
|
|
466
|
-
if (!
|
|
578
|
+
if (!isClassDeclaration(statement) && !isInterfaceDeclaration(statement))
|
|
467
579
|
return;
|
|
468
580
|
const members = [];
|
|
469
581
|
for (const member of statement.members) {
|
|
470
582
|
// Skip private, protected, and _-prefixed members.
|
|
471
|
-
const name =
|
|
583
|
+
const name = _getMemberName(member);
|
|
472
584
|
if (!name || name.startsWith("_"))
|
|
473
585
|
continue;
|
|
474
|
-
const modifiers =
|
|
475
|
-
if (modifiers?.some(m => m.kind ===
|
|
586
|
+
const modifiers = _getModifiers(member);
|
|
587
|
+
if (modifiers?.some(m => m.kind === SyntaxKind.PrivateKeyword || m.kind === SyntaxKind.ProtectedKeyword))
|
|
476
588
|
continue;
|
|
477
589
|
// Skip `override` members — the base class already documents them, so a subclass page lists only its newly-introduced API.
|
|
478
|
-
if (modifiers?.some(m => m.kind ===
|
|
590
|
+
if (modifiers?.some(m => m.kind === SyntaxKind.OverrideKeyword))
|
|
479
591
|
continue;
|
|
480
592
|
// Skip `declare` members — ambient type-only re-declarations (e.g. a subclass narrowing an inherited property's type), not new API.
|
|
481
|
-
if (modifiers?.some(m => m.kind ===
|
|
593
|
+
if (modifiers?.some(m => m.kind === SyntaxKind.DeclareKeyword))
|
|
482
594
|
continue;
|
|
483
595
|
// `static` methods are grouped and labelled separately from instance methods (`static method`),
|
|
484
596
|
// and carry the `static ` keyword in their rendered signature.
|
|
485
|
-
const isStatic = modifiers?.some(m => m.kind ===
|
|
597
|
+
const isStatic = modifiers?.some(m => m.kind === SyntaxKind.StaticKeyword);
|
|
486
598
|
const staticPrefix = isStatic ? "static " : "";
|
|
487
599
|
const memberJSDoc = _getJSDoc(member, source);
|
|
488
600
|
const content = _buildJSDocContent(memberJSDoc?.description, memberJSDoc?.unhandled);
|
|
489
601
|
const description = extractMarkdownProps(memberJSDoc?.description ?? "").description;
|
|
490
|
-
if (
|
|
602
|
+
if (isMethodDeclaration(member) || isMethodSignatureDeclaration(member)) {
|
|
491
603
|
const params = member.parameters.map(p => p.getText(source)).join(", ");
|
|
492
604
|
const ret = member.type ? member.type.getText(source) : "void";
|
|
493
605
|
const signature = `${staticPrefix}${name}(${params}): ${ret}`;
|
|
@@ -524,15 +636,15 @@ function _getClassMembers(statement, source, className) {
|
|
|
524
636
|
* Extract JSDoc from a node via the TypeScript compiler's parsed JSDoc AST.
|
|
525
637
|
* - Reads the compiler's structured tags rather than re-scanning the raw comment text, so tags are only ever recognised at real tag positions — a `@kind`/`@param`/etc. mentioned inline in prose is left in the description, not parsed.
|
|
526
638
|
* - Multi-line `@param` / `@returns` / `@throws` descriptions and `{Type}` expressions come through whole; `*` margins are already stripped.
|
|
527
|
-
* - `@example` bodies are taken verbatim.
|
|
639
|
+
* - `@example` bodies are taken verbatim. The parser is markdown-fence-aware: an `@word` line inside a balanced triple-backtick fence stays part of the example rather than starting a new tag. The flip side: an unbalanced run of three-plus backticks anywhere in the comment opens a fence that swallows every following tag — never write a bare triple-backtick in docblock prose.
|
|
528
640
|
* - `@see` is recognised only to discard it: it's a VS Code hover affordance (a link back to the docs site), never rendered into the page body.
|
|
529
641
|
*/
|
|
530
642
|
function _getJSDoc(node, source) {
|
|
531
643
|
// The compiler attaches every leading `/** */` block here; the last one is the doc comment.
|
|
532
644
|
const jsDoc = node.jsDoc?.at(-1);
|
|
533
|
-
if (!jsDoc)
|
|
645
|
+
if (!jsDoc || !isJSDoc(jsDoc))
|
|
534
646
|
return;
|
|
535
|
-
const description =
|
|
647
|
+
const description = getTextOfJSDocComment(jsDoc.comment)?.trim();
|
|
536
648
|
let kind;
|
|
537
649
|
let def;
|
|
538
650
|
const params = [];
|
|
@@ -541,24 +653,24 @@ function _getJSDoc(node, source) {
|
|
|
541
653
|
const examples = [];
|
|
542
654
|
const unhandled = [];
|
|
543
655
|
for (const tag of jsDoc.tags ?? []) {
|
|
544
|
-
const comment =
|
|
545
|
-
if (
|
|
656
|
+
const comment = getTextOfJSDocComment(tag.comment)?.trim();
|
|
657
|
+
if (isJSDocParameterTag(tag)) {
|
|
546
658
|
const name = tag.name.getText(source);
|
|
547
|
-
const type = tag.typeExpression
|
|
659
|
+
const type = _getJSDocTypeText(tag.typeExpression, source);
|
|
548
660
|
if (name)
|
|
549
661
|
params.push({ name, type: type || undefined, description: comment || undefined });
|
|
550
662
|
}
|
|
551
|
-
else if (
|
|
552
|
-
const type = tag.typeExpression
|
|
663
|
+
else if (isJSDocReturnTag(tag)) {
|
|
664
|
+
const type = _getJSDocTypeText(tag.typeExpression, source);
|
|
553
665
|
if (type || comment)
|
|
554
666
|
returns.push({ type: type || undefined, description: comment || undefined });
|
|
555
667
|
}
|
|
556
|
-
else if (
|
|
557
|
-
const type = tag.typeExpression
|
|
668
|
+
else if (isJSDocThrowsTag(tag)) {
|
|
669
|
+
const type = _getJSDocTypeText(tag.typeExpression, source);
|
|
558
670
|
if (type || comment)
|
|
559
671
|
throws.push({ type: type || undefined, description: comment || undefined });
|
|
560
672
|
}
|
|
561
|
-
else if (
|
|
673
|
+
else if (isJSDocSeeTag(tag)) {
|
|
562
674
|
// `@see` is a hover affordance only — strip it, never render it.
|
|
563
675
|
}
|
|
564
676
|
else {
|
package/markup/rule/fenced.d.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* - Same as Markdown syntax.
|
|
4
4
|
* - Start when we reach an opening fence on a new line.
|
|
5
5
|
* - Stop when we reach a matching closing fence on a new line, or the end of the string.
|
|
6
|
-
* - Closing fence must be exactly the same as the opening fence, and can be made of three or more
|
|
6
|
+
* - Closing fence must be exactly the same as the opening fence, and can be made of three or more backticks, or three or more `~~~` tildes.
|
|
7
7
|
* - If there's no closing fence the code block will run to the end of the current string.
|
|
8
8
|
* - Markdown-style four-space indent syntax is not supported (only fenced code since it's less confusing and more common).
|
|
9
9
|
*
|
package/markup/rule/fenced.js
CHANGED
|
@@ -7,7 +7,7 @@ const FENCE = "`{3,}|~{3,}";
|
|
|
7
7
|
* - Same as Markdown syntax.
|
|
8
8
|
* - Start when we reach an opening fence on a new line.
|
|
9
9
|
* - Stop when we reach a matching closing fence on a new line, or the end of the string.
|
|
10
|
-
* - Closing fence must be exactly the same as the opening fence, and can be made of three or more
|
|
10
|
+
* - Closing fence must be exactly the same as the opening fence, and can be made of three or more backticks, or three or more `~~~` tildes.
|
|
11
11
|
* - If there's no closing fence the code block will run to the end of the current string.
|
|
12
12
|
* - Markdown-style four-space indent syntax is not supported (only fenced code since it's less confusing and more common).
|
|
13
13
|
*
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "shelving",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.271.0",
|
|
4
4
|
"author": "Dave Houlbrooke <dave@shax.com>",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -15,13 +15,12 @@
|
|
|
15
15
|
"@types/bun": "^1.3.14",
|
|
16
16
|
"@types/react": "^19.2.17",
|
|
17
17
|
"@types/react-dom": "^19.2.3",
|
|
18
|
-
"@typescript/native-preview": "^7.0.0-dev.20260706.1",
|
|
19
18
|
"firebase": "^12.15.0",
|
|
20
19
|
"react": "^19.3.0-canary-fef12a01-20260413",
|
|
21
20
|
"react-dom": "^19.3.0-canary-fef12a01-20260413",
|
|
22
21
|
"stylelint": "^17.14.0",
|
|
23
22
|
"stylelint-config-standard": "^40.0.0",
|
|
24
|
-
"typescript": "^
|
|
23
|
+
"typescript": "^7.0.2"
|
|
25
24
|
},
|
|
26
25
|
"peerDependencies": {
|
|
27
26
|
"@google-cloud/firestore": ">=7.0.0",
|
|
@@ -69,7 +68,7 @@
|
|
|
69
68
|
"scripts": {
|
|
70
69
|
"test": "bun run --parallel test:*",
|
|
71
70
|
"test:lint": "biome check .",
|
|
72
|
-
"test:type": "
|
|
71
|
+
"test:type": "tsc --noEmit",
|
|
73
72
|
"test:style": "stylelint \"modules/**/*.css\"",
|
|
74
73
|
"test:unit": "bun test ./modules --concurrent --only-failures",
|
|
75
74
|
"fix": "bun run --sequential fix:*",
|
|
@@ -80,7 +79,7 @@
|
|
|
80
79
|
"build": "bun run --sequential build:*",
|
|
81
80
|
"build:0:setup": "rm -rf ./dist && mkdir -p ./dist",
|
|
82
81
|
"build:1:copy": "cp package.json dist/package.json && cp LICENSE.md dist/LICENSE.md && cp README.md dist/README.md && cp .npmignore dist/.npmignore && cp -r modules/ui dist/ui",
|
|
83
|
-
"build:2:emit": "
|
|
82
|
+
"build:2:emit": "tsc -p tsconfig.build.json",
|
|
84
83
|
"build:3:syntax": "bun run ./dist/index.js",
|
|
85
84
|
"build:4:unit": "bun test ./dist/**/*.test.js --concurrent --only-failures --bail"
|
|
86
85
|
},
|
package/ui/block/Row.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ReactElement } from "react";
|
|
2
|
+
import { type BlockVariants } from "../style/Block.js";
|
|
2
3
|
import { type FlexVariants } from "../style/Flex.js";
|
|
3
4
|
import type { OptionalChildProps } from "../util/index.js";
|
|
4
5
|
import type { BlockElement } from "./Block.js";
|
|
@@ -7,7 +8,7 @@ import type { BlockElement } from "./Block.js";
|
|
|
7
8
|
*
|
|
8
9
|
* @see https://shelving.cc/ui/RowProps
|
|
9
10
|
*/
|
|
10
|
-
export interface RowProps extends FlexVariants, OptionalChildProps {
|
|
11
|
+
export interface RowProps extends BlockVariants, FlexVariants, OptionalChildProps {
|
|
11
12
|
/**
|
|
12
13
|
* Element this `<Row>` renders as, e.g. "header" to output a "<header>"
|
|
13
14
|
* @default "div"
|
package/ui/block/Row.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ReactElement } from "react";
|
|
2
|
-
import { getBlockClass } from "../style/Block.js";
|
|
2
|
+
import { type BlockVariants, getBlockClass } from "../style/Block.js";
|
|
3
3
|
import { type FlexVariants, getFlexClass } from "../style/Flex.js";
|
|
4
4
|
import { getClass } from "../util/css.js";
|
|
5
5
|
import type { OptionalChildProps } from "../util/index.js";
|
|
@@ -10,7 +10,7 @@ import type { BlockElement } from "./Block.js";
|
|
|
10
10
|
*
|
|
11
11
|
* @see https://shelving.cc/ui/RowProps
|
|
12
12
|
*/
|
|
13
|
-
export interface RowProps extends FlexVariants, OptionalChildProps {
|
|
13
|
+
export interface RowProps extends BlockVariants, FlexVariants, OptionalChildProps {
|
|
14
14
|
/**
|
|
15
15
|
* Element this `<Row>` renders as, e.g. "header" to output a "<header>"
|
|
16
16
|
* @default "div"
|
package/ui/misc/Catcher.d.ts
CHANGED
|
@@ -2,7 +2,6 @@ import { Component, type ReactElement, type ReactNode } from "react";
|
|
|
2
2
|
import type { Callback } from "../../util/function.js";
|
|
3
3
|
import type { ChildProps } from "../util/props.js";
|
|
4
4
|
/**
|
|
5
|
-
import { RetryButton, RetryContext } from "../button/RetryButton.js";
|
|
6
5
|
* Props for a component that renders a caught error `reason`.
|
|
7
6
|
*
|
|
8
7
|
* @see https://shelving.cc/ui/ErrorProps
|
package/ui/misc/Catcher.tsx
CHANGED
|
@@ -9,7 +9,6 @@ import { Page } from "../page/Page.js";
|
|
|
9
9
|
import type { ChildProps } from "../util/props.js";
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
|
-
import { RetryButton, RetryContext } from "../button/RetryButton.js";
|
|
13
12
|
* Props for a component that renders a caught error `reason`.
|
|
14
13
|
*
|
|
15
14
|
* @see https://shelving.cc/ui/ErrorProps
|