diff --git a/package.json b/package.json index 6e42ee8..9b37614 100644 --- a/package.json +++ b/package.json @@ -62,6 +62,7 @@ "@iconify-json/ph": "^1.2.1", "@ourongxing/eslint-config": "3.2.3-beta.6", "@ourongxing/tsconfig": "^0.0.4", + "@rollup/pluginutils": "^5.1.3", "@tanstack/react-query": "^5.59.9", "@tanstack/router-devtools": "^1.64.0", "@tanstack/router-plugin": "^1.64.0", @@ -75,11 +76,13 @@ "eslint": "^9.12.0", "eslint-plugin-react-hooks": "^5.1.0-rc-77f43893-20241010", "eslint-plugin-react-refresh": "^0.4.12", + "fast-glob": "^3.3.2", "favicons-scraper": "^1.3.2", "lint-staged": "^15.2.10", "mlly": "^1.7.2", "mockdate": "^3.0.5", "pnpm-patch-i": "^0.4.1", + "rollup": "^4.24.0", "simple-git-hooks": "^2.11.1", "tsx": "^4.19.1", "typescript": "^5.6.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5c5c940..1f5a727 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -120,6 +120,9 @@ importers: '@ourongxing/tsconfig': specifier: ^0.0.4 version: 0.0.4 + '@rollup/pluginutils': + specifier: ^5.1.3 + version: 5.1.3(rollup@4.24.0) '@tanstack/react-query': specifier: ^5.59.9 version: 5.59.16(react@18.3.1) @@ -159,6 +162,9 @@ importers: eslint-plugin-react-refresh: specifier: ^0.4.12 version: 0.4.13(eslint@9.13.0(jiti@2.3.3)) + fast-glob: + specifier: ^3.3.2 + version: 3.3.2 favicons-scraper: specifier: ^1.3.2 version: 1.3.2 @@ -174,6 +180,9 @@ importers: pnpm-patch-i: specifier: ^0.4.1 version: 0.4.1 + rollup: + specifier: ^4.24.0 + version: 4.24.0 simple-git-hooks: specifier: ^2.11.1 version: 2.11.1 diff --git a/server/api/s/[id].ts b/server/api/s/[id].ts index c9eabd8..8271456 100644 --- a/server/api/s/[id].ts +++ b/server/api/s/[id].ts @@ -2,7 +2,7 @@ import process from "node:process" import { TTL } from "@shared/consts" import type { SourceID, SourceResponse } from "@shared/types" import { sources } from "@shared/sources" -import { sourcesGetters } from "#/sources" +import { getters } from "#/getters" import { useCache } from "#/hooks/useCache" export default defineEventHandler(async (event): Promise => { @@ -10,7 +10,7 @@ export default defineEventHandler(async (event): Promise => { let id = getRouterParam(event, "id") as SourceID const query = getQuery(event) const latest = query.latest !== undefined && query.latest !== "false" - const isValid = (id: SourceID) => !id || !sources[id] || !sourcesGetters[id] + const isValid = (id: SourceID) => !id || !sources[id] || !getters[id] if (isValid(id)) { const redirectID = sources?.[id]?.redirect @@ -54,7 +54,7 @@ export default defineEventHandler(async (event): Promise => { } } - const data = (await sourcesGetters[id]()).slice(0, 30) + const data = (await getters[id]()).slice(0, 30) logger.success(`fetch ${id} latest`) if (cacheTable) { if (event.context.waitUntil) event.context.waitUntil(cacheTable.set(id, data)) diff --git a/server/getters.ts b/server/getters.ts new file mode 100644 index 0000000..1255505 --- /dev/null +++ b/server/getters.ts @@ -0,0 +1,16 @@ +import { typeSafeObjectEntries } from "@shared/type.util" +import type { SourceID } from "@shared/types" +import * as x from "glob:./sources/{*.ts,**/index.ts}" +import type { SourceGetter } from "./types" + +export const getters = (function () { + const getters = {} as Record + typeSafeObjectEntries(x).forEach(([id, x]) => { + if (x.default instanceof Function) { + Object.assign(getters, { [id]: x.default }) + } else { + Object.assign(getters, x.default) + } + }) + return getters +})() diff --git a/server/glob.d.ts b/server/glob.d.ts new file mode 100644 index 0000000..aa23646 --- /dev/null +++ b/server/glob.d.ts @@ -0,0 +1,22 @@ +/* eslint-disable */ + +declare module 'glob:./sources/{*.ts,**/index.ts}' { + export const _36kr: typeof import('./sources/_36kr') + export const cankaoxiaoxi: typeof import('./sources/cankaoxiaoxi') + export const cls: typeof import('./sources/cls/index') + export const coolapk: typeof import('./sources/coolapk/index') + export const douyin: typeof import('./sources/douyin') + export const fastbull: typeof import('./sources/fastbull') + export const gelonghui: typeof import('./sources/gelonghui') + export const ithome: typeof import('./sources/ithome') + export const sputniknewscn: typeof import('./sources/sputniknewscn') + export const thepaper: typeof import('./sources/thepaper') + export const tieba: typeof import('./sources/tieba') + export const toutiao: typeof import('./sources/toutiao') + export const v2ex: typeof import('./sources/v2ex') + export const wallstreetcn: typeof import('./sources/wallstreetcn') + export const weibo: typeof import('./sources/weibo') + export const xueqiu: typeof import('./sources/xueqiu') + export const zaobao: typeof import('./sources/zaobao') + export const zhihu: typeof import('./sources/zhihu') +} diff --git a/server/sources/36kr.ts b/server/sources/_36kr.ts similarity index 100% rename from server/sources/36kr.ts rename to server/sources/_36kr.ts diff --git a/server/sources/coolapk/index.ts b/server/sources/coolapk/index.ts index 293b4c1..5136063 100644 --- a/server/sources/coolapk/index.ts +++ b/server/sources/coolapk/index.ts @@ -20,19 +20,21 @@ interface Res { }[] } -export default defineSource(async () => { - const url = "https://api.coolapk.com/v6/page/dataList?url=%2Ffeed%2FstatList%3FcacheExpires%3D300%26statType%3Dday%26sortField%3Ddetailnum%26title%3D%E4%BB%8A%E6%97%A5%E7%83%AD%E9%97%A8&title=%E4%BB%8A%E6%97%A5%E7%83%AD%E9%97%A8&subTitle=&page=1" - const r: Res = await $fetch(url, { - headers: await genHeaders(), - }) - if (!r.data.length) throw new Error("Failed to fetch") - return r.data.filter(k => k.id).map(i => ({ - id: i.id, - title: i.editor_title || load(i.message).text().split("\n")[0], - url: i.shareUrl, - extra: { - info: i.targetRow?.subTitle, - // date: new Date(i.dateline * 1000).getTime(), - }, - })).slice(0, 30) +export default defineSource({ + coolapk: async () => { + const url = "https://api.coolapk.com/v6/page/dataList?url=%2Ffeed%2FstatList%3FcacheExpires%3D300%26statType%3Dday%26sortField%3Ddetailnum%26title%3D%E4%BB%8A%E6%97%A5%E7%83%AD%E9%97%A8&title=%E4%BB%8A%E6%97%A5%E7%83%AD%E9%97%A8&subTitle=&page=1" + const r: Res = await $fetch(url, { + headers: await genHeaders(), + }) + if (!r.data.length) throw new Error("Failed to fetch") + return r.data.filter(k => k.id).map(i => ({ + id: i.id, + title: i.editor_title || load(i.message).text().split("\n")[0], + url: i.shareUrl, + extra: { + info: i.targetRow?.subTitle, + // date: new Date(i.dateline * 1000).getTime(), + }, + })) + }, }) diff --git a/server/sources/douyin.ts b/server/sources/douyin.ts index fe5a661..3a2c132 100644 --- a/server/sources/douyin.ts +++ b/server/sources/douyin.ts @@ -17,12 +17,11 @@ export default defineSource(async () => { cookie: cookie.join("; "), }, }) - return res.data.word_list - .map((k) => { - return { - id: k.sentence_id, - title: k.word, - url: `https://www.douyin.com/hot/${k.sentence_id}`, - } - }) + return res.data.word_list.map((k) => { + return { + id: k.sentence_id, + title: k.word, + url: `https://www.douyin.com/hot/${k.sentence_id}`, + } + }) }) diff --git a/server/sources/index.ts b/server/sources/index.ts deleted file mode 100644 index 851fb85..0000000 --- a/server/sources/index.ts +++ /dev/null @@ -1,41 +0,0 @@ -import type { DisabledSourceID, SourceID } from "@shared/types" -import weibo from "./weibo" -import zaobao from "./zaobao" -import v2ex from "./v2ex" -import ithome from "./ithome" -import zhihu from "./zhihu" -import cankaoxiaoxi from "./cankaoxiaoxi" -import coolapk from "./coolapk" -import kr36 from "./36kr" -import wallstreetcn from "./wallstreetcn" -import douyin from "./douyin" -import toutiao from "./toutiao" -import cls from "./cls" -import sputniknewscn from "./sputniknewscn" -import xueqiu from "./xueqiu" -import gelonghui from "./gelonghui" -import tieba from "./tieba" -import thepaper from "./thepaper" -import fastbull from "./fastbull" -import type { SourceGetter } from "#/types" - -export const sourcesGetters = { - weibo, - zaobao, - ...v2ex, - ithome, - zhihu, - coolapk, - cankaoxiaoxi, - thepaper, - sputniknewscn, - ...fastbull, - ...wallstreetcn, - ...xueqiu, - gelonghui, - douyin, - ...cls, - toutiao, - tieba, - ...kr36, -} as Record & Partial> diff --git a/server/sources/zhihu.ts b/server/sources/zhihu.ts index b76f390..1e5eb80 100644 --- a/server/sources/zhihu.ts +++ b/server/sources/zhihu.ts @@ -19,19 +19,21 @@ interface Res { }[] } -export default defineSource(async () => { - const url = "https://www.zhihu.com/api/v3/feed/topstory/hot-lists/total?limit=20&desktop=true" - const res: Res = await $fetch(url) - return res.data - .slice(0, 30) - .map((k) => { - return { - id: k.target.id, - title: k.target.title, - extra: { - icon: k.card_label?.night_icon, - }, - url: `https://www.zhihu.com/question/${k.target.id}`, - } - }) +export default defineSource({ + zhihu: async () => { + const url = "https://www.zhihu.com/api/v3/feed/topstory/hot-lists/total?limit=20&desktop=true" + const res: Res = await $fetch(url) + return res.data + .slice(0, 30) + .map((k) => { + return { + id: k.target.id, + title: k.target.title, + extra: { + icon: k.card_label?.night_icon, + }, + url: `https://www.zhihu.com/question/${k.target.id}`, + } + }) + }, }) diff --git a/tools/rollup-glob.ts b/tools/rollup-glob.ts new file mode 100644 index 0000000..7934230 --- /dev/null +++ b/tools/rollup-glob.ts @@ -0,0 +1,78 @@ +import path from "node:path" +import { writeFile } from "node:fs/promises" +import type { Plugin } from "rollup" +import glob from "fast-glob" +import type { FilterPattern } from "@rollup/pluginutils" +import { createFilter, normalizePath } from "@rollup/pluginutils" +import { projectDir } from "../shared/dir" + +const ID_PREFIX = "glob:" +const root = path.join(projectDir, "server") +type GlobMap = Record + +export function RollopGlob(): Plugin { + const map: GlobMap = {} + const include: FilterPattern = [] + const exclude: FilterPattern = [] + const filter = createFilter(include, exclude) + return { + name: "rollup-glob", + resolveId(id, src) { + if (!id.startsWith(ID_PREFIX)) return + if (!src || !filter(src)) return + + return `${id}:${encodeURIComponent(src)}` + }, + async load(id) { + if (!id.startsWith(ID_PREFIX)) return + + const [_, pattern, encodePath] = id.split(":") + const currentPath = decodeURIComponent(encodePath) + + const files = ( + await glob(pattern, { + cwd: currentPath ? path.dirname(currentPath) : root, + absolute: true, + }) + ) + .map(file => normalizePath(file)) + .filter(file => file !== normalizePath(currentPath)) + .sort() + map[pattern] = files + + const contents = files.map((file) => { + const r = file.replace("/index", "") + const name = path.basename(r, path.extname(r)) + return `export * as ${name} from '${file}'\n` + }).join("\n") + + await writeTypeDeclaration(map, path.join(root, "glob")) + + return `${contents}\n` + }, + } +} + +async function writeTypeDeclaration(map: GlobMap, filename: string) { + function relatePath(filepath: string) { + return normalizePath(path.relative(path.dirname(filename), filepath)) + } + + let declare = `/* eslint-disable */\n\n` + + const sortedEntries = Object.entries(map).sort(([a], [b]) => + a.localeCompare(b), + ) + + for (const [_idx, [id, files]] of sortedEntries.entries()) { + declare += `declare module '${ID_PREFIX}${id}' {\n` + for (const file of files) { + const relative = `./${relatePath(file)}`.replace(/\.tsx?$/, "") + const r = file.replace("/index", "") + const fileName = path.basename(r, path.extname(r)) + declare += ` export const ${fileName}: typeof import('${relative}')\n` + } + declare += `}\n` + } + await writeFile(`${filename}.d.ts`, declare, "utf-8") +} diff --git a/tsconfig.node.json b/tsconfig.node.json index 3b94848..aaa840c 100644 --- a/tsconfig.node.json +++ b/tsconfig.node.json @@ -8,5 +8,5 @@ "@shared/*": ["shared/*"] } }, - "include": ["server", "*.config.*", "shared", "test", "scripts", "dist/.nitro/types"] + "include": ["server", "*.config.*", "shared", "test", "scripts", "tools", "dist/.nitro/types"] } diff --git a/vite.config.ts b/vite.config.ts index 28513d4..855f5f6 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -10,6 +10,7 @@ import dotenv from "dotenv" import type { VitePWAOptions } from "vite-plugin-pwa" import { VitePWA } from "vite-plugin-pwa" import { projectDir } from "./shared/dir" +import { RollopGlob } from "./tools/rollup-glob" dotenv.config({ path: join(projectDir, ".env.server"), @@ -62,6 +63,9 @@ const nitroOption: Parameters[0] = { experimental: { database: true, }, + rollupConfig: { + plugins: [RollopGlob()], + }, sourceMap: false, database: { default: {