Compare commits

...

142 Commits

Author SHA1 Message Date
himself65
a4d8b65eef v0.5.4-canary.11 2023-04-25 19:00:03 -05:00
himself65
83dafa149c build: add set-version.sh 2023-04-25 18:59:37 -05:00
himself65
3a25f13734 docs: download page redirect to affine.pro 2023-04-25 18:48:39 -05:00
Himself65
db52c63d25 feat: init @toeverything/theme (#2136) 2023-04-25 18:44:17 -05:00
himself65
80f4578f76 v0.5.4-canary.10 2023-04-25 11:44:23 -05:00
JimmFly
15a7e93058 fix: text overflow problem in <a> tag (#2126) 2023-04-25 11:40:14 -05:00
JimmFly
1c41731b4e fix: theme color (#2124) 2023-04-25 11:37:22 -05:00
Himself65
a807647639 fix(component): editor component style (#2120) 2023-04-25 01:58:30 -05:00
JimmFly
3f1293ca3c chore: add changeLog to storybook (#2118)
Co-authored-by: himself65 <himself65@outlook.com>
2023-04-25 06:24:46 +00:00
Himself65
ad58b4d1e9 feat: improve build config (#2115) 2023-04-24 22:33:09 -05:00
Himself65
7e61708850 test: move playwright test suite to top level (#2113) 2023-04-24 22:12:48 -05:00
LongYinan
5c673a8ffc feat(graphql): generate types from graphql files (#2014)
Co-authored-by: forehalo <forehalo@gmail.com>
Co-authored-by: Himself65 <himself65@outlook.com>
2023-04-25 10:13:52 +08:00
himself65
4528df07a5 v0.5.4-canary.9 2023-04-24 19:59:21 -05:00
himself65
b6eb017bd4 docs: add linux badge 2023-04-24 19:55:05 -05:00
Himself65
9d3b9e9848 chore: bump version (#2111) 2023-04-24 19:46:46 -05:00
himself65
04fc619f52 test: fix flaky 2023-04-24 19:33:35 -05:00
himself65
06ef6da370 ci: remove unused 2023-04-24 19:26:30 -05:00
Himself65
d3ce90e721 test: add electron test (#1840) 2023-04-24 18:53:36 -05:00
himself65
9c94d05dd8 docs: format jobs.md 2023-04-24 17:47:41 -05:00
Himself65
ef8dea8cb2 test: fix flaky in customElements (#2109) 2023-04-24 13:18:37 -05:00
Peng Xiao
c27c241482 fix: some improvements to electron app (#2089) 2023-04-24 12:53:21 -05:00
Flrande
b73e9189ef chore: fix color (#2083)
Co-authored-by: Himself65 <himself65@outlook.com>
2023-04-24 11:49:34 -05:00
JimmFly
c95b8e9d71 fix: incorrect text color (#2107) 2023-04-24 11:49:22 -05:00
Peng Xiao
ab8669882a fix: closing modal sometimes covered by header (#2097) 2023-04-23 23:43:40 -05:00
himself65
7ff12a6d0f build: reduce the sample rate to 0.1 2023-04-23 23:40:59 -05:00
himself65
339b133e3f v0.5.4-canary.8 2023-04-23 21:41:43 -05:00
Peng Xiao
be9095ec19 build: fix electron build gain focus on reloading in dev (#2088) 2023-04-23 01:42:52 -05:00
Himself65
33261558f6 chore: bump version (#2087) 2023-04-23 01:42:27 -05:00
Himself65
2ad1b770d0 fix(y-indexeddb): alert user when write operation unfinished (#2085) 2023-04-22 17:32:57 -05:00
Himself65
74e21311dc refactor(y-indexeddb): move migrate function separate (#2086) 2023-04-22 17:25:25 -05:00
Chi Zhang
bf83bfcf63 feat: add short cuts for sidebar (#2075) 2023-04-22 17:24:44 -05:00
Chi Zhang
70d8f9a0a7 feat: add shared page empty tip (#2077)
Co-authored-by: himself65 <himself65@outlook.com>
2023-04-22 17:24:18 -05:00
Moeyua Evod
7d246f87e7 docs: sign CLA (#2079) 2023-04-22 00:05:13 -05:00
Himself65
1ca9fb8ff4 fix(workspace): check affine login auth (#2070) 2023-04-21 20:44:29 -05:00
Moeyua Evod
2c95a0a757 feat: center align button text (#2056)
Co-authored-by: Himself65 <himself65@outlook.com>
2023-04-21 19:45:23 -05:00
himself65
a49d5ea1e2 fix(workspace): load first workspace in index page 2023-04-21 13:46:01 -05:00
三咲智子 Kevin Deng
84e2710e87 docs: fix typo (#2063) 2023-04-21 12:07:44 -05:00
Peng Xiao
044e6da00d build: beta build (#2069) 2023-04-21 11:52:55 -05:00
himself65
023cbc30ea fix(workspace): cloud workspace blob uploading 2023-04-21 11:34:18 -05:00
Peng Xiao
7094385d8b fix: try to sign macos (#2066) 2023-04-21 23:30:49 +08:00
himself65
f66d402cf7 v0.5.4-beta.0 2023-04-21 06:09:38 -05:00
Peng Xiao
971e256cd3 fix: osxSign in build 2023-04-21 18:25:46 +08:00
Peng Xiao
88a297c3c1 chore: bump version 0.5.4-canary.7 2023-04-21 18:10:12 +08:00
Peng Xiao
4bb50e8c25 feat: store local data to local db (#2037) 2023-04-21 18:06:54 +08:00
zuomeng wang
acc5afdd4f fix(web): remove edgeless mode padding (#2061) 2023-04-21 17:56:29 +08:00
Qi
9ec6768272 fix: modify with new blocksuite version about subpage (#2060) 2023-04-21 08:34:32 +00:00
Peng Xiao
5a124831b8 fix: some minor ui issues (#2058) 2023-04-21 00:56:42 -05:00
Flrande
01115f8957 fix: color variable (#2059) 2023-04-20 23:41:43 -05:00
Qi
a5a6203a95 feat: replace react-dnd to dnd-kit (#2028)
Co-authored-by: Himself65 <himself65@outlook.com>
2023-04-20 23:27:32 -05:00
himself65
4a473f5518 Revert "chore: bump version"
This reverts commit 44011b4695.
2023-04-20 22:53:32 -05:00
himself65
6cddacb953 Revert "fix: api compatibility with blocksuite"
This reverts commit 00f44c72ce.
2023-04-20 22:53:32 -05:00
himself65
00f44c72ce fix: api compatibility with blocksuite 2023-04-20 22:29:11 -05:00
himself65
44011b4695 chore: bump version 2023-04-20 21:58:09 -05:00
himself65
e0cd2e780b v0.5.4-canary.7 2023-04-20 18:09:53 -05:00
himself65
985bb55d82 build(y-indexeddb): fix vite config 2023-04-20 18:08:33 -05:00
himself65
66d0640042 ci: fix release.yml 2023-04-20 17:50:29 -05:00
himself65
e399682cad ci: add release.yml 2023-04-20 17:47:06 -05:00
himself65
c4e90f2d8b v0.5.4-canary.6 2023-04-20 17:29:49 -05:00
himself65
b38b01fc98 docs: fix script 2023-04-20 17:27:30 -05:00
Himself65
0a0f825a15 fix: remove mui theme provider (#2055) 2023-04-20 14:31:54 -05:00
Himself65
d24c43e750 chore: bump version (#2054) 2023-04-20 12:25:12 -05:00
ʀᴀʏ
90b51031d2 chore: correct action name (#2053) 2023-04-20 11:32:44 -05:00
himself65
1e771131b0 docs: format releases.md 2023-04-20 11:32:17 -05:00
himself65
4d7a3e5bf1 docs: add releases.md 2023-04-20 11:27:52 -05:00
himself65
92b1244fd7 v0.5.4-canary.5 2023-04-20 11:08:10 -05:00
himself65
d6b1b9f6cf ci: use RELEASE_TOKEN 2023-04-20 10:34:35 -05:00
Flrande
b2e93433e1 chore: fix color (#2049)
Co-authored-by: JimmFly <yangjinfei001@gmail.com>
2023-04-20 09:13:20 -05:00
Chi Zhang
97b1a31f8d Update README.md 2023-04-20 21:38:00 +08:00
Qi
4a1c15c1e9 feat: modify default avatar (#2034) 2023-04-20 17:41:29 +08:00
himself65
f8d1513bb6 chore: release 0.5.4-canary.4 2023-04-20 03:34:00 -05:00
Flrande
372377dd6b feat: upgrate to new theme (#2027)
Co-authored-by: Himself65 <himself65@outlook.com>
2023-04-20 03:31:19 -05:00
Himself65
63f7b2556e feat: init affine blob storage (#2045) 2023-04-20 03:23:41 -05:00
himself65
c08c587efb fix: max length of input 2023-04-20 02:36:25 -05:00
JimmFly
65c1bee7f0 chore: update temp disable affine cloud modal style (#2046) 2023-04-20 02:27:26 -05:00
howarddo
227f59cadc docs: add more instruction for yarn (#2042) 2023-04-20 00:25:10 -05:00
JimmFly
031ab2cfa2 chore: improve disable legacy cloud (#2041) 2023-04-20 12:25:45 +08:00
Chi Zhang
9f33e73429 Update package.json 2023-04-19 14:30:28 +08:00
himself65
f1670af15d ci: fix working-directory 2023-04-18 18:33:46 -05:00
himself65
0d7f65ab36 test(server): fix script 2023-04-18 18:24:35 -05:00
Himself65
3a053af50c feat(server): init user module (#2018) 2023-04-18 18:14:25 -05:00
himself65
c6be29f944 fix: disable legacy cloud in header 2023-04-18 15:01:19 -05:00
Peng Xiao
9ffe45102b fix: macos build 2023-04-19 00:43:51 +08:00
Peng Xiao
6448b6a515 fix: release app workflow (#2017) 2023-04-19 00:21:44 +08:00
Peng Xiao
ba462fb79b fix: artifacts in release (#2016) 2023-04-18 22:20:34 +08:00
Peng Xiao
f36d415c3d build: optimize release app workflow (#2011) 2023-04-18 17:50:29 +08:00
Himself65
f6fb049ff2 feat: support disable legacy cloud (#2006) 2023-04-18 02:23:00 -05:00
JimmFly
94063352f5 chore: disable slider bar link item drag (#2010) 2023-04-18 02:16:38 -05:00
Himself65
c895c18deb ci: collect server coverage report (#2002) 2023-04-18 01:01:14 -05:00
JimmFly
346484ed44 chore: add translation (#2001) 2023-04-18 00:34:21 -05:00
Himself65
18223c22ef test(server): migrate to node internal test (#2000) 2023-04-18 00:07:03 -05:00
himself65
ea9861bfa0 ci: update labeler.yml 2023-04-17 23:13:10 -05:00
Himself65
7be96a2e41 build: remove unused config (#1990) 2023-04-17 23:11:46 -05:00
LongYinan
91c3040db7 feat(server): init nestjs server (#1997)
Co-authored-by: himself65 <himself65@outlook.com>
2023-04-17 22:24:44 -05:00
himself65
a92d0fff4a docs: update badge in README.md 2023-04-17 21:06:29 -05:00
Jordy Delgado
64e5d65eb3 docs: sign CLA (#1995) 2023-04-17 21:03:15 -05:00
Peng Xiao
11de3a681f build: add canary build (#1986)
Co-authored-by: Himself65 <himself65@outlook.com>
Co-authored-by: Horus <lhlxtl@gmail.com>
2023-04-17 11:32:10 -05:00
hehe
54a30bbf20 chore: remove absolete module-resolve (#1991) 2023-04-17 15:02:22 +00:00
usedtobe
6c77006bcc docs: fix typo (#1984) 2023-04-17 08:34:50 -05:00
Qi
143a55a6e8 fix: error style of sidebar (#1981) 2023-04-17 06:52:04 +00:00
Qi
19894aad5a feat: modify empty text & style of favorite & pinboard (#1977)
Co-authored-by: Himself65 <himself65@outlook.com>
2023-04-17 13:41:07 +08:00
JimmFly
f534e4a6dd chore: update change log link (#1973)
Co-authored-by: Himself65 <himself65@outlook.com>
2023-04-16 21:48:29 -05:00
Himself65
3d70a36dd3 refactor: remove null type in hooks (#1955) 2023-04-16 21:36:32 -05:00
Himself65
9c517907eb fix: first binary on y-indexeddb (#1972) 2023-04-16 21:33:54 -05:00
Himself65
4cb6b8fdc8 chore: bump version (#1970) 2023-04-16 20:36:59 -05:00
Horus
134e1e8668 feat: support release windows installer with squirrel (#1965)
Co-authored-by: Himself65 <himself65@outlook.com>
2023-04-16 19:28:29 -05:00
Himself65
c76bbeab67 ci: add sentry in desktop release (#1914) 2023-04-16 21:22:48 +00:00
himself65
ec50d721ea chore: release 0.5.3 2023-04-16 16:04:21 -05:00
Himself65
7bbe67af43 refactor: workspace loading logic (#1966) 2023-04-16 16:02:41 -05:00
Himself65
caa292e097 test: mark public single page as fail (#1967) 2023-04-16 09:45:50 -05:00
HeJiachen-PM
73b8b805c6 Rewrite section 2.3 2023-04-16 15:19:22 +08:00
HeJiachen-PM
084d4e043a Add summery to subsections in section 2 2023-04-16 15:09:08 +08:00
HeJiachen-PM
69a9c34f11 Rewrite the third section 2023-04-16 04:37:35 +08:00
Himself65
d742cab1d5 fix: hydration error (#1961) 2023-04-15 13:10:24 -05:00
Horus
8b3c1fb363 fix: force to use powershell on windows to fix zx script crash (#1962) 2023-04-15 12:24:57 -05:00
Horus
ec445207d6 fix: fix windows build client error and release cannot open (#1959) 2023-04-16 00:00:47 +08:00
HeJiachen-PM
49281e68a6 Rewrite the second section 2023-04-15 15:31:56 +08:00
HeJiachen-PM
a918d6e14c Proofreading introduction 2023-04-15 15:27:09 +08:00
Himself65
7cf7187893 docs: add behind-the-code.md (#1957) 2023-04-15 00:19:13 -05:00
Himself65
2383165470 refactor: remove NoSsr on top level (#1951) 2023-04-14 17:07:41 -05:00
Himself65
43a96fe8e3 fix: move suspense to the correct place (#1954) 2023-04-14 15:44:23 -05:00
Himself65
b771a2504b test: fix flaky (#1953) 2023-04-14 15:03:16 -05:00
himself65
8d2fefb5f8 ci: fix labeler.yml 2023-04-14 14:14:58 -05:00
himself65
c71e5f1c96 fix(cli): run dev server at 8080 2023-04-14 11:06:22 -05:00
Skye Sun
5b96fb0db3 docs: update CLA.md (#1950) 2023-04-14 08:02:21 -05:00
Peng Xiao
46cd0c5c9a fix: share url (#1948) 2023-04-14 08:01:31 -05:00
Qi
261a41f8da feat: add history back & forward for desktop app (#1926) 2023-04-14 09:19:52 +00:00
Himself65
bd387f6551 fix: theme color (#1944) 2023-04-14 02:13:14 -05:00
JimmFly
5335118e93 chore: add translation (#1946) 2023-04-14 15:02:43 +08:00
Himself65
70313eb5ee chore: bump version (#1943) 2023-04-14 01:57:54 -05:00
himself65
ccd2b79d20 docs: update logo in README.md 2023-04-14 00:38:35 -05:00
Himself65
5ca94db5d2 fix: effect deps (#1940) 2023-04-14 00:24:44 -05:00
Himself65
d58f9db289 docs: update BUG-REPORT.yml (#1941) 2023-04-13 22:27:01 -05:00
Chi Zhang
93e78c315c Update jobs.md 2023-04-14 10:27:45 +08:00
himself65
3954f309aa chore: fix packages version 2023-04-13 18:33:21 -05:00
himself65
f902d0c324 ci: fix cache in build-master.yml 2023-04-13 18:22:20 -05:00
Himself65
e79fb1ae3a build: add log when coverage (#1933) 2023-04-13 18:20:41 -05:00
Himself65
08d67b316c docs: update README.md (#1931) 2023-04-13 17:54:20 -05:00
himself65
d12c00d5cb ci: fix coverage report 2023-04-13 17:53:34 -05:00
himself65
68bb538dd1 ci: remove version tag in release 2023-04-13 16:39:50 -05:00
himself65
b394764b1c ci: fix upload-artifact path 2023-04-13 16:33:12 -05:00
Himself65
01a686dc28 feat: enable share menu (#1883)
Co-authored-by: JimmFly <yangjinfei001@gmail.com>
2023-04-13 16:22:49 -05:00
Simon He
32b206a137 chore: add lint cache (#1917) 2023-04-13 20:30:18 +00:00
422 changed files with 18717 additions and 25856 deletions

View File

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

View File

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

View File

@@ -1,8 +1,11 @@
module.exports = {
/**
* @type {import('eslint').Linter.Config}
*/
const config = {
root: true,
settings: {
react: {
version: '18',
version: 'detect',
},
next: {
rootDir: 'apps/web',
@@ -10,6 +13,7 @@ module.exports = {
},
extends: [
'eslint:recommended',
'plugin:react-hooks/recommended',
'plugin:react/recommended',
'plugin:react/jsx-runtime',
'plugin:@typescript-eslint/recommended',
@@ -64,4 +68,20 @@ module.exports = {
},
],
},
overrides: [
{
files: 'apps/server/**/*.ts',
rules: {
'@typescript-eslint/consistent-type-imports': 0,
},
},
{
files: '*.cjs',
rules: {
'@typescript-eslint/no-var-requires': 0,
},
},
],
};
module.exports = config;

5
.github/CLA.md vendored
View File

@@ -53,3 +53,8 @@ Example:
- Aditya Sharma, @adityash1, 2023/03/21
- Fangdun Tsai, @fundon, 2023/03/21
- Zhilin Liu, @lzlme, 2023/04/09
- Skye Sun, @skyesun, 2023/04/14
- Jordy Delgado, @Jdelgad8, 2023/04/17
- Howard Do, @howarddo2208, 2023/04/20
- Kevin Deng, @sxzz, 2023/04/21
- Moeyua, @moeyua, 2023/04/22

View File

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

View File

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

6
.github/labeler.yml vendored
View File

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

View File

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

View File

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

View File

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

View File

@@ -17,6 +17,11 @@ on:
type: boolean
required: true
default: true
build-type:
description: 'Build Type (canary, beta or stable)'
type: string
required: true
default: canary
permissions:
actions: write
@@ -29,142 +34,146 @@ concurrency:
group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.head_ref || github.sha }}
cancel-in-progress: true
jobs:
make-macos:
environment: production
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 }}
API_SERVER_PROFILE: prod
env:
BUILD_TYPE: ${{ github.event.inputs.build-type }}
runs-on: macos-latest
strategy:
matrix:
arch: [x64, arm64]
jobs:
before-make:
runs-on: ubuntu-latest
environment: ${{ github.ref_name == 'master' && 'production' || 'development' }}
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: ./.github/actions/setup-node
- name: generate-assets
working-directory: apps/electron
run: yarn generate-assets
env:
NEXT_PUBLIC_FIREBASE_API_KEY: ${{ secrets.NEXT_PUBLIC_FIREBASE_API_KEY }}
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN: ${{ secrets.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN }}
NEXT_PUBLIC_FIREBASE_PROJECT_ID: ${{ secrets.NEXT_PUBLIC_FIREBASE_PROJECT_ID }}
NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET: ${{ secrets.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET }}
NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID: ${{ secrets.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID }}
NEXT_PUBLIC_FIREBASE_APP_ID: ${{ secrets.NEXT_PUBLIC_FIREBASE_APP_ID }}
NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID: ${{ secrets.NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID }}
AFFINE_GOOGLE_CLIENT_ID: ${{ secrets.AFFINE_GOOGLE_CLIENT_ID }}
AFFINE_GOOGLE_CLIENT_SECRET: ${{ secrets.AFFINE_GOOGLE_CLIENT_SECRET }}
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
NEXT_PUBLIC_SENTRY_DSN: ${{ secrets.NEXT_PUBLIC_SENTRY_DSN }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
API_SERVER_PROFILE: prod
- name: Upload Artifact (web-static)
uses: actions/upload-artifact@v3
with:
electron-workspace-install: true
name: before-make-web-static
path: apps/electron/resources/web-static
- name: install Rust stable
uses: dtolnay/rust-toolchain@stable
- name: add arm64 target
if: matrix.arch == 'arm64'
run: rustup target add aarch64-apple-darwin
- name: Rust cache
uses: swatinem/rust-cache@v2
- name: Upload Artifact (electron dist)
uses: actions/upload-artifact@v3
with:
key: ${{ matrix.arch }}
workspaces: './packages/octobase-node -> target'
name: before-make-electron-dist
path: apps/electron/dist
make-distribution:
environment: ${{ github.ref_name == 'master' && 'production' || 'development' }}
strategy:
# all combinations: macos-latest x64, macos-latest arm64, windows-latest x64, ubuntu-latest x64
matrix:
spec:
- { os: macos-latest, platform: macos, arch: x64 }
- { os: macos-latest, platform: macos, arch: arm64 }
- { os: ubuntu-latest, platform: linux, arch: x64 }
- { os: windows-latest, platform: windows, arch: x64 }
runs-on: ${{ matrix.spec.os }}
needs: before-make
env:
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
SKIP_GENERATE_ASSETS: 1
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: ./.github/actions/setup-node
- uses: actions/download-artifact@v3
with:
name: before-make-web-static
path: apps/electron/resources/web-static
- uses: actions/download-artifact@v3
with:
name: before-make-electron-dist
path: apps/electron/dist
- name: Signing By Apple Developer ID
if: ${{ matrix.spec.platform == 'macos' }}
uses: apple-actions/import-codesign-certs@v2
with:
p12-file-base64: ${{ secrets.CERTIFICATES_P12 }}
p12-password: ${{ secrets.CERTIFICATES_P12_PASSWORD }}
- name: make build
run: yarn make-macos-${{ matrix.arch }}
- name: make
run: yarn make-${{ matrix.spec.platform }}-${{ matrix.spec.arch }}
working-directory: apps/electron
- name: Save artifacts
- name: Save artifacts (mac)
if: ${{ matrix.spec.platform == 'macos' }}
run: |
mkdir -p builds
mv apps/electron/out/make/AFFiNE.dmg ./builds/affine-darwin-${{ matrix.arch }}-${{ github.event.inputs.version }}.dmg
mv apps/electron/out/*/make/*.dmg ./builds/affine-${{ env.BUILD_TYPE }}-macos-${{ matrix.spec.arch }}.dmg
- name: Save artifacts (windows)
if: ${{ matrix.spec.platform == 'windows' }}
run: |
mkdir -p builds
mv apps/electron/out/*/make/zip/win32/x64/AFFiNE*-win32-x64-*.zip ./builds/affine-${{ env.BUILD_TYPE }}-windows-x64.zip
mv apps/electron/out/*/make/squirrel.windows/x64/*.exe ./builds/affine-${{ env.BUILD_TYPE }}-windows-x64.exe
mv apps/electron/out/*/make/squirrel.windows/x64/*.msi ./builds/affine-${{ env.BUILD_TYPE }}-windows-x64.msi
mv apps/electron/out/*/make/squirrel.windows/x64/*.nupkg ./builds/affine-${{ env.BUILD_TYPE }}-windows-x64.nupkg
- name: Save artifacts (linux)
if: ${{ matrix.spec.platform == 'linux' }}
run: |
mkdir -p builds
mv apps/electron/out/*/make/zip/linux/x64/*.zip ./builds/affine-${{ env.BUILD_TYPE }}-linux-x64.zip
- name: Upload Artifact
uses: actions/upload-artifact@v3
with:
name: affine-darwin-${{ matrix.arch }}-builds
path: builds
make-windows:
runs-on: windows-latest
environment: production
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 }}
API_SERVER_PROFILE: prod
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: ./.github/actions/setup-node
with:
electron-workspace-install: true
- name: install Rust stable
uses: dtolnay/rust-toolchain@stable
- name: Rust cache
uses: swatinem/rust-cache@v2
with:
workspaces: './packages/octobase-node -> target'
- name: make build
run: yarn make-windows-x64
working-directory: apps/electron
- name: Save windows artifacts
run: |
mkdir -p builds
mv apps/electron/out/make/zip/win32/x64/AFFiNE-win32-x64-0.0.0.zip ./builds/affine-windows-x64-${{ github.event.inputs.version }}.zip
- name: Upload Artifact
uses: actions/upload-artifact@v3
with:
name: affine-windows-x64-builds
name: affine-${{ matrix.spec.platform }}-${{ matrix.spec.arch }}-builds
path: builds
release:
needs: [make-macos, make-windows]
needs: make-distribution
runs-on: ubuntu-latest
steps:
- name: Download MacOS x64 Artifacts
uses: actions/download-artifact@v3
with:
name: affine-darwin-x64-builds
path: ./
- name: Download MacOS arm64 Artifacts
steps:
- name: Download Artifacts (macos-x64)
uses: actions/download-artifact@v3
with:
name: affine-darwin-arm64-builds
name: affine-macos-x64-builds
path: ./
- name: Download Windows Artifacts
- name: Download Artifacts (macos-arm64)
uses: actions/download-artifact@v3
with:
name: affine-macos-arm64-builds
path: ./
- name: Download Artifacts (windows-x64)
uses: actions/download-artifact@v3
with:
name: affine-windows-x64-builds
path: ./
- name: Download Artifacts (linux-x64)
uses: actions/download-artifact@v3
with:
name: affine-linux-x64-builds
path: ./
- name: 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'

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 }}

4
.gitignore vendored
View File

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

View File

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

View File

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

View File

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

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

112
README.md
View File

@@ -24,7 +24,12 @@ See https://github.com/all-?/all-contributors/issues/361#issuecomment-637166066
<!-- ALL-CONTRIBUTORS-BADGE:END -->
[![affine-app-logo]](https://app.affine.pro)
[![AFFiNE Web](<https://img.shields.io/badge/-Try%20It%20Online%20%E2%86%92-rgb(84,56,255)?style=flat-square&logoColor=white&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAACXBIWXMAAADAAAAAwAEwd99eAAABjElEQVRYhe1W0U3DMBB9RfyTDeoNyAYNG2QDOgJsECYgGxA26AZ4hIxgJqCZ4PjIGV+tUxK7raqiPsmKdXe5e3fOs7IiIlwSdxetfiNw7QRKAD0Ax/ssrI5QgQOw5v03AJOTJHcCL1x84LVmWzJyJlBg7P4BwCvb3pmIAbBPykZEqaulEU7YHNva1HypxUsKqIS9EvbynASs0n3ss+ciUIsuO8VvhL9emjdFBa3YO8XvALwpsZNYSqBB0PwUWgRZNksSL5GhlN0ngGd+dkpsD6AG8IGlslxwTh2fa09EBc3Dir32rRysuQlUAL54/wTAcpePPAXHPsOTGXhSEv69rAlYpZOt6DSO29J4D/TRRLJk6AvtaZSY9PkCFYVLqI9i/NF5YkkECgrXa6P4fVEn4iolrhNxRQqBZu7FqMNdZiMqAUPj2KdGZyicu1dHzlGqBHxn2sdTR53bmeJ+ebJd7LtXhGH4uQEwd0ttAPzMxGi5/6BdxTuMej41Bs59gGP+CU+Cq/4tvxH4HwR+Ab3Uqr/VGbqEAAAAAElFTkSuQmCC>)](https://app.affine.pro)
[![AFFiNE macOS M1/M2 Chip](https://img.shields.io/badge/-macOS_M_Chip%20%E2%86%92-black?style=flat-square&logo=apple&logoColor=white)](https://affine.pro/download)
[![AFFiNE macOS x64](https://img.shields.io/badge/-macOS_x86%20%E2%86%92-black?style=flat-square&logo=apple&logoColor=white)](https://affine.pro/download)
[![AFFiNE Window x64](https://img.shields.io/badge/-Windows%20%E2%86%92-blue?style=flat-square&logo=windows&logoColor=white)](https://affine.pro/download)
[![AFFiNE Linux](https://img.shields.io/badge/-Linux%20%E2%86%92-yellow?style=flat-square&logo=linux&logoColor=white)](https://affine.pro/download)
[![stars-icon]](https://github.com/toeverything/AFFiNE)
[![All Contributors][all-contributors-badge]](#contributors)
[![codecov]](https://codecov.io/gh/toeverything/AFFiNE)
@@ -36,6 +41,8 @@ See https://github.com/all-?/all-contributors/issues/361#issuecomment-637166066
</div>
---
<div align="center">
<a href="http://affine.pro"><img src="https://img.shields.io/badge/-AFFiNE-06449d?style=social&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEsAAABLCAMAAAAPkIrYAAAAP1BMVEU8b9w8b9w+b947cNw7b9w6b908b909b9w8b9w7b9w8b9w7cN08b9w7b908b9w7b9w8b907cNw8b9w8b91HcEx3NJCJAAAAFXRSTlP/3QWSgA+lHPlu6Di4XtIrxk/xRADGudUoAAAB9UlEQVR42tWYwbKjIBREG0GJKkRj/v9bZ1ZvRC99rzib11tTB9qqnKoW3/+X38vy7ifzQ1b/wk/8Q1bCv3y6Z6wFh2x2llIRGB6xRhzz6p+wVhRJD1gRZZYHrADYSyqsjFPGZtYbuFESesUysZXlcMnYyJpxTW5keQh5N7G6CUJCE2uHFNfEGiBmbmB1H4jxDawNcqbuPmtAJTtj6RZ0lpIwiR5jNmgfNtHHwLXPWfFYcS2NMdxkjac/dNaNCJPo3yf9pFuseHbDrBsRFguGs8te8Q4rXzTjVSPCIHp3FePKWbzi30xE+4zlBMmoJaGLfpLUmAmLiN4Xyibahy76WZRQMLJ2WX27on2oFvQVac8yi4p+J2forA0V8W1c++AVS1f1H6p9KKLHxk9RWKmsyB+VLC76gV65DLjokdg5KmsEMXsiDwXWSmTc9ezSoKJHoi9zUVihbMHfQOSsXB7Mrz1S1huKPde69sEsiKgNt8hYTjiWlAyENeu7IFe1D15RSEBN+yCiXw17K1RZm/w7UtJVWYN8f1ZyLlkVb2bT4vIVVrINH1dqX2YttkHmIWsfVWs646wcRFYis6fIVGpfYq1kjpGSW8kSRD+xYSmXRM0Ang9eSZioVdy/5pWaLqzIRyIpuVxYozvGf1m67I7pf/s3UXv+AP61NI2Y+BbSAAAAAElFTkSuQmCC" height=25></a>
&nbsp;
@@ -55,7 +62,6 @@ See https://github.com/all-?/all-contributors/issues/361#issuecomment-637166066
<em>See docs, canvas and tables are hyper merged with AFFiNE - just like the word affine (əˈɪn | a-fine).</em>
</div>
<br />
</div>
![img_v2_37a7cc04-ab3f-4405-ae9a-f84ceb4c948g](https://user-images.githubusercontent.com/79301703/230892907-5fd5c0c5-1665-4d75-8a35-744e0afc36a5.gif)
@@ -123,102 +129,11 @@ Thanks a lot to the community for providing such powerful and simple libraries,
# Contributors
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
<!-- prettier-ignore-start -->
<!-- markdownlint-disable -->
<table>
<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 src="https://user-images.githubusercontent.com/5910926/233382206-312428ca-094a-4579-ae06-213961ed7eab.svg" />
</a>
## Self-Host
@@ -260,11 +175,10 @@ See [LICENSE] for details.
[jobs available]: ./docs/jobs.md
[latest packages]: https://github.com/toeverything/AFFiNE/pkgs/container/affine-self-hosted
[contributor license agreement]: https://github.com/toeverything/affine/edit/master/.github/CLA.md
[affine-app-logo]: https://img.shields.io/static/v1?label=Try%20Online&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAAhpJREFUWEdjZEACtnl3MxgY/0YzMjAaMzAwcCLLUYH9/T/D/7MM/5mXHp6kPANmHiOI4Zx9Xfg3C+tKBob/zlSwiAgjGPey/vkdvneq5luwA+zy7+yhn+Vwv+89NFHFhREU7IyM/6YT4WyqK/n/nymT0Tb/1mFGBkYbqptOhIH/Gf4fYbTLv/2NBgmOCOvBSr6DHPCfWNW0UEe2A2x1uRlakiXBbtpx6jND+7KXZLmPbAdURokzeJjxwi31rrzH8OX7P5IdQbYDtnUoMXBzMMEt7Fj2imH7qU/0cQBy8MNsPHL5K0P13Of0cQB68MNsJScaSI4CHk4mhq3tSnCf3n36k0FZmh3Mn7L+DcPqgx9ICgWSHeBpxsdQESUGtgRk+eqDH+H8O09/MiR3P6atA1qTJRlsdLnhPgYlPOQQCW96wPDi3R+iHUFSCKAHP8wydEeREg0kOQA9+JOgwR1qL8CQEygC9jWp0UCSA+aVysIT3JqDHxgmr38DtlRCiIVhZZ0CPNhB6QDkEGIA0Q4gZAkuxxFyBNEOQA7ml+/+MIQ1PUAxG1kelAhB6YMYQLQDCPmQUAjhcgxRDiDWcEKOxOYIohyQGyjCEGIvANaPLfhhBiNHA6hmBBXNhABRDgCV/aBQAAFQpYMrn4PUgNTCACiXEMoNRDmAkC8okR8UDhjYRumAN8sHvGMCSkAD2jUDOWDAO6ewbDQQ3XMAy/oxKownQR0AAAAASUVORK5CYII=&color=orange&message=%E2%86%92
[rust-version-icon]: https://img.shields.io/badge/Rust-1.70.0-dea584
[stars-icon]: https://img.shields.io/github/stars/toeverything/AFFiNE.svg?style=flat&logo=github&colorB=red&label=stars
[codecov]: https://codecov.io/gh/toeverything/affine/branch/master/graphs/badge.svg?branch=master
[node-version-icon]: https://img.shields.io/badge/node-%3E=18.15.0-success
[node-version-icon]: https://img.shields.io/badge/node-%3E=18.16.0-success
[typescript-version-icon]: https://img.shields.io/github/package-json/dependency-version/toeverything/affine/dev/typescript
[react-version-icon]: https://img.shields.io/github/package-json/dependency-version/toeverything/affine/dev/react?color=rgb%2897%2C%20218%2C%20251%29
[blocksuite-icon]: https://img.shields.io/github/package-json/dependency-version/toeverything/AFFiNE/@blocksuite/store?color=6880ff&filename=apps%2Fweb%2Fpackage.json&label=blocksuite

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1 @@
tmp

View File

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

View File

@@ -1,75 +0,0 @@
import * as os from 'node:os';
import path from 'node:path';
import { Storage } from '@affine/octobase-node';
import { app, shell } from 'electron';
import { BrowserWindow, ipcMain, nativeTheme } from 'electron';
import fs from 'fs-extra';
import { parse } from 'url';
import { getExchangeTokenParams, oauthEndpoint } from './google-auth';
const AFFINE_ROOT = path.join(os.homedir(), '.affine');
fs.ensureDirSync(AFFINE_ROOT);
const logger = console;
// todo: rethink this
export const appState = {
storage: new Storage(path.join(AFFINE_ROOT, 'test.db')),
};
export const registerHandlers = () => {
ipcMain.handle('octo:workspace-sync', async (_, id) => {
return appState.storage.sync(id, '');
});
ipcMain.handle('ui:theme-change', async (_, theme) => {
nativeTheme.themeSource = theme;
logger.info('theme change', theme);
});
ipcMain.handle('ui:sidebar-visibility-change', async (_, visible) => {
// todo
const windows = BrowserWindow.getAllWindows();
windows.forEach(w => {
// hide window buttons when sidebar is not visible
w.setWindowButtonVisibility(visible);
});
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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,10 +3,12 @@ import './security-restrictions';
import { app } from 'electron';
import path from 'path';
import { registerHandlers } from './app-state';
import { logger } from '../../logger';
import { registerHandlers } from './handlers';
import { restoreOrCreateWindow } from './main-window';
import { registerProtocol } from './protocol';
if (require('electron-squirrel-startup')) app.exit();
if (process.defaultApp) {
if (process.argv.length >= 2) {
app.setAsDefaultProtocolClient('affine', process.execPath, [
@@ -21,6 +23,7 @@ if (process.defaultApp) {
*/
const isSingleInstance = app.requestSingleInstanceLock();
if (!isSingleInstance) {
logger.info('Another instance is running, exiting...');
app.quit();
process.exit(0);
}

View File

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

View File

@@ -30,23 +30,21 @@ function toAbsolutePath(url: string) {
}
export function registerProtocol() {
if (process.env.NODE_ENV === 'production') {
protocol.interceptFileProtocol('file', (request, callback) => {
const url = request.url.replace(/^file:\/\//, '');
const realpath = toAbsolutePath(url);
// console.log('realpath', realpath, 'for', url);
callback(realpath);
return true;
});
protocol.interceptFileProtocol('file', (request, callback) => {
const url = request.url.replace(/^file:\/\//, '');
const realpath = toAbsolutePath(url);
// console.log('realpath', realpath, 'for', url);
callback(realpath);
return true;
});
protocol.registerFileProtocol('assets', (request, callback) => {
const url = request.url.replace(/^assets:\/\//, '');
const realpath = toAbsolutePath(url);
// console.log('realpath', realpath, 'for', url);
callback(realpath);
return true;
});
}
protocol.registerFileProtocol('assets', (request, callback) => {
const url = request.url.replace(/^assets:\/\//, '');
const realpath = toAbsolutePath(url);
// console.log('realpath', realpath, 'for', url);
callback(realpath);
return true;
});
session.defaultSession.webRequest.onHeadersReceived(
(responseDetails, callback) => {

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,32 +1,30 @@
{
"name": "@affine/electron",
"productName": "AFFiNE",
"private": true,
"version": "0.0.0",
"version": "0.5.4-canary.11",
"author": "affine",
"description": "AFFiNE App",
"homepage": "https://github.com/toeverything/AFFiNE",
"workspaces": [
"../../packages/*",
"../../tests/fixtures"
],
"scripts": {
"dev": "cross-env NODE_ENV=development node scripts/dev.mjs",
"prod": "cross-env NODE_ENV=production node scripts/dev.mjs",
"dev": "yarn electron-rebuild && yarn cross-env DEV_SERVER_URL=http://localhost:8080 node scripts/dev.mjs",
"prod": "yarn electron-rebuild && yarn node scripts/dev.mjs",
"generate-assets": "zx scripts/generate-assets.mjs",
"package": "electron-forge package",
"make": "electron-forge make",
"make-macos-arm64": "electron-forge make --platform=darwin --arch=arm64",
"make-macos-x64": "electron-forge make --platform=darwin --arch=x64",
"make-windows-x64": "electron-forge make --platform=win32 --arch=x64",
"build:octobase-node": "yarn workspace @affine/octobase-node build",
"postinstall": "ELECTRON_RUN_AS_NODE=1 electron scripts/update-electron-vendors.mjs"
"make-linux-x64": "electron-forge make --platform=linux --arch=x64",
"rebuild:for-test": "yarn rebuild better-sqlite3",
"rebuild:for-electron": "yarn electron-rebuild",
"test": "playwright test"
},
"config": {
"forge": "./forge.config.js"
},
"main": "./dist/layers/main/index.js",
"devDependencies": {
"@affine/octobase-node": "workspace:*",
"@affine-test/kit": "workspace:*",
"@electron-forge/cli": "^6.1.1",
"@electron-forge/core": "^6.1.1",
"@electron-forge/core-utils": "^6.1.1",
@@ -35,19 +33,25 @@
"@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.10",
"@electron/rebuild": "^3.2.13",
"@electron/remote": "2.0.9",
"dts-for-context-bridge": "^0.7.1",
"electron": "24.0.0",
"esbuild": "^0.17.16",
"@types/better-sqlite3": "^7.6.4",
"@types/fs-extra": "^11.0.1",
"cross-env": "7.0.3",
"electron": "24.1.2",
"electron-log": "^5.0.0-beta.23",
"electron-squirrel-startup": "1.0.0",
"electron-window-state": "^5.0.3",
"esbuild": "^0.17.18",
"fs-extra": "^11.1.1",
"playwright": "^1.32.3",
"ts-node": "^10.9.1",
"undici": "^5.22.0",
"zx": "^7.2.1"
},
"dependencies": {
"cross-env": "7.0.3",
"electron-window-state": "^5.0.3",
"firebase": "^9.18.0",
"fs-extra": "^11.1.1",
"undici": "^5.21.2"
"better-sqlite3": "^8.3.0",
"yjs": "^13.6.0"
},
"build": {
"protocols": [
@@ -59,5 +63,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;

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 342 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

View File

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

View File

@@ -1,13 +1,9 @@
import fs from 'node:fs';
import path from 'node:path';
import * as url from 'node:url';
const __dirname = url.fileURLToPath(new URL('.', import.meta.url));
const { node } = JSON.parse(
fs.readFileSync(
path.join(__dirname, '../electron-vendors.autogen.json'),
'utf-8'
)
);
import { resolve } from 'node:path';
import { fileURLToPath } from 'url';
export const root = fileURLToPath(new URL('..', import.meta.url));
export const NODE_MAJOR_VERSION = 18;
const nativeNodeModulesPlugin = {
name: 'native-node-modules',
@@ -23,7 +19,7 @@ const nativeNodeModulesPlugin = {
const ENV_MACROS = ['AFFINE_GOOGLE_CLIENT_ID', 'AFFINE_GOOGLE_CLIENT_SECRET'];
/** @return {{main: import('esbuild').BuildOptions, preload: import('esbuild').BuildOptions}} */
export default () => {
export const config = () => {
const define = Object.fromEntries(
ENV_MACROS.map(key => [
'process.env.' + key,
@@ -32,20 +28,20 @@ export default () => {
);
return {
main: {
entryPoints: ['layers/main/src/index.ts'],
outdir: 'dist/layers/main',
entryPoints: [resolve(root, './layers/main/src/index.ts')],
outdir: resolve(root, './dist/layers/main'),
bundle: true,
target: `node${node}`,
target: `node${NODE_MAJOR_VERSION}`,
platform: 'node',
external: ['electron'],
external: ['electron', 'yjs', 'better-sqlite3'],
plugins: [nativeNodeModulesPlugin],
define: define,
},
preload: {
entryPoints: ['layers/preload/src/index.ts'],
outdir: 'dist/layers/preload',
entryPoints: [resolve(root, './layers/preload/src/index.ts')],
outdir: resolve(root, './dist/layers/preload'),
bundle: true,
target: `node${node}`,
target: `node${NODE_MAJOR_VERSION}`,
platform: 'node',
external: ['electron'],
define: define,

View File

@@ -1,16 +1,11 @@
import { spawn } from 'node:child_process';
import { readFileSync } from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { generateAsync } from 'dts-for-context-bridge';
import electronPath from 'electron';
import * as esbuild from 'esbuild';
import commonFn from './common.mjs';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
import { config, root } from './common.mjs';
/** @type 'production' | 'development'' */
const mode = (process.env.NODE_ENV = process.env.NODE_ENV || 'development');
@@ -23,9 +18,9 @@ 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(root, './dev.json'), 'utf-8');
const devEnv = JSON.parse(devJson);
Object.assign(process.env, devEnv);
} catch (err) {
@@ -35,8 +30,8 @@ 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`;
// fixme(xp): report error if app is not running on DEV_SERVER_URL
const DEV_SERVER_URL = process.env.DEV_SERVER_URL;
/** @type {ChildProcessWithoutNullStreams | null} */
let spawnProcess = null;
@@ -50,10 +45,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;
@@ -66,7 +63,7 @@ function spawnOrReloadElectron() {
spawnProcess.on('exit', process.exit);
}
const common = commonFn();
const common = config();
async function main() {
async function watchPreload(onInitialBuild) {
@@ -79,10 +76,6 @@ async function main() {
setup(build) {
let initialBuild = false;
build.onEnd(() => {
generateAsync({
input: 'layers/preload/src/**/*.ts',
output: 'layers/preload/preload.d.ts',
});
if (initialBuild) {
console.log(`[preload] has changed`);
spawnOrReloadElectron();
@@ -99,13 +92,18 @@ async function main() {
}
async function watchMain() {
const define = {
...common.main.define,
'process.env.NODE_ENV': `"${mode}"`,
};
if (DEV_SERVER_URL) {
define['process.env.DEV_SERVER_URL'] = `"${DEV_SERVER_URL}"`;
}
const mainBuild = await esbuild.context({
...common.main,
define: {
...common.main.define,
'process.env.NODE_ENV': `"${mode}"`,
'process.env.DEV_SERVER_URL': `"${process.env.DEV_SERVER_URL}"`,
},
define: define,
plugins: [
...(common.main.plugins ?? []),
{

View File

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

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,23 @@
import { resolve } from 'node:path';
import { test } from '@affine-test/kit/playwright';
import { expect } from '@playwright/test';
import { _electron as electron } from 'playwright';
test('new page', async () => {
const electronApp = await electron.launch({
args: [resolve(__dirname, '..')],
executablePath: resolve(__dirname, '../node_modules/.bin/electron'),
});
const page = await electronApp.firstWindow();
await page.getByTestId('new-page-button').click({
delay: 100,
});
await page.waitForSelector('v-line');
const flavour = await page.evaluate(
// @ts-expect-error
() => globalThis.currentWorkspace.flavour
);
expect(flavour).toBe('local');
await electronApp.close();
});

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

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

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

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

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

View File

@@ -0,0 +1,67 @@
-- CreateTable
CREATE TABLE "google_users" (
"id" VARCHAR NOT NULL,
"user_id" VARCHAR NOT NULL,
"google_id" VARCHAR NOT NULL,
CONSTRAINT "google_users_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "permissions" (
"id" VARCHAR NOT NULL,
"workspace_id" VARCHAR NOT NULL,
"user_id" VARCHAR,
"user_email" TEXT,
"type" SMALLINT NOT NULL,
"accepted" BOOLEAN NOT NULL DEFAULT false,
"created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "permissions_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "seaql_migrations" (
"version" VARCHAR NOT NULL,
"applied_at" BIGINT NOT NULL,
CONSTRAINT "seaql_migrations_pkey" PRIMARY KEY ("version")
);
-- CreateTable
CREATE TABLE "users" (
"id" VARCHAR NOT NULL,
"name" VARCHAR NOT NULL,
"email" VARCHAR NOT NULL,
"avatar_url" VARCHAR,
"token_nonce" SMALLINT DEFAULT 0,
"password" VARCHAR,
"created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "users_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "workspaces" (
"id" VARCHAR NOT NULL,
"public" BOOLEAN NOT NULL,
"type" SMALLINT NOT NULL,
"created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "workspaces_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "google_users_google_id_key" ON "google_users"("google_id");
-- CreateIndex
CREATE UNIQUE INDEX "users_email_key" ON "users"("email");
-- AddForeignKey
ALTER TABLE "google_users" ADD CONSTRAINT "google_users_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "permissions" ADD CONSTRAINT "permissions_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "permissions" ADD CONSTRAINT "permissions_workspace_id_fkey" FOREIGN KEY ("workspace_id") REFERENCES "workspaces"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

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

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

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

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

@@ -0,0 +1,52 @@
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
generator client {
provider = "prisma-client-js"
}
model google_users {
id String @id @db.VarChar
user_id String @db.VarChar
google_id String @unique @db.VarChar
users users @relation(fields: [user_id], references: [id], onDelete: Cascade)
}
model permissions {
id String @id @db.VarChar
workspace_id String @db.VarChar
user_id String? @db.VarChar
user_email String?
type Int @db.SmallInt
accepted Boolean @default(false)
created_at DateTime? @default(now()) @db.Timestamptz(6)
users users? @relation(fields: [user_id], references: [id], onDelete: Cascade)
workspaces workspaces @relation(fields: [workspace_id], references: [id], onDelete: Cascade)
}
model seaql_migrations {
version String @id @db.VarChar
applied_at BigInt
}
model users {
id String @id @db.VarChar
name String @db.VarChar
email String @unique @db.VarChar
avatar_url String? @db.VarChar
token_nonce Int? @default(0) @db.SmallInt
password String? @db.VarChar
created_at DateTime? @default(now()) @db.Timestamptz(6)
google_users google_users[]
permissions permissions[]
}
model workspaces {
id String @id @db.VarChar
public Boolean
type Int @db.SmallInt
created_at DateTime? @default(now()) @db.Timestamptz(6)
permissions permissions[]
}

View File

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

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

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

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

@@ -0,0 +1,16 @@
import { Module } from '@nestjs/common';
import { ConfigModule } from './config';
import { GqlModule } from './graphql.module';
import { BusinessModules } from './modules';
import { PrismaModule } from './prisma';
@Module({
imports: [
PrismaModule,
GqlModule,
ConfigModule.forRoot(),
...BusinessModules,
],
})
export class AppModule {}

View File

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

View File

@@ -0,0 +1,51 @@
import pkg from '../../package.json' assert { type: 'json' };
import type { AFFiNEConfig } from './def';
export const getDefaultAFFiNEConfig: () => AFFiNEConfig = () => ({
version: pkg.version,
ENV_MAP: {},
env: process.env.NODE_ENV ?? 'development',
get prod() {
return this.env === 'production';
},
get dev() {
return this.env === 'development';
},
get test() {
return this.env === 'test';
},
get deploy() {
return !this.dev && !this.test;
},
https: false,
host: 'localhost',
port: 3010,
path: '',
get origin() {
return this.dev
? 'http://localhost:8080'
: `${this.https ? 'https' : 'http'}://${this.host}${
this.host === 'localhost' ? `:${this.port}` : ''
}`;
},
get baseUrl() {
return `${this.origin}${this.path}`;
},
graphql: {
buildSchemaOptions: {
numberScalarMode: 'integer',
},
introspection: true,
playground: true,
debug: true,
},
auth: {
enableSignup: true,
enableOauth: false,
oauthProviders: {},
},
objectStorage: {
enable: false,
config: {},
},
});

View File

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

View File

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

View File

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

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

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

View File

@@ -0,0 +1,4 @@
import { UsersModule } from './users';
import { WorkspaceModule } from './workspaces';
export const BusinessModules = [WorkspaceModule, UsersModule];

View File

@@ -0,0 +1,8 @@
import { Module } from '@nestjs/common';
import { UserResolver } from './resolver';
@Module({
providers: [UserResolver],
})
export class UsersModule {}

View File

@@ -0,0 +1,37 @@
import { Args, Field, ID, ObjectType, Query, Resolver } from '@nestjs/graphql';
import type { users } from '@prisma/client';
import { PrismaService } from '../../prisma/service';
@ObjectType()
export class User implements users {
@Field(() => ID)
id!: string;
@Field({ description: 'User name' })
name!: string;
@Field({ description: 'User email' })
email!: string;
@Field({ description: 'User password', nullable: true })
password!: string;
@Field({ description: 'User avatar url', nullable: true })
avatar_url!: string;
@Field({ description: 'User token nonce', nullable: true })
token_nonce!: number;
@Field({ description: 'User created date', nullable: true })
created_at!: Date;
}
@Resolver(() => User)
export class UserResolver {
constructor(private readonly prisma: PrismaService) {}
@Query(() => User, {
name: 'user',
description: 'Get user by email',
})
async user(@Args('email') email: string) {
return this.prisma.users.findUnique({
where: { email },
});
}
}

View File

@@ -0,0 +1,8 @@
import { Module } from '@nestjs/common';
import { WorkspaceResolver } from './resolver';
@Module({
providers: [WorkspaceResolver],
})
export class WorkspaceModule {}

View File

@@ -0,0 +1,85 @@
import { randomUUID } from 'node:crypto';
import {
Args,
Field,
ID,
Mutation,
ObjectType,
Query,
registerEnumType,
Resolver,
} from '@nestjs/graphql';
import type { workspaces } from '@prisma/client';
import { PrismaService } from '../../prisma/service';
export enum WorkspaceType {
Private = 0,
Normal = 1,
}
registerEnumType(WorkspaceType, {
name: 'WorkspaceType',
description: 'Workspace type',
valuesMap: {
Normal: {
description: 'Normal workspace',
},
Private: {
description: 'Private workspace',
},
},
});
@ObjectType()
export class Workspace implements workspaces {
@Field(() => ID)
id!: string;
@Field({ description: 'is Public workspace' })
public!: boolean;
@Field(() => WorkspaceType, { description: 'Workspace type' })
type!: WorkspaceType;
@Field({ description: 'Workspace created date' })
created_at!: Date;
}
@Resolver(() => Workspace)
export class WorkspaceResolver {
constructor(private readonly prisma: PrismaService) {}
// debug only query should be removed
@Query(() => [Workspace], {
name: 'workspaces',
description: 'Get all workspaces',
})
async workspaces() {
return this.prisma.workspaces.findMany();
}
@Query(() => Workspace, {
name: 'workspace',
description: 'Get workspace by id',
})
async workspace(@Args('id') id: string) {
return this.prisma.workspaces.findUnique({
where: { id },
});
}
// create workspace
@Mutation(() => Workspace, {
name: 'createWorkspace',
description: 'Create workspace',
})
async createWorkspace() {
return this.prisma.workspaces.create({
data: {
id: randomUUID(),
type: WorkspaceType.Private,
public: false,
created_at: new Date(),
},
});
}
}

View File

@@ -0,0 +1,6 @@
import 'reflect-metadata';
import 'dotenv/config';
import { getDefaultAFFiNEConfig } from './config/default';
globalThis.AFFiNE = getDefaultAFFiNEConfig();

View File

@@ -0,0 +1,10 @@
import { Global, Module } from '@nestjs/common';
import { PrismaService } from './service';
@Global()
@Module({
providers: [PrismaService],
exports: [PrismaService],
})
export class PrismaModule {}

View File

@@ -0,0 +1,16 @@
import type { INestApplication, OnModuleInit } from '@nestjs/common';
import { Injectable } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit {
async onModuleInit() {
await this.$connect();
}
async enableShutdownHooks(app: INestApplication) {
this.$on('beforeExit', async () => {
await app.close();
});
}
}

View File

@@ -0,0 +1,100 @@
import { equal, ok } from 'node:assert';
import { afterEach, beforeEach, describe, test } from 'node:test';
import { INestApplication } from '@nestjs/common';
import { Test } from '@nestjs/testing';
import request from 'supertest';
import { AppModule } from '../app';
import { getDefaultAFFiNEConfig } from '../config/default';
const gql = '/graphql';
globalThis.AFFiNE = getDefaultAFFiNEConfig();
// please run `ts-node-esm ./scripts/init-db.ts` before running this test
describe('AppModule', () => {
let app: INestApplication;
beforeEach(async () => {
const module = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = module.createNestApplication();
await app.init();
});
afterEach(async () => {
await app.close();
});
test('should init app', async () => {
ok(typeof app === 'object');
await request(app.getHttpServer())
.post(gql)
.send({
query: `
query {
error
}
`,
})
.expect(400);
await request(app.getHttpServer())
.post(gql)
.send({
query: `
mutation {
createWorkspace {
id
type
public
created_at
}
}
`,
})
.expect(200)
.expect(res => {
ok(
typeof res.body.data.createWorkspace === 'object',
'res.body.data.createWorkspace is not an object'
);
ok(
typeof res.body.data.createWorkspace.id === 'string',
'res.body.data.createWorkspace.id is not a string'
);
ok(
typeof res.body.data.createWorkspace.type === 'string',
'res.body.data.createWorkspace.type is not a string'
);
ok(
typeof res.body.data.createWorkspace.public === 'boolean',
'res.body.data.createWorkspace.public is not a boolean'
);
ok(
typeof res.body.data.createWorkspace.created_at === 'string',
'res.body.data.createWorkspace.created_at is not a string'
);
});
});
test('should find default user', async () => {
await request(app.getHttpServer())
.post(gql)
.send({
query: `
query {
user(email: "alex.yang@example.org") {
email
avatar_url
}
}
`,
})
.expect(200)
.expect(res => {
equal(res.body.data.user.email, 'alex.yang@example.org');
});
});
});

View File

@@ -0,0 +1,35 @@
import { equal, ok } from 'node:assert';
import { beforeEach, test } from 'node:test';
import { Test } from '@nestjs/testing';
import { Config, ConfigModule } from '../config';
import { getDefaultAFFiNEConfig } from '../config/default';
globalThis.AFFiNE = getDefaultAFFiNEConfig();
let config: Config;
beforeEach(async () => {
const module = await Test.createTestingModule({
imports: [ConfigModule.forRoot()],
}).compile();
config = module.get(Config);
});
test('should be able to get config', t => {
ok(typeof config.host === 'string');
equal(config.env, 'test');
});
test('should be able to override config', async t => {
const module = await Test.createTestingModule({
imports: [
ConfigModule.forRoot({
host: 'testing',
}),
],
}).compile();
const config = module.get(Config);
ok(config.host, 'testing');
});

View File

@@ -0,0 +1,42 @@
export type DeepPartial<T> = T extends Array<infer U>
? DeepPartial<U>[]
: T extends ReadonlyArray<infer U>
? ReadonlyArray<DeepPartial<U>>
: T extends object
? {
[K in keyof T]?: DeepPartial<T[K]>;
}
: T;
type Join<Prefix, Suffixes> = Prefix extends string | number
? Suffixes extends string | number
? Prefix extends ''
? Suffixes
: `${Prefix}.${Suffixes}`
: never
: never;
export type PrimitiveType =
| string
| number
| boolean
| symbol
| null
| undefined;
export type LeafPaths<
T,
Path extends string = '',
MaxDepth extends string = '...',
Depth extends string = ''
> = Depth extends MaxDepth
? never
: T extends Record<string | number, any>
? {
[K in keyof T]-?: K extends string | number
? T[K] extends PrimitiveType
? K
: Join<K, LeafPaths<T[K], Path, MaxDepth, `${Depth}.`>>
: never;
}[keyof T]
: never;

27
apps/server/tsconfig.json Normal file
View File

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

View File

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

View File

@@ -17,6 +17,7 @@ EXPOSE_INTERNAL=1
ENABLE_DEBUG_PAGE=
ENABLE_SUBPAGE=
ENABLE_CHANGELOG=1
ENABLE_LEGACY_PROVIDER=true
# Sentry
SENTRY_AUTH_TOKEN=

View File

@@ -20,10 +20,7 @@ For more information on Next.js, take a look at the [Next.js Documentation](http
`preset.config.mjs` contains the build presets for the application. The presets are used to configure the build process for different environments. The presets are:
- `enableIndexedDBProvider`: Enables the IndexedDB provider for the application. This is used to store data in the browser.
- `enableBroadCastChannelProvider`: Enables the Broadcast Channel provider for the application. This is used to communicate between local browser tabs.
- `prefetchWorkspace`: **deprecated**
- `exposeInternal`: Exposes internal variables into `globalThis` for debugging purposes.
- `enableDebugPage`: Enables the debug page for the application. This is used for debugging purposes.
## BlockSuite Integration

View File

@@ -7,14 +7,15 @@ import { withSentryConfig } from '@sentry/nextjs';
import SentryWebpackPlugin from '@sentry/webpack-plugin';
import debugLocal from 'next-debug-local';
import preset from './preset.config.mjs';
import { blockSuiteFeatureFlags, buildFlags } from './preset.config.mjs';
import { getCommitHash, getGitVersion } from './scripts/gitInfo.mjs';
const require = createRequire(import.meta.url);
const { createVanillaExtractPlugin } = require('@vanilla-extract/next-plugin');
const withVanillaExtract = createVanillaExtractPlugin();
console.info('Runtime Preset', preset);
console.info('Build Flags', buildFlags);
console.info('Editor Flags', blockSuiteFeatureFlags);
const enableDebugLocal = path.isAbsolute(process.env.LOCAL_BLOCK_SUITE ?? '');
@@ -22,6 +23,10 @@ if (enableDebugLocal) {
console.info('Debugging local blocksuite');
}
if (process.env.COVERAGE === 'true') {
console.info('Enable coverage report');
}
const profileTarget = {
ac: '100.85.73.88:12001',
dev: '100.84.105.99:11001',
@@ -74,15 +79,17 @@ const nextConfig = {
},
reactStrictMode: true,
transpilePackages: [
'jotai-devtools',
'@affine/component',
'@affine/i18n',
'@affine/debug',
'@affine/env',
'@affine/templates',
'@toeverything/hooks',
'@affine/workspace',
'@affine/jotai',
'@toeverything/hooks',
'@toeverything/y-indexeddb',
'@toeverything/theme',
],
publicRuntimeConfig: {
PROJECT_NAME: process.env.npm_package_name ?? 'AFFiNE',
@@ -93,7 +100,8 @@ const nextConfig = {
profileTarget[process.env.API_SERVER_PROFILE || 'dev'] ??
profileTarget.dev,
editorVersion: require('./package.json').dependencies['@blocksuite/editor'],
...preset,
editorFlags: blockSuiteFeatureFlags,
...buildFlags,
},
webpack: (config, { dev, isServer }) => {
config.experiments = { ...config.experiments, topLevelAwait: true };
@@ -139,7 +147,9 @@ const nextConfig = {
},
basePath: process.env.NEXT_BASE_PATH,
assetPrefix: process.env.NEXT_ASSET_PREFIX,
pageExtensions: [...(preset.enableDebugPage ? ['tsx', 'dev.tsx'] : ['tsx'])],
pageExtensions: [
...(buildFlags.enableDebugPage ? ['tsx', 'dev.tsx'] : ['tsx']),
],
};
const baseDir = process.env.LOCAL_BLOCK_SUITE ?? '/';

View File

@@ -1,11 +1,12 @@
{
"name": "@affine/web",
"private": true,
"version": "0.5.4-canary.11",
"scripts": {
"dev": "node src/server.mjs",
"dev": "next dev",
"build": "next build",
"export": "next export",
"start": "NODE_ENV=production node src/server.mjs",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
@@ -13,54 +14,58 @@
"@affine/component": "workspace:*",
"@affine/debug": "workspace:*",
"@affine/env": "workspace:*",
"@affine/graphql": "workspace:*",
"@affine/i18n": "workspace:*",
"@affine/jotai": "workspace:*",
"@affine/templates": "workspace:*",
"@affine/workspace": "workspace:*",
"@blocksuite/blocks": "0.0.0-20230412041719-76e5b5b9-nightly",
"@blocksuite/editor": "0.0.0-20230412041719-76e5b5b9-nightly",
"@blocksuite/global": "0.0.0-20230412041719-76e5b5b9-nightly",
"@blocksuite/icons": "^2.1.9",
"@blocksuite/store": "0.0.0-20230412041719-76e5b5b9-nightly",
"@blocksuite/blocks": "0.0.0-20230424194949-ff9cc9dd-nightly",
"@blocksuite/editor": "0.0.0-20230424194949-ff9cc9dd-nightly",
"@blocksuite/global": "0.0.0-20230424194949-ff9cc9dd-nightly",
"@blocksuite/icons": "^2.1.13",
"@blocksuite/store": "0.0.0-20230424194949-ff9cc9dd-nightly",
"@dnd-kit/core": "^6.0.8",
"@dnd-kit/sortable": "^7.0.2",
"@emotion/cache": "^11.10.7",
"@emotion/react": "^11.10.6",
"@emotion/server": "^11.10.0",
"@emotion/styled": "^11.10.6",
"@mui/material": "^5.11.16",
"@sentry/nextjs": "^7.47.0",
"@mui/material": "^5.12.1",
"@react-hookz/web": "^23.0.0",
"@sentry/nextjs": "^7.49.0",
"@toeverything/hooks": "workspace:*",
"cmdk": "^0.2.0",
"css-spring": "^4.1.0",
"dayjs": "^1.11.7",
"graphql": "^16.6.0",
"jotai": "^2.0.4",
"jotai-devtools": "^0.4.0",
"lit": "^2.7.2",
"lottie-web": "^5.11.0",
"next-themes": "^0.2.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-is": "^18.2.0",
"swr": "^2.1.3",
"swr": "^2.1.4",
"y-protocols": "^1.0.5",
"yjs": "^13.5.52",
"yjs": "^13.6.0",
"zod": "^3.21.4"
},
"devDependencies": {
"@perfsee/webpack": "^1.5.0",
"@perfsee/webpack": "^1.6.0",
"@redux-devtools/extension": "^3.2.5",
"@rich-data/viewer": "^2.15.6",
"@sentry/webpack-plugin": "^1.20.0",
"@swc-jotai/debug-label": "^0.0.9",
"@swc-jotai/react-refresh": "^0.0.7",
"@types/react": "=18.0.31",
"@types/react": "^18.0.38",
"@types/react-dom": "^18.0.11",
"@types/webpack-env": "^1.18.0",
"@vanilla-extract/css": "^1.11.0",
"@vanilla-extract/next-plugin": "^2.1.1",
"@vanilla-extract/next-plugin": "^2.1.2",
"dotenv": "^16.0.3",
"eslint": "^8.38.0",
"eslint-config-next": "^13.3.0",
"eslint": "^8.39.0",
"eslint-config-next": "^13.3.1",
"next": "=13.2.3",
"next-debug-local": "^0.1.5",
"next-router-mock": "^0.9.3",
@@ -68,6 +73,7 @@
"redux": "^4.2.1",
"swc-plugin-coverage-instrument": "=0.0.14",
"typescript": "^5.0.4",
"webpack": "^5.78.0"
}
"webpack": "^5.80.0"
},
"stableVersion": "0.0.0"
}

View File

@@ -1,16 +1,30 @@
// @ts-check
import 'dotenv/config';
const config = {
enableIndexedDBProvider: Boolean(process.env.ENABLE_IDB_PROVIDER ?? '1'),
/**
* @type {import('@affine/env').BlockSuiteFeatureFlags}
*/
export const blockSuiteFeatureFlags = {
enable_database: true,
enable_slash_menu: true,
enable_edgeless_toolbar: true,
enable_block_hub: true,
enable_drag_handle: true,
enable_surface: true,
enable_linked_page: true,
};
/**
* @type {import('@affine/env').BuildFlags}
*/
export const buildFlags = {
enableLegacyCloud: process.env.ENABLE_LEGACY_PROVIDER
? process.env.ENABLE_LEGACY_PROVIDER === 'true'
: true,
enableBroadCastChannelProvider: Boolean(
process.env.ENABLE_BC_PROVIDER ?? '1'
),
prefetchWorkspace: Boolean(process.env.PREFETCH_WORKSPACE ?? '1'),
exposeInternal: Boolean(process.env.EXPOSE_INTERNAL ?? '1'),
enableDebugPage: Boolean(
process.env.ENABLE_DEBUG_PAGE ?? process.env.NODE_ENV === 'development'
),
enableSubpage: Boolean(process.env.ENABLE_SUBPAGE),
enableChangeLog: Boolean(process.env.ENABLE_CHANGELOG),
};
export default config;

View File

@@ -4,8 +4,8 @@ const SENTRY_DSN = process.env.NEXT_PUBLIC_SENTRY_DSN;
Sentry.init({
dsn: SENTRY_DSN,
tracesSampleRate: 1.0,
tracesSampleRate: 0.1,
replaysSessionSampleRate: 0.1,
replaysOnErrorSampleRate: 1.0,
replaysOnErrorSampleRate: 0.1,
integrations: [new Sentry.Replay()],
});

View File

@@ -4,5 +4,5 @@ const SENTRY_DSN = process.env.NEXT_PUBLIC_SENTRY_DSN;
Sentry.init({
dsn: SENTRY_DSN,
tracesSampleRate: 1.0,
tracesSampleRate: 0.1,
});

View File

@@ -4,5 +4,5 @@ const SENTRY_DSN = process.env.NEXT_PUBLIC_SENTRY_DSN;
Sentry.init({
dsn: SENTRY_DSN,
tracesSampleRate: 1.0,
tracesSampleRate: 0.1,
});

View File

@@ -1,40 +1,84 @@
import { DebugLogger } from '@affine/debug';
import { atomWithSyncStorage } from '@affine/jotai';
import { jotaiWorkspacesAtom } from '@affine/workspace/atom';
import type { EditorContainer } from '@blocksuite/editor';
import type { RootWorkspaceMetadata } from '@affine/workspace/atom';
import {
rootCurrentEditorAtom,
rootCurrentPageIdAtom,
rootCurrentWorkspaceIdAtom,
rootWorkspacesMetadataAtom,
} from '@affine/workspace/atom';
import { WorkspaceFlavour } from '@affine/workspace/type';
import type { Page } from '@blocksuite/store';
import { assertExists } from '@blocksuite/store';
import { atom } from 'jotai';
import { WorkspacePlugins } from '../plugins';
import type { AllWorkspace } from '../shared';
const logger = new DebugLogger('web:atoms');
// workspace necessary atoms
export const currentWorkspaceIdAtom = atom<string | null>(null);
export const currentPageIdAtom = atom<string | null>(null);
export const currentEditorAtom = atom<Readonly<EditorContainer> | null>(null);
/**
* @deprecated Use `rootCurrentWorkspaceIdAtom` directly instead.
*/
export const currentWorkspaceIdAtom = rootCurrentWorkspaceIdAtom;
// todo(himself65): move this to the workspace package
rootWorkspacesMetadataAtom.onMount = setAtom => {
function createFirst(): RootWorkspaceMetadata[] {
const Plugins = Object.values(WorkspacePlugins).sort(
(a, b) => a.loadPriority - b.loadPriority
);
return Plugins.flatMap(Plugin => {
return Plugin.Events['app:init']?.().map(
id =>
({
id,
flavour: Plugin.flavour,
} satisfies RootWorkspaceMetadata)
);
}).filter((ids): ids is RootWorkspaceMetadata => !!ids);
}
setAtom(metadata => {
if (metadata.length === 0) {
const newMetadata = createFirst();
logger.info('create first workspace', newMetadata);
return newMetadata;
}
return metadata;
});
if (environment.isDesktop) {
window.apis.workspace.list().then(workspaceIDs => {
const newMetadata = workspaceIDs.map(w => ({
id: w,
flavour: WorkspaceFlavour.LOCAL,
}));
setAtom(metadata => {
return [
...metadata,
...newMetadata.filter(m => !metadata.find(m2 => m2.id === m.id)),
];
});
});
}
};
/**
* @deprecated Use `rootCurrentPageIdAtom` directly instead.
*/
export const currentPageIdAtom = rootCurrentPageIdAtom;
/**
* @deprecated Use `rootCurrentEditorAtom` directly instead.
*/
export const currentEditorAtom = rootCurrentEditorAtom;
// modal atoms
export const openWorkspacesModalAtom = atom(false);
export const openCreateWorkspaceModalAtom = atom(false);
export const openQuickSearchModalAtom = atom(false);
export const workspacesAtom = atom<Promise<AllWorkspace[]>>(async get => {
const flavours: string[] = Object.values(WorkspacePlugins).map(
plugin => plugin.flavour
);
const jotaiWorkspaces = get(jotaiWorkspacesAtom).filter(workspace =>
flavours.includes(workspace.flavour)
);
const workspaces = await Promise.all(
jotaiWorkspaces.map(workspace => {
const plugin =
WorkspacePlugins[workspace.flavour as keyof typeof WorkspacePlugins];
assertExists(plugin);
const { CRUD } = plugin;
return CRUD.get(workspace.id);
})
);
return workspaces.filter(workspace => workspace !== null) as AllWorkspace[];
});
export { workspacesAtom } from './root';
type View = { id: string; mode: 'page' | 'edgeless' };

View File

@@ -1,4 +1,5 @@
import { getLoginStorage } from '@affine/workspace/affine/login';
import type { BlockSuiteFeatureFlags } from '@affine/env';
import { config } from '@affine/env';
import type { AffinePublicWorkspace } from '@affine/workspace/type';
import { WorkspaceFlavour } from '@affine/workspace/type';
import { createEmptyBlockSuiteWorkspace } from '@affine/workspace/utils';
@@ -9,23 +10,29 @@ import { affineApis } from '../../shared/apis';
function createPublicWorkspace(
workspaceId: string,
binary: ArrayBuffer
binary: ArrayBuffer,
singlePage = false
): AffinePublicWorkspace {
const blockSuiteWorkspace = createEmptyBlockSuiteWorkspace(
workspaceId,
(k: string) =>
// fixme: token could be expired
({ api: `api/workspace`, token: getLoginStorage()?.token }[k])
WorkspaceFlavour.AFFINE,
{
workspaceApis: affineApis,
cachePrefix: WorkspaceFlavour.PUBLIC + (singlePage ? '-single-page' : ''),
}
);
BlockSuiteWorkspace.Y.applyUpdate(
blockSuiteWorkspace.doc,
new Uint8Array(binary)
);
Object.entries(config.editorFlags).forEach(([key, value]) => {
blockSuiteWorkspace.awarenessStore.setFlag(
key as keyof BlockSuiteFeatureFlags,
value
);
});
// force disable some features
blockSuiteWorkspace.awarenessStore.setFlag('enable_block_hub', false);
blockSuiteWorkspace.awarenessStore.setFlag('enable_set_remote_flag', false);
blockSuiteWorkspace.awarenessStore.setFlag('enable_database', true);
blockSuiteWorkspace.awarenessStore.setFlag('enable_edgeless_toolbar', false);
blockSuiteWorkspace.awarenessStore.setFlag('enable_slash_menu', false);
blockSuiteWorkspace.awarenessStore.setFlag('enable_drag_handle', false);
return {
flavour: WorkspaceFlavour.PUBLIC,
@@ -49,7 +56,7 @@ export const publicPageBlockSuiteAtom = atom<Promise<AffinePublicWorkspace>>(
workspaceId,
pageId
);
return createPublicWorkspace(workspaceId, binary);
return createPublicWorkspace(workspaceId, binary, true);
}
);
export const publicWorkspaceAtom = atom<Promise<AffinePublicWorkspace>>(
@@ -59,6 +66,6 @@ export const publicWorkspaceAtom = atom<Promise<AffinePublicWorkspace>>(
throw new Error('No workspace id');
}
const binary = await affineApis.downloadWorkspace(workspaceId, true);
return createPublicWorkspace(workspaceId, binary);
return createPublicWorkspace(workspaceId, binary, false);
}
);

View File

@@ -0,0 +1,87 @@
//#region async atoms that to load the real workspace data
import { DebugLogger } from '@affine/debug';
import { config } from '@affine/env';
import {
rootCurrentWorkspaceIdAtom,
rootWorkspacesMetadataAtom,
} from '@affine/workspace/atom';
import { WorkspaceFlavour } from '@affine/workspace/type';
import { assertExists } from '@blocksuite/store';
import { atom } from 'jotai';
import { WorkspacePlugins } from '../plugins';
import type { AllWorkspace } from '../shared';
const logger = new DebugLogger('web:atoms:root');
/**
* Fetch all workspaces from the Plugin CRUD
*/
export const workspacesAtom = atom<Promise<AllWorkspace[]>>(async get => {
const flavours: string[] = Object.values(WorkspacePlugins).map(
plugin => plugin.flavour
);
const jotaiWorkspaces = get(rootWorkspacesMetadataAtom)
.filter(
workspace => flavours.includes(workspace.flavour)
// TODO: remove this when we remove the legacy cloud
)
.filter(workspace =>
!config.enableLegacyCloud
? workspace.flavour !== WorkspaceFlavour.AFFINE
: true
);
const workspaces = await Promise.all(
jotaiWorkspaces.map(workspace => {
const plugin =
WorkspacePlugins[workspace.flavour as keyof typeof WorkspacePlugins];
assertExists(plugin);
const { CRUD } = plugin;
return CRUD.get(workspace.id);
})
);
logger.info('workspaces', workspaces);
workspaces.forEach(workspace => {
if (workspace === null) {
console.warn(
'workspace is null. this should not happen. If you see this error, please report it to the developer.'
);
}
});
return workspaces.filter(workspace => workspace !== null) as AllWorkspace[];
});
/**
* This will throw an error if the workspace is not found,
* should not be used on the root component,
* use `rootCurrentWorkspaceIdAtom` instead
*/
export const rootCurrentWorkspaceAtom = atom<Promise<AllWorkspace>>(
async get => {
const metadata = get(rootWorkspacesMetadataAtom);
const targetId = get(rootCurrentWorkspaceIdAtom);
if (targetId === null) {
throw new Error(
'current workspace id is null. this should not happen. If you see this error, please report it to the developer.'
);
}
const targetWorkspace = metadata.find(meta => meta.id === targetId);
if (!targetWorkspace) {
throw new Error(`cannot find the workspace with id ${targetId}.`);
}
const workspace = await WorkspacePlugins[targetWorkspace.flavour].CRUD.get(
targetWorkspace.id
);
if (!workspace) {
throw new Error(
`cannot find the workspace with id ${targetId} in the plugin ${targetWorkspace.flavour}.`
);
}
return workspace;
}
);
// Do not add `rootCurrentWorkspacePageAtom`, this is not needed.
// It can be derived from `rootCurrentWorkspaceAtom` and `rootCurrentPageIdAtom`
//#endregion

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