diff --git a/frontend/src/lib/components/workspaceSettings/projectBundle.test.ts b/frontend/src/lib/components/workspaceSettings/projectBundle.test.ts index e0af8a3232..1a041e26b1 100644 --- a/frontend/src/lib/components/workspaceSettings/projectBundle.test.ts +++ b/frontend/src/lib/components/workspaceSettings/projectBundle.test.ts @@ -107,6 +107,21 @@ describe('buildPathMap', () => { expect(m.get('u/alice/db')).toBe('f/proj/db_2') expect(m.get('u/bob/db')).toBe('f/proj/db_3') }) + it('maps internal paths to themselves, preserving subfolder depth', () => { + const m = buildPathMap(['f/proj/api', 'f/proj/sub/deep/script'], 'proj') + expect(m.get('f/proj/api')).toBe('f/proj/api') + expect(m.get('f/proj/sub/deep/script')).toBe('f/proj/sub/deep/script') + }) + it('does not flatten two internal items sharing a leaf name', () => { + const m = buildPathMap(['f/proj/a/x', 'f/proj/b/x'], 'proj') + expect(m.get('f/proj/a/x')).toBe('f/proj/a/x') + expect(m.get('f/proj/b/x')).toBe('f/proj/b/x') + }) + it('relocates an external onto a suffix when its leaf collides with an internal path', () => { + const m = buildPathMap(['f/proj/db', 'u/admin/db'], 'proj') + expect(m.get('f/proj/db')).toBe('f/proj/db') + expect(m.get('u/admin/db')).toBe('f/proj/db_2') + }) }) describe('rewriteContent', () => { diff --git a/frontend/src/lib/components/workspaceSettings/projectBundle.ts b/frontend/src/lib/components/workspaceSettings/projectBundle.ts index 7ecfe92c65..4a918912ba 100644 --- a/frontend/src/lib/components/workspaceSettings/projectBundle.ts +++ b/frontend/src/lib/components/workspaceSettings/projectBundle.ts @@ -99,16 +99,26 @@ export function extractAppRefs(value: any): Ref[] { } /** - * Build the relocation map for external paths. Each `u//` or - * `f//` maps to `f//`; on collision the later entry - * gets a `_2`, `_3`… suffix. Input is sorted first so the suffix assignment is + * Build the relocation map. Paths already inside the project folder + * (`f//...`) map to themselves — their structure and subfolder depth are + * preserved. Only external paths (`u//` or `f//`) are + * relocated to `f//`; on collision the later entry gets a `_2`, + * `_3`… suffix. Internal paths are reserved first so an external one can never + * land on an occupied internal path. Input is sorted so suffix assignment is * deterministic regardless of discovery order. */ -export function buildPathMap(externalPaths: Iterable, slug: string): Map { +export function buildPathMap(paths: Iterable, slug: string): Map { const map = new Map() const used = new Set() - const sorted = [...new Set(externalPaths)].sort() + const sorted = [...new Set(paths)].sort() + for (const p of sorted) { + if (classifyPath(p, slug) === 'internal') { + map.set(p, p) + used.add(p) + } + } for (const old of sorted) { + if (map.has(old)) continue const name = old.split('/').filter(Boolean).pop() ?? old let candidate = `f/${slug}/${name}` let n = 2