mirror of
https://github.com/DIYgod/RSSHub.git
synced 2025-12-04 19:59:54 +08:00
feat: new build script
This commit is contained in:
2
.github/workflows/build-assets.yml
vendored
2
.github/workflows/build-assets.yml
vendored
@@ -34,7 +34,7 @@ jobs:
|
||||
- name: Install dependencies (yarn)
|
||||
run: pnpm i
|
||||
- name: Build assets
|
||||
run: npm run build:all
|
||||
run: npm run build
|
||||
- name: Deploy
|
||||
uses: peaceiris/actions-gh-pages@v3
|
||||
with:
|
||||
|
||||
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
@@ -149,7 +149,7 @@ jobs:
|
||||
cache: 'pnpm'
|
||||
- run: pnpm i
|
||||
- name: Build radar and maintainer
|
||||
run: npm run build:all
|
||||
run: npm run build
|
||||
|
||||
automerge:
|
||||
if: github.triggering_actor == 'dependabot[bot]' && github.event_name == 'pull_request'
|
||||
|
||||
@@ -14,6 +14,7 @@ const namespaces: Record<
|
||||
routes: Record<string, Route>;
|
||||
}
|
||||
> = {};
|
||||
|
||||
for (const module in modules) {
|
||||
const content = modules[module] as
|
||||
| {
|
||||
@@ -38,9 +39,17 @@ for (const module in modules) {
|
||||
routes: {},
|
||||
};
|
||||
}
|
||||
if (Array.isArray(content.route.path)) {
|
||||
for (const path of content.route.path) {
|
||||
namespaces[namespace].routes[path] = content.route;
|
||||
}
|
||||
} else {
|
||||
namespaces[namespace].routes[content.route.path] = content.route;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export { namespaces };
|
||||
|
||||
export default function (app: Hono) {
|
||||
for (const namespace in namespaces) {
|
||||
|
||||
@@ -19,6 +19,10 @@ export const route: Route = {
|
||||
},
|
||||
name: '番剧',
|
||||
maintainers: ['DIYgod'],
|
||||
radar: {
|
||||
source: ['www.bilibili.com/bangumi/media/:bid'],
|
||||
target: '/bangumi/media/:bid',
|
||||
},
|
||||
handler: async (ctx) => {
|
||||
let seasonid = ctx.req.param('seasonid');
|
||||
const mediaid = ctx.req.param('mediaid');
|
||||
|
||||
@@ -26,7 +26,7 @@ export default {
|
||||
title: '番剧',
|
||||
docs: 'https://docs.rsshub.app/routes/social-media#bilibili',
|
||||
source: '/bangumi/media/:bid',
|
||||
target: (params) => `/bilibili/bangumi/media/${params.bid.replace('md', '')}`,
|
||||
target: `/bilibili/bangumi/media/:bid`,
|
||||
},
|
||||
{
|
||||
title: '当前在线',
|
||||
|
||||
@@ -62,7 +62,7 @@ interface Namespace extends NamespaceItem {
|
||||
export type { Namespace };
|
||||
|
||||
interface RouteItem {
|
||||
path: string;
|
||||
path: string | string[];
|
||||
name: string;
|
||||
url?: string;
|
||||
maintainers: string[];
|
||||
|
||||
@@ -20,9 +20,7 @@
|
||||
"lib"
|
||||
],
|
||||
"scripts": {
|
||||
"build:all": "npm run build:radar && npm run build:maintainer",
|
||||
"build:maintainer": "tsx scripts/workflow/build-maintainer.ts",
|
||||
"build:radar": "tsx scripts/workflow/build-radar.ts",
|
||||
"build": "tsx scripts/workflow/build-routes.ts",
|
||||
"dev": "cross-env NODE_ENV=dev tsx watch --no-cache lib/index.ts",
|
||||
"format": "eslint --cache --fix \"**/*.{ts,js,yml}\" && node website/docs/.format/format.mjs && prettier \"**/*.{ts,js,json}\" --write",
|
||||
"format:check": "eslint --cache \"**/*.{ts,js,yml}\" && prettier \"**/*.{ts,js,json}\" --check",
|
||||
@@ -175,6 +173,7 @@
|
||||
"staged-git-files": "1.3.0",
|
||||
"string-width": "7.1.0",
|
||||
"supertest": "6.3.4",
|
||||
"tldts": "6.1.13",
|
||||
"to-vfile": "8.0.0",
|
||||
"tosource": "2.0.0-alpha.3",
|
||||
"typescript": "5.3.3",
|
||||
|
||||
14
pnpm-lock.yaml
generated
14
pnpm-lock.yaml
generated
@@ -379,6 +379,9 @@ devDependencies:
|
||||
supertest:
|
||||
specifier: 6.3.4
|
||||
version: 6.3.4
|
||||
tldts:
|
||||
specifier: 6.1.13
|
||||
version: 6.1.13
|
||||
to-vfile:
|
||||
specifier: 8.0.0
|
||||
version: 8.0.0
|
||||
@@ -9437,6 +9440,17 @@ packages:
|
||||
hasBin: true
|
||||
dev: false
|
||||
|
||||
/tldts-core@6.1.13:
|
||||
resolution: {integrity: sha512-M1XP4D13YtXARKroULnLsKKuI1NCRAbJmUGGoXqWinajIDOhTeJf/trYUyBoLVx1/Nx1KBKxCrlW57ZW9cMHAA==}
|
||||
dev: true
|
||||
|
||||
/tldts@6.1.13:
|
||||
resolution: {integrity: sha512-+GxHFKVHvUTg2ieNPTx3b/NpZbgJSTZEDdI4cJzTjVYDuxijeHi1tt7CHHsMjLqyc+T50VVgWs3LIb2LrXOzxw==}
|
||||
hasBin: true
|
||||
dependencies:
|
||||
tldts-core: 6.1.13
|
||||
dev: true
|
||||
|
||||
/tmp@0.0.33:
|
||||
resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==}
|
||||
engines: {node: '>=0.6.0'}
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import { directoryImport } from 'directory-import';
|
||||
|
||||
const target = path.join(__dirname, '../../assets/build/maintainer.json');
|
||||
const dirname = path.join(__dirname + '../../../lib/routes');
|
||||
|
||||
// Presence Check
|
||||
for (const dir of fs.readdirSync(dirname)) {
|
||||
const dirPath = path.join(dirname, dir);
|
||||
if (fs.existsSync(path.join(dirPath, 'router.ts')) && !fs.existsSync(path.join(dirPath, 'maintainer.ts'))) {
|
||||
throw new Error(`No maintainer.ts in "${dirPath}".`);
|
||||
}
|
||||
}
|
||||
|
||||
// 遍历整个 routes 文件夹,收集模块 maintainer.ts
|
||||
// const maintainerPath = require('require-all')({
|
||||
// dirname,
|
||||
// filter: /maintainer\.ts$/,
|
||||
// });
|
||||
const imports = directoryImport({
|
||||
targetDirectoryPath: dirname,
|
||||
importPattern: /maintainer\.ts$/,
|
||||
});
|
||||
|
||||
const maintainers = {};
|
||||
|
||||
// 将收集到的自定义模块进行合并
|
||||
for (const dir in imports) {
|
||||
const routes = imports[dir].default; // Do not merge other file
|
||||
|
||||
// typo check e.g., ✘ module.export, ✔ module.exports
|
||||
if (!Object.keys(routes).length) {
|
||||
throw new Error(`No maintainer in "${dir}".`);
|
||||
}
|
||||
for (const author of Object.values(routes)) {
|
||||
if (!Array.isArray(author)) {
|
||||
throw new TypeError(`Maintainers' name should be an array in "${dir}".`);
|
||||
}
|
||||
// check for [], [''] or ['Someone', '']
|
||||
if (author.length < 1 || author.includes('')) {
|
||||
throw new Error(`Empty maintainer in "${dir}".`);
|
||||
}
|
||||
}
|
||||
|
||||
for (const key in routes) {
|
||||
maintainers[dir.replace('/maintainer.ts', '') + (key.endsWith('/') ? key.substring(0, key.length - 1) : key)] = routes[key];
|
||||
}
|
||||
}
|
||||
|
||||
// 兼容旧版路由
|
||||
// const router = require('../../lib/router.js');
|
||||
// for (const e of router.stack) {
|
||||
// if (!maintainers[e.path]) {
|
||||
// maintainers[e.path] = [];
|
||||
// }
|
||||
// }
|
||||
|
||||
const maintainer = Object.keys(maintainers)
|
||||
.sort()
|
||||
.reduce((obj, path) => {
|
||||
obj[path] = maintainers[path];
|
||||
return obj;
|
||||
}, {});
|
||||
|
||||
const count = Object.keys(maintainer).length;
|
||||
const uniqueMaintainer = new Set();
|
||||
for (const e of Object.values(maintainer).flat()) {
|
||||
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));
|
||||
@@ -1,108 +0,0 @@
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import { directoryImport } from 'directory-import';
|
||||
import toSource from 'tosource';
|
||||
|
||||
const targetJs = path.join(__dirname, '../../assets/build/radar-rules.js');
|
||||
const targetJson = path.join(__dirname, '../../assets/build/radar-rules.json');
|
||||
const dirname = path.join(__dirname + '../../../lib/routes');
|
||||
|
||||
// Namespaces that do not require radar.ts
|
||||
const allowNamespace = new Set(['discourse', 'discuz', 'ehentai', 'lemmy', 'mail', 'test', 'index.tsx', 'robots.txt.ts']);
|
||||
// Check if a radar.ts file is exist under each folder of dirname
|
||||
for (const dir of fs.readdirSync(dirname)) {
|
||||
const dirPath = path.join(dirname, dir);
|
||||
if (!fs.existsSync(path.join(dirPath, 'radar.ts')) && !allowNamespace.has(dir)) {
|
||||
throw new Error(`No radar.ts in "${dirPath}".`);
|
||||
}
|
||||
}
|
||||
|
||||
const validateRadarRules = (rule, dir) => {
|
||||
const allowDomains = new Set(['www.gov.cn']);
|
||||
const blockWords = ['/', 'http', 'www'];
|
||||
const domain = Object.keys(rule);
|
||||
if (!domain.length && !allowNamespace.has(dir)) {
|
||||
// typo check e.g., ✘ module.export, ✔ module.exports
|
||||
throw new Error(`No Radar rule in "${dir}".`);
|
||||
}
|
||||
for (const [d, r] of Object.entries(rule)) {
|
||||
if (blockWords.some((word) => d.startsWith(word)) && !allowDomains.has(d)) {
|
||||
throw new Error(`Domain name "${d}" should not contain any of ${blockWords.join(', ')}.`);
|
||||
}
|
||||
if (!Object.hasOwn(r, '_name')) {
|
||||
throw new Error(`No _name in "${dir}".`);
|
||||
}
|
||||
// property check
|
||||
for (const [host, items] of Object.entries(r)) {
|
||||
if (host !== '_name') {
|
||||
if (!Array.isArray(items)) {
|
||||
throw new TypeError(`Radar rules for domain "${host}" in "${dir}" should be placed in an array.`);
|
||||
}
|
||||
for (const item of items) {
|
||||
if (!Object.hasOwn(item, 'title') || !Object.hasOwn(item, 'docs')) {
|
||||
throw new Error(`Radar rules for "${host}" in "${dir}" should have at least "title" and "docs".`);
|
||||
}
|
||||
if (!item.title || !item.docs) {
|
||||
throw new Error(`Radar rules for "${host}" in "${dir}" should not be empty.`);
|
||||
}
|
||||
if (!item.docs.startsWith('https://docs.rsshub.app/')) {
|
||||
throw new Error(`Radar rules for "${host}" in "${dir}" should start with 'https://docs.rsshub.app/'.`);
|
||||
}
|
||||
if (Array.isArray(item.source)) {
|
||||
if (!item.source.length) {
|
||||
// check for []
|
||||
throw new Error(`Radar rule of "${item.title}" for subdomain "${host}" in "${dir}" should not be empty.`);
|
||||
}
|
||||
if (item.source.some((s) => s.includes('#') || s.includes('='))) {
|
||||
// Some will try to match '/some/path?a=1' which is not supported
|
||||
throw new Error(`Radar rule of "${item.title}" for subdomain "${host}" in "${dir}" cannot match URL hash or URL search parameters.`);
|
||||
}
|
||||
if (item.source.some((s) => !s.length)) {
|
||||
// check for ['/some/thing', ''] and ['']
|
||||
throw new Error(`Radar rule of "${item.title}" for subdomain "${host}" in "${dir}" should not be empty.`);
|
||||
}
|
||||
}
|
||||
if (typeof item.source === 'string') {
|
||||
if (!item.source.length) {
|
||||
// check for ''
|
||||
throw new Error(`Radar rule of "${item.title}" for subdomain "${host}" in "${dir}" should not be empty.`);
|
||||
}
|
||||
if (item.source.includes('#') || item.source.includes('=')) {
|
||||
throw new Error(`Radar rule of "${item.title}" for subdomain "${host}" in "${dir}" cannot match URL hash or URL search parameters.`);
|
||||
}
|
||||
}
|
||||
for (const key in item) {
|
||||
if (key !== 'title' && key !== 'docs' && key !== 'source' && key !== 'target') {
|
||||
throw new Error(`Radar rules for "${host}" in "${dir}" should not have property "${key}".`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// const radarRules = require('require-all')({
|
||||
// dirname,
|
||||
// filter: /radar\.ts$/,
|
||||
// });
|
||||
const imports = directoryImport({
|
||||
targetDirectoryPath: dirname,
|
||||
importPattern: /radar\.ts$/,
|
||||
});
|
||||
|
||||
let rules = {};
|
||||
|
||||
for (const dir in imports) {
|
||||
const rule = imports[dir].default; // Do not merge other file
|
||||
|
||||
validateRadarRules(rule, dir.replace('/radar.ts', '').replace(/^\//, ''));
|
||||
|
||||
rules = { ...rules, ...rule };
|
||||
}
|
||||
|
||||
const oldRules = require('./radar-rules.js'); // Match old rules
|
||||
rules = { ...rules, ...oldRules };
|
||||
|
||||
fs.writeFileSync(targetJs, `(${toSource(rules)})`);
|
||||
fs.writeFileSync(targetJson, JSON.stringify(rules, undefined, 2));
|
||||
56
scripts/workflow/build-routes.ts
Normal file
56
scripts/workflow/build-routes.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { namespaces } from '../../lib/registry';
|
||||
import { parse } from 'tldts';
|
||||
import fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
|
||||
const maintainers: Record<string, string[]> = {};
|
||||
const radar: {
|
||||
[domain: string]: {
|
||||
_name: string;
|
||||
[subdomain: string]:
|
||||
| {
|
||||
title: string;
|
||||
docs: string;
|
||||
source: string[];
|
||||
target: string | ((params: any, url: string) => string);
|
||||
}[]
|
||||
| string;
|
||||
};
|
||||
} = {};
|
||||
|
||||
for (const namespace in namespaces) {
|
||||
for (const path in namespaces[namespace].routes) {
|
||||
const realPath = `/${namespace}${path}`;
|
||||
const data = namespaces[namespace].routes[path];
|
||||
if (data.maintainers) {
|
||||
maintainers[realPath] = data.maintainers;
|
||||
}
|
||||
if (data.radar && data.radar.source) {
|
||||
const parsedDomain = parse(new URL('https://' + data.radar.source[0]).hostname);
|
||||
const subdomain = parsedDomain.subdomain || '.';
|
||||
const domain = parsedDomain.domain;
|
||||
if (domain) {
|
||||
if (!radar[domain]) {
|
||||
radar[domain] = {
|
||||
_name: namespaces[namespace].name,
|
||||
};
|
||||
}
|
||||
if (!radar[domain][subdomain]) {
|
||||
radar[domain][subdomain] = [];
|
||||
}
|
||||
radar[domain][subdomain].push({
|
||||
title: data.name,
|
||||
docs: `https://docs.rsshub.app/routes/${data.categories[0] || 'other'}`,
|
||||
source: data.radar.source.map((source) => {
|
||||
const sourceURL = new URL('https://' + source);
|
||||
return sourceURL.pathname + sourceURL.search + sourceURL.hash;
|
||||
}),
|
||||
target: data.radar.target || realPath,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fs.writeFileSync(path.join(__dirname, '../../assets/build/radar-rules.json'), JSON.stringify(radar, null, 2));
|
||||
fs.writeFileSync(path.join(__dirname, '../../assets/build/maintainers.json'), JSON.stringify(maintainers, null, 2));
|
||||
@@ -109,7 +109,7 @@ The `maintainer.ts` file should export an object that provides maintainer inform
|
||||
- Key: Corresponding route path
|
||||
- Value: Array of string, including all maintainers' GitHub ID.
|
||||
|
||||
To generate a list of maintainers, use the following command: `pnpm run build:maintainer`, which will create the list under `assets/build/`.
|
||||
To generate a list of maintainers, use the following command: `pnpm run build`, which will create the list under `assets/build/`.
|
||||
|
||||
:::danger
|
||||
|
||||
@@ -121,7 +121,7 @@ The path should be the same as the `path` in the corresponding documentation bef
|
||||
|
||||
All routes are required to include the `radar.ts` file, which includes the corresponding domain name. The minimum requirement for a successful match is for the rule to show up on the corresponding site which requires filling in the `title` and `docs` fields.
|
||||
|
||||
To generate a complete `radar-rules.ts` file, use the following command: `yarn build:radar`, which will create the file under `assets/build/`.
|
||||
To generate a complete `radar-rules.ts` file, use the following command: `yarn build`, which will create the file under `assets/build/`.
|
||||
|
||||
:::tip
|
||||
|
||||
|
||||
@@ -107,7 +107,7 @@ RSSHub 会将所有路由命名空间的文件夹名附加到路由前面。路
|
||||
- 键: 对应的路由
|
||||
- 值:一个字符串数组,包括所有维护者的 GitHub ID。
|
||||
|
||||
要生成维护者列表,可使用以下命令:`pnpm run build:maintainer`,它将在 `assets/build/` 目录下一份维护者列表。
|
||||
要生成维护者列表,可使用以下命令:`pnpm run build`,它将在 `assets/build/` 目录下一份维护者列表。
|
||||
|
||||
:::danger
|
||||
|
||||
@@ -119,7 +119,7 @@ RSSHub 会将所有路由命名空间的文件夹名附加到路由前面。路
|
||||
|
||||
所有路由都需要包含 `radar.ts` 文件,其中包括相应的域名。最低要求是规则出现在相应的站点上,即需要填写 `title` 和 `docs` 字段。
|
||||
|
||||
要生成完整的 `radar-rules.js` 文件,可使用以下命令:`yarn build:radar`,它将在 `assets/build/` 目录下创建文件。
|
||||
要生成完整的 `radar-rules.js` 文件,可使用以下命令:`yarn build`,它将在 `assets/build/` 目录下创建文件。
|
||||
|
||||
:::tip
|
||||
|
||||
|
||||
Reference in New Issue
Block a user