Skip to content

Commit 14ddd0e

Browse files
committed
fix: honor in-app exclude/delete after export path normalization
Export rebuild strips the project root from file paths, but UI exclude and remove state still used root-prefixed paths, so files like .env reappeared on copy/save. Align both sides before merging manual state.
1 parent 9350908 commit 14ddd0e

2 files changed

Lines changed: 93 additions & 10 deletions

File tree

src/services/exportBuilder.test.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,69 @@ describe('buildExportOutput', () => {
271271
expect(output).not.toContain('"src/removed.ts"');
272272
});
273273

274+
it('honors in-app exclude/delete when UI paths still include the project root prefix', async () => {
275+
// processFiles stores webkitRelativePath as-is (e.g. demo/.env). Export then strips the
276+
// root for packed output. Manual exclude/delete must still match after that strip.
277+
const files = [
278+
createFile('demo/.env', 'SECRET=should-not-export\n'),
279+
createFile('demo/src/app.ts', 'export const answer = 42;\n'),
280+
createFile('demo/src/gone.ts', 'export const gone = true;\n'),
281+
];
282+
283+
const rootPrefixedCurrentData: ProcessedFiles = {
284+
rootName: 'demo',
285+
structureString: 'demo\n├── .env\n└── src\n ├── app.ts\n └── gone.ts\n',
286+
treeData: [],
287+
fileContents: [
288+
{
289+
path: 'demo/.env',
290+
content: 'SECRET=should-not-export\n',
291+
originalContent: 'SECRET=should-not-export\n',
292+
language: 'properties',
293+
stats: { lines: 1, chars: 25, estimatedTokens: 4 },
294+
excluded: true,
295+
},
296+
{
297+
path: 'demo/src/app.ts',
298+
content: 'export const answer = 42;\n',
299+
originalContent: 'export const answer = 42;\n',
300+
language: 'typescript',
301+
stats: { lines: 1, chars: 26, estimatedTokens: 6 },
302+
},
303+
],
304+
analysisSummary: {
305+
totalEstimatedTokens: 10,
306+
securityFindingCount: 0,
307+
scannedFileCount: 2,
308+
},
309+
securityFindings: [],
310+
exportMetadata: {
311+
usesDefaultIgnorePatterns: true,
312+
usesGitignorePatterns: false,
313+
sortsByGitChangeCount: false,
314+
},
315+
// User deleted gone.ts from the explorer; path matches tree/processFiles form.
316+
removedPaths: ['demo/src/gone.ts'],
317+
};
318+
319+
const output = await buildExportOutput({
320+
currentData: rootPrefixedCurrentData,
321+
rawFiles: files,
322+
emptyDirectoryPaths: [],
323+
exportOptions: createExportOptions({ format: 'json', useGitignore: false }),
324+
extractContent: true,
325+
maxCharsThreshold: 100000,
326+
progressCallback: vi.fn(),
327+
});
328+
329+
expect(output).toContain('"src/app.ts"');
330+
expect(output).toContain('export const answer = 42;');
331+
expect(output).not.toContain('SECRET=should-not-export');
332+
expect(output).not.toContain('".env"');
333+
expect(output).not.toContain('"src/gone.ts"');
334+
expect(output).not.toContain('export const gone = true');
335+
});
336+
274337
it('does not emit edited changes sections', async () => {
275338
const files = [createFile('demo/src/app.ts', CURRENT_DATA.fileContents[0].originalContent ?? '')];
276339

src/services/exportData.ts

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -119,15 +119,40 @@ function buildTree(
119119
};
120120
}
121121

122+
/**
123+
* processFiles stores paths with the project root segment (e.g. `demo/.env`).
124+
* Export rebuild strips that root so packed output uses repo-relative paths (`.env`).
125+
* Manual UI state (exclude / delete / edits) still carries the original paths, so
126+
* matching must strip the same root prefix before comparing.
127+
*/
128+
function stripRootPrefix(path: string, rootName: string): string {
129+
if (!rootName) {
130+
return path;
131+
}
132+
133+
const prefix = `${rootName}/`;
134+
return path.startsWith(prefix) ? path.slice(prefix.length) : path;
135+
}
136+
122137
function applyManualState(exportData: ProcessedFiles, currentData: ProcessedFiles): ProcessedFiles {
123-
const currentByPath = new Map(currentData.fileContents.map((file) => [file.path, file]));
124-
const removedPaths = new Set(currentData.removedPaths ?? []);
138+
const rootName = currentData.rootName || exportData.rootName;
139+
const currentByPath = new Map(
140+
currentData.fileContents.map((file) => {
141+
const path = stripRootPrefix(file.path, rootName);
142+
return [path, { ...file, path }] as const;
143+
})
144+
);
145+
const removedPaths = new Set((currentData.removedPaths ?? []).map((path) => stripRootPrefix(path, rootName)));
125146

126147
const mergedFiles = exportData.fileContents
127148
.filter((file) => !removedPaths.has(file.path))
128149
.map((file) => {
129150
const current = currentByPath.get(file.path);
130-
return current ? { ...file, ...current } : file;
151+
if (!current) {
152+
return file;
153+
}
154+
// Keep the export-normalized path; never reintroduce the root prefix from UI state.
155+
return { ...file, ...current, path: file.path };
131156
});
132157
mergedFiles.sort((a, b) => compareFilePaths(a.path, b.path));
133158

@@ -148,18 +173,13 @@ function applyManualState(exportData: ProcessedFiles, currentData: ProcessedFile
148173
}
149174

150175
function normalizeExportPaths(data: ProcessedFiles): ProcessedFiles {
151-
const stripRootPrefix = (path: string) => {
152-
const prefix = `${data.rootName}/`;
153-
return path.startsWith(prefix) ? path.slice(prefix.length) : path;
154-
};
155-
156176
return {
157177
...data,
158178
fileContents: data.fileContents.map((file) => ({
159179
...file,
160-
path: stripRootPrefix(file.path),
180+
path: stripRootPrefix(file.path, data.rootName),
161181
})),
162-
emptyDirectoryPaths: (data.emptyDirectoryPaths ?? []).map(stripRootPrefix),
182+
emptyDirectoryPaths: (data.emptyDirectoryPaths ?? []).map((path) => stripRootPrefix(path, data.rootName)),
163183
};
164184
}
165185

0 commit comments

Comments
 (0)