{"id":13817359,"url":"https://github.com/chinanf-boy/ava-explain","last_synced_at":"2025-04-02T18:16:01.758Z","repository":{"id":90547998,"uuid":"129495546","full_name":"chinanf-boy/ava-explain","owner":"chinanf-boy","description":"explain : 「ava」未来的JavaScript测试运行器 | Futuristic JavaScript test runner","archived":false,"fork":false,"pushed_at":"2018-04-20T07:32:07.000Z","size":92,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-02-08T08:47:12.040Z","etag":null,"topics":["ava","explain","runner","test"],"latest_commit_sha":null,"homepage":null,"language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/chinanf-boy.png","metadata":{"files":{"readme":"readme.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null}},"created_at":"2018-04-14T08:01:53.000Z","updated_at":"2018-04-20T07:32:50.000Z","dependencies_parsed_at":null,"dependency_job_id":"d441e48a-c899-4259-b610-858ee3b29dc4","html_url":"https://github.com/chinanf-boy/ava-explain","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/chinanf-boy%2Fava-explain","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/chinanf-boy%2Fava-explain/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/chinanf-boy%2Fava-explain/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/chinanf-boy%2Fava-explain/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/chinanf-boy","download_url":"https://codeload.github.com/chinanf-boy/ava-explain/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":246866100,"owners_count":20846496,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["ava","explain","runner","test"],"created_at":"2024-08-04T06:00:40.895Z","updated_at":"2025-04-02T18:16:01.735Z","avatar_url":"https://github.com/chinanf-boy.png","language":"JavaScript","funding_links":[],"categories":["JavaScript"],"sub_categories":[],"readme":"# ava\n\n「 下一代未来测试 」\n\n[![explain](http://llever.com/explain.svg)](https://github.com/chinanf-boy/Source-Explain)\n    \nExplanation\n\n\u003e \"version\": \"1.0.0-beta.3\"\n\n[github source](https://github.com/avajs/ava)\n\n~~[english](./README.en.md)~~\n\n---\n\n2018.4.20 停止并总结\n\n1. 在本次项目中, 看到了许多有趣的应用, `Promise-层`,` 函数塔`, `EventEmitter `\n\n2. 有点不自量力, 我选择了以往 `explain` 的写法-流水账的写法, 后果就是说明了, 后继无力, 较为烦闷\n\n3. 现在我感觉代码分两种 `功能` 与 `结构`\n\n如果说中小型项目还没有过量的结构代码, 那么我在 `ava` 中被虐了, 错中复杂的关系网络, 父子进程, EventEmitter的on/emit 写这也能写那, 当然这是一个大型项目的根基, 在我看来, ava 的 结构是很好的, 只是-复杂-\u003e `对我来说`\n\n4. 也许, 下次解释 大型项目时, 应该把功能与结构代码分开解释, 这样也能挑重点解释 , 就像 [learnVue-Vue.js 源码解析](https://github.com/answershuto/learnVue)\n\n从细节开始着手进行, 刚看第一部分就有所收获, 当然, 代码是一个一个打的 Or c/, 还是会继续前行的\n\n\u003e 谢谢 完成度 `60/100` 及格可以吗\n\n---\n\n我们从 应用ava 的方式, 开始解释吧\n\n1. 我们需要一个测试文件, `test.js`\n\n``` js\nimport test from 'ava'\n\ntest('add', t =\u003e{\n    t.pass()\n})\n```\n\n然后\n\n2. 运行 `ava test.js`\n\n而我们我们会分两个部分 进入ava 项目 \n\n`「 1. cli | 2. test 」`\n\n我们 自然是以 `1. cli` 为起点\n\n---\n\n本目录\n\n---\n\n## package.json\n\n``` js\n\t\"bin\": \"cli.js\",\n```\n\n---\n\n### 1. cli.js\n\n`ava/cli.js`\n\n``` js\n#!/usr/bin/env node\n'use strict';\nconst debug = require('debug')('ava');\nconst importLocal = require('import-local');\n\n// 更倾向使用本地 ava\nif (importLocal(__filename)) {\n// 一般, 我们都是使用 「 ava ** 」 这种形式的命令行\n\n// 而在被测试项目中, 我们需要使用 import test from 'ava', 就需要安装 ava 在 package.json 中\n\n// 而 importLocal 就会 require 使用 被测试项目所安装的 ava cli 命令, 也就是回到最终ava项目的这个文件\n\tdebug('Using local install of AVA');\n} else {\n\tif (debug.enabled) {\n        // 输出 信息 待仪\n\t\trequire('@ladjs/time-require'); // eslint-disable-line import/no-unassigned-import\n\t}\n\n\ttry {\n\t\trequire('./lib/cli').run(); // \u003c=== 最终运行的ava 项目\n\t} catch (err) {\n\t\tconsole.error(`\\n  ${err.message}`);\n\t\tprocess.exit(1);\n\t}\n}\n\n```\n\n---\n\n### 2. lib-cli\n\n`ava/lib/cli.js`\n\n代码 19-136\n\n\u003e \n\n\u003cdetails\u003e\n\n``` js\n\n// ava 使用 什么 Promise 的呢\n// 答案是 Bluebird https://github.com/petkaantonov/bluebird\n\n// Bluebird specific\nPromise.longStackTraces(); // ???\n\nexports.run = () =\u003e {\n    // 我们从 1. cli.js 中知道 ava 总是选择被测试项目中的 node_modules/.bin/ava 来运行\n    const conf = pkgConf.sync('ava'); \n    // 所以这里就是, 在被测试项目的package.json 获取 配置\n\n\tconst filepath = pkgConf.filepath(conf);// package.json 的目录\n\tconst projectDir = filepath === null ? process.cwd() : path.dirname(filepath);// 项目目录\n\n    // 用 meow 定义 命令行选项\n\tconst cli = meow(`\n\t\tUsage\n\t\t  ava [\u003cfile|directory|glob\u003e ...]\n\n\t\tOptions 中文翻译选项\n\t\t  --watch, -w             测试和源文件更改时重新运行测试\n\t\t  --match, -m             只能运行匹配标题的测试（可重复）\n\t\t  --update-snapshots, -u  更新快照\n\t\t  --fail-fast             第一次测试失败后停止\n\t\t  --timeout, -T           设置全局超时\n\t\t  --serial, -s            连续运行测试\n\t\t  --concurrency, -c       同时运行的测试文件的最大数量（默认值：CPU核心） \n\t\t  --verbose, -v           启用详细输出\n\t\t  --tap, -t               生成TAP输出\n\t\t  --no-cache              禁用编译器缓存\n\t\t  --color                 强制色彩输出\n\t\t  --no-color              禁用颜色输出\n\n\t\tExamples\n\t\t  ava\n\t\t  ava test.js test2.js\n\t\t  ava test-*.js\n\t\t  ava test\n\n\t\tDefault patterns when no arguments:\n\t\ttest.js test-*.js test/**/*.js **/__tests__/**/*.js **/*.test.js\n\t`, {\n\t\tflags: {\n\t\t\twatch: {\n\t\t\t\ttype: 'boolean',\n\t\t\t\talias: 'w'\n\t\t\t},\n\t\t\tmatch: {\n\t\t\t\ttype: 'string',\n\t\t\t\talias: 'm',\n\t\t\t\tdefault: conf.match\n\t\t\t},\n\t\t\t'update-snapshots': {\n\t\t\t\ttype: 'boolean',\n\t\t\t\talias: 'u'\n\t\t\t},\n\t\t\t'fail-fast': {\n\t\t\t\ttype: 'boolean',\n\t\t\t\tdefault: conf.failFast\n\t\t\t},\n\t\t\ttimeout: {\n\t\t\t\ttype: 'string',\n\t\t\t\talias: 'T',\n\t\t\t\tdefault: conf.timeout\n\t\t\t},\n\t\t\tserial: {\n\t\t\t\ttype: 'boolean',\n\t\t\t\talias: 's',\n\t\t\t\tdefault: conf.serial\n\t\t\t},\n\t\t\tconcurrency: {\n\t\t\t\ttype: 'string',\n\t\t\t\talias: 'c',\n\t\t\t\tdefault: conf.concurrency\n\t\t\t},\n\t\t\tverbose: {\n\t\t\t\ttype: 'boolean',\n\t\t\t\talias: 'v',\n\t\t\t\tdefault: conf.verbose\n\t\t\t},\n\t\t\ttap: {\n\t\t\t\ttype: 'boolean',\n\t\t\t\talias: 't',\n\t\t\t\tdefault: conf.tap\n\t\t\t},\n\t\t\tcache: {\n\t\t\t\ttype: 'boolean',\n\t\t\t\tdefault: conf.cache !== false\n\t\t\t},\n\t\t\tcolor: {\n\t\t\t\ttype: 'boolean',\n\t\t\t\tdefault: 'color' in conf ? conf.color : require('supports-color').stdout !== false\n\t\t\t},\n\t\t\t'--': {\n\t\t\t\ttype: 'string'\n\t\t\t}\n\t\t}\n\t});\n\n\tupdateNotifier({pkg: cli.pkg}).notify(); // 更新-提示\n\n\tif (cli.flags.watch \u0026\u0026 cli.flags.tap \u0026\u0026 !conf.tap) {\n\t\tthrow new Error(`${colors.error(figures.cross)} The TAP reporter is not available when using watch mode.`);\n\t}\n\n\tif (cli.flags.watch \u0026\u0026 isCi) {\n\t\tthrow new Error(`${colors.error(figures.cross)} Watch mode is not available in CI, as it prevents AVA from terminating.`);\n\t}\n\n\tif (\n\t\tcli.flags.concurrency === '' ||\n\t\t(cli.flags.concurrency \u0026\u0026 (!Number.isInteger(Number.parseFloat(cli.flags.concurrency)) || parseInt(cli.flags.concurrency, 10) \u003c 0))\n\t) {\n\t\tthrow new Error(`${colors.error(figures.cross)} The --concurrency or -c flag must be provided with a nonnegative integer.`);\n\t}\n\n\tif ('source' in conf) {\n\t\tthrow new Error(`${colors.error(figures.cross)} The 'source' option has been renamed. Use 'sources' instead.`);\n\t}\n\n\t// 合并配置, \n\tObject.assign(conf, cli.flags);\n\n    const api = new Api({ \n\t\tfailFast: conf.failFast,\n\t\tfailWithoutAssertions: conf.failWithoutAssertions !== false,\n\t\tserial: conf.serial,\n\t\trequire: arrify(conf.require),\n\t\tcacheEnabled: conf.cache,\n\t\tcompileEnhancements: conf.compileEnhancements !== false,\n\t\texplicitTitles: conf.watch,\n\t\tmatch: arrify(conf.match),\n\t\tbabelConfig: babelConfigHelper.validate(conf.babel),\n\t\tresolveTestsFrom: cli.input.length === 0 ? projectDir : process.cwd(),\n\t\tprojectDir,\n\t\ttimeout: conf.timeout,\n\t\tconcurrency: conf.concurrency ? parseInt(conf.concurrency, 10) : 0,\n\t\tupdateSnapshots: conf.updateSnapshots,\n\t\tsnapshotDir: conf.snapshotDir ? path.resolve(projectDir, conf.snapshotDir) : null,\n\t\tcolor: conf.color,\n\t\tworkerArgv: cli.flags['--']\n\t})\n```\n\n#### 2.1 new Api\n\n\u003e [作为测试-总开关 初始化 3. api](#3-api)\n\n- 2.2 [`babelConfigHelper.validate`](./babel-config.md#1-validate)\n\n\u003e 归纳 babel 的 配置, 把[babelConfigHelper 放到 babel-config.md 解释](./babel-config.md)\n\n- 2.3 `Api 传入的值有什么东东`\n\n``` js\n\t\t` --watch, -w            测试和源文件更改时重新运行测试\n\t\t  --match, -m             只能运行匹配标题的测试（可重复）\n\t\t  --update-snapshots, -u  更新快照\n\t\t  --fail-fast             第一次测试失败后停止\n\t\t  --timeout, -T           设置全局超时\n\t\t  --serial, -s            串行测试\n\t\t  --concurrency, -c       同时运行的测试文件的最大数量（默认值：CPU核心） \n\t\t  --verbose, -v           启用详细输出\n\t\t  --tap, -t               生成TAP输出\n\t\t  --no-cache              禁用编译器缓存\n\t\t  --color                 强制色彩输出\n\t\t  --no-color              禁用颜色输出`\n\n\t\t{\n\t\t\t// package.json 里面定义的选项\n\t\tfailFast: conf.failFast,\n\t\tfailWithoutAssertions: conf.failWithoutAssertions !== false,\n\t\tserial: conf.serial,\n\t\trequire: arrify(conf.require),\n\t\tcacheEnabled: conf.cache,\n\t\tcompileEnhancements: conf.compileEnhancements !== false,\n\t\texplicitTitles: conf.watch,\n\t\tmatch: arrify(conf.match),\n\t\tbabelConfig: babelConfigHelper.validate(conf.babel),\n\t\tresolveTestsFrom: cli.input.length === 0 ? projectDir : process.cwd(), // 测试目录\n\t\tprojectDir,\n\t\ttimeout: conf.timeout,\n\t\tconcurrency: conf.concurrency ? parseInt(conf.concurrency, 10) : 0,\n\t\tupdateSnapshots: conf.updateSnapshots, \n\t\tsnapshotDir: conf.snapshotDir ? path.resolve(projectDir, conf.snapshotDir) : null,\n\t\tcolor: conf.color,\n\t\tworkerArgv: cli.flags['--']\n\t\t}\n\t\t{\n\t\tfiles：`文件和目录路径以及选择哪些文件AVA将运行测试的全局模式。只使用扩展名为.js的文件。带有下划线前缀的文件将被忽略。运行所选目录中的所有.js文件`\n\t\tsource：`文件，如果更改，会导致测试在手表模式下重新运行。有关详情，请参阅手表模式配方`\n\t\tmatch：`在package.json配置中通常不是很有用，但相当于在CLI中指定--match`\n\t\tfailFast：`一旦测试失败，停止运行进一步的测试`\n\t\tfailWithoutAssertions：`如果为false，如果不运行断言，则不会使测试失败`\n\t\ttap：`如果为true，则启用TAP记者`\n\t\tsnapshotDir：`指定存储快照文件的固定位置。如果您的快照在错误的位置结束，请使用此选项`\n\t\tcompileEnhancements：`如果为false，则禁用power-assert - 否则有助于提供更多描述性错误消息 - 并检测t.throws（）声明的不当使用`\n\t\trequire：`在测试运行之前需要额外的模块。模块在工作进程中是必需的`\n\t\tbabel：`测试文件特定的Babel选项。有关更多详情，请参阅我们的Babel配方`\n\t\t}\n```\n\n\u003e 其中较为疑惑的应该是 `updateSnapshots` 和  `workerArgv`\n\n[snapshots en 官方解释](https://github.com/avajs/ava#snapshot-testing)\n\n至于 [workerArgv 需要讲到单次测试子进程使用的选项](./main.md#workerargv)\n\n\u003c/details\u003e\n\n---\n\n### 3. Api\n\n`ava/lib/api.js`\n\n代码 34-40\n\n\u003e 用整理的选项-初始化测试总开关\n\n\u003cdetails\u003e\n\n``` js\nclass Api extends EventEmitter {\n\tconstructor(options) {\n\t\tsuper();\n\n\t\tthis.options = Object.assign({match: []}, options);\n        this.options.require = resolveModules(this.options.require);\n        // 存储好 默认和目前 用户定义 选项值\n    }\n    // ...\n```\n\n- 3.1 `EventEmitter`\n\n\u003e 我们先说明 `on/emit `模式 , 请先了解清楚后, 再继续\n\n\u003e 如果你不太了解可以看看[nodejs.cn](http://nodejs.cn/api/events.html)或者关于[mitt- 小小实现的on/emit](https://github.com/chinanf-boy/explain-mitt)\n\n- 3.2 [`resolveModules`](#resolvemodules)\n\n\u003e 找寻需要用到的 类似babel-插件路径\n\n\u003c/details\u003e\n \n ---\n\n\n### 4. cli-logger\n\n`ava/lib/cli.js`\n\n代码 156-169\n\n\u003cdetails\u003e\n\n让我们回到`ava/lib/cli.js`\n\n在我们保存好我们选项**conf**之后, 我们再一次决定测试数据-日志输出方式\n\n[请转到cli-logger.explain.md](./cli-logger.explain.md#1-日志形式)\n\n如果你对此还不想了解！ 😊\n\n\u003e 其实不影响后面的重要逻辑的解释\n\n\u003c/details\u003e\n\n---\n\n\n### 5. runStatus\n\n`ava/lib/cli.js`\n\n代码 171-178\n\n\u003cdetails\u003e\n\n\u003e 运行测试-状态\n\n我们在 [4. cli-logger](#4-cli-logger) 有了 日志工具,\n\n但是我们要把 测试-状态与日志工具拼接 `logger \u003c-\u003e runStatus`\n\n才能 错❌ 就是 输出错误,对✅ 就是 输出正确\n\n``` js\n// 定义 触发 test-run 函数\n\tapi.on('test-run', runStatus =\u003e {\n\t\treporter.api = runStatus;\n\t\trunStatus.on('test', logger.test);\n\t\trunStatus.on('error', logger.unhandledError);\n\n\t\trunStatus.on('stdout', logger.stdout);\n\t\trunStatus.on('stderr', logger.stderr);\n\t});\n```\n\n- 5.1 `runStatus`\n\n\u003e 其实这里还没有使用 - [runStatus 使用在 api.run 中](./api-run#runstatus)\n\n\n\u003c/details\u003e\n\n---\n\n### 6-7. watch\n\n`ava/lib/cli.js`\n\n代码 180-209\n\n\u003e 这小段是分 是否观察文件 不退出进程\n\n这一段很重要, \n\n- 观察吧 导致 [6. Watcher 观察者](#6-watcher)的运行\n\n- 不观察吧 直接运行 [7. api-run](#7-api-run) 测试运行\n\n\u003cdetails\u003e\n\n``` js\nconst files = cli.input.length ? cli.input : arrify(conf.files); // 测试-文件, 未打磨\n\n\tif (conf.watch) {\n\t\ttry {\n\t\t\tconst watcher = new Watcher(logger, api, files, arrify(conf.sources));\n\t\t\twatcher.observeStdin(process.stdin);\n\t\t} catch (err) {\n\t\t\tif (err.name === 'AvaError') {\n\t\t\t\t// An AvaError may be thrown if `chokidar` is not installed. Log it nicely.\n\t\t\t\tconsole.error(`  ${colors.error(figures.cross)} ${err.message}`);\n\t\t\t\tlogger.exit(1);\n\t\t\t} else {\n\t\t\t\t// Rethrow so it becomes an uncaught exception\n\t\t\t\tthrow err;\n\t\t\t}\n\t\t}\n\t} else {\n\t\tapi.run(files)\n\t\t\t.then(runStatus =\u003e {\n\t\t\t\tlogger.finish(runStatus);\n\t\t\t\tlogger.exit(runStatus.failCount \u003e 0 || runStatus.rejectionCount \u003e 0 || runStatus.exceptionCount \u003e 0 ? 1 : 0);\n\t\t\t})\n\t\t\t.catch(err =\u003e {\n\t\t\t\t// Don't swallow exceptions. Note that any expected error should already\n\t\t\t\t// have been logged.\n\t\t\t\tsetImmediate(() =\u003e {\n\t\t\t\t\tthrow err;\n\t\t\t\t});\n\t\t\t});\n\t}\n```\n\n\u003c/details\u003e\n\n---\n\n### 6. Watcher\n\n`ava/lib/cli.js`\n\n代码 183-195\n\n\u003cdetails\u003e\n\n也许你可以先看 [7. api-run 了解一次运行情况再来看 Watcher噢😯](#7-api-run)\n\n``` js\n\t\ttry {\n\t\t\tconst watcher = new Watcher(logger, api, files, arrify(conf.sources));\n\t\t\twatcher.observeStdin(process.stdin);\n\t\t} catch (err) {\n\t\t\tif (err.name === 'AvaError') {\n\t\t\t\t// An AvaError may be thrown if `chokidar` is not installed. Log it nicely.\n\t\t\t\tconsole.error(`  ${colors.error(figures.cross)} ${err.message}`);\n\t\t\t\tlogger.exit(1);\n\t\t\t} else {\n\t\t\t\t// Rethrow so it becomes an uncaught exception\n\t\t\t\tthrow err;\n\t\t\t}\n\t\t}\n```\n\n- 6.1 watcher\n\n\u003e [请转到 watcher.md](./watcher.md)\n\n\n\u003c/details\u003e\n\n---\n\n### 7. api-run\n\n`ava/lib/cli.js`\n\n代码 197-208\n\n\u003cdetails\u003e\n\n``` js\n\t\tapi.run(files)\n\t\t\t.then(runStatus =\u003e {\n\t\t\t\tlogger.finish(runStatus);\n\t\t\t\tlogger.exit(runStatus.failCount \u003e 0 || runStatus.rejectionCount \u003e 0 || runStatus.exceptionCount \u003e 0 ? 1 : 0);\n\t\t\t})\n\t\t\t.catch(err =\u003e {\n\t\t\t\t// Don't swallow exceptions. Note that any expected error should already\n\t\t\t\t// have been logged.\n\t\t\t\tsetImmediate(() =\u003e {\n\t\t\t\t\tthrow err;\n\t\t\t\t});\n\t\t\t});\n```\n\u003c/details\u003e\n\n- 7.1 api-run\n\n\u003e [请转到 api-run.md](./api-run.md)\n\n---\n\n\n## 8 总结\n\n无可否认, 软件经过解释, 似乎失去了一整个的使用和方便.\n\n正如一个苹果🍎, 作为苹果研究的人眼里👀, 可不仅仅是吃的东西\n\n我们也是, 软件使用, 让我们方便, 但我们仍需要知道每一段, 每一个字符串\n\n的代码是如何运作的, 因为我们是`Coder`!\n\n最后我们来描绘一下 ava 运作流程\n\n// 未完成\n\n---\n\n## 其他\n\n\u003e 有关作者 那些 小kuku\n\n\u003cdetails\u003e\n\n\n### resolveModules\n\n\u003e 模块名-去确定是否具有-模块路径, 没有则抛出错误\n\n``` js\nfunction resolveModules(modules) {\n\treturn arrify(modules).map(name =\u003e {\n        const modulePath = resolveCwd.silent(name);\n        // 无法找到模块时返回null 而不是抛出。\n\n\t\tif (modulePath === null) {\n\t\t\tthrow new Error(`Could not resolve required module '${name}'`);\n\t\t}\n\n\t\treturn modulePath;\n\t});\n}\n```\n\n- [resolveCwd](#resolve-cwd)\n\n\u003e 根据当前工作目录解析模块的路径 \n\n---\n\n### resolve-cwd\n\n\u003e 根据当前工作目录解析模块的路径 [-\u003egithub](https://github.com/sindresorhus/resolve-cwd)\n\n### arrify\n\n\u003e将值转换为数组 [-\u003egithub](https://github.com/sindresorhus/arrify)\n\u003c/details\u003e","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fchinanf-boy%2Fava-explain","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fchinanf-boy%2Fava-explain","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fchinanf-boy%2Fava-explain/lists"}