Skip to content

Commit 4efb1a0

Browse files
committed
[refactor] MDX walker supports Async & Front Matter
[add] Lark OAuth 2.0 middleware [migrate] upgrade to MobX-Lark 2.1 & other latest Upstream packages
1 parent 6140b5a commit 4efb1a0

File tree

7 files changed

+349
-314
lines changed

7 files changed

+349
-314
lines changed

.env

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1-
NEXT_PUBLIC_SITE_NAME=Lark-Next-Bootstrap-ts
2-
NEXT_PUBLIC_SITE_SUMMARY=Lark project scaffold based on TypeScript, React, Next.js, Bootstrap & Workbox.
1+
NEXT_PUBLIC_SITE_NAME = Lark-Next-Bootstrap-ts
2+
NEXT_PUBLIC_SITE_SUMMARY = Lark project scaffold based on TypeScript, React, Next.js, Bootstrap & Workbox.
3+
NEXT_PUBLIC_LOGO = https://github.com/idea2app.png
34

45
NEXT_PUBLIC_SENTRY_DSN =
56
SENTRY_ORG =
67
SENTRY_PROJECT =
78

8-
LARK_API_HOST = https://open.larksuite.com/open-apis/
9+
NEXT_PUBLIC_LARK_API_HOST = https://open.larksuite.com/open-apis/
10+
NEXT_PUBLIC_LARK_APP_ID =

README.md

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -79,15 +79,15 @@ You can check out [the Next.js GitHub repository][27] - your feedback and contri
7979

8080
### Environment variables
8181

82-
| name | file | description |
83-
| :----------------------: | :----------: | :-----------------------: |
84-
| `SENTRY_AUTH_TOKEN` | `.env.local` | [Official document][28] |
85-
| `SENTRY_ORG` | `.env` | [Official document][29] |
86-
| `SENTRY_PROJECT` | `.env` | [Official document][29] |
87-
| `NEXT_PUBLIC_SENTRY_DSN` | `.env` | [Official document][30] |
88-
| `LARK_APP_ID` | `.env.local` | [Official document][31] |
89-
| `LARK_APP_SECRET` | `.env.local` | [Official document][31] |
90-
| `NEXT_PUBLIC_CACHE_HOST` | `.env` | Static files CDN for Lark |
82+
| name | file | description |
83+
| :-----------------------: | :----------: | :-----------------------: |
84+
| `SENTRY_AUTH_TOKEN` | `.env.local` | [Official document][28] |
85+
| `SENTRY_ORG` | `.env` | [Official document][29] |
86+
| `SENTRY_PROJECT` | `.env` | [Official document][29] |
87+
| `NEXT_PUBLIC_SENTRY_DSN` | `.env` | [Official document][30] |
88+
| `NEXT_PUBLIC_LARK_APP_ID` | `.env.local` | [Official document][31] |
89+
| `LARK_APP_SECRET` | `.env.local` | [Official document][31] |
90+
| `NEXT_PUBLIC_CACHE_HOST` | `.env` | Static files CDN for Lark |
9191

9292
### Vercel
9393

