Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions models/configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export const Name = process.env.NEXT_PUBLIC_SITE_NAME,
DefaultImage = process.env.NEXT_PUBLIC_LOGO!;

export const {
CI,
SMTP_HOST,
SMTP_PORT = 465,
SMTP_USER,
Expand Down
19 changes: 15 additions & 4 deletions next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,20 +38,31 @@ const rewrites: NextConfig['rewrites'] = async () => ({
source: '/proxy/geo.datav.aliyun.com/:path*',
destination: 'https://geo.datav.aliyun.com/:path*',
},
{
source: '/recipe/images/:path*',
destination: 'https://raw.githubusercontent.com/Gar-b-age/CookLikeHOC/main/images/:path*',
},
],
afterFiles: [],
});

const redirects: NextConfig['redirects'] = async () =>
['/wiki', '/recipe', '/policy'].flatMap(route => [
{
source: route,
destination: `https://wiki.fcc-cd.dev${route}`,
permanent: true,
},
{
source: `${route}/:path*`,
destination: `https://wiki.fcc-cd.dev${route}/:path*`,
permanent: true,
},
]);

export default withPWA(
withLess(
withMDX({
pageExtensions: ['js', 'jsx', 'ts', 'tsx', 'md', 'mdx'],
output: CI ? 'standalone' : undefined,
rewrites,
redirects,
}),
),
);
17 changes: 6 additions & 11 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"install": "pnpx git-utility download https://github.com/Open-Source-Bazaar/key-vault main Open-Source-Bazaar.github.io || true",
"postinstall": "next typegen",
"dev": "next dev --webpack",
"build": "next build --webpack",
"build": "cross-env CI=true next build --webpack",
"start": "next start",
"test": "lint-staged && git add . && tsc --noEmit"
},
Expand All @@ -27,10 +27,10 @@
"koa-jwt": "^4.0.4",
"koajax": "^3.3.0",
"license-filter": "^0.2.5",
"marked": "^18.0.6",
"marked": "^15.0.12",
"mime": "^4.1.0",
"mobx": "^6.16.1",
"mobx-github": "^0.6.2",
"mobx-github": "^0.7.0",
"mobx-i18n": "^0.7.5",
"mobx-lark": "^2.10.0",
"mobx-react": "^9.2.2",
Expand Down Expand Up @@ -66,6 +66,7 @@
"@types/nodemailer": "^8.0.1",
"@types/react": "^19.2.17",
"@types/react-dom": "^19.2.3",
"cross-env": "^10.1.0",
"eslint": "^10.7.0",
"eslint-config-next": "16.2.10",
"eslint-config-prettier": "^10.1.8",
Expand All @@ -82,17 +83,11 @@
"prettier-plugin-css-order": "^2.2.0",
"sass": "^1.101.0",
"typescript": "~5.9.3",
"typescript-eslint": "^8.63.0"
"typescript-eslint": "^8.64.0"
},
"resolutions": {
"mobx-react-helper": "$mobx-react-helper",
"next": "$next"
},
"pnpm": {
"patchedDependencies": {
"next@16.2.10": "patches/next@16.2.10.patch"
}
},
"prettier": {
"singleQuote": true,
"trailingComma": "all",
Expand All @@ -104,6 +99,6 @@
]
},
"lint-staged": {
"*.{html,md,less,scss,json,yml,js,mjs,ts,tsx}": "prettier --write"
"*.{html,md,less,json,yml,js,mjs,ts,tsx}": "prettier --write"
}
}
20 changes: 7 additions & 13 deletions pages/NGO/[year]/index.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { observer } from 'mobx-react';
import dynamic from 'next/dynamic';
import { GetStaticPaths, GetStaticProps } from 'next';
import { GetStaticPaths } from 'next';
import { FC, useContext } from 'react';
import { Button, Container } from 'react-bootstrap';
import { Minute, Second } from 'web-utility';

import { PageHead } from '../../../components/Layout/PageHead';
import { CityStatisticMap } from '../../../components/Map/CityStatisticMap';
Expand All @@ -15,6 +14,7 @@ import {
} from '../../../models/Organization';
import { I18nContext } from '../../../models/Translation';
import { lark } from '../../api/Lark/core';
import { skipBuilding } from '../../api/SSG';

