Skip to content

Commit 3d248a6

Browse files
committed
[refactor] MDX walker supports Async & Front Matter
[migrate] upgrade to PNPM 10, Sentry 9, Next SSR middleware 0.9 & other latest Upstream packages [optimize] use TS for ESLint configuration
1 parent e87070c commit 3d248a6

File tree

6 files changed

+1282
-1176
lines changed

6 files changed

+1282
-1176
lines changed
File renamed without changes.

instrumentation.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
const { NEXT_RUNTIME } = process.env;
2+
3+
export async function register() {
4+
if (NEXT_RUNTIME === 'nodejs') await import('./sentry.server.config');
5+
6+
if (NEXT_RUNTIME === 'edge') await import('./sentry.edge.config');
7+
}

package.json

Lines changed: 36 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -17,70 +17,79 @@
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.3.0",
21+
"@sentry/nextjs": "^9.12.0",
2222
"copy-webpack-plugin": "^13.0.0",
2323
"core-js": "^3.41.0",
2424
"editorjs-html": "^4.0.5",
25-
"idea-react": "^2.0.0-rc.8",
26-
"koajax": "^3.1.1",
27-
"less": "^4.2.2",
25+
"idea-react": "^2.0.0-rc.9",
26+
"koajax": "^3.1.2",
27+
"less": "^4.3.0",
2828
"less-loader": "^12.2.0",
2929
"lodash": "^4.17.21",
30-
"mobx": "^6.13.6",
30+
"mobx": "^6.13.7",
3131
"mobx-github": "^0.3.5",
3232
"mobx-i18n": "^0.6.0",
3333
"mobx-react": "^9.2.0",
3434
"mobx-restful": "^2.1.0",
3535
"mobx-restful-table": "^2.0.2",
36-
"next": "^15.2.2",
36+
"next": "^15.3.0",
3737
"next-pwa": "~5.6.0",
38-
"next-ssr-middleware": "^0.8.9",
38+
"next-ssr-middleware": "^0.9.0",
3939
"next-with-less": "^3.0.1",
4040
"prismjs": "^1.30.0",
41-
"react": "^19.0.0",
41+
"react": "^19.1.0",
4242
"react-bootstrap": "^2.10.9",
4343
"react-bootstrap-editor": "^2.0.7",
44-
"react-dom": "^19.0.0",
44+
"react-dom": "^19.1.0",
4545
"react-editor-js": "^2.1.0",
4646
"remark-frontmatter": "^5.0.0",
4747
"remark-gfm": "^4.0.1",
48-
"remark-mdx-frontmatter": "^5.0.0",
49-
"undici": "^7.5.0",
48+
"remark-mdx-frontmatter": "^5.1.0",
49+
"undici": "^7.8.0",
5050
"web-utility": "^4.4.3",
51-
"webpack": "^5.98.0"
51+
"webpack": "^5.99.5",
52+
"yaml": "^2.7.1"
5253
},
5354
"devDependencies": {
5455
"@babel/plugin-proposal-decorators": "^7.25.9",
55-
"@babel/plugin-transform-typescript": "^7.26.8",
56+
"@babel/plugin-transform-typescript": "^7.27.0",
5657
"@babel/preset-react": "^7.26.3",
57-
"@cspell/eslint-plugin": "^8.17.5",
58-
"@eslint/compat": "^1.2.7",
59-
"@eslint/eslintrc": "^3.3.0",
60-
"@eslint/js": "^9.22.0",
61-
"@next/eslint-plugin-next": "^15.2.2",
58+
"@cspell/eslint-plugin": "^8.18.1",
59+
"@eslint/compat": "^1.2.8",
60+
"@eslint/eslintrc": "^3.3.1",
61+
"@eslint/js": "^9.24.0",
62+
"@next/eslint-plugin-next": "^15.3.0",
6263
"@softonus/prettier-plugin-duplicate-remover": "^1.1.2",
6364
"@stylistic/eslint-plugin": "^4.2.0",
6465
"@types/eslint-config-prettier": "^6.11.3",
6566
"@types/lodash": "^4.17.16",
6667
"@types/next-pwa": "^5.6.9",
67-
"@types/node": "^22.13.10",
68-
"@types/react": "^19.0.10",
69-
"eslint": "^9.22.0",
70-
"eslint-config-prettier": "^10.1.1",
71-
"eslint-plugin-react": "^7.37.4",
68+
"@types/node": "^22.14.0",
69+
"@types/react": "^19.1.0",
70+
"eslint": "^9.24.0",
71+
"eslint-config-prettier": "^10.1.2",
72+
"eslint-plugin-react": "^7.37.5",
7273
"eslint-plugin-simple-import-sort": "^12.1.1",
7374
"globals": "^16.0.0",
7475
"husky": "^9.1.7",
76+
"jiti": "^2.4.2",
7577
"lint-staged": "^15.5.0",
7678
"prettier": "^3.5.3",
7779
"prettier-plugin-css-order": "^2.1.2",
78-
"typescript": "~5.8.2",
79-
"typescript-eslint": "^8.26.1"
80+
"typescript": "~5.8.3",
81+
"typescript-eslint": "^8.29.1"
8082
},
8183
"resolutions": {
8284
"next": "$next"
8385
},
86+
"pnpm": {
87+
"onlyBuiltDependencies": [
88+
"@sentry/cli",
89+
"core-js",
90+
"sharp"
91+
]
92+
},
8493
"prettier": {
8594
"singleQuote": true,
8695
"trailingComma": "all",
@@ -100,7 +109,7 @@
100109
"build": "next build",
101110
"export": "next build && next export",
102111
"start": "next start",
103-
"lint": "next lint && tsc --noEmit",
112+
"lint": "next lint --fix && tsc --noEmit",
104113
"lint:all": "eslint --fix .",
105114
"lint:inspect": "eslint --inspect-config",
106115
"test": "lint-staged && npm run lint",

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/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)