@principal-ai/subsystems-react 0.30.2 → 0.32.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/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/sourceLang.d.ts +20 -0
- package/dist/pierre/sourceLang.d.ts.map +1 -0
- package/dist/pierre/sourceLang.js +78 -0
- package/dist/pierre/sourceLang.js.map +1 -0
- package/dist/pierre/syntaxTokens.d.ts +6 -4
- package/dist/pierre/syntaxTokens.d.ts.map +1 -1
- package/dist/pierre/syntaxTokens.js +22 -6
- package/dist/pierre/syntaxTokens.js.map +1 -1
- package/dist/subsystem/ComponentDeclaration.d.ts +1 -3
- package/dist/subsystem/ComponentDeclaration.d.ts.map +1 -1
- package/dist/subsystem/ComponentDeclaration.js +8 -24
- package/dist/subsystem/ComponentDeclaration.js.map +1 -1
- package/dist/subsystem/ConstructsCatalog.d.ts +1 -2
- package/dist/subsystem/ConstructsCatalog.d.ts.map +1 -1
- package/dist/subsystem/ConstructsCatalog.js +2 -2
- package/dist/subsystem/ConstructsCatalog.js.map +1 -1
- package/dist/subsystem/SubsystemComponentGraph.d.ts +0 -2
- package/dist/subsystem/SubsystemComponentGraph.d.ts.map +1 -1
- package/dist/subsystem/SubsystemComponentGraph.js +3 -3
- package/dist/subsystem/SubsystemComponentGraph.js.map +1 -1
- package/dist/subsystem/tokenizeComponent.d.ts +7 -5
- package/dist/subsystem/tokenizeComponent.d.ts.map +1 -1
- package/dist/subsystem/tokenizeComponent.js +38 -24
- package/dist/subsystem/tokenizeComponent.js.map +1 -1
- package/dist/subsystem/tokenizeFormatted.d.ts +6 -5
- package/dist/subsystem/tokenizeFormatted.d.ts.map +1 -1
- package/dist/subsystem/tokenizeFormatted.js +6 -6
- package/dist/subsystem/tokenizeFormatted.js.map +1 -1
- package/package.json +1 -1
- package/src/pierre/index.ts +5 -0
- package/src/pierre/sourceLang.test.ts +40 -0
- package/src/pierre/sourceLang.ts +83 -0
- package/src/pierre/syntaxTokens.ts +30 -5
- package/src/stories/Subsystem/ComponentDeclarationAudit.stories.tsx +103 -0
- package/src/subsystem/ComponentDeclaration.tsx +6 -38
- package/src/subsystem/ConstructsCatalog.tsx +0 -3
- package/src/subsystem/SubsystemComponentGraph.tsx +1 -5
- package/src/subsystem/tokenizeComponent.test.ts +56 -0
- package/src/subsystem/tokenizeComponent.ts +44 -26
- package/src/subsystem/tokenizeFormatted.ts +7 -5
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import { isPrettierSourceLang, sourceLangForPath } from './sourceLang';
|
|
3
|
+
|
|
4
|
+
describe('sourceLangForPath', () => {
|
|
5
|
+
test('maps JS/TS extensions to their grammar', () => {
|
|
6
|
+
expect(sourceLangForPath('src/a.ts')).toBe('typescript');
|
|
7
|
+
expect(sourceLangForPath('src/a.tsx')).toBe('tsx');
|
|
8
|
+
expect(sourceLangForPath('src/a.mjs')).toBe('javascript');
|
|
9
|
+
expect(sourceLangForPath('src/a.jsx')).toBe('jsx');
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
test('maps other languages', () => {
|
|
13
|
+
expect(sourceLangForPath('graphify/dedup.py')).toBe('python');
|
|
14
|
+
expect(sourceLangForPath('src/lib.rs')).toBe('rust');
|
|
15
|
+
expect(sourceLangForPath('cmd/main.go')).toBe('go');
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test('unknown / missing path falls back to text', () => {
|
|
19
|
+
expect(sourceLangForPath(undefined)).toBe('text');
|
|
20
|
+
expect(sourceLangForPath('')).toBe('text');
|
|
21
|
+
expect(sourceLangForPath('Makefile')).toBe('text');
|
|
22
|
+
expect(sourceLangForPath('src/a.unknownext')).toBe('text');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test('C-family stays text (WebKit-safe grammars)', () => {
|
|
26
|
+
expect(sourceLangForPath('src/main.cpp')).toBe('text');
|
|
27
|
+
expect(sourceLangForPath('src/graph.h')).toBe('text');
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe('isPrettierSourceLang', () => {
|
|
32
|
+
test('true only for the bundled Prettier parsers', () => {
|
|
33
|
+
for (const lang of ['typescript', 'tsx', 'javascript', 'jsx']) {
|
|
34
|
+
expect(isPrettierSourceLang(lang)).toBe(true);
|
|
35
|
+
}
|
|
36
|
+
for (const lang of ['python', 'rust', 'go', 'text']) {
|
|
37
|
+
expect(isPrettierSourceLang(lang)).toBe(false);
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
});
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Source-language inference for a component's declaration.
|
|
3
|
+
*
|
|
4
|
+
* The declaration panel is language-agnostic in shape (it renders a normalized
|
|
5
|
+
* construct), but formatting + highlighting are language-specific: Prettier
|
|
6
|
+
* only ships JS/TS/CSS/HTML/etc. parsers, and Shiki needs a grammar id. Infer
|
|
7
|
+
* the language from the component's `file` extension so non-JS/TS sources are
|
|
8
|
+
* not forced through the TypeScript parser.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { isPierreCFamilyPath } from './pierreFileLang';
|
|
12
|
+
|
|
13
|
+
/** A Shiki language id (e.g. `typescript`, `python`) or `text` when unknown. */
|
|
14
|
+
export type SourceSyntaxLang = string;
|
|
15
|
+
|
|
16
|
+
const EXT_TO_LANG: Record<string, SourceSyntaxLang> = {
|
|
17
|
+
ts: 'typescript',
|
|
18
|
+
mts: 'typescript',
|
|
19
|
+
cts: 'typescript',
|
|
20
|
+
tsx: 'tsx',
|
|
21
|
+
js: 'javascript',
|
|
22
|
+
mjs: 'javascript',
|
|
23
|
+
cjs: 'javascript',
|
|
24
|
+
jsx: 'jsx',
|
|
25
|
+
py: 'python',
|
|
26
|
+
pyi: 'python',
|
|
27
|
+
rb: 'ruby',
|
|
28
|
+
rs: 'rust',
|
|
29
|
+
go: 'go',
|
|
30
|
+
java: 'java',
|
|
31
|
+
kt: 'kotlin',
|
|
32
|
+
kts: 'kotlin',
|
|
33
|
+
swift: 'swift',
|
|
34
|
+
scala: 'scala',
|
|
35
|
+
php: 'php',
|
|
36
|
+
cs: 'csharp',
|
|
37
|
+
sh: 'shellscript',
|
|
38
|
+
bash: 'shellscript',
|
|
39
|
+
zsh: 'shellscript',
|
|
40
|
+
sql: 'sql',
|
|
41
|
+
graphql: 'graphql',
|
|
42
|
+
gql: 'graphql',
|
|
43
|
+
json: 'json',
|
|
44
|
+
jsonc: 'json',
|
|
45
|
+
yaml: 'yaml',
|
|
46
|
+
yml: 'yaml',
|
|
47
|
+
toml: 'toml',
|
|
48
|
+
md: 'markdown',
|
|
49
|
+
mdx: 'markdown',
|
|
50
|
+
html: 'html',
|
|
51
|
+
htm: 'html',
|
|
52
|
+
css: 'css',
|
|
53
|
+
scss: 'scss',
|
|
54
|
+
less: 'less',
|
|
55
|
+
vue: 'vue',
|
|
56
|
+
svelte: 'svelte',
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/** Languages Prettier can parse (its bundled parsers, all via `typescript`). */
|
|
60
|
+
const PRETTIER_LANGS = new Set(['typescript', 'tsx', 'javascript', 'jsx']);
|
|
61
|
+
|
|
62
|
+
function extensionOf(path: string): string {
|
|
63
|
+
const base = path.split(/[/\\]/).pop() ?? path;
|
|
64
|
+
const dot = base.lastIndexOf('.');
|
|
65
|
+
if (dot < 0) return '';
|
|
66
|
+
return base.slice(dot + 1).toLowerCase();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Shiki language id for a source path, or `text` when unknown. C-family paths
|
|
71
|
+
* stay `text` (loading their grammars has trapped WebKit — see
|
|
72
|
+
* `pierreFileLang`).
|
|
73
|
+
*/
|
|
74
|
+
export function sourceLangForPath(path?: string | null): SourceSyntaxLang {
|
|
75
|
+
if (!path) return 'text';
|
|
76
|
+
if (isPierreCFamilyPath(path)) return 'text';
|
|
77
|
+
return EXT_TO_LANG[extensionOf(path)] ?? 'text';
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** True when Prettier should format this language's generated declaration. */
|
|
81
|
+
export function isPrettierSourceLang(lang: SourceSyntaxLang): boolean {
|
|
82
|
+
return PRETTIER_LANGS.has(lang);
|
|
83
|
+
}
|
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Tokenize
|
|
2
|
+
* Tokenize source text with Shiki + Pierre themes — same stack as @pierre/diffs.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
createHighlighter,
|
|
7
|
+
type BundledLanguage,
|
|
8
|
+
type Highlighter,
|
|
9
|
+
type ThemeInput,
|
|
10
|
+
} from 'shiki';
|
|
6
11
|
import pierreDark from '@pierre/theme/pierre-dark';
|
|
7
12
|
import pierreLight from '@pierre/theme/pierre-light';
|
|
8
13
|
import type { SubsystemDeclToken } from '../subsystem/model';
|
|
9
14
|
import type { PierreSyntaxThemeName } from './pierreSyntaxTheme';
|
|
15
|
+
import type { SourceSyntaxLang } from './sourceLang';
|
|
10
16
|
|
|
11
17
|
let highlighterPromise: Promise<Highlighter> | null = null;
|
|
12
18
|
|
|
@@ -23,21 +29,40 @@ async function getPierreHighlighter(): Promise<Highlighter> {
|
|
|
23
29
|
return highlighterPromise;
|
|
24
30
|
}
|
|
25
31
|
|
|
32
|
+
/** Load a grammar on demand; fall back to `text` when it isn't available. */
|
|
33
|
+
async function resolveLang(
|
|
34
|
+
highlighter: Highlighter,
|
|
35
|
+
lang: SourceSyntaxLang,
|
|
36
|
+
): Promise<BundledLanguage | 'text'> {
|
|
37
|
+
if (lang === 'text') return 'text';
|
|
38
|
+
try {
|
|
39
|
+
if (!highlighter.getLoadedLanguages().includes(lang)) {
|
|
40
|
+
await highlighter.loadLanguage(lang as BundledLanguage);
|
|
41
|
+
}
|
|
42
|
+
return lang as BundledLanguage;
|
|
43
|
+
} catch {
|
|
44
|
+
return 'text';
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
26
48
|
function normalizeColor(color?: string): string | undefined {
|
|
27
49
|
return color?.toLowerCase();
|
|
28
50
|
}
|
|
29
51
|
|
|
30
52
|
/**
|
|
31
|
-
* Tokenize
|
|
32
|
-
*
|
|
53
|
+
* Tokenize code using Pierre's Shiki themes. Each token carries the resolved
|
|
54
|
+
* foreground color Pierre would render. `lang` is the inferred source language
|
|
55
|
+
* (`text` when unknown) — TS by default for backward compatibility.
|
|
33
56
|
*/
|
|
34
57
|
export async function tokenizeWithPierreSyntax(
|
|
35
58
|
code: string,
|
|
36
59
|
themeName: PierreSyntaxThemeName,
|
|
60
|
+
lang: SourceSyntaxLang = 'typescript',
|
|
37
61
|
): Promise<SubsystemDeclToken[]> {
|
|
38
62
|
const highlighter = await getPierreHighlighter();
|
|
63
|
+
const resolved = await resolveLang(highlighter, lang);
|
|
39
64
|
const { tokens } = highlighter.codeToTokens(code, {
|
|
40
|
-
lang:
|
|
65
|
+
lang: resolved,
|
|
41
66
|
theme: themeName,
|
|
42
67
|
});
|
|
43
68
|
|
|
@@ -397,3 +397,106 @@ export const AugmentedSignature: StoryObj<{ constrained: boolean }> = {
|
|
|
397
397
|
</div>
|
|
398
398
|
),
|
|
399
399
|
};
|
|
400
|
+
|
|
401
|
+
// ---------------------------------------------------------------------------
|
|
402
|
+
// Prettier fallback — when formatting throws (Electrobun's bundled Prettier
|
|
403
|
+
// does), the declaration is tokenized unformatted as one long line. This story
|
|
404
|
+
// reproduces that path deterministically: an unparseable parameter type makes
|
|
405
|
+
// Prettier throw, so `tokenizeComponent` falls back to the raw string. The
|
|
406
|
+
// panel must wrap at spaces, never break a token mid-word (`func` / `c`).
|
|
407
|
+
// ---------------------------------------------------------------------------
|
|
408
|
+
|
|
409
|
+
const fallbackCases: { label: string; component: SubsystemComponent }[] = [
|
|
410
|
+
{
|
|
411
|
+
label: 'formatted — long signature (Prettier wraps at printWidth)',
|
|
412
|
+
component: {
|
|
413
|
+
alias: 'fallback-formatted',
|
|
414
|
+
name: 'mergeSessions',
|
|
415
|
+
construct: 'function',
|
|
416
|
+
file: 'src/session/merge.ts',
|
|
417
|
+
purl: 'pkg:github/principal-ai/agent-monitoring',
|
|
418
|
+
symbol: 'mergeSessions',
|
|
419
|
+
declaration: {
|
|
420
|
+
kind: 'function',
|
|
421
|
+
parameters: [
|
|
422
|
+
{ name: 'sessions', type: 'SessionRecord[]' },
|
|
423
|
+
{ name: 'options', type: 'MergeOptions' },
|
|
424
|
+
{ name: 'onConflict', type: '((a: SessionRecord, b: SessionRecord) => SessionRecord)' },
|
|
425
|
+
],
|
|
426
|
+
returnType: 'Promise<Map<string, SessionEvent[]>>',
|
|
427
|
+
callers: [],
|
|
428
|
+
callees: [],
|
|
429
|
+
} satisfies GraphifyComponentDetail,
|
|
430
|
+
},
|
|
431
|
+
},
|
|
432
|
+
{
|
|
433
|
+
label: 'fallback — unformatted single line (Prettier throws on `Array<`)',
|
|
434
|
+
component: {
|
|
435
|
+
alias: 'fallback-raw',
|
|
436
|
+
name: 'mergeSessions',
|
|
437
|
+
construct: 'function',
|
|
438
|
+
file: 'src/session/merge.ts',
|
|
439
|
+
purl: 'pkg:github/principal-ai/agent-monitoring',
|
|
440
|
+
symbol: 'mergeSessions',
|
|
441
|
+
declaration: {
|
|
442
|
+
kind: 'function',
|
|
443
|
+
parameters: [
|
|
444
|
+
{ name: 'sessions', type: 'SessionRecord[]' },
|
|
445
|
+
{ name: 'options', type: 'MergeOptions' },
|
|
446
|
+
{ name: 'onConflict', type: 'Array<' },
|
|
447
|
+
],
|
|
448
|
+
returnType: 'Promise<Map<string, SessionEvent[]>>',
|
|
449
|
+
callers: [],
|
|
450
|
+
callees: [],
|
|
451
|
+
} satisfies GraphifyComponentDetail,
|
|
452
|
+
},
|
|
453
|
+
},
|
|
454
|
+
{
|
|
455
|
+
label: 'non-JS/TS (python) — Prettier skipped, python grammar',
|
|
456
|
+
component: {
|
|
457
|
+
alias: 'fallback-python',
|
|
458
|
+
name: 'deduplicate_entities',
|
|
459
|
+
construct: 'function',
|
|
460
|
+
file: 'graphify/dedup.py',
|
|
461
|
+
purl: 'pkg:github/graphify-labs/graphify#graphify/dedup.py',
|
|
462
|
+
symbol: 'deduplicate_entities',
|
|
463
|
+
declaration: {
|
|
464
|
+
kind: 'function',
|
|
465
|
+
parameters: [
|
|
466
|
+
{ name: 'graph', type: 'nx.MultiDiGraph' },
|
|
467
|
+
{ name: 'keep', type: 'bool' },
|
|
468
|
+
],
|
|
469
|
+
returnType: 'list[tuple[str, object]]',
|
|
470
|
+
callers: [],
|
|
471
|
+
callees: [],
|
|
472
|
+
} satisfies GraphifyComponentDetail,
|
|
473
|
+
},
|
|
474
|
+
},
|
|
475
|
+
];
|
|
476
|
+
|
|
477
|
+
export const UnformattedFallback: StoryObj<{ constrained: boolean }> = {
|
|
478
|
+
render: ({ constrained }) => (
|
|
479
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 24 }}>
|
|
480
|
+
{fallbackCases.map((c) => (
|
|
481
|
+
<div key={c.label}>
|
|
482
|
+
<div
|
|
483
|
+
style={{
|
|
484
|
+
fontFamily: 'monospace',
|
|
485
|
+
fontSize: 11,
|
|
486
|
+
color: '#888',
|
|
487
|
+
marginBottom: 4,
|
|
488
|
+
textTransform: 'uppercase',
|
|
489
|
+
letterSpacing: '0.05em',
|
|
490
|
+
}}
|
|
491
|
+
>
|
|
492
|
+
{c.label}
|
|
493
|
+
</div>
|
|
494
|
+
<ComponentDeclaration
|
|
495
|
+
component={c.component}
|
|
496
|
+
maxWidth={constrained ? '80ch' : undefined}
|
|
497
|
+
/>
|
|
498
|
+
</div>
|
|
499
|
+
))}
|
|
500
|
+
</div>
|
|
501
|
+
),
|
|
502
|
+
};
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { useEffect, useRef, useState, type ReactNode } from 'react';
|
|
10
|
-
import { AlignLeft, FileText
|
|
10
|
+
import { AlignLeft, FileText } from 'lucide-react';
|
|
11
11
|
import { useTheme } from '@principal-ade/industry-theme';
|
|
12
12
|
import { resolvePierreSyntaxThemeName } from '../pierre/pierreSyntaxTheme';
|
|
13
13
|
import {
|
|
@@ -115,8 +115,6 @@ export interface ComponentDeclarationProps {
|
|
|
115
115
|
onRelatedSelect?: (ref: string) => void;
|
|
116
116
|
/** Max width of the declaration panel (CSS value). Defaults to none. */
|
|
117
117
|
maxWidth?: string | number;
|
|
118
|
-
/** When set, shows a Verify control that calls back with the component id. */
|
|
119
|
-
onVerify?: (componentAlias: string) => void;
|
|
120
118
|
/** Live verification status for the selected component. */
|
|
121
119
|
verification?: ComponentVerificationState | null;
|
|
122
120
|
/** Start with the file path row visible (catalog / source-first views). */
|
|
@@ -270,7 +268,6 @@ export function ComponentDeclaration({
|
|
|
270
268
|
onOpenFile,
|
|
271
269
|
onRelatedSelect: _onRelatedSelect,
|
|
272
270
|
maxWidth,
|
|
273
|
-
onVerify,
|
|
274
271
|
verification,
|
|
275
272
|
defaultShowFile = false,
|
|
276
273
|
fileOpen = false,
|
|
@@ -400,39 +397,8 @@ export function ComponentDeclaration({
|
|
|
400
397
|
<Icon size={13} />
|
|
401
398
|
</button>
|
|
402
399
|
);
|
|
403
|
-
const verifyBusy = verification?.phase === 'checking';
|
|
404
400
|
const headerActions = (
|
|
405
401
|
<span style={{ marginLeft: 'auto', display: 'flex', alignItems: 'center', gap: 2 }}>
|
|
406
|
-
{onVerify && (
|
|
407
|
-
<button
|
|
408
|
-
type="button"
|
|
409
|
-
title="Verify against graphify"
|
|
410
|
-
aria-label="Verify against graphify"
|
|
411
|
-
disabled={verifyBusy}
|
|
412
|
-
onClick={(e) => {
|
|
413
|
-
e.stopPropagation();
|
|
414
|
-
onVerify(component.alias);
|
|
415
|
-
}}
|
|
416
|
-
style={{
|
|
417
|
-
display: 'flex',
|
|
418
|
-
alignItems: 'center',
|
|
419
|
-
gap: 4,
|
|
420
|
-
height: 20,
|
|
421
|
-
padding: '0 6px',
|
|
422
|
-
border: `1px solid ${theme.colors.border}`,
|
|
423
|
-
borderRadius: 4,
|
|
424
|
-
background: 'transparent',
|
|
425
|
-
color: verifyBusy ? muted : theme.colors.textSecondary,
|
|
426
|
-
cursor: verifyBusy ? 'wait' : 'pointer',
|
|
427
|
-
fontSize: theme.fontSizes[0],
|
|
428
|
-
fontFamily: theme.fonts.body,
|
|
429
|
-
opacity: verifyBusy ? 0.7 : 1,
|
|
430
|
-
}}
|
|
431
|
-
>
|
|
432
|
-
<ShieldCheck size={12} />
|
|
433
|
-
{verifyBusy ? '…' : 'Verify'}
|
|
434
|
-
</button>
|
|
435
|
-
)}
|
|
436
402
|
{!inlineChrome &&
|
|
437
403
|
toggleBtn(showFile, () => setShowFile((v) => !v), 'Toggle file path', FileText)}
|
|
438
404
|
{!inlineChrome && lineLocationLabel}
|
|
@@ -501,7 +467,7 @@ export function ComponentDeclaration({
|
|
|
501
467
|
'purl',
|
|
502
468
|
),
|
|
503
469
|
);
|
|
504
|
-
} else if (
|
|
470
|
+
} else if (!showRepoIdentity && !inlineChrome) {
|
|
505
471
|
lines.push(line(headerActions, 'actions'));
|
|
506
472
|
}
|
|
507
473
|
|
|
@@ -786,8 +752,10 @@ export function ComponentDeclaration({
|
|
|
786
752
|
<div
|
|
787
753
|
key={`decl-${li}`}
|
|
788
754
|
style={{
|
|
789
|
-
|
|
790
|
-
|
|
755
|
+
// Block (not flex): tokens flow inline and wrap at spaces. A flex
|
|
756
|
+
// row lets each token span shrink and break mid-word when a line
|
|
757
|
+
// overflows (e.g. the unformatted prettier-fallback declaration).
|
|
758
|
+
display: 'block',
|
|
791
759
|
whiteSpace: 'pre-wrap',
|
|
792
760
|
overflowWrap: 'anywhere',
|
|
793
761
|
minWidth: 0,
|
|
@@ -39,7 +39,6 @@ export interface ConstructsCatalogProps {
|
|
|
39
39
|
renderFileViewer?: (file: string, opts?: SubsystemOpenFileOptions) => ReactNode;
|
|
40
40
|
renderFileView?: (component: SubsystemComponent) => ReactNode;
|
|
41
41
|
onFileSelect?: (file: string) => void;
|
|
42
|
-
onVerifyComponent?: (componentAlias: string) => void;
|
|
43
42
|
componentVerification?: ComponentVerificationState | null;
|
|
44
43
|
}
|
|
45
44
|
|
|
@@ -145,7 +144,6 @@ export function ConstructsCatalog({
|
|
|
145
144
|
renderFileViewer,
|
|
146
145
|
renderFileView,
|
|
147
146
|
onFileSelect,
|
|
148
|
-
onVerifyComponent,
|
|
149
147
|
componentVerification,
|
|
150
148
|
}: ConstructsCatalogProps) {
|
|
151
149
|
const { theme, mode } = useTheme();
|
|
@@ -950,7 +948,6 @@ export function ConstructsCatalog({
|
|
|
950
948
|
onOpenFile={fileViewer ? onOpenFile : undefined}
|
|
951
949
|
defaultShowFile={!!fileViewer}
|
|
952
950
|
onRelatedSelect={onRelatedSelect}
|
|
953
|
-
onVerify={onVerifyComponent}
|
|
954
951
|
fileOpen={fileOpen}
|
|
955
952
|
declarationOpen={lineOpen}
|
|
956
953
|
showRepoIdentity={false}
|
|
@@ -285,8 +285,6 @@ export interface SubsystemComponentGraphProps {
|
|
|
285
285
|
* path). The tree is derived from the components' `file` values.
|
|
286
286
|
*/
|
|
287
287
|
onFileSelect?: (file: string) => void;
|
|
288
|
-
/** Verify the selected component against graphify (declaration panel). */
|
|
289
|
-
onVerifyComponent?: (componentAlias: string) => void;
|
|
290
288
|
/** Live verification status for the selected component. */
|
|
291
289
|
componentVerification?: ComponentVerificationState | null;
|
|
292
290
|
/**
|
|
@@ -385,7 +383,7 @@ interface InnerProps extends SubsystemComponentGraphProps {
|
|
|
385
383
|
measured: { w: number; h: number } | null;
|
|
386
384
|
}
|
|
387
385
|
|
|
388
|
-
function Inner({ components, relations, walkthroughs, initialWalkthroughId, onReorderWalkthroughs, onSelect, onEdgeSelect, measured: _measured, maxNodeWidth, showEdgeLabels, edgeView, title, hideSidebar, walkthroughStepMode = 'focus', autoPlayWalkthroughs = false, walkthroughAutoPlayIntervalMs = WALKTHROUGH_PLAY_PAUSE_MS, zoomOnWalkthroughFocus = true, graphTitle, showWalkthroughTitle = false, description, canvasOverlay, sidebarExtra, sidebarAfterDescription, diagnostic, issues, showIssues, focusIssueCategory, onSelectIssue, onApplyIssueFix, onHoverIssue, renderFileView, renderFileViewer, renderWalkthroughViewer, onFileSelect,
|
|
386
|
+
function Inner({ components, relations, walkthroughs, initialWalkthroughId, onReorderWalkthroughs, onSelect, onEdgeSelect, measured: _measured, maxNodeWidth, showEdgeLabels, edgeView, title, hideSidebar, walkthroughStepMode = 'focus', autoPlayWalkthroughs = false, walkthroughAutoPlayIntervalMs = WALKTHROUGH_PLAY_PAUSE_MS, zoomOnWalkthroughFocus = true, graphTitle, showWalkthroughTitle = false, description, canvasOverlay, sidebarExtra, sidebarAfterDescription, diagnostic, issues, showIssues, focusIssueCategory, onSelectIssue, onApplyIssueFix, onHoverIssue, renderFileView, renderFileViewer, renderWalkthroughViewer, onFileSelect, componentVerification, persistKey }: InnerProps) {
|
|
389
387
|
const { theme } = useTheme();
|
|
390
388
|
const { fitView } = useReactFlow();
|
|
391
389
|
const viewport = useViewport();
|
|
@@ -2138,7 +2136,6 @@ function Inner({ components, relations, walkthroughs, initialWalkthroughId, onRe
|
|
|
2138
2136
|
component={selected}
|
|
2139
2137
|
onOpenFile={onOpenDeclarationFile}
|
|
2140
2138
|
onRelatedSelect={resolveRelatedComponent}
|
|
2141
|
-
onVerify={onVerifyComponent}
|
|
2142
2139
|
verification={componentVerification}
|
|
2143
2140
|
fileOpen={
|
|
2144
2141
|
drawerTarget?.kind === 'file' &&
|
|
@@ -2367,7 +2364,6 @@ export function SubsystemComponentGraph(props: SubsystemComponentGraphProps) {
|
|
|
2367
2364
|
renderFileViewer={props.renderFileViewer}
|
|
2368
2365
|
renderFileView={props.renderFileView}
|
|
2369
2366
|
onFileSelect={props.onFileSelect}
|
|
2370
|
-
onVerifyComponent={props.onVerifyComponent}
|
|
2371
2367
|
componentVerification={props.componentVerification}
|
|
2372
2368
|
/>
|
|
2373
2369
|
) : (
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import { tokenizeComponent } from './tokenizeComponent';
|
|
3
|
+
import type { SubsystemComponent } from './model';
|
|
4
|
+
|
|
5
|
+
const textOf = (toks: { kind: string; text: string }[]) =>
|
|
6
|
+
toks.filter((t) => t.kind !== 'newline').map((t) => t.text).join('');
|
|
7
|
+
|
|
8
|
+
describe('tokenizeComponent — language inference', () => {
|
|
9
|
+
test('non-JS/TS (python) bypasses Prettier and tokenizes unformatted', async () => {
|
|
10
|
+
const component: SubsystemComponent = {
|
|
11
|
+
alias: 'dedup',
|
|
12
|
+
name: 'deduplicate_entities',
|
|
13
|
+
construct: 'function',
|
|
14
|
+
file: 'graphify/dedup.py',
|
|
15
|
+
purl: 'pkg:github/graphify-labs/graphify#graphify/dedup.py',
|
|
16
|
+
symbol: 'deduplicate_entities',
|
|
17
|
+
declaration: {
|
|
18
|
+
kind: 'function',
|
|
19
|
+
parameters: [{ name: 'graph', type: 'nx.MultiDiGraph' }],
|
|
20
|
+
returnType: 'None',
|
|
21
|
+
callers: [],
|
|
22
|
+
callees: [],
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
const toks = await tokenizeComponent(component, 80, 'pierre-dark');
|
|
26
|
+
// No Prettier run → the raw one-liner comes through verbatim.
|
|
27
|
+
expect(textOf(toks)).toBe(
|
|
28
|
+
'function deduplicate_entities(graph: nx.MultiDiGraph): None;',
|
|
29
|
+
);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('JS/TS still goes through Prettier (wraps to printWidth)', async () => {
|
|
33
|
+
const component: SubsystemComponent = {
|
|
34
|
+
alias: 'merge',
|
|
35
|
+
name: 'mergeSessions',
|
|
36
|
+
construct: 'function',
|
|
37
|
+
file: 'src/session/merge.ts',
|
|
38
|
+
purl: 'pkg:github/acme/widget',
|
|
39
|
+
symbol: 'mergeSessions',
|
|
40
|
+
declaration: {
|
|
41
|
+
kind: 'function',
|
|
42
|
+
parameters: [
|
|
43
|
+
{ name: 'sessions', type: 'SessionRecord[]' },
|
|
44
|
+
{ name: 'options', type: 'MergeOptions' },
|
|
45
|
+
{ name: 'onConflict', type: '((a: SessionRecord, b: SessionRecord) => SessionRecord)' },
|
|
46
|
+
],
|
|
47
|
+
returnType: 'Promise<Map<string, SessionEvent[]>>',
|
|
48
|
+
callers: [],
|
|
49
|
+
callees: [],
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
const toks = await tokenizeComponent(component, 40, 'pierre-dark');
|
|
53
|
+
// Formatted output spans multiple lines.
|
|
54
|
+
expect(toks.some((t) => t.kind === 'newline')).toBe(true);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Tokenize a SubsystemComponent into a flat token stream for the detail panel.
|
|
3
3
|
*
|
|
4
|
-
* Pipeline: generate declaration string → format with Prettier →
|
|
5
|
-
* with Shiki + Pierre themes
|
|
6
|
-
*
|
|
4
|
+
* Pipeline: generate declaration string → (JS/TS only) format with Prettier →
|
|
5
|
+
* tokenize with Shiki + Pierre themes, using the grammar inferred from the
|
|
6
|
+
* component's file. The `component.tokens` field, when present, overrides the
|
|
7
|
+
* entire pipeline (for pre-tokenized data from graphify).
|
|
7
8
|
*
|
|
8
9
|
* This function is async because Prettier's format() is async.
|
|
9
10
|
*/
|
|
10
11
|
|
|
11
12
|
import type { PierreSyntaxThemeName } from '../pierre/pierreSyntaxTheme';
|
|
13
|
+
import {
|
|
14
|
+
isPrettierSourceLang,
|
|
15
|
+
sourceLangForPath,
|
|
16
|
+
} from '../pierre/sourceLang';
|
|
12
17
|
import type { SubsystemComponent, SubsystemDeclToken } from './model';
|
|
13
18
|
import { generateDeclarationString } from './formatDeclaration';
|
|
14
19
|
import { tokenizeFormatted } from './tokenizeFormatted';
|
|
@@ -19,6 +24,8 @@ let prettierPluginsPromise: Promise<{
|
|
|
19
24
|
typescript: typeof import('prettier/plugins/typescript');
|
|
20
25
|
estree: typeof import('prettier/plugins/estree');
|
|
21
26
|
}> | null = null;
|
|
27
|
+
/** One-shot guard so a broken host bundler warns once, not per component. */
|
|
28
|
+
let prettierFailureLogged = false;
|
|
22
29
|
|
|
23
30
|
async function getPrettier() {
|
|
24
31
|
if (!prettierPromise) {
|
|
@@ -38,8 +45,9 @@ async function getPrettier() {
|
|
|
38
45
|
* Tokenize a SubsystemComponent into SubsystemDeclToken[].
|
|
39
46
|
*
|
|
40
47
|
* When `component.tokens` is present (pre-tokenized data from the wire),
|
|
41
|
-
* it's returned as-is. Otherwise the pipeline generates a
|
|
42
|
-
*
|
|
48
|
+
* it's returned as-is. Otherwise the pipeline generates a declaration string,
|
|
49
|
+
* formats it with Prettier when the inferred language is JS/TS, and tokenizes
|
|
50
|
+
* the output with that language's grammar.
|
|
43
51
|
*/
|
|
44
52
|
export async function tokenizeComponent(
|
|
45
53
|
component: SubsystemComponent,
|
|
@@ -49,38 +57,48 @@ export async function tokenizeComponent(
|
|
|
49
57
|
// Pre-tokenized tokens from the wire take precedence.
|
|
50
58
|
if (component.tokens) return component.tokens;
|
|
51
59
|
|
|
52
|
-
|
|
60
|
+
const lang = sourceLangForPath(component.file);
|
|
61
|
+
|
|
62
|
+
// External kind — not valid code, bypass Prettier; plain text.
|
|
53
63
|
const kind = component.declaration?.kind ?? component.construct;
|
|
54
64
|
if (kind === 'external') {
|
|
55
65
|
const label = component.declaration?.kind === 'external' ? component.declaration.label : component.name;
|
|
56
66
|
const escaped = label.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
|
|
57
|
-
return tokenizeFormatted(`external '${escaped}'`, themeName);
|
|
67
|
+
return tokenizeFormatted(`external '${escaped}'`, themeName, 'text');
|
|
58
68
|
}
|
|
59
69
|
|
|
60
|
-
// Custom entity — an actor (Person/agent/queue), not code;
|
|
70
|
+
// Custom entity — an actor (Person/agent/queue), not code; plain text.
|
|
61
71
|
if (kind === 'custom_entity') {
|
|
62
|
-
return tokenizeFormatted(generateDeclarationString(component), themeName);
|
|
72
|
+
return tokenizeFormatted(generateDeclarationString(component), themeName, 'text');
|
|
63
73
|
}
|
|
64
74
|
|
|
65
|
-
// Generate → format → tokenize.
|
|
66
|
-
//
|
|
67
|
-
//
|
|
68
|
-
//
|
|
75
|
+
// Generate → (JS/TS only) format → tokenize.
|
|
76
|
+
//
|
|
77
|
+
// Prettier ships parsers for JS/TS (and CSS/HTML/…) only — never run it for
|
|
78
|
+
// other languages. Even for JS/TS it's best-effort: some host bundlers
|
|
79
|
+
// (Electrobun's) mangle Prettier's internal cross-module calls, so formatting
|
|
80
|
+
// can throw. Fall back to the unformatted declaration rather than rendering
|
|
81
|
+
// nothing — the panel is still correct, just not width-wrapped.
|
|
69
82
|
const raw = generateDeclarationString(component);
|
|
70
83
|
let formatted = raw;
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
84
|
+
if (isPrettierSourceLang(lang)) {
|
|
85
|
+
try {
|
|
86
|
+
const { prettier, plugins } = await getPrettier();
|
|
87
|
+
formatted = await prettier.format(raw, {
|
|
88
|
+
parser: 'typescript',
|
|
89
|
+
plugins: [plugins.typescript, plugins.estree],
|
|
90
|
+
printWidth,
|
|
91
|
+
});
|
|
92
|
+
} catch (err) {
|
|
93
|
+
if (!prettierFailureLogged) {
|
|
94
|
+
prettierFailureLogged = true;
|
|
95
|
+
console.warn(
|
|
96
|
+
'[subsystem] prettier format failed; tokenizing unformatted declarations for this session',
|
|
97
|
+
err,
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
83
101
|
}
|
|
84
102
|
|
|
85
|
-
return tokenizeFormatted(formatted, themeName);
|
|
103
|
+
return tokenizeFormatted(formatted, themeName, lang);
|
|
86
104
|
}
|
|
@@ -1,19 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Tokenize a formatted
|
|
3
|
-
*
|
|
2
|
+
* Tokenize a formatted source string into SubsystemDeclToken[] using Shiki +
|
|
3
|
+
* Pierre themes (same highlighter stack as @pierre/diffs).
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { tokenizeWithPierreSyntax } from '../pierre/syntaxTokens';
|
|
7
7
|
import type { PierreSyntaxThemeName } from '../pierre/pierreSyntaxTheme';
|
|
8
|
+
import type { SourceSyntaxLang } from '../pierre/sourceLang';
|
|
8
9
|
import type { SubsystemDeclToken } from './model';
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
|
-
* Tokenize a
|
|
12
|
-
*
|
|
12
|
+
* Tokenize a source string into SubsystemDeclToken[]. The input should already
|
|
13
|
+
* be formatted (or be valid for the language). `lang` picks the grammar.
|
|
13
14
|
*/
|
|
14
15
|
export function tokenizeFormatted(
|
|
15
16
|
code: string,
|
|
16
17
|
themeName: PierreSyntaxThemeName = 'pierre-dark',
|
|
18
|
+
lang: SourceSyntaxLang = 'typescript',
|
|
17
19
|
): Promise<SubsystemDeclToken[]> {
|
|
18
|
-
return tokenizeWithPierreSyntax(code, themeName);
|
|
20
|
+
return tokenizeWithPierreSyntax(code, themeName, lang);
|
|
19
21
|
}
|