Skip to content

Commit 34a68d5

Browse files
authored
[add] IdeaMall & EasyWebApp OSS based on GitHub API proxy (#50)
1 parent 5a56924 commit 34a68d5

20 files changed

+1149
-892
lines changed

.husky/pre-commit

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1 @@
1-
#!/bin/sh
2-
3-
. "$(dirname "$0")/_/husky.sh"
4-
51
npm test

.husky/pre-push

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1 @@
1-
#!/bin/sh
2-
3-
. "$(dirname "$0")/_/husky.sh"
4-
51
npm run build

Dockerfile

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,12 @@
22

33
FROM node:18-slim AS base
44
RUN apt-get update && \
5-
apt-get install ca-certificates curl -y --no-install-recommends
5+
apt-get install ca-certificates curl libjemalloc-dev -y --no-install-recommends && \
6+
rm -rf /var/lib/apt/lists/*
7+
# set environment variable to preload JEMalloc
8+
ENV LD_PRELOAD="/usr/lib/x86_64-linux-gnu/libjemalloc.so.2"
9+
# set GC time, set arenas number, set background_thread run GC
10+
ENV MALLOC_CONF=dirty_decay_ms:1000,narenas:2,background_thread:true
611
ENV PNPM_HOME="/pnpm"
712
ENV PATH="$PNPM_HOME:$PATH"
813
RUN corepack enable

babel.config.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
module.exports = {
2+
presets: [
3+
// https://babeljs.io/docs/babel-preset-typescript
4+
[
5+
'@babel/preset-typescript',
6+
{
7+
allowDeclareFields: true,
8+
allowNamespaces: true,
9+
allExtensions: true,
10+
isTSX: true,
11+
},
12+
],
13+
// https://babeljs.io/docs/babel-preset-react
14+
[
15+
'@babel/preset-react',
16+
{
17+
runtime: 'automatic',
18+
development: process.env.BABEL_ENV === 'development',
19+
},
20+
],
21+
],
22+
// https://babeljs.io/docs/babel-plugin-proposal-decorators#note-compatibility-with-babelplugin-transform-class-properties
23+
plugins: [['@babel/plugin-proposal-decorators', { version: '2023-05' }]],
24+
};

components/Git/Logo.tsx

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,8 @@ export interface GitLogoProps {
99

1010
@observer
1111
export class GitLogo extends PureComponent<GitLogoProps> {
12-
constructor(props: GitLogoProps) {
13-
super(props);
14-
makeObservable(this);
15-
}
16-
1712
@observable
18-
path = '';
13+
accessor path = '';
1914

2015
async componentDidMount() {
2116
const { name } = this.props;

models/Base.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export const larkClient = new HTTPClient({
1818
});
1919

2020
export const githubClient = new HTTPClient({
21-
baseURI: 'https://api.github.com/',
21+
baseURI: isServer() ? 'https://api.github.com/' : `${API_Host}/api/GitHub/`,
2222
responseType: 'json',
2323
}).use(({ request }, next) => {
2424
if (GithubToken)

models/Client.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { BiDataTable, TableCellValue } from 'mobx-lark';
1+
import { BiDataQueryOptions, BiDataTable, TableCellValue } from 'mobx-lark';
22

33
import { LarkBaseId, larkClient } from './Base';
44

@@ -12,6 +12,8 @@ const CLIENT_TABLE = process.env.NEXT_PUBLIC_CLIENT_TABLE!;
1212
export class ClientModel extends BiDataTable<Client>() {
1313
client = larkClient;
1414

15+
queryOptions: BiDataQueryOptions = { text_field_as_array: false };
16+
1517
constructor(appId = LarkBaseId, tableId = CLIENT_TABLE) {
1618
super(appId, tableId);
1719
}

models/Member.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { BiDataTable, TableCellValue } from 'mobx-lark';
1+
import { BiDataQueryOptions, BiDataTable, TableCellValue } from 'mobx-lark';
22

33
import { LarkBaseId, larkClient } from './Base';
44

@@ -22,6 +22,8 @@ export class MemberModel extends BiDataTable<Member>() {
2222

2323
requiredKeys = ['nickname', 'position', 'type', 'skill', 'joinedAt'] as const;
2424

25+
queryOptions: BiDataQueryOptions = { text_field_as_array: false };
26+
2527
constructor(appId = LarkBaseId, tableId = MEMBER_TABLE) {
2628
super(appId, tableId);
2729
}

models/Project.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,12 @@
1-
import { BiDataTable, makeSimpleFilter, TableCellValue } from 'mobx-lark';
1+
import {
2+
BiDataQueryOptions,
3+
BiDataTable,
4+
makeSimpleFilter,
5+
normalizeText,
6+
TableCellLink,
7+
TableCellValue,
8+
TableRecord,
9+
} from 'mobx-lark';
210
import { NewData } from 'mobx-restful';
311
import { isEmpty } from 'web-utility';
412

@@ -16,7 +24,8 @@ export type Project = Record<
1624
| 'settlementDate'
1725
| 'remark'
1826
| 'image'
19-
| 'openSource',
27+
| 'openSource'
28+
| 'link',
2029
TableCellValue
2130
>;
2231

@@ -27,6 +36,8 @@ export class ProjectModel extends BiDataTable<Project>() {
2736

2837
sort = { settlementDate: 'DESC' } as const;
2938

39+
queryOptions: BiDataQueryOptions = { text_field_as_array: false };
40+
3041
constructor(appId = LarkBaseId, tableId = PROJECT_TABLE) {
3142
super(appId, tableId);
3243
}
@@ -39,6 +50,14 @@ export class ProjectModel extends BiDataTable<Project>() {
3950
.filter(Boolean)
4051
.join('&&');
4152
}
53+
54+
normalize({ id, fields: { link, ...fields } }: TableRecord<Project>) {
55+
return {
56+
...fields,
57+
id,
58+
link: link && normalizeText(link as TableCellLink),
59+
};
60+
}
4261
}
4362

4463
export default new ProjectModel();

models/Repository.ts

Lines changed: 59 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { components } from '@octokit/openapi-types';
22
import { memoize } from 'lodash';
3-
import { makeObservable, observable } from 'mobx';
4-
import { ListModel, toggle } from 'mobx-restful';
5-
import { averageOf, buildURLData } from 'web-utility';
3+
import { observable } from 'mobx';
4+
import { ListModel, Stream, toggle } from 'mobx-restful';
5+
import { averageOf, buildURLData, mergeStream } from 'web-utility';
66

77
import { githubClient } from './Base';
88

@@ -13,40 +13,36 @@ export interface GitRepository extends Repository {
1313
}
1414
export type Organization = components['schemas']['organization-full'];
1515

16-
const getGitLanguages = memoize(async (URI: string) => {
17-
const { body: languageCount } = await githubClient.get<
18-
Record<string, number>
19-
>(`repos/${URI}/languages`);
16+
export class RepositoryModel extends Stream<GitRepository>(ListModel) {
17+
client = githubClient;
18+
indexKey = 'full_name' as const;
2019

21-
const languageAverage = averageOf(...Object.values(languageCount!));
20+
organizations = ['idea2app', 'IdeaMall', 'EasyWebApp'];
2221

23-
const languageList = Object.entries(languageCount!)
24-
.filter(([_, score]) => score >= languageAverage)
25-
.sort(([_, a], [__, b]) => b - a);
22+
@observable
23+
accessor currentGroup: GitRepository[] = [];
2624

27-
return languageList.map(([name]) => name);
28-
});
25+
getGitLanguages = memoize(async (URI: string) => {
26+
const { body: languageCount } = await this.client.get<
27+
Record<string, number>
28+
>(`repos/${URI}/languages`);
2929

30-
export class RepositoryModel extends ListModel<GitRepository> {
31-
constructor() {
32-
super();
33-
makeObservable(this);
34-
}
30+
const languageAverage = averageOf(...Object.values(languageCount!));
3531

36-
client = githubClient;
37-
baseURI = 'orgs/idea2app/repos';
38-
indexKey = 'full_name' as const;
32+
const languageList = Object.entries(languageCount!)
33+
.filter(([_, score]) => score >= languageAverage)
34+
.sort(([_, a], [__, b]) => b - a);
3935

40-
@observable
41-
currentGroup: GitRepository[] = [];
36+
return languageList.map(([name]) => name);
37+
});
4238

4339
@toggle('downloading')
4440
async getOne(URI: string) {
4541
const { body } = await this.client.get<Repository>(`repos/${URI}`);
4642

4743
return (this.currentOne = {
4844
...body!,
49-
languages: await getGitLanguages(URI),
45+
languages: await this.getGitLanguages(URI),
5046
});
5147
}
5248

@@ -56,28 +52,49 @@ export class RepositoryModel extends ListModel<GitRepository> {
5652
));
5753
}
5854

59-
async loadPage(page: number, per_page: number) {
60-
const { body: list } = await this.client.get<Repository[]>(
61-
`${this.baseURI}?${buildURLData({
62-
type: 'public',
63-
sort: 'pushed',
64-
page,
65-
per_page,
66-
})}`,
67-
);
68-
const pageData = await Promise.all(
69-
list!.map(async ({ full_name, ...item }) => ({
70-
...item,
71-
full_name,
72-
languages: await getGitLanguages(full_name),
73-
})),
74-
);
75-
const [_, organization] = this.baseURI.split('/');
55+
async *getRepository(organization: string) {
56+
const per_page = this.pageSize;
57+
58+
this.totalCount ||= 0;
59+
this.totalCount += await this.getRepositoryCount(organization);
60+
61+
for (let page = 1, count = 0; ; page++) {
62+
const { body: list } = await this.client.get<Repository[]>(
63+
`orgs/${organization}/repos?${buildURLData({
64+
type: 'public',
65+
sort: 'pushed',
66+
page,
67+
per_page,
68+
})}`,
69+
);
70+
count += list!.length;
71+
72+
if (count < page * per_page) break;
73+
74+
const pageData = await Promise.all(
75+
list!.map(async ({ full_name, ...item }) => ({
76+
...item,
77+
full_name,
78+
languages: await this.getGitLanguages(full_name),
79+
})),
80+
);
81+
yield* pageData as GitRepository[];
82+
}
83+
}
7684

85+
async getRepositoryCount(organization: string) {
7786
const { body } = await this.client.get<Organization>(
7887
`orgs/${organization}`,
7988
);
80-
return { pageData, totalCount: body!.public_repos };
89+
return body!.public_repos;
90+
}
91+
92+
openStream() {
93+
return mergeStream(
94+
...this.organizations.map(organization =>
95+
this.getRepository.bind(this, organization),
96+
),
97+
);
8198
}
8299
}
83100

models/Translation.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export const i18n = new TranslationModel({
66
'zh-CN': zhCN,
77
'zh-TW': () => import('../translation/zh-TW'),
88
'zh-HK': () => import('../translation/zh-TW'),
9+
'zh-MO': () => import('../translation/zh-TW'),
910
'en-US': () => import('../translation/en-US'),
1011
});
1112

package.json

Lines changed: 23 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,43 +4,46 @@
44
"description": "React project scaffold based on TypeScript, Next.js, Bootstrap & Workbox.",
55
"private": true,
66
"dependencies": {
7-
"@sentry/nextjs": "^7.91.0",
7+
"@sentry/nextjs": "^7.99.0",
88
"classnames": "^2.5.1",
9-
"file-type": "^18.7.0",
10-
"idea-react": "^1.0.0-rc.31",
9+
"file-type": "^19.0.0",
10+
"idea-react": "^2.0.0-rc.1",
1111
"koajax": "^0.9.6",
1212
"less": "^4.2.0",
13-
"less-loader": "^11.1.4",
13+
"less-loader": "^12.2.0",
1414
"lodash": "^4.17.21",
15-
"mobx": "~6.10.2",
16-
"mobx-i18n": "^0.4.2",
17-
"mobx-lark": "^1.1.1",
18-
"mobx-react": "~9.0.2",
19-
"mobx-restful": "^0.6.12",
20-
"mobx-restful-table": "^1.2.2",
21-
"next": "^14.0.4",
15+
"mobx": "^6.12.0",
16+
"mobx-i18n": "^0.5.0",
17+
"mobx-lark": "^2.0.0-rc.1",
18+
"mobx-react": "^9.1.0",
19+
"mobx-restful": "^0.7.0-rc.0",
20+
"mobx-restful-table": "^2.0.0-rc.0",
21+
"next": "^14.1.0",
2222
"next-pwa": "~5.6.0",
23-
"next-ssr-middleware": "^0.6.2",
23+
"next-ssr-middleware": "^0.7.0",
2424
"next-with-less": "^3.0.1",
2525
"react": "^18.2.0",
26-
"react-bootstrap": "^2.9.2",
26+
"react-bootstrap": "^2.10.0",
2727
"react-dom": "^18.2.0",
2828
"react-marked-renderer": "^2.0.1",
2929
"web-utility": "^4.1.3",
30-
"webpack": "^5.89.0"
30+
"webpack": "^5.90.0"
3131
},
3232
"devDependencies": {
33+
"@babel/plugin-proposal-decorators": "^7.23.9",
34+
"@babel/preset-react": "^7.23.3",
35+
"@babel/preset-typescript": "^7.23.3",
3336
"@octokit/openapi-types": "^19.1.0",
3437
"@types/lodash": "^4.14.202",
35-
"@types/node": "^18.19.4",
36-
"@types/react": "^18.2.46",
38+
"@types/node": "^18.19.10",
39+
"@types/react": "^18.2.48",
3740
"eslint": "^8.56.0",
38-
"eslint-config-next": "^14.0.4",
41+
"eslint-config-next": "^14.1.0",
3942
"eslint-config-prettier": "^9.1.0",
4043
"eslint-plugin-simple-import-sort": "^10.0.0",
41-
"husky": "^8.0.3",
44+
"husky": "^9.0.7",
4245
"lint-staged": "^15.2.0",
43-
"prettier": "^3.1.1",
46+
"prettier": "^3.2.4",
4447
"typescript": "~5.3.3"
4548
},
4649
"prettier": {
@@ -53,7 +56,7 @@
5356
"*.{js,jsx,ts,tsx}": "eslint --fix"
5457
},
5558
"scripts": {
56-
"prepare": "husky install",
59+
"prepare": "husky",
5760
"dev": "next dev",
5861
"build": "next build",
5962
"export": "next build && next export",

pages/api/GitHub/[...slug].ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { proxyGithub } from './core';
2+
3+
export default proxyGithub();

pages/api/GitHub/core.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { githubClient } from '../../../models/Base';
2+
import { safeAPI } from '../core';
3+
4+
export const proxyGithub = <T>(dataFilter?: (path: string, data: T) => T) =>
5+
safeAPI(async ({ method, url, headers, body }, response) => {
6+
delete headers.host;
7+
8+
const path = url!.slice(`/api/GitHub/`.length);
9+
10+
const { status, body: data } = await githubClient.request<T>({
11+
// @ts-ignore
12+
method,
13+
path,
14+
// @ts-ignore
15+
headers,
16+
body: body || undefined,
17+
});
18+
19+
response.status(status);
20+
response.send(dataFilter?.(path, data as T) || data);
21+
});

0 commit comments

Comments
 (0)