feat: new build script

This commit is contained in:
DIYgod
2024-03-08 23:31:50 +08:00
parent 75ec77b765
commit 6902129eb0
13 changed files with 94 additions and 195 deletions

View File

@@ -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));

View File

@@ -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));

View 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));