Skip to content

Commit 8520db3

Browse files
authored
feat: api route (#19209)
* feat: api route * test: api route
1 parent 964a386 commit 8520db3

File tree

9 files changed

+175
-8
lines changed

9 files changed

+175
-8
lines changed

lib/middleware/template.test.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,4 +114,12 @@ describe('template', () => {
114114
expect(response.status).toBe(301);
115115
expect(response.headers.get('location')).toBe('/test/1');
116116
});
117+
118+
it(`api`, async () => {
119+
const response = await app.request('/api/test');
120+
expect(response.status).toBe(200);
121+
expect(await response.json()).toEqual({
122+
code: 0,
123+
});
124+
});
117125
});

lib/middleware/template.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@ const middleware: MiddlewareHandler = async (ctx, next) => {
1414
const ttl = (cacheModule.status.available && Math.trunc(config.cache.routeExpire / 60)) || 1;
1515
await next();
1616

17+
const apiData = ctx.get('apiData');
18+
if (apiData) {
19+
return ctx.json(apiData);
20+
}
21+
1722
const data: Data = ctx.get('data');
1823
const outputType = ctx.req.query('format') || 'rss';
1924

lib/registry.ts

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { Namespace, Route } from '@/types';
1+
import type { APIRoute, Namespace, Route } from '@/types';
22
import { directoryImport } from 'directory-import';
33
import { Hono, type Handler } from 'hono';
44
import path from 'node:path';
@@ -22,6 +22,12 @@ let namespaces: Record<
2222
location: string;
2323
}
2424
>;
25+
apiRoutes: Record<
26+
string,
27+
APIRoute & {
28+
location: string;
29+
}
30+
>;
2531
}
2632
> = {};
2733

@@ -48,6 +54,9 @@ if (Object.keys(modules).length) {
4854
}
4955
| {
5056
namespace: Namespace;
57+
}
58+
| {
59+
apiRoute: APIRoute;
5160
};
5261
const namespace = module.split(/[/\\]/)[1];
5362
if ('namespace' in content) {
@@ -63,6 +72,7 @@ if (Object.keys(modules).length) {
6372
namespaces[namespace] = {
6473
name: namespace,
6574
routes: {},
75+
apiRoutes: {},
6676
};
6777
}
6878
if (Array.isArray(content.route.path)) {
@@ -78,6 +88,27 @@ if (Object.keys(modules).length) {
7888
location: module.split(/[/\\]/).slice(2).join('/'),
7989
};
8090
}
91+
} else if ('apiRoute' in content) {
92+
if (!namespaces[namespace]) {
93+
namespaces[namespace] = {
94+
name: namespace,
95+
routes: {},
96+
apiRoutes: {},
97+
};
98+
}
99+
if (Array.isArray(content.apiRoute.path)) {
100+
for (const path of content.apiRoute.path) {
101+
namespaces[namespace].apiRoutes[path] = {
102+
...content.apiRoute,
103+
location: module.split(/[/\\]/).slice(2).join('/'),
104+
};
105+
}
106+
} else {
107+
namespaces[namespace].apiRoutes[content.apiRoute.path] = {
108+
...content.apiRoute,
109+
location: module.split(/[/\\]/).slice(2).join('/'),
110+
};
111+
}
81112
}
82113
}
83114
}
@@ -143,6 +174,42 @@ for (const namespace in namespaces) {
143174
}
144175
}
145176

177+
for (const namespace in namespaces) {
178+
const subApp = app.basePath(`/api/${namespace}`);
179+
180+
const namespaceData = namespaces[namespace];
181+
if (!namespaceData || !namespaceData.apiRoutes) {
182+
continue;
183+
}
184+
185+
const sortedRoutes = Object.entries(namespaceData.apiRoutes) as [
186+
string,
187+
APIRoute & {
188+
location: string;
189+
module?: () => Promise<{ apiRoute: APIRoute }>;
190+
},
191+
][];
192+
193+
for (const [path, routeData] of sortedRoutes) {
194+
const wrappedHandler: Handler = async (ctx) => {
195+
if (!ctx.get('apiData')) {
196+
if (typeof routeData.handler !== 'function') {
197+
if (process.env.NODE_ENV === 'test') {
198+
const { apiRoute } = await import(`./routes/${namespace}/${routeData.location}`);
199+
routeData.handler = apiRoute.handler;
200+
} else if (routeData.module) {
201+
const { apiRoute } = await routeData.module();
202+
routeData.handler = apiRoute.handler;
203+
}
204+
}
205+
const data = await routeData.handler(ctx);
206+
ctx.set('apiData', data);
207+
}
208+
};
209+
subApp.get(path, wrappedHandler);
210+
}
211+
}
212+
146213
app.get('/', index);
147214
app.get('/healthz', healthz);
148215
app.get('/robots.txt', robotstxt);

