From 2eeb88435316c898dedec3fbfa948ed626240680 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 9 Mar 2021 23:02:07 +0100 Subject: [PATCH] todo-backend: implement TodoScmReader Signed-off-by: Patrik Oldsberg --- packages/backend/src/plugins/todo.ts | 3 +- plugins/todo-backend/package.json | 8 +- .../src/lib/TodoReader/TodoScmReader.ts | 88 +++++++++++++++++-- yarn.lock | 34 ++++++- 4 files changed, 123 insertions(+), 10 deletions(-) diff --git a/packages/backend/src/plugins/todo.ts b/packages/backend/src/plugins/todo.ts index 6141108a27..84f2feac92 100644 --- a/packages/backend/src/plugins/todo.ts +++ b/packages/backend/src/plugins/todo.ts @@ -25,9 +25,10 @@ import { PluginEnvironment } from '../types'; export default async function createPlugin({ logger, reader, + config, discovery, }: PluginEnvironment): Promise { - const todoReader = new TodoScmReader({ + const todoReader = TodoScmReader.fromConfig(config, { logger, reader, }); diff --git a/plugins/todo-backend/package.json b/plugins/todo-backend/package.json index 6f3d88f56d..9fd5f68948 100644 --- a/plugins/todo-backend/package.json +++ b/plugins/todo-backend/package.json @@ -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" diff --git a/plugins/todo-backend/src/lib/TodoReader/TodoScmReader.ts b/plugins/todo-backend/src/lib/TodoReader/TodoScmReader.ts index f5457d0dd3..5450d0228b 100644 --- a/plugins/todo-backend/src/lib/TodoReader/TodoScmReader.ts +++ b/plugins/todo-backend/src/lib/TodoReader/TodoScmReader.ts @@ -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 { - 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(); + 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 }; } } diff --git a/yarn.lock b/yarn.lock index 0b5a3e2fa0..c056641ff7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10092,7 +10092,7 @@ commander@^5.0.0, commander@^5.1.0: resolved "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz#46abbd1652f8e059bddaef99bbdcb2ad9cf179ae" integrity sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg== -commander@^6.1.0: +commander@^6.1.0, commander@^6.2.1: version "6.2.1" resolved "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz#0792eb682dfbc325999bb2b84fddddba110ac73c" integrity sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA== @@ -12212,6 +12212,11 @@ envinfo@^7.7.4: resolved "https://registry.npmjs.org/envinfo/-/envinfo-7.7.4.tgz#c6311cdd38a0e86808c1c9343f667e4267c4a320" integrity sha512-TQXTYFVVwwluWSFis6K2XKxgrD22jEv0FTuLCQI+OjH7rn93+iY0fSSFM5lrSxFY+H1+B0/cvvlamr3UsBivdQ== +eol@^0.9.1: + version "0.9.1" + resolved "https://registry.npmjs.org/eol/-/eol-0.9.1.tgz#f701912f504074be35c6117a5c4ade49cd547acd" + integrity sha512-Ds/TEoZjwggRoz/Q2O7SE3i4Jm66mqTDfmdHdq/7DKVk3bro9Q8h6WdXKdPqFLMoqxrDK5SVRzHVPOS6uuGtrg== + err-code@^2.0.2: version "2.0.3" resolved "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz#23c2f3b756ffdfc608d30e27c9a941024807e7f9" @@ -13890,6 +13895,11 @@ get-stdin@^6.0.0: resolved "https://registry.npmjs.org/get-stdin/-/get-stdin-6.0.0.tgz#9e09bf712b360ab9225e812048f71fde9c89657b" integrity sha512-jp4tHawyV7+fkkSKyvjuLZswblUtz+SQKzSWnBbii16BuZksJlU1wuBYXY75r+duh/llF1ur6oNwi+2ZzjKZ7g== +get-stdin@^8.0.0: + version "8.0.0" + resolved "https://registry.npmjs.org/get-stdin/-/get-stdin-8.0.0.tgz#cbad6a73feb75f6eeb22ba9e01f89aa28aa97a53" + integrity sha512-sY22aA6xchAzprjyqmSEQv4UbAAzRN0L2dQB0NlN5acTTK9Don6nhoc3eAbUnpZiCANAMfd/+40kVdKfFygohg== + get-stream@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14" @@ -16881,6 +16891,11 @@ json-to-pretty-yaml@^1.2.2: remedial "^1.0.7" remove-trailing-spaces "^1.0.6" +json2xml@^0.1.3: + version "0.1.3" + resolved "https://registry.npmjs.org/json2xml/-/json2xml-0.1.3.tgz#9ae7c220bedd7c66a668e26f7ac182f6704eca21" + integrity sha1-mufCIL7dfGamaOJvesGC9nBOyiE= + json3@^3.3.2: version "3.3.3" resolved "https://registry.npmjs.org/json3/-/json3-3.3.3.tgz#7fc10e375fc5ae42c4705a5cc0aa6f62be305b81" @@ -17295,6 +17310,23 @@ ldapjs@^2.2.0: vasync "^2.2.0" verror "^1.8.1" +leasot@^11.5.0: + version "11.5.0" + resolved "https://registry.npmjs.org/leasot/-/leasot-11.5.0.tgz#a99eb4479618c9d2ea442a32ee006e5b9da4844d" + integrity sha512-L08QKlmofYIRs5gfOmhOtbEJUu6U/zFGvYpboPq34yAHQ2Oc/QznOw62noe29yRJLiV/XnIDS8vO2um1e1sikA== + dependencies: + async "^3.2.0" + chalk "^4.1.0" + commander "^6.2.1" + eol "^0.9.1" + get-stdin "^8.0.0" + globby "^11.0.1" + json2xml "^0.1.3" + lodash "^4.17.20" + log-symbols "^4.0.0" + strip-ansi "^6.0.0" + text-table "^0.2.0" + left-pad@^1.3.0: version "1.3.0" resolved "https://registry.npmjs.org/left-pad/-/left-pad-1.3.0.tgz#5b8a3a7765dfe001261dde915589e782f8c94d1e"