package.json

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@
1717
"@editorjs/quote": "~2.7.6",
1818
"@mdx-js/loader": "^3.1.0",
1919
"@mdx-js/react": "^3.1.0",
20-
"@next/mdx": "^15.2.2",
21-
"@sentry/nextjs": "^9.5.0",
20+
"@next/mdx": "^15.2.3",
21+
"@sentry/nextjs": "^9.6.0",
2222
"copy-webpack-plugin": "^13.0.0",
2323
"core-js": "^3.41.0",
2424
"editorjs-html": "^4.0.5",
@@ -30,16 +30,16 @@
3030
"lodash": "^4.17.21",
3131
"marked": "^15.0.7",
3232
"mime": "^4.0.6",
33-
"mobx": "^6.13.6",
33+
"mobx": "^6.13.7",
3434
"mobx-github": "^0.3.5",
3535
"mobx-i18n": "^0.6.0",
36-
"mobx-lark": "^2.0.0",
36+
"mobx-lark": "^2.1.0",
3737
"mobx-react": "^9.2.0",
3838
"mobx-restful": "^2.1.0",
3939
"mobx-restful-table": "^2.0.2",
40-
"next": "^15.2.2",
40+
"next": "^15.2.3",
4141
"next-pwa": "~5.6.0",
42-
"next-ssr-middleware": "^0.8.9",
42+
"next-ssr-middleware": "^0.8.10",
4343
"next-with-less": "^3.0.1",
4444
"prismjs": "^1.30.0",
4545
"react": "^19.0.0",
@@ -52,7 +52,8 @@
5252
"remark-mdx-frontmatter": "^5.0.0",
5353
"undici": "^7.5.0",
5454
"web-utility": "^4.4.3",
55-
"webpack": "^5.98.0"
55+
"webpack": "^5.98.0",
56+
"yaml": "^2.7.0"
5657
},
5758
"devDependencies": {
5859
"@babel/plugin-proposal-decorators": "^7.25.9",
@@ -68,9 +69,9 @@
6869
"@types/lodash": "^4.17.16",
6970
"@types/next-pwa": "^5.6.9",
7071
"@types/node": "^22.13.10",
71-
"@types/react": "^19.0.10",
72+
"@types/react": "^19.0.11",
7273
"eslint": "^9.22.0",
73-
"eslint-config-next": "^15.2.2",
74+
"eslint-config-next": "^15.2.3",
7475
"eslint-config-prettier": "^10.1.1",
7576
"eslint-plugin-react": "^7.37.4",
7677
"eslint-plugin-simple-import-sort": "^12.1.1",

pages/api/Lark/core.ts

Lines changed: 18 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -2,53 +2,28 @@ import { marked } from 'marked';
22
import {
33
LarkApp,
44
LarkData,
5-
normalizeText,
6-
TableCellLocation,
5+
normalizeTextArray,
76
TableCellText,
8-
TableCellValue,
97
} from 'mobx-lark';
8+
import { oauth2Signer } from 'next-ssr-middleware';
109

1110
import { safeAPI } from '../core';
1211

13-
export const lark = new LarkApp({
14-
host: process.env.LARK_API_HOST,
15-
id: process.env.LARK_APP_ID!,
12+
export const larkAppMeta = {
13+
host: process.env.NEXT_PUBLIC_LARK_API_HOST,
14+
id: process.env.NEXT_PUBLIC_LARK_APP_ID!,
1615
secret: process.env.LARK_APP_SECRET!,
17-
});
18-
19-
export interface TableFormViewItem
20-
extends Record<'name' | 'description' | 'shared_url', string>,
21-
Record<'shared' | 'submit_limit_once', boolean> {
22-
shared_limit: 'tenant_editable';
23-
}
24-
export type LarkFormData = LarkData<{ form: TableFormViewItem }>;
25-
26-
export const normalizeTextArray = (list: TableCellText[]) =>
27-
list.reduce(
28-
(sum, item) => {
29-
if (item.text === ',') sum.push('');
30-
else sum[sum.length - 1] += normalizeText(item);
31-
32-
return sum;
33-
},
34-
[''],
35-
);
16+
};
17+
export const lark = new LarkApp(larkAppMeta);
3618

3719
export const normalizeMarkdownArray = (list: TableCellText[]) =>
3820
normalizeTextArray(list).map(text => marked(text) as string);
3921

40-
export function coordinateOf(location: TableCellValue): [number, number] {
41-
const [longitude, latitude] =
42-
(location as TableCellLocation)?.location.split(',') || [];
43-
44-
return [+latitude, +longitude];
45-
}
46-
4722
export const proxyLark = <T extends LarkData>(
4823
dataFilter?: (path: string, data: T) => T,
4924
) =>
5025
safeAPI(async ({ method, url, headers, body }, response) => {
51-
await lark.getAccessToken();
26+
if (!headers.authorization) await lark.getAccessToken();
5227

5328
delete headers.host;
5429

@@ -67,3 +42,13 @@ export const proxyLark = <T extends LarkData>(
6742

6843
response.send(dataFilter?.(path, data!) || data);
6944
});
45+
46+
export const larkOauth2 = oauth2Signer({
47+
signInURL: URI => new LarkApp(larkAppMeta).getWebSignInURL(URI),
48+
accessToken: ({ code }) => new LarkApp(larkAppMeta).getUserAccessToken(code),
49+
userProfile: accessToken => {
50+
const { secret, ...option } = larkAppMeta;
51+
52+
return new LarkApp({ ...option, accessToken }).getUserMeta();
53+
},
54+
});

pages/api/core.ts

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { HTTPError } from 'koajax';
2+
import { DataObject } from 'mobx-restful';
23
import { NextApiRequest, NextApiResponse } from 'next';
34
import { ProxyAgent, setGlobalDispatcher } from 'undici';
5+
import { parse } from 'yaml';
46

57
const { HTTP_PROXY } = process.env;
68

@@ -43,3 +45,75 @@ export function safeAPI(handler: NextAPI): NextAPI {
4345
}
4446
};
4547
}
48+
49+
export interface ArticleMeta {
50+
name: string;
51+
path?: string;
52+
meta?: DataObject;
53+
subs: ArticleMeta[];
54+
}
55+
56+
const MDX_pattern = /\.mdx?$/;
57+
58+
export async function frontMatterOf(path: string) {
59+
const { readFile } = await import('fs/promises');
60+
61+
const file = await readFile(path, 'utf-8');
62+
63+
const [, frontMatter] = file.match(/^---[\r\n]([\s\S]+?[\r\n])---/) || [];
64+
65+
return frontMatter && parse(frontMatter);
66+
}
67+
68+
export async function* pageListOf(
69+
path: string,
70+
prefix = 'pages',
71+
): AsyncGenerator<ArticleMeta> {
72+
const { readdir } = await import('fs/promises');
73+
74+
const list = await readdir(prefix + path, { withFileTypes: true });
75+
76+
for (const node of list) {
77+
let { name, path } = node;
78+
79+
if (name.startsWith('.')) continue;
80+
81+
const isMDX = MDX_pattern.test(name);
82+
83+
name = name.replace(MDX_pattern, '');
84+
path = `${path}/${name}`.replace(new RegExp(`^${prefix}`), '');
85+
86+
if (node.isFile())
87+
if (isMDX) {
88+
const article: ArticleMeta = { name, path, subs: [] };
89+
try {
90+
const meta = await frontMatterOf(`${node.path}/${node.name}`);
91+
92+
if (meta) article.meta = meta;
93+
} catch (error) {
94+
console.error(error);
95+
}
96+
yield article;
97+
} else continue;
98+
99+
if (!node.isDirectory()) continue;
100+
101+
const subs = await Array.fromAsync(pageListOf(path, prefix));
102+
103+
if (subs[0]) yield { name, subs };
104+
}
105+
}
106+
107+
export type TreeNode<K extends string> = {
108+
[key in K]: TreeNode<K>[];
109+
};
110+
111+
export function* traverseTree<K extends string>(
112+
tree: TreeNode<K>,
113+
key: K,
114+
): Generator<TreeNode<K>> {
115+
for (const node of tree[key] || []) {
116+
yield node;
117+
yield* traverseTree(node, key);
118+
}
119+
}

