@principal-ai/subsystems-react 0.23.1 → 0.23.3
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/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.map +1 -1
- package/dist/subsystem/SubsystemComponentGraph.js +8 -1
- 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 +1 -1
- package/src/index.ts +2 -0
- package/src/pierre/constructColors.test.ts +2 -2
- package/src/pierre/constructColors.ts +41 -23
- package/src/stories/Subsystem/ComponentGraph/Packages.stories.tsx +242 -0
- package/src/subsystem/SubsystemComponentGraph.tsx +6 -1
- 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
|
+
};
|
|
@@ -1322,7 +1322,12 @@ function Inner({ components, relations, walkthroughs, onSelect, onEdgeSelect, me
|
|
|
1322
1322
|
// the actual edge path (not node-center approximations).
|
|
1323
1323
|
const edgeLabels = useMemo(() => {
|
|
1324
1324
|
return dispEdges
|
|
1325
|
-
.filter((e) =>
|
|
1325
|
+
.filter((e) => {
|
|
1326
|
+
if (e.hidden) return false;
|
|
1327
|
+
const d = e?.data as { dimmed?: boolean } | undefined;
|
|
1328
|
+
if (d?.dimmed) return false;
|
|
1329
|
+
return true;
|
|
1330
|
+
})
|
|
1326
1331
|
.map((e) => {
|
|
1327
1332
|
const d = e.data as {
|
|
1328
1333
|
mechanism?: string;
|
|
@@ -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 = {
|