Compare commits

..

495 Commits

Author SHA1 Message Date
himself65
32f064c2de v0.7.0-canary.13 2023-06-13 14:13:38 +08:00
himself65
39704bc812 build: fix generate-assets.mjs 2023-06-13 14:12:08 +08:00
Himself65
a421265483 fix: remove unused hooks (#2762) 2023-06-13 12:18:32 +08:00
himself65
e6214cb6ec v0.7.0-canary.12 2023-06-13 11:00:39 +08:00
himself65
ba7d34bce5 docs: update thanks section in README.md 2023-06-13 10:45:52 +08:00
Himself65
ace3c37fcc feat: improve copilot (#2758) 2023-06-13 10:29:04 +08:00
Peng Xiao
5ba2dff008 feat: add helper process (#2753) 2023-06-13 10:01:43 +08:00
Himself65
dff8a0db7d fix: nx build input (#2755) 2023-06-12 13:06:23 +08:00
himself65
ccda45bdd2 build: fix nx.json 2023-06-12 12:05:00 +08:00
Himself65
eeb536d460 feat: use nx to manage monorepo (#2748) 2023-06-10 00:46:24 +08:00
Peng Xiao
1e6e0336c3 fix: electron dev crash (#2746) 2023-06-09 08:47:14 +00:00
Himself65
f2ac2e5b84 feat: isolated plugin system (#2742) 2023-06-09 16:43:46 +08:00
Himself65
af6f431c15 fix: replace noop function (#2744) 2023-06-09 16:00:44 +08:00
Peng Xiao
01ae21e1fa feat: add kalam font (#2743) 2023-06-09 15:48:30 +08:00
LongYinan
34141958eb style: remove some verbose codes (#2741) 2023-06-09 14:55:41 +08:00
JimmFly
c194cff0bd chore: remove en.json code owner (#2740) 2023-06-09 13:24:25 +08:00
Himself65
6b6f2d6910 chore: upgrade yarn (#2739) 2023-06-09 12:33:52 +08:00
LongYinan
2e975e79dd style: add ban-ts-comment rule (#2738) 2023-06-09 11:55:23 +08:00
Himself65
c5a295a87b chore: bump typescript to 5.1.3 (#2735)
Co-authored-by: LongYinan <lynweklm@gmail.com>
2023-06-09 11:42:54 +08:00
himself65
119b4cdf10 v0.7.0-canary.11 2023-06-09 11:30:51 +08:00
Himself65
bf6af934f6 fix: regression on the database and bookmark block (#2737) 2023-06-09 11:29:52 +08:00
Himself65
1971749449 test: add test cases for page setting atom (#2736) 2023-06-09 02:00:09 +08:00
Himself65
b383ce36cd build: enhance tsconfig type check (#2732) 2023-06-09 01:42:58 +08:00
Himself65
fc9a9f479b fix: page meta is undefined (#2734) 2023-06-09 01:19:16 +08:00
himself65
227174db1b build: fix eslint config 2023-06-09 01:11:53 +08:00
Himself65
9f129075dd feat: add page setting atom (#2725) 2023-06-09 00:58:46 +08:00
Peng Xiao
935b4f847c fix: potential flaky issues (#2733) 2023-06-09 00:20:12 +08:00
Whitewater
ec99a0ce05 feat: update desc for empty page (#2710) 2023-06-08 20:28:37 +08:00
Himself65
7ba5f82aef chore: bump blocksuite to 0.0.0-20230607055421-9b20fcaf-nightly (#2731) 2023-06-08 20:27:12 +08:00
Peng Xiao
546d5764e6 fix: a potential crash on fav list (#2716) 2023-06-08 20:06:15 +08:00
himself65
6f411bd459 v0.7.0-canary.10 2023-06-08 18:33:14 +08:00
JimmFly
29d8f61c90 feat: add date picker (#2644)
Co-authored-by: himself65 <himself65@outlook.com>
2023-06-08 17:55:16 +08:00
Peng Xiao
bedf838fe5 feat: add new rule for floating promise (#2726)
Co-authored-by: Himself65 <himself65@outlook.com>
2023-06-08 17:54:25 +08:00
Qi
fda89b05e7 feat: replace electron to puppeteer (#2700)
Co-authored-by: himself65 <himself65@outlook.com>
2023-06-08 17:51:45 +08:00
himself65
de8af5f114 revert: page jump once 2023-06-08 16:54:42 +08:00
himself65
14db45ae95 build: fix the directory path on webstorm 2023-06-08 16:26:09 +08:00
Himself65
27b14af388 test: fix flaky on local-first-workspace-list.spec.ts (#2727) 2023-06-08 16:18:10 +08:00
LongYinan
18dc427bc3 style: enable no-non-null-assertion rule (#2723)
Co-authored-by: Peng Xiao <pengxiao@outlook.com>
2023-06-08 07:23:20 +00:00
Himself65
1ad2e629ac refactor: project tsconfig & abstract infra type (#2721) 2023-06-08 12:41:47 +08:00
Himself65
05288be934 fix: flaky when drag workspace list (#2724) 2023-06-08 11:51:02 +08:00
Himself65
05b73a59be fix: first page default mode (#2719)
Co-authored-by: tzhangchi <c@affine.pro>
2023-06-08 10:17:27 +08:00
Himself65
f3fd5ff76b feat: add infra code (#2718) 2023-06-08 09:41:20 +08:00
Himself65
4958d096b0 fix: move workspace to top level (#2717) 2023-06-07 23:20:35 +08:00
Himself65
7f2006488e fix: type import (#2715) 2023-06-07 22:47:02 +08:00
Peng Xiao
008a05a470 fix(electron): should not continue pull when db closed (#2709) 2023-06-07 18:01:59 +08:00
himself65
3d7721d59d v0.7.0-canary.9 2023-06-07 17:59:55 +08:00
Flrande
35fb10c95b feat: add preloading template (#2655)
Co-authored-by: Himself65 <himself65@outlook.com>
2023-06-07 17:31:54 +08:00
Himself65
c4c4ec6a67 refactor: split storybook (#2706) 2023-06-07 16:55:06 +08:00
Himself65
f4be15baec fix: package affine/env modules (#2707) 2023-06-07 16:31:05 +08:00
himself65
17b40b68df docs: update README.md 2023-06-07 16:29:37 +08:00
Himself65
cd5c4b5cb7 fix: remove dependencies in @affine/debug (#2708) 2023-06-07 15:41:42 +08:00
LongYinan
d28c887237 feat(native): move sqlite operation into Rust (#2497)
Co-authored-by: Peng Xiao <pengxiao@outlook.com>
2023-06-07 14:52:19 +08:00
himself65
541011ba90 build: remove unused reference 2023-06-07 14:21:21 +08:00
wonderl17
fc658f4a95 fix: add bookmark operation flag for ts check (#2699) 2023-06-07 10:18:19 +08:00
Peng Xiao
84f68fc2c0 fix: import workspace may only show default preload page (#2685) 2023-06-06 14:42:50 +08:00
JimmFly
f78760cb83 fix: empty svg color missing (#2692) 2023-06-06 04:45:34 +00:00
Himself65
8f6db00402 feat: init window.affine (#2682) 2023-06-06 11:43:34 +08:00
Peng Xiao
d00d0bd951 fix: the top padding should be draggable (#2688) 2023-06-06 11:12:00 +08:00
Peng Xiao
8f5cd13e78 fix: do not show deleted reference (#2689) 2023-06-06 10:36:58 +08:00
Whitewater
3b4cfc642f fix: overflow in radio button group (#2687) 2023-06-05 10:18:53 +00:00
JimmFly
5807f34935 fix: empty icon color error (#2686) 2023-06-05 10:06:17 +00:00
Whitewater
efae4cccd6 feat: sticky table head in page list (#2668)
Co-authored-by: Himself65 <himself65@outlook.com>
2023-06-05 07:43:24 +00:00
Himself65
b461a684ad chore: bump version (#2681) 2023-06-05 01:00:41 +08:00
3720
b95808a052 test: add some e2e tests for all pages filter (#2674) 2023-06-04 23:34:16 +08:00
Himself65
1716e7a397 fix: use react-resizable-panels (#2679) 2023-06-04 23:33:36 +08:00
Himself65
acda594cba refactor: use esbuild instead of vite (#2672) 2023-06-02 13:22:09 +00:00
Himself65
94d20f1bdc feat: plugin system with isolated bundles (#2660) 2023-06-02 16:28:47 +08:00
Vlad Cuciureanu
f9079bb681 fix: README typo 2023-06-02 15:47:09 +08:00
himself65
0d07ff2390 v0.7.0-canary.8 2023-06-02 13:02:48 +08:00
xiaodong zuo
42bab6990e fix: update version bump-blocksuite (#2658) 2023-06-02 11:16:31 +08:00
fourdim
89a566a645 fix: pdf export in client and hide png export (#2604) 2023-06-01 13:26:57 +00:00
LongYinan
7af5bd3894 v0.7.0-canary.7 2023-06-01 19:11:12 +08:00
Peng Xiao
a57c27679d chore: bump blocksuite (#2652) 2023-06-01 10:55:10 +00:00
JimmFly
68a72b2dfc chore: update whats new link (#2651) 2023-06-01 17:59:30 +08:00
LongYinan
602f795133 build: prevent tsconfig includes sources outside (#2643) 2023-06-01 17:08:14 +08:00
himself65
5df89a925b v0.7.0-canary.6 2023-06-01 15:34:08 +07:00
Whitewater
23126e1ff6 fix: show table head when no item in page list (#2642) 2023-06-01 16:31:51 +08:00
xiaodong zuo
e1f715f837 chore: update blocksuite to 0.0.0-20230601062752-68dbf1a4-nightly (#2641) 2023-06-01 16:25:54 +08:00
himself65
fbcaed40e7 fix: block hub not working in the editor 2023-06-01 14:32:41 +08:00
JimmFly
88757ce488 chore: update all page style (#2638) 2023-06-01 12:38:14 +08:00
Simon He
fc9462eee9 perf: getEnvironment() -> env (#2636) 2023-06-01 03:23:38 +00:00
Qi
e1314730be feat: support get dynamic page meta data (#2632) 2023-06-01 11:03:16 +08:00
Peng Xiao
36978dbed6 fix: plugin bootstrap (#2631)
Co-authored-by: himself65 <himself65@outlook.com>
2023-06-01 01:14:37 +08:00
Whitewater
53d1991211 chore: update page group naming (#2628) 2023-05-31 16:41:25 +00:00
LongYinan
1ea445ab15 build: perform TypeCheck for all packages (#2573)
Co-authored-by: himself65 <himself65@outlook.com>
Co-authored-by: Peng Xiao <pengxiao@outlook.com>
2023-05-31 12:49:56 +00:00
Himself65
78410f531a chore: bump version (#2627) 2023-05-31 18:16:18 +08:00
himself65
96c0321696 v0.7.0-canary.5 2023-05-31 17:34:21 +08:00
himself65
0895f1fb30 ci: enable bookmark block in canary 2023-05-31 17:33:11 +08:00
himself65
b6188f4b11 docs: update logo in README.md 2023-05-31 17:23:01 +08:00
JimmFly
2ed1a7b219 chore: update filter style (#2625) 2023-05-31 17:20:18 +08:00
Himself65
9bee6bd5cc docs: update logo (#2626) 2023-05-31 17:16:50 +08:00
himself65
198f30c86d docs: update README.md 2023-05-31 17:12:27 +08:00
Himself65
454f1887cf feat: add @affine/bookmark-block plugin (#2618) 2023-05-31 17:08:03 +08:00
himself65
4e1e4e9435 v0.7.0-canary.4 2023-05-31 16:47:16 +08:00
3720
f7768563e1 fix: wrong use of dayjs (#2624) 2023-05-31 16:46:36 +08:00
Himself65
6aa0e71b84 chore: bump blocksuite to 0.0.0-20230531080915-ca9c55a2-nightly (#2622) 2023-05-31 16:32:35 +08:00
himself65
f1b3a10969 test: fix mouse click down timeout 2023-05-31 16:22:43 +08:00
Whitewater
90e70ed986 fix: drag delay (#2621) 2023-05-31 16:21:50 +08:00
xiaodong zuo
094a479c2a fix: remove the feature of exporting pdf/png (#2619)
Co-authored-by: himself65 <himself65@outlook.com>
2023-05-31 16:20:42 +08:00
Whitewater
20f1d487c8 feat: add page preview (#2620) 2023-05-31 08:18:48 +00:00
himself65
4c9bda1406 v0.7.0-canary.3 2023-05-31 15:41:52 +08:00
JimmFly
d5debc0bf5 chore: update filter style (#2617)
Co-authored-by: Himself65 <himself65@outlook.com>
2023-05-31 15:41:16 +08:00
Peng Xiao
f5aee7c360 fix: unify sidebar switch (#2616) 2023-05-31 07:06:13 +00:00
Himself65
248cd9a8ab chore: prohibit import package itself (#2612)
Co-authored-by: Whitewater <me@waterwater.moe>
2023-05-31 15:00:50 +08:00
Himself65
06abb702f5 refactor: remove deprecated atoms (#2615) 2023-05-31 14:54:59 +08:00
Himself65
ee289706ec refactor: move affine utils into @affine/workspace (#2611) 2023-05-31 13:13:59 +08:00
Himself65
6cbf310a5a chore: bump blocksuite to 0.0.0-20230531040027-44cd9d8e-nightly (#2610) 2023-05-31 13:10:41 +08:00
Whitewater
855fd8a73a feat: page list supports preview (#2606) 2023-05-31 04:24:55 +00:00
Himself65
8dbd354659 fix: logic after delete all workspaces (#2587)
Co-authored-by: JimmFly <yangjinfei001@gmail.com>
2023-05-31 12:24:14 +08:00
Himself65
1c7ae04f4f feat: update filter button (#2609) 2023-05-31 11:26:20 +08:00
Whitewater
0bb6e362bf feat: add page mode filter (#2601)
Co-authored-by: himself65 <himself65@outlook.com>
2023-05-31 11:15:23 +08:00
Peng Xiao
617350fc7d fix: optimize DB pull (#2589) 2023-05-31 11:09:18 +08:00
Hyden Liu
2713340532 fix(web): header div props error (#2607) 2023-05-31 10:34:42 +08:00
Whitewater
31d552ab7e fix: update breakpoint in all page (#2602) 2023-05-30 18:27:42 +08:00
Himself65
e11326f05f feat: add hook useBlockSuitePagePreview (#2603) 2023-05-30 18:26:13 +08:00
Himself65
6648fe4dcc feat: init @affine/copilot (#2511) 2023-05-30 18:02:49 +08:00
Peng Xiao
f669164674 fix: popover may not be closable (#2598) 2023-05-30 17:29:00 +08:00
JimmFly
c6d8904ca2 fix: quick search result missing title (#2594) 2023-05-30 16:45:00 +08:00
3720
8c5a1e2de3 test: add some tests for page filter (#2593) 2023-05-30 16:39:14 +08:00
himself65
395414c336 v0.7.0-canary.2 2023-05-30 15:17:35 +08:00
xiaodong zuo
96f653ea19 chore: bump blocksuit to 0.0.0-20230530061436-d0702cc0-nightly (#2590) 2023-05-30 15:13:54 +08:00
fourdim
fa089de40d feat: add support for exporting pdf and png (#2588)
This closes #2583.
2023-05-30 14:04:35 +08:00
Doma
4175f5391e feat(web): drag page to trash folder (#2385)
Co-authored-by: Himself65 <himself65@outlook.com>
2023-05-30 13:14:10 +08:00
Himself65
61c417992a feat: init support for multiple tiles (#2585) 2023-05-30 12:27:38 +08:00
Himself65
befae6bc9b feat: page view persistence (#2581) 2023-05-30 00:21:21 +08:00
himself65
e7eb13e966 v0.7.0-canary.1 2023-05-30 00:19:05 +08:00
Whitewater
88eaaf9ce4 feat: add radio group (#2572) 2023-05-29 23:15:22 +08:00
Himself65
20cc082a02 refactor: abstract header adapter (#2580) 2023-05-29 22:52:04 +08:00
Qi
402d12a0e1 fix: bookmark popper menu only display after pasted (#2578) 2023-05-29 14:25:30 +00:00
Himself65
58ba11e13c refactor: ui adapter (#2577) 2023-05-29 21:56:00 +08:00
Horus
cb6ca52b03 fix: replace windows installer loading gif (#2575) 2023-05-29 21:36:08 +08:00
Himself65
cd2ab73e5d chore: bump blocksuite to 0.0.0-20230529102007-5ac37643-nightly (#2569) 2023-05-29 18:51:33 +08:00
JimmFly
b16e725514 chore: adjust switch style (#2570) 2023-05-29 18:40:47 +08:00
JimmFly
004fcc8e80 fix: updater button text overflow (#2571) 2023-05-29 18:39:47 +08:00
Hyden Liu
a01a3ef011 fix: dropdown menu entire right can be pulled down (#2568)
Co-authored-by: Whitewater <me@waterwater.moe>
2023-05-29 07:03:26 +00:00
Peng Xiao
20cf45270d refactor(electron): sqlite db data workflow (remove symlink & fs watcher) (#2491) 2023-05-29 12:53:15 +08:00
3720
f3ac12254c feat: headless filter in all pages tab (#2566)
Co-authored-by: himself65 <himself65@outlook.com>
2023-05-29 04:06:40 +00:00
himself65
e0eb216b9b chore: revert @vanilla-extract/next-plugin to 2.1.2 2023-05-27 21:36:24 +08:00
Himself65
90afed1e74 feat: add build flag enableAllPageFilter (#2562) 2023-05-27 16:35:07 +08:00
Himself65
83d2ed8ace chore: bump version (#2559) 2023-05-27 16:08:07 +08:00
himself65
a0b64ca3e3 docs: update releases.md 2023-05-26 16:12:53 +08:00
himself65
b9fc5ad769 v0.7.0-canary.0 2023-05-26 15:50:00 +08:00
Himself65
ef1a44a413 chore: bump version (#2542) 2023-05-26 15:47:45 +08:00
xiaodong zuo
798dc49da4 feat: the UI of importing Html/Markdown/Notion (#2533)
Co-authored-by: Himself65 <himself65@outlook.com>
2023-05-26 15:30:39 +08:00
himself65
902081d44e ci: remove concurrency in languages-sync.yml 2023-05-26 15:26:12 +08:00
Whitewater
7969b73979 chore: tweak all page styles (#2540) 2023-05-26 15:13:50 +08:00
Himself65
c8734bd6ee chore: bump blocksuite to 0.0.0-20230526024755-74df4d56-nightly (#2541) 2023-05-26 15:13:00 +08:00
Qi
6d3c273ffd feat: support bookmark (#2458)
Co-authored-by: himself65 <himself65@outlook.com>
2023-05-26 06:52:36 +00:00
Himself65
f4b3830a0e feat(component): init notification center (#2426)
Co-authored-by: JimmFly <yangjinfei001@gmail.com>
2023-05-26 14:32:01 +08:00
Whitewater
36534f1915 feat: add storybook i18n decorator (#2538)
Co-authored-by: Himself65 <himself65@outlook.com>
2023-05-26 14:28:11 +08:00
Whitewater
7dcbe64d4e feat: group all page by date (#2532)
Co-authored-by: Himself65 <himself65@outlook.com>
2023-05-26 05:23:51 +00:00
Himself65
60057c666d fix: cannot delete last workspace (#2537) 2023-05-26 13:04:20 +08:00
JimmFly
60a83f4907 chore: update user guide style (#2536) 2023-05-26 12:55:06 +08:00
Horus
b11ce2c8d2 docs: add native build command to readme (#2535) 2023-05-26 10:29:30 +08:00
Whitewater
3b8f2c1ac3 refactor: use date obj in all pages (#2523) 2023-05-25 18:22:57 +08:00
Whitewater
be065e2de3 fix: sort in desc based update date by default (#2510) 2023-05-25 15:33:02 +08:00
Horus
675c737e48 fix: replace new windows install loading gif (#2513) 2023-05-25 14:30:52 +08:00
himself65
1255384cab v0.6.0-canary.8 2023-05-25 14:13:32 +08:00
Himself65
3d423c3299 fix: dispose on editor props.onInit (#2521) 2023-05-25 14:13:06 +08:00
ShortCipher5
ad4737850d chore: update pre-load content (#2518) 2023-05-25 05:41:49 +00:00
Himself65
9dcacd413c chore: bump blocksuite to 0.0.0-20230525011821-20259c76-nightly (#2515) 2023-05-25 13:04:18 +08:00
JimmFly
c1998eddf3 chore: bump electron (#2516) 2023-05-25 03:46:34 +00:00
himself65
db3f63e8f2 v0.6.0-canary.7 2023-05-25 09:17:21 +08:00
JimmFly
e562ca1011 chore: update download tip link (#2509) 2023-05-24 16:43:45 +08:00
fourdim
f6adf93f90 feat: add simple support for pdf (#2503) 2023-05-24 16:40:20 +08:00
Chi Zhang
053eba5d98 docs: update README.md (#2506) 2023-05-24 14:46:26 +08:00
Himself65
49f1ba676f fix: regression on toast component (#2502) 2023-05-24 13:10:25 +08:00
Aditya Sharma
48c109e149 feat(component): keyboard navigation for image-viewer (#2334)
Co-authored-by: Himself65 <himself65@outlook.com>
2023-05-23 09:35:11 +00:00
LongYinan
259d7988d9 chore(native): upgrade notify to v6 (#2489) 2023-05-22 22:45:43 +08:00
fourdim
0a49258ddd docs: update build guideline (#2434)
Co-authored-by: Himself65 <himself65@outlook.com>
2023-05-22 12:18:43 +00:00
himself65
fd35d3427e fix: make editor width to 800px
Fixes: https://github.com/toeverything/AFFiNE/issues/2486
2023-05-22 17:40:51 +08:00
Himself65
ef0a20b358 fix: use data-testid (#2487) 2023-05-22 17:36:52 +08:00
Himself65
f01997f8ee refactor: remove unused code (#2484) 2023-05-22 17:11:18 +08:00
Whitewater
281a068cfb chore(i18n): remove unused dependencies (#2485) 2023-05-22 17:03:55 +08:00
Whitewater
fe5be0cb47 fix: flatten i18n keys (#2483) 2023-05-22 08:08:43 +00:00
himself65
8aab1d6459 docs: add comment on legacy affine adapter 2023-05-22 16:02:05 +08:00
Himself65
2eaaeef4a7 fix: use hook with first render (#2481) 2023-05-22 15:58:13 +08:00
Himself65
5fbfabb3b2 refactor: rename plugins to adapters (#2480) 2023-05-22 15:48:01 +08:00
himself65
ec64260b6a v0.6.0-canary.6 2023-05-22 14:25:20 +08:00
LongYinan
2e23a4830b ci: add circular import detect (#2475)
Co-authored-by: himself65 <himself65@outlook.com>
2023-05-22 04:53:55 +00:00
Himself65
41a3d6f62f fix: wrap all workspaces with Suspense (#2477) 2023-05-22 12:39:07 +08:00
Peng Xiao
752bc9ca0e fix: fav reference style issue (#2476) 2023-05-22 12:01:03 +08:00
Himself65
c08f6fdba4 chore: update blocksuite to 0.0.0-20230519102837-01acd96b-nightly (#2472) 2023-05-22 02:27:03 +00:00
Geoffrey Biggs
b23b7e896b docs: correct spelling (#2469) 2023-05-22 07:26:30 +08:00
Whitewater
d68b421a4b feat: add responvise page view (#2453) 2023-05-22 07:25:25 +08:00
Horus
1f510799e2 fix: add windows install loading gif (#2462) 2023-05-21 16:03:48 +08:00
Peng Xiao
66ea97c7c9 fix: adjust some windows style issues (#2454) 2023-05-19 09:39:51 -07:00
Shishu
ee300e7b60 docs: sign CLA (#2457) 2023-05-19 08:40:59 -07:00
Peng Xiao
ef2d135e9b fix: optimize app updater (#2452) 2023-05-19 00:07:07 -07:00
JimmFly
c82fb89d57 chore: remove unused i18n key (#2451) 2023-05-19 03:38:48 +00:00
himself65
725bf63a32 ci: remove add-to-project.yml 2023-05-18 18:35:19 -07:00
himself65
c1ca578f7d docs: add download count 2023-05-18 13:12:26 -07:00
Whitewater
530dd5ff7f feat: add new page button (#2417) 2023-05-18 13:07:05 -07:00
Himself65
11370bc07e chore: bump version (#2444) 2023-05-18 11:43:45 -07:00
JimmFly
1c53daf1c4 chore: bump icon version (#2441) 2023-05-18 10:37:40 -07:00
Peng Xiao
b2556db33b fix: adjust some styles (#2438) 2023-05-18 09:24:23 +00:00
JimmFly
89310c9b97 chore: adjust delete description style (#2437) 2023-05-18 09:17:38 +00:00
JimmFly
8e09af910f fix: create workspace card responsive (#2435) 2023-05-18 09:11:15 +00:00
himself65
885aea3425 v0.6.0-canary.5 2023-05-18 01:21:25 -07:00
ShortCipher5
a616150f2d chore: update pre-load content (#2432) 2023-05-18 00:08:35 -07:00
Himself65
d80dae8a89 fix: open non-trash page when open (#2431) 2023-05-17 23:22:31 -07:00
Himself65
34ff08b92b chore: bump blocksuite to 0.0.0-20230518051344-45970a96-nightly (#2430) 2023-05-17 22:30:45 -07:00
Peng Xiao
2f7b51d7ff feat: fav page references (#2422)
Co-authored-by: Himself65 <himself65@outlook.com>
2023-05-17 22:18:40 -07:00
ShortCipher5
b7cee3185e chore: update pre-loading page (#2429) 2023-05-17 22:16:19 -07:00
JimmFly
1c5455e6ed chore: adjust copywriting for onboarding (#2428) 2023-05-17 22:15:01 -07:00
himself65
2d303fd5d3 v0.6.0-canary.4 2023-05-17 17:46:39 -07:00
himself65
fbe2543c03 fix: version check 2023-05-17 17:44:58 -07:00
Himself65
d6b640726e refactor: remove unused code (#2425) 2023-05-17 17:15:12 -07:00
Peng Xiao
f875b37641 fix: configurable changelog url (#2418) 2023-05-17 16:16:22 -07:00
Himself65
53c4fc6dfa fix: sidebar fallback ui position (#2424) 2023-05-17 15:49:55 -07:00
Himself65
6c310249d9 chore: bump version (#2423) 2023-05-17 15:02:55 -07:00
Horus
02e1f528bf fix: add workflow to check release version match with package.json (#2420) 2023-05-17 10:37:28 -07:00
Peng Xiao
c870104370 chore: bump blocksuite to 0.0.0-20230517102216-36bda4ab-nightly (#2411) 2023-05-17 10:09:29 -07:00
himself65
627d8ef787 v0.6.0-canary.3 2023-05-17 09:48:51 -07:00
LongYinan
5563823a7a build: missing build native step in nightly build (#2421) 2023-05-17 23:52:32 +08:00
JimmFly
d6804bb0fd chore: update prompt (#2410) 2023-05-17 17:52:57 +08:00
LongYinan
1350633690 build: fix electron release build process (#2408) 2023-05-17 17:22:49 +08:00
JimmFly
50196d8fde chore: update preloading page (#2409) 2023-05-17 09:16:07 +00:00
Peng Xiao
2e0ccb53ec feat: update button enhancements (#2401) 2023-05-17 16:58:14 +08:00
Whitewater
1498ee405b feat: add dropdown button (#2407) 2023-05-17 16:32:37 +08:00
Peng Xiao
cb863c4afa chore: disable image modal by default (#2400) 2023-05-16 23:14:31 -07:00
Himself65
2629d39501 fix: infinite reloading (#2405) 2023-05-17 13:58:34 +08:00
Himself65
38305cd984 fix: hydration error (#2404) 2023-05-17 05:23:55 +00:00
LongYinan
93116c24f2 feat(electron): use affine native (#2329) 2023-05-17 12:36:51 +08:00
Whitewater
017b9c8615 feat: add block card component (#2398) 2023-05-16 18:19:28 +08:00
Whitewater
9ce3a96862 fix: unexpected undefined class in popup (#2394) 2023-05-16 10:01:27 +00:00
Peng Xiao
a0ff520ba4 fix: some style updates (#2396) 2023-05-16 09:46:51 +00:00
Whitewater
a8b8986d89 chore: disable confused storybook backgrounds addon (#2395) 2023-05-16 17:46:35 +08:00
JimmFly
8ffc096fee fix: text overflows in the header option menu (#2393) 2023-05-16 17:35:57 +08:00
JimmFly
7e457f7b4c chore: add responsive styles for workspace card (#2390) 2023-05-16 16:51:46 +08:00
xiaodong zuo
aedf2d339e Update jobs.md
Added a job posting for a full-time or internship engineer.
2023-05-16 15:35:23 +08:00
JimmFly
ffd5ae52b3 feat: add Japanese support and update translation (#2388) 2023-05-16 14:21:51 +08:00
DiamondThree
3093194da8 docs: update jobs.md (#2389) 2023-05-15 22:24:27 -07:00
Horus
68b4f792f0 fix: app updater not working for internal release (#2377) 2023-05-15 20:34:54 -07:00
himself65
e2c6e4f9fc ci: use samver 2023-05-15 09:34:04 -07:00
Whitewater
9ff7dbffb7 feat: supports sort all page (#2356) 2023-05-15 08:50:43 -07:00
JimmFly
0c561da061 chore: remove favorite page (#2372) 2023-05-15 08:41:38 -07:00
JimmFly
06951319a6 chore: remove quick search tips (#2375) 2023-05-15 08:41:10 -07:00
JimmFly
0bfcab4067 chore: add animation for tour modal (#2365) 2023-05-15 16:48:52 +08:00
himself65
2c4db4fa16 v0.6.0-canary.2 2023-05-14 23:14:36 -07:00
Himself65
23b4f9ee12 feat(electron): track router history (#2336)
Co-authored-by: Peng Xiao <pengxiao@outlook.com>
2023-05-14 23:13:30 -07:00
himself65
e5330b1917 build: add app bundle id for internal 2023-05-14 22:35:40 -07:00
Peng Xiao
183611a556 fix: some style updates (#2348) 2023-05-14 21:58:13 -07:00
Himself65
7786456ba4 chore: update blocksuite to 0.0.0-20230514141009-705c0fac-nightly (#2357) 2023-05-14 19:32:27 -07:00
Ikko Eltociear Ashimine
f4bf7e3ddf fix: typo in AFFiNE-Docs.md (#2355) 2023-05-13 22:37:42 -07:00
Doma
05d88215d1 feat(electron): app menu item and hotkey for creating new page (#2267)
Co-authored-by: Peng Xiao <pengxiao@outlook.com>
2023-05-13 15:45:12 +00:00
Himself65
b240a70e51 chore: update blocksuite to 0.0.0-20230512192655-e61e272b-nightly (#2352) 2023-05-12 15:39:05 -05:00
LongYinan
00fd468e9b chore(server): remove bcrypt to avoid node-gyp usage (#2349) 2023-05-12 13:48:38 -05:00
Himself65
b5a7f8b7eb chore: bump version (#2331) 2023-05-12 13:47:14 -05:00
himself65
f03277fd17 v0.6.0-canary.1 2023-05-12 01:30:54 -05:00
himself65
ee93071149 chore: update icons 2023-05-12 01:06:05 -05:00
Himself65
21fdced2bd fix: correct router logic (#2342) 2023-05-12 00:55:45 -05:00
Peng Xiao
10b4558947 feat: new sidebar (app shell) styles (#2303) 2023-05-11 22:13:51 -05:00
Himself65
0fbed5d9d6 ci: collect test coverage on electron (#2335) 2023-05-11 20:51:13 -05:00
Himself65
8d117123d7 fix: remove useEffect on router sync with atoms (#2241) 2023-05-11 16:37:42 -05:00
Himself65
063ffda09d refactor: rename WorkspacePlugin to WorkspaceAdapter (#2330) 2023-05-11 12:43:39 -05:00
Himself65
39c83bd25b fix: delay setAom on rootWorkspacesMetadataAtom (#2271) 2023-05-11 15:03:11 +00:00
Peng Xiao
4444c3d1a6 fix: updater issue 2023-05-11 14:44:54 +08:00
LongYinan
717dd93f37 fix(electron): close db before move db file 2023-05-11 14:41:51 +08:00
LongYinan
c58673c55f chore(native): license 2023-05-11 14:41:51 +08:00
LongYinan
768e55072d ci: rust build config 2023-05-11 14:41:51 +08:00
LongYinan
8c84daec2b feat(native): NotifyEvent types 2023-05-11 14:41:51 +08:00
LongYinan
e54a5b6128 feat(native): provide FSWatcher 2023-05-11 14:41:51 +08:00
LongYinan
ee1e50f391 refactor(native): rename folder name 2023-05-11 14:41:51 +08:00
himself65
268636c440 v0.6.0-canary.0 2023-05-11 01:09:21 -05:00
Peng Xiao
06fa0cdb60 fix: should not show open folder if it is not moved (#2299) 2023-05-11 05:36:22 +00:00
Himself65
73dbb39009 feat(component): improve fallback skeleton (#2323) 2023-05-11 00:35:42 -05:00
JimmFly
47848cb5da fix: delete modal on confirm does not close (#2322) 2023-05-11 05:19:11 +00:00
JimmFly
eff6a03a51 chore: update AFFiNE Cloud prompt (#2321) 2023-05-11 00:18:12 -05:00
himself65
08f6a41ef4 ci: fix set version scripts 2023-05-10 23:00:36 -05:00
himself65
6d1345ffe5 build: replace version 2023-05-10 22:24:34 -05:00
Himself65
689f615b11 chore: bump version (#2310) 2023-05-10 21:43:14 -05:00
Himself65
f82ea5d9c4 build(electron): add internal release channel (#2309) 2023-05-10 21:42:56 -05:00
himself65
dc4979a80c fix(electron): remove unused code 2023-05-10 15:04:13 -05:00
JimmFly
1f48bc4301 refactor: tour modal (#2297) 2023-05-10 08:11:42 +00:00
himself65
beabd1e050 v0.5.4-canary.31 2023-05-10 00:56:04 -05:00
Himself65
19e20a6a20 fix(component): toast too many times when switch page mode (#2296) 2023-05-10 00:50:51 -05:00
Peng Xiao
e4f13ddae4 fix: try to fix updater not working (#2294)
Co-authored-by: Himself65 <himself65@outlook.com>
2023-05-10 00:40:22 -05:00
Himself65
752d0545ca feat: enhance root div styles (#2295) 2023-05-10 00:39:51 -05:00
himself65
08341d3d6c ci: remove master branch build 2023-05-09 23:11:46 -05:00
himself65
ef665df330 ci: add nightly-build.yml 2023-05-09 23:04:24 -05:00
Himself65
b38017cd23 feat(component): add skeleton in page detail (#2292) 2023-05-09 22:38:30 -05:00
Peng Xiao
0c550a2827 fix: theme not being persisted issue (#2283) 2023-05-09 22:04:36 -05:00
Chi Zhang
87ffdad862 docs: update README.md (#2291) 2023-05-09 21:51:44 -05:00
himself65
c6e8024e16 ci: disable fall-test in desktop-test 2023-05-09 21:13:33 -05:00
himself65
4200b3c3e5 ci: build staging and release branches 2023-05-09 20:27:07 -05:00
Himself65
10976a9257 chore: bump version (#2287) 2023-05-09 15:40:36 -05:00
himself65
b81b5439ae v0.5.4-canary.30 2023-05-09 15:02:14 -05:00
Himself65
a84ca43ce2 fix(component): input style (#2285) 2023-05-09 15:01:29 -05:00
JimmFly
b978bb171a feat: add download tips banner (#2151) 2023-05-09 11:07:34 -05:00
JimmFly
b937c1b5f6 chore: replace lang icon (#2281) 2023-05-09 15:27:05 +00:00
himself65
3c97e01513 v0.5.4-canary.29 2023-05-09 02:30:43 -05:00
Peng Xiao
7c2574b1ca feat: create workspace from loading existing exported file (#2122)
Co-authored-by: Himself65 <himself65@outlook.com>
2023-05-09 02:30:01 -05:00
himself65
5432aae85c v0.5.4-canary.28 2023-05-09 01:58:58 -05:00
Himself65
be41c99602 chore: bump version (#2279) 2023-05-09 01:35:49 -05:00
Himself65
c41718e80d feat(component): support image preview by double click (#2198) 2023-05-09 01:09:39 -05:00
Himself65
242e074ae6 feat: add suspense fallback for editor (#2278) 2023-05-08 23:57:36 -05:00
Himself65
793b689b81 fix(component): updated at in page meta (#2277) 2023-05-09 03:54:07 +00:00
Himself65
53db6a6e9d refactor(component): input component (#2275) 2023-05-09 02:39:39 +00:00
Fangdun Tsai
cba3293326 fix: width of cmd-item button on quick-search modal (#2273) 2023-05-08 21:13:11 -05:00
Fangdun Tsai
11d1d773ff fix: vertical alignment of item on share menu (#2274) 2023-05-08 21:12:19 -05:00
Fangdun Tsai
f071361347 fix: kebab-case warning for CSS properties in dev mode (#2276) 2023-05-08 21:03:32 -05:00
himself65
2c18fadb2d build: commit after the set version 2023-05-08 18:31:16 -05:00
Himself65
1e8c5a4482 fix(electron): self-update is pending (#2272) 2023-05-08 18:16:07 -05:00
Himself65
4f99ad2db4 feat: forced file naming format (#2270) 2023-05-08 17:37:07 -05:00
Himself65
95bc5cac49 refactor: remove sync storage (#2269) 2023-05-08 17:21:42 -05:00
himself65
3a6be4510b v0.5.4-canary.27 2023-05-08 16:33:44 -05:00
Horus
41d4af1dc1 feat: implement latest version updater for macos (#2214)
Co-authored-by: himself65 <himself65@outlook.com>
2023-05-08 13:21:15 -05:00
Doma
d1457075b3 feat(web): improve keyboard navigation in RootAppSidebar (#2256)
Co-authored-by: Himself65 <himself65@outlook.com>
2023-05-08 09:57:14 -05:00
Fangdun Tsai
d040a7fb50 fix: background color on shortcuts header (#2264) 2023-05-08 09:53:53 -05:00
Himself65
f1c3d575ad fix: dispatch workspace change event (#2261) 2023-05-08 14:47:47 +08:00
Fangdun Tsai
a942add87a fix: width of new page button (#2258) 2023-05-08 01:06:00 -05:00
himself65
d408a8bbb1 v0.5.4-canary.26 2023-05-07 23:44:02 -05:00
Himself65
4298ff7c7c chore: bump version (#2255) 2023-05-07 23:43:33 -05:00
Himself65
c55bfcc1fc refactor: remove @toeverything/theme (#2254) 2023-05-08 03:41:29 +00:00
Himself65
d795fb6b37 chore: bump version (#2253) 2023-05-08 03:23:55 +00:00
JimmFly
29cbbf5c97 chore: add border for header when in edgeless page (#2239) 2023-05-06 19:00:04 -05:00
JimmFly
aaa4b4f0cb fix: wrong menu position (#2238) 2023-05-06 18:58:41 -05:00
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
878 changed files with 51402 additions and 34294 deletions

View File

@@ -6,19 +6,22 @@
"always",
[
"electron",
"server",
"web",
"docs",
"component",
"workspace",
"env",
"graphql",
"cli",
"hooks",
"i18n",
"jotai",
"octobase-node",
"native",
"templates",
"y-indexeddb",
"debug"
"debug",
"theme"
]
]
}

View File

@@ -5,3 +5,6 @@ out
storybook-static
affine-out
_next
lib
.eslintrc.js
packages/i18n/src/i18n-generated.ts

View File

@@ -1,8 +1,58 @@
module.exports = {
const { resolve } = require('node:path');
const createPattern = packageName => [
{
group: ['**/dist', '**/dist/**'],
message: 'Do not import from dist',
allowTypeImports: false,
},
{
group: ['**/src', '**/src/**'],
message: 'Do not import from src',
allowTypeImports: false,
},
{
group: [`@affine/${packageName}`],
message: 'Do not import package itself',
allowTypeImports: false,
},
{
group: [`@toeverything/${packageName}`],
message: 'Do not import package itself',
allowTypeImports: false,
},
];
const allPackages = [
'packages/cli',
'packages/component',
'packages/debug',
'packages/env',
'packages/graphql',
'packages/hooks',
'packages/i18n',
'packages/jotai',
'packages/native',
'packages/plugin-infra',
'packages/templates',
'packages/theme',
'packages/workspace',
'packages/y-indexeddb',
'apps/web',
'apps/server',
'apps/electron',
'plugins/copilot',
'plugins/bookmark-block',
];
/**
* @type {import('eslint').Linter.Config}
*/
const config = {
root: true,
settings: {
react: {
version: '18',
version: 'detect',
},
next: {
rootDir: 'apps/web',
@@ -24,6 +74,7 @@ module.exports = {
},
ecmaVersion: 'latest',
sourceType: 'module',
project: resolve(__dirname, './tsconfig.eslint.json'),
},
plugins: [
'react',
@@ -31,6 +82,7 @@ module.exports = {
'simple-import-sort',
'import',
'unused-imports',
'unicorn',
],
rules: {
'no-undef': 'off',
@@ -39,14 +91,29 @@ module.exports = {
'no-cond-assign': 'off',
'react/prop-types': 'off',
'@typescript-eslint/consistent-type-imports': 'error',
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/no-non-null-assertion': 'error',
'@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',
'@typescript-eslint/ban-ts-comment': 0,
'@typescript-eslint/ban-ts-comment': [
'error',
{
'ts-expect-error': 'allow-with-description',
'ts-ignore': true,
'ts-nocheck': true,
'ts-check': false,
},
],
'@typescript-eslint/no-restricted-imports': [
'error',
{
@@ -64,5 +131,62 @@ module.exports = {
],
},
],
'unicorn/filename-case': [
'error',
{
case: 'kebabCase',
ignore: ['^\\[[a-zA-Z0-9-_]+\\]\\.tsx$'],
},
],
},
overrides: [
{
files: 'apps/server/**/*.ts',
rules: {
'@typescript-eslint/consistent-type-imports': 0,
},
},
{
files: '*.cjs',
rules: {
'@typescript-eslint/no-var-requires': 0,
},
},
{
files: [
'**/__tests__/**/*',
'**/*.stories.tsx',
'**/*.spec.ts',
'**/tests/**/*',
'scripts/**/*',
'**/benchmark/**/*',
'**/__debug__/**/*',
],
rules: {
'@typescript-eslint/no-non-null-assertion': 0,
'@typescript-eslint/ban-ts-comment': [
'error',
{
'ts-expect-error': false,
'ts-ignore': true,
'ts-nocheck': true,
'ts-check': false,
},
],
},
},
...allPackages.map(pkg => ({
files: [`${pkg}/src/**/*.ts`, `${pkg}/src/**/*.tsx`],
rules: {
'@typescript-eslint/no-restricted-imports': [
'error',
{
patterns: createPattern(pkg),
},
],
},
})),
],
};
module.exports = config;

5
.github/CLA.md vendored
View File

@@ -54,3 +54,8 @@ Example:
- 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
- Shishu, @shishudesu, 2023/05/19

1
.github/CODEOWNERS vendored
View File

@@ -1 +0,0 @@
**/en.json @JimmFly

51
.github/actions/build-rust/action.yml vendored Normal file
View File

@@ -0,0 +1,51 @@
name: 'AFFiNE Rust build'
description: 'Rust build setup, including cache configuration'
inputs:
target:
description: 'Cargo target'
required: true
runs:
using: 'composite'
steps:
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
with:
toolchain: stable
targets: ${{ inputs.target }}
- name: Cache cargo
uses: actions/cache@v3
with:
path: |
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
.cargo-cache
target/${{ inputs.target }}
key: stable-${{ inputs.target }}-cargo-cache
- name: Build
if: ${{ inputs.target != 'x86_64-unknown-linux-gnu' && inputs.target != 'aarch64-unknown-linux-gnu' }}
shell: bash
run: yarn workspace @affine/native build --target ${{ inputs.target }}
- name: Build
if: ${{ inputs.target == 'x86_64-unknown-linux-gnu' }}
uses: addnab/docker-run-action@v3
with:
image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian
options: --user 0:0 -v ${{ github.workspace }}/.cargo-cache/git/db:/usr/local/cargo/git/db -v ${{ github.workspace }}/.cargo/registry/cache:/usr/local/cargo/registry/cache -v ${{ github.workspace }}/.cargo/registry/index:/usr/local/cargo/registry/index -v ${{ github.workspace }}:/build -w /build
run: >-
export CC=x86_64-unknown-linux-gnu-gcc &&
export CC_x86_64_unknown_linux_gnu=x86_64-unknown-linux-gnu-gcc &&
yarn workspace @affine/native build --target ${{ inputs.target }}
- name: Build
if: ${{ inputs.target == 'aarch64-unknown-linux-gnu' }}
uses: addnab/docker-run-action@v3
with:
image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian-aarch64
options: --user 0:0 -v ${{ github.workspace }}/.cargo-cache/git/db:/usr/local/cargo/git/db -v ${{ github.workspace }}/.cargo/registry/cache:/usr/local/cargo/registry/cache -v ${{ github.workspace }}/.cargo/registry/index:/usr/local/cargo/registry/index -v ${{ github.workspace }}:/build -w /build
run: >-
yarn workspace @affine/native build --target ${{ inputs.target }}

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
@@ -82,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' }}

8
.github/labeler.yml vendored
View File

@@ -8,11 +8,17 @@ test:
- '**/tests/**/*'
- '**/__tests__/**/*'
plugin:copilot:
- 'plugins/copilot/**/*'
mod:dev:
- 'scripts/**/*'
- 'packages/cli/**/*'
- 'packages/debug/**/*'
mod:plugin-infra:
- 'packages/plugin-infra/**/*'
mod:workspace: 'packages/workspace/**/*'
mod:i18n: 'packages/i18n/**/*'
@@ -40,3 +46,5 @@ package:y-indexeddb: 'packages/y-indexeddb/**/*'
app:web: 'apps/web/**/*'
app:electron: 'apps/electron/**/*'
app:server: 'apps/server/**/*'

View File

@@ -1,24 +0,0 @@
name: Add to GitHub projects
on:
issues:
types:
- opened
pull_request_target:
types:
- opened
- reopened
jobs:
add-to-project:
name: Add issues and pull requests
runs-on: ubuntu-latest
steps:
- uses: actions/add-to-project@v0.4.0
with:
# You can target a repository in a different organization
# to the issue
project-url: https://github.com/orgs/toeverything/projects/10
github-token: ${{ secrets.ADD_TO_PROJECT_PAT }}
# labeled: bug, needs-triage
# label-operator: OR

View File

@@ -1,301 +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 }}
build-frontend-dev:
name: Build @affine/web dev
runs-on: ubuntu-latest
environment: development
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-dev-${{ hashFiles('**/yarn.lock') }}-${{ hashFiles('**.[jt]s', '**.[jt]sx') }}
restore-keys: |
${{ runner.os }}-nextjs-dev-${{ 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 }}
API_SERVER_PROFILE: local
ENABLE_DEBUG_PAGE: true
COVERAGE: true
- name: Upload artifact
uses: actions/upload-artifact@v3
with:
name: next-js-dev
path: ./apps/web/.next
if-no-files-found: error
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-dev, 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-dev
path: ./apps/web/.next
- name: Download storybook artifact
uses: actions/download-artifact@v3
with:
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:
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-${{ matrix.shard }}
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,9 +1,30 @@
name: Build & Test
on:
push:
branches:
- master
- v[0-9]+.[0-9]+.x-staging
- v[0-9]+.[0-9]+.x
paths-ignore:
- README.md
- .github/**
- '!.github/workflows/build.yml'
pull_request:
branches:
- master
- v[0-9]+.[0-9]+.x-staging
- v[0-9]+.[0-9]+.x
paths-ignore:
- README.md
- .github/**
- '!.github/workflows/build.yml'
env:
DEBUG: napi:*
APP_NAME: affine
COVERAGE: true
MACOSX_DEPLOYMENT_TARGET: '10.13'
jobs:
lint:
@@ -15,19 +36,12 @@ jobs:
- uses: actions/checkout@v3
- name: Setup Node.js
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
- name: Run checks
run: |
yarn i18n-codegen gen
yarn typecheck
yarn lint --max-warnings=0
yarn circular
build-storybook:
name: Build Storybook
@@ -38,15 +52,15 @@ jobs:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: ./.github/actions/setup-node
- run: yarn build:storybook
- run: yarn nx build @affine/storybook
- name: Upload storybook artifact
uses: actions/upload-artifact@v3
with:
name: storybook
path: ./packages/component/storybook-static
path: ./packages/storybook/storybook-static
if-no-files-found: error
build:
build-web:
name: Build @affine/web
runs-on: ubuntu-latest
environment: development
@@ -63,19 +77,15 @@ jobs:
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
- name: Build Web
run: yarn nx build @affine/web
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: local
ENABLE_DEBUG_PAGE: true
COVERAGE: true
ENABLE_DEBUG_PAGE: 1
ENABLE_PLUGIN: true
ENABLE_ALL_PAGE_FILTER: true
ENABLE_LEGACY_PROVIDER: true
ENABLE_PRELOADING: false
- name: Upload artifact
uses: actions/upload-artifact@v3
@@ -84,6 +94,78 @@ jobs:
path: ./apps/web/.next
if-no-files-found: error
- name: Build Web (Desktop)
run: yarn nx build @affine/web
env:
API_SERVER_PROFILE: affine
ENABLE_DEBUG_PAGE: 1
ENABLE_PLUGIN: true
ENABLE_ALL_PAGE_FILTER: true
ENABLE_LEGACY_PROVIDER: false
ENABLE_PRELOADING: false
- name: Export static resources
run: yarn workspace @affine/web export
- 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 nx test:coverage @affine/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
@@ -99,19 +181,11 @@ jobs:
uses: actions/download-artifact@v3
with:
name: storybook
path: ./packages/component/storybook-static
path: ./packages/storybook/storybook-static
- name: Run storybook tests
working-directory: ./packages/component
working-directory: ./packages/storybook
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
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"
e2e-test:
name: E2E Test
@@ -121,7 +195,7 @@ jobs:
matrix:
shard: [1, 2, 3, 4]
environment: development
needs: [build, build-storybook]
needs: [build-web, build-storybook]
services:
octobase:
image: ghcr.io/toeverything/cloud-self-hosted:nightly-latest
@@ -148,14 +222,14 @@ jobs:
uses: actions/download-artifact@v3
with:
name: storybook
path: ./packages/component/storybook-static
path: ./packages/storybook/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 }}
run: yarn e2e --forbid-only --shard=${{ matrix.shard }}/${{ strategy.job-total }}
env:
COVERAGE: true
@@ -173,12 +247,108 @@ jobs:
- name: Upload test results
if: ${{ failure() }}
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v3
with:
name: test-results-e2e-${{ matrix.shard }}
path: ./test-results
if-no-files-found: ignore
desktop-test:
name: Desktop Test
runs-on: ${{ matrix.spec.os }}
environment: development
strategy:
fail-fast: false
# all combinations: macos-latest x64, macos-latest arm64, windows-latest x64, ubuntu-latest x64
matrix:
spec:
- {
os: macos-latest,
platform: macos,
arch: x64,
target: x86_64-apple-darwin,
test: true,
}
- {
os: macos-latest,
platform: macos,
arch: arm64,
target: aarch64-apple-darwin,
test: false,
}
- {
os: ubuntu-latest,
platform: linux,
arch: x64,
target: x86_64-unknown-linux-gnu,
test: true,
}
- {
os: windows-latest,
platform: windows,
arch: x64,
target: x86_64-pc-windows-msvc,
test: true,
}
needs: [build-web]
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: ./.github/actions/setup-node
with:
playwright-install: true
- name: Build AFFiNE native
uses: ./.github/actions/build-rust
with:
target: ${{ matrix.spec.target }}
- name: Run unit tests
if: ${{ matrix.spec.test }}
shell: bash
run: yarn nx test @affine/monorepo
env:
NATIVE_TEST: 'true'
- name: Build layers
run: yarn workspace @affine/electron build
- 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
if: ${{ matrix.spec.test && matrix.spec.os == 'ubuntu-latest' }}
run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- yarn workspace @affine/electron test
env:
COVERAGE: true
- name: Run desktop tests
if: ${{ matrix.spec.test && matrix.spec.os != 'ubuntu-latest' }}
run: yarn workspace @affine/electron test
env:
COVERAGE: true
- name: Collect code coverage report
if: ${{ matrix.spec.test }}
run: yarn exec nyc report -t .nyc_output --report-dir .coverage --reporter=lcov
- name: Upload e2e test coverage results
if: ${{ matrix.spec.test }}
uses: codecov/codecov-action@v3
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./.coverage/lcov.info
flags: e2etest-${{ matrix.spec.os }}-${{ matrix.spec.arch }}
name: affine
fail_ci_if_error: true
- name: Upload test results
if: ${{ failure() }}
uses: actions/upload-artifact@v3
with:
name: test-results-e2e-${{ matrix.spec.os }}-${{ matrix.spec.arch }}
path: ./test-results
if-no-files-found: ignore
unit-test:
name: Unit Test
runs-on: ubuntu-latest
@@ -199,7 +369,7 @@ jobs:
uses: ./.github/actions/setup-node
- name: Unit Test
run: yarn run test:unit:coverage
run: yarn nx test:coverage @affine/monorepo
- name: Upload unit test coverage results
uses: codecov/codecov-action@v3

View File

@@ -13,14 +13,6 @@ on:
- '.github/workflows/languages-sync.yml'
workflow_dispatch:
# 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:
main:
runs-on: ubuntu-latest

219
.github/workflows/nightly-build.yml vendored Normal file
View File

@@ -0,0 +1,219 @@
name: Build Canary Desktop App on Staging Branch
on:
push:
branches:
- v[0-9]+.[0-9]+.x-staging
paths-ignore:
- README.md
- .github/**
- '!.github/workflows/nightly-build.yml'
permissions:
actions: write
contents: write
security-events: write
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
env:
BUILD_TYPE: internal
jobs:
set-build-version:
runs-on: ubuntu-latest
environment: production
outputs:
version: 0.0.0-${{ steps.version.outputs.version }}
steps:
- uses: actions/checkout@v3
- uses: toeverything/set-build-version@latest
- id: version
run: echo ::set-output name=version::${{ env.BUILD_VERSION }}
before-make:
runs-on: ubuntu-latest
environment: production
needs:
- set-build-version
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: ./.github/actions/setup-node
- name: Replace Version
run: ./scripts/set-version.sh ${{ needs.set-build-version.outputs.version }}
- name: generate-assets
working-directory: apps/electron
run: yarn generate-assets
env:
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
ENABLE_IMAGE_PREVIEW_MODAL: false
ENABLE_BOOKMARK_OPERATION: true
RELEASE_VERSION: ${{ needs.set-build-version.outputs.version }}
- name: Upload Artifact (web-static)
uses: actions/upload-artifact@v3
with:
name: before-make-web-static
path: apps/electron/resources/web-static
make-distribution:
environment: production
strategy:
# all combinations: macos-latest x64, macos-latest arm64, windows-latest x64, ubuntu-latest x64
matrix:
spec:
- {
os: macos-latest,
platform: darwin,
arch: x64,
target: x86_64-apple-darwin,
}
- {
os: macos-latest,
platform: darwin,
arch: arm64,
target: aarch64-apple-darwin,
}
- {
os: ubuntu-latest,
platform: linux,
arch: x64,
target: x86_64-unknown-linux-gnu,
}
- {
os: windows-latest,
platform: win32,
arch: x64,
target: x86_64-pc-windows-msvc,
}
runs-on: ${{ matrix.spec.os }}
needs:
- before-make
- set-build-version
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
- name: Build AFFiNE native
uses: ./.github/actions/build-rust
with:
target: ${{ matrix.spec.target }}
- name: Replace Version
run: ./scripts/set-version.sh ${{ needs.set-build-version.outputs.version }}
- uses: actions/download-artifact@v3
with:
name: before-make-web-static
path: apps/electron/resources/web-static
- name: Build layers
run: yarn workspace @affine/electron build
- name: Signing By Apple Developer ID
if: ${{ matrix.spec.platform == 'darwin' }}
uses: apple-actions/import-codesign-certs@v2
with:
p12-file-base64: ${{ secrets.CERTIFICATES_P12 }}
p12-password: ${{ secrets.CERTIFICATES_P12_PASSWORD }}
- name: make
run: yarn workspace @affine/electron make --platform=${{ matrix.spec.platform }} --arch=${{ matrix.spec.arch }}
- name: Save artifacts (mac)
if: ${{ matrix.spec.platform == 'darwin' }}
run: |
mkdir -p builds
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 == 'win32' }}
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-${{ matrix.spec.platform }}-${{ matrix.spec.arch }}-builds
path: builds
release:
needs:
- make-distribution
- set-build-version
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Download Artifacts (macos-x64)
uses: actions/download-artifact@v3
with:
name: affine-darwin-x64-builds
path: ./
- name: Download Artifacts (macos-arm64)
uses: actions/download-artifact@v3
with:
name: affine-darwin-arm64-builds
path: ./
- name: Download Artifacts (windows-x64)
uses: actions/download-artifact@v3
with:
name: affine-win32-x64-builds
path: ./
- name: Download Artifacts (linux-x64)
uses: actions/download-artifact@v3
with:
name: affine-linux-x64-builds
path: ./
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: 18
- name: Generate Release yml
run: |
cp ./apps/electron/scripts/generate-yml.js .
node generate-yml.js
env:
RELEASE_VERSION: ${{ needs.set-build-version.outputs.version }}
- name: Create Release Draft
uses: softprops/action-gh-release@v1
env:
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
with:
repository: 'toeverything/AFFiNE-Releases'
name: ${{ needs.set-build-version.outputs.version }}
tag_name: ${{ needs.set-build-version.outputs.version }}
prerelease: true
files: |
./VERSION
./*.zip
./*.dmg
./*.exe
./*.nupkg
./RELEASES
./*.AppImage
./*.apk
./*.yml

View File

@@ -17,11 +17,11 @@ on:
type: boolean
required: true
default: true
is-canary:
description: 'Canary Release? The app will be named as "AFFiNE Canary"'
type: boolean
build-type:
description: 'Build Type (canary, beta or stable)'
type: string
required: true
default: true
default: canary
permissions:
actions: write
@@ -35,137 +35,117 @@ concurrency:
cancel-in-progress: true
env:
BUILD_TYPE: ${{ github.event.inputs.is-canary == 'true' && 'canary' || 'stable' }}
BUILD_TYPE: ${{ github.event.inputs.build-type }}
DEBUG: napi:*
APP_NAME: affine
MACOSX_DEPLOYMENT_TARGET: '10.13'
jobs:
make-macos:
before-make:
runs-on: ubuntu-latest
environment: ${{ github.ref_name == 'master' && 'production' || 'development' }}
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 }}
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
runs-on: macos-latest
strategy:
matrix:
arch: [x64, arm64]
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: ./.github/actions/setup-node
- name: generate-assets
run: yarn workspace @affine/electron generate-assets
env:
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
API_SERVER_PROFILE: prod
ENABLE_TEST_PROPERTIES: false
ENABLE_IMAGE_PREVIEW_MODAL: false
ENABLE_BOOKMARK_OPERATION: true
RELEASE_VERSION: ${{ github.event.inputs.version }}
- 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
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: darwin,
arch: x64,
target: x86_64-apple-darwin,
}
- {
os: macos-latest,
platform: darwin,
arch: arm64,
target: aarch64-apple-darwin,
}
- {
os: ubuntu-latest,
platform: linux,
arch: x64,
target: x86_64-unknown-linux-gnu,
}
- {
os: windows-latest,
platform: win32,
arch: x64,
target: x86_64-pc-windows-msvc,
}
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
- name: Build AFFiNE native
uses: ./.github/actions/build-rust
with:
target: ${{ matrix.spec.target }}
- uses: actions/download-artifact@v3
with:
name: before-make-web-static
path: apps/electron/resources/web-static
- name: Build layers
run: yarn workspace @affine/electron build
- name: Signing By Apple Developer ID
if: ${{ matrix.spec.platform == 'darwin' }}
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 }}
working-directory: apps/electron
- name: make
run: yarn workspace @affine/electron make --platform=${{ matrix.spec.platform }} --arch=${{ matrix.spec.arch }}
- name: Save artifacts
- name: Save artifacts (mac)
if: ${{ matrix.spec.platform == 'darwin' }}
run: |
mkdir -p builds
mv apps/electron/out/*/make/*.dmg ./builds/affine-${{ env.BUILD_TYPE }}-darwin-${{ matrix.arch }}.dmg
- name: Upload Artifact
uses: actions/upload-artifact@v3
with:
name: affine-darwin-${{ matrix.arch }}-builds
path: builds
make-windows:
runs-on: windows-latest
environment: ${{ github.ref_name == 'master' && 'production' || 'development' }}
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
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: ./.github/actions/setup-node
with:
electron-workspace-install: true
- name: make build
run: yarn make-windows-x64
working-directory: apps/electron
- name: Save windows artifacts
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 == 'win32' }}
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
mv apps/electron/out/*/make/squirrel.windows/x64/RELEASES ./builds/RELEASES
- name: Upload Artifact
uses: actions/upload-artifact@v3
with:
name: affine-windows-x64-builds
path: builds
make-linux:
runs-on: ubuntu-latest
environment: ${{ github.ref_name == 'master' && 'production' || 'development' }}
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
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: ./.github/actions/setup-node
with:
electron-workspace-install: true
- name: make build
run: yarn make
working-directory: apps/electron
- name: Save Linux artifacts
- 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
@@ -173,39 +153,48 @@ jobs:
- name: Upload Artifact
uses: actions/upload-artifact@v3
with:
name: affine-Linux-x64-builds
name: affine-${{ matrix.spec.platform }}-${{ matrix.spec.arch }}-builds
path: builds
release:
needs: [make-macos, make-windows, make-linux]
needs: make-distribution
runs-on: ubuntu-latest
steps:
- name: Download MacOS x64 Artifacts
- uses: actions/checkout@v3
- name: Download Artifacts (macos-x64)
uses: actions/download-artifact@v3
with:
name: affine-darwin-x64-builds
path: ./
- name: Download MacOS arm64 Artifacts
- name: Download Artifacts (macos-arm64)
uses: actions/download-artifact@v3
with:
name: affine-darwin-arm64-builds
path: ./
- name: Download Windows Artifacts
- name: Download Artifacts (windows-x64)
uses: actions/download-artifact@v3
with:
name: affine-windows-x64-builds
name: affine-win32-x64-builds
path: ./
- name: Download Linux Artifacts
- name: Download Artifacts (linux-x64)
uses: actions/download-artifact@v3
with:
name: affine-Linux-x64-builds
name: affine-linux-x64-builds
path: ./
- uses: actions/setup-node@v3
with:
node-version: 18
- name: Generate Release yml
run: |
cp ./apps/electron/scripts/generate-yml.js .
node generate-yml.js
env:
RELEASE_VERSION: ${{ github.event.inputs.version }}
- 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'
@@ -220,3 +209,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 }}

13
.gitignore vendored
View File

@@ -28,9 +28,9 @@ node_modules
# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/settings.template.json
!.vscode/launch.template.json
!.vscode/extensions.json
# misc
@@ -57,6 +57,7 @@ Thumbs.db
.next
out/
storybook-static
i18n-generated.ts
/test-results/
/playwright-report/
@@ -65,3 +66,11 @@ storybook-static
# Cache
.eslintcache
next-env.d.ts
.rollup.cache
# Rust
target
*.node
tsconfig.node.tsbuildinfo
lib
affine.db

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,4 @@
pnpm-lock.yaml
apps/electron/layers/preload/preload.d.ts
target
lib
test-results

9
.taplo.toml Normal file
View File

@@ -0,0 +1,9 @@
exclude = ["node_modules/**/*.toml"]
[[rule]]
keys = ["dependencies", "*-dependencies"]
[rule.formatting]
align_entries = true
indent_tables = true
reorder_keys = true

View File

@@ -6,6 +6,12 @@
"name": "Run Dev",
"request": "launch",
"type": "node-terminal"
},
{
"command": "yarn run dev:local",
"name": "Run Dev Locally",
"request": "launch",
"type": "node-terminal"
}
]
}

View File

@@ -26,8 +26,19 @@
"[toml]": {
"editor.defaultFormatter": "tamasfe.even-better-toml"
},
"rust-analyzer.linkedProjects": ["packages/octobase-node/Cargo.toml"],
"[typescriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"vitest.include": [
"packages/**/*.spec.ts",
"packages/**/*.spec.tsx",
"apps/web/**/*.spec.ts",
"apps/web/**/*.spec.tsx",
"apps/electron/src/**/*.spec.ts",
"tests/unit/**/*.spec.ts",
"tests/unit/**/*.spec.tsx"
],
"rust-analyzer.check.extraEnv": {
"DATABASE_URL": "sqlite:affine.db"
}
}

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
@@ -16,4 +16,4 @@ plugins:
- path: .yarn/plugins/@yarnpkg/plugin-workspace-tools.cjs
spec: '@yarnpkg/plugin-workspace-tools'
yarnPath: .yarn/releases/yarn-3.5.0.cjs
yarnPath: .yarn/releases/yarn-3.6.0.cjs

2122
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

11
Cargo.toml Normal file
View File

@@ -0,0 +1,11 @@
[workspace]
members = ["./packages/native", "./packages/native/schema"]
[profile.dev.package.sqlx-macros]
opt-level = 3
[profile.release]
lto = true
codegen-units = 1
opt-level = 3
strip = "symbols"

139
README.md
View File

@@ -24,11 +24,13 @@ See https://github.com/all-?/all-contributors/issues/361#issuecomment-637166066
<!-- ALL-CONTRIBUTORS-BADGE:END -->
[![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://github.com/toeverything/AFFiNE/releases/latest)
[![AFFiNE macOS x64](https://img.shields.io/badge/-macOS_x86%20%E2%86%92-black?style=flat-square&logo=apple&logoColor=white)](https://github.com/toeverything/AFFiNE/releases/latest)
[![AFFiNE Window x64](https://img.shields.io/badge/-Windows%20%E2%86%92-blue?style=flat-square&logo=windows&logoColor=white)](https://github.com/toeverything/AFFiNE/releases/latest)
[![AFFiNE Web](<https://img.shields.io/badge/-Try%20It%20Online%20%E2%86%92-rgb(84,56,255)?style=flat-square&logoColor=white&logo=affine>)](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)
[![Releases](https://img.shields.io/github/downloads/toeverything/AFFiNE/total)](https://github.com/toeverything/AFFiNE/releases/latest)
[![stars-icon]](https://github.com/toeverything/AFFiNE)
[![All Contributors][all-contributors-badge]](#contributors)
[![codecov]](https://codecov.io/gh/toeverything/AFFiNE)
@@ -43,7 +45,7 @@ See https://github.com/all-?/all-contributors/issues/361#issuecomment-637166066
---
<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>
<a href="http://affine.pro"><img src="https://img.shields.io/badge/-AFFiNE-06449d?style=social&logo=affine" 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>
&nbsp;
@@ -68,11 +70,11 @@ See https://github.com/all-?/all-contributors/issues/361#issuecomment-637166066
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 & Stay tunned with us.
## Getting started & staying tuned with us.
⚠️ Please note that AFFiNE is still under active development and is not yet ready for production use. ⚠️
[![affine.pro](https://img.shields.io/static/v1?label=Try%20it%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=&message=%E2%86%92&style=for-the-badge)](https://app.affine.pro) No installation or registration required! Head over to our website and try it out now.
[![affine.pro](https://img.shields.io/static/v1?label=Try%20it%20Online&logo=affine&message=%E2%86%92&style=for-the-badge)](https://app.affine.pro) No installation or registration required! Head over to our website and try it out now.
[![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.
@@ -81,7 +83,7 @@ Star us, and you will receive all releases notifications from GitHub without any
## 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.
- **Hyper merged** — Write, draw and plan all at once. Assemble any blocks you love on any canvas you like to enjoy seamless transitions between 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.
@@ -110,6 +112,23 @@ 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) |
## Plugins
> Plugins are a way to extend the functionality of AFFiNE.
| Name | |
| ------------------------------------------------ | ----------------------------------------- |
| [@affine/bookmark-block](plugins/bookmark-block) | A block for bookmarking a website |
| [@affine/copilot](plugins/copilot) | AI Copilot that help you document writing |
## Thanks
We would also like to give thanks to open-source projects that make AFFiNE possible:
@@ -117,113 +136,23 @@ We would also like to give thanks to open-source projects that make AFFiNE possi
- [BlockSuite](https://github.com/toeverything/BlockSuite) - 💠 BlockSuite is the open-source collaborative editor project behind AFFiNE.
- [OctoBase](https://github.com/toeverything/OctoBase) - 🐙 OctoBase is the open-source database behind AFFiNE, local-first, yet collaborative. A light-weight, scalable, data engine written in Rust.
- [Yjs](https://github.com/yjs/yjs) & [Yrs](https://github.com/y-crdt/y-crdt) - Fundamental support of CRDTs for our implementation on state management and data sync.
- [Next.js](https://github.com/vercel/next.js) - The React Framework.
- [Electron](https://github.com/electron/electron) - Build cross-platform desktop apps with JavaScript, HTML, and CSS.
- [React](https://github.com/facebook/react) - View layer support and web GUI framework.
- [Rust](https://github.com/rust-lang/rust) - High performance language that extends the ability and availability of our real-time backend, OctoBase.
- [Jotai](https://github.com/pmndrs/jotai) - Primitive and flexible state management for React.
- [MUI](https://github.com/mui/material-ui) - Our most used graphic UI component library.
- [async-call-rpc](https://github.com/Jack-Works/async-call-rpc) - A lightweight JSON RPC client & server.
- Other upstream [dependencies](https://github.com/toeverything/AFFiNE/network/dependencies).
Thanks a lot to the community for providing such powerful and simple libraries, so that we can focus more on the implementation of the product logic, and we hope that in the future our projects will also provide a more easy-to-use knowledge base for everyone.
# Contributors
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
<!-- prettier-ignore-start -->
<!-- markdownlint-disable -->
<table>
<tr>
<td align="center"><a href="https://github.com/doodlewind"><img src="https://avatars.githubusercontent.com/u/7312949?v=4?s=50" width="50px;" alt=""/><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"><a href="https://darksky.eu.org/"><img src="https://avatars.githubusercontent.com/u/25152247?v=4?s=50" width="50px;" alt=""/><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"><a href="http://zhangchi.page/"><img src="https://avatars.githubusercontent.com/u/5910926?v=4?s=50" width="50px;" alt=""/><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"><a href="https://github.com/alt1o"><img src="https://avatars.githubusercontent.com/u/21084335?v=4?s=50" width="50px;" alt=""/><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"><a href="https://github.com/Brooooooklyn"><img src="https://avatars.githubusercontent.com/u/3468483?v=4?s=50" width="50px;" alt=""/><br /><sub><b>LongYinan</b></sub></a><br /><a href="https://github.com/toeverything/AFFiNE/commits?author=Brooooooklyn" title="Code">💻</a> <a href="https://github.com/toeverything/AFFiNE/commits?author=Brooooooklyn" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/hwangdev97"><img src="https://avatars.githubusercontent.com/u/24713927?v=4?s=50" width="50px;" alt=""/><br /><sub><b>Hwang</b></sub></a><br /><a href="https://github.com/toeverything/AFFiNE/commits?author=hwangdev97" title="Code">💻</a> <a href="https://github.com/toeverything/AFFiNE/commits?author=hwangdev97" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/kobeshanks"><img src="https://avatars.githubusercontent.com/u/82570088?v=4?s=50" width="50px;" alt=""/><br /><sub><b>kobeshanks</b></sub></a><br /><a href="https://github.com/toeverything/AFFiNE/commits?author=kobeshanks" title="Code">💻</a> <a href="https://github.com/toeverything/AFFiNE/commits?author=kobeshanks" title="Documentation">📖</a></td>
</tr>
<tr>
<td align="center"><a href="https://pengx17.vercel.app/"><img src="https://avatars.githubusercontent.com/u/584378?v=4?s=50" width="50px;" alt=""/><br /><sub><b>Peng Xiao</b></sub></a><br /><a href="https://github.com/toeverything/AFFiNE/commits?author=pengx17" title="Code">💻</a> <a href="https://github.com/toeverything/AFFiNE/commits?author=pengx17" title="Documentation">📖</a></td>
<td align="center"><a href="https://mirone.me/"><img src="https://avatars.githubusercontent.com/u/10047788?v=4?s=50" width="50px;" alt=""/><br /><sub><b>Mirone</b></sub></a><br /><a href="https://github.com/toeverything/AFFiNE/commits?author=Saul-Mirone" title="Code">💻</a> <a href="https://github.com/toeverything/AFFiNE/commits?author=Saul-Mirone" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/zqran"><img src="https://avatars.githubusercontent.com/u/15389209?v=4?s=50" width="50px;" alt=""/><br /><sub><b>zqran</b></sub></a><br /><a href="https://github.com/toeverything/AFFiNE/commits?author=zqran" title="Code">💻</a> <a href="https://github.com/toeverything/AFFiNE/commits?author=zqran" title="Documentation">📖</a></td>
<td align="center"><a href="https://sunebear.com/"><img src="https://avatars.githubusercontent.com/u/7693264?v=4?s=50" width="50px;" alt=""/><br /><sub><b>Shule Hsiung</b></sub></a><br /><a href="https://github.com/toeverything/AFFiNE/commits?author=SuneBear" title="Code">💻</a> <a href="https://github.com/toeverything/AFFiNE/commits?author=SuneBear" title="Documentation">📖</a></td>
<td align="center"><a href="https://fundon.viz.rs/"><img src="https://avatars.githubusercontent.com/u/27926?v=4?s=50" width="50px;" alt=""/><br /><sub><b>Fangdun Tsai</b></sub></a><br /><a href="https://github.com/toeverything/AFFiNE/commits?author=fundon" title="Code">💻</a> <a href="https://github.com/toeverything/AFFiNE/commits?author=fundon" title="Documentation">📖</a></td>
<td align="center"><a href="https://lawvs.github.io/profile/"><img src="https://avatars.githubusercontent.com/u/18554747?v=4?s=50" width="50px;" alt=""/><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"><a href="https://github.com/zuoxiaodong0815"><img src="https://avatars.githubusercontent.com/u/53252747?v=4?s=50" width="50px;" alt=""/><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"><a href="https://github.com/Himself65"><img src="https://avatars.githubusercontent.com/u/14026360?v=4?s=50" width="50px;" alt=""/><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"><a href="https://github.com/DiamondThree"><img src="https://avatars.githubusercontent.com/u/24630517?v=4?s=50" width="50px;" alt=""/><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"><a href="https://github.com/QiShaoXuan"><img src="https://avatars.githubusercontent.com/u/22772830?v=4?s=50" width="50px;" alt=""/><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"><a href="https://colelawrence.com/"><img src="https://avatars.githubusercontent.com/u/2925395?v=4?s=50" width="50px;" alt=""/><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"><a href="https://onetwo.ren/wiki"><img src="https://avatars.githubusercontent.com/u/3746270?v=4?s=50" width="50px;" alt=""/><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"><a href="https://github.com/thorseraq"><img src="https://avatars.githubusercontent.com/u/20554850?v=4?s=50" width="50px;" alt=""/><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"><a href="https://github.com/HeJiachen-PM"><img src="https://avatars.githubusercontent.com/u/79301703?v=4?s=50" width="50px;" alt=""/><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"><a href="https://www.notion.so/houjoe/Joe-2a85f5be01004cd2b6a5ad26fbb948b1"><img src="https://avatars.githubusercontent.com/u/22443345?v=4?s=50" width="50px;" alt=""/><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"><a href="https://github.com/Yipei-Operation"><img src="https://avatars.githubusercontent.com/u/79373028?v=4?s=50" width="50px;" alt=""/><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"><a href="https://github.com/VelikaHF"><img src="https://avatars.githubusercontent.com/u/121547898?v=4?s=50" width="50px;" alt=""/><br /><sub><b>Velika</b></sub></a><br /><a href="#design-VelikaHF" title="Design">🎨</a></td>
<td align="center"><a href="https://github.com/Svaney-ssman"><img src="https://avatars.githubusercontent.com/u/110808979?v=4?s=50" width="50px;" alt=""/><br /><sub><b>Svaney</b></sub></a><br /><a href="#design-Svaney-ssman" title="Design">🎨</a></td>
<td align="center"><a href="http://xell.me/"><img src="https://avatars.githubusercontent.com/u/132558?v=4?s=50" width="50px;" alt=""/><br /><sub><b>Guozhu Liu</b></sub></a><br /><a href="#design-xell" title="Design">🎨</a></td>
<td align="center"><a href="https://github.com/fyZheng07"><img src="https://avatars.githubusercontent.com/u/63830919?v=4?s=50" width="50px;" alt=""/><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>
<td align="center"><a href="https://github.com/CJSS"><img src="https://avatars.githubusercontent.com/u/4605025?v=4?s=50" width="50px;" alt=""/><br /><sub><b>CJSS</b></sub></a><br /><a href="https://github.com/toeverything/AFFiNE/commits?author=CJSS" title="Documentation">📖</a></td>
</tr>
<tr>
<td align="center"><a href="https://github.com/JimmFly"><img src="https://avatars.githubusercontent.com/u/102217452?v=4?s=50" width="50px;" alt=""/><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"><a href="https://github.com/mitsuhatu"><img src="https://avatars.githubusercontent.com/u/110213079?v=4?s=50" width="50px;" alt=""/><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"><a href="https://shockwave.me/"><img src="https://avatars.githubusercontent.com/u/15013925?v=4?s=50" width="50px;" alt=""/><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"><a href="https://github.com/uptonking"><img src="https://avatars.githubusercontent.com/u/11391549?v=4?s=50" width="50px;" alt=""/><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"><a href="https://github.com/CarlosZoft"><img src="https://avatars.githubusercontent.com/u/62192072?v=4?s=50" width="50px;" alt=""/><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"><a href="https://github.com/caleboleary"><img src="https://avatars.githubusercontent.com/u/12816579?v=4?s=50" width="50px;" alt=""/><br /><sub><b>Caleb OLeary</b></sub></a><br /><a href="https://github.com/toeverything/AFFiNE/commits?author=caleboleary" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/westongraham"><img src="https://avatars.githubusercontent.com/u/89493023?v=4?s=50" width="50px;" alt=""/><br /><sub><b>Weston Graham</b></sub></a><br /><a href="https://github.com/toeverything/AFFiNE/commits?author=westongraham" title="Documentation">📖</a></td>
</tr>
<tr>
<td align="center"><a href="https://github.com/SaikaSakura"><img src="https://avatars.githubusercontent.com/u/11530942?v=4?s=50" width="50px;" alt=""/><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"><a href="https://github.com/fanjing22"><img src="https://avatars.githubusercontent.com/u/109729699?v=4?s=50" width="50px;" alt=""/><br /><sub><b>fanjing22</b></sub></a><br /><a href="#design-fanjing22" title="Design">🎨</a></td>
<td align="center"><a href="https://github.com/pointmax"><img src="https://avatars.githubusercontent.com/u/49361135?v=4?s=50" width="50px;" alt=""/><br /><sub><b>pointmax</b></sub></a><br /><a href="https://github.com/toeverything/AFFiNE/commits?author=pointmax" title="Documentation">📖</a> <a href="https://github.com/toeverything/AFFiNE/commits?author=pointmax" title="Code">💻</a></td>
<td align="center"><a href="https://liby.github.io/notes"><img src="https://avatars.githubusercontent.com/u/38807139?v=4?s=50" width="50px;" alt=""/><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"><a href="https://github.com/chenmoonmo"><img src="https://avatars.githubusercontent.com/u/36295999?v=4?s=50" width="50px;" alt=""/><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"><a href="https://github.com/githbq"><img src="https://avatars.githubusercontent.com/u/10009709?v=4?s=50" width="50px;" alt=""/><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"><a href="https://quavo.vercel.app/"><img src="https://avatars.githubusercontent.com/u/67266933?v=4?s=50" width="50px;" alt=""/><br /><sub><b>Quavo</b></sub></a><br /><a href="https://github.com/toeverything/AFFiNE/commits?author=lucky-chap" title="Documentation">📖</a> <a href="https://github.com/toeverything/AFFiNE/commits?author=lucky-chap" title="Code">💻</a></td>
</tr>
<tr>
<td align="center"><a href="https://github.com/LuciNyan"><img src="https://avatars.githubusercontent.com/u/22126563?v=4?s=50" width="50px;" alt=""/><br /><sub><b>子瞻 Luci</b></sub></a><br /><a href="https://github.com/toeverything/AFFiNE/commits?author=LuciNyan" title="Code">💻</a></td>
<td align="center"><a href="http://blog.ipili.me/"><img src="https://avatars.githubusercontent.com/u/4948120?v=4?s=50" width="50px;" alt=""/><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"><a href="https://segmentfault.com/u/qzuser_584786517d31a"><img src="https://avatars.githubusercontent.com/u/15103283?v=4?s=50" width="50px;" alt=""/><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"><a href="https://wangyu-1999.github.io/"><img src="https://avatars.githubusercontent.com/u/80874770?v=4?s=50" width="50px;" alt=""/><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"><a href="https://felixc.at/"><img src="https://avatars.githubusercontent.com/u/1006477?v=4?s=50" width="50px;" alt=""/><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"><a href="https://github.com/lynettelopez"><img src="https://avatars.githubusercontent.com/u/32908859?v=4?s=50" width="50px;" alt=""/><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"><a href="http://manjusaka.itscoder.com/"><img src="https://avatars.githubusercontent.com/u/7054676?v=4?s=50" width="50px;" alt=""/><br /><sub><b>Manjusaka</b></sub></a><br /><a href="https://github.com/toeverything/AFFiNE/commits?author=Zheaoli" title="Code">💻</a></td>
</tr>
<tr>
<td align="center"><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=""/><br /><sub><b>Frozen FIsh</b></sub></a><br /><a href="https://github.com/toeverything/AFFiNE/commits?author=sudongyuer" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/MuhammedFaraz"><img src="https://avatars.githubusercontent.com/u/92734739?v=4?s=50" width="50px;" alt=""/><br /><sub><b>Mohammed Faraz</b></sub></a><br /><a href="https://github.com/toeverything/AFFiNE/commits?author=MuhammedFaraz" title="Documentation">📖</a> <a href="https://github.com/toeverything/AFFiNE/commits?author=MuhammedFaraz" title="Code">💻</a></td>
<td align="center"><a href="https://pranavsriram.dev/"><img src="https://avatars.githubusercontent.com/u/28348429?v=4?s=50" width="50px;" alt=""/><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"><a href="https://github.com/Reson-a"><img src="https://avatars.githubusercontent.com/u/20806266?v=4?s=50" width="50px;" alt=""/><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"><a href="https://t.me/littlepoint"><img src="https://avatars.githubusercontent.com/u/7611700?v=4?s=50" width="50px;" alt=""/><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"><a href="https://akr.moe/"><img src="https://avatars.githubusercontent.com/u/85140972?v=4?s=50" width="50px;" alt=""/><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"><a href="https://github.com/suyanhanx"><img src="https://avatars.githubusercontent.com/u/24221472?v=4?s=50" width="50px;" alt=""/><br /><sub><b>Suyan</b></sub></a><br /><a href="https://github.com/toeverything/AFFiNE/commits?author=suyanhanx" title="Code">💻</a></td>
</tr>
<tr>
<td align="center"><a href="https://github.com/hehex9"><img src="https://avatars.githubusercontent.com/u/9209882?v=4?s=50" width="50px;" alt=""/><br /><sub><b>hehe</b></sub></a><br /><a href="https://github.com/toeverything/AFFiNE/commits?author=hehex9" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/albertodlc"><img src="https://avatars.githubusercontent.com/u/32411964?v=4?s=50" width="50px;" alt=""/><br /><sub><b>Alberto de la Cruz</b></sub></a><br /><a href="https://github.com/toeverything/AFFiNE/commits?author=albertodlc" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/AlessioGr"><img src="https://avatars.githubusercontent.com/u/70709113?v=4?s=50" width="50px;" alt=""/><br /><sub><b>Alessio Gravili</b></sub></a><br /><a href="https://github.com/toeverything/AFFiNE/commits?author=AlessioGr" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/lzlme"><img src="https://avatars.githubusercontent.com/u/117659326?v=4?s=50" width="50px;" alt=""/><br /><sub><b>Zhilin Liu</b></sub></a><br /><a href="https://github.com/toeverything/AFFiNE/commits?author=lzlme" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/suica"><img src="https://avatars.githubusercontent.com/u/8041462?v=4?s=50" width="50px;" alt=""/><br /><sub><b>Sg</b></sub></a><br /><a href="https://github.com/toeverything/AFFiNE/commits?author=suica" title="Code">💻</a></td>
<td align="center"><a href="https://sinchang.me/"><img src="https://avatars.githubusercontent.com/u/3297859?v=4?s=50" width="50px;" alt=""/><br /><sub><b>Jeff Wen</b></sub></a><br /><a href="https://github.com/toeverything/AFFiNE/commits?author=sinchang" title="Code">💻</a></td>
<td align="center"><a href="https://m1212e.github.io/portfolio/"><img src="https://avatars.githubusercontent.com/u/14091540?v=4?s=50" width="50px;" alt=""/><br /><sub><b>m1212e</b></sub></a><br /><a href="https://github.com/toeverything/AFFiNE/commits?author=m1212e" title="Code">💻</a></td>
</tr>
<tr>
<td align="center"><a href="https://adityash1.github.io/"><img src="https://avatars.githubusercontent.com/u/65771169?v=4?s=50" width="50px;" alt=""/><br /><sub><b>Aditya Sharma</b></sub></a><br /><a href="https://github.com/toeverything/AFFiNE/commits?author=adityash1" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/sheben404"><img src="https://avatars.githubusercontent.com/u/61317160?v=4?s=50" width="50px;" alt=""/><br /><sub><b>Kehan Wang</b></sub></a><br /><a href="https://github.com/toeverything/AFFiNE/commits?author=sheben404" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/VictorNanka"><img src="https://avatars.githubusercontent.com/u/30154366?v=4?s=50" width="50px;" alt=""/><br /><sub><b>VictorNanka</b></sub></a><br /><a href="https://github.com/toeverything/AFFiNE/commits?author=VictorNanka" title="Code">💻</a></td>
</tr>
</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 alt="contributors" src="https://opencollective.com/affine/contributors.svg?width=890&button=false" />
</a>
## Self-Host
@@ -268,7 +197,7 @@ See [LICENSE] for details.
[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

@@ -1,5 +1,6 @@
*.autogen.*
dist
e2e-dist-*
resources/web-static

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

@@ -7,6 +7,7 @@ To run AFFiNE Desktop Client Application locally, run the following commands:
```sh
# in repo root
yarn install
yarn workspace @affine/native build
yarn dev
# in apps/electron
@@ -14,6 +15,8 @@ yarn generate-assets
yarn dev # or yarn prod for production build
```
## Troubleshooting
## Credits
Most of the boilerplate code is generously borrowed from the following

View File

@@ -0,0 +1,4 @@
owner: toeverything
repo: AFFiNE
provider: github
private: false

View File

@@ -1,27 +1,41 @@
/* eslint-disable @typescript-eslint/no-var-requires */
const { z } = require('zod');
const {
utils: { fromBuildIdentifier },
} = require('@electron-forge/core');
const isCanary = process.env.BUILD_TYPE === 'canary';
const path = require('node:path');
const productName = isCanary ? 'AFFiNE-Canary' : 'AFFiNE';
const icoPath = isCanary
? './resources/icons/icon_canary.ico'
const ReleaseTypeSchema = z.enum(['stable', 'beta', 'canary', 'internal']);
const envBuildType = (process.env.BUILD_TYPE || 'canary').trim().toLowerCase();
const buildType = ReleaseTypeSchema.parse(envBuildType);
const stableBuild = buildType === 'stable';
const productName = !stableBuild ? `AFFiNE-${buildType}` : 'AFFiNE';
const icoPath = !stableBuild
? `./resources/icons/icon_${buildType}.ico`
: './resources/icons/icon.ico';
const icnsPath = isCanary
? './resources/icons/icon_canary.icns'
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: isCanary ? 'canary' : 'stable',
buildIdentifier: buildType,
packagerConfig: {
name: productName,
appBundleId: fromBuildIdentifier({
internal: 'pro.affine.internal',
canary: 'pro.affine.canary',
beta: 'pro.affine.beta',
stable: 'pro.affine.app',
}),
icon: icnsPath,
@@ -37,6 +51,8 @@ module.exports = {
teamId: process.env.APPLE_TEAM_ID,
}
: undefined,
// do we need the following line?
extraResource: ['./resources/app-update.yml'],
},
makers: [
{
@@ -45,6 +61,23 @@ module.exports = {
format: 'ULFO',
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' },
],
},
},
{
@@ -61,7 +94,7 @@ module.exports = {
config: {
name: 'AFFiNE',
setupIcon: icoPath,
// loadingGif: './resources/icons/loading.gif',
loadingGif: './resources/icons/affine_installing.gif',
},
},
],
@@ -72,8 +105,14 @@ module.exports = {
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

View File

@@ -1,23 +0,0 @@
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

@@ -1,69 +0,0 @@
import * as os from 'node:os';
import path from 'node:path';
import { app, shell } from 'electron';
import { BrowserWindow, ipcMain, nativeTheme } from 'electron';
import fs from 'fs-extra';
import { parse } from 'url';
import { isMacOS } from '../../../utils';
import { getExchangeTokenParams, oauthEndpoint } from './google-auth';
const AFFINE_ROOT = path.join(os.homedir(), '.affine');
fs.ensureDirSync(AFFINE_ROOT);
const logger = console;
export const registerHandlers = () => {
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: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;
});
};

View File

@@ -1,75 +0,0 @@
import './security-restrictions';
import { app } from 'electron';
import path from 'path';
import { registerHandlers } from './app-state';
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) {
app.quit();
process.exit(0);
}
app.on('second-instance', (event, argv) => {
restoreOrCreateWindow();
});
app.on('open-url', async (_, url) => {
// todo: handle `affine://...` urls
});
/**
* Disable Hardware Acceleration for more power-save
*/
app.disableHardwareAcceleration();
/**
* Shout down background process if all windows was closed
*/
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit();
}
});
/**
* @see https://www.electronjs.org/docs/v14-x-y/api/app#event-activate-macos Event: 'activate'
*/
app.on('activate', restoreOrCreateWindow);
/**
* Create app window when background process will be ready
*/
app
.whenReady()
.then(registerProtocol)
.then(registerHandlers)
.then(restoreOrCreateWindow)
.catch(e => console.error('Failed create window:', e));
/**
* Check new app version in production mode only
*/
// FIXME: add me back later
// if (import.meta.env.PROD) {
// app
// .whenReady()
// .then(() => import('electron-updater'))
// .then(({ autoUpdater }) => autoUpdater.checkForUpdatesAndNotify())
// .catch(e => console.error('Failed check updates:', e));
// }

View File

@@ -1,15 +0,0 @@
{
"compilerOptions": {
"module": "esnext",
"target": "esnext",
"sourceMap": false,
"moduleResolution": "Node",
"skipLibCheck": true,
"strict": true,
"isolatedModules": true,
"allowSyntheticDefaultImports": true,
"types": ["node"]
},
"include": ["src/**/*.ts", "../../types/**/*.d.ts", "index.ts", "../utils.ts"]
}

View File

@@ -1,12 +0,0 @@
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>; getGoogleOauthCode: () => Promise<{ requestInit: RequestInit; url: string; }>; updateEnv: (env: string, value: string) => void; };
readonly appInfo: { electron: boolean; isMacOS: boolean; };
}

View File

@@ -1,51 +0,0 @@
/**
* @module preload
*/
import { contextBridge, ipcRenderer } from 'electron';
import { isMacOS } from '../../utils';
/**
* The "Main World" is the JavaScript context that your main renderer code runs in.
* By default, the page you load in your renderer executes code in this world.
*
* @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),
/**
* 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);
},
});
contextBridge.exposeInMainWorld('appInfo', {
electron: true,
isMacOS: isMacOS(),
});

View File

@@ -1,14 +0,0 @@
{
"compilerOptions": {
"module": "esnext",
"target": "esnext",
"sourceMap": false,
"moduleResolution": "Node",
"skipLibCheck": true,
"strict": true,
"isolatedModules": true,
"types": ["node"]
},
"include": ["src/**/*.ts", "../../types/**/*.d.ts"]
}

View File

@@ -1,3 +0,0 @@
export const isMacOS = () => {
return process.platform === 'darwin';
};

View File

@@ -1,30 +1,34 @@
{
"name": "@affine/electron",
"private": true,
"version": "0.5.3",
"version": "0.7.0-canary.13",
"author": "affine",
"repository": {
"url": "https://github.com/toeverything/AFFiNE",
"type": "git"
},
"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 cross-env DEV_SERVER_URL=http://localhost:8080 node scripts/dev.mjs",
"watch": "yarn cross-env DEV_SERVER_URL=http://localhost:8080 node scripts/dev.mjs --watch",
"prod": "yarn node scripts/dev.mjs",
"build": "zx scripts/build-layers.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",
"postinstall": "ELECTRON_RUN_AS_NODE=1 electron scripts/update-electron-vendors.mjs"
"test": "DEBUG=pw:browser playwright test"
},
"config": {
"forge": "./forge.config.js"
},
"main": "./dist/layers/main/index.js",
"main": "./dist/main.js",
"exports": {
"./scripts/plugins/build-plugins.mjs": "./scripts/plugins/build-plugins.mjs"
},
"devDependencies": {
"@affine-test/kit": "workspace:*",
"@affine/native": "workspace:*",
"@electron-forge/cli": "^6.1.1",
"@electron-forge/core": "^6.1.1",
"@electron-forge/core-utils": "^6.1.1",
@@ -33,20 +37,32 @@
"@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.12",
"@electron/remote": "2.0.9",
"dts-for-context-bridge": "^0.7.1",
"electron": "24.1.2",
"@toeverything/infra": "workspace:*",
"@types/fs-extra": "^11.0.1",
"@types/uuid": "^9.0.1",
"cross-env": "7.0.3",
"electron": "=25.0.1",
"electron-log": "^5.0.0-beta.24",
"electron-squirrel-startup": "1.0.0",
"esbuild": "^0.17.17",
"zx": "^7.2.1"
"electron-window-state": "^5.0.3",
"esbuild": "^0.17.19",
"fs-extra": "^11.1.1",
"playwright": "=1.33.0",
"ts-node": "^10.9.1",
"undici": "^5.22.1",
"uuid": "^9.0.0",
"zx": "^7.2.2"
},
"dependencies": {
"cross-env": "7.0.3",
"electron-window-state": "^5.0.3",
"firebase": "^9.19.1",
"fs-extra": "^11.1.1",
"undici": "^5.21.2"
"@toeverything/plugin-infra": "workspace:*",
"async-call-rpc": "^6.3.1",
"cheerio": "^1.0.0-rc.12",
"electron-updater": "^5.3.0",
"lodash-es": "^4.17.21",
"nanoid": "^4.0.2",
"rxjs": "^7.8.1",
"yjs": "^13.6.1"
},
"build": {
"protocols": [
@@ -58,5 +74,12 @@
}
]
},
"packageManager": "yarn@3.5.0"
"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;

View File

@@ -0,0 +1,4 @@
owner: toeverything
repo: AFFiNE
provider: github
private: false

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

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: 43 KiB

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 229 KiB

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.

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

View File

@@ -0,0 +1,42 @@
#!/usr/bin/env zx
import 'zx/globals';
import { resolve } from 'node:path';
import { spawnSync } from 'child_process';
import * as esbuild from 'esbuild';
import { config, rootDir } from './common.mjs';
const NODE_ENV =
process.env.NODE_ENV === 'development' ? 'development' : 'production';
if (process.platform === 'win32') {
$.shell = true;
$.prefix = '';
}
async function buildLayers() {
const common = config();
console.log('Build plugin infra');
spawnSync('yarn', ['build'], {
stdio: 'inherit',
cwd: resolve(rootDir, './packages/plugin-infra'),
});
console.log('Build plugins');
await import('./plugins/build-plugins.mjs');
await esbuild.build(common.workers);
await esbuild.build({
...common.layers,
define: {
...common.define,
'process.env.NODE_ENV': `"${NODE_ENV}"`,
'process.env.BUILD_TYPE': `"${process.env.BUILD_TYPE || 'stable'}"`,
},
});
}
await buildLayers();
echo('Build layers done');

View File

@@ -1,54 +1,76 @@
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 { node } = JSON.parse(
fs.readFileSync(
path.join(__dirname, '../electron-vendors.autogen.json'),
'utf-8'
)
);
import { resolve } from 'node:path';
const nativeNodeModulesPlugin = {
name: 'native-node-modules',
setup(build) {
// Mark native Node.js modules as external
build.onResolve({ filter: /\.node$/, namespace: 'file' }, args => {
return { path: args.path, external: true };
});
},
};
import { fileURLToPath } from 'url';
export const electronDir = fileURLToPath(new URL('..', import.meta.url));
export const rootDir = resolve(electronDir, '..', '..');
export const NODE_MAJOR_VERSION = 18;
// hard-coded for now:
// fixme(xp): report error if app is not running on DEV_SERVER_URL
const DEV_SERVER_URL = process.env.DEV_SERVER_URL;
/** @type 'production' | 'development'' */
const mode = (process.env.NODE_ENV = process.env.NODE_ENV || 'development');
// List of env that will be replaced by esbuild
const ENV_MACROS = ['AFFINE_GOOGLE_CLIENT_ID', 'AFFINE_GOOGLE_CLIENT_SECRET'];
/** @return {{main: import('esbuild').BuildOptions, preload: import('esbuild').BuildOptions}} */
export default () => {
const define = Object.fromEntries(
ENV_MACROS.map(key => [
/** @return {{layers: import('esbuild').BuildOptions, workers: import('esbuild').BuildOptions}} */
export const config = () => {
const define = Object.fromEntries([
...ENV_MACROS.map(key => [
'process.env.' + key,
JSON.stringify(process.env[key] ?? ''),
])
);
]),
['process.env.NODE_ENV', `"${mode}"`],
['process.env.USE_WORKER', '"true"'],
]);
if (DEV_SERVER_URL) {
define['process.env.DEV_SERVER_URL'] = `"${DEV_SERVER_URL}"`;
}
return {
main: {
entryPoints: ['layers/main/src/index.ts'],
outdir: 'dist/layers/main',
layers: {
entryPoints: [
resolve(electronDir, './src/main/index.ts'),
resolve(electronDir, './src/preload/index.ts'),
resolve(electronDir, './src/helper/index.ts'),
],
entryNames: '[dir]',
outdir: resolve(electronDir, './dist'),
bundle: true,
target: `node${node}`,
target: `node${NODE_MAJOR_VERSION}`,
platform: 'node',
external: ['electron'],
plugins: [nativeNodeModulesPlugin],
external: ['electron', 'electron-updater', '@toeverything/plugin-infra'],
define: define,
format: 'cjs',
loader: {
'.node': 'copy',
},
assetNames: '[name]',
treeShaking: true,
},
preload: {
entryPoints: ['layers/preload/src/index.ts'],
outdir: 'dist/layers/preload',
workers: {
entryPoints: [
resolve(electronDir, './src/main/workers/plugin.worker.ts'),
],
entryNames: '[dir]/[name]',
outdir: resolve(electronDir, './dist/workers'),
bundle: true,
target: `node${node}`,
target: `node${NODE_MAJOR_VERSION}`,
platform: 'node',
external: ['electron'],
external: ['electron', 'electron-updater', '@toeverything/plugin-infra'],
define: define,
format: 'cjs',
loader: {
'.node': 'copy',
},
assetNames: '[name]',
treeShaking: true,
},
};
};

View File

@@ -1,19 +1,15 @@
/* eslint-disable no-async-promise-executor */
import { spawn } from 'node:child_process';
import { readFileSync } from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import path, { resolve } from 'node:path';
import { generateAsync } from 'dts-for-context-bridge';
import electronPath from 'electron';
import * as esbuild from 'esbuild';
import commonFn from './common.mjs';
import { config, electronDir, rootDir } from './common.mjs';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
/** @type 'production' | 'development'' */
const mode = (process.env.NODE_ENV = process.env.NODE_ENV || 'development');
// this means we don't spawn electron windows, mainly for testing
const watchMode = process.argv.includes('--watch');
/** Messages on stderr that match any of the contained patterns will be stripped from output */
const stderrFilterPatterns = [
@@ -23,9 +19,12 @@ const stderrFilterPatterns = [
/ExtensionLoadWarning/,
];
// these are set before calling commonFn so we have a chance to override them
// these are set before calling `config`, so we have a chance to override them
try {
const devJson = readFileSync(path.resolve(__dirname, '../dev.json'), 'utf-8');
const devJson = readFileSync(
path.resolve(electronDir, './dev.json'),
'utf-8'
);
const devEnv = JSON.parse(devJson);
Object.assign(process.env, devEnv);
} catch (err) {
@@ -34,14 +33,13 @@ try {
);
}
// hard-coded for now:
// fixme(xp): report error if app is not running on port 8080
process.env.DEV_SERVER_URL = `http://localhost:8080`;
/** @type {ChildProcessWithoutNullStreams | null} */
let spawnProcess = null;
function spawnOrReloadElectron() {
if (watchMode) {
return;
}
if (spawnProcess !== null) {
spawnProcess.off('exit', process.exit);
spawnProcess.kill('SIGINT');
@@ -50,10 +48,12 @@ function spawnOrReloadElectron() {
spawnProcess = spawn(String(electronPath), ['.']);
spawnProcess.stdout.on(
'data',
d => d.toString().trim() && console.warn(d.toString())
);
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;
@@ -62,76 +62,87 @@ function spawnOrReloadElectron() {
console.error(data);
});
// Stops the watch script when the application has been quit
// Stops the watch script when the application has quit
spawnProcess.on('exit', process.exit);
}
const common = commonFn();
const common = config();
async function main() {
async function watchPreload(onInitialBuild) {
const preloadBuild = await esbuild.context({
...common.preload,
async function watchPlugins() {
spawn('yarn', ['dev'], {
stdio: 'inherit',
cwd: resolve(rootDir, './packages/plugin-infra'),
});
await import('./plugins/dev-plugins.mjs');
}
async function watchLayers() {
return new Promise(async resolve => {
let initialBuild = false;
const buildContext = await esbuild.context({
...common.layers,
plugins: [
...(common.preload.plugins ?? []),
...(common.layers.plugins ?? []),
{
name: 'affine-dev:reload-app-on-preload-change',
name: 'electron-dev:reload-app-on-layers-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`);
console.log(`[layers] has changed, [re]launching electron...`);
spawnOrReloadElectron();
} else {
resolve();
initialBuild = true;
onInitialBuild();
}
});
},
},
],
});
await preloadBuild.watch();
}
async function watchMain() {
const mainBuild = await esbuild.context({
...common.main,
define: {
...common.main.define,
'process.env.NODE_ENV': `"${mode}"`,
'process.env.DEV_SERVER_URL': `"${process.env.DEV_SERVER_URL}"`,
},
plugins: [
...(common.main.plugins ?? []),
{
name: 'affine-dev:reload-app-on-main-change',
setup(build) {
let initialBuild = false;
build.onEnd(() => {
if (initialBuild) {
console.log(`[main] has changed, [re]launching electron...`);
} else {
initialBuild = true;
}
spawnOrReloadElectron();
});
},
},
],
});
await mainBuild.watch();
}
await watchPreload(async () => {
await watchMain();
spawnOrReloadElectron();
console.log(`Electron is started, watching for changes...`);
await buildContext.watch();
});
}
async function watchWorkers() {
return new Promise(async resolve => {
let initialBuild = false;
const buildContext = await esbuild.context({
...common.workers,
plugins: [
...(common.workers.plugins ?? []),
{
name: 'electron-dev:reload-app-on-workers-change',
setup(build) {
build.onEnd(() => {
if (initialBuild) {
console.log(`[workers] has changed, [re]launching electron...`);
spawnOrReloadElectron();
} else {
resolve();
initialBuild = true;
}
});
},
},
],
});
await buildContext.watch();
});
}
async function main() {
await watchPlugins();
await watchLayers();
await watchWorkers();
if (watchMode) {
console.log(`Watching for changes...`);
} else {
spawnOrReloadElectron();
console.log(`Electron is started, watching for changes...`);
}
}
main();

86
apps/electron/scripts/generate-assets.mjs Normal file → Executable file
View File

@@ -1,11 +1,10 @@
#!/usr/bin/env zx
import 'zx/globals';
import { createRequire } from 'node:module';
import path from 'node:path';
import * as esbuild from 'esbuild';
import commonFn from './common.mjs';
const require = createRequire(import.meta.url);
const repoRootDir = path.join(__dirname, '..', '..', '..');
const electronRootDir = path.join(__dirname, '..');
@@ -13,6 +12,7 @@ const publicDistDir = path.join(electronRootDir, 'resources');
const affineWebDir = path.join(repoRootDir, 'apps', 'web');
const affineWebOutDir = path.join(affineWebDir, 'out');
const publicAffineOutDir = path.join(publicDistDir, `web-static`);
const releaseVersionEnv = process.env.RELEASE_VERSION || '';
console.log('build with following dir', {
repoRootDir,
@@ -23,9 +23,16 @@ console.log('build with following dir', {
publicAffineOutDir,
});
// step 0: check version match
const electronPackageJson = require(`${electronRootDir}/package.json`);
if (releaseVersionEnv && electronPackageJson.version !== releaseVersionEnv) {
throw new Error(
`Version mismatch, expected ${releaseVersionEnv} but got ${electronPackageJson.version}`
);
}
// copy web dist files to electron dist
// step 0: clean up
// step 1: clean up
await cleanup();
echo('Clean up done');
@@ -33,52 +40,51 @@ if (process.platform === 'win32') {
$.shell = 'powershell.exe';
$.prefix = '';
}
// step 1: build web (nextjs) dist
cd(repoRootDir);
await $`yarn add`;
await $`yarn build`;
await $`yarn export`;
// 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`;
// step 2: build web (nextjs) dist
if (!process.env.SKIP_WEB_BUILD) {
process.env.ENABLE_LEGACY_PROVIDER = 'false';
await $`yarn nx build @affine/web`;
await $`yarn nx export @affine/web`;
// 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.writeFile(fullpath, content);
});
});
await fs.move(affineWebOutDir, publicAffineOutDir, { overwrite: true });
await fs.move(affineWebOutDir, publicAffineOutDir, { overwrite: true });
}
// step 2: build electron resources
await buildLayers();
echo('Build layers done');
// step 3: update app-updater.yml content with build type in resources folder
if (process.env.BUILD_TYPE === 'internal') {
const appUpdaterYml = path.join(publicDistDir, 'app-update.yml');
const appUpdaterYmlContent = await fs.readFile(appUpdaterYml, 'utf-8');
const newAppUpdaterYmlContent = appUpdaterYmlContent.replace(
'AFFiNE',
'AFFiNE-Releases'
);
await fs.writeFile(appUpdaterYml, newAppUpdaterYmlContent);
}
/// --------
/// --------
/// --------
async function cleanup() {
await fs.emptyDir(publicAffineOutDir);
await fs.emptyDir(path.join(electronRootDir, 'layers', 'main', 'dist'));
await fs.emptyDir(path.join(electronRootDir, 'layers', 'preload', 'dist'));
if (!process.env.SKIP_WEB_BUILD) {
await fs.emptyDir(publicAffineOutDir);
}
await fs.remove(path.join(electronRootDir, 'dist'));
await fs.remove(path.join(electronRootDir, 'out'));
}
async function buildLayers() {
const common = commonFn();
await esbuild.build(common.preload);
await esbuild.build({
...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

@@ -0,0 +1,20 @@
#!/usr/bin/env node
import { build } from 'esbuild';
import { definePluginServerConfig } from './utils.mjs';
await build({
...definePluginServerConfig('bookmark-block'),
external: [
// server.ts
'link-preview-js',
// ui.ts
'@toeverything/plugin-infra',
'@affine/component',
'@blocksuite/store',
'@blocksuite/blocks',
'react',
'react-dom',
'foxact',
],
});

View File

@@ -0,0 +1,22 @@
#!/usr/bin/env node
import { context } from 'esbuild';
import { definePluginServerConfig } from './utils.mjs';
const plugin = await context({
...definePluginServerConfig('bookmark-block'),
external: [
// server.ts
'link-preview-js',
// ui.ts
'@toeverything/plugin-infra',
'@affine/component',
'@blocksuite/store',
'@blocksuite/blocks',
'react',
'react-dom',
'foxact',
],
});
await plugin.watch();

View File

@@ -0,0 +1,34 @@
import { resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
export const rootDir = fileURLToPath(new URL('../../../..', import.meta.url));
export const electronOutputDir = resolve(
rootDir,
'apps',
'electron',
'dist',
'plugins'
);
export const pluginDir = resolve(rootDir, 'plugins');
/**
*
* @param pluginDirName {string}
* @return {import('esbuild').BuildOptions}
*/
export function definePluginServerConfig(pluginDirName) {
const pluginRootDir = resolve(pluginDir, pluginDirName);
const mainEntryFile = resolve(pluginRootDir, 'src/index.ts');
const serverOutputDir = resolve(electronOutputDir, pluginDirName);
return {
entryPoints: [mainEntryFile],
platform: 'neutral',
format: 'esm',
outExtension: {
'.js': '.mjs',
},
outdir: serverOutputDir,
bundle: true,
splitting: true,
};
}

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 @@
tmp

View File

@@ -0,0 +1,134 @@
import path from 'node:path';
import { setTimeout } from 'node:timers/promises';
import fs from 'fs-extra';
import { v4 } from 'uuid';
import { afterEach, beforeEach, expect, test, vi } from 'vitest';
const tmpDir = path.join(__dirname, 'tmp');
const appDataPath = path.join(tmpDir, 'app-data');
vi.doMock('../../main-rpc', () => ({
mainRPC: {
getPath: async () => appDataPath,
},
}));
const constructorStub = vi.fn();
const destroyStub = vi.fn();
destroyStub.mockReturnValue(Promise.resolve());
function existProcess() {
process.emit('beforeExit', 0);
}
vi.doMock('../secondary-db', () => {
return {
SecondaryWorkspaceSQLiteDB: class {
constructor(...args: any[]) {
constructorStub(...args);
}
connectIfNeeded = () => Promise.resolve();
pull = () => Promise.resolve();
destroy = destroyStub;
},
};
});
beforeEach(() => {
vi.useFakeTimers({ shouldAdvanceTime: true });
});
afterEach(async () => {
existProcess();
// wait for the db to be closed on Windows
if (process.platform === 'win32') {
await setTimeout(200);
}
await fs.remove(tmpDir);
vi.useRealTimers();
});
test('can get a valid WorkspaceSQLiteDB', async () => {
const { ensureSQLiteDB } = await import('../ensure-db');
const workspaceId = v4();
const db0 = await ensureSQLiteDB(workspaceId);
expect(db0).toBeDefined();
expect(db0.workspaceId).toBe(workspaceId);
const db1 = await ensureSQLiteDB(v4());
expect(db1).not.toBe(db0);
expect(db1.workspaceId).not.toBe(db0.workspaceId);
// ensure that the db is cached
expect(await ensureSQLiteDB(workspaceId)).toBe(db0);
});
test('db should be destroyed when app quits', async () => {
const { ensureSQLiteDB } = await import('../ensure-db');
const workspaceId = v4();
const db0 = await ensureSQLiteDB(workspaceId);
const db1 = await ensureSQLiteDB(v4());
expect(db0.db).not.toBeNull();
expect(db1.db).not.toBeNull();
existProcess();
// wait the async `db.destroy()` to be called
await setTimeout(100);
expect(db0.db).toBeNull();
expect(db1.db).toBeNull();
});
test('db should be removed in db$Map after destroyed', async () => {
const { ensureSQLiteDB, db$Map } = await import('../ensure-db');
const workspaceId = v4();
const db = await ensureSQLiteDB(workspaceId);
await db.destroy();
await setTimeout(100);
expect(db$Map.has(workspaceId)).toBe(false);
});
test('if db has a secondary db path, we should also poll that', async () => {
const { ensureSQLiteDB } = await import('../ensure-db');
const { storeWorkspaceMeta } = await import('../../workspace');
const workspaceId = v4();
await storeWorkspaceMeta(workspaceId, {
secondaryDBPath: path.join(tmpDir, 'secondary.db'),
});
const db = await ensureSQLiteDB(workspaceId);
await setTimeout(10);
expect(constructorStub).toBeCalledTimes(1);
expect(constructorStub).toBeCalledWith(path.join(tmpDir, 'secondary.db'), db);
// if secondary meta is changed
await storeWorkspaceMeta(workspaceId, {
secondaryDBPath: path.join(tmpDir, 'secondary2.db'),
});
// wait the async `db.destroy()` to be called
await setTimeout(100);
expect(constructorStub).toBeCalledTimes(2);
expect(destroyStub).toBeCalledTimes(1);
// if secondary meta is changed (but another workspace)
await storeWorkspaceMeta(v4(), {
secondaryDBPath: path.join(tmpDir, 'secondary3.db'),
});
await vi.advanceTimersByTimeAsync(1500);
expect(constructorStub).toBeCalledTimes(2);
expect(destroyStub).toBeCalledTimes(1);
// if primary is destroyed, secondary should also be destroyed
await db.destroy();
await setTimeout(100);
expect(destroyStub).toBeCalledTimes(2);
});

View File

@@ -0,0 +1,99 @@
import path from 'node:path';
import fs from 'fs-extra';
import { v4 } from 'uuid';
import { afterEach, expect, test, vi } from 'vitest';
import * as Y from 'yjs';
import { dbSubjects } from '../subjects';
const tmpDir = path.join(__dirname, 'tmp');
const appDataPath = path.join(tmpDir, 'app-data');
vi.doMock('../../main-rpc', () => ({
mainRPC: {
getPath: async () => appDataPath,
},
}));
afterEach(async () => {
await fs.remove(tmpDir);
});
function getTestUpdates() {
const testYDoc = new Y.Doc();
const yText = testYDoc.getText('test');
yText.insert(0, 'hello');
const updates = Y.encodeStateAsUpdate(testYDoc);
return updates;
}
test('can create new db file if not exists', async () => {
const { openWorkspaceDatabase } = await import('../workspace-db-adapter');
const workspaceId = v4();
const db = await openWorkspaceDatabase(workspaceId);
const dbPath = path.join(
appDataPath,
`workspaces/${workspaceId}`,
`storage.db`
);
expect(await fs.exists(dbPath)).toBe(true);
await db.destroy();
});
test('on applyUpdate (from self), will not trigger update', async () => {
const { openWorkspaceDatabase } = await import('../workspace-db-adapter');
const workspaceId = v4();
const onUpdate = vi.fn();
const db = await openWorkspaceDatabase(workspaceId);
db.update$.subscribe(onUpdate);
db.applyUpdate(getTestUpdates(), 'self');
expect(onUpdate).not.toHaveBeenCalled();
await db.destroy();
});
test('on applyUpdate (from renderer), will trigger update', async () => {
const { openWorkspaceDatabase } = await import('../workspace-db-adapter');
const workspaceId = v4();
const onUpdate = vi.fn();
const onExternalUpdate = vi.fn();
const db = await openWorkspaceDatabase(workspaceId);
db.update$.subscribe(onUpdate);
const sub = dbSubjects.externalUpdate.subscribe(onExternalUpdate);
db.applyUpdate(getTestUpdates(), 'renderer');
expect(onUpdate).toHaveBeenCalled();
sub.unsubscribe();
await db.destroy();
});
test('on applyUpdate (from external), will trigger update & send external update event', async () => {
const { openWorkspaceDatabase } = await import('../workspace-db-adapter');
const workspaceId = v4();
const onUpdate = vi.fn();
const onExternalUpdate = vi.fn();
const db = await openWorkspaceDatabase(workspaceId);
db.update$.subscribe(onUpdate);
const sub = dbSubjects.externalUpdate.subscribe(onExternalUpdate);
db.applyUpdate(getTestUpdates(), 'external');
expect(onUpdate).toHaveBeenCalled();
expect(onExternalUpdate).toHaveBeenCalled();
sub.unsubscribe();
await db.destroy();
});
test('on destroy, check if resources have been released', async () => {
const { openWorkspaceDatabase } = await import('../workspace-db-adapter');
const workspaceId = v4();
const db = await openWorkspaceDatabase(workspaceId);
const updateSub = {
complete: vi.fn(),
next: vi.fn(),
};
db.update$ = updateSub as any;
await db.destroy();
expect(db.db).toBe(null);
expect(updateSub.complete).toHaveBeenCalled();
});

View File

@@ -0,0 +1,116 @@
import { SqliteConnection } from '@affine/native';
import { logger } from '../logger';
/**
* A base class for SQLite DB adapter that provides basic methods around updates & blobs
*/
export abstract class BaseSQLiteAdapter {
db: SqliteConnection | null = null;
abstract role: string;
constructor(public readonly path: string) {}
async connectIfNeeded() {
if (!this.db) {
this.db = new SqliteConnection(this.path);
await this.db.connect();
logger.info(`[SQLiteAdapter:${this.role}]`, 'connected:', this.path);
}
return this.db;
}
async destroy() {
const { db } = this;
this.db = null;
// log after close will sometimes crash the app when quitting
logger.info(`[SQLiteAdapter:${this.role}]`, 'destroyed:', this.path);
await db?.close();
}
async addBlob(key: string, data: Uint8Array) {
try {
if (!this.db) {
logger.warn(`${this.path} is not connected`);
return;
}
await this.db.addBlob(key, data);
} catch (error) {
logger.error('addBlob', error);
}
}
async getBlob(key: string) {
try {
if (!this.db) {
logger.warn(`${this.path} is not connected`);
return;
}
const blob = await this.db.getBlob(key);
return blob?.data;
} catch (error) {
logger.error('getBlob', error);
return null;
}
}
async deleteBlob(key: string) {
try {
if (!this.db) {
logger.warn(`${this.path} is not connected`);
return;
}
await this.db.deleteBlob(key);
} catch (error) {
logger.error(`${this.path} delete blob failed`, error);
}
}
async getBlobKeys() {
try {
if (!this.db) {
logger.warn(`${this.path} is not connected`);
return [];
}
return await this.db.getBlobKeys();
} catch (error) {
logger.error(`getBlobKeys failed`, error);
return [];
}
}
async getUpdates() {
try {
if (!this.db) {
logger.warn(`${this.path} is not connected`);
return [];
}
return await this.db.getUpdates();
} catch (error) {
logger.error('getUpdates', error);
return [];
}
}
// add a single update to SQLite
async addUpdateToSQLite(updates: Uint8Array[]) {
// batch write instead write per key stroke?
try {
if (!this.db) {
logger.warn(`${this.path} is not connected`);
return;
}
const start = performance.now();
await this.db.insertUpdates(updates);
logger.debug(
`[SQLiteAdapter][${this.role}] addUpdateToSQLite`,
'length:',
updates.length,
performance.now() - start,
'ms'
);
} catch (error) {
logger.error('addUpdateToSQLite', this.path, error);
}
}
}

View File

@@ -0,0 +1,140 @@
import type { Subject } from 'rxjs';
import { Observable } from 'rxjs';
import {
concat,
defer,
from,
fromEvent,
interval,
lastValueFrom,
merge,
} from 'rxjs';
import {
concatMap,
distinctUntilChanged,
filter,
ignoreElements,
last,
map,
shareReplay,
startWith,
switchMap,
take,
takeUntil,
tap,
} from 'rxjs/operators';
import { logger } from '../logger';
import { getWorkspaceMeta, workspaceSubjects } from '../workspace';
import { SecondaryWorkspaceSQLiteDB } from './secondary-db';
import type { WorkspaceSQLiteDB } from './workspace-db-adapter';
import { openWorkspaceDatabase } from './workspace-db-adapter';
// export for testing
export const db$Map = new Map<string, Observable<WorkspaceSQLiteDB>>();
// use defer to prevent `app` is undefined while running tests
const beforeQuit$ = defer(() => fromEvent(process, 'beforeExit'));
// return a stream that emit a single event when the subject completes
function completed<T>(subject: Subject<T>) {
return new Observable(subscriber => {
const sub = subject.subscribe({
complete: () => {
subscriber.next();
subscriber.complete();
},
});
return () => sub.unsubscribe();
});
}
function getWorkspaceDB$(id: string) {
if (!db$Map.has(id)) {
db$Map.set(
id,
from(openWorkspaceDatabase(id)).pipe(
tap({
next: db => {
logger.info(
'[ensureSQLiteDB] db connection established',
db.workspaceId
);
},
}),
switchMap(db =>
// takeUntil the polling stream, and then destroy the db
concat(
startPollingSecondaryDB(db).pipe(
ignoreElements(),
startWith(db),
takeUntil(merge(beforeQuit$, completed(db.update$))),
last(),
tap({
next() {
logger.info(
'[ensureSQLiteDB] polling secondary db complete',
db.workspaceId
);
},
})
),
defer(async () => {
try {
await db.destroy();
db$Map.delete(id);
return db;
} catch (err) {
logger.error('[ensureSQLiteDB] destroy db failed', err);
throw err;
}
})
).pipe(startWith(db))
),
shareReplay(1)
)
);
}
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return db$Map.get(id)!;
}
function startPollingSecondaryDB(db: WorkspaceSQLiteDB) {
return merge(
getWorkspaceMeta(db.workspaceId),
workspaceSubjects.meta.pipe(
map(({ meta }) => meta),
filter(meta => meta.id === db.workspaceId)
)
).pipe(
map(meta => meta?.secondaryDBPath),
filter((p): p is string => !!p),
distinctUntilChanged(),
switchMap(path => {
// on secondary db path change, destroy the old db and create a new one
const secondaryDB = new SecondaryWorkspaceSQLiteDB(path, db);
return new Observable<SecondaryWorkspaceSQLiteDB>(subscriber => {
subscriber.next(secondaryDB);
return () => secondaryDB.destroy();
});
}),
switchMap(secondaryDB => {
return interval(300000).pipe(
startWith(0),
concatMap(() => secondaryDB.pull()),
tap({
error: err => {
logger.error(`[ensureSQLiteDB] polling secondary db error`, err);
},
complete: () => {
logger.info('[ensureSQLiteDB] polling secondary db complete');
},
})
);
})
);
}
export function ensureSQLiteDB(id: string) {
return lastValueFrom(getWorkspaceDB$(id).pipe(take(1)));
}

View File

@@ -0,0 +1,48 @@
import { mainRPC } from '../main-rpc';
import type { MainEventRegister } from '../type';
import { ensureSQLiteDB } from './ensure-db';
import { dbSubjects } from './subjects';
export * from './ensure-db';
export * from './subjects';
export const dbHandlers = {
getDocAsUpdates: async (id: string) => {
const workspaceDB = await ensureSQLiteDB(id);
return workspaceDB.getDocAsUpdates();
},
applyDocUpdate: async (id: string, update: Uint8Array) => {
const workspaceDB = await ensureSQLiteDB(id);
return workspaceDB.applyUpdate(update);
},
addBlob: async (workspaceId: string, key: string, data: Uint8Array) => {
const workspaceDB = await ensureSQLiteDB(workspaceId);
return workspaceDB.addBlob(key, data);
},
getBlob: async (workspaceId: string, key: string) => {
const workspaceDB = await ensureSQLiteDB(workspaceId);
return workspaceDB.getBlob(key);
},
deleteBlob: async (workspaceId: string, key: string) => {
const workspaceDB = await ensureSQLiteDB(workspaceId);
return workspaceDB.deleteBlob(key);
},
getBlobKeys: async (workspaceId: string) => {
const workspaceDB = await ensureSQLiteDB(workspaceId);
return workspaceDB.getBlobKeys();
},
getDefaultStorageLocation: async () => {
return await mainRPC.getPath('sessionData');
},
};
export const dbEvents = {
onExternalUpdate: (
fn: (update: { workspaceId: string; update: Uint8Array }) => void
) => {
const sub = dbSubjects.externalUpdate.subscribe(fn);
return () => {
sub.unsubscribe();
};
},
} satisfies Record<string, MainEventRegister>;

View File

@@ -0,0 +1,11 @@
import * as Y from 'yjs';
export function mergeUpdate(updates: Uint8Array[]) {
const yDoc = new Y.Doc();
Y.transact(yDoc, () => {
for (const update of updates) {
Y.applyUpdate(yDoc, update);
}
});
return Y.encodeStateAsUpdate(yDoc);
}

View File

@@ -0,0 +1,215 @@
import assert from 'node:assert';
import type { SqliteConnection } from '@affine/native';
import { debounce } from 'lodash-es';
import * as Y from 'yjs';
import { logger } from '../logger';
import type { YOrigin } from '../type';
import { getWorkspaceMeta } from '../workspace';
import { BaseSQLiteAdapter } from './base-db-adapter';
import { mergeUpdate } from './merge-update';
import type { WorkspaceSQLiteDB } from './workspace-db-adapter';
const FLUSH_WAIT_TIME = 5000;
const FLUSH_MAX_WAIT_TIME = 10000;
export class SecondaryWorkspaceSQLiteDB extends BaseSQLiteAdapter {
role = 'secondary';
yDoc = new Y.Doc();
firstConnected = false;
destroyed = false;
updateQueue: Uint8Array[] = [];
unsubscribers = new Set<() => void>();
constructor(
public override path: string,
public upstream: WorkspaceSQLiteDB
) {
super(path);
this.setupAndListen();
logger.debug('[SecondaryWorkspaceSQLiteDB] created', this.workspaceId);
}
override async destroy() {
await this.flushUpdateQueue();
this.unsubscribers.forEach(unsub => unsub());
this.yDoc.destroy();
await super.destroy();
this.destroyed = true;
}
get workspaceId() {
return this.upstream.workspaceId;
}
// do not update db immediately, instead, push to a queue
// and flush the queue in a future time
async addUpdateToUpdateQueue(db: SqliteConnection, update: Uint8Array) {
this.updateQueue.push(update);
await this.debouncedFlush();
}
async flushUpdateQueue() {
if (this.destroyed) {
return;
}
logger.debug(
'flushUpdateQueue',
this.workspaceId,
'queue',
this.updateQueue.length
);
const updates = [...this.updateQueue];
this.updateQueue = [];
await this.run(async () => {
await this.addUpdateToSQLite(updates);
});
}
// flush after 5s, but will not wait for more than 10s
debouncedFlush = debounce(this.flushUpdateQueue, FLUSH_WAIT_TIME, {
maxWait: FLUSH_MAX_WAIT_TIME,
});
runCounter = 0;
// wrap the fn with connect and close
async run<T extends (...args: any[]) => any>(
fn: T
): Promise<
(T extends (...args: any[]) => infer U ? Awaited<U> : unknown) | undefined
> {
try {
if (this.destroyed) {
return;
}
await this.connectIfNeeded();
this.runCounter++;
return await fn();
} catch (err) {
logger.error(err);
throw err;
} finally {
this.runCounter--;
if (this.runCounter === 0) {
// just close db, but not the yDoc
await super.destroy();
}
}
}
setupAndListen() {
if (this.firstConnected) {
return;
}
this.firstConnected = true;
const onUpstreamUpdate = (update: Uint8Array, origin: YOrigin) => {
if (origin === 'renderer') {
// update to upstream yDoc should be replicated to self yDoc
this.applyUpdate(update, 'upstream');
}
};
const onSelfUpdate = async (update: Uint8Array, origin: YOrigin) => {
// for self update from upstream, we need to push it to external DB
if (origin === 'upstream' && this.db) {
await this.addUpdateToUpdateQueue(this.db, update);
}
if (origin === 'self') {
this.upstream.applyUpdate(update, 'external');
}
};
// listen to upstream update
this.upstream.yDoc.on('update', onUpstreamUpdate);
this.yDoc.on('update', onSelfUpdate);
this.unsubscribers.add(() => {
this.upstream.yDoc.off('update', onUpstreamUpdate);
this.yDoc.off('update', onSelfUpdate);
});
this.run(() => {
// apply all updates from upstream
const upstreamUpdate = this.upstream.getDocAsUpdates();
// to initialize the yDoc, we need to apply all updates from the db
this.applyUpdate(upstreamUpdate, 'upstream');
})
.then(() => {
logger.debug('run success');
})
.catch(err => {
logger.error('run error', err);
});
}
applyUpdate = (data: Uint8Array, origin: YOrigin = 'upstream') => {
Y.applyUpdate(this.yDoc, data, origin);
};
// TODO: have a better solution to handle blobs
async syncBlobs() {
await this.run(async () => {
// skip if upstream db is not connected (maybe it is already closed)
const blobsKeys = await this.getBlobKeys();
if (!this.upstream.db || this.upstream.db?.isClose) {
return;
}
const upstreamBlobsKeys = await this.upstream.getBlobKeys();
// put every missing blob to upstream
for (const key of blobsKeys) {
if (!upstreamBlobsKeys.includes(key)) {
const blob = await this.getBlob(key);
if (blob) {
await this.upstream.addBlob(key, blob);
logger.debug('syncBlobs', this.workspaceId, key);
}
}
}
});
}
/**
* pull from external DB file and apply to embedded yDoc
* workflow:
* - connect to external db
* - get updates
* - apply updates to local yDoc
* - get blobs and put new blobs to upstream
* - disconnect
*/
async pull() {
const start = performance.now();
assert(this.upstream.db, 'upstream db should be connected');
const updates = await this.run(async () => {
// TODO: no need to get all updates, just get the latest ones (using a cursor, etc)?
await this.syncBlobs();
return (await this.getUpdates()).map(update => update.data);
});
if (!updates || this.destroyed) {
return;
}
const merged = mergeUpdate(updates);
this.applyUpdate(merged, 'self');
logger.debug(
'pull external updates',
this.path,
updates.length,
(performance.now() - start).toFixed(2),
'ms'
);
}
}
export async function getSecondaryWorkspaceDBPath(workspaceId: string) {
const meta = await getWorkspaceMeta(workspaceId);
return meta?.secondaryDBPath;
}

View File

@@ -0,0 +1,5 @@
import { Subject } from 'rxjs';
export const dbSubjects = {
externalUpdate: new Subject<{ workspaceId: string; update: Uint8Array }>(),
};

View File

@@ -0,0 +1,102 @@
import { Subject } from 'rxjs';
import * as Y from 'yjs';
import { logger } from '../logger';
import type { YOrigin } from '../type';
import { getWorkspaceMeta } from '../workspace';
import { BaseSQLiteAdapter } from './base-db-adapter';
import { mergeUpdate } from './merge-update';
import { dbSubjects } from './subjects';
export class WorkspaceSQLiteDB extends BaseSQLiteAdapter {
role = 'primary';
yDoc = new Y.Doc();
firstConnected = false;
update$ = new Subject<void>();
constructor(public override path: string, public workspaceId: string) {
super(path);
}
override async destroy() {
await super.destroy();
this.yDoc.destroy();
// when db is closed, we can safely remove it from ensure-db list
this.update$.complete();
this.firstConnected = false;
}
getWorkspaceName = () => {
return this.yDoc.getMap('space:meta').get('name') as string;
};
async init() {
const db = await super.connectIfNeeded();
if (!this.firstConnected) {
this.yDoc.on('update', async (update: Uint8Array, origin: YOrigin) => {
if (origin === 'renderer') {
await this.addUpdateToSQLite([update]);
} else if (origin === 'external') {
dbSubjects.externalUpdate.next({
workspaceId: this.workspaceId,
update,
});
await this.addUpdateToSQLite([update]);
logger.debug('external update', this.workspaceId);
}
});
}
const updates = await this.getUpdates();
const merged = mergeUpdate(updates.map(update => update.data));
// to initialize the yDoc, we need to apply all updates from the db
this.applyUpdate(merged, 'self');
this.firstConnected = true;
this.update$.next();
return db;
}
getDocAsUpdates = () => {
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, origin: YOrigin = 'renderer') => {
// 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
Y.applyUpdate(this.yDoc, data, origin);
};
override async addBlob(key: string, value: Uint8Array) {
this.update$.next();
const res = await super.addBlob(key, value);
return res;
}
override async deleteBlob(key: string) {
this.update$.next();
await super.deleteBlob(key);
}
override async addUpdateToSQLite(data: Uint8Array[]) {
this.update$.next();
await super.addUpdateToSQLite(data);
}
}
export async function openWorkspaceDatabase(workspaceId: string) {
const meta = await getWorkspaceMeta(workspaceId);
const db = new WorkspaceSQLiteDB(meta.mainDBPath, workspaceId);
await db.init();
logger.info(`openWorkspaceDatabase [${workspaceId}]`);
return db;
}

View File

@@ -0,0 +1,341 @@
import path from 'node:path';
import fs from 'fs-extra';
import { nanoid } from 'nanoid';
import { ensureSQLiteDB } from '../db/ensure-db';
import type { WorkspaceSQLiteDB } from '../db/workspace-db-adapter';
import { logger } from '../logger';
import { mainRPC } from '../main-rpc';
import {
getWorkspaceDBPath,
getWorkspaceMeta,
getWorkspacesBasePath,
listWorkspaces,
storeWorkspaceMeta,
} from '../workspace';
// NOTE:
// we are using native dialogs because HTML dialogs do not give full file paths
export async function revealDBFile(workspaceId: string) {
const meta = await getWorkspaceMeta(workspaceId);
if (!meta) {
return;
}
await mainRPC.showItemInFolder(meta.secondaryDBPath ?? meta.mainDBPath);
}
// provide a backdoor to set dialog path for testing in playwright
export interface FakeDialogResult {
canceled?: boolean;
filePath?: string;
filePaths?: string[];
}
// result will be used in the next call to showOpenDialog
// if it is being read once, it will be reset to undefined
let fakeDialogResult: FakeDialogResult | undefined = undefined;
function getFakedResult() {
const result = fakeDialogResult;
fakeDialogResult = undefined;
return result;
}
export function setFakeDialogResult(result: FakeDialogResult | undefined) {
fakeDialogResult = result;
// for convenience, we will fill filePaths with filePath if it is not set
if (result?.filePaths === undefined && result?.filePath !== undefined) {
result.filePaths = [result.filePath];
}
}
const ErrorMessages = [
'DB_FILE_ALREADY_LOADED',
'DB_FILE_PATH_INVALID',
'DB_FILE_INVALID',
'FILE_ALREADY_EXISTS',
'UNKNOWN_ERROR',
] as const;
type ErrorMessage = (typeof ErrorMessages)[number];
export interface SaveDBFileResult {
filePath?: string;
canceled?: boolean;
error?: ErrorMessage;
}
const extension = 'affine';
function getDefaultDBFileName(name: string, id: string) {
const fileName = `${name}_${id}.${extension}`;
// make sure fileName is a valid file name
return fileName.replace(/[/\\?%*:|"<>]/g, '-');
}
/**
* This function is called when the user clicks the "Save" button in the "Save Workspace" dialog.
*
* It will just copy the file to the given path
*/
export async function saveDBFileAs(
workspaceId: string
): Promise<SaveDBFileResult> {
try {
const db = await ensureSQLiteDB(workspaceId);
const ret =
getFakedResult() ??
(await mainRPC.showSaveDialog({
properties: ['showOverwriteConfirmation'],
title: 'Save Workspace',
showsTagField: false,
buttonLabel: 'Save',
filters: [
{
extensions: [extension],
name: '',
},
],
defaultPath: getDefaultDBFileName(db.getWorkspaceName(), workspaceId),
message: 'Save Workspace as a SQLite Database file',
}));
const filePath = ret.filePath;
if (ret.canceled || !filePath) {
return {
canceled: true,
};
}
await fs.copyFile(db.path, filePath);
logger.log('saved', filePath);
mainRPC.showItemInFolder(filePath);
return { filePath };
} catch (err) {
logger.error('saveDBFileAs', err);
return {
error: 'UNKNOWN_ERROR',
};
}
}
export interface SelectDBFileLocationResult {
filePath?: string;
error?: ErrorMessage;
canceled?: boolean;
}
export async function selectDBFileLocation(): Promise<SelectDBFileLocationResult> {
try {
const ret =
getFakedResult() ??
(await mainRPC.showOpenDialog({
properties: ['openDirectory'],
title: 'Set Workspace Storage Location',
buttonLabel: 'Select',
defaultPath: await mainRPC.getPath('documents'),
message: "Select a location to store the workspace's database file",
}));
const dir = ret.filePaths?.[0];
if (ret.canceled || !dir) {
return {
canceled: true,
};
}
return { filePath: dir };
} catch (err) {
logger.error('selectDBFileLocation', err);
return {
error: (err as any).message,
};
}
}
export interface LoadDBFileResult {
workspaceId?: string;
error?: ErrorMessage;
canceled?: boolean;
}
/**
* This function is called when the user clicks the "Load" button in the "Load Workspace" dialog.
*
* It will
* - symlink the source db file to a new workspace id to app-data
* - return the new workspace id
*
* eg, it will create a new folder in app-data:
* <app-data>/<app-name>/workspaces/<workspace-id>/storage.db
*
* On the renderer side, after the UI got a new workspace id, it will
* update the local workspace id list and then connect to it.
*
*/
export async function loadDBFile(): Promise<LoadDBFileResult> {
try {
const ret =
getFakedResult() ??
(await mainRPC.showOpenDialog({
properties: ['openFile'],
title: 'Load Workspace',
buttonLabel: 'Load',
filters: [
{
name: 'SQLite Database',
// do we want to support other file format?
extensions: ['db', 'affine'],
},
],
message: 'Load Workspace from a AFFiNE file',
}));
const filePath = ret.filePaths?.[0];
if (ret.canceled || !filePath) {
logger.info('loadDBFile canceled');
return { canceled: true };
}
// the imported file should not be in app data dir
if (filePath.startsWith(await getWorkspacesBasePath())) {
logger.warn('loadDBFile: db file in app data dir');
return { error: 'DB_FILE_PATH_INVALID' };
}
if (await dbFileAlreadyLoaded(filePath)) {
logger.warn('loadDBFile: db file already loaded');
return { error: 'DB_FILE_ALREADY_LOADED' };
}
const { SqliteConnection } = await import('@affine/native');
if (!(await SqliteConnection.validate(filePath))) {
// TODO: report invalid db file error?
return { error: 'DB_FILE_INVALID' }; // invalid db file
}
// copy the db file to a new workspace id
const workspaceId = nanoid(10);
const internalFilePath = await getWorkspaceDBPath(workspaceId);
await fs.ensureDir(await getWorkspacesBasePath());
await fs.copy(filePath, internalFilePath);
logger.info(`loadDBFile, copy: ${filePath} -> ${internalFilePath}`);
await storeWorkspaceMeta(workspaceId, {
id: workspaceId,
mainDBPath: internalFilePath,
secondaryDBPath: filePath,
});
return { workspaceId };
} catch (err) {
logger.error('loadDBFile', err);
return {
error: 'UNKNOWN_ERROR',
};
}
}
export interface MoveDBFileResult {
filePath?: string;
error?: ErrorMessage;
canceled?: boolean;
}
/**
* This function is called when the user clicks the "Move" button in the "Move Workspace Storage" setting.
*
* It will
* - copy the source db file to a new location
* - remove the old db external file
* - update the external db file path in the workspace meta
* - return the new file path
*/
export async function moveDBFile(
workspaceId: string,
dbFileDir?: string
): Promise<MoveDBFileResult> {
let db: WorkspaceSQLiteDB | null = null;
try {
db = await ensureSQLiteDB(workspaceId);
const meta = await getWorkspaceMeta(workspaceId);
const oldDir = meta.secondaryDBPath
? path.dirname(meta.secondaryDBPath)
: null;
const defaultDir = oldDir ?? (await mainRPC.getPath('documents'));
const newName = getDefaultDBFileName(db.getWorkspaceName(), workspaceId);
const newDirPath =
dbFileDir ??
(
getFakedResult() ??
(await mainRPC.showOpenDialog({
properties: ['openDirectory'],
title: 'Move Workspace Storage',
buttonLabel: 'Move',
defaultPath: defaultDir,
message: 'Move Workspace storage file',
}))
).filePaths?.[0];
// skips if
// - user canceled the dialog
// - user selected the same dir
if (!newDirPath || newDirPath === oldDir) {
return {
canceled: true,
};
}
const newFilePath = path.join(newDirPath, newName);
if (await fs.pathExists(newFilePath)) {
return {
error: 'FILE_ALREADY_EXISTS',
};
}
logger.info(`[moveDBFile] copy ${meta.mainDBPath} -> ${newFilePath}`);
await fs.copy(meta.mainDBPath, newFilePath);
// remove the old db file, but we don't care if it fails
if (meta.secondaryDBPath) {
await fs
.remove(meta.secondaryDBPath)
.then(() => {
logger.info(`[moveDBFile] removed ${meta.secondaryDBPath}`);
})
.catch(err => {
logger.error(
`[moveDBFile] remove ${meta.secondaryDBPath} failed`,
err
);
});
}
// update meta
await storeWorkspaceMeta(workspaceId, {
secondaryDBPath: newFilePath,
});
return {
filePath: newFilePath,
};
} catch (err) {
await db?.destroy();
logger.error('[moveDBFile]', err);
return {
error: 'UNKNOWN_ERROR',
};
}
}
async function dbFileAlreadyLoaded(path: string) {
const meta = await listWorkspaces();
const paths = meta.map(m => m[1].secondaryDBPath);
return paths.includes(path);
}

View File

@@ -0,0 +1,31 @@
import {
loadDBFile,
moveDBFile,
revealDBFile,
saveDBFileAs,
selectDBFileLocation,
setFakeDialogResult,
} from './dialog';
export const dialogHandlers = {
revealDBFile: async (workspaceId: string) => {
return revealDBFile(workspaceId);
},
loadDBFile: async () => {
return loadDBFile();
},
saveDBFileAs: async (workspaceId: string) => {
return saveDBFileAs(workspaceId);
},
moveDBFile: (workspaceId: string, dbFileLocation?: string) => {
return moveDBFile(workspaceId, dbFileLocation);
},
selectDBFileLocation: async () => {
return selectDBFileLocation();
},
setFakeDialogResult: async (
result: Parameters<typeof setFakeDialogResult>[0]
) => {
return setFakeDialogResult(result);
},
};

View File

@@ -0,0 +1,33 @@
import { dbEvents, dbHandlers } from './db';
import { dialogHandlers } from './dialog';
import { workspaceEvents, workspaceHandlers } from './workspace';
export const handlers = {
db: dbHandlers,
workspace: workspaceHandlers,
dialog: dialogHandlers,
};
export const events = {
db: dbEvents,
workspace: workspaceEvents,
};
export const getExposedMeta = () => {
const handlersMeta = Object.entries(handlers).map(
([namespace, namespaceHandlers]) => {
return [namespace, Object.keys(namespaceHandlers)] as [string, string[]];
}
);
const eventsMeta = Object.entries(events).map(
([namespace, namespaceHandlers]) => {
return [namespace, Object.keys(namespaceHandlers)] as [string, string[]];
}
);
return {
handlers: handlersMeta,
events: eventsMeta,
};
};

View File

@@ -0,0 +1,86 @@
import type { EventBasedChannel } from 'async-call-rpc';
import { AsyncCall } from 'async-call-rpc';
import { events, handlers } from './exposed';
import { logger } from './logger';
const createMessagePortMainChannel = (
connection: Electron.MessagePortMain
): EventBasedChannel => {
return {
on(listener) {
const f = (e: Electron.MessageEvent) => {
listener(e.data);
};
connection.on('message', f);
// MUST start the connection to receive messages
connection.start();
return () => {
connection.off('message', f);
};
},
send(data) {
connection.postMessage(data);
},
};
};
function setupRendererConnection(rendererPort: Electron.MessagePortMain) {
const flattenedHandlers = Object.entries(handlers).flatMap(
([namespace, namespaceHandlers]) => {
return Object.entries(namespaceHandlers).map(([name, handler]) => {
const handlerWithLog = async (...args: any[]) => {
try {
const start = performance.now();
const result = await handler(...args);
logger.info(
'[async-api]',
`${namespace}.${name}`,
args.filter(
arg => typeof arg !== 'function' && typeof arg !== 'object'
),
'-',
(performance.now() - start).toFixed(2),
'ms'
);
return result;
} catch (error) {
logger.error('[async-api]', `${namespace}.${name}`, error);
}
};
return [`${namespace}:${name}`, handlerWithLog];
});
}
);
const rpc = AsyncCall<PeersAPIs.RendererToHelper>(
Object.fromEntries(flattenedHandlers),
{
channel: createMessagePortMainChannel(rendererPort),
log: false,
}
);
for (const [namespace, namespaceEvents] of Object.entries(events)) {
for (const [key, eventRegister] of Object.entries(namespaceEvents)) {
const subscription = eventRegister((...args: any[]) => {
const chan = `${namespace}:${key}`;
rpc.postEvent(chan, ...args);
});
process.on('exit', () => {
subscription();
});
}
}
}
function main() {
process.parentPort.on('message', e => {
if (e.data.channel === 'renderer-connect' && e.ports.length === 1) {
const rendererPort = e.ports[0];
setupRendererConnection(rendererPort);
logger.info('[helper] renderer connected');
}
});
}
main();

View File

@@ -0,0 +1,3 @@
import log from 'electron-log';
export const logger = log.scope('helper');

View File

@@ -0,0 +1,33 @@
import { AsyncCall, type EventBasedChannel } from 'async-call-rpc';
import { getExposedMeta } from './exposed';
function createMessagePortMainChannel(
connection: Electron.ParentPort
): EventBasedChannel {
return {
on(listener) {
const f = (e: Electron.MessageEvent) => {
listener(e.data);
};
connection.on('message', f);
return () => {
connection.off('message', f);
};
},
send(data) {
connection.postMessage(data);
},
};
}
const helperToMainServer: PeersAPIs.HelperToMain = {
getMeta: () => getExposedMeta(),
};
export const mainRPC = AsyncCall<PeersAPIs.MainToHelper>(helperToMainServer, {
strict: {
unknownMessage: false,
},
channel: createMessagePortMainChannel(process.parentPort),
});

View File

@@ -0,0 +1,9 @@
export interface WorkspaceMeta {
id: string;
mainDBPath: string;
secondaryDBPath?: string; // assume there will be only one
}
export type YOrigin = 'self' | 'external' | 'upstream' | 'renderer';
export type MainEventRegister = (...args: any[]) => () => void;

View File

@@ -0,0 +1 @@
tmp

View File

@@ -0,0 +1,142 @@
import path from 'node:path';
import fs from 'fs-extra';
import { v4 } from 'uuid';
import { afterEach, describe, expect, test, vi } from 'vitest';
const tmpDir = path.join(__dirname, 'tmp');
const appDataPath = path.join(tmpDir, 'app-data');
vi.doMock('../../db/ensure-db', () => ({
ensureSQLiteDB: async () => ({
destroy: () => {},
}),
}));
vi.doMock('../../main-rpc', () => ({
mainRPC: {
getPath: async () => appDataPath,
},
}));
afterEach(async () => {
await fs.remove(tmpDir);
});
describe('list workspaces', () => {
test('listWorkspaces (valid)', async () => {
const { listWorkspaces } = await import('../handlers');
const workspaceId = v4();
const workspacePath = path.join(appDataPath, 'workspaces', workspaceId);
const meta = {
id: workspaceId,
};
await fs.ensureDir(workspacePath);
await fs.writeJSON(path.join(workspacePath, 'meta.json'), meta);
const workspaces = await listWorkspaces();
expect(workspaces).toEqual([[workspaceId, meta]]);
});
test('listWorkspaces (without meta json file)', async () => {
const { listWorkspaces } = await import('../handlers');
const workspaceId = v4();
const workspacePath = path.join(appDataPath, 'workspaces', workspaceId);
await fs.ensureDir(workspacePath);
const workspaces = await listWorkspaces();
expect(workspaces).toEqual([
[
workspaceId,
// meta file will be created automatically
{ id: workspaceId, mainDBPath: path.join(workspacePath, 'storage.db') },
],
]);
});
});
describe('delete workspace', () => {
test('deleteWorkspace', async () => {
const { deleteWorkspace } = await import('../handlers');
const workspaceId = v4();
const workspacePath = path.join(appDataPath, 'workspaces', workspaceId);
await fs.ensureDir(workspacePath);
await deleteWorkspace(workspaceId);
expect(await fs.pathExists(workspacePath)).toBe(false);
// removed workspace will be moved to deleted-workspaces
expect(
await fs.pathExists(
path.join(appDataPath, 'deleted-workspaces', workspaceId)
)
).toBe(true);
});
});
describe('getWorkspaceMeta', () => {
test('can get meta', async () => {
const { getWorkspaceMeta } = await import('../handlers');
const workspaceId = v4();
const workspacePath = path.join(appDataPath, 'workspaces', workspaceId);
const meta = {
id: workspaceId,
};
await fs.ensureDir(workspacePath);
await fs.writeJSON(path.join(workspacePath, 'meta.json'), meta);
expect(await getWorkspaceMeta(workspaceId)).toEqual(meta);
});
test('can create meta if not exists', async () => {
const { getWorkspaceMeta } = await import('../handlers');
const workspaceId = v4();
const workspacePath = path.join(appDataPath, 'workspaces', workspaceId);
await fs.ensureDir(workspacePath);
expect(await getWorkspaceMeta(workspaceId)).toEqual({
id: workspaceId,
mainDBPath: path.join(workspacePath, 'storage.db'),
});
expect(
await fs.pathExists(path.join(workspacePath, 'meta.json'))
).toBeTruthy();
});
test('can migrate meta if db file is a link', async () => {
const { getWorkspaceMeta } = await import('../handlers');
const workspaceId = v4();
const workspacePath = path.join(appDataPath, 'workspaces', workspaceId);
await fs.ensureDir(workspacePath);
const sourcePath = path.join(tmpDir, 'source.db');
await fs.writeFile(sourcePath, 'test');
await fs.ensureSymlink(sourcePath, path.join(workspacePath, 'storage.db'));
expect(await getWorkspaceMeta(workspaceId)).toEqual({
id: workspaceId,
mainDBPath: path.join(workspacePath, 'storage.db'),
secondaryDBPath: sourcePath,
});
expect(
await fs.pathExists(path.join(workspacePath, 'meta.json'))
).toBeTruthy();
});
});
test('storeWorkspaceMeta', async () => {
const { storeWorkspaceMeta } = await import('../handlers');
const workspaceId = v4();
const workspacePath = path.join(appDataPath, 'workspaces', workspaceId);
await fs.ensureDir(workspacePath);
const meta = {
id: workspaceId,
mainDBPath: path.join(workspacePath, 'storage.db'),
};
await storeWorkspaceMeta(workspaceId, meta);
expect(await fs.readJSON(path.join(workspacePath, 'meta.json'))).toEqual(
meta
);
await storeWorkspaceMeta(workspaceId, {
secondaryDBPath: path.join(tmpDir, 'test.db'),
});
expect(await fs.readJSON(path.join(workspacePath, 'meta.json'))).toEqual({
...meta,
secondaryDBPath: path.join(tmpDir, 'test.db'),
});
});

View File

@@ -0,0 +1,143 @@
import path from 'node:path';
import fs from 'fs-extra';
import { ensureSQLiteDB } from '../db/ensure-db';
import { logger } from '../logger';
import { mainRPC } from '../main-rpc';
import type { WorkspaceMeta } from '../type';
import { workspaceSubjects } from './subjects';
let _appDataPath = '';
async function getAppDataPath() {
if (_appDataPath) {
return _appDataPath;
}
_appDataPath = await mainRPC.getPath('sessionData');
return _appDataPath;
}
export async function listWorkspaces(): Promise<
[workspaceId: string, meta: WorkspaceMeta][]
> {
const basePath = await getWorkspacesBasePath();
try {
await fs.ensureDir(basePath);
const dirs = (
await fs.readdir(basePath, {
withFileTypes: true,
})
).filter(d => d.isDirectory());
const metaList = (
await Promise.all(
dirs.map(async dir => {
// ? shall we put all meta in a single file instead of one file per workspace?
return await getWorkspaceMeta(dir.name);
})
)
).filter((w): w is WorkspaceMeta => !!w);
return metaList.map(meta => [meta.id, meta]);
} catch (error) {
logger.error('listWorkspaces', error);
return [];
}
}
export async function deleteWorkspace(id: string) {
const basePath = await getWorkspaceBasePath(id);
const movedPath = path.join(await getDeletedWorkspacesBasePath(), `${id}`);
try {
const db = await ensureSQLiteDB(id);
await db.destroy();
return await fs.move(basePath, movedPath, {
overwrite: true,
});
} catch (error) {
logger.error('deleteWorkspace', error);
}
}
export async function getWorkspacesBasePath() {
return path.join(await getAppDataPath(), 'workspaces');
}
export async function getWorkspaceBasePath(workspaceId: string) {
return path.join(await getAppDataPath(), 'workspaces', workspaceId);
}
async function getDeletedWorkspacesBasePath() {
return path.join(await getAppDataPath(), 'deleted-workspaces');
}
export async function getWorkspaceDBPath(workspaceId: string) {
return path.join(await getWorkspaceBasePath(workspaceId), 'storage.db');
}
export async function getWorkspaceMetaPath(workspaceId: string) {
return path.join(await getWorkspaceBasePath(workspaceId), 'meta.json');
}
/**
* Get workspace meta, create one if not exists
* This function will also migrate the workspace if needed
*/
export async function getWorkspaceMeta(
workspaceId: string
): Promise<WorkspaceMeta> {
try {
const basePath = await getWorkspaceBasePath(workspaceId);
const metaPath = await getWorkspaceMetaPath(workspaceId);
if (!(await fs.exists(metaPath))) {
// since not meta is found, we will migrate symlinked db file if needed
await fs.ensureDir(basePath);
const dbPath = await getWorkspaceDBPath(workspaceId);
// todo: remove this after migration (in stable version)
const realDBPath = (await fs.exists(dbPath))
? await fs.realpath(dbPath)
: dbPath;
const isLink = realDBPath !== dbPath;
if (isLink) {
await fs.copy(realDBPath, dbPath);
}
// create one if not exists
const meta = {
id: workspaceId,
mainDBPath: dbPath,
secondaryDBPath: isLink ? realDBPath : undefined,
};
await fs.writeJSON(metaPath, meta);
return meta;
} else {
const meta = await fs.readJSON(metaPath);
return meta;
}
} catch (err) {
logger.error('getWorkspaceMeta failed', err);
throw err;
}
}
export async function storeWorkspaceMeta(
workspaceId: string,
meta: Partial<WorkspaceMeta>
) {
try {
const basePath = await getWorkspaceBasePath(workspaceId);
await fs.ensureDir(basePath);
const metaPath = path.join(basePath, 'meta.json');
const currentMeta = await getWorkspaceMeta(workspaceId);
const newMeta = {
...currentMeta,
...meta,
};
await fs.writeJSON(metaPath, newMeta);
workspaceSubjects.meta.next({
workspaceId,
meta: newMeta,
});
} catch (err) {
logger.error('storeWorkspaceMeta failed', err);
}
}

View File

@@ -0,0 +1,25 @@
import type { MainEventRegister, WorkspaceMeta } from '../type';
import { deleteWorkspace, getWorkspaceMeta, listWorkspaces } from './handlers';
import { workspaceSubjects } from './subjects';
export * from './handlers';
export * from './subjects';
export const workspaceEvents = {
onMetaChange: (
fn: (meta: { workspaceId: string; meta: WorkspaceMeta }) => void
) => {
const sub = workspaceSubjects.meta.subscribe(fn);
return () => {
sub.unsubscribe();
};
},
} satisfies Record<string, MainEventRegister>;
export const workspaceHandlers = {
list: async () => listWorkspaces(),
delete: async (id: string) => deleteWorkspace(id),
getMeta: async (id: string) => {
return getWorkspaceMeta(id);
},
};

View File

@@ -0,0 +1,7 @@
import { Subject } from 'rxjs';
import type { WorkspaceMeta } from '../type';
export const workspaceSubjects = {
meta: new Subject<{ workspaceId: string; meta: WorkspaceMeta }>(),
};

View File

@@ -0,0 +1 @@
tmp

View File

@@ -0,0 +1,173 @@
import assert from 'node:assert';
import path from 'node:path';
import { setTimeout } from 'node:timers/promises';
import fs from 'fs-extra';
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
import type { MainIPCHandlerMap } from '../exposed';
const registeredHandlers = new Map<
string,
((...args: any[]) => Promise<any>)[]
>();
type WithoutFirstParameter<T> = T extends (_: any, ...args: infer P) => infer R
? (...args: P) => R
: T;
// common mock dispatcher for ipcMain.handle AND app.on
// alternatively, we can use single parameter for T & F, eg, dispatch('workspace:list'),
// however this is too hard to be typed correctly
async function dispatch<
T extends keyof MainIPCHandlerMap,
F extends keyof MainIPCHandlerMap[T]
>(
namespace: T,
functionName: F,
...args: Parameters<WithoutFirstParameter<MainIPCHandlerMap[T][F]>>
): // @ts-expect-error
ReturnType<MainIPCHandlerMap[T][F]> {
// @ts-expect-error
const handlers = registeredHandlers.get(namespace + ':' + functionName);
assert(handlers);
// we only care about the first handler here
return await handlers[0](null, ...args);
}
const SESSION_DATA_PATH = path.join(__dirname, './tmp', 'affine-test');
const DOCUMENTS_PATH = path.join(__dirname, './tmp', 'affine-test-documents');
const browserWindow = {
isDestroyed: () => {
return false;
},
setWindowButtonVisibility: (_v: boolean) => {
// will be stubbed later
},
webContents: {
send: (_type: string, ..._args: any[]) => {
// will be stubbed later
},
},
};
const ipcMain = {
handle: (key: string, callback: (...args: any[]) => Promise<any>) => {
const handlers = registeredHandlers.get(key) || [];
handlers.push(callback);
registeredHandlers.set(key, handlers);
},
setMaxListeners: (_n: number) => {
// noop
},
};
const nativeTheme = {
themeSource: 'light',
};
const electronModule = {
app: {
getPath: (name: string) => {
if (name === 'sessionData') {
return SESSION_DATA_PATH;
} else if (name === 'documents') {
return DOCUMENTS_PATH;
}
throw new Error('not implemented');
},
name: 'affine-test',
on: (name: string, callback: (...args: any[]) => any) => {
const handlers = registeredHandlers.get(name) || [];
handlers.push(callback);
registeredHandlers.set(name, handlers);
},
addListener: (...args: any[]) => {
// @ts-expect-error
electronModule.app.on(...args);
},
removeListener: () => {},
},
BrowserWindow: {
getAllWindows: () => {
return [browserWindow];
},
},
nativeTheme: nativeTheme,
ipcMain,
shell: {} as Partial<Electron.Shell>,
dialog: {} as Partial<Electron.Dialog>,
};
// dynamically import handlers so that we can inject local variables to mocks
vi.doMock('electron', () => {
return electronModule;
});
beforeEach(async () => {
const { registerHandlers } = await import('../handlers');
registerHandlers();
// should also register events
const { registerEvents } = await import('../events');
registerEvents();
await fs.mkdirp(SESSION_DATA_PATH);
registeredHandlers.get('ready')?.forEach(fn => fn());
});
afterEach(async () => {
// reset registered handlers
registeredHandlers.get('before-quit')?.forEach(fn => fn());
// wait for the db to be closed on Windows
if (process.platform === 'win32') {
await setTimeout(200);
}
await fs.remove(SESSION_DATA_PATH);
});
describe('UI handlers', () => {
test('theme-change', async () => {
await dispatch('ui', 'handleThemeChange', 'dark');
expect(nativeTheme.themeSource).toBe('dark');
await dispatch('ui', 'handleThemeChange', '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', 'handleSidebarVisibilityChange', true);
expect(setWindowButtonVisibility).toBeCalledWith(true);
await dispatch('ui', 'handleSidebarVisibilityChange', 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', 'handleSidebarVisibilityChange', true);
expect(setWindowButtonVisibility).not.toBeCalled();
vi.unstubAllGlobals();
});
});
describe('applicationMenu', () => {
// test some basic IPC events
test('applicationMenu event', async () => {
const { applicationMenuSubjects } = await import('../application-menu');
const sendStub = vi.fn();
browserWindow.webContents.send = sendStub;
applicationMenuSubjects.newPageAction.next();
expect(sendStub).toHaveBeenCalledWith(
'applicationMenu:onNewPageAction',
undefined
);
browserWindow.webContents.send = () => {};
});
});

View File

@@ -0,0 +1,142 @@
import { app, Menu } from 'electron';
import { revealLogFile } from '../logger';
import { checkForUpdatesAndNotify } from '../updater';
import { isMacOS } from '../utils';
import { applicationMenuSubjects } from './subject';
// Unique id for menuitems
const MENUITEM_NEW_PAGE = 'affine:new-page';
export function createApplicationMenu() {
const isMac = isMacOS();
// Electron menu cannot be modified
// You have to copy the complete default menu template event if you want to add a single custom item
// See https://www.electronjs.org/docs/latest/api/menu#examples
const template = [
// { role: 'appMenu' }
...(isMac
? [
{
label: app.name,
submenu: [
{ role: 'about' },
{ type: 'separator' },
{ role: 'services' },
{ type: 'separator' },
{ role: 'hide' },
{ role: 'hideOthers' },
{ role: 'unhide' },
{ type: 'separator' },
{ role: 'quit' },
],
},
]
: []),
// { role: 'fileMenu' }
{
label: 'File',
submenu: [
{
id: MENUITEM_NEW_PAGE,
label: 'New Page',
accelerator: isMac ? 'Cmd+N' : 'Ctrl+N',
click: () => {
applicationMenuSubjects.newPageAction.next();
},
},
{ type: 'separator' },
isMac ? { role: 'close' } : { role: 'quit' },
],
},
// { role: 'editMenu' }
{
label: 'Edit',
submenu: [
{ role: 'undo' },
{ role: 'redo' },
{ type: 'separator' },
{ role: 'cut' },
{ role: 'copy' },
{ role: 'paste' },
...(isMac
? [
{ role: 'pasteAndMatchStyle' },
{ role: 'delete' },
{ role: 'selectAll' },
{ type: 'separator' },
{
label: 'Speech',
submenu: [{ role: 'startSpeaking' }, { role: 'stopSpeaking' }],
},
]
: [{ role: 'delete' }, { type: 'separator' }, { role: 'selectAll' }]),
],
},
// { role: 'viewMenu' }
{
label: 'View',
submenu: [
{ role: 'reload' },
{ role: 'forceReload' },
{ role: 'toggleDevTools' },
{ type: 'separator' },
{ role: 'resetZoom' },
{ role: 'zoomIn' },
{ role: 'zoomOut' },
{ type: 'separator' },
{ role: 'togglefullscreen' },
],
},
// { role: 'windowMenu' }
{
label: 'Window',
submenu: [
{ role: 'minimize' },
{ role: 'zoom' },
...(isMac
? [
{ type: 'separator' },
{ role: 'front' },
{ type: 'separator' },
{ role: 'window' },
]
: [{ role: 'close' }]),
],
},
{
role: 'help',
submenu: [
{
label: 'Learn More',
click: async () => {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { shell } = require('electron');
await shell.openExternal('https://affine.pro/');
},
},
{
label: 'Open log file',
click: async () => {
await revealLogFile();
},
},
{
label: 'Check for Updates',
click: async () => {
await checkForUpdatesAndNotify(true);
},
},
],
},
];
// @ts-expect-error: The snippet is copied from Electron official docs.
// It's working as expected. No idea why it contains type errors.
// Just ignore for now.
const menu = Menu.buildFromTemplate(template);
Menu.setApplicationMenu(menu);
return menu;
}

View File

@@ -0,0 +1,20 @@
import type { MainEventRegister } from '../type';
import { applicationMenuSubjects } from './subject';
export * from './create';
export * from './subject';
/**
* Events triggered by application menu
*/
export const applicationMenuEvents = {
/**
* File -> New Page
*/
onNewPageAction: (fn: () => void) => {
const sub = applicationMenuSubjects.newPageAction.subscribe(fn);
return () => {
sub.unsubscribe();
};
},
} satisfies Record<string, MainEventRegister>;

View File

@@ -0,0 +1,5 @@
import { Subject } from 'rxjs';
export const applicationMenuSubjects = {
newPageAction: new Subject<void>(),
};

View File

@@ -0,0 +1,39 @@
import { app, BrowserWindow } from 'electron';
import { applicationMenuEvents } from './application-menu';
import { logger } from './logger';
import { updaterEvents } from './updater/event';
export const allEvents = {
applicationMenu: applicationMenuEvents,
updater: updaterEvents,
};
function getActiveWindows() {
return BrowserWindow.getAllWindows().filter(win => !win.isDestroyed());
}
export function registerEvents() {
// register events
for (const [namespace, namespaceEvents] of Object.entries(allEvents)) {
for (const [key, eventRegister] of Object.entries(namespaceEvents)) {
const subscription = eventRegister((...args: any[]) => {
const chan = `${namespace}:${key}`;
logger.info(
'[ipc-event]',
chan,
args.filter(
a =>
a !== undefined &&
typeof a !== 'function' &&
typeof a !== 'object'
)
);
getActiveWindows().forEach(win => win.webContents.send(chan, ...args));
});
app.on('before-quit', () => {
subscription();
});
}
}
}

View File

@@ -0,0 +1,10 @@
import type { NamespaceHandlers } from '../type';
import { savePDFFileAs } from './pdf';
export const exportHandlers = {
savePDFFileAs: async (_, title: string) => {
return savePDFFileAs(title);
},
} satisfies NamespaceHandlers;
export * from './pdf';

View File

@@ -0,0 +1,61 @@
import { BrowserWindow, dialog, shell } from 'electron';
import fs from 'fs-extra';
import { logger } from '../logger';
import type { ErrorMessage } from './utils';
import { getFakedResult } from './utils';
export interface SavePDFFileResult {
filePath?: string;
canceled?: boolean;
error?: ErrorMessage;
}
/**
* This function is called when the user clicks the "Export to PDF" button in the electron.
*
* It will just copy the file to the given path
*/
export async function savePDFFileAs(
pageTitle: string
): Promise<SavePDFFileResult> {
try {
const ret =
getFakedResult() ??
(await dialog.showSaveDialog({
properties: ['showOverwriteConfirmation'],
title: 'Save PDF',
showsTagField: false,
buttonLabel: 'Save',
defaultPath: `${pageTitle}.pdf`,
message: 'Save Page as a PDF file',
}));
const filePath = ret.filePath;
if (ret.canceled || !filePath) {
return {
canceled: true,
};
}
await BrowserWindow.getFocusedWindow()
?.webContents.printToPDF({
pageSize: 'A4',
printBackground: true,
landscape: false,
})
.then(data => {
fs.writeFile(filePath, data, error => {
if (error) throw error;
logger.log(`Wrote PDF successfully to ${filePath}`);
});
});
await shell.openPath(filePath);
return { filePath };
} catch (err) {
logger.error('savePDFFileAs', err);
return {
error: 'UNKNOWN_ERROR',
};
}
}

View File

@@ -0,0 +1,24 @@
// provide a backdoor to set dialog path for testing in playwright
interface FakeDialogResult {
canceled?: boolean;
filePath?: string;
filePaths?: string[];
}
// result will be used in the next call to showOpenDialog
// if it is being read once, it will be reset to undefined
let fakeDialogResult: FakeDialogResult | undefined = undefined;
export function getFakedResult() {
const result = fakeDialogResult;
fakeDialogResult = undefined;
return result;
}
export function setFakeDialogResult(result: FakeDialogResult | undefined) {
fakeDialogResult = result;
// for convenience, we will fill filePaths with filePath if it is not set
if (result?.filePaths === undefined && result?.filePath !== undefined) {
result.filePaths = [result.filePath];
}
}
const ErrorMessages = ['FILE_ALREADY_EXISTS', 'UNKNOWN_ERROR'] as const;
export type ErrorMessage = (typeof ErrorMessages)[number];

View File

@@ -0,0 +1,29 @@
import { allEvents as events } from './events';
import { allHandlers as handlers } from './handlers';
// this will be used by preload script to expose all handlers and events to the renderer process
// - register in exposeInMainWorld in preload
// - provide type hints
export { events, handlers };
export const getExposedMeta = () => {
const handlersMeta = Object.entries(handlers).map(
([namespace, namespaceHandlers]) => {
return [namespace, Object.keys(namespaceHandlers)];
}
);
const eventsMeta = Object.entries(events).map(
([namespace, namespaceHandlers]) => {
return [namespace, Object.keys(namespaceHandlers)];
}
);
return {
handlers: handlersMeta,
events: eventsMeta,
};
};
export type MainIPCHandlerMap = typeof handlers;
export type MainIPCEventMap = typeof events;

View File

@@ -0,0 +1,79 @@
import type {
DebugHandlerManager,
ExportHandlerManager,
UIHandlerManager,
UnwrapManagerHandlerToServerSide,
UpdaterHandlerManager,
} from '@toeverything/infra';
import { ipcMain } from 'electron';
import { exportHandlers } from './export';
import { getLogFilePath, logger, revealLogFile } from './logger';
import { uiHandlers } from './ui';
import { updaterHandlers } from './updater';
export const debugHandlers = {
revealLogFile: async () => {
return revealLogFile();
},
logFilePath: async () => {
return getLogFilePath();
},
};
type AllHandlers = {
debug: UnwrapManagerHandlerToServerSide<
Electron.IpcMainInvokeEvent,
DebugHandlerManager
>;
export: UnwrapManagerHandlerToServerSide<
Electron.IpcMainInvokeEvent,
ExportHandlerManager
>;
ui: UnwrapManagerHandlerToServerSide<
Electron.IpcMainInvokeEvent,
UIHandlerManager
>;
updater: UnwrapManagerHandlerToServerSide<
Electron.IpcMainInvokeEvent,
UpdaterHandlerManager
>;
};
// Note: all of these handlers will be the single-source-of-truth for the apis exposed to the renderer process
export const allHandlers = {
debug: debugHandlers,
ui: uiHandlers,
export: exportHandlers,
updater: updaterHandlers,
} satisfies AllHandlers;
export const registerHandlers = () => {
// TODO: listen to namespace instead of individual event types
ipcMain.setMaxListeners(100);
for (const [namespace, namespaceHandlers] of Object.entries(allHandlers)) {
for (const [key, handler] of Object.entries(namespaceHandlers)) {
const chan = `${namespace}:${key}`;
ipcMain.handle(chan, async (e, ...args) => {
const start = performance.now();
try {
// @ts-expect-error - TODO: fix this
const result = await handler(e, ...args);
logger.info(
'[ipc-api]',
chan,
args.filter(
arg => typeof arg !== 'function' && typeof arg !== 'object'
),
'-',
(performance.now() - start).toFixed(2),
'ms'
);
return result;
} catch (error) {
logger.error('[ipc]', chan, error);
}
});
}
}
};

View File

@@ -0,0 +1,111 @@
import path from 'node:path';
import { type _AsyncVersionOf, AsyncCall } from 'async-call-rpc';
import {
app,
dialog,
MessageChannelMain,
shell,
type UtilityProcess,
utilityProcess,
type WebContents,
} from 'electron';
import { logger } from './logger';
import { MessageEventChannel } from './utils';
const HELPER_PROCESS_PATH = path.join(__dirname, './helper.js');
function pickAndBind<T extends object, U extends keyof T>(
obj: T,
keys: U[]
): { [K in U]: T[K] } {
return keys.reduce((acc, key) => {
const prop = obj[key];
acc[key] =
typeof prop === 'function'
? // @ts-expect-error - a hack to bind the function
prop.bind(obj)
: prop;
return acc;
}, {} as any);
}
class HelperProcessManager {
ready: Promise<void>;
#process: UtilityProcess;
// a rpc server for the main process -> helper process
rpc?: _AsyncVersionOf<PeersAPIs.HelperToMain>;
static instance = new HelperProcessManager();
private constructor() {
const helperProcess = utilityProcess.fork(HELPER_PROCESS_PATH);
this.#process = helperProcess;
this.ready = new Promise((resolve, reject) => {
helperProcess.once('spawn', () => {
try {
this.#connectMain();
resolve();
} catch (err) {
logger.error('[helper] connectMain error', err);
reject(err);
}
});
});
app.on('before-quit', () => {
this.#process.kill();
});
}
// bridge renderer <-> helper process
connectRenderer(renderer: WebContents) {
// connect to the helper process
const { port1: helperPort, port2: rendererPort } = new MessageChannelMain();
this.#process.postMessage({ channel: 'renderer-connect' }, [helperPort]);
renderer.postMessage('helper-connection', null, [rendererPort]);
return () => {
helperPort.close();
rendererPort.close();
};
}
// bridge main <-> helper process
// also set up the RPC to the helper process
#connectMain() {
const dialogMethods = pickAndBind(dialog, [
'showOpenDialog',
'showSaveDialog',
]);
const shellMethods = pickAndBind(shell, [
'openExternal',
'showItemInFolder',
]);
const appMethods = pickAndBind(app, ['getPath']);
const mainToHelperServer: PeersAPIs.MainToHelper = {
...dialogMethods,
...shellMethods,
...appMethods,
};
const server = AsyncCall<PeersAPIs.HelperToMain>(mainToHelperServer, {
strict: {
// the channel is shared for other purposes as well so that we do not want to
// restrict to only JSONRPC messages
unknownMessage: false,
},
channel: new MessageEventChannel(this.#process),
});
this.rpc = server;
}
}
export async function ensureHelperProcess() {
const helperProcessManager = HelperProcessManager.instance;
await helperProcessManager.ready;
return helperProcessManager;
}

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