@principal-ai/principal-view-react 0.16.45 → 0.16.47
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/dist/subsystem/ComponentDeclaration.d.ts +3 -1
- package/dist/subsystem/ComponentDeclaration.d.ts.map +1 -1
- package/dist/subsystem/ComponentDeclaration.js +77 -74
- package/dist/subsystem/ComponentDeclaration.js.map +1 -1
- package/dist/subsystem/formatDeclaration.d.ts +11 -0
- package/dist/subsystem/formatDeclaration.d.ts.map +1 -0
- package/dist/subsystem/formatDeclaration.js +96 -0
- package/dist/subsystem/formatDeclaration.js.map +1 -0
- package/dist/subsystem/model.d.ts +11 -0
- package/dist/subsystem/model.d.ts.map +1 -1
- package/dist/subsystem/model.js.map +1 -1
- package/dist/subsystem/tokenizeComponent.d.ts +19 -0
- package/dist/subsystem/tokenizeComponent.d.ts.map +1 -0
- package/dist/subsystem/tokenizeComponent.js +61 -0
- package/dist/subsystem/tokenizeComponent.js.map +1 -0
- package/dist/subsystem/tokenizeFormatted.d.ts +15 -0
- package/dist/subsystem/tokenizeFormatted.d.ts.map +1 -0
- package/dist/subsystem/tokenizeFormatted.js +84 -0
- package/dist/subsystem/tokenizeFormatted.js.map +1 -0
- package/package.json +3 -2
- package/src/stories/ComponentDeclarationAudit.stories.tsx +290 -0
- package/src/subsystem/ComponentDeclaration.tsx +90 -268
- package/src/subsystem/formatDeclaration.ts +116 -0
- package/src/subsystem/model.ts +24 -0
- package/src/subsystem/tokenizeComponent.ts +70 -0
- package/src/subsystem/tokenizeFormatted.ts +92 -0
|
@@ -6,15 +6,16 @@
|
|
|
6
6
|
* comments. File content lives in the bottom FileDrawer, not here.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import {
|
|
9
|
+
import { useCallback, useEffect, useRef, useState, type ReactNode } from 'react';
|
|
10
10
|
import { AlignLeft, FileText } from 'lucide-react';
|
|
11
11
|
import { useTheme } from '@principal-ade/industry-theme';
|
|
12
12
|
import {
|
|
13
13
|
KIND_COLOR,
|
|
14
|
-
deriveNameFromSymbol,
|
|
15
14
|
formatPurl,
|
|
16
15
|
type SubsystemComponent,
|
|
16
|
+
type SubsystemDeclTokenKind,
|
|
17
17
|
} from './model';
|
|
18
|
+
import { tokenizeComponent } from './tokenizeComponent';
|
|
18
19
|
|
|
19
20
|
/** Shared affordance for clickable text pieces. */
|
|
20
21
|
const clickableStyle = {
|
|
@@ -54,10 +55,12 @@ export interface ComponentDeclarationProps {
|
|
|
54
55
|
/** Related-name click (types, callers/callees, implementors, …) → select
|
|
55
56
|
* that component if one matches; unmatched refs no-op. */
|
|
56
57
|
onRelatedSelect?: (ref: string) => void;
|
|
58
|
+
/** Max width of the declaration panel (CSS value). Defaults to none. */
|
|
59
|
+
maxWidth?: string | number;
|
|
57
60
|
}
|
|
58
61
|
|
|
59
62
|
/** Declaration panel for the selected component — its definition, code-style. */
|
|
60
|
-
export function ComponentDeclaration({ component, onOpenFile, onRelatedSelect }: ComponentDeclarationProps) {
|
|
63
|
+
export function ComponentDeclaration({ component, onOpenFile, onRelatedSelect: _onRelatedSelect, maxWidth }: ComponentDeclarationProps) {
|
|
61
64
|
const { theme } = useTheme();
|
|
62
65
|
const [fileHovered, setFileHovered] = useState(false);
|
|
63
66
|
// Header visibility toggles — off by default so the panel opens as pure
|
|
@@ -66,26 +69,26 @@ export function ComponentDeclaration({ component, onOpenFile, onRelatedSelect }:
|
|
|
66
69
|
const [showPurpose, setShowPurpose] = useState(false);
|
|
67
70
|
const muted = theme.colors.textMuted ?? theme.colors.textSecondary;
|
|
68
71
|
const color = KIND_COLOR[component.kind] ?? '#888';
|
|
69
|
-
const displayName = deriveNameFromSymbol(component.symbol, component.kind, component.name, component.file);
|
|
70
72
|
|
|
71
|
-
// Token
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
const str = (text: string) => <span style={{ color: theme.colors.success }}>{text}</span>;
|
|
82
|
-
const pn = (text: string) => <span style={{ color: muted }}>{text}</span>;
|
|
73
|
+
// Token-kind → theme-color map (used by the token iterator below).
|
|
74
|
+
const tokenColor: Record<SubsystemDeclTokenKind, string> = {
|
|
75
|
+
keyword: theme.colors.secondary,
|
|
76
|
+
name: color,
|
|
77
|
+
member: theme.colors.info,
|
|
78
|
+
type: theme.colors.accent,
|
|
79
|
+
punctuation: muted,
|
|
80
|
+
string: theme.colors.success,
|
|
81
|
+
newline: '', // not rendered — drives line breaks
|
|
82
|
+
};
|
|
83
83
|
|
|
84
|
-
const line = (children: ReactNode, key?: string, indent?: boolean) =>
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
84
|
+
const line = (children: ReactNode, key?: string, indent?: boolean | number) => {
|
|
85
|
+
const paddingLeft = typeof indent === 'number' ? indent : indent ? 16 : 0;
|
|
86
|
+
return (
|
|
87
|
+
<div key={key} style={{ display: 'flex', alignItems: 'center', whiteSpace: 'pre', minHeight: 18, ...(paddingLeft ? { paddingLeft } : {}) }}>
|
|
88
|
+
{children}
|
|
89
|
+
</div>
|
|
90
|
+
);
|
|
91
|
+
};
|
|
89
92
|
const commentStyle = { color: muted, fontStyle: 'italic' } as const;
|
|
90
93
|
const commentLine = (text: string, key?: string, indent?: boolean, onClick?: () => void) =>
|
|
91
94
|
line(
|
|
@@ -109,15 +112,6 @@ export function ComponentDeclaration({ component, onOpenFile, onRelatedSelect }:
|
|
|
109
112
|
// `// implemented by:`, …) are deliberately not rendered — the graph's
|
|
110
113
|
// edges carry interactions, and duplicated name lists read as clutter. The
|
|
111
114
|
// data stays on the Graphify detail types for other surfaces.
|
|
112
|
-
// Comma-join rendered tokens without raw-array key churn.
|
|
113
|
-
const list = (items: ReactNode[]) =>
|
|
114
|
-
items.map((item, i) => (
|
|
115
|
-
<Fragment key={i}>
|
|
116
|
-
{i > 0 ? ', ' : null}
|
|
117
|
-
{item}
|
|
118
|
-
</Fragment>
|
|
119
|
-
));
|
|
120
|
-
|
|
121
115
|
const lines: ReactNode[] = [];
|
|
122
116
|
// Repo identity — owner avatar + name for GitHub-hosted purls; anything
|
|
123
117
|
// else falls back to the formatted purl as a quiet comment. The avatar
|
|
@@ -207,246 +201,71 @@ export function ComponentDeclaration({ component, onOpenFile, onRelatedSelect }:
|
|
|
207
201
|
),
|
|
208
202
|
);
|
|
209
203
|
|
|
210
|
-
|
|
211
|
-
//
|
|
212
|
-
//
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
(m.parameters ?? []).map((p, j) => (
|
|
228
|
-
<Fragment key={j}>
|
|
229
|
-
{p.name ? (
|
|
230
|
-
<>
|
|
231
|
-
{mb(p.name)}
|
|
232
|
-
{pn(': ')}
|
|
233
|
-
</>
|
|
234
|
-
) : null}
|
|
235
|
-
{ty(p.type)}
|
|
236
|
-
</Fragment>
|
|
237
|
-
)),
|
|
238
|
-
)}
|
|
239
|
-
{pn(')')}
|
|
240
|
-
{m.returnType ? (
|
|
241
|
-
<>
|
|
242
|
-
{pn(': ')}
|
|
243
|
-
{ty(m.returnType)}
|
|
244
|
-
</>
|
|
245
|
-
) : null}
|
|
246
|
-
</>,
|
|
247
|
-
`m${i}`,
|
|
248
|
-
true,
|
|
249
|
-
),
|
|
250
|
-
),
|
|
251
|
-
);
|
|
252
|
-
(detail.properties ?? []).forEach((prop, i) =>
|
|
253
|
-
members.push(
|
|
254
|
-
line(
|
|
255
|
-
<>
|
|
256
|
-
{mb(prop.name)}
|
|
257
|
-
{prop.type ? (
|
|
258
|
-
<>
|
|
259
|
-
{pn(': ')}
|
|
260
|
-
{ty(prop.type)}
|
|
261
|
-
</>
|
|
262
|
-
) : null}
|
|
263
|
-
</>,
|
|
264
|
-
`p${i}`,
|
|
265
|
-
true,
|
|
266
|
-
),
|
|
267
|
-
),
|
|
268
|
-
);
|
|
269
|
-
}
|
|
270
|
-
const cls = detail?.kind === 'class' ? detail : undefined;
|
|
271
|
-
const hasBody = members.length > 0;
|
|
272
|
-
lines.push(
|
|
273
|
-
line(
|
|
274
|
-
<>
|
|
275
|
-
{kw('class')}
|
|
276
|
-
{' '}
|
|
277
|
-
{nm(displayName)}
|
|
278
|
-
{cls && (cls.extends ?? []).length > 0 && (
|
|
279
|
-
<>
|
|
280
|
-
{' '}
|
|
281
|
-
{kw('extends')}
|
|
282
|
-
{' '}
|
|
283
|
-
{list((cls.extends ?? []).map((e, i) => <Fragment key={i}>{ty(e)}</Fragment>))}
|
|
284
|
-
</>
|
|
285
|
-
)}
|
|
286
|
-
{cls && (cls.implements ?? []).length > 0 && (
|
|
287
|
-
<>
|
|
288
|
-
{' '}
|
|
289
|
-
{kw('implements')}
|
|
290
|
-
{' '}
|
|
291
|
-
{list((cls.implements ?? []).map((e, i) => <Fragment key={i}>{ty(e)}</Fragment>))}
|
|
292
|
-
</>
|
|
293
|
-
)}
|
|
294
|
-
{hasBody ? pn(' {') : null}
|
|
295
|
-
</>,
|
|
296
|
-
'decl',
|
|
297
|
-
),
|
|
298
|
-
);
|
|
299
|
-
lines.push(...members);
|
|
300
|
-
if (hasBody) lines.push(line(pn('}'), 'end'));
|
|
301
|
-
break;
|
|
302
|
-
}
|
|
303
|
-
case 'function': {
|
|
304
|
-
const fn = detail?.kind === 'function' ? detail : undefined;
|
|
305
|
-
const params = fn?.parameters ?? [];
|
|
306
|
-
lines.push(
|
|
307
|
-
line(
|
|
308
|
-
<>
|
|
309
|
-
{kw('function')}
|
|
310
|
-
{' '}
|
|
311
|
-
{nm(displayName)}
|
|
312
|
-
{pn('(')}
|
|
313
|
-
{params.length > 0 &&
|
|
314
|
-
list(
|
|
315
|
-
params.map((param, i) => (
|
|
316
|
-
<Fragment key={i}>
|
|
317
|
-
{param.name ? (
|
|
318
|
-
<>
|
|
319
|
-
{mb(param.name)}
|
|
320
|
-
{pn(': ')}
|
|
321
|
-
</>
|
|
322
|
-
) : null}
|
|
323
|
-
{ty(param.type)}
|
|
324
|
-
</Fragment>
|
|
325
|
-
)),
|
|
326
|
-
)}
|
|
327
|
-
{pn(')')}
|
|
328
|
-
{fn?.returnType ? (
|
|
329
|
-
<>
|
|
330
|
-
{pn(': ')}
|
|
331
|
-
{ty(fn.returnType)}
|
|
332
|
-
</>
|
|
333
|
-
) : null}
|
|
334
|
-
{pn(';')}
|
|
335
|
-
</>,
|
|
336
|
-
'decl',
|
|
337
|
-
),
|
|
338
|
-
);
|
|
339
|
-
// Caller/callee relationship comments (`// called by:` / `// calls:`)
|
|
340
|
-
// are deliberately not rendered here — the graph's edges carry
|
|
341
|
-
// interactions, and duplicated call lists read as clutter. The data
|
|
342
|
-
// stays on `GraphifyFunctionDetail` for other surfaces.
|
|
343
|
-
break;
|
|
344
|
-
}
|
|
345
|
-
case 'type': {
|
|
346
|
-
const tpe = detail?.kind === 'type' ? detail : undefined;
|
|
347
|
-
const hasBody = ((tpe?.properties ?? []).length) > 0;
|
|
348
|
-
lines.push(
|
|
349
|
-
line(
|
|
350
|
-
<>
|
|
351
|
-
{kw('interface')}
|
|
352
|
-
{' '}
|
|
353
|
-
{nm(displayName)}
|
|
354
|
-
{hasBody ? pn(' {') : null}
|
|
355
|
-
</>,
|
|
356
|
-
'decl',
|
|
357
|
-
),
|
|
358
|
-
);
|
|
359
|
-
if (tpe) {
|
|
360
|
-
(tpe.properties ?? []).forEach((prop, i) =>
|
|
361
|
-
lines.push(
|
|
362
|
-
line(
|
|
363
|
-
<>
|
|
364
|
-
{mb(prop.name)}
|
|
365
|
-
{prop.type ? (
|
|
366
|
-
<>
|
|
367
|
-
{pn(': ')}
|
|
368
|
-
{ty(prop.type)}
|
|
369
|
-
</>
|
|
370
|
-
) : null}
|
|
371
|
-
{pn(';')}
|
|
372
|
-
</>,
|
|
373
|
-
`p${i}`,
|
|
374
|
-
true,
|
|
375
|
-
),
|
|
376
|
-
),
|
|
377
|
-
);
|
|
378
|
-
}
|
|
379
|
-
if (hasBody) lines.push(line(pn('}'), 'end'));
|
|
380
|
-
break;
|
|
204
|
+
// --- Container width measurement --------------------------------------
|
|
205
|
+
// Approximate character count from the container's clientWidth so Prettier
|
|
206
|
+
// wraps at the same column the code block will actually use.
|
|
207
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
208
|
+
const [printWidth, setPrintWidth] = useState(80);
|
|
209
|
+
|
|
210
|
+
const measure = useCallback(() => {
|
|
211
|
+
const el = containerRef.current;
|
|
212
|
+
if (!el) return;
|
|
213
|
+
const ch = el.querySelector('[data-declaration-line]');
|
|
214
|
+
if (ch) {
|
|
215
|
+
// Use the width of a single `ch` unit from the monospace container.
|
|
216
|
+
const charWidth = ch.getBoundingClientRect().width || 8.4;
|
|
217
|
+
setPrintWidth(Math.max(40, Math.floor(el.clientWidth / charWidth)));
|
|
218
|
+
} else {
|
|
219
|
+
// Fallback: assume ~8.4 px per char at 14px monospace.
|
|
220
|
+
setPrintWidth(Math.max(40, Math.floor(el.clientWidth / 8.4)));
|
|
381
221
|
}
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
line(
|
|
402
|
-
<>
|
|
403
|
-
{kw('export')}
|
|
404
|
-
{' '}
|
|
405
|
-
{pn('{ ')}
|
|
406
|
-
{list((mod.exports ?? []).map((e, i) => <Fragment key={i}>{mb(e)}</Fragment>))}
|
|
407
|
-
{pn(' }')}
|
|
408
|
-
{pn(';')}
|
|
409
|
-
</>,
|
|
410
|
-
'exports',
|
|
411
|
-
),
|
|
412
|
-
);
|
|
413
|
-
if ((mod.symbols ?? []).length > 0)
|
|
414
|
-
lines.push(commentLine(`// defines: ${(mod.symbols ?? []).join(', ')}`, 'symbols'));
|
|
415
|
-
} else {
|
|
416
|
-
lines.push(
|
|
417
|
-
line(
|
|
418
|
-
<>
|
|
419
|
-
{kw('module')}
|
|
420
|
-
{' '}
|
|
421
|
-
{nm(displayName)}
|
|
422
|
-
</>,
|
|
423
|
-
'decl',
|
|
424
|
-
),
|
|
425
|
-
);
|
|
426
|
-
}
|
|
427
|
-
break;
|
|
222
|
+
}, []);
|
|
223
|
+
|
|
224
|
+
useEffect(() => {
|
|
225
|
+
measure();
|
|
226
|
+
const el = containerRef.current;
|
|
227
|
+
if (!el) return;
|
|
228
|
+
const ro = new ResizeObserver(measure);
|
|
229
|
+
ro.observe(el);
|
|
230
|
+
return () => ro.disconnect();
|
|
231
|
+
}, [measure]);
|
|
232
|
+
|
|
233
|
+
// --- Declaration tokens ------------------------------------------------
|
|
234
|
+
// Tokenize asynchronously via Prettier + highlight.js. Pre-tokenized
|
|
235
|
+
// tokens from the wire skip the pipeline entirely.
|
|
236
|
+
const [tokens, setTokens] = useState(component.tokens ?? []);
|
|
237
|
+
useEffect(() => {
|
|
238
|
+
if (component.tokens) {
|
|
239
|
+
setTokens(component.tokens);
|
|
240
|
+
return;
|
|
428
241
|
}
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
nm(displayName)
|
|
443
|
-
)}
|
|
444
|
-
</>,
|
|
445
|
-
'decl',
|
|
446
|
-
),
|
|
447
|
-
);
|
|
448
|
-
break;
|
|
242
|
+
let cancelled = false;
|
|
243
|
+
tokenizeComponent(component, printWidth).then((t) => {
|
|
244
|
+
if (!cancelled) setTokens(t);
|
|
245
|
+
});
|
|
246
|
+
return () => { cancelled = true; };
|
|
247
|
+
}, [component, printWidth]);
|
|
248
|
+
const declLines: ReactNode[][] = [[]];
|
|
249
|
+
let di = 0;
|
|
250
|
+
for (const tok of tokens) {
|
|
251
|
+
if (tok.kind === 'newline') {
|
|
252
|
+
declLines.push([]);
|
|
253
|
+
di++;
|
|
254
|
+
continue;
|
|
449
255
|
}
|
|
256
|
+
declLines[di].push(
|
|
257
|
+
<span key={`${di}-${declLines[di].length}`} style={{ color: tokenColor[tok.kind] }}>
|
|
258
|
+
{tok.text}
|
|
259
|
+
</span>,
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
for (let li = 0; li < declLines.length; li++) {
|
|
263
|
+
if (declLines[li].length === 0) continue;
|
|
264
|
+
lines.push(
|
|
265
|
+
<div key={`decl-${li}`} data-declaration-line style={{ display: 'flex', alignItems: 'center', whiteSpace: 'pre', minHeight: 18 }}>
|
|
266
|
+
{declLines[li]}
|
|
267
|
+
</div>,
|
|
268
|
+
);
|
|
450
269
|
}
|
|
451
270
|
|
|
452
271
|
// Purpose prose follows the declaration — identity and syntax first, the
|
|
@@ -468,13 +287,16 @@ export function ComponentDeclaration({ component, onOpenFile, onRelatedSelect }:
|
|
|
468
287
|
|
|
469
288
|
return (
|
|
470
289
|
<div
|
|
290
|
+
ref={containerRef}
|
|
471
291
|
style={{
|
|
472
292
|
background: theme.colors.backgroundSecondary,
|
|
473
293
|
padding: '10px 12px',
|
|
474
294
|
fontFamily: theme.fonts.monospace,
|
|
475
295
|
fontSize: theme.fontSizes[1],
|
|
476
296
|
lineHeight: 1.7,
|
|
477
|
-
|
|
297
|
+
...(maxWidth != null
|
|
298
|
+
? { maxWidth, wordBreak: 'break-word' }
|
|
299
|
+
: { overflowX: 'auto' }),
|
|
478
300
|
}}
|
|
479
301
|
>
|
|
480
302
|
{lines}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generate a TypeScript declaration string from a SubsystemComponent's detail.
|
|
3
|
+
*
|
|
4
|
+
* This is the "source code" that Prettier will format. The output is valid
|
|
5
|
+
* TypeScript (except for `external`, which is handled separately). The string
|
|
6
|
+
* is intentionally simple — no indentation, no line breaks — because Prettier
|
|
7
|
+
* handles all formatting.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { SubsystemComponent } from './model';
|
|
11
|
+
import type { GraphifyComponentDetail } from '../graphify';
|
|
12
|
+
|
|
13
|
+
export function generateDeclarationString(component: SubsystemComponent): string {
|
|
14
|
+
const detail = component.detail;
|
|
15
|
+
const kind = detail?.kind ?? component.kind;
|
|
16
|
+
const name = component.symbol || component.name || 'untitled';
|
|
17
|
+
|
|
18
|
+
switch (kind) {
|
|
19
|
+
case 'class':
|
|
20
|
+
return generateClass(name, detail);
|
|
21
|
+
case 'function':
|
|
22
|
+
return generateFunction(name, detail);
|
|
23
|
+
case 'type':
|
|
24
|
+
return generateType(name, detail);
|
|
25
|
+
case 'module':
|
|
26
|
+
return generateModule(detail);
|
|
27
|
+
case 'external':
|
|
28
|
+
// Not valid TypeScript — caller should handle formatting.
|
|
29
|
+
return `external '${detail?.kind === 'external' ? detail.label : name}'`;
|
|
30
|
+
default:
|
|
31
|
+
return `${kind} ${name}`;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
// Helpers
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
/** Format parameters, synthesising names for unnamed positionals. */
|
|
40
|
+
function formatParams(params: { name?: string; type: string }[]): string {
|
|
41
|
+
return params
|
|
42
|
+
.map((p, i) => (p.name ? `${p.name}: ${p.type}` : `arg${i}: ${p.type}`))
|
|
43
|
+
.join(', ');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
// Per-kind generators
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
function generateClass(name: string, detail?: GraphifyComponentDetail): string {
|
|
51
|
+
const cls = detail?.kind === 'class' ? detail : undefined;
|
|
52
|
+
const parts: string[] = [`class ${name}`];
|
|
53
|
+
|
|
54
|
+
if (cls?.extends && cls.extends.length > 0) {
|
|
55
|
+
parts.push(`extends ${cls.extends.join(', ')}`);
|
|
56
|
+
}
|
|
57
|
+
if (cls?.implements && cls.implements.length > 0) {
|
|
58
|
+
parts.push(`implements ${cls.implements.join(', ')}`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const members: string[] = [];
|
|
62
|
+
|
|
63
|
+
for (const m of cls?.methods ?? []) {
|
|
64
|
+
const ret = m.returnType ? `: ${m.returnType}` : '';
|
|
65
|
+
members.push(` ${m.name}(${formatParams(m.parameters ?? [])})${ret};`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
for (const prop of cls?.properties ?? []) {
|
|
69
|
+
const t = prop.type ? `: ${prop.type}` : '';
|
|
70
|
+
members.push(` ${prop.name}${t};`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (members.length > 0) {
|
|
74
|
+
parts.push(`{\n${members.join('\n')}\n}`);
|
|
75
|
+
} else {
|
|
76
|
+
parts.push('{}');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return parts.join(' ');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function generateFunction(name: string, detail?: GraphifyComponentDetail): string {
|
|
83
|
+
const fn = detail?.kind === 'function' ? detail : undefined;
|
|
84
|
+
const params = formatParams(fn?.parameters ?? []);
|
|
85
|
+
const ret = fn?.returnType ? `: ${fn.returnType}` : '';
|
|
86
|
+
return `function ${name}(${params})${ret};`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function generateType(name: string, detail?: GraphifyComponentDetail): string {
|
|
90
|
+
const tpe = detail?.kind === 'type' ? detail : undefined;
|
|
91
|
+
const props = (tpe?.properties ?? [])
|
|
92
|
+
.map((p) => ` ${p.name}${p.type ? `: ${p.type}` : ''};`)
|
|
93
|
+
.join('\n');
|
|
94
|
+
|
|
95
|
+
if (props) {
|
|
96
|
+
return `interface ${name} {\n${props}\n}`;
|
|
97
|
+
}
|
|
98
|
+
return `interface ${name} {}`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function generateModule(detail?: GraphifyComponentDetail): string {
|
|
102
|
+
const mod = detail?.kind === 'module' ? detail : undefined;
|
|
103
|
+
if (!mod) return 'module {}';
|
|
104
|
+
|
|
105
|
+
const parts: string[] = [];
|
|
106
|
+
|
|
107
|
+
for (const imp of mod.imports ?? []) {
|
|
108
|
+
parts.push(`import '${imp.name}';`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if ((mod.exports ?? []).length > 0) {
|
|
112
|
+
parts.push(`export { ${mod.exports!.join(', ')} };`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return parts.join('\n') || `module {}`;
|
|
116
|
+
}
|
package/src/subsystem/model.ts
CHANGED
|
@@ -27,6 +27,24 @@ export type SubsystemComponentKind =
|
|
|
27
27
|
| 'module'
|
|
28
28
|
| 'external';
|
|
29
29
|
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
// Declaration tokens — structured source representation
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
export type SubsystemDeclTokenKind =
|
|
35
|
+
| 'keyword'
|
|
36
|
+
| 'name'
|
|
37
|
+
| 'member'
|
|
38
|
+
| 'type'
|
|
39
|
+
| 'punctuation'
|
|
40
|
+
| 'string'
|
|
41
|
+
| 'newline';
|
|
42
|
+
|
|
43
|
+
export interface SubsystemDeclToken {
|
|
44
|
+
text: string;
|
|
45
|
+
kind: SubsystemDeclTokenKind;
|
|
46
|
+
}
|
|
47
|
+
|
|
30
48
|
export type SubsystemEdgeMechanism =
|
|
31
49
|
| 'imports'
|
|
32
50
|
| 'imports_from'
|
|
@@ -87,6 +105,12 @@ export interface SubsystemComponent {
|
|
|
87
105
|
* may claim `verified`.
|
|
88
106
|
*/
|
|
89
107
|
detailProvenance?: 'verified' | 'authored';
|
|
108
|
+
/**
|
|
109
|
+
* Pre-tokenized declaration for the detail panel. When present, the
|
|
110
|
+
* renderer skips client-side tokenization. Tokens are language-agnostic;
|
|
111
|
+
* a different language just needs a different tokenizer and text joiner.
|
|
112
|
+
*/
|
|
113
|
+
tokens?: SubsystemDeclToken[];
|
|
90
114
|
}
|
|
91
115
|
|
|
92
116
|
/** A cross-component edge in the subsystem graph. */
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tokenize a SubsystemComponent into a flat token stream for the detail panel.
|
|
3
|
+
*
|
|
4
|
+
* Pipeline: generate declaration string → format with Prettier → tokenize
|
|
5
|
+
* with highlight.js. The `component.tokens` field, when present, overrides
|
|
6
|
+
* the entire pipeline (for pre-tokenized data from graphify).
|
|
7
|
+
*
|
|
8
|
+
* This function is async because Prettier's format() is async.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { SubsystemComponent, SubsystemDeclToken } from './model';
|
|
12
|
+
import { generateDeclarationString } from './formatDeclaration';
|
|
13
|
+
import { tokenizeFormatted } from './tokenizeFormatted';
|
|
14
|
+
|
|
15
|
+
// Lazy-loaded Prettier to avoid startup cost.
|
|
16
|
+
let prettierPromise: Promise<typeof import('prettier/standalone')> | null = null;
|
|
17
|
+
let prettierPluginsPromise: Promise<{
|
|
18
|
+
typescript: typeof import('prettier/plugins/typescript');
|
|
19
|
+
estree: typeof import('prettier/plugins/estree');
|
|
20
|
+
}> | null = null;
|
|
21
|
+
|
|
22
|
+
async function getPrettier() {
|
|
23
|
+
if (!prettierPromise) {
|
|
24
|
+
prettierPromise = import('prettier/standalone');
|
|
25
|
+
}
|
|
26
|
+
if (!prettierPluginsPromise) {
|
|
27
|
+
prettierPluginsPromise = Promise.all([
|
|
28
|
+
import('prettier/plugins/typescript'),
|
|
29
|
+
import('prettier/plugins/estree'),
|
|
30
|
+
]).then(([typescript, estree]) => ({ typescript, estree }));
|
|
31
|
+
}
|
|
32
|
+
const [prettier, plugins] = await Promise.all([prettierPromise, prettierPluginsPromise]);
|
|
33
|
+
return { prettier, plugins };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Tokenize a SubsystemComponent into SubsystemDeclToken[].
|
|
38
|
+
*
|
|
39
|
+
* When `component.tokens` is present (pre-tokenized data from the wire),
|
|
40
|
+
* it's returned as-is. Otherwise the pipeline generates a TypeScript
|
|
41
|
+
* declaration string, formats it with Prettier, and tokenizes the output.
|
|
42
|
+
*/
|
|
43
|
+
export async function tokenizeComponent(component: SubsystemComponent, printWidth = 80): Promise<SubsystemDeclToken[]> {
|
|
44
|
+
// Pre-tokenized tokens from the wire take precedence.
|
|
45
|
+
if (component.tokens) return component.tokens;
|
|
46
|
+
|
|
47
|
+
// External kind — not valid TypeScript, bypass Prettier.
|
|
48
|
+
const kind = component.detail?.kind ?? component.kind;
|
|
49
|
+
if (kind === 'external') {
|
|
50
|
+
const label = component.detail?.kind === 'external' ? component.detail.label : component.name;
|
|
51
|
+
return [
|
|
52
|
+
{ text: 'external', kind: 'keyword' },
|
|
53
|
+
{ text: ' ', kind: 'punctuation' },
|
|
54
|
+
{ text: "'", kind: 'punctuation' },
|
|
55
|
+
{ text: label, kind: 'string' },
|
|
56
|
+
{ text: "'", kind: 'punctuation' },
|
|
57
|
+
];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Generate → format → tokenize
|
|
61
|
+
const raw = generateDeclarationString(component);
|
|
62
|
+
const { prettier, plugins } = await getPrettier();
|
|
63
|
+
const formatted = await prettier.format(raw, {
|
|
64
|
+
parser: 'typescript',
|
|
65
|
+
plugins: [plugins.typescript, plugins.estree],
|
|
66
|
+
printWidth,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
return tokenizeFormatted(formatted);
|
|
70
|
+
}
|