@principal-ai/principal-view-react 0.16.45 → 0.16.46
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 +49 -75
- 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 +61 -269
- 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 { useEffect, 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,42 @@ export function ComponentDeclaration({ component, onOpenFile, onRelatedSelect }:
|
|
|
207
201
|
),
|
|
208
202
|
);
|
|
209
203
|
|
|
210
|
-
|
|
211
|
-
//
|
|
212
|
-
//
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
if (detail?.kind === 'class') {
|
|
219
|
-
(detail.methods ?? []).forEach((m, i) =>
|
|
220
|
-
members.push(
|
|
221
|
-
line(
|
|
222
|
-
<>
|
|
223
|
-
{mb(m.name)}
|
|
224
|
-
{pn('(')}
|
|
225
|
-
{(m.parameters ?? []).length > 0 &&
|
|
226
|
-
list(
|
|
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;
|
|
204
|
+
// --- Declaration tokens ------------------------------------------------
|
|
205
|
+
// Tokenize asynchronously via Prettier + highlight.js. Pre-tokenized
|
|
206
|
+
// tokens from the wire skip the pipeline entirely.
|
|
207
|
+
const [tokens, setTokens] = useState(component.tokens ?? []);
|
|
208
|
+
useEffect(() => {
|
|
209
|
+
if (component.tokens) {
|
|
210
|
+
setTokens(component.tokens);
|
|
211
|
+
return;
|
|
302
212
|
}
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
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;
|
|
381
|
-
}
|
|
382
|
-
case 'module': {
|
|
383
|
-
const mod = detail?.kind === 'module' ? detail : undefined;
|
|
384
|
-
if (mod) {
|
|
385
|
-
(mod.imports ?? []).forEach((imp, i) =>
|
|
386
|
-
lines.push(
|
|
387
|
-
line(
|
|
388
|
-
<>
|
|
389
|
-
{kw('import')}
|
|
390
|
-
{' '}
|
|
391
|
-
{pn("'")}
|
|
392
|
-
{str(imp.name)}
|
|
393
|
-
{pn("';")}
|
|
394
|
-
</>,
|
|
395
|
-
`imp${i}`,
|
|
396
|
-
),
|
|
397
|
-
),
|
|
398
|
-
);
|
|
399
|
-
if ((mod.exports ?? []).length > 0)
|
|
400
|
-
lines.push(
|
|
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;
|
|
428
|
-
}
|
|
429
|
-
case 'external': {
|
|
430
|
-
lines.push(
|
|
431
|
-
line(
|
|
432
|
-
<>
|
|
433
|
-
{kw('external')}
|
|
434
|
-
{' '}
|
|
435
|
-
{detail?.kind === 'external' ? (
|
|
436
|
-
<>
|
|
437
|
-
{pn("'")}
|
|
438
|
-
{str(detail.label)}
|
|
439
|
-
{pn("'")}
|
|
440
|
-
</>
|
|
441
|
-
) : (
|
|
442
|
-
nm(displayName)
|
|
443
|
-
)}
|
|
444
|
-
</>,
|
|
445
|
-
'decl',
|
|
446
|
-
),
|
|
447
|
-
);
|
|
448
|
-
break;
|
|
213
|
+
let cancelled = false;
|
|
214
|
+
tokenizeComponent(component).then((t) => {
|
|
215
|
+
if (!cancelled) setTokens(t);
|
|
216
|
+
});
|
|
217
|
+
return () => { cancelled = true; };
|
|
218
|
+
}, [component]);
|
|
219
|
+
const declLines: ReactNode[][] = [[]];
|
|
220
|
+
let di = 0;
|
|
221
|
+
for (const tok of tokens) {
|
|
222
|
+
if (tok.kind === 'newline') {
|
|
223
|
+
declLines.push([]);
|
|
224
|
+
di++;
|
|
225
|
+
continue;
|
|
449
226
|
}
|
|
227
|
+
declLines[di].push(
|
|
228
|
+
<span key={`${di}-${declLines[di].length}`} style={{ color: tokenColor[tok.kind] }}>
|
|
229
|
+
{tok.text}
|
|
230
|
+
</span>,
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
for (let li = 0; li < declLines.length; li++) {
|
|
234
|
+
if (declLines[li].length === 0) continue;
|
|
235
|
+
lines.push(
|
|
236
|
+
<div key={`decl-${li}`} style={{ display: 'flex', alignItems: 'center', whiteSpace: 'pre', minHeight: 18 }}>
|
|
237
|
+
{declLines[li]}
|
|
238
|
+
</div>,
|
|
239
|
+
);
|
|
450
240
|
}
|
|
451
241
|
|
|
452
242
|
// Purpose prose follows the declaration — identity and syntax first, the
|
|
@@ -474,7 +264,9 @@ export function ComponentDeclaration({ component, onOpenFile, onRelatedSelect }:
|
|
|
474
264
|
fontFamily: theme.fonts.monospace,
|
|
475
265
|
fontSize: theme.fontSizes[1],
|
|
476
266
|
lineHeight: 1.7,
|
|
477
|
-
|
|
267
|
+
...(maxWidth != null
|
|
268
|
+
? { maxWidth, wordBreak: 'break-word' }
|
|
269
|
+
: { overflowX: 'auto' }),
|
|
478
270
|
}}
|
|
479
271
|
>
|
|
480
272
|
{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): 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: 80,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
return tokenizeFormatted(formatted);
|
|
70
|
+
}
|