pages/article/index.tsx

Lines changed: 20 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,61 +1,33 @@
11
import { observer } from 'mobx-react';
2-
import { GetStaticProps, InferGetStaticPropsType } from 'next';
2+
import { InferGetStaticPropsType } from 'next';
33
import { FC } from 'react';
44

55
import { MDXLayout } from '../../components/Layout/MDXLayout';
66
import { i18n } from '../../models/Translation';
7+
import { ArticleMeta, pageListOf, traverseTree } from '../api/core';
78

8-
interface ArticleMeta {
9-
name: string;
10-
path?: string;
11-
subs: ArticleMeta[];
12-
}
9+
export const getStaticProps = async () => {
10+
const tree = await Array.fromAsync(pageListOf('/article'));
11+
const list = tree.map(root => [...traverseTree(root, 'subs')]).flat();
1312

14-
const MDX_pattern = /\.mdx?$/;
15-
16-
export const getStaticProps: GetStaticProps<{
17-
list: ArticleMeta[];
18-
}> = async () => {
19-
const { readdirSync } = await import('fs');
20-
21-
const pageListOf = (path: string, prefix = 'pages'): ArticleMeta[] =>
22-
readdirSync(prefix + path, { withFileTypes: true })
23-
.map(node => {
24-
let { name, path } = node;
25-
26-
if (name.startsWith('.')) return;
27-
28-
const isMDX = MDX_pattern.test(name);
29-
30-
name = name.replace(MDX_pattern, '');
31-
path = `${path}/${name}`.replace(new RegExp(`^${prefix}`), '');
32-
33-
if (node.isFile()) return isMDX && { name, path };
34-
35-
if (!node.isDirectory()) return;
36-
37-
const subs = pageListOf(path, prefix);
38-
39-
return subs[0] && { name, subs };
40-
})
41-
.filter(Boolean) as ArticleMeta[];
42-
43-
try {
44-
const list = pageListOf('/article');
45-
46-
return { props: { list } };
47-
} catch {
48-
return { props: { list: [] } };
49-
}
13+
return { props: { tree, list } };
5014
};
5115

5216
const renderTree = (list: ArticleMeta[]) => (
5317
<ol>
54-
{list.map(({ name, path, subs }) => (
18+
{list.map(({ name, path, meta, subs }) => (
5519
<li key={name}>
5620
{path ? (
57-
<a className="h4" href={path}>
58-
{name}
21+
<a
22+
className="h4 d-flex justify-content-between align-items-center"
23+
href={path}
24+
>
25+
{name}{' '}
26+
{meta && (
27+
<time className="fs-6" dateTime={meta.updated || meta.date}>
28+
{meta.updated || meta.date}
29+
</time>
30+
)}
5931
</a>
6032
) : (
6133
<details>
@@ -69,9 +41,9 @@ const renderTree = (list: ArticleMeta[]) => (
6941
);
7042

7143
const ArticleIndexPage: FC<InferGetStaticPropsType<typeof getStaticProps>> =
72-
observer(({ list }) => (
73-
<MDXLayout className="" title={i18n.t('article')}>
74-
{renderTree(list)}
44+
observer(({ tree, list: { length } }) => (
45+
<MDXLayout className="" title={`${i18n.t('article')} (${length})`}>
46+
{renderTree(tree)}
7547
</MDXLayout>
7648
));
7749

0 commit comments

Comments
 (0)