diff --git a/.changeset/mean-pumpkins-search.md b/.changeset/mean-pumpkins-search.md new file mode 100644 index 0000000000..ddaea98b9e --- /dev/null +++ b/.changeset/mean-pumpkins-search.md @@ -0,0 +1,25 @@ +--- +'@backstage/plugin-todo-backend': patch +--- + +Add support to exclude certain folders in `todo` plugin. + +You can add function by configuring your own exclusion logic, for example: + +```ts +import { + TodoScmReader, + createTodoParser, +} from '@backstage/plugin-todo-backend'; + +// ... + +const todoReader = TodoScmReader.fromConfig(config, { + logger, + reader, + filePathFilter: (filePath: string): boolean => { + ... + YOUR LOGIC HERE + }, +}); +``` diff --git a/plugins/todo-backend/api-report.md b/plugins/todo-backend/api-report.md index ac8b2ce957..71094e03f8 100644 --- a/plugins/todo-backend/api-report.md +++ b/plugins/todo-backend/api-report.md @@ -131,6 +131,7 @@ export type TodoScmReaderOptions = { reader: UrlReader; integrations: ScmIntegrations; parser?: TodoParser; + filePathFilter?: (filePath: string) => boolean; }; // @public (undocumented) diff --git a/plugins/todo-backend/src/lib/TodoReader/TodoScmReader.test.ts b/plugins/todo-backend/src/lib/TodoReader/TodoScmReader.test.ts index 9675a285e0..3e074f78a6 100644 --- a/plugins/todo-backend/src/lib/TodoReader/TodoScmReader.test.ts +++ b/plugins/todo-backend/src/lib/TodoReader/TodoScmReader.test.ts @@ -189,4 +189,91 @@ describe('TodoScmReader', () => { 'Failed to parse TODO in https://github.com/o/r/catalog-info.yaml at my-file.sh, Error: failed to parse', ); }); + + it('should not filter out exclude folders', async () => { + const reader = mockReader(); + const filePathFilter = jest.fn(() => true); + + const todoReader = new TodoScmReader({ + logger: getVoidLogger(), + reader, + integrations: ScmIntegrations.fromConfig(new ConfigReader({})), + filePathFilter, + }); + reader.readTree.mockResolvedValueOnce({ + files: async () => [ + { + content: async () => Buffer.from('// TODO: my-todo', 'utf8'), + path: 'my-folder/my-file.js', + }, + ], + } as ReadTreeResponse); + await expect( + todoReader.readTodos({ + url: 'https://github.com/backstage/backstage/catalog-info.yaml', + }), + ).resolves.toEqual({ + items: [ + { + text: 'my-todo', + tag: 'TODO', + lineNumber: 1, + repoFilePath: 'my-folder/my-file.js', + viewUrl: + 'https://github.com/backstage/backstage/my-folder/my-file.js#L1', + }, + ], + }); + expect(reader.readTree).toHaveBeenCalledTimes(1); + expect(reader.readTree).toHaveBeenCalledWith( + 'https://github.com/backstage/backstage/catalog-info.yaml', + { + etag: undefined, + filter: expect.any(Function), + }, + ); + // Filter function should filter out exclude folders + const filterFunc = reader.readTree.mock.calls[0][1]!.filter!; + expect(filterFunc('my-file.js')).toBe(true); + expect(filterFunc('my-folder/my-file.js')).toBe(true); + }); + + it('should filter out exclude folders', async () => { + const reader = mockReader(); + const filePathFilter = jest.fn(() => false); + + const todoReader = new TodoScmReader({ + logger: getVoidLogger(), + reader, + integrations: ScmIntegrations.fromConfig(new ConfigReader({})), + filePathFilter, + }); + reader.readTree.mockResolvedValueOnce({ + files: async () => [ + { + content: async () => Buffer.from('// TODO: my-todo', 'utf8'), + path: '', + }, + ], + } as ReadTreeResponse); + await expect( + todoReader.readTodos({ + url: 'https://github.com/backstage/backstage/catalog-info.yaml', + }), + ).resolves.toEqual({ + items: [], + }); + expect(reader.readTree).toHaveBeenCalledTimes(1); + expect(reader.readTree).toHaveBeenCalledWith( + 'https://github.com/backstage/backstage/catalog-info.yaml', + { + etag: undefined, + filter: expect.any(Function), + }, + ); + // Filter function should filter out exclude folders + const filterFunc = reader.readTree.mock.calls[0][1]!.filter!; + expect(filterFunc('my-file.js')).toBe(false); + expect(filterFunc('my-folder/my-file.js')).toBe(false); + }); }); diff --git a/plugins/todo-backend/src/lib/TodoReader/TodoScmReader.ts b/plugins/todo-backend/src/lib/TodoReader/TodoScmReader.ts index 19890a8d23..2696f0d54b 100644 --- a/plugins/todo-backend/src/lib/TodoReader/TodoScmReader.ts +++ b/plugins/todo-backend/src/lib/TodoReader/TodoScmReader.ts @@ -47,6 +47,7 @@ export type TodoScmReaderOptions = { reader: UrlReader; integrations: ScmIntegrations; parser?: TodoParser; + filePathFilter?: (filePath: string) => boolean; }; type CacheItem = { @@ -60,6 +61,7 @@ export class TodoScmReader implements TodoReader { private readonly reader: UrlReader; private readonly parser: TodoParser; private readonly integrations: ScmIntegrations; + private readonly filePathFilter: (filePath: string) => boolean; private readonly cache = new Map(); private readonly inFlightReads = new Map>(); @@ -79,6 +81,7 @@ export class TodoScmReader implements TodoReader { this.reader = options.reader; this.parser = options.parser ?? createTodoParser(); this.integrations = options.integrations; + this.filePathFilter = options.filePathFilter ?? (() => true); } async readTodos(options: ReadTodosOptions): Promise { @@ -111,6 +114,7 @@ export class TodoScmReader implements TodoReader { etag?: string, ): Promise { const { url } = options; + const filePathFilter = this.filePathFilter; const tree = await this.reader.readTree(url, { etag, filter(filePath, info) { @@ -121,7 +125,8 @@ export class TodoScmReader implements TodoReader { return ( !filePath.startsWith('.') && !filePath.includes('/.') && - !excludedExtensions.includes(extname) + !excludedExtensions.includes(extname) && + filePathFilter(filePath) ); }, });