@principal-ai/subsystems-react 0.23.0 → 0.23.2
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 +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/pierre/PierreWalkthroughCodeView.d.ts +2 -1
- package/dist/pierre/PierreWalkthroughCodeView.d.ts.map +1 -1
- package/dist/pierre/PierreWalkthroughCodeView.js +4 -4
- package/dist/pierre/PierreWalkthroughCodeView.js.map +1 -1
- package/dist/pierre/constructColors.d.ts +2 -3
- package/dist/pierre/constructColors.d.ts.map +1 -1
- package/dist/pierre/constructColors.js +31 -21
- package/dist/pierre/constructColors.js.map +1 -1
- package/dist/subsystem/SubsystemComponentGraph.d.ts +1 -1
- package/dist/subsystem/SubsystemComponentGraph.d.ts.map +1 -1
- package/dist/subsystem/SubsystemComponentGraph.js +8 -6
- package/dist/subsystem/SubsystemComponentGraph.js.map +1 -1
- package/dist/subsystem/model.d.ts +69 -23
- package/dist/subsystem/model.d.ts.map +1 -1
- package/dist/subsystem/model.js +184 -40
- package/dist/subsystem/model.js.map +1 -1
- package/dist/subsystem/nodes.d.ts.map +1 -1
- package/dist/subsystem/nodes.js +53 -25
- package/dist/subsystem/nodes.js.map +1 -1
- package/package.json +3 -3
- package/src/index.ts +2 -0
- package/src/pierre/PierreWalkthroughCodeView.tsx +12 -5
- package/src/pierre/constructColors.test.ts +2 -2
- package/src/pierre/constructColors.ts +41 -23
- package/src/stories/Subsystem/ComponentGraph/DetailPanel.stories.tsx +2 -1
- package/src/stories/Subsystem/ComponentGraph/Flows.stories.tsx +1 -0
- package/src/stories/Subsystem/ComponentGraph/Packages.stories.tsx +242 -0
- package/src/subsystem/SubsystemComponentGraph.tsx +23 -7
- package/src/subsystem/model.test.ts +167 -0
- package/src/subsystem/model.ts +236 -46
- package/src/subsystem/nodes.tsx +55 -26
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import '@xyflow/react/dist/style.css';
|
|
2
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
3
|
+
import { ThemeProvider, defaultEditorTheme } from '@principal-ade/industry-theme';
|
|
4
|
+
import { SubsystemComponentGraph } from '../../../subsystem/SubsystemComponentGraph';
|
|
5
|
+
import type { SubsystemComponent } from '../../../subsystem/model';
|
|
6
|
+
import { graphSpecFromEdges } from './fixtures';
|
|
7
|
+
|
|
8
|
+
const meta = {
|
|
9
|
+
title: 'Subsystem/ComponentGraph/Packages',
|
|
10
|
+
component: SubsystemComponentGraph,
|
|
11
|
+
parameters: { layout: 'fullscreen' },
|
|
12
|
+
tags: ['autodocs'],
|
|
13
|
+
decorators: [
|
|
14
|
+
(Story) => (
|
|
15
|
+
<ThemeProvider theme={defaultEditorTheme}>
|
|
16
|
+
<Story />
|
|
17
|
+
</ThemeProvider>
|
|
18
|
+
),
|
|
19
|
+
],
|
|
20
|
+
} satisfies Meta<typeof SubsystemComponentGraph>;
|
|
21
|
+
|
|
22
|
+
export default meta;
|
|
23
|
+
type Story = StoryObj<typeof meta>;
|
|
24
|
+
|
|
25
|
+
const APP = 'pkg:github/acme/app';
|
|
26
|
+
const LIB = 'pkg:github/acme/lib';
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Package frames are derived from `purl` when a graph spans multiple repos.
|
|
30
|
+
* No separate `package` field — single-repo graphs stay unframed at this level.
|
|
31
|
+
*/
|
|
32
|
+
const multiRepoComponents: SubsystemComponent[] = [
|
|
33
|
+
{
|
|
34
|
+
id: 'dispatch',
|
|
35
|
+
name: 'dispatch',
|
|
36
|
+
construct: 'function',
|
|
37
|
+
symbol: 'dispatch',
|
|
38
|
+
file: 'src/host/dispatch.ts',
|
|
39
|
+
module: 'src/host/dispatch.ts',
|
|
40
|
+
process: 'app/host',
|
|
41
|
+
purl: APP,
|
|
42
|
+
purpose: 'host entry',
|
|
43
|
+
layer: 1,
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
id: 'session',
|
|
47
|
+
name: 'SessionService',
|
|
48
|
+
construct: 'class',
|
|
49
|
+
symbol: 'SessionService',
|
|
50
|
+
file: 'src/host/session.ts',
|
|
51
|
+
module: 'src/host/session.ts',
|
|
52
|
+
process: 'app/host',
|
|
53
|
+
purl: APP,
|
|
54
|
+
purpose: 'session state',
|
|
55
|
+
layer: 2,
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
id: 'parse',
|
|
59
|
+
name: 'parseEvent',
|
|
60
|
+
construct: 'function',
|
|
61
|
+
symbol: 'parseEvent',
|
|
62
|
+
file: 'src/parse.ts',
|
|
63
|
+
module: 'src/parse.ts',
|
|
64
|
+
process: 'lib/worker',
|
|
65
|
+
purl: LIB,
|
|
66
|
+
purpose: 'shared parser',
|
|
67
|
+
layer: 1,
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
id: 'normalize',
|
|
71
|
+
name: 'normalize',
|
|
72
|
+
construct: 'function',
|
|
73
|
+
symbol: 'normalize',
|
|
74
|
+
file: 'src/normalize.ts',
|
|
75
|
+
module: 'src/normalize.ts',
|
|
76
|
+
process: 'lib/worker',
|
|
77
|
+
purl: LIB,
|
|
78
|
+
purpose: 'normalize payloads',
|
|
79
|
+
layer: 2,
|
|
80
|
+
},
|
|
81
|
+
];
|
|
82
|
+
|
|
83
|
+
const multiRepoEdges = graphSpecFromEdges([
|
|
84
|
+
['dispatch', 'session', 'calls'],
|
|
85
|
+
['dispatch', 'parse', 'calls'],
|
|
86
|
+
['parse', 'normalize', 'calls'],
|
|
87
|
+
]);
|
|
88
|
+
|
|
89
|
+
export const MultiRepoPackageFrames: Story = {
|
|
90
|
+
name: 'Multi-repo package frames',
|
|
91
|
+
args: {
|
|
92
|
+
components: multiRepoComponents,
|
|
93
|
+
relations: multiRepoEdges.relations,
|
|
94
|
+
walkthroughs: multiRepoEdges.walkthroughs,
|
|
95
|
+
},
|
|
96
|
+
render: (args) => (
|
|
97
|
+
<div style={{ width: '100vw', height: '100vh' }}>
|
|
98
|
+
<SubsystemComponentGraph {...args} />
|
|
99
|
+
</div>
|
|
100
|
+
),
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
/** Same components, one purl — no package frames (process/module only). */
|
|
104
|
+
const singleRepo = multiRepoComponents.map((c) => ({ ...c, purl: APP }));
|
|
105
|
+
|
|
106
|
+
export const SingleRepoNoPackageFrames: Story = {
|
|
107
|
+
name: 'Single-repo (no package frames)',
|
|
108
|
+
args: {
|
|
109
|
+
components: singleRepo,
|
|
110
|
+
relations: multiRepoEdges.relations,
|
|
111
|
+
walkthroughs: multiRepoEdges.walkthroughs,
|
|
112
|
+
},
|
|
113
|
+
render: (args) => (
|
|
114
|
+
<div style={{ width: '100vw', height: '100vh' }}>
|
|
115
|
+
<SubsystemComponentGraph {...args} />
|
|
116
|
+
</div>
|
|
117
|
+
),
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Full nest: package → process → module → leaves.
|
|
122
|
+
* Each module path has 2+ exports so module frames survive the singleton rule.
|
|
123
|
+
*/
|
|
124
|
+
const nestedComponents: SubsystemComponent[] = [
|
|
125
|
+
{
|
|
126
|
+
id: 'dispatch',
|
|
127
|
+
name: 'dispatch',
|
|
128
|
+
construct: 'function',
|
|
129
|
+
symbol: 'dispatch',
|
|
130
|
+
file: 'src/host/main.ts',
|
|
131
|
+
module: 'src/host/main.ts',
|
|
132
|
+
process: 'app/host',
|
|
133
|
+
purl: APP,
|
|
134
|
+
purpose: 'host entry',
|
|
135
|
+
layer: 1,
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
id: 'boot',
|
|
139
|
+
name: 'boot',
|
|
140
|
+
construct: 'function',
|
|
141
|
+
symbol: 'boot',
|
|
142
|
+
file: 'src/host/main.ts',
|
|
143
|
+
module: 'src/host/main.ts',
|
|
144
|
+
process: 'app/host',
|
|
145
|
+
purl: APP,
|
|
146
|
+
purpose: 'host bootstrap',
|
|
147
|
+
layer: 1,
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
id: 'session',
|
|
151
|
+
name: 'SessionService',
|
|
152
|
+
construct: 'class',
|
|
153
|
+
symbol: 'SessionService',
|
|
154
|
+
file: 'src/host/session.ts',
|
|
155
|
+
module: 'src/host/session.ts',
|
|
156
|
+
process: 'app/host',
|
|
157
|
+
purl: APP,
|
|
158
|
+
purpose: 'session state',
|
|
159
|
+
layer: 2,
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
id: 'session-types',
|
|
163
|
+
name: 'SessionRecord',
|
|
164
|
+
construct: 'type_alias',
|
|
165
|
+
symbol: 'SessionRecord',
|
|
166
|
+
file: 'src/host/session.ts',
|
|
167
|
+
module: 'src/host/session.ts',
|
|
168
|
+
process: 'app/host',
|
|
169
|
+
purl: APP,
|
|
170
|
+
purpose: 'session record shape',
|
|
171
|
+
layer: 2,
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
id: 'parse',
|
|
175
|
+
name: 'parseEvent',
|
|
176
|
+
construct: 'function',
|
|
177
|
+
symbol: 'parseEvent',
|
|
178
|
+
file: 'src/parse.ts',
|
|
179
|
+
module: 'src/parse.ts',
|
|
180
|
+
process: 'lib/worker',
|
|
181
|
+
purl: LIB,
|
|
182
|
+
purpose: 'shared parser',
|
|
183
|
+
layer: 1,
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
id: 'parse-types',
|
|
187
|
+
name: 'RawEvent',
|
|
188
|
+
construct: 'type_alias',
|
|
189
|
+
symbol: 'RawEvent',
|
|
190
|
+
file: 'src/parse.ts',
|
|
191
|
+
module: 'src/parse.ts',
|
|
192
|
+
process: 'lib/worker',
|
|
193
|
+
purl: LIB,
|
|
194
|
+
purpose: 'raw event shape',
|
|
195
|
+
layer: 1,
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
id: 'normalize',
|
|
199
|
+
name: 'normalize',
|
|
200
|
+
construct: 'function',
|
|
201
|
+
symbol: 'normalize',
|
|
202
|
+
file: 'src/normalize.ts',
|
|
203
|
+
module: 'src/normalize.ts',
|
|
204
|
+
process: 'lib/worker',
|
|
205
|
+
purl: LIB,
|
|
206
|
+
purpose: 'normalize payloads',
|
|
207
|
+
layer: 2,
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
id: 'normalize-types',
|
|
211
|
+
name: 'NormalizedEvent',
|
|
212
|
+
construct: 'type_alias',
|
|
213
|
+
symbol: 'NormalizedEvent',
|
|
214
|
+
file: 'src/normalize.ts',
|
|
215
|
+
module: 'src/normalize.ts',
|
|
216
|
+
process: 'lib/worker',
|
|
217
|
+
purl: LIB,
|
|
218
|
+
purpose: 'normalized event shape',
|
|
219
|
+
layer: 2,
|
|
220
|
+
},
|
|
221
|
+
];
|
|
222
|
+
|
|
223
|
+
const nestedEdges = graphSpecFromEdges([
|
|
224
|
+
['boot', 'dispatch', 'calls'],
|
|
225
|
+
['dispatch', 'session', 'calls'],
|
|
226
|
+
['dispatch', 'parse', 'calls'],
|
|
227
|
+
['parse', 'normalize', 'calls'],
|
|
228
|
+
]);
|
|
229
|
+
|
|
230
|
+
export const PackageProcessModuleNesting: Story = {
|
|
231
|
+
name: 'Package → process → module nesting',
|
|
232
|
+
args: {
|
|
233
|
+
components: nestedComponents,
|
|
234
|
+
relations: nestedEdges.relations,
|
|
235
|
+
walkthroughs: nestedEdges.walkthroughs,
|
|
236
|
+
},
|
|
237
|
+
render: (args) => (
|
|
238
|
+
<div style={{ width: '100vw', height: '100vh' }}>
|
|
239
|
+
<SubsystemComponentGraph {...args} />
|
|
240
|
+
</div>
|
|
241
|
+
),
|
|
242
|
+
};
|
|
@@ -70,7 +70,7 @@ export interface WalkthroughViewerContext {
|
|
|
70
70
|
/** Focused step index; `null` means the whole flow (no specific step). */
|
|
71
71
|
stepIndex: number | null;
|
|
72
72
|
/** Open a step's full source file over the walkthrough drawer (keeps snippets mounted). */
|
|
73
|
-
onOpenFile: (path: string) => void;
|
|
73
|
+
onOpenFile: (path: string, opts?: SubsystemOpenFileOptions) => void;
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
type DrawerTarget =
|
|
@@ -192,12 +192,23 @@ const FileDrawerContent = memo(function FileDrawerContent({
|
|
|
192
192
|
render,
|
|
193
193
|
file,
|
|
194
194
|
startLine,
|
|
195
|
+
fullFile,
|
|
195
196
|
}: {
|
|
196
197
|
render: (file: string, opts?: SubsystemOpenFileOptions) => ReactNode;
|
|
197
198
|
file: string;
|
|
198
199
|
startLine?: number;
|
|
200
|
+
fullFile?: boolean;
|
|
199
201
|
}) {
|
|
200
|
-
return
|
|
202
|
+
return (
|
|
203
|
+
<>
|
|
204
|
+
{render(
|
|
205
|
+
file,
|
|
206
|
+
startLine != null
|
|
207
|
+
? { startLine, ...(fullFile ? { fullFile: true } : {}) }
|
|
208
|
+
: undefined,
|
|
209
|
+
)}
|
|
210
|
+
</>
|
|
211
|
+
);
|
|
201
212
|
});
|
|
202
213
|
|
|
203
214
|
function FileOverlayCloseButton({ onClose }: { onClose: () => void }) {
|
|
@@ -240,7 +251,7 @@ const WalkthroughDrawerContent = memo(function WalkthroughDrawerContent({
|
|
|
240
251
|
render: (ctx: WalkthroughViewerContext) => ReactNode;
|
|
241
252
|
walkthrough: SubsystemWalkthrough;
|
|
242
253
|
stepIndex: number | null;
|
|
243
|
-
onOpenFile: (path: string) => void;
|
|
254
|
+
onOpenFile: (path: string, opts?: SubsystemOpenFileOptions) => void;
|
|
244
255
|
}) {
|
|
245
256
|
return <>{render({ walkthrough, stepIndex, onOpenFile })}</>;
|
|
246
257
|
});
|
|
@@ -266,7 +277,10 @@ function Inner({ components, relations, walkthroughs, onSelect, onEdgeSelect, me
|
|
|
266
277
|
/** Bottom drawer: single file or walkthrough multi-snippet mode. */
|
|
267
278
|
const [drawerTarget, setDrawerTarget] = useState<DrawerTarget | null>(null);
|
|
268
279
|
/** Full-file layer over an open walkthrough drawer — walkthrough stays mounted. */
|
|
269
|
-
const [fileOverlay, setFileOverlay] = useState<{
|
|
280
|
+
const [fileOverlay, setFileOverlay] = useState<{
|
|
281
|
+
file: string;
|
|
282
|
+
startLine?: number;
|
|
283
|
+
} | null>(null);
|
|
270
284
|
// Mirror so step-focus can decide whether to wait for the drawer open
|
|
271
285
|
// transition before fitView (avoids framing against full-height canvas).
|
|
272
286
|
const drawerOpenRef = useRef(false);
|
|
@@ -356,7 +370,7 @@ function Inner({ components, relations, walkthroughs, onSelect, onEdgeSelect, me
|
|
|
356
370
|
fileOverlay?.file ??
|
|
357
371
|
(drawerTarget?.kind === 'file' ? drawerTarget.file : null);
|
|
358
372
|
openFileRef.current = fileOverlay
|
|
359
|
-
? { file: fileOverlay.file }
|
|
373
|
+
? { file: fileOverlay.file, startLine: fileOverlay.startLine }
|
|
360
374
|
: drawerTarget?.kind === 'file'
|
|
361
375
|
? { file: drawerTarget.file, startLine: drawerTarget.startLine }
|
|
362
376
|
: null;
|
|
@@ -932,8 +946,8 @@ function Inner({ components, relations, walkthroughs, onSelect, onEdgeSelect, me
|
|
|
932
946
|
);
|
|
933
947
|
|
|
934
948
|
const onOpenFileFromWalkthrough = useCallback(
|
|
935
|
-
(file: string) => {
|
|
936
|
-
setFileOverlay({ file });
|
|
949
|
+
(file: string, opts?: SubsystemOpenFileOptions) => {
|
|
950
|
+
setFileOverlay({ file, startLine: opts?.startLine });
|
|
937
951
|
onFileSelect?.(file);
|
|
938
952
|
},
|
|
939
953
|
[onFileSelect],
|
|
@@ -1955,6 +1969,8 @@ function Inner({ components, relations, walkthroughs, onSelect, onEdgeSelect, me
|
|
|
1955
1969
|
<FileDrawerContent
|
|
1956
1970
|
render={renderDrawerContent}
|
|
1957
1971
|
file={fileOverlay.file}
|
|
1972
|
+
startLine={fileOverlay.startLine}
|
|
1973
|
+
fullFile
|
|
1958
1974
|
/>
|
|
1959
1975
|
</div>
|
|
1960
1976
|
</div>
|
|
@@ -5,11 +5,17 @@ import {
|
|
|
5
5
|
convertSubsystemToGroups,
|
|
6
6
|
getSubsystemRegions,
|
|
7
7
|
getSubsystemModuleRegions,
|
|
8
|
+
getSubsystemPackageRegions,
|
|
8
9
|
processGroupNodeId,
|
|
9
10
|
moduleGroupNodeId,
|
|
11
|
+
packageGroupNodeId,
|
|
12
|
+
buildBoundaryLayoutGroups,
|
|
10
13
|
buildSubsystemGraph,
|
|
14
|
+
componentPackageKey,
|
|
11
15
|
deriveNameFromSymbol,
|
|
12
16
|
constructBadgeLabel,
|
|
17
|
+
constructBadgeColor,
|
|
18
|
+
FRAMEWORK_BADGE_COLOR,
|
|
13
19
|
rightBadgeLabel,
|
|
14
20
|
nodeMinWidthForBadges,
|
|
15
21
|
formatPurl,
|
|
@@ -148,6 +154,15 @@ describe('subsystem graph model', () => {
|
|
|
148
154
|
).toBe('custom_entity');
|
|
149
155
|
});
|
|
150
156
|
|
|
157
|
+
test('constructBadgeColor uses React brand only for framework stereotype badges', () => {
|
|
158
|
+
expect(
|
|
159
|
+
constructBadgeColor({ framework: 'react', stereotype: 'component' }),
|
|
160
|
+
).toBe(FRAMEWORK_BADGE_COLOR.react);
|
|
161
|
+
expect(constructBadgeColor({ framework: 'react' })).toBeNull();
|
|
162
|
+
expect(constructBadgeColor({ stereotype: 'hook' })).toBeNull();
|
|
163
|
+
expect(constructBadgeColor({ framework: 'vue', stereotype: 'component' })).toBeNull();
|
|
164
|
+
});
|
|
165
|
+
|
|
151
166
|
test('nodeMinWidthForBadges widens for long construct badges and role pairs', () => {
|
|
152
167
|
const plain = nodeMinWidthForBadges({ construct: 'function' });
|
|
153
168
|
expect(plain).toBe(150);
|
|
@@ -445,6 +460,158 @@ describe('subsystem graph model', () => {
|
|
|
445
460
|
expect(nodes.find((n) => n.id === moduleGroupNodeId('src/session/transcript.ts'))).toBeDefined();
|
|
446
461
|
});
|
|
447
462
|
|
|
463
|
+
test('getSubsystemPackageRegions groups by purl repo key, skipping externals', () => {
|
|
464
|
+
const regions = getSubsystemPackageRegions({
|
|
465
|
+
components: [
|
|
466
|
+
{ id: 'a', name: 'a', construct: 'function', file: 'a.ts', purl: 'pkg:github/acme/app#a.ts' },
|
|
467
|
+
{ id: 'b', name: 'b', construct: 'function', file: 'b.ts', purl: 'pkg:github/acme/app' },
|
|
468
|
+
{ id: 'c', name: 'c', construct: 'function', file: 'c.ts', purl: 'pkg:github/other/lib' },
|
|
469
|
+
{ id: 'ext', name: 'Stripe', construct: 'external', file: '', purl: 'pkg:npm/stripe' },
|
|
470
|
+
],
|
|
471
|
+
});
|
|
472
|
+
expect(regions.map((r) => r.key).sort()).toEqual([
|
|
473
|
+
'pkg:github/acme/app',
|
|
474
|
+
'pkg:github/other/lib',
|
|
475
|
+
].sort());
|
|
476
|
+
expect(regions.every((r) => r.kind === 'package')).toBe(true);
|
|
477
|
+
expect(componentPackageKey({ construct: 'external', purl: 'pkg:npm/stripe' })).toBeUndefined();
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
test('buildBoundaryLayoutGroups skips package frames for single-repo graphs', () => {
|
|
481
|
+
const groups = buildBoundaryLayoutGroups({
|
|
482
|
+
components: [
|
|
483
|
+
{ id: 'a', name: 'a', construct: 'function', file: 'a.ts', purl: 'pkg:github/acme/app' },
|
|
484
|
+
{ id: 'b', name: 'b', construct: 'function', file: 'b.ts', purl: 'pkg:github/acme/app' },
|
|
485
|
+
],
|
|
486
|
+
});
|
|
487
|
+
expect(groups.filter((g) => g.region.kind === 'package')).toHaveLength(0);
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
test('buildBoundaryLayoutGroups frames packages when multi-repo', () => {
|
|
491
|
+
const groups = buildBoundaryLayoutGroups({
|
|
492
|
+
components: [
|
|
493
|
+
{ id: 'a', name: 'a', construct: 'function', file: 'a.ts', purl: 'pkg:github/acme/app' },
|
|
494
|
+
{ id: 'b', name: 'b', construct: 'function', file: 'b.ts', purl: 'pkg:github/acme/app' },
|
|
495
|
+
{ id: 'c', name: 'c', construct: 'function', file: 'c.ts', purl: 'pkg:github/other/lib' },
|
|
496
|
+
{ id: 'd', name: 'd', construct: 'function', file: 'd.ts', purl: 'pkg:github/other/lib' },
|
|
497
|
+
],
|
|
498
|
+
});
|
|
499
|
+
const pkgs = groups.filter((g) => g.region.kind === 'package');
|
|
500
|
+
expect(pkgs.map((g) => g.region.key).sort()).toEqual([
|
|
501
|
+
'pkg:github/acme/app',
|
|
502
|
+
'pkg:github/other/lib',
|
|
503
|
+
].sort());
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
test('buildBoundaryLayoutGroups nests process under package', () => {
|
|
507
|
+
const groups = buildBoundaryLayoutGroups({
|
|
508
|
+
components: [
|
|
509
|
+
{
|
|
510
|
+
id: 'a',
|
|
511
|
+
name: 'a',
|
|
512
|
+
construct: 'function',
|
|
513
|
+
file: 'a.ts',
|
|
514
|
+
purl: 'pkg:github/acme/app',
|
|
515
|
+
process: 'app/host',
|
|
516
|
+
},
|
|
517
|
+
{
|
|
518
|
+
id: 'b',
|
|
519
|
+
name: 'b',
|
|
520
|
+
construct: 'function',
|
|
521
|
+
file: 'b.ts',
|
|
522
|
+
purl: 'pkg:github/acme/app',
|
|
523
|
+
process: 'app/host',
|
|
524
|
+
},
|
|
525
|
+
{
|
|
526
|
+
id: 'c',
|
|
527
|
+
name: 'c',
|
|
528
|
+
construct: 'function',
|
|
529
|
+
file: 'c.ts',
|
|
530
|
+
purl: 'pkg:github/other/lib',
|
|
531
|
+
process: 'lib/worker',
|
|
532
|
+
},
|
|
533
|
+
{
|
|
534
|
+
id: 'd',
|
|
535
|
+
name: 'd',
|
|
536
|
+
construct: 'function',
|
|
537
|
+
file: 'd.ts',
|
|
538
|
+
purl: 'pkg:github/other/lib',
|
|
539
|
+
process: 'lib/worker',
|
|
540
|
+
},
|
|
541
|
+
],
|
|
542
|
+
});
|
|
543
|
+
const host = groups.find((g) => g.id === processGroupNodeId('app/host'));
|
|
544
|
+
expect(host?.parentId).toBe(packageGroupNodeId('pkg:github/acme/app'));
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
test('buildSubsystemGraph nests package → process → module', async () => {
|
|
548
|
+
const { nodes } = await buildSubsystemGraph({
|
|
549
|
+
components: [
|
|
550
|
+
{
|
|
551
|
+
id: 'a1',
|
|
552
|
+
name: 'a1',
|
|
553
|
+
construct: 'function',
|
|
554
|
+
file: 'a.ts',
|
|
555
|
+
purl: 'pkg:github/acme/app',
|
|
556
|
+
process: 'app/host',
|
|
557
|
+
module: 'src/a.ts',
|
|
558
|
+
},
|
|
559
|
+
{
|
|
560
|
+
id: 'a2',
|
|
561
|
+
name: 'a2',
|
|
562
|
+
construct: 'function',
|
|
563
|
+
file: 'a.ts',
|
|
564
|
+
purl: 'pkg:github/acme/app',
|
|
565
|
+
process: 'app/host',
|
|
566
|
+
module: 'src/a.ts',
|
|
567
|
+
},
|
|
568
|
+
{
|
|
569
|
+
id: 'b1',
|
|
570
|
+
name: 'b1',
|
|
571
|
+
construct: 'function',
|
|
572
|
+
file: 'b.ts',
|
|
573
|
+
purl: 'pkg:github/other/lib',
|
|
574
|
+
process: 'lib/worker',
|
|
575
|
+
module: 'src/b.ts',
|
|
576
|
+
},
|
|
577
|
+
{
|
|
578
|
+
id: 'b2',
|
|
579
|
+
name: 'b2',
|
|
580
|
+
construct: 'function',
|
|
581
|
+
file: 'b.ts',
|
|
582
|
+
purl: 'pkg:github/other/lib',
|
|
583
|
+
process: 'lib/worker',
|
|
584
|
+
module: 'src/b.ts',
|
|
585
|
+
},
|
|
586
|
+
],
|
|
587
|
+
relations: [],
|
|
588
|
+
});
|
|
589
|
+
const pkgA = nodes.find((n) => n.id === packageGroupNodeId('pkg:github/acme/app'));
|
|
590
|
+
const proc = nodes.find((n) => n.id === processGroupNodeId('app/host'));
|
|
591
|
+
const mod = nodes.find((n) => n.id === moduleGroupNodeId('src/a.ts'));
|
|
592
|
+
expect(pkgA).toBeDefined();
|
|
593
|
+
expect((proc as { parentId?: string }).parentId).toBe(
|
|
594
|
+
packageGroupNodeId('pkg:github/acme/app'),
|
|
595
|
+
);
|
|
596
|
+
expect((mod as { parentId?: string }).parentId).toBe(processGroupNodeId('app/host'));
|
|
597
|
+
expect((nodes.find((n) => n.id === 'a1') as { parentId?: string }).parentId).toBe(
|
|
598
|
+
moduleGroupNodeId('src/a.ts'),
|
|
599
|
+
);
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
test('packageFrames always draws single-repo package frames when multi-member', () => {
|
|
603
|
+
const groups = buildBoundaryLayoutGroups(
|
|
604
|
+
{
|
|
605
|
+
components: [
|
|
606
|
+
{ id: 'a', name: 'a', construct: 'function', file: 'a.ts', purl: 'pkg:github/acme/app' },
|
|
607
|
+
{ id: 'b', name: 'b', construct: 'function', file: 'b.ts', purl: 'pkg:github/acme/app' },
|
|
608
|
+
],
|
|
609
|
+
},
|
|
610
|
+
{ packageFrames: 'always' },
|
|
611
|
+
);
|
|
612
|
+
expect(groups.filter((g) => g.region.kind === 'package')).toHaveLength(1);
|
|
613
|
+
});
|
|
614
|
+
|
|
448
615
|
test('subsystemGraphLayoutKey ignores declarationRef-only changes', () => {
|
|
449
616
|
const base = doc;
|
|
450
617
|
const withRef = {
|