Skip to content

Commit 91d893e

Browse files
committed
Allow fqn names when adding packages
1 parent a8bad33 commit 91d893e

9 files changed

Lines changed: 318 additions & 105 deletions

File tree

src/backend/generation.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,11 @@ function isClass(codeUnit: FakeClass | FakePackage): codeUnit is FakeClass {
2929

3030
export function getClassFqn(fakeClass: FakeClass): string {
3131
let fqn: string = fakeClass.identifier;
32-
let obj: FakeClass | FakePackage = fakeClass;
33-
while (obj.parent !== undefined) {
34-
fqn = obj.parent.name + '.' + fqn;
35-
obj = obj.parent;
32+
let obj: FakeClass | FakePackage | undefined = fakeClass;
33+
while (obj && obj.parent !== undefined) {
34+
const parent: FakePackage = obj.parent;
35+
fqn = parent.name + '.' + fqn;
36+
obj = parent;
3637
}
3738
return fqn;
3839
}
@@ -262,7 +263,7 @@ function generateFakeApp(params: AppGenerationParameters, nameGenerator: NameGen
262263

263264
const fakeApp: FakeApp = {
264265
name: appName,
265-
rootPackage: rootPackage1,
266+
rootPackages: [rootPackage1],
266267
entryPoint: entryPoint,
267268
classes: classes,
268269
packages: packages,

src/backend/shared/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ export interface FakePackage {
2323

2424
export interface FakeApp {
2525
name: string;
26-
rootPackage: FakePackage;
26+
rootPackages: Array<FakePackage>;
2727
entryPoint: FakeClass;
2828
classes: Array<FakeClass>;
2929
packages: Array<FakePackage>;

src/backend/utils.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,20 @@ export function appTreeToString(app: FakeApp): string {
184184
return result.slice(0, -1); // Remove newline
185185
}
186186

187-
return packageTreeToString(app.rootPackage);
187+
// Handle multiple root packages
188+
if (app.rootPackages.length === 0) {
189+
return '';
190+
}
191+
192+
let result = '';
193+
app.rootPackages.forEach((rootPkg, idx) => {
194+
if (idx > 0) {
195+
result += '\n';
196+
}
197+
result += packageTreeToString(rootPkg);
198+
});
199+
200+
return result;
188201
}
189202

190203
/**

src/backend/utils/landscape.utils.ts

Lines changed: 25 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export function cleanLandscapeForSerialization(landscape: Array<FakeApp>): Clean
1212
const entryPointFqn = getClassFqn(app.entryPoint);
1313
return {
1414
name: app.name,
15-
rootPackages: [cleanPackage(app.rootPackage)],
15+
rootPackages: app.rootPackages.map(cleanPackage),
1616
entryPointFqn,
1717
classes: app.classes.map(cleanClass),
1818
packages: app.packages.map(cleanPackage),
@@ -37,59 +37,16 @@ function cleanClass(cls: FakeClass): CleanedClass {
3737
};
3838
}
3939

40-
/**
41-
* Find class by FQN in an app
42-
*/
43-
function findClassByFqn(app: FakeApp, fqn: string): FakeClass | null {
44-
function searchInPackage(pkg: FakePackage): FakeClass | null {
45-
for (const cls of pkg.classes) {
46-
const clsFqn = getClassFqn(cls);
47-
if (clsFqn === fqn) {
48-
return cls;
49-
}
50-
}
51-
for (const subPkg of pkg.subpackages) {
52-
const found = searchInPackage(subPkg);
53-
if (found) return found;
54-
}
55-
return null;
56-
}
57-
58-
return searchInPackage(app.rootPackage);
59-
}
60-
6140
/**
6241
* Reconstruct parent references after deserialization
6342
*/
6443
export function reconstructParentReferences(landscapeData: Array<any>): Array<FakeApp> {
6544
return landscapeData.map((appData: any) => {
6645
const rootPackages: FakePackage[] = appData.rootPackages || [];
6746

68-
// For FakeApp, we still use a single rootPackage
69-
// If there are multiple root packages, create a synthetic root that contains them all
70-
// If there's only one, use it directly
71-
// If there are none, create an empty synthetic root
72-
let primaryRootPackage: FakePackage;
73-
if (rootPackages.length === 0) {
74-
primaryRootPackage = {
75-
name: appData.name,
76-
subpackages: [],
77-
classes: [],
78-
};
79-
} else if (rootPackages.length === 1) {
80-
primaryRootPackage = rootPackages[0];
81-
} else {
82-
// Multiple root packages: create a synthetic root containing them all
83-
primaryRootPackage = {
84-
name: appData.name,
85-
subpackages: rootPackages,
86-
classes: [],
87-
};
88-
}
89-
9047
const app: FakeApp = {
9148
name: appData.name,
92-
rootPackage: primaryRootPackage,
49+
rootPackages: rootPackages,
9350
entryPoint: null as any, // Will be set below
9451
classes: [],
9552
packages: [],
@@ -134,6 +91,29 @@ export function reconstructParentReferences(landscapeData: Array<any>): Array<Fa
13491

13592
// Reconstruct entryPoint reference from FQN
13693
if (appData.entryPointFqn) {
94+
function findClassByFqn(app: FakeApp, fqn: string): FakeClass | null {
95+
function searchInPackage(pkg: FakePackage): FakeClass | null {
96+
for (const cls of pkg.classes) {
97+
const clsFqn = getClassFqn(cls);
98+
if (clsFqn === fqn) {
99+
return cls;
100+
}
101+
}
102+
for (const subPkg of pkg.subpackages) {
103+
const found = searchInPackage(subPkg);
104+
if (found) return found;
105+
}
106+
return null;
107+
}
108+
109+
// Search through all root packages
110+
for (const rootPkg of app.rootPackages) {
111+
const found = searchInPackage(rootPkg);
112+
if (found) return found;
113+
}
114+
return null;
115+
}
116+
137117
const entryPoint = findClassByFqn(app, appData.entryPointFqn);
138118
if (entryPoint) {
139119
app.entryPoint = entryPoint;

src/frontend/components/LandscapeEditor.tsx

Lines changed: 108 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,50 @@ export function LandscapeEditor({ landscape, onLandscapeUpdated, onError }: Land
140140
return null;
141141
};
142142

143+
/**
144+
* Helper function to create or find a package hierarchy from a dot-separated package name.
145+
* Returns the deepest package and all newly created packages.
146+
*/
147+
const createPackageHierarchy = (
148+
parentPackages: CleanedPackage[],
149+
packageNameParts: string[]
150+
): { deepestPackage: CleanedPackage; allCreatedPackages: CleanedPackage[] } => {
151+
const allCreatedPackages: CleanedPackage[] = [];
152+
let currentPackages = parentPackages;
153+
let deepestPackage: CleanedPackage | null = null;
154+
155+
for (let i = 0; i < packageNameParts.length; i++) {
156+
const partName = packageNameParts[i];
157+
let foundPackage = currentPackages.find((pkg) => pkg.name === partName);
158+
159+
if (!foundPackage) {
160+
// Create new package
161+
foundPackage = {
162+
name: partName,
163+
classes: [],
164+
subpackages: [],
165+
};
166+
allCreatedPackages.push(foundPackage);
167+
// Add to current packages array (either parentPackages for root, or deepestPackage.subpackages for nested)
168+
if (deepestPackage) {
169+
deepestPackage.subpackages.push(foundPackage);
170+
} else {
171+
// This is the first level - add to parentPackages array
172+
parentPackages.push(foundPackage);
173+
}
174+
}
175+
176+
deepestPackage = foundPackage;
177+
currentPackages = foundPackage.subpackages;
178+
}
179+
180+
if (!deepestPackage) {
181+
throw new Error('Failed to create package hierarchy');
182+
}
183+
184+
return { deepestPackage, allCreatedPackages };
185+
};
186+
143187
const updateLocalLandscape = (updated: CleanedLandscape[]) => {
144188
isInternalUpdateRef.current = true;
145189
setLocalLandscape(updated);
@@ -247,15 +291,18 @@ export function LandscapeEditor({ landscape, onLandscapeUpdated, onError }: Land
247291
if (packageName && packageName.trim() !== '') {
248292
const updated = [...localLandscape];
249293
const app = updated[appIdx];
250-
const newPkg: CleanedPackage = {
251-
name: packageName.trim(),
252-
classes: [],
253-
subpackages: [],
254-
};
294+
const packageNameParts = packageName.trim().split('.');
295+
296+
// Create a copy of rootPackages to avoid mutating the original
297+
const rootPackagesCopy = [...app.rootPackages];
298+
299+
// Create or find the package hierarchy
300+
const { allCreatedPackages } = createPackageHierarchy(rootPackagesCopy, packageNameParts);
301+
255302
updated[appIdx] = {
256303
...app,
257-
rootPackages: [...app.rootPackages, newPkg],
258-
packages: [...app.packages, newPkg],
304+
rootPackages: rootPackagesCopy,
305+
packages: [...app.packages, ...allCreatedPackages],
259306
};
260307
updateLocalLandscape(updated);
261308
}
@@ -264,6 +311,30 @@ export function LandscapeEditor({ landscape, onLandscapeUpdated, onError }: Land
264311
const addPackage = (appIdx: number) => {
265312
const packageName = prompt('Enter package name:', 'newpackage');
266313
if (packageName && packageName.trim() !== '') {
314+
const trimmedName = packageName.trim();
315+
316+
// If the package name contains dots, treat it as a hierarchy and add as root package
317+
if (trimmedName.includes('.')) {
318+
const updated = [...localLandscape];
319+
const app = updated[appIdx];
320+
const packageNameParts = trimmedName.split('.');
321+
322+
// Create a copy of rootPackages to avoid mutating the original
323+
const rootPackagesCopy = [...app.rootPackages];
324+
325+
// Create or find the package hierarchy
326+
const { allCreatedPackages } = createPackageHierarchy(rootPackagesCopy, packageNameParts);
327+
328+
updated[appIdx] = {
329+
...app,
330+
rootPackages: rootPackagesCopy,
331+
packages: [...app.packages, ...allCreatedPackages],
332+
};
333+
updateLocalLandscape(updated);
334+
return;
335+
}
336+
337+
// For non-hierarchical names, use the existing behavior
267338
const updated = [...localLandscape];
268339
const app = updated[appIdx];
269340
// Find the first root package to add subpackage to
@@ -280,7 +351,7 @@ export function LandscapeEditor({ landscape, onLandscapeUpdated, onError }: Land
280351
}
281352
}
282353
const newPkg: CleanedPackage = {
283-
name: packageName.trim(),
354+
name: trimmedName,
284355
classes: [],
285356
subpackages: [],
286357
};
@@ -304,21 +375,44 @@ export function LandscapeEditor({ landscape, onLandscapeUpdated, onError }: Land
304375
if (packageName && packageName.trim() !== '') {
305376
const updated = [...localLandscape];
306377
const app = updated[appIdx];
307-
const newPkg: CleanedPackage = {
308-
name: packageName.trim(),
309-
classes: [],
310-
subpackages: [],
378+
const packageNameParts = packageName.trim().split('.');
379+
380+
// Find the parent package in the updated landscape
381+
const findPkgInUpdated = (p: CleanedPackage): CleanedPackage | null => {
382+
if (p.name === parentPackageName) return p;
383+
for (const subPkg of p.subpackages) {
384+
const found = findPkgInUpdated(subPkg);
385+
if (found) return found;
386+
}
387+
return null;
311388
};
389+
390+
let parentPkg: CleanedPackage | null = null;
391+
for (const rootPkg of app.rootPackages) {
392+
parentPkg = findPkgInUpdated(rootPkg);
393+
if (parentPkg) break;
394+
}
395+
396+
if (!parentPkg) {
397+
onError(`Parent package "${parentPackageName}" not found`);
398+
return;
399+
}
400+
401+
// Create or find the package hierarchy under the parent
402+
const { allCreatedPackages } = createPackageHierarchy(parentPkg.subpackages, packageNameParts);
403+
404+
// Update the parent package in the tree to ensure React detects the change
312405
const updatePkg = (p: CleanedPackage): CleanedPackage => {
313406
if (p.name === parentPackageName) {
314-
return { ...p, subpackages: [...p.subpackages, newPkg] };
407+
return { ...p, subpackages: [...parentPkg!.subpackages] };
315408
}
316409
return { ...p, subpackages: p.subpackages.map(updatePkg) };
317410
};
411+
318412
updated[appIdx] = {
319413
...app,
320414
rootPackages: app.rootPackages.map(updatePkg),
321-
packages: [...app.packages, newPkg],
415+
packages: [...app.packages, ...allCreatedPackages],
322416
};
323417
updateLocalLandscape(updated);
324418
}

0 commit comments

Comments
 (0)