const OrganizationCharts = dynamic(() => import('../../../components/Organization/Charts'), {
ssr: false,
Expand All @@ -41,12 +41,10 @@ export const getStaticPaths: GetStaticPaths<{ year: string }> = async () => {
};
};

export const getStaticProps: GetStaticProps<OrganizationPageProps, { year: string }> = async ({
params,
}) => {
const { year } = params!;
export const getStaticProps = skipBuilding<OrganizationPageProps, { year: string }>(
async ({ params }) => {
const { year } = params!;

try {
await lark.getAccessToken();

const organizationStore = new OrganizationModel();
Expand All @@ -55,12 +53,8 @@ export const getStaticProps: GetStaticProps<OrganizationPageProps, { year: strin
const statistic = await organizationStore.getStatistic({ startYear: year });

return { props: { year, statistic } };
} catch (error) {
console.error(error);

return { notFound: true, revalidate: Minute / Second };
}
};
},
);

const OrganizationPage: FC<OrganizationPageProps> = observer(({ year, statistic }) => {
const { t } = useContext(I18nContext);
Expand Down
21 changes: 7 additions & 14 deletions pages/NGO/[year]/landscape.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { observer } from 'mobx-react';
import { GetStaticPaths, GetStaticProps } from 'next';
import { GetStaticPaths } from 'next';
import { FC, useContext } from 'react';
import { Container } from 'react-bootstrap';
import { Minute, Second } from 'web-utility';

import { PageHead } from '../../../components/Layout/PageHead';
import {
Expand All @@ -12,6 +11,7 @@ import {
import { OrganizationModel, OrganizationYearStatisticModel } from '../../../models/Organization';
import { I18nContext } from '../../../models/Translation';
import { lark } from '../../api/Lark/core';
import { skipBuilding } from '../../api/SSG';

export const getStaticPaths: GetStaticPaths<{ year: string }> = async () => {
await lark.getAccessToken();
Expand All @@ -29,13 +29,10 @@ export const getStaticPaths: GetStaticPaths<{ year: string }> = async () => {
};
};

export const getStaticProps: GetStaticProps<
Pick<OrganizationModel, 'typeMap'>,
{ year: string }
> = async ({ params }) => {
const { year } = params!;
export const getStaticProps = skipBuilding<Pick<OrganizationModel, 'typeMap'>, { year: string }>(
async ({ params }) => {
const { year } = params!;

try {
await lark.getAccessToken();

const organizationStore = new OrganizationModel();
Expand All @@ -44,12 +41,8 @@ export const getStaticProps: GetStaticProps<
const typeMap = await organizationStore.groupAllByType({ startYear: year });

return { props: JSON.parse(JSON.stringify({ typeMap })) };
} catch (error) {
console.error(error);

return { notFound: true, revalidate: Minute / Second };
}
};
},
);

const LandscapePage: FC<OpenCollaborationLandscapeProps> = observer(props => {
const { t } = useContext(I18nContext);
Expand Down
25 changes: 25 additions & 0 deletions pages/api/SSG.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { DataObject } from 'mobx-restful';
import { GetStaticProps, GetStaticPropsResult } from 'next';
import { ParsedUrlQuery } from 'querystring';
import { Minute, Second } from 'web-utility';

import { CI } from '../../models/configuration';

export const skipBuilding =
<Props extends DataObject, Params extends ParsedUrlQuery = ParsedUrlQuery>(
rawHandler: GetStaticProps<Props, Params>,
revalidate = Minute / Second,
): GetStaticProps<Props, Params> =>
async context => {
const fallback: GetStaticPropsResult<any> = { notFound: true, revalidate };

if (CI) return fallback;

try {
return await rawHandler(context);
} catch (error) {
console.error(error);

return fallback;
}
Comment on lines +14 to +24

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

遵循规范让错误自然冒泡,并严谨判断 CI 环境变量

根据项目编码规范要求:“For static generation, allow errors to bubble naturally (do not swallow errors)”,静态数据获取阶段不应该使用 try/catch 吞没错误。直接捕获错误并返回 404 状态会导致 CI/CD 环境下无法正确暴露构建失败,并且可能会导致暂时的接口异常被作为 404 页面缓存下来。

此外,环境变量 CI 解析后为字符串类型。如果在运行命令时显式设定了 CI=false,原先的 if (CI) 依然会判定为真并意外跳过构建。建议采用严格的字符串值比对。

💡 建议的修复方案

移除多余的类型和错误捕获代码,让错误在构建阶段自然向上冒泡:

-    const fallback: GetStaticPropsResult<any> = { notFound: true, revalidate };
-
-    if (CI) return fallback;
-
-    try {
-      return await rawHandler(context);
-    } catch (error) {
-      console.error(error);
-
-      return fallback;
-    }
+    if (String(CI).toLowerCase() === 'true') {
+      return { notFound: true, revalidate };
+    }
+
+    return await rawHandler(context);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const fallback: GetStaticPropsResult<any> = { notFound: true, revalidate };
if (CI) return fallback;
try {
return await rawHandler(context);
} catch (error) {
console.error(error);
return fallback;
}
if (String(CI).toLowerCase() === 'true') {
return { notFound: true, revalidate };
}
return await rawHandler(context);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pages/api/SSG.ts` around lines 14 - 24, Update the static-generation handler
around rawHandler and CI: remove the try/catch so errors from
rawHandler(context) bubble naturally, and replace the truthiness check on CI
with an explicit string comparison that only skips generation when CI is exactly
"true". Preserve the existing fallback result for the intended CI path.

Source: Coding guidelines

};
93 changes: 0 additions & 93 deletions pages/api/core.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
import 'core-js/full/array/from-async';

import { JsonWebTokenError, sign } from 'jsonwebtoken';
import { Context, Middleware, ParameterizedContext } from 'koa';
import JWT from 'koa-jwt';
import { HTTPError } from 'koajax';
import { Content } from 'mobx-github';
import { DataObject } from 'mobx-restful';
import { KoaOption, withKoa } from 'next-ssr-middleware';
import { parse } from 'yaml';

import { LarkAppMeta } from '../../models/configuration';

Expand Down Expand Up @@ -60,92 +56,3 @@ export const safeAPI: Middleware<any, any> = async (context: Context, next) => {

export const withSafeKoa = <S, C>(...middlewares: Middleware<S, C>[]) =>
withKoa<S, C>({} as KoaOption, safeAPI, ...middlewares);

export interface ArticleMeta {
name: string;
path?: string;
meta?: DataObject;
subs: ArticleMeta[];
}

export const MD_pattern = /\.(md|markdown)$/i,
MDX_pattern = /\.mdx?$/i;

export function splitFrontMatter(raw: string) {
const [, frontMatter, markdown] =
raw.trim().match(/^---[\r\n]([\s\S]+?[\r\n])---[\r\n]([\s\S]*)/) || [];

if (!frontMatter) return { markdown: raw };

try {
const meta = parse(frontMatter) as DataObject;

return { markdown, meta };
} catch (error) {
console.error(`Error parsing Front Matter:`, error);

return { markdown };
}
}

export async function* pageListOf(path: string, prefix = 'pages'): AsyncGenerator<ArticleMeta> {
const { readdir, readFile } = await import('fs/promises');

const list = await readdir(prefix + path, { withFileTypes: true });

for (const node of list) {
let { name, parentPath } = node;

if (name.startsWith('.')) continue;

const isMDX = MDX_pattern.test(name);

name = name.replace(MDX_pattern, '');
const path = `${parentPath}/${name}`.replace(new RegExp(`^${prefix}`), '');

if (node.isFile() && isMDX) {
const article: ArticleMeta = { name, path, subs: [] };

const file = await readFile(`${parentPath}/${node.name}`, 'utf-8');

const { meta } = splitFrontMatter(file);

if (meta) article.meta = meta;

yield article;
}
if (!node.isDirectory()) continue;

const subs = await Array.fromAsync(pageListOf(path, prefix));

if (subs[0]) yield { name, subs };
}
}

export type TreeNode<K extends string> = {
[key in K]: TreeNode<K>[];
};

export function* traverseTree<K extends string, N extends TreeNode<K>>(
tree: N,
key: K,
): Generator<N> {
for (const node of tree[key] || []) {
yield node as N;
yield* traverseTree(node as N, key);
}
}

export const filterMarkdownFiles = (nodes: Content[]) =>
nodes
.filter(
({ path, type, name }) =>
!path.startsWith('.') &&
!name.startsWith('.') &&
(type !== 'file' || MD_pattern.test(name)),
)
.map(({ content, ...rest }) => {
const { meta, markdown } = content ? splitFrontMatter(content) : {};

return { ...rest, content: markdown, meta };
});
Loading
Loading