todo-backend: implement TodoScmReader

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2021-03-09 23:02:07 +01:00
parent 76e6336312
commit 2eeb884353
4 changed files with 123 additions and 10 deletions
+5 -3
View File
@@ -22,18 +22,20 @@
"@backstage/catalog-client": "^0.3.6",
"@backstage/catalog-model": "^0.7.3",
"@backstage/config": "^0.1.3",
"@backstage/integration": "^0.5.0",
"@types/express": "^4.17.6",
"cross-fetch": "^3.0.6",
"express": "^4.17.1",
"express-promise-router": "^3.0.3",
"leasot": "^11.5.0",
"winston": "^3.2.1",
"cross-fetch": "^3.0.6",
"yn": "^4.0.0"
},
"devDependencies": {
"@backstage/cli": "^0.6.3",
"@types/supertest": "^2.0.8",
"supertest": "^4.0.2",
"msw": "^0.21.2"
"msw": "^0.21.2",
"supertest": "^4.0.2"
},
"files": [
"dist"
@@ -14,27 +14,105 @@
* limitations under the License.
*/
import { extname } from 'path';
import { UrlReader } from '@backstage/backend-common';
import { ScmIntegrations } from '@backstage/integration';
import { Logger } from 'winston';
import { ReadTodosOptions, ReadTodosResult, TodoReader } from './types';
import { parse } from 'leasot';
import {
ReadTodosOptions,
ReadTodosResult,
TodoItem,
TodoReader,
} from './types';
import { Config } from '@backstage/config';
type TodoParser = (ctx: {
content: string;
path: string;
}) => (TodoItem & { line: number })[];
type Options = {
logger: Logger;
reader: UrlReader;
parser?: TodoParser;
};
const defaultTodoParser: TodoParser = ({ content, path }) => {
try {
const comments = parse(content, {
extension: extname(path),
});
return comments.map(comment => ({
text: comment.text,
author: comment.ref,
line: comment.line,
}));
} catch /* ignore unsupported extensions */ {
return [];
}
};
export class TodoScmReader implements TodoReader {
private readonly logger: Logger;
private readonly reader: UrlReader;
private readonly parser: TodoParser;
private readonly integrations: ScmIntegrations;
constructor(options: Options) {
static fromConfig(config: Config, options: Options) {
return new TodoScmReader(options, ScmIntegrations.fromConfig(config));
}
private constructor(options: Options, integrations: ScmIntegrations) {
this.logger = options.logger;
this.reader = options.reader;
this.parser = options.parser ?? defaultTodoParser;
this.integrations = integrations;
}
async readTodos({ url }: ReadTodosOptions): Promise<ReadTodosResult> {
return {
items: [{ text: 'My mock todo', viewUrl: url }],
};
const tree = await this.reader.readTree(url, {
filter(path) {
return !path.startsWith('.yarn');
},
});
const files = await tree.files();
this.logger.info(`Read ${files.length} files from ${url}`);
const todos = new Array<TodoItem>();
for (const file of files) {
const content = await file.content();
try {
const items = this.parser({
path: file.path,
content: content.toString('utf8'),
});
const viewUrl = this.integrations.resolveUrl({
url: file.path,
base: url,
});
let editUrl: string | undefined = this.integrations.resolveEditUrl(
viewUrl,
);
if (editUrl === viewUrl) {
editUrl = undefined;
}
todos.push(
...items.map(item => ({
...item,
editUrl,
viewUrl: item.line ? `${viewUrl}#L${item.line}` : viewUrl,
})),
);
} catch (error) {
this.logger.error(`Failed to parse TODO in ${url}, ${error}`);
}
}
return { items: todos };
}
}