@xh/hoist 83.0.1 → 83.1.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/CHANGELOG.md +23 -1
- package/build/types/cmp/error/ErrorBoundary.d.ts +2 -0
- package/build/types/cmp/error/ErrorBoundaryModel.d.ts +12 -0
- package/build/types/cmp/filter/FilterChooserModel.d.ts +18 -0
- package/build/types/cmp/form/Form.d.ts +4 -4
- package/build/types/cmp/form/formfieldset/FormFieldSetModel.d.ts +9 -0
- package/build/types/cmp/grouping/GroupingChooserModel.d.ts +16 -0
- package/build/types/cmp/pinpad/PinPadModel.d.ts +8 -0
- package/build/types/cmp/treemap/SplitTreeMapModel.d.ts +10 -10
- package/build/types/cmp/treemap/TreeMapModel.d.ts +19 -20
- package/build/types/core/HoistComponent.d.ts +20 -22
- package/build/types/core/load/LoadSpec.d.ts +19 -19
- package/build/types/data/impl/RecordSet.d.ts +2 -2
- package/build/types/desktop/cmp/input/DateInput.d.ts +1 -1
- package/build/types/desktop/cmp/leftrightchooser/LeftRightChooserModel.d.ts +1 -1
- package/build/types/svc/AutoRefreshService.d.ts +1 -1
- package/build/types/svc/ChangelogService.d.ts +2 -3
- package/build/types/svc/FetchService.d.ts +19 -17
- package/cmp/error/ErrorBoundary.ts +2 -0
- package/cmp/error/ErrorBoundaryModel.ts +12 -0
- package/cmp/filter/FilterChooserModel.ts +18 -0
- package/cmp/form/Form.ts +4 -4
- package/cmp/form/formfieldset/FormFieldSetModel.ts +9 -0
- package/cmp/grouping/GroupingChooserModel.ts +16 -0
- package/cmp/pinpad/PinPadModel.ts +8 -0
- package/cmp/treemap/SplitTreeMapModel.ts +10 -10
- package/cmp/treemap/TreeMapModel.ts +19 -20
- package/core/HoistComponent.ts +20 -22
- package/core/load/LoadSpec.ts +19 -20
- package/data/impl/RecordSet.ts +3 -3
- package/desktop/cmp/input/DateInput.ts +1 -1
- package/desktop/cmp/leftrightchooser/LeftRightChooserModel.ts +1 -1
- package/mcp/cli/ts.ts +9 -2
- package/mcp/data/ts-registry.ts +116 -12
- package/mcp/formatters/typescript.ts +50 -4
- package/mcp/tools/typescript.ts +9 -2
- package/package.json +2 -2
- package/svc/AutoRefreshService.ts +1 -1
- package/svc/ChangelogService.ts +2 -3
- package/svc/EnvironmentService.ts +7 -5
- package/svc/FetchService.ts +20 -18
- package/svc/WebSocketService.ts +1 -1
|
@@ -14,26 +14,6 @@ import {throwIf, withDefault} from '@xh/hoist/utils/js';
|
|
|
14
14
|
import {ReactNode} from 'react';
|
|
15
15
|
import {cloneDeep, get, isEmpty, isFinite, max, set, sortBy, sumBy, unset} from 'lodash';
|
|
16
16
|
|
|
17
|
-
/**
|
|
18
|
-
* Core Model for a TreeMap.
|
|
19
|
-
*
|
|
20
|
-
* You should specify the TreeMap's data store, in addition to which StoreRecord fields should be
|
|
21
|
-
* mapped to label (a node's display name), value (a node's size), and heat (a node's color).
|
|
22
|
-
*
|
|
23
|
-
* Can also (optionally) be bound to a GridModel. This will enable selection syncing and
|
|
24
|
-
* expand / collapse syncing for GridModels in `treeMode`.
|
|
25
|
-
*
|
|
26
|
-
* Supports any Highcharts TreeMap algorithm ('squarified', 'sliceAndDice', 'stripes' or 'strip').
|
|
27
|
-
*
|
|
28
|
-
* Node colors are normalized to a 0-1 range and mapped to a colorAxis via the following colorModes:
|
|
29
|
-
* 'linear' distributes normalized color values across the colorAxis according to the heatField.
|
|
30
|
-
* 'wash' ignores the intensity of the heat value, applying a single positive and negative color.
|
|
31
|
-
* 'none' will ignore the colorAxis, and instead use the neutral color.
|
|
32
|
-
*
|
|
33
|
-
* Color customization can be managed by setting colorAxis stops via the `highchartsConfig`.
|
|
34
|
-
*
|
|
35
|
-
* @see https://www.highcharts.com/docs/chart-and-series-types/treemap for Highcharts config options
|
|
36
|
-
*/
|
|
37
17
|
export interface TreeMapConfig {
|
|
38
18
|
/** A store containing records to be displayed. */
|
|
39
19
|
store?: Store;
|
|
@@ -117,6 +97,25 @@ export interface TreeMapModelDefaults {
|
|
|
117
97
|
maxNodes?: number;
|
|
118
98
|
}
|
|
119
99
|
|
|
100
|
+
/**
|
|
101
|
+
* Core Model for a TreeMap, backed by a data Store with fields mapped to each node's label
|
|
102
|
+
* (display name), value (size), and heat (color).
|
|
103
|
+
*
|
|
104
|
+
* Can optionally bind to a GridModel to enable selection and expand/collapse syncing for
|
|
105
|
+
* GridModels in `treeMode`.
|
|
106
|
+
*
|
|
107
|
+
* Supports Highcharts TreeMap algorithms: 'squarified', 'sliceAndDice', 'stripes', and 'strip'.
|
|
108
|
+
*
|
|
109
|
+
* Node colors are normalized to a 0-1 range and mapped to a colorAxis. The `colorMode` config
|
|
110
|
+
* controls how:
|
|
111
|
+
* - 'linear' — distributes values across the colorAxis according to the heatField.
|
|
112
|
+
* - 'wash' — ignores heat intensity, applying a single positive/negative color.
|
|
113
|
+
* - 'none' — ignores the colorAxis entirely, using the neutral color.
|
|
114
|
+
*
|
|
115
|
+
* Color customization can be managed by setting colorAxis stops via `highchartsConfig`.
|
|
116
|
+
* See {@link https://www.highcharts.com/docs/chart-and-series-types/treemap} for Highcharts
|
|
117
|
+
* TreeMap config options.
|
|
118
|
+
*/
|
|
120
119
|
export class TreeMapModel extends HoistModel {
|
|
121
120
|
/** App-level defaults for TreeMapModel. Instance config takes precedence. */
|
|
122
121
|
static defaults: TreeMapModelDefaults = {
|
package/core/HoistComponent.ts
CHANGED
|
@@ -108,34 +108,32 @@ export type ComponentConfig<P extends HoistProps> =
|
|
|
108
108
|
let cmpIndex = 0; // index for anonymous component dispay names
|
|
109
109
|
|
|
110
110
|
/**
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
* configuration object to specify that function and additional options, as described below.
|
|
111
|
+
* The primary entry point for defining Hoist components — functional React components enhanced
|
|
112
|
+
* with MobX reactivity and integrated model support.
|
|
114
113
|
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
114
|
+
* Accepts a configuration object (or a bare render function) and returns a React functional
|
|
115
|
+
* component. The `model` config is the key option: use {@link creates} to have the component
|
|
116
|
+
* create and own its backing model, {@link uses} to source a model from props or context, or
|
|
117
|
+
* `false` for simple components with no model. Defaults to `uses('*')` if not specified.
|
|
118
118
|
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
* any other sources of observable state.
|
|
119
|
+
* Components are wrapped in the MobX `observer` HOC by default, enabling automatic re-rendering
|
|
120
|
+
* when observable state read during render changes.
|
|
122
121
|
*
|
|
123
|
-
* Forward refs {@link https://reactjs.org/docs/forwarding-refs.html} are supported by
|
|
124
|
-
* render function
|
|
125
|
-
*
|
|
122
|
+
* Forward refs ({@link https://reactjs.org/docs/forwarding-refs.html}) are supported by
|
|
123
|
+
* specifying a render function with two arguments — the second is treated as a ref, and
|
|
124
|
+
* `React.forwardRef` is applied automatically.
|
|
126
125
|
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
126
|
+
* Most components should be defined via one of two convenience methods rather than calling
|
|
127
|
+
* this function directly:
|
|
128
|
+
* - `hoistCmp.factory()` — returns an element factory (the standard pattern for app components).
|
|
129
|
+
* - `hoistCmp.withFactory()` — returns a `[Component, factory]` pair (the standard pattern for
|
|
130
|
+
* library components that need to export both).
|
|
131
131
|
*
|
|
132
|
-
*
|
|
132
|
+
* See `core/README.md` for full documentation on component configuration, model specs, and
|
|
133
|
+
* context lookup behavior.
|
|
133
134
|
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
* - `hoistCmp.withFactory` - return a 2-element list containing both the newly
|
|
138
|
-
* defined Component and an elementFactory for it.
|
|
135
|
+
* @param config - specification object, or a render function defining the component.
|
|
136
|
+
* @returns a functional React Component for use within Hoist apps.
|
|
139
137
|
*/
|
|
140
138
|
export function hoistCmp<M extends HoistModel>(
|
|
141
139
|
config: ComponentConfig<DefaultHoistProps<M>>
|
package/core/load/LoadSpec.ts
CHANGED
|
@@ -8,26 +8,6 @@
|
|
|
8
8
|
import {PlainObject} from '../types/Types';
|
|
9
9
|
import {LoadSupport} from './';
|
|
10
10
|
|
|
11
|
-
/**
|
|
12
|
-
* Object describing a load/refresh request in Hoist.
|
|
13
|
-
*
|
|
14
|
-
* Instances of this class are created within the public APIs provided by {@link LoadSupport}
|
|
15
|
-
* and are passed to subclass (i.e. app-level) implementations of `doLoadAsync()`.
|
|
16
|
-
*
|
|
17
|
-
* Application implementations of `doLoadAsync()` can consult this object's flags. Of particular
|
|
18
|
-
* interest are {@link isStale} and {@link isObsolete}, which implementations can read after any
|
|
19
|
-
* async calls return to determine if a newer, subsequent load has already been requested.
|
|
20
|
-
*
|
|
21
|
-
* In addition, `doLoadAsync()` implementations should typically pass along this object to any
|
|
22
|
-
* calls they make to `loadAsync()` on other objects + all calls to {@link FetchService} APIs.
|
|
23
|
-
*
|
|
24
|
-
* Note that Hoist's exception handling and activity tracking will consult the {@link isAutoRefresh}
|
|
25
|
-
* flag on specs passed to their calls to automatically adjust their behavior (e.g. not showing an
|
|
26
|
-
* exception dialog on error, not tracking background refresh activity).
|
|
27
|
-
*
|
|
28
|
-
* @see LoadSupport
|
|
29
|
-
*/
|
|
30
|
-
|
|
31
11
|
export type LoadSpecConfig = {
|
|
32
12
|
/** True if triggered by a refresh request (automatic or user-driven). */
|
|
33
13
|
isRefresh?: boolean;
|
|
@@ -37,6 +17,25 @@ export type LoadSpecConfig = {
|
|
|
37
17
|
meta?: PlainObject;
|
|
38
18
|
};
|
|
39
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Immutable descriptor for a load/refresh request, passed to `doLoadAsync()` implementations.
|
|
22
|
+
*
|
|
23
|
+
* Instances are created automatically by {@link LoadSupport} when `loadAsync()`,
|
|
24
|
+
* `refreshAsync()`, or `autoRefreshAsync()` are called. Application code should not construct
|
|
25
|
+
* LoadSpec instances directly.
|
|
26
|
+
*
|
|
27
|
+
* Within `doLoadAsync()`, check {@link isStale} after any async call to determine if a newer
|
|
28
|
+
* load has already been requested — if so, return early to avoid applying outdated results.
|
|
29
|
+
* Check {@link isAutoRefresh} to adjust behavior for background refreshes (e.g. skip expensive
|
|
30
|
+
* operations, avoid user-facing error dialogs).
|
|
31
|
+
*
|
|
32
|
+
* Pass this object along to any nested `loadAsync()` calls and to all {@link FetchService}
|
|
33
|
+
* requests. Hoist's exception handling and activity tracking consult the LoadSpec to
|
|
34
|
+
* automatically suppress error dialogs and skip tracking for auto-refresh operations.
|
|
35
|
+
*
|
|
36
|
+
* @see LoadSupport
|
|
37
|
+
* @see HoistModel.doLoadAsync
|
|
38
|
+
*/
|
|
40
39
|
export class LoadSpec {
|
|
41
40
|
/** True if triggered by a refresh request (automatic or user-driven). */
|
|
42
41
|
isRefresh: boolean;
|
package/data/impl/RecordSet.ts
CHANGED
|
@@ -12,15 +12,15 @@ import {StoreRecord, StoreRecordId} from '../StoreRecord';
|
|
|
12
12
|
import {Store} from '../Store';
|
|
13
13
|
import {Filter} from '../filter/Filter';
|
|
14
14
|
|
|
15
|
+
type StoreRecordMap = Map<StoreRecordId, StoreRecord>;
|
|
16
|
+
type ChildRecordMap = Map<StoreRecordId, StoreRecord[]>;
|
|
17
|
+
|
|
15
18
|
/**
|
|
16
19
|
* Internal container for StoreRecord management within a Store.
|
|
17
20
|
* Note this is an immutable object; its update and filtering APIs return new instances as required.
|
|
18
21
|
*
|
|
19
22
|
* @internal
|
|
20
23
|
*/
|
|
21
|
-
type StoreRecordMap = Map<StoreRecordId, StoreRecord>;
|
|
22
|
-
type ChildRecordMap = Map<StoreRecordId, StoreRecord[]>;
|
|
23
|
-
|
|
24
24
|
export class RecordSet {
|
|
25
25
|
store: Store;
|
|
26
26
|
recordMap: StoreRecordMap; // Map of all Records by id
|
|
@@ -146,7 +146,7 @@ export interface DateInputProps extends HoistProps, LayoutProps, HoistInputProps
|
|
|
146
146
|
/**
|
|
147
147
|
* Type of value to publish. Defaults to 'date'. The use of 'localDate' is often a good
|
|
148
148
|
* choice for use cases where there is no time component.
|
|
149
|
-
* @see LocalDate
|
|
149
|
+
* @see LocalDate
|
|
150
150
|
*/
|
|
151
151
|
valueType?: 'date' | 'localDate';
|
|
152
152
|
}
|
|
@@ -100,7 +100,7 @@ export class LeftRightChooserModel extends HoistModel {
|
|
|
100
100
|
* Note that this will *not* affect the actual 'value' property, which will continue
|
|
101
101
|
* to include unfiltered records.
|
|
102
102
|
*
|
|
103
|
-
* @see LeftRightChooserFilter
|
|
103
|
+
* @see LeftRightChooserFilter
|
|
104
104
|
* @param fn - predicate function for filtering.
|
|
105
105
|
*/
|
|
106
106
|
setDisplayFilter(fn: FilterTestFn) {
|
package/mcp/cli/ts.ts
CHANGED
|
@@ -6,7 +6,13 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import {Command} from 'commander';
|
|
8
8
|
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
searchSymbols,
|
|
11
|
+
searchMembers,
|
|
12
|
+
getSymbolDetail,
|
|
13
|
+
getMembers,
|
|
14
|
+
getCompanionSymbols
|
|
15
|
+
} from '../data/ts-registry.js';
|
|
10
16
|
import {formatSymbolSearch, formatSymbolDetail, formatMembers} from '../formatters/typescript.js';
|
|
11
17
|
|
|
12
18
|
const VALID_KINDS = ['class', 'interface', 'type', 'function', 'const', 'enum'] as const;
|
|
@@ -101,7 +107,8 @@ program
|
|
|
101
107
|
process.exit(1);
|
|
102
108
|
}
|
|
103
109
|
|
|
104
|
-
|
|
110
|
+
const companions = await getCompanionSymbols(detail);
|
|
111
|
+
let text = formatSymbolDetail(detail, name, companions);
|
|
105
112
|
if (detail.kind === 'class' || detail.kind === 'interface') {
|
|
106
113
|
text +=
|
|
107
114
|
'\n\nTip: Use `hoist-ts members ' + name + '` to see all properties and methods.';
|
package/mcp/data/ts-registry.ts
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* Detailed symbol info is extracted on-demand.
|
|
16
16
|
*/
|
|
17
17
|
import {Project, Node, Scope, SyntaxKind} from 'ts-morph';
|
|
18
|
-
import type {ClassDeclaration, SourceFile} from 'ts-morph';
|
|
18
|
+
import type {ClassDeclaration, FunctionDeclaration, SourceFile} from 'ts-morph';
|
|
19
19
|
import {resolve} from 'node:path';
|
|
20
20
|
|
|
21
21
|
import {log} from '../util/logger.js';
|
|
@@ -35,6 +35,8 @@ export interface SymbolEntry {
|
|
|
35
35
|
filePath: string;
|
|
36
36
|
isExported: boolean;
|
|
37
37
|
sourcePackage: string;
|
|
38
|
+
/** JSDoc, if available. Populated at index time; displayed in search results. */
|
|
39
|
+
jsDoc: string;
|
|
38
40
|
}
|
|
39
41
|
|
|
40
42
|
/** Detailed symbol information extracted on-demand. */
|
|
@@ -285,7 +287,8 @@ function buildSymbolIndex(proj: Project): {
|
|
|
285
287
|
kind: 'class',
|
|
286
288
|
filePath,
|
|
287
289
|
isExported: cls.isExported(),
|
|
288
|
-
sourcePackage: pkg
|
|
290
|
+
sourcePackage: pkg,
|
|
291
|
+
jsDoc: extractJsDoc(cls)
|
|
289
292
|
};
|
|
290
293
|
addToIndex(index, entry);
|
|
291
294
|
counts.total++;
|
|
@@ -308,7 +311,7 @@ function buildSymbolIndex(proj: Project): {
|
|
|
308
311
|
sourcePackage: pkg,
|
|
309
312
|
isStatic: m.isStatic,
|
|
310
313
|
type: m.kind === 'method' ? formatMethodType(m) : m.type,
|
|
311
|
-
jsDoc: m.jsDoc
|
|
314
|
+
jsDoc: m.jsDoc,
|
|
312
315
|
decorators: m.decorators
|
|
313
316
|
};
|
|
314
317
|
addToMemberIndex(mIndex, mEntry);
|
|
@@ -324,12 +327,15 @@ function buildSymbolIndex(proj: Project): {
|
|
|
324
327
|
for (const iface of sourceFile.getInterfaces()) {
|
|
325
328
|
const name = iface.getName();
|
|
326
329
|
if (!name) continue;
|
|
330
|
+
let jsDoc = extractJsDoc(iface);
|
|
331
|
+
if (!jsDoc) jsDoc = extractCompanionJsDoc(sourceFile, name);
|
|
327
332
|
const entry: SymbolEntry = {
|
|
328
333
|
name,
|
|
329
334
|
kind: 'interface',
|
|
330
335
|
filePath,
|
|
331
336
|
isExported: iface.isExported(),
|
|
332
|
-
sourcePackage: pkg
|
|
337
|
+
sourcePackage: pkg,
|
|
338
|
+
jsDoc
|
|
333
339
|
};
|
|
334
340
|
addToIndex(index, entry);
|
|
335
341
|
counts.total++;
|
|
@@ -346,7 +352,8 @@ function buildSymbolIndex(proj: Project): {
|
|
|
346
352
|
kind: 'type',
|
|
347
353
|
filePath,
|
|
348
354
|
isExported: typeAlias.isExported(),
|
|
349
|
-
sourcePackage: pkg
|
|
355
|
+
sourcePackage: pkg,
|
|
356
|
+
jsDoc: extractJsDoc(typeAlias)
|
|
350
357
|
};
|
|
351
358
|
addToIndex(index, entry);
|
|
352
359
|
counts.total++;
|
|
@@ -363,7 +370,8 @@ function buildSymbolIndex(proj: Project): {
|
|
|
363
370
|
kind: 'function',
|
|
364
371
|
filePath,
|
|
365
372
|
isExported: func.isExported(),
|
|
366
|
-
sourcePackage: pkg
|
|
373
|
+
sourcePackage: pkg,
|
|
374
|
+
jsDoc: extractFunctionJsDoc(func)
|
|
367
375
|
};
|
|
368
376
|
addToIndex(index, entry);
|
|
369
377
|
counts.total++;
|
|
@@ -380,7 +388,8 @@ function buildSymbolIndex(proj: Project): {
|
|
|
380
388
|
kind: 'enum',
|
|
381
389
|
filePath,
|
|
382
390
|
isExported: enumDecl.isExported(),
|
|
383
|
-
sourcePackage: pkg
|
|
391
|
+
sourcePackage: pkg,
|
|
392
|
+
jsDoc: extractJsDoc(enumDecl)
|
|
384
393
|
};
|
|
385
394
|
addToIndex(index, entry);
|
|
386
395
|
counts.total++;
|
|
@@ -391,6 +400,7 @@ function buildSymbolIndex(proj: Project): {
|
|
|
391
400
|
// Exported const variables
|
|
392
401
|
for (const stmt of sourceFile.getVariableStatements()) {
|
|
393
402
|
if (!stmt.isExported()) continue;
|
|
403
|
+
const stmtJsDoc = extractJsDoc(stmt);
|
|
394
404
|
for (const decl of stmt.getDeclarations()) {
|
|
395
405
|
// For destructured exports (e.g. `export const [Foo, foo] = ...`),
|
|
396
406
|
// index each binding element as a separate symbol so both the
|
|
@@ -420,7 +430,8 @@ function buildSymbolIndex(proj: Project): {
|
|
|
420
430
|
kind: 'const',
|
|
421
431
|
filePath,
|
|
422
432
|
isExported: true,
|
|
423
|
-
sourcePackage: pkg
|
|
433
|
+
sourcePackage: pkg,
|
|
434
|
+
jsDoc: stmtJsDoc
|
|
424
435
|
};
|
|
425
436
|
addToIndex(index, entry);
|
|
426
437
|
counts.total++;
|
|
@@ -508,7 +519,7 @@ function indexPromiseExtensions(
|
|
|
508
519
|
sourcePackage: pkg,
|
|
509
520
|
isStatic: false,
|
|
510
521
|
type: `(${paramStr}) => ${returnType}`,
|
|
511
|
-
jsDoc
|
|
522
|
+
jsDoc,
|
|
512
523
|
decorators: []
|
|
513
524
|
});
|
|
514
525
|
|
|
@@ -518,7 +529,8 @@ function indexPromiseExtensions(
|
|
|
518
529
|
kind: 'function',
|
|
519
530
|
filePath,
|
|
520
531
|
isExported: true,
|
|
521
|
-
sourcePackage: pkg
|
|
532
|
+
sourcePackage: pkg,
|
|
533
|
+
jsDoc
|
|
522
534
|
});
|
|
523
535
|
|
|
524
536
|
// Pre-compute detail for `getSymbolDetail()` since these can't be
|
|
@@ -703,6 +715,55 @@ export async function getSymbolDetail(
|
|
|
703
715
|
}
|
|
704
716
|
}
|
|
705
717
|
|
|
718
|
+
/**
|
|
719
|
+
* Find companion symbols for cross-referencing in detail output.
|
|
720
|
+
*
|
|
721
|
+
* For a Props interface (e.g. `PanelProps`), returns the companion component
|
|
722
|
+
* consts (`Panel`, `panel`) from the same file. For a component const, returns
|
|
723
|
+
* the companion Props interface. Returns an empty array if no companions found.
|
|
724
|
+
*/
|
|
725
|
+
export async function getCompanionSymbols(entry: SymbolEntry): Promise<SymbolEntry[]> {
|
|
726
|
+
await ensureInitialized();
|
|
727
|
+
|
|
728
|
+
if (entry.kind === 'interface' && entry.name.endsWith('Props')) {
|
|
729
|
+
// Props interface → look for companion component consts
|
|
730
|
+
const baseName = entry.name.slice(0, -5);
|
|
731
|
+
if (!baseName) return [];
|
|
732
|
+
return findCompanionEntries(baseName, entry.filePath);
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
if (entry.kind === 'const') {
|
|
736
|
+
// Component const → look for companion Props interface
|
|
737
|
+
const pascalName = entry.name[0].toUpperCase() + entry.name.slice(1) + 'Props';
|
|
738
|
+
const key = pascalName.toLowerCase();
|
|
739
|
+
const entries = symbolIndex!.get(key);
|
|
740
|
+
if (!entries) return [];
|
|
741
|
+
return entries.filter(
|
|
742
|
+
e => e.name === pascalName && e.kind === 'interface' && e.filePath === entry.filePath
|
|
743
|
+
);
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
return [];
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
/** Find companion const entries (PascalCase and camelCase) in the same file. */
|
|
750
|
+
function findCompanionEntries(baseName: string, filePath: string): SymbolEntry[] {
|
|
751
|
+
const results: SymbolEntry[] = [];
|
|
752
|
+
const camelName = baseName[0].toLowerCase() + baseName.slice(1);
|
|
753
|
+
|
|
754
|
+
for (const name of [baseName, camelName]) {
|
|
755
|
+
const key = name.toLowerCase();
|
|
756
|
+
const entries = symbolIndex!.get(key);
|
|
757
|
+
if (!entries) continue;
|
|
758
|
+
for (const e of entries) {
|
|
759
|
+
if (e.name === name && e.kind === 'const' && e.filePath === filePath) {
|
|
760
|
+
results.push(e);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
return results;
|
|
765
|
+
}
|
|
766
|
+
|
|
706
767
|
/**
|
|
707
768
|
* Get members (properties, methods, accessors) of a class or interface.
|
|
708
769
|
*
|
|
@@ -929,6 +990,45 @@ function extractJsDoc(node: {getJsDocs?: () => Array<{getDescription: () => stri
|
|
|
929
990
|
}
|
|
930
991
|
}
|
|
931
992
|
|
|
993
|
+
/**
|
|
994
|
+
* For a Props interface (e.g. `PanelProps`), look up the companion component's
|
|
995
|
+
* JSDoc from the same file. Uses the reliable `FooProps → Foo/foo` naming
|
|
996
|
+
* convention: strips the `Props` suffix and checks for a matching exported const.
|
|
997
|
+
*
|
|
998
|
+
* Returns the companion JSDoc string, or empty string if no match is found.
|
|
999
|
+
*/
|
|
1000
|
+
function extractCompanionJsDoc(sourceFile: SourceFile, propsName: string): string {
|
|
1001
|
+
if (!propsName.endsWith('Props')) return '';
|
|
1002
|
+
const companionName = propsName.slice(0, -5);
|
|
1003
|
+
if (!companionName) return '';
|
|
1004
|
+
|
|
1005
|
+
// Look for `const [Foo, foo] = ...` or `const foo = ...` in the same file
|
|
1006
|
+
const varDecl =
|
|
1007
|
+
sourceFile.getVariableDeclaration(companionName) ??
|
|
1008
|
+
sourceFile.getVariableDeclaration(companionName[0].toLowerCase() + companionName.slice(1));
|
|
1009
|
+
if (!varDecl) return '';
|
|
1010
|
+
|
|
1011
|
+
const varStmt = varDecl.getFirstAncestorByKind(SyntaxKind.VariableStatement);
|
|
1012
|
+
return varStmt ? extractJsDoc(varStmt) : '';
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
/**
|
|
1016
|
+
* Extract JSDoc from a function, falling back to the first overload signature if the
|
|
1017
|
+
* implementation has no JSDoc. TypeScript best practice places JSDoc on overload signatures
|
|
1018
|
+
* (which are visible to consumers) rather than the implementation (which is hidden).
|
|
1019
|
+
*/
|
|
1020
|
+
function extractFunctionJsDoc(func: FunctionDeclaration): string {
|
|
1021
|
+
const implDoc = extractJsDoc(func);
|
|
1022
|
+
if (implDoc) return implDoc;
|
|
1023
|
+
|
|
1024
|
+
const overloads = func.getOverloads();
|
|
1025
|
+
for (const overload of overloads) {
|
|
1026
|
+
const doc = extractJsDoc(overload);
|
|
1027
|
+
if (doc) return doc;
|
|
1028
|
+
}
|
|
1029
|
+
return '';
|
|
1030
|
+
}
|
|
1031
|
+
|
|
932
1032
|
/** Safely extract a type's text representation. */
|
|
933
1033
|
function safeGetTypeText(node: Node, enclosing?: Node): string {
|
|
934
1034
|
try {
|
|
@@ -996,10 +1096,14 @@ function extractInterfaceDetail(
|
|
|
996
1096
|
const braceIdx = text.indexOf('{');
|
|
997
1097
|
const signature = braceIdx === -1 ? text.trim() : text.slice(0, braceIdx).trim();
|
|
998
1098
|
|
|
1099
|
+
// For Props interfaces without their own JSDoc, inherit from the companion component
|
|
1100
|
+
let jsDoc = extractJsDoc(iface);
|
|
1101
|
+
if (!jsDoc) jsDoc = extractCompanionJsDoc(sourceFile, name);
|
|
1102
|
+
|
|
999
1103
|
return {
|
|
1000
1104
|
...base,
|
|
1001
1105
|
signature,
|
|
1002
|
-
jsDoc
|
|
1106
|
+
jsDoc,
|
|
1003
1107
|
...(extendsClauses.length > 0 ? {extends: extendsClauses.join(', ')} : {})
|
|
1004
1108
|
};
|
|
1005
1109
|
}
|
|
@@ -1056,7 +1160,7 @@ function extractFunctionDetail(
|
|
|
1056
1160
|
return {
|
|
1057
1161
|
...base,
|
|
1058
1162
|
signature,
|
|
1059
|
-
jsDoc:
|
|
1163
|
+
jsDoc: extractFunctionJsDoc(func)
|
|
1060
1164
|
};
|
|
1061
1165
|
}
|
|
1062
1166
|
|
|
@@ -7,6 +7,14 @@
|
|
|
7
7
|
import type {MemberInfo, MemberIndexEntry, SymbolEntry, SymbolDetail} from '../data/ts-registry.js';
|
|
8
8
|
import {resolveRepoRoot} from '../util/paths.js';
|
|
9
9
|
|
|
10
|
+
/** Remove blank lines from a JSDoc string to produce more compact output. */
|
|
11
|
+
function collapseJsDoc(jsDoc: string): string {
|
|
12
|
+
return jsDoc
|
|
13
|
+
.split('\n')
|
|
14
|
+
.filter(l => l.trim().length > 0)
|
|
15
|
+
.join('\n');
|
|
16
|
+
}
|
|
17
|
+
|
|
10
18
|
/** Maximum length for type strings before truncation. */
|
|
11
19
|
const MAX_TYPE_LENGTH = 200;
|
|
12
20
|
|
|
@@ -45,7 +53,11 @@ export function formatMember(member: MemberInfo): string {
|
|
|
45
53
|
}
|
|
46
54
|
|
|
47
55
|
if (member.jsDoc) {
|
|
48
|
-
|
|
56
|
+
const indented = collapseJsDoc(member.jsDoc)
|
|
57
|
+
.split('\n')
|
|
58
|
+
.map(l => ` ${l}`)
|
|
59
|
+
.join('\n');
|
|
60
|
+
lines.push(indented);
|
|
49
61
|
}
|
|
50
62
|
|
|
51
63
|
return lines.join('\n');
|
|
@@ -62,7 +74,11 @@ export function formatMemberIndexEntry(entry: MemberIndexEntry, index: number):
|
|
|
62
74
|
`${index}. [${entry.memberKind}] ${staticPrefix}${entry.name}: ${typeStr} (on ${entry.ownerName} \u2014 ${entry.ownerDescription})`
|
|
63
75
|
);
|
|
64
76
|
if (entry.jsDoc) {
|
|
65
|
-
|
|
77
|
+
const indented = collapseJsDoc(entry.jsDoc)
|
|
78
|
+
.split('\n')
|
|
79
|
+
.map(l => ` ${l}`)
|
|
80
|
+
.join('\n');
|
|
81
|
+
lines.push(indented);
|
|
66
82
|
}
|
|
67
83
|
return lines.join('\n');
|
|
68
84
|
}
|
|
@@ -81,6 +97,13 @@ export function formatSymbolSearch(
|
|
|
81
97
|
lines.push(
|
|
82
98
|
`${i + 1}. [${result.kind}] ${result.name} (package: ${result.sourcePackage}, file: ${toRelativePath(result.filePath)}, exported: ${result.isExported ? 'yes' : 'no'})`
|
|
83
99
|
);
|
|
100
|
+
if (result.jsDoc) {
|
|
101
|
+
const indented = collapseJsDoc(result.jsDoc)
|
|
102
|
+
.split('\n')
|
|
103
|
+
.map(l => ` ${l}`)
|
|
104
|
+
.join('\n');
|
|
105
|
+
lines.push(indented);
|
|
106
|
+
}
|
|
84
107
|
});
|
|
85
108
|
}
|
|
86
109
|
|
|
@@ -100,7 +123,11 @@ export function formatSymbolSearch(
|
|
|
100
123
|
}
|
|
101
124
|
|
|
102
125
|
/** Format detailed symbol information as a readable text block. */
|
|
103
|
-
export function formatSymbolDetail(
|
|
126
|
+
export function formatSymbolDetail(
|
|
127
|
+
detail: SymbolDetail | null,
|
|
128
|
+
name: string,
|
|
129
|
+
companionSymbols?: SymbolEntry[]
|
|
130
|
+
): string {
|
|
104
131
|
if (!detail) {
|
|
105
132
|
return `Symbol '${name}' not found. Use search to find available symbols.`;
|
|
106
133
|
}
|
|
@@ -132,7 +159,26 @@ export function formatSymbolDetail(detail: SymbolDetail | null, name: string): s
|
|
|
132
159
|
if (detail.jsDoc) {
|
|
133
160
|
lines.push('');
|
|
134
161
|
lines.push('## Documentation');
|
|
135
|
-
lines.push(detail.jsDoc);
|
|
162
|
+
lines.push(collapseJsDoc(detail.jsDoc));
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Cross-reference: link Props interfaces to their companion component and vice versa
|
|
166
|
+
if (companionSymbols && companionSymbols.length > 0) {
|
|
167
|
+
lines.push('');
|
|
168
|
+
const companionNames = companionSymbols.map(s => `\`${s.name}\``).join(', ');
|
|
169
|
+
if (detail.kind === 'interface' && detail.name.endsWith('Props')) {
|
|
170
|
+
lines.push(`## Component`);
|
|
171
|
+
lines.push(
|
|
172
|
+
`This is the Props interface for ${companionNames}. ` +
|
|
173
|
+
`Use hoist-get-members on ${detail.name} to see all available props.`
|
|
174
|
+
);
|
|
175
|
+
} else {
|
|
176
|
+
const propsName = companionSymbols[0].name;
|
|
177
|
+
lines.push(`## Props`);
|
|
178
|
+
lines.push(
|
|
179
|
+
`Accepts \`${propsName}\` — use hoist-get-members on ${propsName} to see all available props.`
|
|
180
|
+
);
|
|
181
|
+
}
|
|
136
182
|
}
|
|
137
183
|
|
|
138
184
|
return lines.join('\n');
|
package/mcp/tools/typescript.ts
CHANGED
|
@@ -8,7 +8,13 @@
|
|
|
8
8
|
import type {McpServer} from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
9
9
|
import {z} from 'zod';
|
|
10
10
|
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
searchSymbols,
|
|
13
|
+
searchMembers,
|
|
14
|
+
getSymbolDetail,
|
|
15
|
+
getMembers,
|
|
16
|
+
getCompanionSymbols
|
|
17
|
+
} from '../data/ts-registry.js';
|
|
12
18
|
import {formatSymbolSearch, formatSymbolDetail, formatMembers} from '../formatters/typescript.js';
|
|
13
19
|
|
|
14
20
|
/**
|
|
@@ -100,7 +106,8 @@ export function registerTsTools(server: McpServer): void {
|
|
|
100
106
|
},
|
|
101
107
|
async ({name, filePath}) => {
|
|
102
108
|
const detail = await getSymbolDetail(name, filePath);
|
|
103
|
-
|
|
109
|
+
const companions = detail ? await getCompanionSymbols(detail) : [];
|
|
110
|
+
let text = formatSymbolDetail(detail, name, companions);
|
|
104
111
|
|
|
105
112
|
if (detail && (detail.kind === 'class' || detail.kind === 'interface')) {
|
|
106
113
|
text += '\n\nUse hoist-get-members to see all properties and methods.';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xh/hoist",
|
|
3
|
-
"version": "83.0
|
|
3
|
+
"version": "83.1.0",
|
|
4
4
|
"description": "Hoist add-on for building and deploying React Applications.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -108,7 +108,7 @@
|
|
|
108
108
|
"devDependencies": {
|
|
109
109
|
"@types/react": "18.x",
|
|
110
110
|
"@types/react-dom": "18.x",
|
|
111
|
-
"@xh/hoist-dev-utils": "
|
|
111
|
+
"@xh/hoist-dev-utils": "12.x",
|
|
112
112
|
"ag-grid-community": "34.x",
|
|
113
113
|
"ag-grid-react": "34.x",
|
|
114
114
|
"csstype": "3.x",
|
|
@@ -24,7 +24,7 @@ import {withDefault} from '@xh/hoist/utils/js';
|
|
|
24
24
|
* to customize via the global options dialog, or set a default pref value if per-user
|
|
25
25
|
* customization is not desirable.
|
|
26
26
|
*
|
|
27
|
-
* @see RefreshContextModel
|
|
27
|
+
* @see RefreshContextModel
|
|
28
28
|
*/
|
|
29
29
|
export class AutoRefreshService extends HoistService {
|
|
30
30
|
override xhImpl = true;
|
package/svc/ChangelogService.ts
CHANGED
|
@@ -26,9 +26,8 @@ import {isEmpty, forOwn, includes} from 'lodash';
|
|
|
26
26
|
*
|
|
27
27
|
* Several additional options can be controlled via soft-config - see below.
|
|
28
28
|
*
|
|
29
|
-
* @see XH.showChangelog
|
|
30
|
-
* @see whatsNewButton
|
|
31
|
-
* the currently deployed app version. Installed by default in desktop appBar.
|
|
29
|
+
* @see XH.showChangelog
|
|
30
|
+
* @see whatsNewButton
|
|
32
31
|
*/
|
|
33
32
|
export class ChangelogService extends HoistService {
|
|
34
33
|
override xhImpl = true;
|
|
@@ -162,16 +162,18 @@ export class EnvironmentService extends HoistService {
|
|
|
162
162
|
|
|
163
163
|
private ensureVersionRunnable() {
|
|
164
164
|
const hcVersion = this.get('hoistCoreVersion'),
|
|
165
|
-
|
|
166
|
-
|
|
165
|
+
// This app version value is sourced by the network call to 'xh/environment'.
|
|
166
|
+
serverAppVersion = this.get('appVersion'),
|
|
167
|
+
// This app version value is packaged from configureWebpack -> appVersion.
|
|
168
|
+
clientAppVersion = this.get('clientVersion');
|
|
167
169
|
|
|
168
170
|
// Check for client/server mismatch version. It's an ok transitory state *during* the
|
|
169
171
|
// client app lifetime, but app should *never* start this way, would indicate caching issue.
|
|
170
172
|
throwIf(
|
|
171
|
-
|
|
173
|
+
clientAppVersion != serverAppVersion,
|
|
172
174
|
XH.exception(
|
|
173
|
-
`The version of this client (${
|
|
174
|
-
available server (${
|
|
175
|
+
`The version of this client (${clientAppVersion}) is out of sync with the
|
|
176
|
+
available server (${serverAppVersion}). Please reload to continue.`
|
|
175
177
|
)
|
|
176
178
|
);
|
|
177
179
|
|