lib/routes/test/api-index.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { APIRoute } from '@/types';
2+
3+
export const apiRoute: APIRoute = {
4+
path: '/',
5+
maintainers: ['DIYgod'],
6+
handler,
7+
};
8+
9+
function handler() {
10+
return {
11+
code: 0,
12+
};
13+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { APIRoute } from '@/types';
2+
import { checkCookie } from './util';
3+
4+
export const apiRoute: APIRoute = {
5+
path: '/check-cookie',
6+
description: '检查小红书 cookie 是否有效',
7+
maintainers: ['DIYgod'],
8+
handler,
9+
};
10+
11+
async function handler() {
12+
const valid = await checkCookie();
13+
return {
14+
code: valid ? 0 : -1,
15+
};
16+
}

lib/routes/xiaohongshu/user.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ async function handler(ctx) {
6464
if (cookie && category === 'notes') {
6565
try {
6666
const urlNotePrefix = 'https://www.xiaohongshu.com/explore';
67-
const user = await getUserWithCookie(url, cookie);
67+
const user = await getUserWithCookie(url);
6868
const notes = await renderNotesFulltext(user.notes, urlNotePrefix, displayLivePhoto);
6969
return {
7070
title: `${user.userPageData.basicInfo.nickname} - 笔记 • 小红书 / RED`,

lib/routes/xiaohongshu/util.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { config } from '@/config';
22
import logger from '@/utils/logger';
33
import { parseDate } from '@/utils/parse-date';
44
import puppeteer from '@/utils/puppeteer';
5-
import { ofetch } from 'ofetch';
5+
import ofetch from '@/utils/ofetch';
66
import { load } from 'cheerio';
77
import cache from '@/utils/cache';
88

@@ -235,7 +235,8 @@ async function getFullNote(link, displayLivePhoto) {
235235
return data;
236236
}
237237

238-
async function getUserWithCookie(url: string, cookie: string) {
238+
async function getUserWithCookie(url: string) {
239+
const cookie = config.xiaohongshu.cookie;
239240
const res = await ofetch(url, {
240241
headers: getHeaders(cookie),
241242
});
@@ -267,4 +268,12 @@ function extractInitialState($) {
267268
return script;
268269
}
269270

270-
export { getUser, getBoard, formatText, formatNote, renderNotesFulltext, getFullNote, getUserWithCookie };
271+
async function checkCookie() {
272+
const cookie = config.xiaohongshu.cookie;
273+
const res = await ofetch('https://edith.xiaohongshu.com/api/sns/web/v2/user/me', {
274+
headers: getHeaders(cookie),
275+
});
276+
return res.code === 0 && !!res.data.user_id;
277+
}
278+
279+
export { getUser, getBoard, formatText, formatNote, renderNotesFulltext, getFullNote, getUserWithCookie, checkCookie };

lib/types.ts

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -359,14 +359,12 @@ interface RouteItem {
359359
view?: ViewType;
360360
}
361361

362-
interface Route extends RouteItem {
362+
export interface Route extends RouteItem {
363363
ja?: RouteItem;
364364
zh?: RouteItem;
365365
'zh-TW'?: RouteItem;
366366
}
367367

368-
export type { Route };
369-
370368
// radar
371369
export type RadarItem = {
372370
/**
@@ -412,3 +410,50 @@ export type RadarDomain = {
412410
} & {
413411
[subdomain: string]: RadarItem[];
414412
};
413+
414+
export interface APIRoute {
415+
/**
416+
* The route path, using [Hono routing](https://hono.dev/api/routing) syntax
417+
*/
418+
path: string;
419+
420+
/**
421+
* The GitHub handle of the people responsible for maintaining this route
422+
*/
423+
maintainers: string[];
424+
425+
/**
426+
* The handler function of the route
427+
*/
428+
handler: (ctx: Context) =>
429+
| Promise<{
430+
code: number;
431+
message?: string;
432+
data?: any;
433+
}>
434+
| {
435+
code: number;
436+
message?: string;
437+
data?: any;
438+
};
439+
440+
/**
441+
* The description of the route parameters
442+
*/
443+
parameters?: Record<
444+
string,
445+
{
446+
description: string;
447+
default?: string;
448+
options?: {
449+
value: string;
450+
label: string;
451+
}[];
452+
}
453+
>;
454+
455+
/**
456+
* Hints and additional explanations for users using this route, it will be appended after the route component, supports markdown
457+
*/
458+
description?: string;
459+
}

scripts/workflow/build-routes.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,10 @@ for (const namespace in namespaces) {
6666
}
6767
data.module = `() => import('@/routes/${namespace}/${data.location}')`;
6868
}
69+
for (const path in namespaces[namespace].apiRoutes) {
70+
const data = namespaces[namespace].apiRoutes[path];
71+
data.module = `() => import('@/routes/${namespace}/${data.location}')`;
72+
}
6973
}
7074

7175
fs.writeFileSync(path.join(__dirname, '../../assets/build/radar-rules.json'), JSON.stringify(radar, null, 2));

0 commit comments

Comments
 (0)