@principal-ai/principal-view-react 0.16.54 → 0.16.56
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/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/pierre/index.d.ts +2 -0
- package/dist/pierre/index.d.ts.map +1 -1
- package/dist/pierre/index.js +1 -0
- package/dist/pierre/index.js.map +1 -1
- package/dist/pierre/pierreSyntaxTheme.d.ts +11 -0
- package/dist/pierre/pierreSyntaxTheme.d.ts.map +1 -0
- package/dist/pierre/pierreSyntaxTheme.js +20 -0
- package/dist/pierre/pierreSyntaxTheme.js.map +1 -0
- package/dist/pierre/syntaxTokens.d.ts +11 -0
- package/dist/pierre/syntaxTokens.d.ts.map +1 -0
- package/dist/pierre/syntaxTokens.js +51 -0
- package/dist/pierre/syntaxTokens.js.map +1 -0
- package/dist/subsystem/ComponentDeclaration.d.ts.map +1 -1
- package/dist/subsystem/ComponentDeclaration.js +35 -13
- package/dist/subsystem/ComponentDeclaration.js.map +1 -1
- package/dist/subsystem/SubsystemComponentGraph.d.ts.map +1 -1
- package/dist/subsystem/SubsystemComponentGraph.js +12 -5
- package/dist/subsystem/SubsystemComponentGraph.js.map +1 -1
- package/dist/subsystem/model.d.ts +4 -0
- package/dist/subsystem/model.d.ts.map +1 -1
- package/dist/subsystem/model.js +12 -0
- package/dist/subsystem/model.js.map +1 -1
- package/dist/subsystem/tokenizeComponent.d.ts +4 -3
- package/dist/subsystem/tokenizeComponent.d.ts.map +1 -1
- package/dist/subsystem/tokenizeComponent.js +6 -11
- package/dist/subsystem/tokenizeComponent.js.map +1 -1
- package/dist/subsystem/tokenizeFormatted.d.ts +3 -6
- package/dist/subsystem/tokenizeFormatted.d.ts.map +1 -1
- package/dist/subsystem/tokenizeFormatted.js +4 -75
- package/dist/subsystem/tokenizeFormatted.js.map +1 -1
- package/package.json +3 -1
- package/src/index.ts +2 -1
- package/src/pierre/index.ts +5 -0
- package/src/pierre/pierreSyntaxTheme.ts +23 -0
- package/src/pierre/syntaxTokens.test.ts +35 -0
- package/src/pierre/syntaxTokens.ts +60 -0
- package/src/subsystem/ComponentDeclaration.tsx +51 -13
- package/src/subsystem/SubsystemComponentGraph.tsx +16 -4
- package/src/subsystem/model.test.ts +22 -0
- package/src/subsystem/model.ts +18 -0
- package/src/subsystem/tokenizeComponent.ts +11 -11
- package/src/subsystem/tokenizeFormatted.ts +9 -82
|
@@ -37,6 +37,7 @@ import { IndustryMarkdownSlide } from 'themed-markdown';
|
|
|
37
37
|
import {
|
|
38
38
|
buildSubsystemGraph,
|
|
39
39
|
MECHANISM_COLOR,
|
|
40
|
+
subsystemGraphLayoutKey,
|
|
40
41
|
type SubsystemComponentEdge,
|
|
41
42
|
type SubsystemComponent,
|
|
42
43
|
type SubsystemEdgeMechanism,
|
|
@@ -163,19 +164,30 @@ function Inner({ components, edges, onSelect, onEdgeSelect, measured: _measured,
|
|
|
163
164
|
}, [components, selected]);
|
|
164
165
|
|
|
165
166
|
// Pass 1: build with estimated widths so React Flow can measure the DOM.
|
|
166
|
-
// The pane stays hidden until Pass 2 completes.
|
|
167
|
+
// The pane stays hidden until Pass 2 completes. Key off layout-affecting
|
|
168
|
+
// fields only — declarationRef updates after verify must not re-run ELK.
|
|
169
|
+
const layoutKey = useMemo(
|
|
170
|
+
() => subsystemGraphLayoutKey({ components, edges }),
|
|
171
|
+
[components, edges],
|
|
172
|
+
);
|
|
173
|
+
const componentsRef = useRef(components);
|
|
174
|
+
const edgesRef = useRef(edges);
|
|
175
|
+
componentsRef.current = components;
|
|
176
|
+
edgesRef.current = edges;
|
|
177
|
+
|
|
167
178
|
useEffect(() => {
|
|
168
179
|
let alive = true;
|
|
169
180
|
pass2DoneRef.current = false;
|
|
170
181
|
measuredDimsRef.current = new Map();
|
|
171
182
|
prevMeasuredSigRef.current = '';
|
|
172
|
-
|
|
183
|
+
const doc = { components: componentsRef.current, edges: edgesRef.current };
|
|
184
|
+
void buildSubsystemGraph(doc, { maxNodeWidth, showEdgeLabels }).then(({ nodes, edges: e }) => {
|
|
173
185
|
if (!alive) return;
|
|
174
186
|
setBuilt({ nodes, edges: e as Edge[] });
|
|
175
187
|
setLayoutReady(false);
|
|
176
188
|
});
|
|
177
189
|
return () => { alive = false; };
|
|
178
|
-
}, [
|
|
190
|
+
}, [layoutKey, maxNodeWidth, showEdgeLabels]);
|
|
179
191
|
|
|
180
192
|
// Track measured dimensions from React Flow's dimension changes.
|
|
181
193
|
// These arrive as { type: 'dimensions', id, dimensions } in onNodesChange.
|
|
@@ -722,7 +734,7 @@ function Inner({ components, edges, onSelect, onEdgeSelect, measured: _measured,
|
|
|
722
734
|
top: 10,
|
|
723
735
|
right: 10,
|
|
724
736
|
zIndex: 8,
|
|
725
|
-
|
|
737
|
+
width: 'min(560px, 70%)',
|
|
726
738
|
maxHeight: '46%',
|
|
727
739
|
overflowY: 'auto',
|
|
728
740
|
background: theme.colors.background,
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
deriveNameFromSymbol,
|
|
7
7
|
formatPurl,
|
|
8
8
|
packageColor,
|
|
9
|
+
subsystemGraphLayoutKey,
|
|
9
10
|
} from './model';
|
|
10
11
|
import type { SubsystemComponent, SubsystemComponentEdge } from './model';
|
|
11
12
|
|
|
@@ -87,4 +88,25 @@ describe('subsystem graph model', () => {
|
|
|
87
88
|
// Malformed purls pass through untouched.
|
|
88
89
|
expect(formatPurl('not-a-purl')).toBe('not-a-purl');
|
|
89
90
|
});
|
|
91
|
+
|
|
92
|
+
test('subsystemGraphLayoutKey ignores declarationRef-only changes', () => {
|
|
93
|
+
const base = { components: comps, edges };
|
|
94
|
+
const withRef = {
|
|
95
|
+
components: comps.map((c, i) =>
|
|
96
|
+
i === 0
|
|
97
|
+
? {
|
|
98
|
+
...c,
|
|
99
|
+
declarationRef: {
|
|
100
|
+
file: c.file,
|
|
101
|
+
startLine: 42,
|
|
102
|
+
lineHash: 'abc',
|
|
103
|
+
capturedAt: new Date(0).toISOString(),
|
|
104
|
+
},
|
|
105
|
+
}
|
|
106
|
+
: c,
|
|
107
|
+
),
|
|
108
|
+
edges,
|
|
109
|
+
};
|
|
110
|
+
expect(subsystemGraphLayoutKey(base)).toBe(subsystemGraphLayoutKey(withRef));
|
|
111
|
+
});
|
|
90
112
|
});
|
package/src/subsystem/model.ts
CHANGED
|
@@ -45,6 +45,8 @@ export type SubsystemDeclTokenKind =
|
|
|
45
45
|
export interface SubsystemDeclToken {
|
|
46
46
|
text: string;
|
|
47
47
|
kind: SubsystemDeclTokenKind;
|
|
48
|
+
/** Shiki/Pierre foreground when tokenized client-side; omitted on legacy wire tokens. */
|
|
49
|
+
color?: string;
|
|
48
50
|
}
|
|
49
51
|
|
|
50
52
|
export type SubsystemEdgeMechanism =
|
|
@@ -380,6 +382,22 @@ export function convertSubsystemToEdges(doc: SubsystemGraphDocument): SubsystemG
|
|
|
380
382
|
return edges;
|
|
381
383
|
}
|
|
382
384
|
|
|
385
|
+
/** Stable key for layout-affecting graph fields (ignores declarationRef, etc.). */
|
|
386
|
+
export function subsystemGraphLayoutKey(
|
|
387
|
+
doc: Pick<SubsystemGraphDocument, 'components' | 'edges'>,
|
|
388
|
+
): string {
|
|
389
|
+
const components = doc.components
|
|
390
|
+
.map(({ id, purl, name, symbol, kind, file, purpose }) =>
|
|
391
|
+
[id, purl, name, symbol ?? '', kind, file, purpose ?? ''].join('\0'))
|
|
392
|
+
.sort()
|
|
393
|
+
.join('\n');
|
|
394
|
+
const edgeKey = doc.edges
|
|
395
|
+
.map(({ id, from, to, mechanism }) => [id, from, to, mechanism].join('\0'))
|
|
396
|
+
.sort()
|
|
397
|
+
.join('\n');
|
|
398
|
+
return `${components}|${edgeKey}`;
|
|
399
|
+
}
|
|
400
|
+
|
|
383
401
|
/**
|
|
384
402
|
* Build the full React Flow graph (nodes + edges) using **ELK auto-layout** to
|
|
385
403
|
* position every node (including external stub targets), so the graph is
|
|
@@ -2,12 +2,13 @@
|
|
|
2
2
|
* Tokenize a SubsystemComponent into a flat token stream for the detail panel.
|
|
3
3
|
*
|
|
4
4
|
* Pipeline: generate declaration string → format with Prettier → tokenize
|
|
5
|
-
* with
|
|
6
|
-
* the entire pipeline (for pre-tokenized data from graphify).
|
|
5
|
+
* with Shiki + Pierre themes. The `component.tokens` field, when present,
|
|
6
|
+
* overrides the entire pipeline (for pre-tokenized data from graphify).
|
|
7
7
|
*
|
|
8
8
|
* This function is async because Prettier's format() is async.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
+
import type { PierreSyntaxThemeName } from '../pierre/pierreSyntaxTheme';
|
|
11
12
|
import type { SubsystemComponent, SubsystemDeclToken } from './model';
|
|
12
13
|
import { generateDeclarationString } from './formatDeclaration';
|
|
13
14
|
import { tokenizeFormatted } from './tokenizeFormatted';
|
|
@@ -40,7 +41,11 @@ async function getPrettier() {
|
|
|
40
41
|
* it's returned as-is. Otherwise the pipeline generates a TypeScript
|
|
41
42
|
* declaration string, formats it with Prettier, and tokenizes the output.
|
|
42
43
|
*/
|
|
43
|
-
export async function tokenizeComponent(
|
|
44
|
+
export async function tokenizeComponent(
|
|
45
|
+
component: SubsystemComponent,
|
|
46
|
+
printWidth = 80,
|
|
47
|
+
themeName: PierreSyntaxThemeName = 'pierre-dark',
|
|
48
|
+
): Promise<SubsystemDeclToken[]> {
|
|
44
49
|
// Pre-tokenized tokens from the wire take precedence.
|
|
45
50
|
if (component.tokens) return component.tokens;
|
|
46
51
|
|
|
@@ -48,13 +53,8 @@ export async function tokenizeComponent(component: SubsystemComponent, printWidt
|
|
|
48
53
|
const kind = component.detail?.kind ?? component.kind;
|
|
49
54
|
if (kind === 'external') {
|
|
50
55
|
const label = component.detail?.kind === 'external' ? component.detail.label : component.name;
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
{ text: ' ', kind: 'punctuation' },
|
|
54
|
-
{ text: "'", kind: 'punctuation' },
|
|
55
|
-
{ text: label, kind: 'string' },
|
|
56
|
-
{ text: "'", kind: 'punctuation' },
|
|
57
|
-
];
|
|
56
|
+
const escaped = label.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
|
|
57
|
+
return tokenizeFormatted(`external '${escaped}'`, themeName);
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
// Generate → format → tokenize
|
|
@@ -66,5 +66,5 @@ export async function tokenizeComponent(component: SubsystemComponent, printWidt
|
|
|
66
66
|
printWidth,
|
|
67
67
|
});
|
|
68
68
|
|
|
69
|
-
return tokenizeFormatted(formatted);
|
|
69
|
+
return tokenizeFormatted(formatted, themeName);
|
|
70
70
|
}
|
|
@@ -1,92 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Tokenize a formatted TypeScript declaration string into SubsystemDeclToken[]
|
|
3
|
-
* using
|
|
4
|
-
*
|
|
5
|
-
* highlight.js produces HTML with `<span class="hljs-...">` wrappers. This
|
|
6
|
-
* module parses that output into our token format so the renderer stays
|
|
7
|
-
* language-agnostic.
|
|
3
|
+
* using Shiki + Pierre themes (same highlighter stack as @pierre/diffs).
|
|
8
4
|
*/
|
|
9
5
|
|
|
10
|
-
import
|
|
11
|
-
import
|
|
12
|
-
import type { SubsystemDeclToken
|
|
13
|
-
|
|
14
|
-
// Register once — safe to call multiple times.
|
|
15
|
-
try {
|
|
16
|
-
hljs.registerLanguage('typescript', typescript);
|
|
17
|
-
} catch {
|
|
18
|
-
// Already registered.
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
// hljs class → our token kind
|
|
22
|
-
const CLASS_MAP: Record<string, SubsystemDeclTokenKind> = {
|
|
23
|
-
'hljs-keyword': 'keyword',
|
|
24
|
-
'hljs-title': 'name',
|
|
25
|
-
'hljs-title.function_': 'name',
|
|
26
|
-
'hljs-params': 'member',
|
|
27
|
-
'hljs-type': 'type',
|
|
28
|
-
'hljs-built_in': 'type',
|
|
29
|
-
'hljs-string': 'string',
|
|
30
|
-
'hljs-number': 'string',
|
|
31
|
-
'hljs-comment': 'punctuation',
|
|
32
|
-
};
|
|
6
|
+
import { tokenizeWithPierreSyntax } from '../pierre/syntaxTokens';
|
|
7
|
+
import type { PierreSyntaxThemeName } from '../pierre/pierreSyntaxTheme';
|
|
8
|
+
import type { SubsystemDeclToken } from './model';
|
|
33
9
|
|
|
34
10
|
/**
|
|
35
11
|
* Tokenize a formatted TypeScript string into SubsystemDeclToken[].
|
|
36
12
|
* The input should already be formatted by Prettier (or be valid TS).
|
|
37
13
|
*/
|
|
38
|
-
export function tokenizeFormatted(
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
// ---------------------------------------------------------------------------
|
|
44
|
-
// HTML parser — walks the hljs output and extracts tokens
|
|
45
|
-
// ---------------------------------------------------------------------------
|
|
46
|
-
|
|
47
|
-
function unescapeHtml(text: string): string {
|
|
48
|
-
return text
|
|
49
|
-
.replace(/"/g, '"')
|
|
50
|
-
.replace(/</g, '<')
|
|
51
|
-
.replace(/>/g, '>')
|
|
52
|
-
.replace(/&/g, '&')
|
|
53
|
-
.replace(/'/g, "'");
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function parseHighlightedHtml(html: string): SubsystemDeclToken[] {
|
|
57
|
-
const tokens: SubsystemDeclToken[] = [];
|
|
58
|
-
// Regex matches either `<span class="hljs-...">`, `</span>`, or text content.
|
|
59
|
-
const re = /<span class="([^"]+)">|<\/span>|[^<]+/g;
|
|
60
|
-
const classStack: string[] = [];
|
|
61
|
-
let match: RegExpExecArray | null;
|
|
62
|
-
|
|
63
|
-
while ((match = re.exec(html)) !== null) {
|
|
64
|
-
const chunk = match[0];
|
|
65
|
-
|
|
66
|
-
if (chunk.startsWith('<span')) {
|
|
67
|
-
// Opening tag — push class onto stack.
|
|
68
|
-
classStack.push(match[1]);
|
|
69
|
-
} else if (chunk === '</span>') {
|
|
70
|
-
// Closing tag — pop class.
|
|
71
|
-
classStack.pop();
|
|
72
|
-
} else {
|
|
73
|
-
// Text content — emit tokens for each line segment.
|
|
74
|
-
const currentClass = classStack[classStack.length - 1] ?? '';
|
|
75
|
-
const kind = CLASS_MAP[currentClass] ?? 'punctuation';
|
|
76
|
-
const text = chunk;
|
|
77
|
-
|
|
78
|
-
// Split on newlines so each line becomes its own token set.
|
|
79
|
-
const lines = text.split('\n');
|
|
80
|
-
for (let i = 0; i < lines.length; i++) {
|
|
81
|
-
if (i > 0) {
|
|
82
|
-
tokens.push({ text: '', kind: 'newline' });
|
|
83
|
-
}
|
|
84
|
-
if (lines[i]) {
|
|
85
|
-
tokens.push({ text: unescapeHtml(lines[i]), kind });
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
return tokens;
|
|
14
|
+
export function tokenizeFormatted(
|
|
15
|
+
code: string,
|
|
16
|
+
themeName: PierreSyntaxThemeName = 'pierre-dark',
|
|
17
|
+
): Promise<SubsystemDeclToken[]> {
|
|
18
|
+
return tokenizeWithPierreSyntax(code, themeName);
|
|
92
19
|
}
|