diff --git a/.github/workflows/build-assets.yml b/.github/workflows/build-assets.yml new file mode 100644 index 0000000000..a3c2a9d348 --- /dev/null +++ b/.github/workflows/build-assets.yml @@ -0,0 +1,32 @@ +name: build assets + +on: + push: + branches: + - master + paths: + - 'lib/**' + +jobs: + build: + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: + - 14.x + + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v1 + with: + node-version: ${{ matrix.node-version }} + - name: build file + run: npm ci && npm run build:all + - name: Deploy + uses: peaceiris/actions-gh-pages@v3 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./assets \ No newline at end of file diff --git a/.gitignore b/.gitignore index fc0f64a663..eed3c9f7ff 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,5 @@ app-minimal/ .now .vercel + +assets/build/ \ No newline at end of file diff --git a/assets/404.html b/assets/404.html new file mode 100644 index 0000000000..f8c6427942 --- /dev/null +++ b/assets/404.html @@ -0,0 +1,4 @@ + \ No newline at end of file diff --git a/assets/CNAME b/assets/CNAME new file mode 100644 index 0000000000..5ab3a9991b --- /dev/null +++ b/assets/CNAME @@ -0,0 +1 @@ +rsshub.js.org \ No newline at end of file diff --git a/assets/index.html b/assets/index.html new file mode 100644 index 0000000000..f8c6427942 --- /dev/null +++ b/assets/index.html @@ -0,0 +1,4 @@ + \ No newline at end of file diff --git a/docs/.vuepress/nav/zh.js b/docs/.vuepress/nav/zh.js index 26c1c336c3..1f46309ef8 100644 --- a/docs/.vuepress/nav/zh.js +++ b/docs/.vuepress/nav/zh.js @@ -14,6 +14,10 @@ module.exports = [ { text: '详细规范', items: [ + { + text: '路由规范', + link: '/joinus/script-standard', + }, { text: '日期处理', link: '/joinus/pub-date', diff --git a/docs/joinus/script-standard.md b/docs/joinus/script-standard.md new file mode 100644 index 0000000000..fa6e261e16 --- /dev/null +++ b/docs/joinus/script-standard.md @@ -0,0 +1,150 @@ +# 路由规范 + +::: warning 警告 + +这个规范仍在制定过程中,可能会随着时间推移而发生改变,请记得多回来看看! + +::: + +在编写新的路由时,RSSHub会读取文件夹中的: + +- `router.js`注册路由 +- `maintainer.js`获取路由路径,维护者 +- `radar.js`获取路由所对应的网站,以及匹配规则:https://github.com/DIYgod/RSSHub-Radar/ +- `templates` 渲染模版 + +**以上文件为所有插件必备** + +``` +├───lib/v2 +│ ├───furstar +│ ├─── templates +│ ├─── description.art +│ ├─── router.js +│ ├─── maintainer.js +│ ├─── radar.js +│ ├─── someOtherJs.js +│ └───test +│ └───someOtherScript +... +``` + +**所有符合条件的,在`/v2`路径下的路由,将会被自动载入,无需更新`router.js`** + +## 路由示例 + +参考`furstar`: `./lib/v2/furstar` + +可以复制该文件夹作为新路由模版 + +## 注册路由 + +`router.js` 应当导出一个方法,我们在初始化路由的时候,会提供一个`@koa/router`对象 + +### 命名规范 + +我们会默认将所有的路由文件夹名字附加在真正的路由前面。路由维护者可以认定自己获取的就是根,我们会在附加对应的命名空间,在这空间底下,开发者有所有的控制权 + +### 例子 + +```js +module.exports = function (router) { + router.get('/characters/:lang?', require('./index')); + router.get('/artists/:lang?', require('./artists')); + router.get('/archive/:lang?', require('./archive')); +}; +``` + +## 维护者列表 + +`maintainer.js` 应当导出一个对象,在我们获取路径相关信息时,将会在从这里调取开发者信息等 + +- key: `@koa/router` 对应的路径匹配 +- value: 数组,包含所有开发者的Github Username + +Github ID可能是更好的选择,但是后续处理不便,目前暂定Username + +### 例子 + +```js +module.exports = { + '/characters/:lang?': ['NeverBehave'], + '/artists/:lang?': ['NeverBehave'], + '/archive/:lang?': ['NeverBehave'], +}; +``` + +`npm run build:maintainer` 将会在`assets/build`下生成一份贡献者清单 + +## Radar Rules + +书写方式:https://docs.rsshub.app/joinus/quick-start.html#ti-jiao-xin-de-rsshub-radar-gui-ze + +**我们目前要求所有路由,必须包含这个文件,并且包含对应的域名 -- 我们不要求完全的路由匹配,最低要求是在对应的网站,可以显示支持即可。这个文件后续会用于帮助bug反馈。** + +### 例子 + +```js +module.exports = { + 'furstar.jp': { + _name: 'Furstar', + '.': [ + { + title: '最新售卖角色列表', + docs: 'https://docs.rsshub.app/shopping.html#furstar-zui-xin-shou-mai-jiao-se-lie-biao', + source: ['/:lang', '/'], + target: '/characters/:lang', + }, + { + title: '已经出售的角色列表', + docs: 'https://docs.rsshub.app/shopping.html#furstar-yi-jing-chu-shou-de-jiao-se-lie-biao', + source: ['/:lang/archive.php', '/archive.php'], + target: '/archive/:lang', + }, + { + title: '画师列表', + docs: 'https://docs.rsshub.app/shopping.html#furstar-hua-shi-lie-biao', + source: ['/'], + target: '/artists', + }, + ], + }, +}; +``` + + +`npm run build:radar` 将会在`/assets/build/`下生成一份完整的`radar-rules.js` + + +## Template + +我们目前要求所有路由,在渲染`description`等带HTML的内容时,**必须**使用art引擎进行排版 + +art说明文档:https://aui.github.io/art-template/docs/ + +同时,所有模版应该放在插件`templates`文件夹中 -- 后续我们会以此提供自定义模版切换/渲染等需求 + +### 例子 + +```art +
+``` + +```js +const { art } = require('@/utils/render'); +const renderAuthor = (author) => art(path.join(__dirname, 'templates/author.art'), author); +``` + +## ctx.state.json + +插件目前可以提供一个自定义的对象,用于调试 -- 访问对应路由+`.debug.json`即可获取到对应内容 + +我们对这个部分格式内容没有任何限制,完全可选,目前会继续观察这个选项的发展方向 + diff --git a/docs/parameter.md b/docs/parameter.md index b5e302b554..7e449bb946 100644 --- a/docs/parameter.md +++ b/docs/parameter.md @@ -94,6 +94,16 @@ RSSHub 同时支持 RSS 2.0 和 Atom 输出格式,在路由末尾添加 `.rss` - Atom -{{ desc }}
+{{ each pics }} +${desc}
${rpics}${rauthor}`; -}; +const renderAuthor = (author) => art(path.join(__dirname, 'templates/author.art'), author); +const renderDesc = (desc, pics, author) => + art(path.join(__dirname, 'templates/description.art'), { + desc, + pics, + author: renderAuthor(author), + }); const authorDetail = (el) => { const $ = cheerio.load(el); diff --git a/lib/routes/test/index.js b/lib/v2/test/index.js similarity index 100% rename from lib/routes/test/index.js rename to lib/v2/test/index.js diff --git a/lib/v2/test/maintainer.js b/lib/v2/test/maintainer.js new file mode 100644 index 0000000000..49378171dd --- /dev/null +++ b/lib/v2/test/maintainer.js @@ -0,0 +1,3 @@ +module.exports = { + '/:id': ['DIYgod', 'NeverBehave'], +}; diff --git a/lib/v2/test/router.js b/lib/v2/test/router.js new file mode 100644 index 0000000000..641c23df3a --- /dev/null +++ b/lib/v2/test/router.js @@ -0,0 +1,3 @@ +module.exports = function (router) { + router.get('/:id', require('./index')); +}; diff --git a/lib/v2router.js b/lib/v2router.js new file mode 100644 index 0000000000..cf2b5cec90 --- /dev/null +++ b/lib/v2router.js @@ -0,0 +1,17 @@ +const dirname = __dirname + '/v2'; + +// 遍历整个 routes 文件夹,收集模块路由 router.js +const RouterPath = require('require-all')({ + dirname, + filter: /router\.js$/, +}); + +const routes = {}; + +// 将收集到的自定义模块路由进行合并 +for (const dir in RouterPath) { + const project = RouterPath[dir]['router.js']; // Do not merge other file + routes[dir] = project; +} + +module.exports = routes; diff --git a/lib/views/error.art b/lib/views/error.art index 831d7f35be..5f22bb4f89 100644 --- a/lib/views/error.art +++ b/lib/views/error.art @@ -39,6 +39,15 @@ +
+Helpful Information to provide when opening issue:
+Path: {{@ errorPath }}
+Node version: {{@ nodeVersion}}
+
+
在线文档与支持,请访问docs.rsshub.app。
diff --git a/package-lock.json b/package-lock.json index 7dab85150e..3b3728f9ca 100644 --- a/package-lock.json +++ b/package-lock.json @@ -76,10 +76,12 @@ "@vuepress/plugin-back-to-top": "1.8.2", "@vuepress/plugin-google-analytics": "1.8.2", "@vuepress/plugin-pwa": "1.8.2", + "ci-info": "^2.0.0", "cross-env": "7.0.3", "eslint": "7.29.0", "eslint-config-prettier": "8.3.0", "eslint-plugin-prettier": "3.4.0", + "fs-extra": "^8.1.0", "jest": "26.6.3", "mockdate": "3.0.5", "nock": "13.0.11", @@ -94,6 +96,7 @@ "staged-git-files": "1.2.0", "string-width": "4.2.2", "supertest": "6.1.3", + "tosource": "2.0.0-alpha.3", "vuepress": "1.8.2", "yorkie": "2.0.0" } @@ -22930,6 +22933,15 @@ "x-ray-scraper": "^3.0.5" } }, + "node_modules/tosource": { + "version": "2.0.0-alpha.3", + "resolved": "https://registry.npmjs.org/tosource/-/tosource-2.0.0-alpha.3.tgz", + "integrity": "sha512-KAB2lrSS48y91MzFPFuDg4hLbvDiyTjOVgaK7Erw+5AmZXNq4sFRVn8r6yxSLuNs15PaokrDRpS61ERY9uZOug==", + "dev": true, + "engines": { + "node": ">=10" + } + }, "node_modules/touch": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz", @@ -45089,6 +45101,12 @@ "x-ray-scraper": "^3.0.5" } }, + "tosource": { + "version": "2.0.0-alpha.3", + "resolved": "https://registry.npmjs.org/tosource/-/tosource-2.0.0-alpha.3.tgz", + "integrity": "sha512-KAB2lrSS48y91MzFPFuDg4hLbvDiyTjOVgaK7Erw+5AmZXNq4sFRVn8r6yxSLuNs15PaokrDRpS61ERY9uZOug==", + "dev": true + }, "touch": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz", diff --git a/package.json b/package.json index 08ddc0d534..4f999715e4 100644 --- a/package.json +++ b/package.json @@ -12,12 +12,16 @@ "profiling": "NODE_ENV=production node --prof lib/index.js", "docs:dev": "vuepress dev docs", "docs:build": "vuepress build docs", + "build:all": "npm run build:radar && npm run build:maintainer", + "build:radar": "node scripts/workflow/build-radar.js", + "build:maintainer": "node scripts/workflow/build-maintainer.js", "lint": "eslint .", "format": "eslint \"**/*.js\" --fix && node docs/.format/format.js && prettier \"**/*.{js,json}\" --write", "format:staged": "eslint \"**/*.js\" --fix && node docs/.format/format.js --staged && pretty-quick --staged --verbose --pattern \"**/*.{js,json}\"", "format:check": "eslint \"**/*.js\" && prettier-check \"**/*.{js,json}\"", "test": "npm run format:check && cross-env NODE_ENV=test jest --coverage --runInBand --forceExit", - "jest": "cross-env NODE_ENV=test jest --runInBand --forceExit" + "jest": "cross-env NODE_ENV=test jest --runInBand --forceExit", + "jest:watch": "cross-env NODE_ENV=test jest --watch" }, "repository": { "type": "git", @@ -44,10 +48,12 @@ "@vuepress/plugin-back-to-top": "1.8.2", "@vuepress/plugin-google-analytics": "1.8.2", "@vuepress/plugin-pwa": "1.8.2", + "ci-info": "^2.0.0", "cross-env": "7.0.3", "eslint": "7.29.0", "eslint-config-prettier": "8.3.0", "eslint-plugin-prettier": "3.4.0", + "fs-extra": "^8.1.0", "jest": "26.6.3", "mockdate": "3.0.5", "nock": "13.0.11", @@ -62,9 +68,9 @@ "staged-git-files": "1.2.0", "string-width": "4.2.2", "supertest": "6.1.3", + "tosource": "2.0.0-alpha.3", "vuepress": "1.8.2", - "yorkie": "2.0.0", - "fs-extra": "^8.1.0" + "yorkie": "2.0.0" }, "dependencies": { "@koa/router": "10.0.0", diff --git a/scripts/workflow/build-maintainer.js b/scripts/workflow/build-maintainer.js new file mode 100644 index 0000000000..f57f488b8a --- /dev/null +++ b/scripts/workflow/build-maintainer.js @@ -0,0 +1,16 @@ +/** */ +const fs = require('fs'); +const path = require('path'); +const target = path.join(__dirname, '../../assets/build/maintainer.json'); +const maintainer = require(path.join(__dirname, '../../lib/maintainer.js')); + +const count = Object.keys(maintainer).length; +const uniqueMaintainer = new Set(); +Object.values(maintainer) + .flat() + .forEach((e) => uniqueMaintainer.add(e)); + +// eslint-disable-next-line no-console +console.log(`We have ${count} routes and maintained by ${uniqueMaintainer.size} contributors!`); + +fs.writeFileSync(target, JSON.stringify(maintainer, null, 4)); diff --git a/scripts/workflow/build-radar.js b/scripts/workflow/build-radar.js new file mode 100644 index 0000000000..822b33afe5 --- /dev/null +++ b/scripts/workflow/build-radar.js @@ -0,0 +1,6 @@ +const fs = require('fs'); +const path = require('path'); +const target = path.join(__dirname, '../../assets/build/radar-rules.js'); +const radar = require(path.join(__dirname, '../../lib/radar.js')); + +fs.writeFileSync(target, radar.toSource()); diff --git a/test/middleware/cache.js b/test/middleware/cache.js index eb66470c69..bae584f079 100644 --- a/test/middleware/cache.js +++ b/test/middleware/cache.js @@ -134,8 +134,8 @@ describe('cache', () => { it('redis with quit', async () => { process.env.CACHE_TYPE = 'redis'; server = require('../../lib/index'); - const client = require('../../lib/app').context.cache.client; - await client.quit(); + const { redisClient } = require('../../lib/app').context.cache.clients; + await redisClient.quit(); const request = supertest(server); const response1 = await request.get('/test/cache'); diff --git a/test/middleware/template.js b/test/middleware/template.js index 1ba6b4296c..3f454250af 100644 --- a/test/middleware/template.js +++ b/test/middleware/template.js @@ -60,8 +60,8 @@ describe('template', () => { it(`.json`, async () => { const response = await request.get('/test/1.json'); - expect(response.status).toBe(404); - expect(response.text).toMatch(/Error: JSON output had been removed/); + const responseXML = await request.get('/test/1.rss'); + expect(response.text.slice(0, 50)).toEqual(responseXML.text.slice(0, 50)); }); it(`long title`, async () => {