mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-09 19:18:07 +00:00
Compare commits
142 Commits
v0.5.1
...
v0.5.4-can
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a4d8b65eef | ||
|
|
83dafa149c | ||
|
|
3a25f13734 | ||
|
|
db52c63d25 | ||
|
|
80f4578f76 | ||
|
|
15a7e93058 | ||
|
|
1c41731b4e | ||
|
|
a807647639 | ||
|
|
3f1293ca3c | ||
|
|
ad58b4d1e9 | ||
|
|
7e61708850 | ||
|
|
5c673a8ffc | ||
|
|
4528df07a5 | ||
|
|
b6eb017bd4 | ||
|
|
9d3b9e9848 | ||
|
|
04fc619f52 | ||
|
|
06ef6da370 | ||
|
|
d3ce90e721 | ||
|
|
9c94d05dd8 | ||
|
|
ef8dea8cb2 | ||
|
|
c27c241482 | ||
|
|
b73e9189ef | ||
|
|
c95b8e9d71 | ||
|
|
ab8669882a | ||
|
|
7ff12a6d0f | ||
|
|
339b133e3f | ||
|
|
be9095ec19 | ||
|
|
33261558f6 | ||
|
|
2ad1b770d0 | ||
|
|
74e21311dc | ||
|
|
bf83bfcf63 | ||
|
|
70d8f9a0a7 | ||
|
|
7d246f87e7 | ||
|
|
1ca9fb8ff4 | ||
|
|
2c95a0a757 | ||
|
|
a49d5ea1e2 | ||
|
|
84e2710e87 | ||
|
|
044e6da00d | ||
|
|
023cbc30ea | ||
|
|
7094385d8b | ||
|
|
f66d402cf7 | ||
|
|
971e256cd3 | ||
|
|
88a297c3c1 | ||
|
|
4bb50e8c25 | ||
|
|
acc5afdd4f | ||
|
|
9ec6768272 | ||
|
|
5a124831b8 | ||
|
|
01115f8957 | ||
|
|
a5a6203a95 | ||
|
|
4a473f5518 | ||
|
|
6cddacb953 | ||
|
|
00f44c72ce | ||
|
|
44011b4695 | ||
|
|
e0cd2e780b | ||
|
|
985bb55d82 | ||
|
|
66d0640042 | ||
|
|
e399682cad | ||
|
|
c4e90f2d8b | ||
|
|
b38b01fc98 | ||
|
|
0a0f825a15 | ||
|
|
d24c43e750 | ||
|
|
90b51031d2 | ||
|
|
1e771131b0 | ||
|
|
4d7a3e5bf1 | ||
|
|
92b1244fd7 | ||
|
|
d6b1b9f6cf | ||
|
|
b2e93433e1 | ||
|
|
97b1a31f8d | ||
|
|
4a1c15c1e9 | ||
|
|
f8d1513bb6 | ||
|
|
372377dd6b | ||
|
|
63f7b2556e | ||
|
|
c08c587efb | ||
|
|
65c1bee7f0 | ||
|
|
227f59cadc | ||
|
|
031ab2cfa2 | ||
|
|
9f33e73429 | ||
|
|
f1670af15d | ||
|
|
0d7f65ab36 | ||
|
|
3a053af50c | ||
|
|
c6be29f944 | ||
|
|
9ffe45102b | ||
|
|
6448b6a515 | ||
|
|
ba462fb79b | ||
|
|
f36d415c3d | ||
|
|
f6fb049ff2 | ||
|
|
94063352f5 | ||
|
|
c895c18deb | ||
|
|
346484ed44 | ||
|
|
18223c22ef | ||
|
|
ea9861bfa0 | ||
|
|
7be96a2e41 | ||
|
|
91c3040db7 | ||
|
|
a92d0fff4a | ||
|
|
64e5d65eb3 | ||
|
|
11de3a681f | ||
|
|
54a30bbf20 | ||
|
|
6c77006bcc | ||
|
|
143a55a6e8 | ||
|
|
19894aad5a | ||
|
|
f534e4a6dd | ||
|
|
3d70a36dd3 | ||
|
|
9c517907eb | ||
|
|
4cb6b8fdc8 | ||
|
|
134e1e8668 | ||
|
|
c76bbeab67 | ||
|
|
ec50d721ea | ||
|
|
7bbe67af43 | ||
|
|
caa292e097 | ||
|
|
73b8b805c6 | ||
|
|
084d4e043a | ||
|
|
69a9c34f11 | ||
|
|
d742cab1d5 | ||
|
|
8b3c1fb363 | ||
|
|
ec445207d6 | ||
|
|
49281e68a6 | ||
|
|
a918d6e14c | ||
|
|
7cf7187893 | ||
|
|
2383165470 | ||
|
|
43a96fe8e3 | ||
|
|
b771a2504b | ||
|
|
8d2fefb5f8 | ||
|
|
c71e5f1c96 | ||
|
|
5b96fb0db3 | ||
|
|
46cd0c5c9a | ||
|
|
261a41f8da | ||
|
|
bd387f6551 | ||
|
|
5335118e93 | ||
|
|
70313eb5ee | ||
|
|
ccd2b79d20 | ||
|
|
5ca94db5d2 | ||
|
|
d58f9db289 | ||
|
|
93e78c315c | ||
|
|
3954f309aa | ||
|
|
f902d0c324 | ||
|
|
e79fb1ae3a | ||
|
|
08d67b316c | ||
|
|
d12c00d5cb | ||
|
|
68bb538dd1 | ||
|
|
b394764b1c | ||
|
|
01a686dc28 | ||
|
|
32b206a137 |
@@ -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"
|
||||
]
|
||||
]
|
||||
}
|
||||
|
||||
@@ -4,3 +4,4 @@ dist
|
||||
out
|
||||
storybook-static
|
||||
affine-out
|
||||
_next
|
||||
|
||||
24
.eslintrc.js
24
.eslintrc.js
@@ -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
5
.github/CLA.md
vendored
@@ -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
|
||||
|
||||
5
.github/ISSUE_TEMPLATE/BUG-REPORT.yml
vendored
5
.github/ISSUE_TEMPLATE/BUG-REPORT.yml
vendored
@@ -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
|
||||
|
||||
19
.github/actions/setup-node/action.yml
vendored
19
.github/actions/setup-node/action.yml
vendored
@@ -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
6
.github/labeler.yml
vendored
@@ -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/**/*'
|
||||
|
||||
258
.github/workflows/build-master.yml
vendored
258
.github/workflows/build-master.yml
vendored
@@ -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
|
||||
105
.github/workflows/build-test-version.yml
vendored
105
.github/workflows/build-test-version.yml
vendored
@@ -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 }}
|
||||
156
.github/workflows/build.yml
vendored
156
.github/workflows/build.yml
vendored
@@ -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
|
||||
|
||||
|
||||
203
.github/workflows/release-desktop-app.yml
vendored
203
.github/workflows/release-desktop-app.yml
vendored
@@ -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
19
.github/workflows/release.yml
vendored
Normal 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
4
.gitignore
vendored
@@ -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/
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
# check lockfile is up to date
|
||||
yarn install
|
||||
cd ./apps/eletron && yarn install
|
||||
|
||||
# lint staged files
|
||||
yarn exec lint-staged
|
||||
|
||||
@@ -1,2 +1 @@
|
||||
pnpm-lock.yaml
|
||||
apps/electron/layers/preload/preload.d.ts
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"singleQuote": true,
|
||||
"trailingComma": "es5",
|
||||
"tabWidth": 2,
|
||||
"arrowParens": "avoid"
|
||||
"singleQuote": true,
|
||||
"trailingComma": "es5",
|
||||
"tabWidth": 2,
|
||||
"arrowParens": "avoid"
|
||||
}
|
||||
|
||||
550
.yarn/plugins/@yarnpkg/plugin-version.cjs
vendored
Normal file
550
.yarn/plugins/@yarnpkg/plugin-version.cjs
vendored
Normal file
File diff suppressed because one or more lines are too long
28
.yarn/plugins/@yarnpkg/plugin-workspace-tools.cjs
vendored
Normal file
28
.yarn/plugins/@yarnpkg/plugin-workspace-tools.cjs
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -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
112
README.md
@@ -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)
|
||||
[?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)
|
||||
[](https://affine.pro/download)
|
||||
[](https://affine.pro/download)
|
||||
[](https://affine.pro/download)
|
||||
[](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>
|
||||
|
||||
@@ -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 (əˈfʌɪn | a-fine).</em>
|
||||
</div>
|
||||
<br />
|
||||
</div>
|
||||
|
||||

|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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__'
|
||||
@@ -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
|
||||
|
||||
@@ -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`;
|
||||
},
|
||||
|
||||
3
apps/electron/layers/logger.ts
Normal file
3
apps/electron/layers/logger.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import log from 'electron-log';
|
||||
|
||||
export const logger = log;
|
||||
5
apps/electron/layers/main-events.ts
Normal file
5
apps/electron/layers/main-events.ts
Normal 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;
|
||||
}
|
||||
1
apps/electron/layers/main/src/__tests__/.gitignore
vendored
Normal file
1
apps/electron/layers/main/src/__tests__/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
tmp
|
||||
226
apps/electron/layers/main/src/__tests__/handlers.spec.ts
Normal file
226
apps/electron/layers/main/src/__tests__/handlers.spec.ts
Normal 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']);
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
});
|
||||
};
|
||||
9
apps/electron/layers/main/src/context.ts
Normal file
9
apps/electron/layers/main/src/context.ts
Normal 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;
|
||||
34
apps/electron/layers/main/src/data/export.ts
Normal file
34
apps/electron/layers/main/src/data/export.ts
Normal 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);
|
||||
// });
|
||||
// }
|
||||
7
apps/electron/layers/main/src/data/fs-watch.ts
Normal file
7
apps/electron/layers/main/src/data/fs-watch.ts
Normal 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();
|
||||
}
|
||||
174
apps/electron/layers/main/src/data/sqlite.ts
Normal file
174
apps/electron/layers/main/src/data/sqlite.ts
Normal 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);
|
||||
}
|
||||
30
apps/electron/layers/main/src/data/workspace.ts
Normal file
30
apps/electron/layers/main/src/data/workspace.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
227
apps/electron/layers/main/src/handlers.ts
Normal file
227
apps/electron/layers/main/src/handlers.ts
Normal 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();
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
14
apps/electron/layers/main/src/send-main-event.ts
Normal file
14
apps/electron/layers/main/src/send-main-event.ts
Normal 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));
|
||||
}
|
||||
14
apps/electron/layers/preload/preload.d.ts
vendored
14
apps/electron/layers/preload/preload.d.ts
vendored
@@ -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;
|
||||
}
|
||||
|
||||
81
apps/electron/layers/preload/src/affine-apis.ts
Normal file
81
apps/electron/layers/preload/src/affine-apis.ts
Normal 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 };
|
||||
@@ -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);
|
||||
|
||||
@@ -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": "*"
|
||||
}
|
||||
}
|
||||
|
||||
27
apps/electron/playwright.config.ts
Normal file
27
apps/electron/playwright.config.ts
Normal 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;
|
||||
BIN
apps/electron/resources/icons/dmg-background.png
Normal file
BIN
apps/electron/resources/icons/dmg-background.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 105 KiB |
BIN
apps/electron/resources/icons/dmg-background@2x.png
Normal file
BIN
apps/electron/resources/icons/dmg-background@2x.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 342 KiB |
BIN
apps/electron/resources/icons/icon_beta.icns
Normal file
BIN
apps/electron/resources/icons/icon_beta.icns
Normal file
Binary file not shown.
BIN
apps/electron/resources/icons/icon_beta.ico
Normal file
BIN
apps/electron/resources/icons/icon_beta.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 62 KiB |
BIN
apps/electron/resources/icons/icon_canary.icns
Normal file
BIN
apps/electron/resources/icons/icon_canary.icns
Normal file
Binary file not shown.
BIN
apps/electron/resources/icons/icon_canary.ico
Normal file
BIN
apps/electron/resources/icons/icon_canary.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 62 KiB |
17
apps/electron/scripts/build-ci.mts
Executable file
17
apps/electron/scripts/build-ci.mts
Executable 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.');
|
||||
@@ -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,
|
||||
|
||||
@@ -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 ?? []),
|
||||
{
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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 })
|
||||
);
|
||||
23
apps/electron/tests/basic.spec.ts
Normal file
23
apps/electron/tests/basic.spec.ts
Normal 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();
|
||||
});
|
||||
8
apps/electron/tests/tsconfig.json
Normal file
8
apps/electron/tests/tsconfig.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "../../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"noEmit": true
|
||||
},
|
||||
"include": ["**.spec.ts", "**.test.ts"]
|
||||
}
|
||||
27
apps/electron/tsconfig.json
Normal file
27
apps/electron/tsconfig.json
Normal 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"
|
||||
}
|
||||
}
|
||||
11
apps/electron/tsconfig.node.json
Normal file
11
apps/electron/tsconfig.node.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"resolveJsonModule": true,
|
||||
"moduleResolution": "Node",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["./scripts", "package.json"]
|
||||
}
|
||||
17733
apps/electron/yarn.lock
17733
apps/electron/yarn.lock
File diff suppressed because it is too large
Load Diff
1
apps/server/.env.example
Normal file
1
apps/server/.env.example
Normal file
@@ -0,0 +1 @@
|
||||
DATABASE_URL="postgresql://affine@localhost:5432/affine"
|
||||
2
apps/server/.gitignore
vendored
Normal file
2
apps/server/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
.env
|
||||
src/schema.gql
|
||||
67
apps/server/migrations/20230418212743_init/migration.sql
Normal file
67
apps/server/migrations/20230418212743_init/migration.sql
Normal 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;
|
||||
3
apps/server/migrations/migration_lock.toml
Normal file
3
apps/server/migrations/migration_lock.toml
Normal 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
79
apps/server/package.json
Normal 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
52
apps/server/schema.prisma
Normal 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[]
|
||||
}
|
||||
24
apps/server/scripts/init-db.ts
Normal file
24
apps/server/scripts/init-db.ts
Normal 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
56
apps/server/scripts/run-test.ts
Executable 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
16
apps/server/src/app.ts
Normal 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 {}
|
||||
202
apps/server/src/config/def.ts
Normal file
202
apps/server/src/config/def.ts
Normal 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>;
|
||||
}
|
||||
>
|
||||
>;
|
||||
};
|
||||
}
|
||||
51
apps/server/src/config/default.ts
Normal file
51
apps/server/src/config/default.ts
Normal 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: {},
|
||||
},
|
||||
});
|
||||
15
apps/server/src/config/env.ts
Normal file
15
apps/server/src/config/env.ts
Normal 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]);
|
||||
}
|
||||
}
|
||||
69
apps/server/src/config/index.ts
Normal file
69
apps/server/src/config/index.ts
Normal 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';
|
||||
30
apps/server/src/graphql.module.ts
Normal file
30
apps/server/src/graphql.module.ts
Normal 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
20
apps/server/src/index.ts
Normal 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);
|
||||
4
apps/server/src/modules/index.ts
Normal file
4
apps/server/src/modules/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { UsersModule } from './users';
|
||||
import { WorkspaceModule } from './workspaces';
|
||||
|
||||
export const BusinessModules = [WorkspaceModule, UsersModule];
|
||||
8
apps/server/src/modules/users/index.ts
Normal file
8
apps/server/src/modules/users/index.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { UserResolver } from './resolver';
|
||||
|
||||
@Module({
|
||||
providers: [UserResolver],
|
||||
})
|
||||
export class UsersModule {}
|
||||
37
apps/server/src/modules/users/resolver.ts
Normal file
37
apps/server/src/modules/users/resolver.ts
Normal 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 },
|
||||
});
|
||||
}
|
||||
}
|
||||
8
apps/server/src/modules/workspaces/index.ts
Normal file
8
apps/server/src/modules/workspaces/index.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { WorkspaceResolver } from './resolver';
|
||||
|
||||
@Module({
|
||||
providers: [WorkspaceResolver],
|
||||
})
|
||||
export class WorkspaceModule {}
|
||||
85
apps/server/src/modules/workspaces/resolver.ts
Normal file
85
apps/server/src/modules/workspaces/resolver.ts
Normal 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(),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
6
apps/server/src/prelude.ts
Normal file
6
apps/server/src/prelude.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import 'reflect-metadata';
|
||||
import 'dotenv/config';
|
||||
|
||||
import { getDefaultAFFiNEConfig } from './config/default';
|
||||
|
||||
globalThis.AFFiNE = getDefaultAFFiNEConfig();
|
||||
10
apps/server/src/prisma/index.ts
Normal file
10
apps/server/src/prisma/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Global, Module } from '@nestjs/common';
|
||||
|
||||
import { PrismaService } from './service';
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
providers: [PrismaService],
|
||||
exports: [PrismaService],
|
||||
})
|
||||
export class PrismaModule {}
|
||||
16
apps/server/src/prisma/service.ts
Normal file
16
apps/server/src/prisma/service.ts
Normal 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();
|
||||
});
|
||||
}
|
||||
}
|
||||
100
apps/server/src/tests/app.e2e.ts
Normal file
100
apps/server/src/tests/app.e2e.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
});
|
||||
35
apps/server/src/tests/config.spec.ts
Normal file
35
apps/server/src/tests/config.spec.ts
Normal 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');
|
||||
});
|
||||
42
apps/server/src/utils/types.ts
Normal file
42
apps/server/src/utils/types.ts
Normal 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
27
apps/server/tsconfig.json
Normal 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"
|
||||
}
|
||||
}
|
||||
11
apps/server/tsconfig.node.json
Normal file
11
apps/server/tsconfig.node.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"resolveJsonModule": true,
|
||||
"moduleResolution": "Node",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["./scripts", "package.json"]
|
||||
}
|
||||
@@ -17,6 +17,7 @@ EXPOSE_INTERNAL=1
|
||||
ENABLE_DEBUG_PAGE=
|
||||
ENABLE_SUBPAGE=
|
||||
ENABLE_CHANGELOG=1
|
||||
ENABLE_LEGACY_PROVIDER=true
|
||||
|
||||
# Sentry
|
||||
SENTRY_AUTH_TOKEN=
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 ?? '/';
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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()],
|
||||
});
|
||||
|
||||
@@ -4,5 +4,5 @@ const SENTRY_DSN = process.env.NEXT_PUBLIC_SENTRY_DSN;
|
||||
|
||||
Sentry.init({
|
||||
dsn: SENTRY_DSN,
|
||||
tracesSampleRate: 1.0,
|
||||
tracesSampleRate: 0.1,
|
||||
});
|
||||
|
||||
@@ -4,5 +4,5 @@ const SENTRY_DSN = process.env.NEXT_PUBLIC_SENTRY_DSN;
|
||||
|
||||
Sentry.init({
|
||||
dsn: SENTRY_DSN,
|
||||
tracesSampleRate: 1.0,
|
||||
tracesSampleRate: 0.1,
|
||||
});
|
||||
|
||||
@@ -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' };
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
);
|
||||
|
||||
87
apps/web/src/atoms/root.ts
Normal file
87
apps/web/src/atoms/root.ts
Normal 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
Reference in New Issue
Block a user