Compare commits

..

271 Commits

Author SHA1 Message Date
himself65
10cd000822 v0.5.4-canary.25 2023-05-05 23:57:10 -05:00
Himself65
496225a92e chore: bump version (#2249) 2023-05-05 23:41:51 -05:00
JimmFly
1ef408c9ad chore: update the style of help island in edgeless mode (#2244) 2023-05-05 15:35:05 -05:00
JimmFly
8d8119b39b chore: update theme color (#2242) 2023-05-05 15:34:01 -05:00
JimmFly
80c1f9e546 chore: disable navigation path (#2243) 2023-05-05 15:33:36 -05:00
Whitewater
dbd3249ae5 chore: clean all page list (#2245) 2023-05-05 14:46:58 -05:00
himself65
fbbcb4bad9 v0.5.4-canary.24 2023-05-04 23:30:02 -05:00
himself65
33069c87d0 build(theme): generate css file 2023-05-04 23:29:32 -05:00
himself65
637b8203d3 v0.5.4-canary.23 2023-05-04 23:20:02 -05:00
阿良仔
92859bf8b9 perf: remove data-testid in production (#2228)
Co-authored-by: Himself65 <himself65@outlook.com>
2023-05-05 04:18:54 +00:00
夏宇航
8a617f91e6 style: fix popover z-index (#2215) 2023-05-05 04:13:56 +00:00
Whitewater
84b36c1d35 refactor: clean all pages component (#2176)
Co-authored-by: himself65 <himself65@outlook.com>
2023-05-04 22:59:16 -05:00
三咲智子 Kevin Deng
2c49c774af feat(y-indexeddb): add connected (#2208)
Co-authored-by: Himself65 <himself65@outlook.com>
2023-05-05 03:42:49 +00:00
JimmFly
de0b300aca chore: optimize onboarding component style (#2235) 2023-05-04 22:26:40 -05:00
Himself65
4a50fe584c fix(electron): system theme (#2237) 2023-05-05 03:22:53 +00:00
Himself65
f7d1d922fa fix: cleanup page id in time (#2236) 2023-05-04 22:22:11 -05:00
Himself65
1b12972afd fix(electron): theme sync (#2231) 2023-05-04 21:00:05 -05:00
himself65
33c48eed79 v0.5.4-canary.22 2023-05-04 18:50:20 -05:00
Himself65
9631c99f7b chore: bump version (#2229) 2023-05-04 18:49:08 -05:00
Himself65
097cce34b5 fix: reduce useState and useEffect (#2223) 2023-05-04 17:53:52 -05:00
三咲智子 Kevin Deng
52b9734a7b feat(y-indexeddb): cleanup (#2207)
Co-authored-by: himself65 <himself65@outlook.com>
2023-05-04 20:25:58 +00:00
JimmFly
6d7f06c1c3 feat: add onboarding for client (#2144)
Co-authored-by: Himself65 <himself65@outlook.com>
2023-05-04 15:29:16 +08:00
Fangdun Tsai
238f69b4e7 fix(component): click area of the item (#2221)
Co-authored-by: Himself65 <himself65@outlook.com>
2023-05-04 05:46:56 +00:00
Himself65
3d43e61087 feat(i18n): static type on i18n (#2225) 2023-05-04 05:35:09 +00:00
Himself65
66c3b09c67 fix(hooks): reduce unused assertExists (#2224) 2023-05-04 03:55:45 +00:00
Himself65
1e84ad1484 fix: reduce pageMeta instance (#2222) 2023-05-03 22:09:43 -05:00
himself65
b3a3911cea v0.5.4-canary.21 2023-05-03 18:58:22 -05:00
Himself65
86988bd6e8 fix: dock to blocksuite latest API (#2219) 2023-05-03 18:57:59 -05:00
Himself65
9096ac2960 refactor: workspace provider (#2218) 2023-05-03 18:16:22 -05:00
himself65
ec39c23fb7 fix(web): add meta description 2023-05-03 18:15:52 -05:00
himself65
b036fe8502 chore: add codecov.yml 2023-05-03 00:47:43 -05:00
himself65
71142a3f1d v0.5.4-canary.20 2023-05-03 00:29:58 -05:00
Himself65
aace740df5 fix: prohibit delete last workspace (#2212) 2023-05-03 04:31:04 +00:00
Horus
f42d656cfa feat: add mac release zip file and release info yml (#2185) 2023-05-03 12:13:40 +08:00
Himself65
88124994e1 chore: bump version (#2211) 2023-05-02 22:40:53 -05:00
Fangdun Tsai
5a881ec223 fix(electron): ignore .DS_Store on MacOS (#2203) 2023-05-03 03:00:09 +00:00
Himself65
12b61d34c3 chore: bump version (#2210) 2023-05-02 16:50:58 -05:00
三咲智子 Kevin Deng
4eff5f3c38 chore: upgrade jotai devtools (#2209) 2023-05-02 21:27:01 +00:00
Himself65
648fad65e0 chore: bump version (#2206) 2023-04-30 20:03:40 -05:00
himself65
a2844e54d2 chore(y-indexeddb): add types fields 2023-04-30 18:40:34 -05:00
Fangdun Tsai
850cfe1187 fix: theme button width (#2202) 2023-04-30 01:51:33 -05:00
himself65
9030767d16 v0.5.4-canary.19 2023-04-29 05:23:48 -05:00
LongYinan
a4e7d0d0c3 fix(electron): remove disableHardwareAcceleration (#2199) 2023-04-29 05:22:59 -05:00
himself65
99898b2260 v0.5.4-canary.18 2023-04-28 16:00:57 -05:00
Himself65
1031fbc7ec refactor: guide atoms (#2196) 2023-04-28 15:59:59 -05:00
Himself65
31cccafb40 fix: sidebar regression (#2195) 2023-04-28 15:02:47 -05:00
Himself65
73a7c01580 revert: resize in app sidebar (#2193) 2023-04-28 05:41:17 -05:00
Whitewater
f9b012cac9 feat: add breakpoints (#2191) 2023-04-28 05:21:14 -05:00
himself65
101cd18067 v0.5.4-canary.17 2023-04-28 04:31:29 -05:00
Himself65
2c466617de fix: remove shake in first render (#2190) 2023-04-28 04:31:01 -05:00
JimmFly
2ff5ef9d5d feat: move theme switch and language switch to editor option menu (#2025)
Co-authored-by: himself65 <himself65@outlook.com>
2023-04-28 04:28:51 -05:00
Himself65
903b6eaf30 revert: lastVersionAtom atom (#2188) 2023-04-28 08:53:23 +00:00
Himself65
fd4b664e4f chore: bump version (#2187) 2023-04-28 02:39:21 -05:00
himself65
51a4bdc5e4 fix: state in lastVersionAtom 2023-04-28 02:27:31 -05:00
Himself65
ee695bbcb9 fix: shadow theme (#2186) 2023-04-28 01:53:20 -05:00
Himself65
ef0521fa2a test(electron): theme check (#2182) 2023-04-28 06:40:44 +00:00
Himself65
73d5b2081a feat(electron): enable disable cloud alert (#2184) 2023-04-28 06:26:14 +00:00
Himself65
70fbbb39c1 chore: enable no-unused vars (#2181) 2023-04-28 00:41:06 -05:00
JimmFly
b6ca2aa063 chore: update menu placement (#2183) 2023-04-28 00:40:37 -05:00
himself65
b3c1434055 v0.5.4-canary.16 2023-04-28 00:37:43 -05:00
himself65
4599a9a601 fix: remove min width in the main container 2023-04-28 00:34:05 -05:00
himself65
549dddc65f v0.5.4-canary.15 2023-04-27 23:50:26 -05:00
himself65
9f8b38f9f3 fix(electron): drag window behavior in header 2023-04-27 23:18:00 -05:00
Himself65
3a5a66a5a3 feat: init auth service (#2180) 2023-04-27 22:49:44 -05:00
liuyi
b4bb57b2a5 feat(server): port resolvers to node server (#2026)
Co-authored-by: Himself65 <himself65@outlook.com>
2023-04-27 18:02:05 -05:00
Himself65
3df3498523 chore: bump version (#2178) 2023-04-27 17:59:54 -05:00
himself65
567092a1ff v0.5.4-canary.14 2023-04-27 16:54:12 -05:00
himself65
f3e1c1eb08 docs: update releases.md 2023-04-27 16:53:20 -05:00
himself65
a04cfe2b68 chore: update desktop icons 2023-04-27 16:52:21 -05:00
Himself65
c1a65b6b76 feat(component): init app sidebar (#2135) 2023-04-27 16:46:08 -05:00
JimmFly
f3cbe54625 chore: update menu background color (#2170) 2023-04-27 18:06:17 +00:00
JimmFly
dcf7e83eec chore: update shadow and color (#2171) 2023-04-27 12:57:25 -05:00
JimmFly
50006efb57 chore: update workspace setting button color (#2169) 2023-04-27 12:55:58 -05:00
Himself65
606f6652ac chore: bump version (#2162) 2023-04-27 00:23:34 -05:00
himself65
afff15c435 fix: background warning color syntax 2023-04-26 22:30:33 -05:00
himself65
f7b8797bb2 v0.5.4-canary.13 2023-04-26 19:33:41 -05:00
Whitewater
2b05a1254b chore: hide pinboard (#2149)
Co-authored-by: himself65 <himself65@outlook.com>
2023-04-26 19:31:01 -05:00
himself65
40e7074475 fix(component): remove css import from blocksuite 2023-04-26 02:29:34 -05:00
himself65
e1ad3e38b9 v0.5.4-canary.12 2023-04-26 01:55:32 -05:00
himself65
f03fdde770 chore(electron): update canary icons 2023-04-26 01:55:05 -05:00
Himself65
d2eba54550 chore: bump version (#2146) 2023-04-26 01:54:44 -05:00
himself65
fa7baaf5c1 docs: add the ecosystem section in README.md 2023-04-25 19:22:47 -05:00
himself65
a4d8b65eef v0.5.4-canary.11 2023-04-25 19:00:03 -05:00
himself65
83dafa149c build: add set-version.sh 2023-04-25 18:59:37 -05:00
himself65
3a25f13734 docs: download page redirect to affine.pro 2023-04-25 18:48:39 -05:00
Himself65
db52c63d25 feat: init @toeverything/theme (#2136) 2023-04-25 18:44:17 -05:00
himself65
80f4578f76 v0.5.4-canary.10 2023-04-25 11:44:23 -05:00
JimmFly
15a7e93058 fix: text overflow problem in <a> tag (#2126) 2023-04-25 11:40:14 -05:00
JimmFly
1c41731b4e fix: theme color (#2124) 2023-04-25 11:37:22 -05:00
Himself65
a807647639 fix(component): editor component style (#2120) 2023-04-25 01:58:30 -05:00
JimmFly
3f1293ca3c chore: add changeLog to storybook (#2118)
Co-authored-by: himself65 <himself65@outlook.com>
2023-04-25 06:24:46 +00:00
Himself65
ad58b4d1e9 feat: improve build config (#2115) 2023-04-24 22:33:09 -05:00
Himself65
7e61708850 test: move playwright test suite to top level (#2113) 2023-04-24 22:12:48 -05:00
LongYinan
5c673a8ffc feat(graphql): generate types from graphql files (#2014)
Co-authored-by: forehalo <forehalo@gmail.com>
Co-authored-by: Himself65 <himself65@outlook.com>
2023-04-25 10:13:52 +08:00
himself65
4528df07a5 v0.5.4-canary.9 2023-04-24 19:59:21 -05:00
himself65
b6eb017bd4 docs: add linux badge 2023-04-24 19:55:05 -05:00
Himself65
9d3b9e9848 chore: bump version (#2111) 2023-04-24 19:46:46 -05:00
himself65
04fc619f52 test: fix flaky 2023-04-24 19:33:35 -05:00
himself65
06ef6da370 ci: remove unused 2023-04-24 19:26:30 -05:00
Himself65
d3ce90e721 test: add electron test (#1840) 2023-04-24 18:53:36 -05:00
himself65
9c94d05dd8 docs: format jobs.md 2023-04-24 17:47:41 -05:00
Himself65
ef8dea8cb2 test: fix flaky in customElements (#2109) 2023-04-24 13:18:37 -05:00
Peng Xiao
c27c241482 fix: some improvements to electron app (#2089) 2023-04-24 12:53:21 -05:00
Flrande
b73e9189ef chore: fix color (#2083)
Co-authored-by: Himself65 <himself65@outlook.com>
2023-04-24 11:49:34 -05:00
JimmFly
c95b8e9d71 fix: incorrect text color (#2107) 2023-04-24 11:49:22 -05:00
Peng Xiao
ab8669882a fix: closing modal sometimes covered by header (#2097) 2023-04-23 23:43:40 -05:00
himself65
7ff12a6d0f build: reduce the sample rate to 0.1 2023-04-23 23:40:59 -05:00
himself65
339b133e3f v0.5.4-canary.8 2023-04-23 21:41:43 -05:00
Peng Xiao
be9095ec19 build: fix electron build gain focus on reloading in dev (#2088) 2023-04-23 01:42:52 -05:00
Himself65
33261558f6 chore: bump version (#2087) 2023-04-23 01:42:27 -05:00
Himself65
2ad1b770d0 fix(y-indexeddb): alert user when write operation unfinished (#2085) 2023-04-22 17:32:57 -05:00
Himself65
74e21311dc refactor(y-indexeddb): move migrate function separate (#2086) 2023-04-22 17:25:25 -05:00
Chi Zhang
bf83bfcf63 feat: add short cuts for sidebar (#2075) 2023-04-22 17:24:44 -05:00
Chi Zhang
70d8f9a0a7 feat: add shared page empty tip (#2077)
Co-authored-by: himself65 <himself65@outlook.com>
2023-04-22 17:24:18 -05:00
Moeyua Evod
7d246f87e7 docs: sign CLA (#2079) 2023-04-22 00:05:13 -05:00
Himself65
1ca9fb8ff4 fix(workspace): check affine login auth (#2070) 2023-04-21 20:44:29 -05:00
Moeyua Evod
2c95a0a757 feat: center align button text (#2056)
Co-authored-by: Himself65 <himself65@outlook.com>
2023-04-21 19:45:23 -05:00
himself65
a49d5ea1e2 fix(workspace): load first workspace in index page 2023-04-21 13:46:01 -05:00
三咲智子 Kevin Deng
84e2710e87 docs: fix typo (#2063) 2023-04-21 12:07:44 -05:00
Peng Xiao
044e6da00d build: beta build (#2069) 2023-04-21 11:52:55 -05:00
himself65
023cbc30ea fix(workspace): cloud workspace blob uploading 2023-04-21 11:34:18 -05:00
Peng Xiao
7094385d8b fix: try to sign macos (#2066) 2023-04-21 23:30:49 +08:00
himself65
f66d402cf7 v0.5.4-beta.0 2023-04-21 06:09:38 -05:00
Peng Xiao
971e256cd3 fix: osxSign in build 2023-04-21 18:25:46 +08:00
Peng Xiao
88a297c3c1 chore: bump version 0.5.4-canary.7 2023-04-21 18:10:12 +08:00
Peng Xiao
4bb50e8c25 feat: store local data to local db (#2037) 2023-04-21 18:06:54 +08:00
zuomeng wang
acc5afdd4f fix(web): remove edgeless mode padding (#2061) 2023-04-21 17:56:29 +08:00
Qi
9ec6768272 fix: modify with new blocksuite version about subpage (#2060) 2023-04-21 08:34:32 +00:00
Peng Xiao
5a124831b8 fix: some minor ui issues (#2058) 2023-04-21 00:56:42 -05:00
Flrande
01115f8957 fix: color variable (#2059) 2023-04-20 23:41:43 -05:00
Qi
a5a6203a95 feat: replace react-dnd to dnd-kit (#2028)
Co-authored-by: Himself65 <himself65@outlook.com>
2023-04-20 23:27:32 -05:00
himself65
4a473f5518 Revert "chore: bump version"
This reverts commit 44011b4695.
2023-04-20 22:53:32 -05:00
himself65
6cddacb953 Revert "fix: api compatibility with blocksuite"
This reverts commit 00f44c72ce.
2023-04-20 22:53:32 -05:00
himself65
00f44c72ce fix: api compatibility with blocksuite 2023-04-20 22:29:11 -05:00
himself65
44011b4695 chore: bump version 2023-04-20 21:58:09 -05:00
himself65
e0cd2e780b v0.5.4-canary.7 2023-04-20 18:09:53 -05:00
himself65
985bb55d82 build(y-indexeddb): fix vite config 2023-04-20 18:08:33 -05:00
himself65
66d0640042 ci: fix release.yml 2023-04-20 17:50:29 -05:00
himself65
e399682cad ci: add release.yml 2023-04-20 17:47:06 -05:00
himself65
c4e90f2d8b v0.5.4-canary.6 2023-04-20 17:29:49 -05:00
himself65
b38b01fc98 docs: fix script 2023-04-20 17:27:30 -05:00
Himself65
0a0f825a15 fix: remove mui theme provider (#2055) 2023-04-20 14:31:54 -05:00
Himself65
d24c43e750 chore: bump version (#2054) 2023-04-20 12:25:12 -05:00
ʀᴀʏ
90b51031d2 chore: correct action name (#2053) 2023-04-20 11:32:44 -05:00
himself65
1e771131b0 docs: format releases.md 2023-04-20 11:32:17 -05:00
himself65
4d7a3e5bf1 docs: add releases.md 2023-04-20 11:27:52 -05:00
himself65
92b1244fd7 v0.5.4-canary.5 2023-04-20 11:08:10 -05:00
himself65
d6b1b9f6cf ci: use RELEASE_TOKEN 2023-04-20 10:34:35 -05:00
Flrande
b2e93433e1 chore: fix color (#2049)
Co-authored-by: JimmFly <yangjinfei001@gmail.com>
2023-04-20 09:13:20 -05:00
Chi Zhang
97b1a31f8d Update README.md 2023-04-20 21:38:00 +08:00
Qi
4a1c15c1e9 feat: modify default avatar (#2034) 2023-04-20 17:41:29 +08:00
himself65
f8d1513bb6 chore: release 0.5.4-canary.4 2023-04-20 03:34:00 -05:00
Flrande
372377dd6b feat: upgrate to new theme (#2027)
Co-authored-by: Himself65 <himself65@outlook.com>
2023-04-20 03:31:19 -05:00
Himself65
63f7b2556e feat: init affine blob storage (#2045) 2023-04-20 03:23:41 -05:00
himself65
c08c587efb fix: max length of input 2023-04-20 02:36:25 -05:00
JimmFly
65c1bee7f0 chore: update temp disable affine cloud modal style (#2046) 2023-04-20 02:27:26 -05:00
howarddo
227f59cadc docs: add more instruction for yarn (#2042) 2023-04-20 00:25:10 -05:00
JimmFly
031ab2cfa2 chore: improve disable legacy cloud (#2041) 2023-04-20 12:25:45 +08:00
Chi Zhang
9f33e73429 Update package.json 2023-04-19 14:30:28 +08:00
himself65
f1670af15d ci: fix working-directory 2023-04-18 18:33:46 -05:00
himself65
0d7f65ab36 test(server): fix script 2023-04-18 18:24:35 -05:00
Himself65
3a053af50c feat(server): init user module (#2018) 2023-04-18 18:14:25 -05:00
himself65
c6be29f944 fix: disable legacy cloud in header 2023-04-18 15:01:19 -05:00
Peng Xiao
9ffe45102b fix: macos build 2023-04-19 00:43:51 +08:00
Peng Xiao
6448b6a515 fix: release app workflow (#2017) 2023-04-19 00:21:44 +08:00
Peng Xiao
ba462fb79b fix: artifacts in release (#2016) 2023-04-18 22:20:34 +08:00
Peng Xiao
f36d415c3d build: optimize release app workflow (#2011) 2023-04-18 17:50:29 +08:00
Himself65
f6fb049ff2 feat: support disable legacy cloud (#2006) 2023-04-18 02:23:00 -05:00
JimmFly
94063352f5 chore: disable slider bar link item drag (#2010) 2023-04-18 02:16:38 -05:00
Himself65
c895c18deb ci: collect server coverage report (#2002) 2023-04-18 01:01:14 -05:00
JimmFly
346484ed44 chore: add translation (#2001) 2023-04-18 00:34:21 -05:00
Himself65
18223c22ef test(server): migrate to node internal test (#2000) 2023-04-18 00:07:03 -05:00
himself65
ea9861bfa0 ci: update labeler.yml 2023-04-17 23:13:10 -05:00
Himself65
7be96a2e41 build: remove unused config (#1990) 2023-04-17 23:11:46 -05:00
LongYinan
91c3040db7 feat(server): init nestjs server (#1997)
Co-authored-by: himself65 <himself65@outlook.com>
2023-04-17 22:24:44 -05:00
himself65
a92d0fff4a docs: update badge in README.md 2023-04-17 21:06:29 -05:00
Jordy Delgado
64e5d65eb3 docs: sign CLA (#1995) 2023-04-17 21:03:15 -05:00
Peng Xiao
11de3a681f build: add canary build (#1986)
Co-authored-by: Himself65 <himself65@outlook.com>
Co-authored-by: Horus <lhlxtl@gmail.com>
2023-04-17 11:32:10 -05:00
hehe
54a30bbf20 chore: remove absolete module-resolve (#1991) 2023-04-17 15:02:22 +00:00
usedtobe
6c77006bcc docs: fix typo (#1984) 2023-04-17 08:34:50 -05:00
Qi
143a55a6e8 fix: error style of sidebar (#1981) 2023-04-17 06:52:04 +00:00
Qi
19894aad5a feat: modify empty text & style of favorite & pinboard (#1977)
Co-authored-by: Himself65 <himself65@outlook.com>
2023-04-17 13:41:07 +08:00
JimmFly
f534e4a6dd chore: update change log link (#1973)
Co-authored-by: Himself65 <himself65@outlook.com>
2023-04-16 21:48:29 -05:00
Himself65
3d70a36dd3 refactor: remove null type in hooks (#1955) 2023-04-16 21:36:32 -05:00
Himself65
9c517907eb fix: first binary on y-indexeddb (#1972) 2023-04-16 21:33:54 -05:00
Himself65
4cb6b8fdc8 chore: bump version (#1970) 2023-04-16 20:36:59 -05:00
Horus
134e1e8668 feat: support release windows installer with squirrel (#1965)
Co-authored-by: Himself65 <himself65@outlook.com>
2023-04-16 19:28:29 -05:00
Himself65
c76bbeab67 ci: add sentry in desktop release (#1914) 2023-04-16 21:22:48 +00:00
himself65
ec50d721ea chore: release 0.5.3 2023-04-16 16:04:21 -05:00
Himself65
7bbe67af43 refactor: workspace loading logic (#1966) 2023-04-16 16:02:41 -05:00
Himself65
caa292e097 test: mark public single page as fail (#1967) 2023-04-16 09:45:50 -05:00
HeJiachen-PM
73b8b805c6 Rewrite section 2.3 2023-04-16 15:19:22 +08:00
HeJiachen-PM
084d4e043a Add summery to subsections in section 2 2023-04-16 15:09:08 +08:00
HeJiachen-PM
69a9c34f11 Rewrite the third section 2023-04-16 04:37:35 +08:00
Himself65
d742cab1d5 fix: hydration error (#1961) 2023-04-15 13:10:24 -05:00
Horus
8b3c1fb363 fix: force to use powershell on windows to fix zx script crash (#1962) 2023-04-15 12:24:57 -05:00
Horus
ec445207d6 fix: fix windows build client error and release cannot open (#1959) 2023-04-16 00:00:47 +08:00
HeJiachen-PM
49281e68a6 Rewrite the second section 2023-04-15 15:31:56 +08:00
HeJiachen-PM
a918d6e14c Proofreading introduction 2023-04-15 15:27:09 +08:00
Himself65
7cf7187893 docs: add behind-the-code.md (#1957) 2023-04-15 00:19:13 -05:00
Himself65
2383165470 refactor: remove NoSsr on top level (#1951) 2023-04-14 17:07:41 -05:00
Himself65
43a96fe8e3 fix: move suspense to the correct place (#1954) 2023-04-14 15:44:23 -05:00
Himself65
b771a2504b test: fix flaky (#1953) 2023-04-14 15:03:16 -05:00
himself65
8d2fefb5f8 ci: fix labeler.yml 2023-04-14 14:14:58 -05:00
himself65
c71e5f1c96 fix(cli): run dev server at 8080 2023-04-14 11:06:22 -05:00
Skye Sun
5b96fb0db3 docs: update CLA.md (#1950) 2023-04-14 08:02:21 -05:00
Peng Xiao
46cd0c5c9a fix: share url (#1948) 2023-04-14 08:01:31 -05:00
Qi
261a41f8da feat: add history back & forward for desktop app (#1926) 2023-04-14 09:19:52 +00:00
Himself65
bd387f6551 fix: theme color (#1944) 2023-04-14 02:13:14 -05:00
JimmFly
5335118e93 chore: add translation (#1946) 2023-04-14 15:02:43 +08:00
Himself65
70313eb5ee chore: bump version (#1943) 2023-04-14 01:57:54 -05:00
himself65
ccd2b79d20 docs: update logo in README.md 2023-04-14 00:38:35 -05:00
Himself65
5ca94db5d2 fix: effect deps (#1940) 2023-04-14 00:24:44 -05:00
Himself65
d58f9db289 docs: update BUG-REPORT.yml (#1941) 2023-04-13 22:27:01 -05:00
Chi Zhang
93e78c315c Update jobs.md 2023-04-14 10:27:45 +08:00
himself65
3954f309aa chore: fix packages version 2023-04-13 18:33:21 -05:00
himself65
f902d0c324 ci: fix cache in build-master.yml 2023-04-13 18:22:20 -05:00
Himself65
e79fb1ae3a build: add log when coverage (#1933) 2023-04-13 18:20:41 -05:00
Himself65
08d67b316c docs: update README.md (#1931) 2023-04-13 17:54:20 -05:00
himself65
d12c00d5cb ci: fix coverage report 2023-04-13 17:53:34 -05:00
himself65
68bb538dd1 ci: remove version tag in release 2023-04-13 16:39:50 -05:00
himself65
b394764b1c ci: fix upload-artifact path 2023-04-13 16:33:12 -05:00
Himself65
01a686dc28 feat: enable share menu (#1883)
Co-authored-by: JimmFly <yangjinfei001@gmail.com>
2023-04-13 16:22:49 -05:00
Simon He
32b206a137 chore: add lint cache (#1917) 2023-04-13 20:30:18 +00:00
Peng Xiao
42756045bb fix: failed to load blobs in electron (#1927) 2023-04-13 15:14:46 +00:00
Peng Xiao
934e242116 fix: electron sourcemap issues (#1919) 2023-04-13 08:37:50 -05:00
Qi
6571ec2df6 fix: pinboard operation menu disappear inexplicably when hover to menu from button, fixed #1898 (#1922) 2023-04-13 07:58:22 -05:00
Qi
7d64815aca feat: add navigation path in quick search (#1920) 2023-04-13 16:31:28 +08:00
Himself65
f20a151e57 fix(y-indexeddb): migration in firefox (#1904) 2023-04-12 22:42:17 -05:00
Himself65
6180a4c3cb fix: wrap React.lazy with Suspense (#1915) 2023-04-12 22:33:31 -05:00
Himself65
2bcda973d3 build: support sourcemap in sentry (#1910) 2023-04-12 21:26:06 -05:00
Himself65
1162bffb30 build: support sentry replay (#1908) 2023-04-12 21:18:41 -05:00
Himself65
2a2d682211 fix: cannot update a component while rendering a different component (#1907) 2023-04-12 16:46:29 -05:00
Sirocco
8f53043100 fix: improve UX of dropdown (#1905)
Removed the logic of onMouseLeave. The logic of clicking to open and clicking to close is clearer.

Fixes: #1898
2023-04-12 15:35:41 -05:00
Himself65
6d5b101bb3 fix: use startTransition (#1903) 2023-04-12 12:06:22 -05:00
Himself65
8bcef957fc refactor: remove next/dynamic (#1901) 2023-04-12 11:08:50 -05:00
Peng Xiao
d9c4fc3a9e fix: cors header handling (#1900) 2023-04-13 00:05:54 +08:00
Peng Xiao
407c72ba2c fix: try fix electron build (#1899) 2023-04-12 23:23:47 +08:00
Peng Xiao
95aa86cdf0 fix: ws prefix url in electron (#1896) 2023-04-12 14:11:47 +00:00
TimLi
25d7f7c848 fix: export to html/markdown (#1892)
Co-authored-by: himself65 <himself65@outlook.com>
2023-04-12 14:11:03 +00:00
Yifeng Wang
23e33a6061 chore: enable databse feature flag (#1895) 2023-04-12 19:23:46 +08:00
Peng Xiao
f647fb6070 fix: update app icons (#1893) 2023-04-12 16:04:45 +08:00
Qi
af04c1b889 feat: refactor trash, page would delete from its parent's subpageIds after move to trash (#1871) 2023-04-12 01:14:57 -05:00
Peng Xiao
ba4a2fc9d2 fix: API server address in electron (#1888)
Co-authored-by: himself65 <himself65@outlook.com>
2023-04-12 00:26:42 -05:00
Himself65
fb0d2992c2 chore: bump version (#1890) 2023-04-12 00:19:12 -05:00
himself65
ea00c208e6 fix: set affine loading speed to 2 2023-04-11 23:54:15 -05:00
DarkSky
6ce270bffd feat: update favicon & manifest (#1889) 2023-04-11 23:52:38 -05:00
Himself65
9dcb96839b fix(component): affine loading (#1887) 2023-04-11 23:48:42 -05:00
Himself65
5535440c55 docs(y-indexeddb): add README.md (#1886) 2023-04-11 23:37:24 -05:00
himself65
db8fe4e09a feat: add new affine loading component 2023-04-11 22:57:20 -05:00
Chi Zhang
07a11ed767 Update package.json 2023-04-12 10:55:00 +08:00
Himself65
a06113d48c refactor: workspace header (#1880) 2023-04-11 21:39:39 -05:00
JimmFly
2e823c2fee feat: single page sharing support (#1805)
Co-authored-by: Himself65 <himself65@outlook.com>
2023-04-11 17:58:11 -05:00
Himself65
f3af128baf perf(y-indexeddb): improve boost and loading time (#1879) 2023-04-11 17:29:44 -05:00
Himself65
a599364218 fix(y-indexeddb): migration for once (#1868) 2023-04-11 14:30:36 -05:00
Horus
c0669359ed feat: support google cloud login in client (#1822)
Co-authored-by: Himself65 <himself65@outlook.com>
Co-authored-by: Peng Xiao <pengxiao@outlook.com>
2023-04-11 18:42:36 +00:00
Himself65
024c469a2c chore: bump version (#1873) 2023-04-11 09:47:46 -05:00
Qi
f5e51bb471 feat: ensure subpage is appended in its parent after subpageLinked (#1872) 2023-04-11 09:43:35 -05:00
Himself65
778f76dfed feat: add affine next theme (#1867) 2023-04-10 23:43:24 -05:00
Chi Zhang
b2ff6e379c docs: make intro,badge,desc align center (#1866) 2023-04-10 22:12:36 -05:00
Zhilin Liu
cc4e48e5bb fix: update i18n (#1860) 2023-04-10 22:08:04 -05:00
Chi Zhang
7afc61ac36 docs: remove the global center style (#1865) 2023-04-11 11:02:38 +08:00
Chi Zhang
537200c064 docs: thanks to all contributors, update the readme (#1864) 2023-04-10 21:55:53 -05:00
Himself65
d95dbd5af4 chore: bump version (#1863) 2023-04-10 17:41:07 -05:00
Himself65
7fea55d81f feat: support page sharing by meta (#1858) 2023-04-10 17:13:44 -05:00
Qi
ea2a146c82 feat: connect pinboard and reference link (#1859) 2023-04-10 11:49:51 -05:00
HeJiachen-PM
9acbba7016 Replaced obsolete pics and updated READ.me
Pics are now replaced by more intuitive gifs. description are fixed accordingly.
2023-04-10 19:34:55 +08:00
Himself65
401cad799e refactor: move non-affine hooks (#1857) 2023-04-09 17:15:07 -05:00
Horus
d9e42d6a0f ci: fix windows build artifacts path name error (#1856) 2023-04-09 14:11:34 -05:00
Zhilin Liu
73a1a979f9 docs: sign CLA (#1855) 2023-04-09 08:42:27 -05:00
Himself65
626b906bc0 ci: build macos on pull request (#1854) 2023-04-08 20:44:47 -05:00
560 changed files with 29175 additions and 28669 deletions

View File

@@ -53,10 +53,80 @@
]
},
{
"login": "DiamondThree",
"name": "DiamondThree",
"avatar_url": "https://avatars.githubusercontent.com/u/24630517?v=4",
"profile": "https://github.com/DiamondThree",
"login": "Brooooooklyn",
"name": "LongYinan",
"avatar_url": "https://avatars.githubusercontent.com/u/3468483?v=4",
"profile": "https://github.com/Brooooooklyn",
"contributions": [
"code",
"doc"
]
},
{
"login": "hwangdev97",
"name": "Hwang",
"avatar_url": "https://avatars.githubusercontent.com/u/24713927?v=4",
"profile": "https://github.com/hwangdev97",
"contributions": [
"code",
"doc"
]
},
{
"login": "kobeshanks",
"name": "kobeshanks",
"avatar_url": "https://avatars.githubusercontent.com/u/82570088?v=4",
"profile": "https://github.com/kobeshanks",
"contributions": [
"code",
"doc"
]
},
{
"login": "pengx17",
"name": "Peng Xiao",
"avatar_url": "https://avatars.githubusercontent.com/u/584378?v=4",
"profile": "https://pengx17.vercel.app/",
"contributions": [
"code",
"doc"
]
},
{
"login": "Saul-Mirone",
"name": "Mirone",
"avatar_url": "https://avatars.githubusercontent.com/u/10047788?v=4",
"profile": "https://mirone.me/",
"contributions": [
"code",
"doc"
]
},
{
"login": "zqran",
"name": "zqran",
"avatar_url": "https://avatars.githubusercontent.com/u/15389209?v=4",
"profile": "https://github.com/zqran",
"contributions": [
"code",
"doc"
]
},
{
"login": "SuneBear",
"name": "Shule Hsiung",
"avatar_url": "https://avatars.githubusercontent.com/u/7693264?v=4",
"profile": "https://sunebear.com/",
"contributions": [
"code",
"doc"
]
},
{
"login": "fundon",
"name": "Fangdun Tsai",
"avatar_url": "https://avatars.githubusercontent.com/u/27926?v=4",
"profile": "https://fundon.viz.rs/",
"contributions": [
"code",
"doc"
@@ -93,10 +163,10 @@
]
},
{
"login": "SaikaSakura",
"name": "MingLIang Wang",
"avatar_url": "https://avatars.githubusercontent.com/u/11530942?v=4",
"profile": "https://github.com/SaikaSakura",
"login": "DiamondThree",
"name": "DiamondThree",
"avatar_url": "https://avatars.githubusercontent.com/u/24630517?v=4",
"profile": "https://github.com/DiamondThree",
"contributions": [
"code",
"doc"
@@ -113,20 +183,20 @@
]
},
{
"login": "linonetwo",
"name": "lin onetwo",
"avatar_url": "https://avatars.githubusercontent.com/u/3746270?v=4",
"profile": "https://onetwo.ren/wiki",
"login": "colelawrence",
"name": "Cole Lawrence",
"avatar_url": "https://avatars.githubusercontent.com/u/2925395?v=4",
"profile": "https://colelawrence.com/",
"contributions": [
"code",
"doc"
]
},
{
"login": "colelawrence",
"name": "Cole Lawrence",
"avatar_url": "https://avatars.githubusercontent.com/u/2925395?v=4",
"profile": "https://colelawrence.com/",
"login": "linonetwo",
"name": "lin onetwo",
"avatar_url": "https://avatars.githubusercontent.com/u/3746270?v=4",
"profile": "https://onetwo.ren/wiki",
"contributions": [
"code",
"doc"
@@ -189,15 +259,6 @@
"design"
]
},
{
"login": "fanjing22",
"name": "fanjing22",
"avatar_url": "https://avatars.githubusercontent.com/u/109729699?v=4",
"profile": "https://github.com/fanjing22",
"contributions": [
"design"
]
},
{
"login": "xell",
"name": "Guozhu Liu",
@@ -292,13 +353,33 @@
"doc"
]
},
{
"login": "SaikaSakura",
"name": "MingLIang Wang",
"avatar_url": "https://avatars.githubusercontent.com/u/11530942?v=4",
"profile": "https://github.com/SaikaSakura",
"contributions": [
"code",
"doc"
]
},
{
"login": "fanjing22",
"name": "fanjing22",
"avatar_url": "https://avatars.githubusercontent.com/u/109729699?v=4",
"profile": "https://github.com/fanjing22",
"contributions": [
"design"
]
},
{
"login": "pointmax",
"name": "pointmax",
"avatar_url": "https://avatars.githubusercontent.com/u/49361135?v=4",
"profile": "https://github.com/pointmax",
"contributions": [
"doc"
"doc",
"code"
]
},
{
@@ -334,7 +415,8 @@
"avatar_url": "https://avatars.githubusercontent.com/u/67266933?v=4",
"profile": "https://quavo.vercel.app/",
"contributions": [
"doc"
"doc",
"code"
]
},
{
@@ -416,7 +498,8 @@
"avatar_url": "https://avatars.githubusercontent.com/u/92734739?v=4",
"profile": "https://github.com/MuhammedFaraz",
"contributions": [
"doc"
"doc",
"code"
]
},
{
@@ -463,6 +546,96 @@
"contributions": [
"code"
]
},
{
"login": "hehex9",
"name": "hehe",
"avatar_url": "https://avatars.githubusercontent.com/u/9209882?v=4",
"profile": "https://github.com/hehex9",
"contributions": [
"code"
]
},
{
"login": "albertodlc",
"name": "Alberto de la Cruz",
"avatar_url": "https://avatars.githubusercontent.com/u/32411964?v=4",
"profile": "https://github.com/albertodlc",
"contributions": [
"code"
]
},
{
"login": "AlessioGr",
"name": "Alessio Gravili",
"avatar_url": "https://avatars.githubusercontent.com/u/70709113?v=4",
"profile": "https://github.com/AlessioGr",
"contributions": [
"code"
]
},
{
"login": "lzlme",
"name": "Zhilin Liu",
"avatar_url": "https://avatars.githubusercontent.com/u/117659326?v=4",
"profile": "https://github.com/lzlme",
"contributions": [
"code"
]
},
{
"login": "suica",
"name": "Sg",
"avatar_url": "https://avatars.githubusercontent.com/u/8041462?v=4",
"profile": "https://github.com/suica",
"contributions": [
"code"
]
},
{
"login": "sinchang",
"name": "Jeff Wen",
"avatar_url": "https://avatars.githubusercontent.com/u/3297859?v=4",
"profile": "https://sinchang.me/",
"contributions": [
"code"
]
},
{
"login": "m1212e",
"name": "m1212e",
"avatar_url": "https://avatars.githubusercontent.com/u/14091540?v=4",
"profile": "https://m1212e.github.io/portfolio/",
"contributions": [
"code"
]
},
{
"login": "adityash1",
"name": "Aditya Sharma",
"avatar_url": "https://avatars.githubusercontent.com/u/65771169?v=4",
"profile": "https://adityash1.github.io/",
"contributions": [
"code"
]
},
{
"login": "sheben404",
"name": "Kehan Wang",
"avatar_url": "https://avatars.githubusercontent.com/u/61317160?v=4",
"profile": "https://github.com/sheben404",
"contributions": [
"code"
]
},
{
"login": "VictorNanka",
"name": "VictorNanka",
"avatar_url": "https://avatars.githubusercontent.com/u/30154366?v=4",
"profile": "https://github.com/VictorNanka",
"contributions": [
"code"
]
}
]
}

View File

@@ -6,11 +6,13 @@
"always",
[
"electron",
"server",
"web",
"docs",
"component",
"workspace",
"env",
"graphql",
"cli",
"hooks",
"i18n",
@@ -18,7 +20,8 @@
"octobase-node",
"templates",
"y-indexeddb",
"debug"
"debug",
"theme"
]
]
}

View File

@@ -4,3 +4,4 @@ dist
out
storybook-static
affine-out
_next

View File

@@ -1,8 +1,11 @@
module.exports = {
/**
* @type {import('eslint').Linter.Config}
*/
const config = {
root: true,
settings: {
react: {
version: '18',
version: 'detect',
},
next: {
rootDir: 'apps/web',
@@ -10,6 +13,7 @@ module.exports = {
},
extends: [
'eslint:recommended',
'plugin:react-hooks/recommended',
'plugin:react/recommended',
'plugin:react/jsx-runtime',
'plugin:@typescript-eslint/recommended',
@@ -41,7 +45,14 @@ module.exports = {
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-empty-function': 'off',
'@typescript-eslint/no-unused-vars': 'off',
'@typescript-eslint/no-unused-vars': [
'error',
{
varsIgnorePattern: '^_',
argsIgnorePattern: '^_',
caughtErrorsIgnorePattern: '^_',
},
],
'unused-imports/no-unused-imports': 'error',
'simple-import-sort/imports': 'error',
'simple-import-sort/exports': 'error',
@@ -64,4 +75,20 @@ module.exports = {
},
],
},
overrides: [
{
files: 'apps/server/**/*.ts',
rules: {
'@typescript-eslint/consistent-type-imports': 0,
},
},
{
files: '*.cjs',
rules: {
'@typescript-eslint/no-var-requires': 0,
},
},
],
};
module.exports = config;

6
.github/CLA.md vendored
View File

@@ -52,3 +52,9 @@ Example:
- Victor Nanka, @victornanka, 2023/03/09
- Aditya Sharma, @adityash1, 2023/03/21
- Fangdun Tsai, @fundon, 2023/03/21
- Zhilin Liu, @lzlme, 2023/04/09
- Skye Sun, @skyesun, 2023/04/14
- Jordy Delgado, @Jdelgad8, 2023/04/17
- Howard Do, @howarddo2208, 2023/04/20
- 三咲智子 Kevin Deng, @sxzz, 2023/04/21
- Moeyua, @moeyua, 2023/04/22

View File

@@ -23,6 +23,11 @@ body:
options:
- app.affine.pro
- stage.affine.pro
- dev.affine.live
- affine-preview.vercel.app
- macOS x64
- macOS ARM 64
- Windows x64
validations:
required: true
- type: dropdown

View File

@@ -9,10 +9,6 @@ inputs:
description: 'Run the install step.'
required: false
default: 'true'
electron-workspace-install:
description: 'Run the install step for the electron workspace.'
required: false
default: 'false'
playwright-install:
description: 'Run the install step for Playwright.'
required: false
@@ -33,10 +29,6 @@ runs:
scope: '@toeverything'
cache: 'yarn'
- name: CI Module Resolve
shell: bash
run: node scripts/module-resolve/ci.cjs
- name: Expose yarn config as "$GITHUB_OUTPUT"
id: yarn-config
shell: bash
@@ -86,17 +78,6 @@ runs:
YARN_INSTALL_STATE_PATH: .yarn/ci-cache/install-state.gz
HUSKY: '0'
- name: yarn install (electron)
if: ${{ inputs.electron-workspace-install == 'true' }}
shell: bash
run: yarn install ${{ inputs.extra-flags }}
working-directory: apps/electron
env:
NODE_AUTH_TOKEN: ${{ inputs.npm-token }}
YARN_ENABLE_GLOBAL_CACHE: 'false'
YARN_INSTALL_STATE_PATH: ../../.yarn/ci-cache/install-state.gz
HUSKY: '0'
- name: Get installed Playwright version
id: playwright-version
if: ${{ inputs.playwright-install == 'true' }}

6
.github/labeler.yml vendored
View File

@@ -3,7 +3,7 @@ docs:
- '**/README.md'
- 'packages/templates/**/*'
tests:
test:
- 'tests/**/*'
- '**/tests/**/*'
- '**/__tests__/**/*'
@@ -15,6 +15,8 @@ mod:dev:
mod:workspace: 'packages/workspace/**/*'
mod:theme: 'packages/theme/**/*'
mod:i18n: 'packages/i18n/**/*'
mod:env: 'packages/env/**/*'
@@ -40,3 +42,5 @@ package:y-indexeddb: 'packages/y-indexeddb/**/*'
app:web: 'apps/web/**/*'
app:electron: 'apps/electron/**/*'
app:server: 'apps/server/**/*'

View File

@@ -1,258 +0,0 @@
name: Build & Test
on:
push:
branches: [master]
jobs:
lint:
name: Lint
runs-on: ubuntu-latest
environment: development
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: ./.github/actions/setup-node
- run: yarn lint --max-warnings=0
build-storybook:
name: Build Storybook
runs-on: ubuntu-latest
environment: development
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: ./.github/actions/setup-node
- run: yarn build:storybook
- name: Upload storybook artifact
uses: actions/upload-artifact@v3
with:
name: storybook
path: ./packages/component/storybook-static
if-no-files-found: error
build-frontend:
name: Build @affine/web
runs-on: ubuntu-latest
environment: production
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: ./.github/actions/setup-node
- name: Cache Next.js
uses: actions/cache@v3
with:
path: |
${{ github.workspace }}/apps/web/.next/cache
key: ${{ runner.os }}-nextjs-${{ hashFiles('**/yarn.lock') }}-${{ hashFiles('**.[jt]s', '**.[jt]sx') }}
restore-keys: |
${{ runner.os }}-nextjs-${{ hashFiles('**/yarn.lock') }}-
- name: Build
run: yarn build
env:
NEXT_PUBLIC_FIREBASE_API_KEY: ${{ secrets.NEXT_PUBLIC_FIREBASE_API_KEY }}
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN: ${{ secrets.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN }}
NEXT_PUBLIC_FIREBASE_PROJECT_ID: ${{ secrets.NEXT_PUBLIC_FIREBASE_PROJECT_ID }}
NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET: ${{ secrets.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET }}
NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID: ${{ secrets.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID }}
NEXT_PUBLIC_FIREBASE_APP_ID: ${{ secrets.NEXT_PUBLIC_FIREBASE_APP_ID }}
NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID: ${{ secrets.NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID }}
PERFSEE_TOKEN: ${{ secrets.PERFSEE_TOKEN }}
- name: Export
run: yarn export
- name: Upload artifact
uses: actions/upload-artifact@v3
with:
name: next-js
path: ./apps/web/out
if-no-files-found: error
publish-frontend:
name: Push frontend image
runs-on: ubuntu-latest
needs: build-frontend
permissions:
contents: read
packages: write
env:
REGISTRY: ghcr.io
IMAGE_NAME: 'toeverything/affine-pathfinder'
IMAGE_TAG: canary-${{ github.sha }}
IMAGE_TAG_LATEST: nightly-latest
steps:
- name: Check out the repo
uses: actions/checkout@v3
- name: Download artifact
uses: actions/download-artifact@v3
with:
name: next-js
path: ./apps/web/out
- name: Log in to Docker Hub
uses: docker/login-action@v2
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v4
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
${{ env.IMAGE_TAG }}
${{ env.IMAGE_TAG_LATEST }}
- name: Build Docker image
uses: docker/build-push-action@v3
with:
context: .
push: true
file: ./.github/deployment/Dockerfile
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
storybook-test:
name: Storybook Test
runs-on: ubuntu-latest
environment: development
needs: [build-storybook]
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: ./.github/actions/setup-node
with:
playwright-install: true
- name: Download storybook artifact
uses: actions/download-artifact@v3
with:
name: storybook
path: ./packages/component/storybook-static
- name: Run storybook tests
working-directory: ./packages/component
run: |
yarn exec concurrently -k -s first -n "SB,TEST" -c "magenta,blue" "yarn exec serve ./storybook-static -l 6006" "yarn exec wait-on tcp:6006 && yarn test-storybook --coverage"
- name: Upload storybook test coverage results
uses: codecov/codecov-action@v3
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./packages/component/coverage/storybook/coverage-storybook.json
flags: storybook-test
name: affine
fail_ci_if_error: true
e2e-test:
name: E2E Test
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
shard: [1, 2, 3, 4]
environment: development
needs: [build-frontend, build-storybook]
services:
octobase:
image: ghcr.io/toeverything/cloud-self-hosted:nightly-latest
ports:
- 3000:3000
env:
SIGN_KEY: 'test123'
RUST_LOG: 'debug'
JWST_DEV: '1'
credentials:
username: ${{ github.actor }}
password: ${{ secrets.ACTIONS_PACKAGE_PUBLISH }}
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: ./.github/actions/setup-node
with:
playwright-install: true
- name: Download artifact
uses: actions/download-artifact@v3
with:
name: next-js
path: ./apps/web/.next
- name: Download storybook artifact
uses: actions/download-artifact@v3
with:
name: storybook
path: ./packages/component/storybook-static
- name: Run playwright tests
run: yarn test --forbid-only --shard=${{ matrix.shard }}/${{ strategy.job-total }}
env:
COVERAGE: true
- name: Collect code coverage report
run: yarn exec nyc report -t .nyc_output --report-dir .coverage --reporter=lcov
- name: Upload e2e test coverage results
uses: codecov/codecov-action@v3
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./.coverage/lcov.info
flags: e2etest
name: affine
fail_ci_if_error: true
- name: Upload test results
if: ${{ failure() }}
uses: actions/upload-artifact@v2
with:
name: test-results-e2e
path: ./test-results
if-no-files-found: ignore
unit-test:
name: Unit Test
runs-on: ubuntu-latest
environment: development
needs: build-frontend
services:
octobase:
image: ghcr.io/toeverything/cloud-self-hosted:nightly-latest
ports:
- 3000:3000
env:
SIGN_KEY: 'test123'
RUST_LOG: 'debug'
JWST_DEV: '1'
credentials:
username: ${{ github.actor }}
password: ${{ secrets.ACTIONS_PACKAGE_PUBLISH }}
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: ./.github/actions/setup-node
- name: Download artifact
uses: actions/download-artifact@v3
with:
name: next-js
path: ./apps/web/.next
- name: Unit Test
run: yarn run test:unit:coverage
- name: Upload unit test coverage results
uses: codecov/codecov-action@v3
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./.coverage/store/lcov.info
flags: unittest
name: affine
fail_ci_if_error: true

View File

@@ -1,105 +0,0 @@
name: Build Test Version
on:
workflow_dispatch:
inputs:
tag:
description: 'Custom Tag. Set nightly-latest will publish to development.'
required: true
type: string
# Cancels all previous workflow runs for pull requests that have not completed.
# See https://docs.github.com/en/actions/using-jobs/using-concurrency
concurrency:
# The concurrency group contains the workflow name and the branch name for
# pull requests or the commit hash for any other events.
group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.head_ref || github.sha }}
cancel-in-progress: true
jobs:
build:
name: Lint and Build
runs-on: self-hosted
environment: development
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: ./.github/actions/setup-node
- name: Lint
run: |
yarn lint --max-warnings=0
# - name: Test
# run: yarn test
- name: Build
run: yarn build
env:
NEXT_PUBLIC_FIREBASE_API_KEY: ${{ secrets.NEXT_PUBLIC_FIREBASE_API_KEY }}
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN: ${{ secrets.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN }}
NEXT_PUBLIC_FIREBASE_PROJECT_ID: ${{ secrets.NEXT_PUBLIC_FIREBASE_PROJECT_ID }}
NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET: ${{ secrets.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET }}
NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID: ${{ secrets.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID }}
NEXT_PUBLIC_FIREBASE_APP_ID: ${{ secrets.NEXT_PUBLIC_FIREBASE_APP_ID }}
NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID: ${{ secrets.NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID }}
- name: Export
run: yarn export
- name: Upload artifact
uses: actions/upload-artifact@v3
with:
path: ./apps/web/out
push_to_registry:
# See https://docs.github.com/en/actions/publishing-packages/publishing-docker-images
name: Push Docker image to Docker Hub
runs-on: ubuntu-latest
needs: build
permissions:
contents: read
packages: write
env:
REGISTRY: ghcr.io
IMAGE_NAME: 'toeverything/affine-pathfinder-testing'
IMAGE_TAG: canary-${{ github.sha }}
IMAGE_TAG_LATEST: nightly-latest
steps:
- name: Check out the repo
uses: actions/checkout@v3
- name: Download artifact
uses: actions/download-artifact@v3
with:
name: artifact
path: apps/web/out/
- name: Log in to Docker Hub
uses: docker/login-action@v2
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v4
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
${{ env.IMAGE_TAG }}
${{ inputs.tag }}
- name: Build and push Docker image
uses: docker/build-push-action@v3
with:
context: .
push: true
file: ./.github/deployment/Dockerfile
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

View File

@@ -1,6 +1,9 @@
name: Build & Test
on:
push:
branches:
- master
pull_request:
branches:
- master
@@ -17,18 +20,6 @@ jobs:
uses: ./.github/actions/setup-node
- run: yarn lint --max-warnings=0
install-all:
name: Install All Dependencies
runs-on: ubuntu-latest
environment: development
steps:
- uses: actions/checkout@v3
- name: Install All Dependencies
uses: ./.github/actions/setup-node
with:
electron-workspace-install: true
build-storybook:
name: Build Storybook
runs-on: ubuntu-latest
@@ -46,6 +37,23 @@ jobs:
path: ./packages/component/storybook-static
if-no-files-found: error
build-electron:
name: Build @affine/electron
runs-on: ubuntu-latest
environment: development
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: ./.github/actions/setup-node
- name: Build Electron
working-directory: apps/electron
run: yarn exec ts-node-esm ./scripts/build-ci.mts
- name: Upload Ubuntu desktop artifact
uses: actions/upload-artifact@v3
with:
name: affine-ubuntu
path: ./apps/electron/dist
build:
name: Build @affine/web
runs-on: ubuntu-latest
@@ -73,6 +81,10 @@ jobs:
NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID: ${{ secrets.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID }}
NEXT_PUBLIC_FIREBASE_APP_ID: ${{ secrets.NEXT_PUBLIC_FIREBASE_APP_ID }}
NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID: ${{ secrets.NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID }}
API_SERVER_PROFILE: local
ENABLE_DEBUG_PAGE: true
ENABLE_LEGACY_PROVIDER: true
COVERAGE: true
- name: Upload artifact
uses: actions/upload-artifact@v3
@@ -81,6 +93,85 @@ jobs:
path: ./apps/web/.next
if-no-files-found: error
- name: Build @affine/web for desktop
run: yarn build
env:
NEXT_PUBLIC_FIREBASE_API_KEY: ${{ secrets.NEXT_PUBLIC_FIREBASE_API_KEY }}
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN: ${{ secrets.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN }}
NEXT_PUBLIC_FIREBASE_PROJECT_ID: ${{ secrets.NEXT_PUBLIC_FIREBASE_PROJECT_ID }}
NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET: ${{ secrets.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET }}
NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID: ${{ secrets.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID }}
NEXT_PUBLIC_FIREBASE_APP_ID: ${{ secrets.NEXT_PUBLIC_FIREBASE_APP_ID }}
NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID: ${{ secrets.NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID }}
API_SERVER_PROFILE: affine
ENABLE_DEBUG_PAGE: true
ENABLE_LEGACY_PROVIDER: false
COVERAGE: true
- name: Export static resources
run: yarn export
working-directory: apps/web
- name: Upload static resources artifact
uses: actions/upload-artifact@v3
with:
name: next-js-static
path: ./apps/web/out
if-no-files-found: error
server-test:
name: Server Test
runs-on: ubuntu-latest
environment: development
services:
postgres:
image: postgres
env:
POSTGRES_PASSWORD: affine
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: ./.github/actions/setup-node
- name: Initialize database
run: |
psql -h localhost -U postgres -c "CREATE DATABASE affine;"
psql -h localhost -U postgres -c "CREATE USER affine WITH PASSWORD 'affine';"
psql -h localhost -U postgres -c "ALTER USER affine WITH SUPERUSER;"
env:
PGPASSWORD: affine
- name: Generate prisma client
run: |
yarn exec prisma generate
yarn exec prisma db push
working-directory: apps/server
env:
DATABASE_URL: postgresql://affine:affine@localhost:5432/affine
- name: Run init-db script
run: yarn exec ts-node-esm ./scripts/init-db.ts
working-directory: apps/server
env:
DATABASE_URL: postgresql://affine:affine@localhost:5432/affine
- name: Run server tests
run: yarn test:coverage
working-directory: apps/server
env:
DATABASE_URL: postgresql://affine:affine@localhost:5432/affine
- name: Upload server test coverage results
uses: codecov/codecov-action@v3
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./apps/server/.coverage/lcov.info
flags: server-test
name: affine
fail_ci_if_error: true
storybook-test:
name: Storybook Test
runs-on: ubuntu-latest
@@ -147,6 +238,10 @@ jobs:
name: storybook
path: ./packages/component/storybook-static
- name: Wait for Octobase Ready
run: |
node ./scripts/wait-3000-healthz.mjs
- name: Run playwright tests
run: yarn test --forbid-only --shard=${{ matrix.shard }}/${{ strategy.job-total }}
env:
@@ -168,7 +263,42 @@ jobs:
if: ${{ failure() }}
uses: actions/upload-artifact@v2
with:
name: test-results-e2e
name: test-results-e2e-${{ matrix.shard }}
path: ./test-results
if-no-files-found: ignore
dekstop-test:
name: Desktop Test
runs-on: ubuntu-latest
environment: development
needs: [build, build-electron]
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: ./.github/actions/setup-node
with:
playwright-install: true
- name: Download Ubuntu desktop artifact
uses: actions/download-artifact@v3
with:
name: affine-ubuntu
path: ./apps/electron/dist
- name: Download static resource artifact
uses: actions/download-artifact@v3
with:
name: next-js-static
path: ./apps/electron/resources/web-static
- name: Run desktop tests
run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- yarn test
working-directory: apps/electron
- name: Upload test results
if: ${{ failure() }}
uses: actions/upload-artifact@v2
with:
name: test-results-e2e-${{ matrix.shard }}
path: ./test-results
if-no-files-found: ignore

View File

@@ -17,6 +17,11 @@ on:
type: boolean
required: true
default: true
build-type:
description: 'Build Type (canary, beta or stable)'
type: string
required: true
default: canary
permissions:
actions: write
@@ -30,120 +35,162 @@ concurrency:
cancel-in-progress: true
env:
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
NEXT_PUBLIC_FIREBASE_API_KEY: ${{ secrets.NEXT_PUBLIC_FIREBASE_API_KEY }}
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN: ${{ secrets.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN }}
NEXT_PUBLIC_FIREBASE_PROJECT_ID: ${{ secrets.NEXT_PUBLIC_FIREBASE_PROJECT_ID }}
NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET: ${{ secrets.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET }}
NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID: ${{ secrets.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID }}
NEXT_PUBLIC_FIREBASE_APP_ID: ${{ secrets.NEXT_PUBLIC_FIREBASE_APP_ID }}
NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID: ${{ secrets.NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID }}
BUILD_TYPE: ${{ github.event.inputs.build-type }}
jobs:
make-macos:
runs-on: macos-latest
strategy:
matrix:
arch: [x64, arm64]
before-make:
runs-on: ubuntu-latest
environment: ${{ github.ref_name == 'master' && 'production' || 'development' }}
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: ./.github/actions/setup-node
- name: generate-assets
working-directory: apps/electron
run: yarn generate-assets
env:
NEXT_PUBLIC_FIREBASE_API_KEY: ${{ secrets.NEXT_PUBLIC_FIREBASE_API_KEY }}
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN: ${{ secrets.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN }}
NEXT_PUBLIC_FIREBASE_PROJECT_ID: ${{ secrets.NEXT_PUBLIC_FIREBASE_PROJECT_ID }}
NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET: ${{ secrets.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET }}
NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID: ${{ secrets.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID }}
NEXT_PUBLIC_FIREBASE_APP_ID: ${{ secrets.NEXT_PUBLIC_FIREBASE_APP_ID }}
NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID: ${{ secrets.NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID }}
AFFINE_GOOGLE_CLIENT_ID: ${{ secrets.AFFINE_GOOGLE_CLIENT_ID }}
AFFINE_GOOGLE_CLIENT_SECRET: ${{ secrets.AFFINE_GOOGLE_CLIENT_SECRET }}
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
NEXT_PUBLIC_SENTRY_DSN: ${{ secrets.NEXT_PUBLIC_SENTRY_DSN }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
API_SERVER_PROFILE: prod
ENABLE_TEST_PROPERTIES: false
- name: Upload Artifact (web-static)
uses: actions/upload-artifact@v3
with:
electron-workspace-install: true
name: before-make-web-static
path: apps/electron/resources/web-static
- name: install Rust stable
uses: dtolnay/rust-toolchain@stable
- name: add arm64 target
if: matrix.arch == 'arm64'
run: rustup target add aarch64-apple-darwin
- name: Rust cache
uses: swatinem/rust-cache@v2
- name: Upload Artifact (electron dist)
uses: actions/upload-artifact@v3
with:
workspaces: './packages/octobase-node -> target'
name: before-make-electron-dist
path: apps/electron/dist
- name: Upload YML Build Script
uses: actions/upload-artifact@v3
with:
name: release-yml-build-script
path: apps/electron/scripts/generate-yml.js
make-distribution:
environment: ${{ github.ref_name == 'master' && 'production' || 'development' }}
strategy:
# all combinations: macos-latest x64, macos-latest arm64, windows-latest x64, ubuntu-latest x64
matrix:
spec:
- { os: macos-latest, platform: macos, arch: x64 }
- { os: macos-latest, platform: macos, arch: arm64 }
- { os: ubuntu-latest, platform: linux, arch: x64 }
- { os: windows-latest, platform: windows, arch: x64 }
runs-on: ${{ matrix.spec.os }}
needs: before-make
env:
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
SKIP_GENERATE_ASSETS: 1
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: ./.github/actions/setup-node
- uses: actions/download-artifact@v3
with:
name: before-make-web-static
path: apps/electron/resources/web-static
- uses: actions/download-artifact@v3
with:
name: before-make-electron-dist
path: apps/electron/dist
- name: Signing By Apple Developer ID
if: ${{ matrix.spec.platform == 'macos' }}
uses: apple-actions/import-codesign-certs@v2
with:
p12-file-base64: ${{ secrets.CERTIFICATES_P12 }}
p12-password: ${{ secrets.CERTIFICATES_P12_PASSWORD }}
- name: make build
run: yarn make-macos-${{ matrix.arch }}
- name: make
run: yarn make-${{ matrix.spec.platform }}-${{ matrix.spec.arch }}
working-directory: apps/electron
- name: Save artifacts
- name: Save artifacts (mac)
if: ${{ matrix.spec.platform == 'macos' }}
run: |
mkdir -p builds
mv apps/electron/out/make/AFFiNE.dmg ./builds/affine-darwin-${{ matrix.arch }}-${{ github.event.inputs.version }}.dmg
mv apps/electron/out/*/make/*.dmg ./builds/affine-${{ env.BUILD_TYPE }}-macos-${{ matrix.spec.arch }}.dmg
mv apps/electron/out/*/make/zip/darwin/${{ matrix.spec.arch }}/*.zip ./builds/affine-${{ env.BUILD_TYPE }}-macos-${{ matrix.spec.arch }}.zip
- name: Save artifacts (windows)
if: ${{ matrix.spec.platform == 'windows' }}
run: |
mkdir -p builds
mv apps/electron/out/*/make/zip/win32/x64/AFFiNE*-win32-x64-*.zip ./builds/affine-${{ env.BUILD_TYPE }}-windows-x64.zip
mv apps/electron/out/*/make/squirrel.windows/x64/*.exe ./builds/affine-${{ env.BUILD_TYPE }}-windows-x64.exe
mv apps/electron/out/*/make/squirrel.windows/x64/*.msi ./builds/affine-${{ env.BUILD_TYPE }}-windows-x64.msi
mv apps/electron/out/*/make/squirrel.windows/x64/*.nupkg ./builds/affine-${{ env.BUILD_TYPE }}-windows-x64.nupkg
- name: Save artifacts (linux)
if: ${{ matrix.spec.platform == 'linux' }}
run: |
mkdir -p builds
mv apps/electron/out/*/make/zip/linux/x64/*.zip ./builds/affine-${{ env.BUILD_TYPE }}-linux-x64.zip
- name: Upload Artifact
uses: actions/upload-artifact@v3
with:
name: affine-darwin-${{ matrix.arch }}-builds
path: builds
make-windows:
runs-on: windows-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: ./.github/actions/setup-node
with:
electron-workspace-install: true
- name: install Rust stable
uses: dtolnay/rust-toolchain@stable
- name: Rust cache
uses: swatinem/rust-cache@v2
with:
workspaces: './packages/octobase-node -> target'
- name: make build
run: yarn make-windows-x64
working-directory: apps/electron
- name: Save windows artifacts
run: |
mkdir -p builds
mv apps/electron/out/make/zip/win32/x64/AFFiNE-win32-x64-${{ github.event.inputs.version }}.zip ./builds/affine-windows-x64-${{ github.event.inputs.version }}.zip
- name: Upload Artifact
uses: actions/upload-artifact@v3
with:
name: affine-windows-x64-builds
name: affine-${{ matrix.spec.platform }}-${{ matrix.spec.arch }}-builds
path: builds
release:
needs: [make-macos, make-windows]
needs: make-distribution
runs-on: ubuntu-latest
steps:
- name: Download MacOS x64 Artifacts
uses: actions/download-artifact@v3
with:
name: affine-darwin-x64-builds
path: ./
- name: Download MacOS arm64 Artifacts
steps:
- name: Download Artifacts (macos-x64)
uses: actions/download-artifact@v3
with:
name: affine-darwin-arm64-builds
name: affine-macos-x64-builds
path: ./
- name: Download Windows Artifacts
- name: Download Artifacts (macos-arm64)
uses: actions/download-artifact@v3
with:
name: affine-macos-arm64-builds
path: ./
- name: Download Artifacts (windows-x64)
uses: actions/download-artifact@v3
with:
name: affine-windows-x64-builds
path: ./
- name: Download Artifacts (linux-x64)
uses: actions/download-artifact@v3
with:
name: affine-linux-x64-builds
path: ./
- name: Download Artifacts
uses: actions/download-artifact@v3
with:
name: release-yml-build-script
path: ./
- uses: actions/setup-node@v3
with:
node-version: 18
- name: Generate Release yml
run: |
RELEASE_VERSION=${{ github.event.inputs.version }} node generate-yml.js
- name: Create Release Draft
uses: softprops/action-gh-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
with:
name: Desktop APP ${{ github.event.inputs.version }}
body: 'TODO: Add release notes here'
@@ -158,3 +205,4 @@ jobs:
./RELEASES
./*.AppImage
./*.apk
./*.yml

19
.github/workflows/release.yml vendored Normal file
View File

@@ -0,0 +1,19 @@
name: Release
on:
push:
branches:
- master
jobs:
release:
name: Try publishing npm@latest release
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: ./.github/actions/setup-node
- name: Try publishing to NPM
run: ./scripts/publish.sh
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

5
.gitignore vendored
View File

@@ -5,7 +5,7 @@
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions
.yarn/versions
# compiled output
*dist
@@ -57,9 +57,8 @@ Thumbs.db
.next
out/
storybook-static
i18n_generated.ts
module-resolve.js
module-resolve.cjs
/test-results/
/playwright-report/
/playwright/.cache/

View File

@@ -3,7 +3,6 @@
# check lockfile is up to date
yarn install
cd ./apps/eletron && yarn install
# lint staged files
yarn exec lint-staged

21
.i18n-codegen.json Normal file
View File

@@ -0,0 +1,21 @@
{
"$schema": "./node_modules/@magic-works/i18n-codegen/schema.json",
"version": 1,
"list": [
{
"input": "./packages/i18n/src/resources/en.json",
"output": "./packages/i18n/src/i18n_generated",
"parser": {
"type": "i18next",
"contextSeparator": "$",
"pluralSeparator": "_"
},
"generator": {
"type": "i18next/react-hooks",
"hooks": "useAFFiNEI18N",
"emitTS": true,
"shouldUnescape": true
}
}
]
}

View File

@@ -1,2 +1 @@
pnpm-lock.yaml
apps/electron/layers/preload/preload.d.ts

View File

@@ -1,6 +1,6 @@
{
"singleQuote": true,
"trailingComma": "es5",
"tabWidth": 2,
"arrowParens": "avoid"
"singleQuote": true,
"trailingComma": "es5",
"tabWidth": 2,
"arrowParens": "avoid"
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -2,7 +2,7 @@ nmMode: hardlinks-local
nodeLinker: node-modules
npmAuthToken: '${NODE_AUTH_TOKEN:-NONE}'
npmAuthToken: '${NPM_TOKEN:-NONE}'
npmPublishAccess: public
@@ -11,5 +11,9 @@ npmPublishRegistry: 'https://registry.npmjs.org'
plugins:
- path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs
spec: '@yarnpkg/plugin-interactive-tools'
- path: .yarn/plugins/@yarnpkg/plugin-version.cjs
spec: '@yarnpkg/plugin-version'
- path: .yarn/plugins/@yarnpkg/plugin-workspace-tools.cjs
spec: '@yarnpkg/plugin-workspace-tools'
yarnPath: .yarn/releases/yarn-3.5.0.cjs

122
README.md
View File

@@ -11,18 +11,25 @@
Privacy first, open-source, customizable and ready to use - a free replacement for Notion & Miro. <br />
</p>
<div>
</div>
<div align="center">
<!--
Make New Badge Pattern badges inline
See https://github.com/all-?/all-contributors/issues/361#issuecomment-637166066
-->
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
[all-contributors-badge]: https://img.shields.io/badge/all_contributors-48-orange.svg?style=flat-square
[all-contributors-badge]: https://img.shields.io/badge/all_contributors-66-orange.svg?style=flat-square
<!-- ALL-CONTRIBUTORS-BADGE:END -->
[![affine-app-logo]](https://app.affine.pro)
[![AFFiNE Web](<https://img.shields.io/badge/-Try%20It%20Online%20%E2%86%92-rgb(84,56,255)?style=flat-square&logoColor=white&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAACXBIWXMAAADAAAAAwAEwd99eAAABjElEQVRYhe1W0U3DMBB9RfyTDeoNyAYNG2QDOgJsECYgGxA26AZ4hIxgJqCZ4PjIGV+tUxK7raqiPsmKdXe5e3fOs7IiIlwSdxetfiNw7QRKAD0Ax/ssrI5QgQOw5v03AJOTJHcCL1x84LVmWzJyJlBg7P4BwCvb3pmIAbBPykZEqaulEU7YHNva1HypxUsKqIS9EvbynASs0n3ss+ciUIsuO8VvhL9emjdFBa3YO8XvALwpsZNYSqBB0PwUWgRZNksSL5GhlN0ngGd+dkpsD6AG8IGlslxwTh2fa09EBc3Dir32rRysuQlUAL54/wTAcpePPAXHPsOTGXhSEv69rAlYpZOt6DSO29J4D/TRRLJk6AvtaZSY9PkCFYVLqI9i/NF5YkkECgrXa6P4fVEn4iolrhNxRQqBZu7FqMNdZiMqAUPj2KdGZyicu1dHzlGqBHxn2sdTR53bmeJ+ebJd7LtXhGH4uQEwd0ttAPzMxGi5/6BdxTuMej41Bs59gGP+CU+Cq/4tvxH4HwR+Ab3Uqr/VGbqEAAAAAElFTkSuQmCC>)](https://app.affine.pro)
[![AFFiNE macOS M1/M2 Chip](https://img.shields.io/badge/-macOS_M_Chip%20%E2%86%92-black?style=flat-square&logo=apple&logoColor=white)](https://affine.pro/download)
[![AFFiNE macOS x64](https://img.shields.io/badge/-macOS_x86%20%E2%86%92-black?style=flat-square&logo=apple&logoColor=white)](https://affine.pro/download)
[![AFFiNE Window x64](https://img.shields.io/badge/-Windows%20%E2%86%92-blue?style=flat-square&logo=windows&logoColor=white)](https://affine.pro/download)
[![AFFiNE Linux](https://img.shields.io/badge/-Linux%20%E2%86%92-yellow?style=flat-square&logo=linux&logoColor=white)](https://affine.pro/download)
[![stars-icon]](https://github.com/toeverything/AFFiNE)
[![All Contributors][all-contributors-badge]](#contributors)
[![codecov]](https://codecov.io/gh/toeverything/AFFiNE)
@@ -34,7 +41,9 @@ See https://github.com/all-?/all-contributors/issues/361#issuecomment-637166066
</div>
<div>
---
<div align="center">
<a href="http://affine.pro"><img src="https://img.shields.io/badge/-AFFiNE-06449d?style=social&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEsAAABLCAMAAAAPkIrYAAAAP1BMVEU8b9w8b9w+b947cNw7b9w6b908b909b9w8b9w7b9w8b9w7cN08b9w7b908b9w7b9w8b907cNw8b9w8b91HcEx3NJCJAAAAFXRSTlP/3QWSgA+lHPlu6Di4XtIrxk/xRADGudUoAAAB9UlEQVR42tWYwbKjIBREG0GJKkRj/v9bZ1ZvRC99rzib11tTB9qqnKoW3/+X38vy7ifzQ1b/wk/8Q1bCv3y6Z6wFh2x2llIRGB6xRhzz6p+wVhRJD1gRZZYHrADYSyqsjFPGZtYbuFESesUysZXlcMnYyJpxTW5keQh5N7G6CUJCE2uHFNfEGiBmbmB1H4jxDawNcqbuPmtAJTtj6RZ0lpIwiR5jNmgfNtHHwLXPWfFYcS2NMdxkjac/dNaNCJPo3yf9pFuseHbDrBsRFguGs8te8Q4rXzTjVSPCIHp3FePKWbzi30xE+4zlBMmoJaGLfpLUmAmLiN4Xyibahy76WZRQMLJ2WX27on2oFvQVac8yi4p+J2forA0V8W1c++AVS1f1H6p9KKLHxk9RWKmsyB+VLC76gV65DLjokdg5KmsEMXsiDwXWSmTc9ezSoKJHoi9zUVihbMHfQOSsXB7Mrz1S1huKPde69sEsiKgNt8hYTjiWlAyENeu7IFe1D15RSEBN+yCiXw17K1RZm/w7UtJVWYN8f1ZyLlkVb2bT4vIVVrINH1dqX2YttkHmIWsfVWs646wcRFYis6fIVGpfYq1kjpGSW8kSRD+xYSmXRM0Ang9eSZioVdy/5pWaLqzIRyIpuVxYozvGf1m67I7pf/s3UXv+AP61NI2Y+BbSAAAAAElFTkSuQmCC" height=25></a>
&nbsp;
<a href="https://community.affine.pro"><img src="https://img.shields.io/badge/-Community-424549?style=social&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAAXNJREFUWEftlitLRUEURtdVEVExWUx2qxgNVouoXYtNDP4Tw20WtftAsItZrHaTYBJREZ98MAc248wcZxi4CGfSeezHmm/23kyPAa/egPPTAXQK/FsFBP7ldVDRZoqcgO9I+2bHy3ZIJBfTCPCZM1tqAxwBmzUBrNQNbEx+5b0B5oEN4NCBrAMnMaiUAuPAs3HU82TLEZwBqwGbaJ4UgKQ8CFR6SoEl4LIWwCJwZQCegKkWBWLHVKSActvdzgG3DqitDf3/VQBskBDALrDnAKXUo3ueAF5KinAf2DKOmnzD7l214bdbA6hC1XHZNQa8hSBC0hwDa57xDHDvvvWB7ciOZoE79+8CWPbsBGc769eFxJdWIKcuyIdRoG3W7AAC1dJkHDIOo8B78+4rEBo8r4AkLFk6Jk3HaeDBBTgHVmIAfpJUz+cAFXVBreQCvQYW/lqEjV1NAMUMqpAaxQMHyDnjYtuS+0BxstwaqJooFqxToFPgB5FuPCEB6XK2AAAAAElFTkSuQmCC" height=25></a>
@@ -49,18 +58,18 @@ See https://github.com/all-?/all-contributors/issues/361#issuecomment-637166066
</div>
<br />
<div>
<em>AFFiNE - just like the word affine (əˈɪn | a-fine).</em>
<div align="center">
<em>See docs, canvas and tables are hyper merged with AFFiNE - just like the word affine (əˈɪn | a-fine).</em>
</div>
<br />
<img width="1920" alt="affine_screen" src="https://user-images.githubusercontent.com/4605025/198174913-d4f90da8-ae2a-4eb6-8424-55c94de432a1.jpeg"></div>
![img_v2_37a7cc04-ab3f-4405-ae9a-f84ceb4c948g](https://user-images.githubusercontent.com/79301703/230892907-5fd5c0c5-1665-4d75-8a35-744e0afc36a5.gif)
## Join our community
Before we tell you how to get started with AFFiNE, we'd like to shamelessly plug our awesome user and developer communities across [official social platforms](https://community.affine.pro/c/start-here/)! Once youre familiar with using the software, maybe you will share your wisdom with others and even consider joining the [AFFiNE Ambassador program](https://community.affine.pro/c/start-here/affine-ambassador) to help spread AFFiNE to the world.
## Getting started
## Getting started & Stay tunned with us.
⚠️ Please note that AFFiNE is still under active development and is not yet ready for production use. ⚠️
@@ -68,16 +77,21 @@ Before we tell you how to get started with AFFiNE, we'd like to shamelessly plug
[![community.affine.pro](https://img.shields.io/static/v1?label=Join%20the%20community&message=%E2%86%92&style=for-the-badge)](https://community.affine.pro) Our wonderful community, where you can meet and engage with the team, developers and other like-minded enthusiastic user of AFFiNE.
Star us, and you will receive all releases notifications from GitHub without any delay!
![rbU3YmmsQT](https://user-images.githubusercontent.com/79301703/230891830-0110681e-8c7e-483b-b6d9-9e42b291b9ef.gif)
## Features
- **Hyper merged** — Write, draw and plan all at once. Assemble any blocks you love on any canvas you like to enjoy seamless transitions bewtween workflows with AFFiNE.
- **Privacy focussed** — AFFiNE is built with your privacy in mind and is one of our key concerns. We want you to keep control of your data, allowing you to store it as you like, where you like while still being able to freely edit and view your data on-demand.
- **Offline-first** - With your privacy in mind we also decided to go offline-first. This means that AFFiNE can be used offline, whether you want to view or edit, with support for conflict-free merging when you are back online.
- **Clean, intuitive design** — With AFFiNE you can concentrate on editing with a clean and modern interface. Which is responsive, so it looks great on tablets too, and mobile support is coming in the future.
- **Seamless transitions** — However you want your data displayed, whichever viewing mode you use, AFFiNE supports easy transitions to allow you to quickly and effortlessly view your data in the way you want.
- **Markdown support** — When you write in AFFiNE you can use Markdown syntax which helps create an easier editing experience, that can be experienced with just a keyboard. And this allows you to export your data cleanly into Markdown.
- **Modern Block Editor with Markdown support** — A modern block editor can help you not only for docs, but slides and tables as well. When you write in AFFiNE you can use Markdown syntax which helps create an easier editing experience, that can be experienced with just a keyboard. And this allows you to export your data cleanly into Markdown.
- **Collaboration** — Whether you want to collaborate with yourself across multiple devices, or work together with others, support for collaboration and multiplayer is out-of-the-box, which makes it easy for teams to get started with AFFiNE.
- **Choice of multiple languages** — Thanks to community contributions AFFiNE offers support for multiple languages. If you don't find your language or would like to suggest some changes we welcome your contributions.
![img_v2_3a4ee0da-6dd7-48cb-8f19-5411f86768ag](https://user-images.githubusercontent.com/79301703/230893796-dc707955-e4e5-4a42-a3c9-18d1ea754f6f.gif)
## Contributing
| Bug Reports | Feature Requests | Questions/Discussions | AFFiNE Community |
@@ -97,6 +111,14 @@ Looking for **others ways to contribute** and wondering where to start? Check ou
If you have questions, you are welcome to contact us. One of the best places to get more info and learn more is in the [AFFiNE Community](https://community.affine.pro) where you can engage with other like-minded individuals.
## Ecosystem
| Name | | |
| --------------------------------------------------------- | ---------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| [@affine/component](https://affine-storybook.vercel.app/) | AFFiNE Component Resources | [![](https://img.shields.io/codecov/c/github/toeverything/affine?style=flat-square)](https://affine-storybook.vercel.app/) |
| [@toeverything/y-indexeddb](packages/y-indexeddb) | IndexedDB database adapter for Yjs | [![](https://img.shields.io/npm/dm/@toeverything/y-indexeddb?style=flat-square&color=eee)](https://www.npmjs.com/package/@toeverything/y-indexeddb) |
| [@toeverything/theme](packages/theme) | AFFiNE theme | [![](https://img.shields.io/npm/dm/@toeverything/theme?style=flat-square&color=eee)](https://www.npmjs.com/package/@toeverything/theme) |
## Thanks
We would also like to give thanks to open-source projects that make AFFiNE possible:
@@ -115,80 +137,11 @@ Thanks a lot to the community for providing such powerful and simple libraries,
# Contributors
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
<!-- prettier-ignore-start -->
<!-- markdownlint-disable -->
<table>
<tbody>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/doodlewind"><img src="https://avatars.githubusercontent.com/u/7312949?v=4?s=50" width="50px;" alt="Yifeng Wang"/><br /><sub><b>Yifeng Wang</b></sub></a><br /><a href="https://github.com/toeverything/AFFiNE/commits?author=doodlewind" title="Code">💻</a> <a href="https://github.com/toeverything/AFFiNE/commits?author=doodlewind" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://darksky.eu.org/"><img src="https://avatars.githubusercontent.com/u/25152247?v=4?s=50" width="50px;" alt="DarkSky"/><br /><sub><b>DarkSky</b></sub></a><br /><a href="https://github.com/toeverything/AFFiNE/commits?author=darkskygit" title="Code">💻</a> <a href="https://github.com/toeverything/AFFiNE/commits?author=darkskygit" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://zhangchi.page/"><img src="https://avatars.githubusercontent.com/u/5910926?v=4?s=50" width="50px;" alt="Chi Zhang"/><br /><sub><b>Chi Zhang</b></sub></a><br /><a href="https://github.com/toeverything/AFFiNE/commits?author=tzhangchi" title="Code">💻</a> <a href="https://github.com/toeverything/AFFiNE/commits?author=tzhangchi" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/alt1o"><img src="https://avatars.githubusercontent.com/u/21084335?v=4?s=50" width="50px;" alt="wang xinglong"/><br /><sub><b>wang xinglong</b></sub></a><br /><a href="https://github.com/toeverything/AFFiNE/commits?author=alt1o" title="Code">💻</a> <a href="https://github.com/toeverything/AFFiNE/commits?author=alt1o" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/DiamondThree"><img src="https://avatars.githubusercontent.com/u/24630517?v=4?s=50" width="50px;" alt="DiamondThree"/><br /><sub><b>DiamondThree</b></sub></a><br /><a href="https://github.com/toeverything/AFFiNE/commits?author=DiamondThree" title="Code">💻</a> <a href="https://github.com/toeverything/AFFiNE/commits?author=DiamondThree" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://lawvs.github.io/profile/"><img src="https://avatars.githubusercontent.com/u/18554747?v=4?s=50" width="50px;" alt="Whitewater"/><br /><sub><b>Whitewater</b></sub></a><br /><a href="https://github.com/toeverything/AFFiNE/commits?author=lawvs" title="Code">💻</a> <a href="https://github.com/toeverything/AFFiNE/commits?author=lawvs" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/zuoxiaodong0815"><img src="https://avatars.githubusercontent.com/u/53252747?v=4?s=50" width="50px;" alt="xiaodong zuo"/><br /><sub><b>xiaodong zuo</b></sub></a><br /><a href="https://github.com/toeverything/AFFiNE/commits?author=zuoxiaodong0815" title="Code">💻</a> <a href="https://github.com/toeverything/AFFiNE/commits?author=zuoxiaodong0815" title="Documentation">📖</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Himself65"><img src="https://avatars.githubusercontent.com/u/14026360?v=4?s=50" width="50px;" alt="Himself65"/><br /><sub><b>Himself65</b></sub></a><br /><a href="https://github.com/toeverything/AFFiNE/commits?author=Himself65" title="Code">💻</a> <a href="https://github.com/toeverything/AFFiNE/commits?author=Himself65" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/SaikaSakura"><img src="https://avatars.githubusercontent.com/u/11530942?v=4?s=50" width="50px;" alt="MingLIang Wang"/><br /><sub><b>MingLIang Wang</b></sub></a><br /><a href="https://github.com/toeverything/AFFiNE/commits?author=SaikaSakura" title="Code">💻</a> <a href="https://github.com/toeverything/AFFiNE/commits?author=SaikaSakura" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/QiShaoXuan"><img src="https://avatars.githubusercontent.com/u/22772830?v=4?s=50" width="50px;" alt="Qi"/><br /><sub><b>Qi</b></sub></a><br /><a href="https://github.com/toeverything/AFFiNE/commits?author=QiShaoXuan" title="Code">💻</a> <a href="https://github.com/toeverything/AFFiNE/commits?author=QiShaoXuan" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://onetwo.ren/wiki"><img src="https://avatars.githubusercontent.com/u/3746270?v=4?s=50" width="50px;" alt="lin onetwo"/><br /><sub><b>lin onetwo</b></sub></a><br /><a href="https://github.com/toeverything/AFFiNE/commits?author=linonetwo" title="Code">💻</a> <a href="https://github.com/toeverything/AFFiNE/commits?author=linonetwo" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://colelawrence.com/"><img src="https://avatars.githubusercontent.com/u/2925395?v=4?s=50" width="50px;" alt="Cole Lawrence"/><br /><sub><b>Cole Lawrence</b></sub></a><br /><a href="https://github.com/toeverything/AFFiNE/commits?author=colelawrence" title="Code">💻</a> <a href="https://github.com/toeverything/AFFiNE/commits?author=colelawrence" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/thorseraq"><img src="https://avatars.githubusercontent.com/u/20554850?v=4?s=50" width="50px;" alt="x1a0t"/><br /><sub><b>x1a0t</b></sub></a><br /><a href="https://github.com/toeverything/AFFiNE/commits?author=thorseraq" title="Code">💻</a> <a href="https://github.com/toeverything/AFFiNE/commits?author=thorseraq" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/HeJiachen-PM"><img src="https://avatars.githubusercontent.com/u/79301703?v=4?s=50" width="50px;" alt="HeJiachen-PM"/><br /><sub><b>HeJiachen-PM</b></sub></a><br /><a href="#research-HeJiachen-PM" title="Research">🔬</a> <a href="https://github.com/toeverything/AFFiNE/commits?author=HeJiachen-PM" title="Documentation">📖</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://www.notion.so/houjoe/Joe-2a85f5be01004cd2b6a5ad26fbb948b1"><img src="https://avatars.githubusercontent.com/u/22443345?v=4?s=50" width="50px;" alt="houjoe"/><br /><sub><b>houjoe</b></sub></a><br /><a href="#research-joebeijing" title="Research">🔬</a> <a href="https://github.com/toeverything/AFFiNE/commits?author=joebeijing" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Yipei-Operation"><img src="https://avatars.githubusercontent.com/u/79373028?v=4?s=50" width="50px;" alt="Yipei Wei"/><br /><sub><b>Yipei Wei</b></sub></a><br /><a href="https://github.com/toeverything/AFFiNE/commits?author=Yipei-Operation" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/VelikaHF"><img src="https://avatars.githubusercontent.com/u/121547898?v=4?s=50" width="50px;" alt="Velika"/><br /><sub><b>Velika</b></sub></a><br /><a href="#design-VelikaHF" title="Design">🎨</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Svaney-ssman"><img src="https://avatars.githubusercontent.com/u/110808979?v=4?s=50" width="50px;" alt="Svaney"/><br /><sub><b>Svaney</b></sub></a><br /><a href="#design-Svaney-ssman" title="Design">🎨</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/fanjing22"><img src="https://avatars.githubusercontent.com/u/109729699?v=4?s=50" width="50px;" alt="fanjing22"/><br /><sub><b>fanjing22</b></sub></a><br /><a href="#design-fanjing22" title="Design">🎨</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://xell.me/"><img src="https://avatars.githubusercontent.com/u/132558?v=4?s=50" width="50px;" alt="Guozhu Liu"/><br /><sub><b>Guozhu Liu</b></sub></a><br /><a href="#design-xell" title="Design">🎨</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/fyZheng07"><img src="https://avatars.githubusercontent.com/u/63830919?v=4?s=50" width="50px;" alt="fyZheng07"/><br /><sub><b>fyZheng07</b></sub></a><br /><a href="#eventOrganizing-fyZheng07" title="Event Organizing">📋</a> <a href="#userTesting-fyZheng07" title="User Testing">📓</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/CJSS"><img src="https://avatars.githubusercontent.com/u/4605025?v=4?s=50" width="50px;" alt="CJSS"/><br /><sub><b>CJSS</b></sub></a><br /><a href="https://github.com/toeverything/AFFiNE/commits?author=CJSS" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/JimmFly"><img src="https://avatars.githubusercontent.com/u/102217452?v=4?s=50" width="50px;" alt="JimmFly"/><br /><sub><b>JimmFly</b></sub></a><br /><a href="https://github.com/toeverything/AFFiNE/commits?author=JimmFly" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/mitsuhatu"><img src="https://avatars.githubusercontent.com/u/110213079?v=4?s=50" width="50px;" alt="mitsuhatu"/><br /><sub><b>mitsuhatu</b></sub></a><br /><a href="https://github.com/toeverything/AFFiNE/commits?author=mitsuhatu" title="Code">💻</a> <a href="https://github.com/toeverything/AFFiNE/commits?author=mitsuhatu" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://shockwave.me/"><img src="https://avatars.githubusercontent.com/u/15013925?v=4?s=50" width="50px;" alt="Austaras"/><br /><sub><b>Austaras</b></sub></a><br /><a href="https://github.com/toeverything/AFFiNE/commits?author=Austaras" title="Code">💻</a> <a href="https://github.com/toeverything/AFFiNE/commits?author=Austaras" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/uptonking"><img src="https://avatars.githubusercontent.com/u/11391549?v=4?s=50" width="50px;" alt="Jin Yao"/><br /><sub><b>Jin Yao</b></sub></a><br /><a href="https://github.com/toeverything/AFFiNE/commits?author=uptonking" title="Code">💻</a> <a href="https://github.com/toeverything/AFFiNE/commits?author=uptonking" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/CarlosZoft"><img src="https://avatars.githubusercontent.com/u/62192072?v=4?s=50" width="50px;" alt="Carlos Rafael "/><br /><sub><b>Carlos Rafael </b></sub></a><br /><a href="https://github.com/toeverything/AFFiNE/commits?author=CarlosZoft" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/caleboleary"><img src="https://avatars.githubusercontent.com/u/12816579?v=4?s=50" width="50px;" alt="Caleb OLeary"/><br /><sub><b>Caleb OLeary</b></sub></a><br /><a href="https://github.com/toeverything/AFFiNE/commits?author=caleboleary" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/westongraham"><img src="https://avatars.githubusercontent.com/u/89493023?v=4?s=50" width="50px;" alt="Weston Graham"/><br /><sub><b>Weston Graham</b></sub></a><br /><a href="https://github.com/toeverything/AFFiNE/commits?author=westongraham" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/pointmax"><img src="https://avatars.githubusercontent.com/u/49361135?v=4?s=50" width="50px;" alt="pointmax"/><br /><sub><b>pointmax</b></sub></a><br /><a href="https://github.com/toeverything/AFFiNE/commits?author=pointmax" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://liby.github.io/notes"><img src="https://avatars.githubusercontent.com/u/38807139?v=4?s=50" width="50px;" alt="Bryan Lee"/><br /><sub><b>Bryan Lee</b></sub></a><br /><a href="https://github.com/toeverything/AFFiNE/commits?author=liby" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/chenmoonmo"><img src="https://avatars.githubusercontent.com/u/36295999?v=4?s=50" width="50px;" alt="Simon Li"/><br /><sub><b>Simon Li</b></sub></a><br /><a href="https://github.com/toeverything/AFFiNE/commits?author=chenmoonmo" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/githbq"><img src="https://avatars.githubusercontent.com/u/10009709?v=4?s=50" width="50px;" alt="Bob Hu"/><br /><sub><b>Bob Hu</b></sub></a><br /><a href="https://github.com/toeverything/AFFiNE/commits?author=githbq" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://quavo.vercel.app/"><img src="https://avatars.githubusercontent.com/u/67266933?v=4?s=50" width="50px;" alt="Quavo"/><br /><sub><b>Quavo</b></sub></a><br /><a href="https://github.com/toeverything/AFFiNE/commits?author=lucky-chap" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/LuciNyan"><img src="https://avatars.githubusercontent.com/u/22126563?v=4?s=50" width="50px;" alt="子瞻 Luci"/><br /><sub><b>子瞻 Luci</b></sub></a><br /><a href="https://github.com/toeverything/AFFiNE/commits?author=LuciNyan" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="http://blog.ipili.me/"><img src="https://avatars.githubusercontent.com/u/4948120?v=4?s=50" width="50px;" alt="Horus"/><br /><sub><b>Horus</b></sub></a><br /><a href="https://github.com/toeverything/AFFiNE/commits?author=m1911star" title="Code">💻</a> <a href="#platform-m1911star" title="Packaging/porting to new platform">📦</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://segmentfault.com/u/qzuser_584786517d31a"><img src="https://avatars.githubusercontent.com/u/15103283?v=4?s=50" width="50px;" alt="Super.x"/><br /><sub><b>Super.x</b></sub></a><br /><a href="https://github.com/toeverything/AFFiNE/commits?author=fanshyiis" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://wangyu-1999.github.io/"><img src="https://avatars.githubusercontent.com/u/80874770?v=4?s=50" width="50px;" alt="Wang Yu"/><br /><sub><b>Wang Yu</b></sub></a><br /><a href="https://github.com/toeverything/AFFiNE/commits?author=wangyu-1999" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://felixc.at/"><img src="https://avatars.githubusercontent.com/u/1006477?v=4?s=50" width="50px;" alt="Felix Yan"/><br /><sub><b>Felix Yan</b></sub></a><br /><a href="https://github.com/toeverything/AFFiNE/commits?author=felixonmars" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/lynettelopez"><img src="https://avatars.githubusercontent.com/u/32908859?v=4?s=50" width="50px;" alt="Lynette Lopez"/><br /><sub><b>Lynette Lopez</b></sub></a><br /><a href="https://github.com/toeverything/AFFiNE/commits?author=lynettelopez" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://manjusaka.itscoder.com/"><img src="https://avatars.githubusercontent.com/u/7054676?v=4?s=50" width="50px;" alt="Manjusaka"/><br /><sub><b>Manjusaka</b></sub></a><br /><a href="https://github.com/toeverything/AFFiNE/commits?author=Zheaoli" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://juejin.cn/user/2867982785579102/posts?sort=popular"><img src="https://avatars.githubusercontent.com/u/76603360?v=4?s=50" width="50px;" alt="Frozen FIsh"/><br /><sub><b>Frozen FIsh</b></sub></a><br /><a href="https://github.com/toeverything/AFFiNE/commits?author=sudongyuer" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/MuhammedFaraz"><img src="https://avatars.githubusercontent.com/u/92734739?v=4?s=50" width="50px;" alt="Mohammed Faraz"/><br /><sub><b>Mohammed Faraz</b></sub></a><br /><a href="https://github.com/toeverything/AFFiNE/commits?author=MuhammedFaraz" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://pranavsriram.dev/"><img src="https://avatars.githubusercontent.com/u/28348429?v=4?s=50" width="50px;" alt="Pranav Sriram "/><br /><sub><b>Pranav Sriram </b></sub></a><br /><a href="https://github.com/toeverything/AFFiNE/commits?author=Pranav4399" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Reson-a"><img src="https://avatars.githubusercontent.com/u/20806266?v=4?s=50" width="50px;" alt="Reson-a"/><br /><sub><b>Reson-a</b></sub></a><br /><a href="https://github.com/toeverything/AFFiNE/commits?author=Reson-a" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://t.me/littlepoint"><img src="https://avatars.githubusercontent.com/u/7611700?v=4?s=50" width="50px;" alt="Zhizhen He"/><br /><sub><b>Zhizhen He</b></sub></a><br /><a href="https://github.com/toeverything/AFFiNE/commits?author=hezhizhen" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://akr.moe/"><img src="https://avatars.githubusercontent.com/u/85140972?v=4?s=50" width="50px;" alt="AkaraChen"/><br /><sub><b>AkaraChen</b></sub></a><br /><a href="https://github.com/toeverything/AFFiNE/commits?author=AkaraChen" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/suyanhanx"><img src="https://avatars.githubusercontent.com/u/24221472?v=4?s=50" width="50px;" alt="Suyan"/><br /><sub><b>Suyan</b></sub></a><br /><a href="https://github.com/toeverything/AFFiNE/commits?author=suyanhanx" title="Code">💻</a></td>
</tr>
</tbody>
</table>
We would like to express our gratitude to all the individuals who have already contributed to AFFiNE! If you have any AFFiNE-related project, documentation, tool or template, please feel free to contribute it by submitting a pull request to our curated list on GitHub: [awesome-affine](https://github.com/toeverything/awesome-affine).
<!-- markdownlint-restore -->
<!-- prettier-ignore-end -->
<!-- ALL-CONTRIBUTORS-LIST:END -->
<a href="https://github.com/toeverything/affine/graphs/contributors">
<img src="https://user-images.githubusercontent.com/5910926/233382206-312428ca-094a-4579-ae06-213961ed7eab.svg" />
</a>
## Self-Host
@@ -230,11 +183,10 @@ See [LICENSE] for details.
[jobs available]: ./docs/jobs.md
[latest packages]: https://github.com/toeverything/AFFiNE/pkgs/container/affine-self-hosted
[contributor license agreement]: https://github.com/toeverything/affine/edit/master/.github/CLA.md
[affine-app-logo]: https://img.shields.io/static/v1?label=Try%20Online&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAAhpJREFUWEdjZEACtnl3MxgY/0YzMjAaMzAwcCLLUYH9/T/D/7MM/5mXHp6kPANmHiOI4Zx9Xfg3C+tKBob/zlSwiAgjGPey/vkdvneq5luwA+zy7+yhn+Vwv+89NFHFhREU7IyM/6YT4WyqK/n/nymT0Tb/1mFGBkYbqptOhIH/Gf4fYbTLv/2NBgmOCOvBSr6DHPCfWNW0UEe2A2x1uRlakiXBbtpx6jND+7KXZLmPbAdURokzeJjxwi31rrzH8OX7P5IdQbYDtnUoMXBzMMEt7Fj2imH7qU/0cQBy8MNsPHL5K0P13Of0cQB68MNsJScaSI4CHk4mhq3tSnCf3n36k0FZmh3Mn7L+DcPqgx9ICgWSHeBpxsdQESUGtgRk+eqDH+H8O09/MiR3P6atA1qTJRlsdLnhPgYlPOQQCW96wPDi3R+iHUFSCKAHP8wydEeREg0kOQA9+JOgwR1qL8CQEygC9jWp0UCSA+aVysIT3JqDHxgmr38DtlRCiIVhZZ0CPNhB6QDkEGIA0Q4gZAkuxxFyBNEOQA7ml+/+MIQ1PUAxG1kelAhB6YMYQLQDCPmQUAjhcgxRDiDWcEKOxOYIohyQGyjCEGIvANaPLfhhBiNHA6hmBBXNhABRDgCV/aBQAAFQpYMrn4PUgNTCACiXEMoNRDmAkC8okR8UDhjYRumAN8sHvGMCSkAD2jUDOWDAO6ewbDQQ3XMAy/oxKownQR0AAAAASUVORK5CYII=&color=orange&message=%E2%86%92
[rust-version-icon]: https://img.shields.io/badge/Rust-1.70.0-dea584
[stars-icon]: https://img.shields.io/github/stars/toeverything/AFFiNE.svg?style=flat&logo=github&colorB=red&label=stars
[codecov]: https://codecov.io/gh/toeverything/affine/branch/master/graphs/badge.svg?branch=master
[node-version-icon]: https://img.shields.io/badge/node-%3E=18.15.0-success
[node-version-icon]: https://img.shields.io/badge/node-%3E=18.16.0-success
[typescript-version-icon]: https://img.shields.io/github/package-json/dependency-version/toeverything/affine/dev/typescript
[react-version-icon]: https://img.shields.io/github/package-json/dependency-version/toeverything/affine/dev/react?color=rgb%2897%2C%20218%2C%20251%29
[blocksuite-icon]: https://img.shields.io/github/package-json/dependency-version/toeverything/AFFiNE/@blocksuite/store?color=6880ff&filename=apps%2Fweb%2Fpackage.json&label=blocksuite

View File

@@ -11,3 +11,4 @@ resources/web-static
!.yarn/releases
!.yarn/sdks
!.yarn/versions
dev.json

View File

@@ -1,8 +0,0 @@
cacheFolder: '../../.yarn/cache'
# deferredVersionFolder: '../../.yarn/versions'
globalFolder: '../../.yarn/global'
installStatePath: '../../.yarn/install-state.gz'
patchFolder: '../../.yarn/patches'
pnpUnpluggedFolder: '../../.yarn/unplugged'
yarnPath: '../../.yarn/releases/yarn-3.5.0.cjs'
virtualFolder: '../../.yarn/__virtual__'

View File

@@ -1,21 +1,35 @@
# AFFiNE Electron App
# ⚠️ NOTE ⚠️
Due to PNPM related issues, this project is currently using **yarn 3**.
See https://github.com/electron/forge/issues/2633
## Development
To run AFFiNE Desktop Client Application locally, run the following commands:
```sh
# in repo root
yarn install
yarn dev
# in apps/electron
yarn generate-assets
yarn dev # or yarn prod for production build
```
# in project root, start web app at :8080
yarn dev
# build octobase-node
yarn workspace @affine/octobase-node build
## Troubleshooting
# in /apps/electron, start electron app
yarn dev
### better-sqlite3 error
When running tests or starting electron, you may encounter the following error:
> Error: The module 'apps/electron/node_modules/better-sqlite3/build/Release/better_sqlite3.node'
This is due to the fact that the `better-sqlite3` package is built for the Node.js version in Electron & in your machine. To fix this, run the following command based on different cases:
```sh
# for running unit tests, we are not using Electron's node:
yarn rebuild better-sqlite3
# for running Electron, we are using Electron's node:
yarn postinstall
```
## Credits

View File

@@ -1,46 +1,121 @@
/* eslint-disable @typescript-eslint/no-var-requires */
const {
utils: { fromBuildIdentifier },
} = require('@electron-forge/core');
const path = require('node:path');
const buildType = (process.env.BUILD_TYPE || 'stable').trim().toLowerCase();
const stableBuild = buildType === 'stable';
const productName = !stableBuild ? `AFFiNE-${buildType}` : 'AFFiNE';
const icoPath = !stableBuild
? `./resources/icons/icon_${buildType}.ico`
: './resources/icons/icon.ico';
const icnsPath = !stableBuild
? `./resources/icons/icon_${buildType}.icns`
: './resources/icons/icon.icns';
const arch =
process.argv.indexOf('--arch') > 0
? process.argv[process.argv.indexOf('--arch') + 1]
: process.arch;
/**
* @type {import('@electron-forge/shared-types').ForgeConfig}
*/
module.exports = {
buildIdentifier: buildType,
packagerConfig: {
name: 'AFFiNE',
icon: './resources/icons/icon.icns',
name: productName,
appBundleId: fromBuildIdentifier({
canary: 'pro.affine.canary',
beta: 'pro.affine.beta',
stable: 'pro.affine.app',
}),
icon: icnsPath,
osxSign: {
identity: 'Developer ID Application: TOEVERYTHING PTE. LTD.',
'hardened-runtime': true,
}, // object must exist even if empty
osxNotarize: {
tool: 'notarytool',
appleId: process.env.APPLE_ID,
appleIdPassword: process.env.APPLE_PASSWORD,
teamId: process.env.APPLE_TEAM_ID,
},
osxNotarize: process.env.APPLE_ID
? {
tool: 'notarytool',
appleId: process.env.APPLE_ID,
appleIdPassword: process.env.APPLE_PASSWORD,
teamId: process.env.APPLE_TEAM_ID,
}
: undefined,
},
makers: [
{
name: '@electron-forge/maker-dmg',
config: {
format: 'ULFO',
icon: './resources/icons/icon.icns',
icon: icnsPath,
name: 'AFFiNE',
'icon-size': 128,
background: './resources/icons/dmg-background.png',
contents: [
{
x: 176,
y: 192,
type: 'file',
path: path.resolve(
__dirname,
'out',
buildType,
`${productName}-darwin-${arch}`,
`${productName}.app`
),
},
{ x: 432, y: 192, type: 'link', path: '/Applications' },
],
},
},
{
name: '@electron-forge/maker-zip',
config: {
name: 'affine',
iconUrl: './resources/icons/icon.ico',
setupIcon: './resources/icons/icon.ico',
iconUrl: icoPath,
setupIcon: icoPath,
platforms: ['darwin', 'linux', 'win32'],
},
},
{
name: '@electron-forge/maker-squirrel',
config: {
name: 'AFFiNE',
setupIcon: icoPath,
// loadingGif: './resources/icons/loading.gif',
},
},
],
hooks: {
readPackageJson: async (_, packageJson) => {
// we want different package name for canary build
// so stable and canary will not share the same app data
packageJson.productName = productName;
},
generateAssets: async (_, platform, arch) => {
if (process.env.SKIP_GENERATE_ASSETS) {
return;
}
const { $ } = await import('zx');
// TODO: right now we do not need the following
// it is for octobase-node, but we dont use it for now.
if (platform === 'darwin' && arch === 'arm64') {
// In GitHub Actions runner, MacOS is always x64
// we need to manually set TARGET to aarch64-apple-darwin
process.env.TARGET = 'aarch64-apple-darwin';
}
if (platform === 'win32') {
$.shell = 'powershell.exe';
$.prefix = '';
}
// run yarn generate-assets
await $`yarn generate-assets`;
},

View File

@@ -0,0 +1,3 @@
import log from 'electron-log';
export const logger = log;

View File

@@ -0,0 +1,5 @@
// This file contains the main process events
// It will guide preload and main process on the correct event types and payloads
export interface MainEventMap {
'main:on-db-update': (workspaceId: string) => void;
}

View File

@@ -0,0 +1 @@
tmp

View File

@@ -0,0 +1,226 @@
import assert from 'node:assert';
import path from 'node:path';
import fs from 'fs-extra';
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
import * as Y from 'yjs';
const registeredHandlers = new Map<string, (...args: any[]) => any>();
// common mock dispatcher for ipcMain.handle and app.on
async function dispatch(key: string, ...args: any[]) {
const handler = registeredHandlers.get(key);
assert(handler);
return await handler(null, ...args);
}
const APP_PATH = path.join(__dirname, './tmp');
const browserWindow = {
isDestroyed: () => {
return false;
},
setWindowButtonVisibility: (_v: boolean) => {
// will be stubbed later
},
webContents: {
send: (_type: string, ..._args: any[]) => {
// ...
},
},
};
const ipcMain = {
handle: (key: string, callback: (...args: any[]) => any) => {
registeredHandlers.set(key, callback);
},
};
const nativeTheme = {
themeSource: 'light',
};
function compareBuffer(a: Uint8Array, b: Uint8Array) {
if (a.length !== b.length) {
return false;
}
for (let i = 0; i < a.length; i++) {
if (a[i] !== b[i]) {
return false;
}
}
return true;
}
// dynamically import handlers so that we can inject local variables to mocks
vi.doMock('electron', () => {
return {
app: {
getPath: (name: string) => {
assert(name === 'appData');
return APP_PATH;
},
name: 'affine-test',
on: (name: string, callback: (...args: any[]) => any) => {
registeredHandlers.set(name, callback);
},
},
BrowserWindow: {
getAllWindows: () => {
return [browserWindow];
},
},
nativeTheme: nativeTheme,
ipcMain,
};
});
beforeEach(async () => {
// clean up tmp folder
const { registerHandlers } = await import('../handlers');
registerHandlers();
});
afterEach(async () => {
const { cleanupWorkspaceDBs } = await import('../handlers');
cleanupWorkspaceDBs();
await fs.remove(APP_PATH);
});
describe('ensureWorkspaceDB', () => {
test('should create db file on connection if it does not exist', async () => {
const id = 'test-workspace-id';
const { ensureWorkspaceDB } = await import('../handlers');
const workspaceDB = await ensureWorkspaceDB(id);
const file = workspaceDB.path;
const fileExists = await fs.pathExists(file);
expect(fileExists).toBe(true);
});
});
describe('workspace handlers', () => {
test('list all workspace ids', async () => {
const ids = ['test-workspace-id', 'test-workspace-id-2'];
const { ensureWorkspaceDB } = await import('../handlers');
await Promise.all(ids.map(id => ensureWorkspaceDB(id)));
const list = await dispatch('workspace:list');
expect(list).toEqual(ids);
});
test('delete workspace', async () => {
const ids = ['test-workspace-id', 'test-workspace-id-2'];
const { ensureWorkspaceDB } = await import('../handlers');
await Promise.all(ids.map(id => ensureWorkspaceDB(id)));
await dispatch('workspace:delete', 'test-workspace-id-2');
const list = await dispatch('workspace:list');
expect(list).toEqual(['test-workspace-id']);
});
});
describe('UI handlers', () => {
test('theme-change', async () => {
await dispatch('ui:theme-change', 'dark');
expect(nativeTheme.themeSource).toBe('dark');
await dispatch('ui:theme-change', 'light');
expect(nativeTheme.themeSource).toBe('light');
});
test('sidebar-visibility-change (macOS)', async () => {
vi.stubGlobal('process', { platform: 'darwin' });
const setWindowButtonVisibility = vi.fn();
browserWindow.setWindowButtonVisibility = setWindowButtonVisibility;
await dispatch('ui:sidebar-visibility-change', true);
expect(setWindowButtonVisibility).toBeCalledWith(true);
await dispatch('ui:sidebar-visibility-change', false);
expect(setWindowButtonVisibility).toBeCalledWith(false);
vi.unstubAllGlobals();
});
test('sidebar-visibility-change (non-macOS)', async () => {
vi.stubGlobal('process', { platform: 'linux' });
const setWindowButtonVisibility = vi.fn();
browserWindow.setWindowButtonVisibility = setWindowButtonVisibility;
await dispatch('ui:sidebar-visibility-change', true);
expect(setWindowButtonVisibility).not.toBeCalled();
vi.unstubAllGlobals();
});
});
describe('db handlers', () => {
test('will reconnect on activate', async () => {
const { ensureWorkspaceDB } = await import('../handlers');
const workspaceDB = await ensureWorkspaceDB('test-workspace-id');
const instance = vi.spyOn(workspaceDB, 'reconnectDB');
await dispatch('activate');
expect(instance).toBeCalled();
});
test('apply doc and get doc updates', async () => {
const workspaceId = 'test-workspace-id';
const bin = await dispatch('db:get-doc', workspaceId);
// ? is this a good test?
expect(bin.every((byte: number) => byte === 0)).toBe(true);
const ydoc = new Y.Doc();
const ytext = ydoc.getText('test');
ytext.insert(0, 'hello world');
const bin2 = Y.encodeStateAsUpdate(ydoc);
await dispatch('db:apply-doc-update', workspaceId, bin2);
const bin3 = await dispatch('db:get-doc', workspaceId);
const ydoc2 = new Y.Doc();
Y.applyUpdate(ydoc2, bin3);
const ytext2 = ydoc2.getText('test');
expect(ytext2.toString()).toBe('hello world');
});
test('get non existent doc', async () => {
const workspaceId = 'test-workspace-id';
const bin = await dispatch('db:get-blob', workspaceId, 'non-existent-id');
expect(bin).toBeNull();
});
test('list blobs (empty)', async () => {
const workspaceId = 'test-workspace-id';
const list = await dispatch('db:get-persisted-blobs', workspaceId);
expect(list).toEqual([]);
});
test('CRUD blobs', async () => {
const testBin = new Uint8Array([1, 2, 3, 4, 5]);
const testBin2 = new Uint8Array([6, 7, 8, 9, 10]);
const workspaceId = 'test-workspace-id';
// add blob
await dispatch('db:add-blob', workspaceId, 'testBin', testBin);
// get blob
expect(
compareBuffer(
await dispatch('db:get-blob', workspaceId, 'testBin'),
testBin
)
).toBe(true);
// add another blob
await dispatch('db:add-blob', workspaceId, 'testBin2', testBin2);
expect(
compareBuffer(
await dispatch('db:get-blob', workspaceId, 'testBin2'),
testBin2
)
).toBe(true);
// list blobs
let lists = await dispatch('db:get-persisted-blobs', workspaceId);
expect(lists).toHaveLength(2);
expect(lists).toContain('testBin');
expect(lists).toContain('testBin2');
// delete blob
await dispatch('db:delete-blob', workspaceId, 'testBin');
lists = await dispatch('db:get-persisted-blobs', workspaceId);
expect(lists).toEqual(['testBin2']);
});
});

View File

@@ -1,34 +0,0 @@
import * as os from 'node:os';
import path from 'node:path';
import { Storage } from '@affine/octobase-node';
import { BrowserWindow, ipcMain, nativeTheme } from 'electron';
import fs from 'fs-extra';
const AFFINE_ROOT = path.join(os.homedir(), '.affine');
fs.ensureDirSync(AFFINE_ROOT);
// todo: rethink this
export const appState = {
storage: new Storage(path.join(AFFINE_ROOT, 'test.db')),
};
export const registerHandlers = () => {
ipcMain.handle('octo:workspace-sync', async (_, id) => {
return appState.storage.sync(id, '');
});
ipcMain.handle('ui:theme-change', async (_, theme) => {
nativeTheme.themeSource = theme;
});
ipcMain.handle('ui:sidebar-visibility-change', async (_, visible) => {
// todo
const windows = BrowserWindow.getAllWindows();
windows.forEach(w => {
// hide window buttons when sidebar is not visible
w.setWindowButtonVisibility(visible);
});
});
};

View File

@@ -0,0 +1,9 @@
import { app } from 'electron';
import path from 'path';
export const appContext = {
appName: app.name,
appDataPath: path.join(app.getPath('appData'), app.name),
};
export type AppContext = typeof appContext;

View File

@@ -0,0 +1,34 @@
import fs from 'fs-extra';
import { logger } from '../../../logger';
import type { WorkspaceDatabase } from './sqlite';
/**
* Start a backup of the database to the given destination.
*/
export async function exportDatabase(db: WorkspaceDatabase, dest: string) {
await fs.copyFile(db.path, dest);
logger.log('export: ', dest);
}
// export async function startBackup(db: WorkspaceDatabase, dest: string) {
// let timeout: NodeJS.Timeout | null;
// async function backup() {
// await fs.copyFile(db.path, dest);
// logger.log('backup: ', dest);
// }
// backup();
// const _db = await db.sqliteDB$;
// _db.on('change', () => {
// if (timeout) {
// clearTimeout(timeout);
// }
// timeout = setTimeout(async () => {
// await backup();
// timeout = null;
// }, 1000);
// });
// }

View File

@@ -0,0 +1,7 @@
import type { WatchListener } from 'fs-extra';
import fs from 'fs-extra';
export function watchFile(path: string, callback: WatchListener<string>) {
const watcher = fs.watch(path, callback);
return () => watcher.close();
}

View File

@@ -0,0 +1,174 @@
import path from 'node:path';
import type { Database } from 'better-sqlite3';
import sqlite from 'better-sqlite3';
import fs from 'fs-extra';
import * as Y from 'yjs';
import { logger } from '../../../logger';
import type { AppContext } from '../context';
const schemas = [
`CREATE TABLE IF NOT EXISTS "updates" (
id INTEGER PRIMARY KEY AUTOINCREMENT,
data BLOB NOT NULL,
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL
)`,
`CREATE TABLE IF NOT EXISTS "blobs" (
key TEXT PRIMARY KEY NOT NULL,
data BLOB NOT NULL,
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL
)`,
];
interface UpdateRow {
id: number;
data: Buffer;
timestamp: string;
}
interface BlobRow {
key: string;
data: Buffer;
timestamp: string;
}
export class WorkspaceDatabase {
sqliteDB: Database;
ydoc = new Y.Doc();
firstConnect = false;
constructor(public path: string) {
this.sqliteDB = this.reconnectDB();
}
// release resources
destroy = () => {
this.sqliteDB?.close();
this.ydoc.destroy();
};
reconnectDB = () => {
logger.log('open db', this.path);
if (this.sqliteDB) {
this.sqliteDB.close();
}
// use cached version?
const db = (this.sqliteDB = sqlite(this.path));
db.exec(schemas.join(';'));
if (!this.firstConnect) {
this.ydoc.on('update', this.addUpdateToSQLite);
}
const updates = this.getUpdates();
updates.forEach(update => {
Y.applyUpdate(this.ydoc, update.data);
});
this.firstConnect = true;
return db;
};
getEncodedDocUpdates = () => {
return Y.encodeStateAsUpdate(this.ydoc);
};
// non-blocking and use yDoc to validate the update
// after that, the update is added to the db
applyUpdate = (data: Uint8Array) => {
Y.applyUpdate(this.ydoc, data);
// todo: trim the updates when the number of records is too large
// 1. store the current ydoc state in the db
// 2. then delete the old updates
// yjs-idb will always trim the db for the first time after DB is loaded
};
addBlob = (key: string, data: Uint8Array) => {
try {
const statement = this.sqliteDB.prepare(
'INSERT INTO blobs (key, data) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET data = ?'
);
statement.run(key, data, data);
} catch (error) {
logger.error('addBlob', error);
}
};
getBlob = (key: string) => {
try {
const statement = this.sqliteDB.prepare(
'SELECT data FROM blobs WHERE key = ?'
);
const row = statement.get(key) as BlobRow;
if (!row) {
return null;
}
return row.data;
} catch (error) {
logger.error('getBlob', error);
return null;
}
};
deleteBlob = (key: string) => {
try {
const statement = this.sqliteDB.prepare(
'DELETE FROM blobs WHERE key = ?'
);
statement.run(key);
} catch (error) {
logger.error('deleteBlob', error);
}
};
getPersistentBlobKeys = () => {
try {
const statement = this.sqliteDB.prepare('SELECT key FROM blobs');
const rows = statement.all() as BlobRow[];
return rows.map(row => row.key);
} catch (error) {
logger.error('getPersistentBlobKeys', error);
return [];
}
};
private getUpdates = () => {
try {
const statement = this.sqliteDB.prepare('SELECT * FROM updates');
const rows = statement.all() as UpdateRow[];
return rows;
} catch (error) {
logger.error('getUpdates', error);
return [];
}
};
// batch write instead write per key stroke?
private addUpdateToSQLite = (data: Uint8Array) => {
try {
const start = performance.now();
const statement = this.sqliteDB.prepare(
'INSERT INTO updates (data) VALUES (?)'
);
statement.run(data);
logger.debug('addUpdateToSQLite', performance.now() - start, 'ms');
} catch (error) {
logger.error('addUpdateToSQLite', error);
}
};
}
export async function openWorkspaceDatabase(
context: AppContext,
workspaceId: string
) {
const basePath = path.join(context.appDataPath, 'workspaces', workspaceId);
// hmmm.... blocking api but it should be fine, right?
await fs.ensureDir(basePath);
const dbPath = path.join(basePath, 'storage.db');
return new WorkspaceDatabase(dbPath);
}

View File

@@ -0,0 +1,34 @@
import path from 'node:path';
import fs from 'fs-extra';
import { logger } from '../../../logger';
import type { AppContext } from '../context';
export async function listWorkspaces(context: AppContext) {
const basePath = path.join(context.appDataPath, 'workspaces');
try {
return fs
.readdir(basePath, {
withFileTypes: true,
})
.then(dirs => dirs.filter(dir => dir.isDirectory()).map(dir => dir.name));
} catch (error) {
logger.error('listWorkspaces', error);
return [];
}
}
export async function deleteWorkspace(context: AppContext, id: string) {
const basePath = path.join(context.appDataPath, 'workspaces', id);
const movedPath = path.join(
context.appDataPath,
'delete-workspaces',
`${id}`
);
try {
return fs.move(basePath, movedPath);
} catch (error) {
logger.error('deleteWorkspace', error);
}
}

View File

@@ -0,0 +1,23 @@
const redirectUri = 'https://affine.pro/client/auth-callback';
export const oauthEndpoint = `https://accounts.google.com/o/oauth2/v2/auth?client_id=${process.env.AFFINE_GOOGLE_CLIENT_ID}&redirect_uri=${redirectUri}&response_type=code&scope=openid https://www.googleapis.com/auth/userinfo.email profile&access_type=offline&customParameters={"prompt":"select_account"}`;
const tokenEndpoint = 'https://oauth2.googleapis.com/token';
export const getExchangeTokenParams = (code: string) => {
const postData = {
code,
client_id: process.env.AFFINE_GOOGLE_CLIENT_ID || '',
client_secret: process.env.AFFINE_GOOGLE_CLIENT_SECRET || '',
redirect_uri: redirectUri,
grant_type: 'authorization_code',
};
const requestInit: RequestInit = {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams(postData).toString(),
};
return { requestInit, url: tokenEndpoint };
};

View File

@@ -0,0 +1,227 @@
import {
app,
BrowserWindow,
dialog,
ipcMain,
nativeTheme,
shell,
} from 'electron';
import { parse } from 'url';
import { logger } from '../../logger';
import { isMacOS } from '../../utils';
import { appContext } from './context';
import { exportDatabase } from './data/export';
import { watchFile } from './data/fs-watch';
import type { WorkspaceDatabase } from './data/sqlite';
import { openWorkspaceDatabase } from './data/sqlite';
import { deleteWorkspace, listWorkspaces } from './data/workspace';
import { getExchangeTokenParams, oauthEndpoint } from './google-auth';
import { sendMainEvent } from './send-main-event';
let currentWorkspaceId = '';
const dbMapping = new Map<string, WorkspaceDatabase>();
const dbWatchers = new Map<string, () => void>();
const dBLastUse = new Map<string, number>();
export async function ensureWorkspaceDB(id: string) {
let workspaceDB = dbMapping.get(id);
if (!workspaceDB) {
// hmm... potential race condition?
workspaceDB = await openWorkspaceDatabase(appContext, id);
dbMapping.set(id, workspaceDB);
logger.info('watch db file', workspaceDB.path);
dbWatchers.set(
id,
watchFile(workspaceDB.path, (event, filename) => {
const minTime = 1000;
logger.debug(
'db file changed',
event,
filename,
Date.now() - dBLastUse.get(id)!
);
if (Date.now() - dBLastUse.get(id)! < minTime || !filename) {
logger.debug('skip db update');
return;
}
sendMainEvent('main:on-db-update', id);
// handle DB file update by other process
dbWatchers.get(id)?.();
dbMapping.delete(id);
dbWatchers.delete(id);
ensureWorkspaceDB(id);
})
);
}
dBLastUse.set(id, Date.now());
return workspaceDB;
}
export async function cleanupWorkspaceDBs() {
for (const [id, db] of dbMapping) {
logger.info('close db connection', id);
db.destroy();
dbWatchers.get(id)?.();
}
dbMapping.clear();
dbWatchers.clear();
dBLastUse.clear();
}
function registerWorkspaceHandlers() {
ipcMain.handle('workspace:list', async _ => {
logger.info('list workspaces');
return listWorkspaces(appContext);
});
ipcMain.handle('workspace:delete', async (_, id) => {
logger.info('delete workspace', id);
return deleteWorkspace(appContext, id);
});
}
function registerUIHandlers() {
ipcMain.handle('ui:theme-change', async (_, theme) => {
nativeTheme.themeSource = theme;
logger.info('theme change', theme);
});
ipcMain.handle('ui:sidebar-visibility-change', async (_, visible) => {
// todo
// detect if os is macos
if (isMacOS()) {
const windows = BrowserWindow.getAllWindows();
windows.forEach(w => {
// hide window buttons when sidebar is not visible
w.setWindowButtonVisibility(visible);
});
logger.info('sidebar visibility change', visible);
}
});
ipcMain.handle('ui:workspace-change', async (_, workspaceId) => {
logger.info('workspace change', workspaceId);
currentWorkspaceId = workspaceId;
});
// @deprecated
ipcMain.handle('ui:get-google-oauth-code', async () => {
logger.info('starting google sign in ...');
shell.openExternal(oauthEndpoint);
return new Promise((resolve, reject) => {
const handleOpenUrl = async (_: any, url: string) => {
const mainWindow = BrowserWindow.getAllWindows().find(
w => !w.isDestroyed()
);
const urlObj = parse(url.replace('??', '?'), true);
if (!mainWindow || !url.startsWith('affine://auth-callback')) return;
const code = urlObj.query['code'] as string;
if (!code) return;
logger.info('google sign in code received from callback', code);
app.removeListener('open-url', handleOpenUrl);
resolve(getExchangeTokenParams(code));
};
app.on('open-url', handleOpenUrl);
setTimeout(() => {
reject(new Error('Timed out'));
app.removeListener('open-url', handleOpenUrl);
}, 30000);
});
});
ipcMain.handle('main:env-update', async (_, env, value) => {
process.env[env] = value;
});
}
function registerDBHandlers() {
app.on('activate', () => {
for (const [_, workspaceDB] of dbMapping) {
workspaceDB.reconnectDB();
}
});
ipcMain.handle('db:get-doc', async (_, id) => {
logger.log('main: get doc', id);
const workspaceDB = await ensureWorkspaceDB(id);
return workspaceDB.getEncodedDocUpdates();
});
ipcMain.handle('db:apply-doc-update', async (_, id, update) => {
logger.log('main: apply doc update', id);
const workspaceDB = await ensureWorkspaceDB(id);
return workspaceDB.applyUpdate(update);
});
ipcMain.handle('db:add-blob', async (_, workspaceId, key, data) => {
logger.log('main: add blob', workspaceId, key);
const workspaceDB = await ensureWorkspaceDB(workspaceId);
return workspaceDB.addBlob(key, data);
});
ipcMain.handle('db:get-blob', async (_, workspaceId, key) => {
logger.log('main: get blob', workspaceId, key);
const workspaceDB = await ensureWorkspaceDB(workspaceId);
return workspaceDB.getBlob(key);
});
ipcMain.handle('db:get-persisted-blobs', async (_, workspaceId) => {
logger.log('main: get persisted blob keys', workspaceId);
const workspaceDB = await ensureWorkspaceDB(workspaceId);
return workspaceDB.getPersistentBlobKeys();
});
ipcMain.handle('db:delete-blob', async (_, workspaceId, key) => {
logger.log('main: delete blob', workspaceId, key);
const workspaceDB = await ensureWorkspaceDB(workspaceId);
return workspaceDB.deleteBlob(key);
});
ipcMain.handle('ui:open-db-folder', async _ => {
const workspaceDB = await ensureWorkspaceDB(currentWorkspaceId);
logger.log('main: open db folder', workspaceDB.path);
shell.showItemInFolder(workspaceDB.path);
});
ipcMain.handle('ui:open-load-db-file-dialog', async () => {
// todo
});
ipcMain.handle('ui:open-save-db-file-dialog', async () => {
logger.log('main: open save db file dialog', currentWorkspaceId);
const workspaceDB = await ensureWorkspaceDB(currentWorkspaceId);
const ret = await dialog.showSaveDialog({
properties: ['showOverwriteConfirmation'],
title: 'Save Workspace',
buttonLabel: 'Save',
defaultPath: currentWorkspaceId + '.db',
message: 'Save Workspace as SQLite Database',
});
const filePath = ret.filePath;
if (ret.canceled || !filePath) {
return null;
}
await exportDatabase(workspaceDB, filePath);
shell.showItemInFolder(filePath);
return filePath;
});
}
export const registerHandlers = () => {
registerWorkspaceHandlers();
registerUIHandlers();
registerDBHandlers();
};

View File

@@ -1,26 +1,40 @@
import './security-restrictions';
import { app } from 'electron';
import path from 'path';
import { registerHandlers } from './app-state';
import { logger } from '../../logger';
import { registerHandlers } from './handlers';
import { restoreOrCreateWindow } from './main-window';
import { registerProtocol } from './protocol';
if (require('electron-squirrel-startup')) app.exit();
if (process.defaultApp) {
if (process.argv.length >= 2) {
app.setAsDefaultProtocolClient('affine', process.execPath, [
path.resolve(process.argv[1]),
]);
}
} else {
app.setAsDefaultProtocolClient('affine');
}
/**
* Prevent multiple instances
*/
const isSingleInstance = app.requestSingleInstanceLock();
if (!isSingleInstance) {
logger.info('Another instance is running, exiting...');
app.quit();
process.exit(0);
}
app.on('second-instance', restoreOrCreateWindow);
app.on('second-instance', () => {
restoreOrCreateWindow();
});
/**
* Disable Hardware Acceleration for more power-save
*/
app.disableHardwareAcceleration();
app.on('open-url', async (_, _url) => {
// todo: handle `affine://...` urls
});
/**
* Shout down background process if all windows was closed
@@ -45,7 +59,6 @@ app
.then(registerHandlers)
.then(restoreOrCreateWindow)
.catch(e => console.error('Failed create window:', e));
/**
* Check new app version in production mode only
*/

View File

@@ -2,11 +2,16 @@ import { BrowserWindow, nativeTheme } from 'electron';
import electronWindowState from 'electron-window-state';
import { join } from 'path';
import { logger } from '../../logger';
import { isMacOS } from '../../utils';
const IS_DEV = process.env.NODE_ENV === 'development';
const IS_DEV: boolean =
process.env.NODE_ENV === 'development' && !process.env.CI;
const DEV_TOOL = process.env.DEV_TOOL === 'true';
async function createWindow() {
logger.info('create window');
const mainWindowState = electronWindowState({
defaultWidth: 1000,
defaultHeight: 800,
@@ -14,12 +19,12 @@ async function createWindow() {
const browserWindow = new BrowserWindow({
titleBarStyle: isMacOS() ? 'hiddenInset' : 'default',
trafficLightPosition: { x: 20, y: 18 },
trafficLightPosition: { x: 24, y: 18 },
x: mainWindowState.x,
y: mainWindowState.y,
width: mainWindowState.width,
minWidth: 640,
transparent: true,
transparent: isMacOS(),
visualEffectState: 'active',
vibrancy: 'under-window',
height: mainWindowState.height,
@@ -45,9 +50,16 @@ async function createWindow() {
* @see https://github.com/electron/electron/issues/25012
*/
browserWindow.on('ready-to-show', () => {
browserWindow.show();
if (IS_DEV) {
// do not gain focus in dev mode
browserWindow.showInactive();
} else {
browserWindow.show();
}
logger.info('main window is ready to show');
if (DEV_TOOL) {
browserWindow.webContents.openDevTools();
}
});
@@ -61,13 +73,12 @@ async function createWindow() {
/**
* URL for main window.
*/
const pageUrl =
IS_DEV && process.env.DEV_SERVER_URL !== undefined
? process.env.DEV_SERVER_URL
: 'file://./index.html'; // see protocol.ts
const pageUrl = process.env.DEV_SERVER_URL || 'file://./index.html'; // see protocol.ts
await browserWindow.loadURL(pageUrl);
logger.info('main window is loaded at' + pageUrl);
return browserWindow;
}
@@ -85,8 +96,8 @@ export async function restoreOrCreateWindow() {
if (browserWindow.isMinimized()) {
browserWindow.restore();
logger.info('restore main window');
}
browserWindow.focus();
return browserWindow;
}

View File

@@ -1,23 +1,68 @@
import { protocol } from 'electron';
import { protocol, session } from 'electron';
import { join } from 'path';
export function registerProtocol() {
if (process.env.NODE_ENV === 'production') {
protocol.interceptFileProtocol('file', (request, callback) => {
const url = request.url.replace(/^file:\/\//, '');
const webStaticDir = join(__dirname, '../../../resources/web-static');
if (url.startsWith('./')) {
// if is a file type, load the file in resources
if (url.split('/').at(-1)?.includes('.')) {
const realpath = join(webStaticDir, decodeURIComponent(url));
callback(realpath);
} else {
// else, fallback to load the index.html instead
const realpath = join(webStaticDir, 'index.html');
console.log(realpath, 'realpath', url, 'url');
callback(realpath);
}
}
});
protocol.registerSchemesAsPrivileged([
{
scheme: 'assets',
privileges: {
secure: false,
corsEnabled: true,
supportFetchAPI: true,
standard: true,
bypassCSP: true,
},
},
]);
function toAbsolutePath(url: string) {
let realpath = decodeURIComponent(url);
const webStaticDir = join(__dirname, '../../../resources/web-static');
if (url.startsWith('./')) {
// if is a file type, load the file in resources
if (url.split('/').at(-1)?.includes('.')) {
realpath = join(webStaticDir, decodeURIComponent(url));
} else {
// else, fallback to load the index.html instead
realpath = join(webStaticDir, 'index.html');
}
}
return realpath;
}
export function registerProtocol() {
protocol.interceptFileProtocol('file', (request, callback) => {
const url = request.url.replace(/^file:\/\//, '');
const realpath = toAbsolutePath(url);
// console.log('realpath', realpath, 'for', url);
callback(realpath);
return true;
});
protocol.registerFileProtocol('assets', (request, callback) => {
const url = request.url.replace(/^assets:\/\//, '');
const realpath = toAbsolutePath(url);
// console.log('realpath', realpath, 'for', url);
callback(realpath);
return true;
});
session.defaultSession.webRequest.onHeadersReceived(
(responseDetails, callback) => {
const { responseHeaders } = responseDetails;
if (responseHeaders) {
delete responseHeaders['access-control-allow-origin'];
delete responseHeaders['access-control-allow-methods'];
responseHeaders['Access-Control-Allow-Origin'] = ['*'];
responseHeaders['Access-Control-Allow-Methods'] = [
'GET',
'POST',
'PUT',
'DELETE',
'OPTIONS',
];
}
callback({ responseHeaders });
}
);
}

View File

@@ -10,6 +10,14 @@ app.on('web-contents-created', (_, contents) => {
* @see https://www.electronjs.org/docs/latest/tutorial/security#13-disable-or-limit-navigation
*/
contents.on('will-navigate', (event, url) => {
if (
(process.env.DEV_SERVER_URL &&
url.startsWith(process.env.DEV_SERVER_URL)) ||
url.startsWith('affine://') ||
url.startsWith('file://.')
) {
return;
}
// Prevent navigation
event.preventDefault();
shell.openExternal(url).catch(console.error);

View File

@@ -0,0 +1,14 @@
import { BrowserWindow } from 'electron';
import type { MainEventMap } from '../../main-events';
function getActiveWindows() {
return BrowserWindow.getAllWindows().filter(win => !win.isDestroyed());
}
export function sendMainEvent<T extends keyof MainEventMap>(
type: T,
...args: Parameters<MainEventMap[T]>
) {
getActiveWindows().forEach(win => win.webContents.send(type, ...args));
}

View File

@@ -1,12 +1,6 @@
/* eslint-disable @typescript-eslint/consistent-type-imports */
interface Window {
/**
* After analyzing the `exposeInMainWorld` calls,
* `packages/preload/exposedInMainWorld.d.ts` file will be generated.
* It contains all interfaces.
* `packages/preload/exposedInMainWorld.d.ts` file is required for TS is `renderer`
*
* @see https://github.com/cawa-93/dts-for-context-bridge
*/
readonly apis: { workspaceSync: (id: string) => Promise<any>; onThemeChange: (theme: string) => Promise<any>; onSidebarVisibilityChange: (visible: boolean) => Promise<any>; };
readonly appInfo: { electron: boolean; isMacOS: boolean; };
apis: typeof import('./src/affine-apis').apis;
appInfo: typeof import('./src/affine-apis').appInfo;
}

View File

@@ -0,0 +1,81 @@
// NOTE: we will generate preload types from this file
import { ipcRenderer } from 'electron';
import type { MainEventMap } from '../../main-events';
// main -> renderer
function onMainEvent<T extends keyof MainEventMap>(
eventName: T,
callback: MainEventMap[T]
): () => void {
// @ts-expect-error fix me later
const fn = (_, ...args) => callback(...args);
ipcRenderer.on(eventName, fn);
return () => ipcRenderer.off(eventName, fn);
}
const apis = {
db: {
// workspace providers
getDoc: (id: string): Promise<Uint8Array | null> =>
ipcRenderer.invoke('db:get-doc', id),
applyDocUpdate: (id: string, update: Uint8Array) =>
ipcRenderer.invoke('db:apply-doc-update', id, update),
addBlob: (workspaceId: string, key: string, data: Uint8Array) =>
ipcRenderer.invoke('db:add-blob', workspaceId, key, data),
getBlob: (workspaceId: string, key: string): Promise<Uint8Array | null> =>
ipcRenderer.invoke('db:get-blob', workspaceId, key),
deleteBlob: (workspaceId: string, key: string) =>
ipcRenderer.invoke('db:delete-blob', workspaceId, key),
getPersistedBlobs: (workspaceId: string): Promise<string[]> =>
ipcRenderer.invoke('db:get-persisted-blobs', workspaceId),
// listeners
onDBUpdate: (callback: (workspaceId: string) => void) => {
return onMainEvent('main:on-db-update', callback);
},
},
workspace: {
list: (): Promise<string[]> => ipcRenderer.invoke('workspace:list'),
delete: (id: string): Promise<void> =>
ipcRenderer.invoke('workspace:delete', id),
// create will be implicitly called by db functions
},
openLoadDBFileDialog: () => ipcRenderer.invoke('ui:open-load-db-file-dialog'),
openSaveDBFileDialog: () => ipcRenderer.invoke('ui:open-save-db-file-dialog'),
// ui
onThemeChange: (theme: string) =>
ipcRenderer.invoke('ui:theme-change', theme),
onSidebarVisibilityChange: (visible: boolean) =>
ipcRenderer.invoke('ui:sidebar-visibility-change', visible),
onWorkspaceChange: (workspaceId: string) =>
ipcRenderer.invoke('ui:workspace-change', workspaceId),
openDBFolder: () => ipcRenderer.invoke('ui:open-db-folder'),
/**
* Try sign in using Google and return a request object to exchange the code for a token
* Not exchange in Node side because it is easier to do it in the renderer with VPN
*/
getGoogleOauthCode: (): Promise<{ requestInit: RequestInit; url: string }> =>
ipcRenderer.invoke('ui:get-google-oauth-code'),
/**
* Secret backdoor to update environment variables in main process
*/
updateEnv: (env: string, value: string) => {
ipcRenderer.invoke('main:env-update', env, value);
},
};
const appInfo = {
electron: true,
};
export { apis, appInfo };

View File

@@ -2,9 +2,9 @@
* @module preload
*/
import { contextBridge, ipcRenderer } from 'electron';
import { contextBridge } from 'electron';
import { isMacOS } from '../../utils';
import * as affineApis from './affine-apis';
/**
* The "Main World" is the JavaScript context that your main renderer code runs in.
@@ -13,26 +13,5 @@ import { isMacOS } from '../../utils';
* @see https://www.electronjs.org/docs/api/context-bridge
*/
/**
* After analyzing the `exposeInMainWorld` calls,
* `packages/preload/exposedInMainWorld.d.ts` file will be generated.
* It contains all interfaces.
* `packages/preload/exposedInMainWorld.d.ts` file is required for TS is `renderer`
*
* @see https://github.com/cawa-93/dts-for-context-bridge
*/
contextBridge.exposeInMainWorld('apis', {
workspaceSync: (id: string) => ipcRenderer.invoke('octo:workspace-sync', id),
// ui
onThemeChange: (theme: string) =>
ipcRenderer.invoke('ui:theme-change', theme),
onSidebarVisibilityChange: (visible: boolean) =>
ipcRenderer.invoke('ui:sidebar-visibility-change', visible),
});
contextBridge.exposeInMainWorld('appInfo', {
electron: true,
isMacOS: isMacOS(),
});
contextBridge.exposeInMainWorld('apis', affineApis.apis);
contextBridge.exposeInMainWorld('appInfo', affineApis.appInfo);

View File

@@ -1,50 +1,74 @@
{
"name": "@affine/electron",
"productName": "AFFiNE",
"private": true,
"version": "0.0.0",
"version": "0.5.4-canary.25",
"author": "affine",
"description": "AFFiNE App",
"homepage": "https://github.com/toeverything/AFFiNE",
"workspaces": [
"../../packages/*",
"../../tests/fixtures"
],
"scripts": {
"dev": "cross-env NODE_ENV=development node scripts/dev.mjs",
"prod": "cross-env NODE_ENV=production node scripts/dev.mjs",
"dev": "yarn electron-rebuild && yarn cross-env DEV_SERVER_URL=http://localhost:8080 node scripts/dev.mjs",
"prod": "yarn electron-rebuild && yarn node scripts/dev.mjs",
"generate-assets": "zx scripts/generate-assets.mjs",
"package": "electron-forge package",
"make": "electron-forge make",
"make-macos-arm64": "electron-forge make --platform=darwin --arch=arm64",
"make-macos-x64": "electron-forge make --platform=darwin --arch=x64",
"make-windows-x64": "electron-forge make --platform=win32 --arch=x64",
"build:octobase-node": "yarn workspace @affine/octobase-node build",
"postinstall": "ELECTRON_RUN_AS_NODE=1 electron scripts/update-electron-vendors.mjs"
"make-linux-x64": "electron-forge make --platform=linux --arch=x64",
"rebuild:for-test": "yarn rebuild better-sqlite3",
"rebuild:for-electron": "yarn electron-rebuild",
"test": "playwright test"
},
"config": {
"forge": "./forge.config.js"
},
"main": "./dist/layers/main/index.js",
"devDependencies": {
"@affine/octobase-node": "workspace:*",
"@electron-forge/cli": "^6.1.0",
"@electron-forge/core": "^6.1.0",
"@electron-forge/core-utils": "^6.1.0",
"@electron-forge/maker-deb": "^6.1.0",
"@electron-forge/maker-dmg": "^6.1.0",
"@electron-forge/maker-squirrel": "^6.1.0",
"@electron-forge/maker-zip": "^6.1.0",
"@electron-forge/shared-types": "^6.1.0",
"@electron/rebuild": "^3.2.10",
"dts-for-context-bridge": "^0.7.1",
"electron": "24.0.0",
"esbuild": "^0.17.15",
"zx": "^7.2.1"
"@affine-test/kit": "workspace:*",
"@electron-forge/cli": "^6.1.1",
"@electron-forge/core": "^6.1.1",
"@electron-forge/core-utils": "^6.1.1",
"@electron-forge/maker-deb": "^6.1.1",
"@electron-forge/maker-dmg": "^6.1.1",
"@electron-forge/maker-squirrel": "^6.1.1",
"@electron-forge/maker-zip": "^6.1.1",
"@electron-forge/shared-types": "^6.1.1",
"@electron/rebuild": "^3.2.13",
"@electron/remote": "2.0.9",
"@types/better-sqlite3": "^7.6.4",
"@types/fs-extra": "^11.0.1",
"cross-env": "7.0.3",
"electron": "24.2.0",
"electron-log": "^5.0.0-beta.23",
"electron-squirrel-startup": "1.0.0",
"electron-window-state": "^5.0.3",
"esbuild": "^0.17.18",
"fs-extra": "^11.1.1",
"playwright": "^1.33.0",
"ts-node": "^10.9.1",
"undici": "^5.22.0",
"zx": "^7.2.2"
},
"dependencies": {
"cross-env": "7.0.3",
"electron-window-state": "^5.0.3",
"fs-extra": "^11.1.1"
"better-sqlite3": "^8.3.0",
"yjs": "^13.6.1"
},
"packageManager": "yarn@3.5.0"
"build": {
"protocols": [
{
"name": "affine",
"schemes": [
"affine"
]
}
]
},
"stableVersion": "0.5.3",
"installConfig": {
"hoistingLimits": "workspaces"
},
"peerDependencies": {
"playwright": "*",
"ts-node": "*"
}
}

View File

@@ -0,0 +1,27 @@
import type { PlaywrightTestConfig } from '@playwright/test';
// import { devices } from '@playwright/test';
/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
// require('dotenv').config();
/**
* See https://playwright.dev/docs/test-configuration.
*/
const config: PlaywrightTestConfig = {
testDir: './tests',
fullyParallel: true,
timeout: process.env.CI ? 50_000 : 30_000,
use: {
viewport: { width: 1440, height: 800 },
},
};
if (process.env.CI) {
config.retries = 3;
config.workers = '50%';
}
export default config;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 947 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 342 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 85 KiB

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

View File

@@ -0,0 +1,17 @@
#!/usr/bin/env ts-node-esm
import * as esbuild from 'esbuild';
import { config } from './common.mjs';
const common = config();
await esbuild.build(common.preload);
await esbuild.build({
...common.main,
define: {
...common.main.define,
'process.env.NODE_ENV': `"production"`,
},
});
console.log('Compiled successfully.');

View File

@@ -1,14 +1,9 @@
import fs from 'node:fs';
import path from 'node:path';
import * as url from 'node:url';
const __dirname = url.fileURLToPath(new URL('.', import.meta.url));
// const __dirname = new URL('.', import.meta.url).pathname;
const { node } = JSON.parse(
fs.readFileSync(
path.join(__dirname, '../electron-vendors.autogen.json'),
'utf-8'
)
);
import { resolve } from 'node:path';
import { fileURLToPath } from 'url';
export const root = fileURLToPath(new URL('..', import.meta.url));
export const NODE_MAJOR_VERSION = 18;
const nativeNodeModulesPlugin = {
name: 'native-node-modules',
@@ -20,22 +15,36 @@ const nativeNodeModulesPlugin = {
},
};
/** @type {import('esbuild').BuildOptions} */
export const mainConfig = {
entryPoints: ['layers/main/src/index.ts'],
outdir: 'dist/layers/main',
bundle: true,
target: `node${node}`,
platform: 'node',
external: ['electron'],
plugins: [nativeNodeModulesPlugin],
};
// List of env that will be replaced by esbuild
const ENV_MACROS = ['AFFINE_GOOGLE_CLIENT_ID', 'AFFINE_GOOGLE_CLIENT_SECRET'];
export const preloadConfig = {
entryPoints: ['layers/preload/src/index.ts'],
outdir: 'dist/layers/preload',
bundle: true,
target: `node${node}`,
platform: 'node',
external: ['electron'],
/** @return {{main: import('esbuild').BuildOptions, preload: import('esbuild').BuildOptions}} */
export const config = () => {
const define = Object.fromEntries(
ENV_MACROS.map(key => [
'process.env.' + key,
JSON.stringify(process.env[key] ?? ''),
])
);
return {
main: {
entryPoints: [resolve(root, './layers/main/src/index.ts')],
outdir: resolve(root, './dist/layers/main'),
bundle: true,
target: `node${NODE_MAJOR_VERSION}`,
platform: 'node',
external: ['electron', 'yjs', 'better-sqlite3'],
plugins: [nativeNodeModulesPlugin],
define: define,
},
preload: {
entryPoints: [resolve(root, './layers/preload/src/index.ts')],
outdir: resolve(root, './dist/layers/preload'),
bundle: true,
target: `node${NODE_MAJOR_VERSION}`,
platform: 'node',
external: ['electron'],
define: define,
},
};
};

View File

@@ -1,10 +1,11 @@
import { spawn } from 'node:child_process';
import { readFileSync } from 'node:fs';
import path from 'node:path';
import { generateAsync } from 'dts-for-context-bridge';
import electronPath from 'electron';
import * as esbuild from 'esbuild';
import { mainConfig, preloadConfig } from './common.mjs';
import { config, root } from './common.mjs';
/** @type 'production' | 'development'' */
const mode = (process.env.NODE_ENV = process.env.NODE_ENV || 'development');
@@ -17,9 +18,20 @@ const stderrFilterPatterns = [
/ExtensionLoadWarning/,
];
// these are set before calling `config`, so we have a chance to override them
try {
const devJson = readFileSync(path.resolve(root, './dev.json'), 'utf-8');
const devEnv = JSON.parse(devJson);
Object.assign(process.env, devEnv);
} catch (err) {
console.warn(
`Could not read dev.json. Some functions may not work as expected.`
);
}
// hard-coded for now:
// fixme(xp): report error if app is not running on port 8080
process.env.DEV_SERVER_URL = `http://localhost:8080`;
// fixme(xp): report error if app is not running on DEV_SERVER_URL
const DEV_SERVER_URL = process.env.DEV_SERVER_URL;
/** @type {ChildProcessWithoutNullStreams | null} */
let spawnProcess = null;
@@ -33,37 +45,37 @@ function spawnOrReloadElectron() {
spawnProcess = spawn(String(electronPath), ['.']);
spawnProcess.stdout.on(
'data',
d => d.toString().trim() && console.warn(d.toString(), { timestamp: true })
);
spawnProcess.stdout.on('data', d => {
let str = d.toString().trim();
if (str) {
console.log(str);
}
});
spawnProcess.stderr.on('data', d => {
const data = d.toString().trim();
if (!data) return;
const mayIgnore = stderrFilterPatterns.some(r => r.test(data));
if (mayIgnore) return;
console.error(data, { timestamp: true });
console.error(data);
});
// Stops the watch script when the application has been quit
spawnProcess.on('exit', process.exit);
}
const common = config();
async function main() {
async function watchPreload(onInitialBuild) {
const preloadBuild = await esbuild.context({
...preloadConfig,
...common.preload,
plugins: [
...(preloadConfig.plugins ?? []),
...(common.preload.plugins ?? []),
{
name: 'affine-dev:reload-app-on-preload-change',
setup(build) {
let initialBuild = false;
build.onEnd(() => {
generateAsync({
input: 'layers/preload/src/**/*.ts',
output: 'layers/preload/preload.d.ts',
});
if (initialBuild) {
console.log(`[preload] has changed`);
spawnOrReloadElectron();
@@ -80,14 +92,20 @@ async function main() {
}
async function watchMain() {
const define = {
...common.main.define,
'process.env.NODE_ENV': `"${mode}"`,
};
if (DEV_SERVER_URL) {
define['process.env.DEV_SERVER_URL'] = `"${DEV_SERVER_URL}"`;
}
const mainBuild = await esbuild.context({
...mainConfig,
define: {
'process.env.NODE_ENV': `"${mode}"`,
'process.env.DEV_SERVER_URL': `"${process.env.DEV_SERVER_URL}"`,
},
...common.main,
define: define,
plugins: [
...(mainConfig.plugins ?? []),
...(common.main.plugins ?? []),
{
name: 'affine-dev:reload-app-on-main-change',
setup(build) {

View File

@@ -5,7 +5,7 @@ import path from 'node:path';
import * as esbuild from 'esbuild';
import { mainConfig, preloadConfig } from './common.mjs';
import { config } from './common.mjs';
const repoRootDir = path.join(__dirname, '..', '..', '..');
const electronRootDir = path.join(__dirname, '..');
@@ -29,46 +29,61 @@ console.log('build with following dir', {
await cleanup();
echo('Clean up done');
// step 1: build web (nextjs) dist
cd(repoRootDir);
await $`yarn add`;
await $`yarn build`;
await $`yarn export`;
await fs.move(affineWebOutDir, publicAffineOutDir, { overwrite: true });
if (process.platform === 'win32') {
$.shell = 'powershell.exe';
$.prefix = '';
}
// step 2: build electron resources
cd(repoRootDir);
// step 1: build electron resources
await buildLayers();
echo('Build layers done');
// step 3: build octobase-node
let buildOctobaseNode = 'yarn workspace @affine/octobase-node build';
if (process.env.TARGET) {
buildOctobaseNode += ` --target=${process.env.TARGET}`;
}
await $([buildOctobaseNode]);
// step 2: build web (nextjs) dist
if (!process.env.SKIP_WEB_BUILD) {
process.env.ENABLE_LEGACY_PROVIDER = 'false';
await $`yarn build`;
await $`yarn export`;
// step 4: copy octobase-node to electron dist
await fs.ensureDir('./apps/electron/dist/layers/main/');
await $`cp ./packages/octobase-node/octobase.*.node ./apps/electron/dist/layers/main/`;
// step 1.5: amend sourceMappingURL to allow debugging in devtools
await glob('**/*.{js,css}', { cwd: affineWebOutDir }).then(files => {
return files.map(async file => {
const dir = path.dirname(file);
const fullpath = path.join(affineWebOutDir, file);
let content = await fs.readFile(fullpath, 'utf-8');
// replace # sourceMappingURL=76-6370cd185962bc89.js.map
// to # sourceMappingURL=assets://./{dir}/76-6370cd185962bc89.js.map
content = content.replace(/# sourceMappingURL=(.*)\.map/g, (_, p1) => {
return `# sourceMappingURL=assets://./${dir}/${p1}.map`;
});
await fs.writeFile(fullpath, content);
});
});
await fs.move(affineWebOutDir, publicAffineOutDir, { overwrite: true });
}
/// --------
/// --------
/// --------
async function cleanup() {
await fs.emptyDir(publicAffineOutDir);
if (!process.env.SKIP_WEB_BUILD) {
await fs.emptyDir(publicAffineOutDir);
}
await fs.emptyDir(path.join(electronRootDir, 'layers', 'main', 'dist'));
await fs.emptyDir(path.join(electronRootDir, 'layers', 'preload', 'dist'));
await fs.remove(path.join(electronRootDir, 'out'));
}
async function buildLayers() {
await esbuild.build({
...preloadConfig,
});
const common = config();
await esbuild.build(common.preload);
await esbuild.build({
...mainConfig,
...common.main,
define: {
...common.main.define,
'process.env.NODE_ENV': `"production"`,
},
});

View File

@@ -0,0 +1,63 @@
// do not run in your local machine
/* eslint-disable */
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
/* eslint-enable */
const yml = {
version: process.env.RELEASE_VERSION ?? '0.0.0',
files: [],
};
let fileList = [];
// TODO: maybe add `beta` and `stable`
const BUILD_TYPE = process.env.BUILD_TYPE || 'canary';
const generateYml = async () => {
fileList = [
`affine-${BUILD_TYPE}-macos-arm64.dmg`,
`affine-${BUILD_TYPE}-macos-arm64.zip`,
`affine-${BUILD_TYPE}-macos-x64.zip`,
`affine-${BUILD_TYPE}-macos-x64.dmg`,
];
fileList.forEach(fileName => {
const filePath = path.join(__dirname, './', fileName);
try {
const fileData = fs.readFileSync(filePath);
const hash = crypto
.createHash('sha512')
.update(fileData)
.digest('base64');
const size = fs.statSync(filePath).size;
yml.files.push({
url: fileName,
sha512: hash,
size: size,
});
} catch (e) {}
});
yml.path = yml.files[0].url;
yml.sha512 = yml.files[0].sha512;
yml.releaseDate = new Date().toISOString();
const ymlStr =
`version: ${yml.version}\n` +
`files:\n` +
yml.files
.map(file => {
return (
` - url: ${file.url}\n` +
` sha512: ${file.sha512}\n` +
` size: ${file.size}\n`
);
})
.join('') +
`path: ${yml.path}\n` +
`sha512: ${yml.sha512}\n` +
`releaseDate: ${yml.releaseDate}\n`;
fs.writeFileSync(`./latest-mac.yml`, ymlStr);
};
generateYml();

View File

@@ -1,17 +0,0 @@
/**
* This script should be run in electron context
* @example
* ELECTRON_RUN_AS_NODE=1 electron scripts/update-electron-vendors.mjs
*/
import { writeFileSync } from 'fs';
const electronRelease = process.versions;
const node = electronRelease.node.split('.')[0];
const chrome = electronRelease.v8.split('.').splice(0, 2).join('');
writeFileSync(
'./electron-vendors.autogen.json',
JSON.stringify({ chrome, node })
);

View File

@@ -0,0 +1,99 @@
import { resolve } from 'node:path';
import { test, testResultDir } from '@affine-test/kit/playwright';
import type { Page } from '@playwright/test';
import { expect } from '@playwright/test';
import type { ElectronApplication } from 'playwright';
import { _electron as electron } from 'playwright';
let electronApp: ElectronApplication;
let page: Page;
test.beforeEach(async () => {
electronApp = await electron.launch({
args: [resolve(__dirname, '..')],
executablePath: resolve(__dirname, '../node_modules/.bin/electron'),
colorScheme: 'light',
});
page = await electronApp.firstWindow();
await page.getByTestId('onboarding-modal-close-button').click({
delay: 100,
});
// cleanup page data
await page.evaluate(() => localStorage.clear());
});
test.afterEach(async () => {
// cleanup page data
await page.evaluate(() => localStorage.clear());
await page.close();
await electronApp.close();
});
test('new page', async () => {
await page.getByTestId('new-page-button').click({
delay: 100,
});
await page.waitForSelector('v-line');
const flavour = await page.evaluate(
// @ts-expect-error
() => globalThis.currentWorkspace.flavour
);
expect(flavour).toBe('local');
});
test('app theme', async () => {
await page.waitForSelector('v-line');
const root = page.locator('html');
{
const themeMode = await root.evaluate(element =>
element.getAttribute('data-theme')
);
expect(themeMode).toBe('light');
}
await page.screenshot({
path: resolve(testResultDir, 'affine-light-theme-electron.png'),
});
await page.getByTestId('editor-option-menu').click();
await page.getByTestId('change-theme-dark').click();
await page.waitForTimeout(50);
{
const themeMode = await root.evaluate(element =>
element.getAttribute('data-theme')
);
expect(themeMode).toBe('dark');
}
await page.screenshot({
path: resolve(testResultDir, 'affine-dark-theme-electron.png'),
});
});
test('affine cloud disabled', async () => {
await page.getByTestId('new-page-button').click({
delay: 100,
});
await page.waitForSelector('v-line');
await page.getByTestId('current-workspace').click();
await page.getByTestId('sign-in-button').click();
await page.getByTestId('disable-affine-cloud-modal').waitFor({
state: 'visible',
});
});
test('affine onboarding button', async () => {
await page.getByTestId('help-island').click();
await page.getByTestId('easy-guide').click();
const onboardingModal = page.locator('[data-testid=onboarding-modal]');
expect(await onboardingModal.isVisible()).toEqual(true);
const switchVideo = page.locator(
'[data-testid=onboarding-modal-switch-video]'
);
expect(await switchVideo.isVisible()).toEqual(true);
await page.getByTestId('onboarding-modal-next-button').click();
const editingVideo = page.locator(
'[data-testid=onboarding-modal-editing-video]'
);
expect(await editingVideo.isVisible()).toEqual(true);
await page.getByTestId('onboarding-modal-ok-button').click();
expect(await onboardingModal.isVisible()).toEqual(false);
});

View File

@@ -0,0 +1,8 @@
{
"extends": "../../../tsconfig.json",
"compilerOptions": {
"baseUrl": ".",
"noEmit": true
},
"include": ["**.spec.ts", "**.test.ts"]
}

View File

@@ -0,0 +1,27 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"composite": true,
"target": "ESNext",
"module": "ESNext",
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"moduleResolution": "bundler",
"isolatedModules": false,
"resolveJsonModule": true,
"types": ["node"],
"outDir": "dist",
"noEmit": false
},
"include": ["layers", "types", "package.json"],
"exclude": ["out", "dist", "node_modules"],
"references": [
{
"path": "./tsconfig.node.json"
}
],
"ts-node": {
"esm": true,
"experimentalSpecifierResolution": "node"
}
}

View File

@@ -0,0 +1,11 @@
{
"compilerOptions": {
"composite": true,
"target": "ESNext",
"module": "ESNext",
"resolveJsonModule": true,
"moduleResolution": "Node",
"allowSyntheticDefaultImports": true
},
"include": ["./scripts", "package.json"]
}

File diff suppressed because it is too large Load Diff

2
apps/server/.env.example Normal file
View File

@@ -0,0 +1,2 @@
SECRET_KEY="secret"
DATABASE_URL="postgresql://affine@localhost:5432/affine"

2
apps/server/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
.env
src/schema.gql

View File

@@ -0,0 +1,59 @@
-- CreateTable
CREATE TABLE "users" (
"id" VARCHAR NOT NULL,
"name" VARCHAR NOT NULL,
"email" VARCHAR NOT NULL,
"token_nonce" SMALLINT NOT NULL DEFAULT 0,
"avatar_url" VARCHAR,
"password" VARCHAR,
"fulfilled" BOOLEAN NOT NULL DEFAULT true,
"created_at" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "users_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "workspaces" (
"id" VARCHAR NOT NULL,
"public" BOOLEAN NOT NULL,
"created_at" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "workspaces_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "connected_accounts" (
"id" VARCHAR NOT NULL,
"user_id" TEXT NOT NULL,
"provider" VARCHAR NOT NULL,
"provider_user_id" VARCHAR NOT NULL,
CONSTRAINT "connected_accounts_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "user_workspace_permissions" (
"id" VARCHAR NOT NULL,
"workspace_id" VARCHAR NOT NULL,
"entity_id" VARCHAR NOT NULL,
"type" SMALLINT NOT NULL,
"accepted" BOOLEAN NOT NULL DEFAULT false,
"created_at" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "user_workspace_permissions_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "users_email_key" ON "users"("email");
-- CreateIndex
CREATE UNIQUE INDEX "connected_accounts_provider_user_id_key" ON "connected_accounts"("provider_user_id");
-- AddForeignKey
ALTER TABLE "connected_accounts" ADD CONSTRAINT "connected_accounts_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "user_workspace_permissions" ADD CONSTRAINT "user_workspace_permissions_workspace_id_fkey" FOREIGN KEY ("workspace_id") REFERENCES "workspaces"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "user_workspace_permissions" ADD CONSTRAINT "user_workspace_permissions_entity_id_fkey" FOREIGN KEY ("entity_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@@ -0,0 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (i.e. Git)
provider = "postgresql"

86
apps/server/package.json Normal file
View File

@@ -0,0 +1,86 @@
{
"name": "@affine/server",
"private": true,
"version": "0.5.4-canary.25",
"description": "Affine Node.js server",
"type": "module",
"bin": {
"run-test": "./scripts/run-test.ts"
},
"scripts": {
"dev": "nodemon ./src/index.ts",
"test": "yarn exec ts-node-esm ./scripts/run-test.ts all",
"test:coverage": "c8 yarn exec ts-node-esm ./scripts/run-test.ts all",
"postinstall": "prisma generate"
},
"dependencies": {
"@apollo/server": "^4.7.1",
"@nestjs/apollo": "^11.0.5",
"@nestjs/common": "^9.4.0",
"@nestjs/core": "^9.4.0",
"@nestjs/graphql": "^11.0.5",
"@nestjs/platform-express": "^9.4.0",
"@prisma/client": "^4.13.0",
"bcrypt": "^5.1.0",
"dotenv": "^16.0.3",
"express": "^4.18.2",
"graphql": "^16.6.0",
"graphql-type-json": "^0.3.2",
"jsonwebtoken": "^9.0.0",
"lodash-es": "^4.17.21",
"prisma": "^4.13.0",
"reflect-metadata": "^0.1.13",
"rxjs": "^7.8.1"
},
"devDependencies": {
"@nestjs/testing": "^9.4.0",
"@types/bcrypt": "^5.0.0",
"@types/express": "^4.17.17",
"@types/jsonwebtoken": "^9.0.2",
"@types/lodash-es": "^4.17.7",
"@types/node": "^18.16.5",
"@types/supertest": "^2.0.12",
"c8": "^7.13.0",
"nodemon": "^2.0.22",
"supertest": "^6.3.3",
"ts-node": "^10.9.1",
"typescript": "^5.0.4",
"vitest": "^0.31.0"
},
"nodemonConfig": {
"exec": "node",
"script": "./src/index.ts",
"nodeArgs": [
"--loader",
"ts-node/esm.mjs",
"--es-module-specifier-resolution",
"node"
],
"ignore": [
"**/__tests__/**",
"**/dist/**"
],
"env": {
"TS_NODE_TRANSPILE_ONLY": true,
"TS_NODE_PROJECT": "./tsconfig.json",
"NODE_ENV": "development",
"DEBUG": "affine:*",
"FORCE_COLOR": true,
"DEBUG_COLORS": true
},
"delay": 1000
},
"c8": {
"reporter": [
"text",
"lcov"
],
"report-dir": ".coverage",
"exclude": [
"scripts",
"node_modules",
"**/*.spec.ts"
]
},
"stableVersion": "0.5.3"
}

63
apps/server/schema.prisma Normal file
View File

@@ -0,0 +1,63 @@
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
generator client {
provider = "prisma-client-js"
}
model User {
id String @id @default(uuid()) @db.VarChar
name String @db.VarChar
email String @unique @db.VarChar
tokenNonce Int @default(0) @map("token_nonce") @db.SmallInt
avatarUrl String? @map("avatar_url") @db.VarChar
/// Available if user signed up through OAuth providers
password String? @db.VarChar
/// User may created by email collobration invitation before signup.
/// We will precreate a user entity in such senarios but leave fulfilled as false until they signed up
/// This implementation is convenient for handing unregistered user permissoin
fulfilled Boolean @default(true)
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
connectedAccounts ConnectedAccount[]
workspaces UserWorkspacePermission[]
@@map("users")
}
model Workspace {
id String @id @default(uuid()) @db.VarChar
public Boolean
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
users UserWorkspacePermission[]
@@map("workspaces")
}
model ConnectedAccount {
id String @id @default(uuid()) @db.VarChar
userId String @map("user_id")
/// the general provider name, e.g. google, github, facebook
provider String @db.VarChar
/// the user id provided by OAuth providers, or other user identitive credential like `username` provided by GitHub
providerUserId String @unique @map("provider_user_id") @db.VarChar
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@map("connected_accounts")
}
model UserWorkspacePermission {
id String @id @default(uuid()) @db.VarChar
workspaceId String @map("workspace_id") @db.VarChar
userId String @map("entity_id") @db.VarChar
/// Read/Write/Admin/Owner
type Int @db.SmallInt
/// Whether the permission invitation is accepted by the user
accepted Boolean @default(false)
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@map("user_workspace_permissions")
}

View File

@@ -0,0 +1,19 @@
import crypto from 'node:crypto';
import { genSalt } from 'bcrypt';
const { privateKey, publicKey } = crypto.generateKeyPairSync('ec', {
namedCurve: 'prime256v1',
publicKeyEncoding: {
type: 'spki',
format: 'pem',
},
privateKeyEncoding: {
type: 'pkcs8',
format: 'pem',
},
});
console.log('Salt:\n', await genSalt(10));
console.log('ECDSA Public Key:\n', publicKey);
console.log('ECDSA Private Key:\n', privateKey);

View File

@@ -0,0 +1,19 @@
import userA from '@affine-test/fixtures/userA.json' assert { type: 'json' };
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
async function main() {
await prisma.user.create({
data: userA,
});
}
main()
.then(async () => {
await prisma.$disconnect();
})
.catch(async e => {
console.error(e);
await prisma.$disconnect();
process.exit(1);
});

65
apps/server/scripts/run-test.ts Executable file
View File

@@ -0,0 +1,65 @@
#!/usr/bin/env ts-node-esm
import { resolve } from 'node:path';
import * as p from '@clack/prompts';
import { spawn } from 'child_process';
import { readdir } from 'fs/promises';
import * as process from 'process';
import { fileURLToPath } from 'url';
import pkg from '../package.json' assert { type: 'json' };
const root = fileURLToPath(new URL('..', import.meta.url));
const testDir = resolve(root, 'src', 'tests');
const files = await readdir(testDir);
const sharedArgs = [...pkg.nodemonConfig.nodeArgs, '--test'];
const env = {
PATH: process.env.PATH,
NODE_ENV: 'test',
DATABASE_URL: process.env.DATABASE_URL,
};
if (process.argv[2] === 'all') {
const cp = spawn('node', [...sharedArgs, resolve(testDir, '*')], {
cwd: root,
env,
stdio: 'inherit',
shell: true,
});
cp.on('exit', code => {
process.exit(code ?? 0);
});
} else {
const result = await p.group({
file: () =>
p.select({
message: 'Select a file to run',
options: files.map(file => ({
label: file,
value: file as any,
})),
}),
});
const target = resolve(testDir, result.file);
const cp = spawn(
'node',
[
...sharedArgs,
'--test-reporter=spec',
'--test-reporter-destination=stdout',
target,
],
{
cwd: root,
env,
stdio: 'inherit',
shell: true,
}
);
cp.on('exit', code => {
process.exit(code ?? 0);
});
}

17
apps/server/src/app.ts Normal file
View File

@@ -0,0 +1,17 @@
/// <reference types="./global.d.ts" />
import { Module } from '@nestjs/common';
import { ConfigModule } from './config';
import { GqlModule } from './graphql.module';
import { BusinessModules } from './modules';
import { PrismaModule } from './prisma';
@Module({
imports: [
PrismaModule,
GqlModule,
ConfigModule.forRoot(),
...BusinessModules,
],
})
export class AppModule {}

View File

@@ -0,0 +1,228 @@
import type { ApolloDriverConfig } from '@nestjs/apollo';
import type { LeafPaths } from '../utils/types';
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace globalThis {
// eslint-disable-next-line no-var
var AFFiNE: AFFiNEConfig;
}
}
export const enum ExternalAccount {
github = 'github',
google = 'google',
firebase = 'firebase',
}
type EnvConfigType = 'string' | 'int' | 'float' | 'boolean';
type ConfigPaths = LeafPaths<
Omit<
AFFiNEConfig,
| 'ENV_MAP'
| 'version'
| 'baseUrl'
| 'origin'
| 'prod'
| 'dev'
| 'test'
| 'deploy'
>,
'',
'....'
>;
/**
* parse number value from environment variables
*/
function int(value: string) {
const n = parseInt(value);
return Number.isNaN(n) ? undefined : n;
}
function float(value: string) {
const n = parseFloat(value);
return Number.isNaN(n) ? undefined : n;
}
function boolean(value: string) {
return value === '1' || value.toLowerCase() === 'true';
}
export function parseEnvValue(value: string | undefined, type?: EnvConfigType) {
if (typeof value === 'undefined') {
return;
}
return type === 'int'
? int(value)
: type === 'float'
? float(value)
: type === 'boolean'
? boolean(value)
: value;
}
/**
* All Configurations that would control AFFiNE server behaviors
*
*/
export interface AFFiNEConfig {
ENV_MAP: Record<string, ConfigPaths | [ConfigPaths, EnvConfigType?]>;
/**
* Server Identity
*/
readonly serverId: string;
/**
* System version
*/
readonly version: string;
/**
* alias to `process.env.NODE_ENV`
*
* @default 'production'
* @env NODE_ENV
*/
readonly env: string;
/**
* fast environment judge
*/
get prod(): boolean;
get dev(): boolean;
get test(): boolean;
get deploy(): boolean;
/**
* Whether the server is hosted on a ssl enabled domain
*/
https: boolean;
/**
* where the server get deployed.
*
* @default 'localhost'
* @env AFFINE_SERVER_HOST
*/
host: string;
/**
* which port the server will listen on
*
* @default 3000
* @env AFFINE_SERVER_PORT
*/
port: number;
/**
* subpath where the server get deployed if there is.
*
* @default '' // empty string
* @env AFFINE_SERVER_SUB_PATH
*/
path: string;
/**
* Readonly property `baseUrl` is the full url of the server consists of `https://HOST:PORT/PATH`.
*
* if `host` is not `localhost` then the port will be ignored
*/
get baseUrl(): string;
/**
* Readonly property `origin` is domain origin in the form of `https://HOST:PORT` without subpath.
*
* if `host` is not `localhost` then the port will be ignored
*/
get origin(): string;
/**
* the apollo driver config
*/
graphql: ApolloDriverConfig;
/**
* object storage Config
*
* all artifacts and logs will be stored on instance disk,
* and can not shared between instances if not configured
*/
objectStorage: {
/**
* whether use remote object storage
*/
enable: boolean;
/**
* used to store all uploaded builds and analysis reports
*
* the concrete type definition is not given here because different storage providers introduce
* significant differences in configuration
*
* @example
* {
* provider: 'aws',
* region: 'eu-west-1',
* aws_access_key_id: '',
* aws_secret_access_key: '',
* // other aws storage config...
* }
*/
config: Record<string, string>;
};
/**
* authentication config
*/
auth: {
/**
* Application sign key secret
*/
readonly salt: string;
/**
* Application access token expiration time
*/
readonly accessTokenExpiresIn: string;
/**
* Application refresh token expiration time
*/
readonly refreshTokenExpiresIn: string;
/**
* Application public key
*
*/
readonly publicKey: string;
/**
* Application private key
*
*/
readonly privateKey: string;
/**
* whether allow user to signup with email directly
*/
enableSignup: boolean;
/**
* whether allow user to signup by oauth providers
*/
enableOauth: boolean;
/**
* all available oauth providers
*/
oauthProviders: Partial<
Record<
ExternalAccount,
{
clientId: string;
clientSecret: string;
/**
* uri to start oauth flow
*/
authorizationUri?: string;
/**
* uri to authenticate `access_token` when user is redirected back from oauth provider with `code`
*/
accessTokenUri?: string;
/**
* uri to get user info with authenticated `access_token`
*/
userInfoUri?: string;
args?: Record<string, any>;
}
>
>;
};
}

View File

@@ -0,0 +1,72 @@
/// <reference types="../global.d.ts" />
import pkg from '../../package.json' assert { type: 'json' };
import type { AFFiNEConfig } from './def';
// Don't use this in production
export const examplePublicKey = `-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEnxM+GhB6eNKPmTP6uH5Gpr+bmQ87
hHGeOiCsay0w/aPwMqzAOKkZGqX+HZ9BNGy/yiXmnscey5b2vOTzxtRvxA==
-----END PUBLIC KEY-----`;
// Don't use this in production
export const examplePrivateKey = `-----BEGIN PRIVATE KEY-----
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgWOog5SFXs1Vjh/WP
QCYPQKgf/jsNmWsvD+jYSn6mi3yhRANCAASfEz4aEHp40o+ZM/q4fkamv5uZDzuE
cZ46IKxrLTD9o/AyrMA4qRkapf4dn0E0bL/KJeaexx7Llva85PPG1G/E
-----END PRIVATE KEY-----`;
export const getDefaultAFFiNEConfig: () => AFFiNEConfig = () => ({
serverId: 'affine-nestjs-server',
version: pkg.version,
ENV_MAP: {},
env: process.env.NODE_ENV ?? 'development',
get prod() {
return this.env === 'production';
},
get dev() {
return this.env === 'development';
},
get test() {
return this.env === 'test';
},
get deploy() {
return !this.dev && !this.test;
},
https: false,
host: 'localhost',
port: 3010,
path: '',
get origin() {
return this.dev
? 'http://localhost:8080'
: `${this.https ? 'https' : 'http'}://${this.host}${
this.host === 'localhost' ? `:${this.port}` : ''
}`;
},
get baseUrl() {
return `${this.origin}${this.path}`;
},
graphql: {
buildSchemaOptions: {
numberScalarMode: 'integer',
},
introspection: true,
playground: true,
debug: true,
},
auth: {
salt: '$2b$10$x4VDo2nmlo74yB5jflNhlu',
accessTokenExpiresIn: '1h',
refreshTokenExpiresIn: '7d',
publicKey: examplePublicKey,
privateKey: examplePrivateKey,
enableSignup: true,
enableOauth: false,
oauthProviders: {},
},
objectStorage: {
enable: false,
config: {},
},
});

View File

@@ -0,0 +1,15 @@
import { set } from 'lodash-es';
import { parseEnvValue } from './def';
for (const env in AFFiNE.ENV_MAP) {
const config = AFFiNE.ENV_MAP[env];
const [path, value] =
typeof config === 'string'
? [config, process.env[env]]
: [config[0], parseEnvValue(process.env[env], config[1])];
if (typeof value !== 'undefined') {
set(globalThis.AFFiNE, path, process.env[env]);
}
}

View File

@@ -0,0 +1,69 @@
import type { DynamicModule, FactoryProvider } from '@nestjs/common';
import { merge } from 'lodash-es';
import type { DeepPartial } from '../utils/types';
import type { AFFiNEConfig } from './def';
type ConstructorOf<T> = {
new (): T;
};
function ApplyType<T>(): ConstructorOf<T> {
// @ts-expect-error used to fake the type of config
return class Inner implements T {
constructor() {}
};
}
/**
* usage:
* ```
* import { Config } from '@affine/server'
*
* class TestConfig {
* constructor(private readonly config: Config) {}
* test() {
* return this.config.env
* }
* }
* ```
*/
export class Config extends ApplyType<AFFiNEConfig>() {}
function createConfigProvider(
override?: DeepPartial<Config>
): FactoryProvider<Config> {
return {
provide: Config,
useFactory: () => {
const wrapper = new Config();
const config = merge({}, AFFiNE, override);
const proxy: Config = new Proxy(wrapper, {
get: (_target, property: keyof Config) => {
const desc = Object.getOwnPropertyDescriptor(AFFiNE, property);
if (desc?.get) {
return desc.get.call(proxy);
}
return config[property];
},
});
return proxy;
},
};
}
export class ConfigModule {
static forRoot = (override?: DeepPartial<Config>): DynamicModule => {
const provider = createConfigProvider(override);
return {
global: true,
module: ConfigModule,
providers: [provider],
exports: [provider],
};
};
}
export type { AFFiNEConfig } from './def';

5
apps/server/src/global.d.ts vendored Normal file
View File

@@ -0,0 +1,5 @@
declare namespace Express {
interface Request {
user?: import('@prisma/client').User | null;
}
}

View File

@@ -0,0 +1,30 @@
import type { ApolloDriverConfig } from '@nestjs/apollo';
import { ApolloDriver } from '@nestjs/apollo';
import { Global, Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';
import { join } from 'path';
import { fileURLToPath } from 'url';
import { Config } from './config';
@Global()
@Module({
imports: [
GraphQLModule.forRootAsync<ApolloDriverConfig>({
driver: ApolloDriver,
useFactory: (config: Config) => {
return {
...config.graphql,
path: `${config.path}/graphql`,
autoSchemaFile: join(
fileURLToPath(import.meta.url),
'..',
'schema.gql'
),
};
},
inject: [Config],
}),
],
})
export class GqlModule {}

25
apps/server/src/index.ts Normal file
View File

@@ -0,0 +1,25 @@
import './prelude';
import { NestFactory } from '@nestjs/core';
import type { NestExpressApplication } from '@nestjs/platform-express';
import { AppModule } from './app';
const app = await NestFactory.create<NestExpressApplication>(AppModule, {
cors: {
origin:
process.env.AFFINE_ENV === 'preview'
? ['https://affine-preview.vercel.app']
: ['http://localhost:8080'],
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowedHeaders: '*',
},
bodyParser: true,
});
const host = process.env.HOST ?? 'localhost';
const port = process.env.PORT ?? 3010;
await app.listen(port, host);
console.log(`Listening on http://${host}:${port}`);

View File

@@ -0,0 +1,82 @@
import {
CanActivate,
createParamDecorator,
ExecutionContext,
Injectable,
UseGuards,
} from '@nestjs/common';
import { PrismaService } from '../../prisma';
import { getRequestResponseFromContext } from '../../utils/nestjs';
import { AuthService } from './service';
export function getUserFromContext(context: ExecutionContext) {
const req = getRequestResponseFromContext(context).req;
return req.user;
}
/**
* Used to fetch current user from the request context.
*
* > The user may be undefined if authorization token is not provided.
*
* @example
*
* ```typescript
* // Graphql Query
* \@Query(() => UserType)
* user(@CurrentUser() user?: User) {
* return user;
* }
* ```
*
* ```typescript
* // HTTP Controller
* \@Get('/user)
* user(@CurrentUser() user?: User) {
* return user;
* }
* ```
*/
export const CurrentUser = createParamDecorator(
(_: unknown, context: ExecutionContext) => {
return getUserFromContext(context);
}
);
@Injectable()
class AuthGuard implements CanActivate {
constructor(private auth: AuthService, private prisma: PrismaService) {}
async canActivate(context: ExecutionContext) {
const { req } = getRequestResponseFromContext(context);
const token = req.headers.authorization;
if (!token) {
return false;
}
const claims = this.auth.verify(token);
req.user = await this.prisma.user.findUnique({ where: { id: claims.id } });
return !!req.user;
}
}
/**
* This guard is used to protect routes/queries/mutations that require a user to be logged in.
*
* The `@CurrentUser()` parameter decorator used in a `Auth` guarded queries would always give us the user because the `Auth` guard will
* fast throw if user is not logged in.
*
* @example
*
* ```typescript
* \@Auth()
* \@Query(() => UserType)
* user(@CurrentUser() user: User) {
* return user;
* }
* ```
*/
export const Auth = () => {
return UseGuards(AuthGuard);
};

Some files were not shown because too many files have changed in this diff Show More