Compare commits

..

1 Commits

Author SHA1 Message Date
DarkSky 6557e5d01d feat: init disk remote source 2026-02-27 02:39:53 +08:00
2311 changed files with 69198 additions and 167999 deletions
-5
View File
@@ -19,8 +19,3 @@ rustflags = [
# pthread_key_create() destructors and segfault after a DSO unloading
[target.'cfg(all(target_env = "gnu", not(target_os = "windows")))']
rustflags = ["-C", "link-args=-Wl,-z,nodelete"]
# Temporary local llm_adapter override.
# Uncomment when verifying AFFiNE against the sibling llm_adapter workspace.
# [patch.crates-io]
# llm_adapter = { path = "../llm_adapter" }
+80 -111
View File
@@ -62,18 +62,6 @@
"concurrency": 10
}
},
"queues.calendar": {
"type": "object",
"description": "The config for calendar job queue\n@default {\"concurrency\":4}",
"properties": {
"concurrency": {
"type": "number"
}
},
"default": {
"concurrency": 4
}
},
"queues.doc": {
"type": "object",
"description": "The config for doc job queue\n@default {\"concurrency\":1}",
@@ -135,17 +123,17 @@
},
"throttlers.default": {
"type": "object",
"description": "The config for the default throttler.\n@default {\"ttl\":60000,\"limit\":120}",
"description": "The config for the default throttler.\n@default {\"ttl\":60,\"limit\":120}",
"default": {
"ttl": 60000,
"ttl": 60,
"limit": 120
}
},
"throttlers.strict": {
"type": "object",
"description": "The config for the strict throttler.\n@default {\"ttl\":60000,\"limit\":20}",
"description": "The config for the strict throttler.\n@default {\"ttl\":60,\"limit\":20}",
"default": {
"ttl": 60000,
"ttl": 60,
"limit": 20
}
}
@@ -175,11 +163,6 @@
"description": "Whether require email verification before accessing restricted resources(not implemented).\n@default true",
"default": true
},
"newAccountShareActionDelay": {
"type": "number",
"description": "Minimum account age in seconds before new accounts can invite members or create share links.\n@default 86400",
"default": 86400
},
"passwordRequirements": {
"type": "object",
"description": "The password strength requirements when set new password.\n@default {\"min\":8,\"max\":32}",
@@ -214,8 +197,8 @@
"properties": {
"SMTP.name": {
"type": "string",
"description": "Hostname used for SMTP HELO/EHLO (e.g. mail.example.com). Leave empty to use the system hostname.\n@default \"\"\n@environment `MAILER_SERVERNAME`",
"default": ""
"description": "Name of the email server (e.g. your domain name)\n@default \"AFFiNE Server\"\n@environment `MAILER_SERVERNAME`",
"default": "AFFiNE Server"
},
"SMTP.host": {
"type": "string",
@@ -254,8 +237,8 @@
},
"fallbackSMTP.name": {
"type": "string",
"description": "Hostname used for fallback SMTP HELO/EHLO (e.g. mail.example.com). Leave empty to use the system hostname.\n@default \"\"",
"default": ""
"description": "Name of the fallback email server (e.g. your domain name)\n@default \"AFFiNE Server\"",
"default": "AFFiNE Server"
},
"fallbackSMTP.host": {
"type": "string",
@@ -305,22 +288,6 @@
}
}
},
"permission": {
"type": "object",
"description": "Configuration for permission module",
"properties": {
"readModel": {
"type": "string",
"description": "Permission data source for Rust evaluation\n@default \"projection\"\n@environment `AFFINE_PERMISSION_READ_MODEL`",
"default": "projection"
},
"fallbackLegacyLoader": {
"type": "boolean",
"description": "Fallback from projection loader to legacy loader when projection input loading fails\n@default false\n@environment `AFFINE_PERMISSION_FALLBACK_LEGACY_LOADER`",
"default": false
}
}
},
"storages": {
"type": "object",
"description": "Configuration for storages module",
@@ -374,7 +341,7 @@
"properties": {
"endpoint": {
"type": "string",
"description": "The S3 compatible endpoint (used by aws-s3 provider). Optional; if omitted, endpoint is derived from region."
"description": "The S3 compatible endpoint. Example: \"https://s3.us-east-1.amazonaws.com\" or \"https://<account>.r2.cloudflarestorage.com\"."
},
"region": {
"type": "string",
@@ -441,6 +408,10 @@
"type": "object",
"description": "The config for the S3 compatible storage provider.",
"properties": {
"endpoint": {
"type": "string",
"description": "The S3 compatible endpoint. Example: \"https://s3.us-east-1.amazonaws.com\" or \"https://<account>.r2.cloudflarestorage.com\"."
},
"region": {
"type": "string",
"description": "The region for the storage provider. Example: \"us-east-1\" or \"auto\" for R2."
@@ -490,13 +461,6 @@
"type": "string",
"description": "The account id for the cloudflare r2 storage provider."
},
"jurisdiction": {
"type": "string",
"enum": [
"eu"
],
"description": "Optional jurisdiction for the cloudflare r2 endpoint. Set to \"eu\" for EU buckets."
},
"usePresignedURL": {
"type": "object",
"description": "The presigned url config for the cloudflare r2 storage provider.",
@@ -572,7 +536,7 @@
"properties": {
"endpoint": {
"type": "string",
"description": "The S3 compatible endpoint (used by aws-s3 provider). Optional; if omitted, endpoint is derived from region."
"description": "The S3 compatible endpoint. Example: \"https://s3.us-east-1.amazonaws.com\" or \"https://<account>.r2.cloudflarestorage.com\"."
},
"region": {
"type": "string",
@@ -639,6 +603,10 @@
"type": "object",
"description": "The config for the S3 compatible storage provider.",
"properties": {
"endpoint": {
"type": "string",
"description": "The S3 compatible endpoint. Example: \"https://s3.us-east-1.amazonaws.com\" or \"https://<account>.r2.cloudflarestorage.com\"."
},
"region": {
"type": "string",
"description": "The region for the storage provider. Example: \"us-east-1\" or \"auto\" for R2."
@@ -688,13 +656,6 @@
"type": "string",
"description": "The account id for the cloudflare r2 storage provider."
},
"jurisdiction": {
"type": "string",
"enum": [
"eu"
],
"description": "Optional jurisdiction for the cloudflare r2 endpoint. Set to \"eu\" for EU buckets."
},
"usePresignedURL": {
"type": "object",
"description": "The presigned url config for the cloudflare r2 storage provider.",
@@ -882,14 +843,11 @@
"properties": {
"google": {
"type": "object",
"description": "Google Calendar integration config\n@default {\"enabled\":false,\"allowNewAccounts\":true,\"clientId\":\"\",\"clientSecret\":\"\",\"externalWebhookUrl\":\"\",\"webhookVerificationToken\":\"\",\"requestTimeoutMs\":10000}\n@link https://developers.google.com/calendar/api/guides/push",
"description": "Google Calendar integration config\n@default {\"enabled\":false,\"clientId\":\"\",\"clientSecret\":\"\",\"externalWebhookUrl\":\"\",\"webhookVerificationToken\":\"\"}\n@link https://developers.google.com/calendar/api/guides/push",
"properties": {
"enabled": {
"type": "boolean"
},
"allowNewAccounts": {
"type": "boolean"
},
"clientId": {
"type": "string"
},
@@ -901,19 +859,14 @@
},
"webhookVerificationToken": {
"type": "string"
},
"requestTimeoutMs": {
"type": "number"
}
},
"default": {
"enabled": false,
"allowNewAccounts": true,
"clientId": "",
"clientSecret": "",
"externalWebhookUrl": "",
"webhookVerificationToken": "",
"requestTimeoutMs": 10000
"webhookVerificationToken": ""
}
},
"caldav": {
@@ -1016,35 +969,24 @@
"description": "Whether to enable the copilot plugin. <br> Document: <a href=\"https://docs.affine.pro/self-host-affine/administer/ai\" target=\"_blank\">https://docs.affine.pro/self-host-affine/administer/ai</a>\n@default false",
"default": false
},
"byok.enabled": {
"type": "boolean",
"description": "Whether to enable workspace BYOK.\n@default true",
"default": true
},
"byok.allowedProviders": {
"type": "array",
"description": "The allowlist for workspace BYOK providers.\n@default [\"openai\",\"anthropic\",\"gemini\",\"fal\"]",
"default": [
"openai",
"anthropic",
"gemini",
"fal"
]
},
"byok.allowCustomEndpoint": {
"type": "boolean",
"description": "Whether workspace BYOK custom endpoints are accepted.\n@default false",
"default": false
},
"providers.profiles": {
"type": "array",
"description": "The profile list for copilot providers.\n@default []",
"default": []
},
"providers.defaults": {
"scenarios": {
"type": "object",
"description": "The default provider ids for model output types and global fallback.\n@default {}",
"default": {}
"description": "Use custom models in scenarios and override default settings.\n@default {\"override_enabled\":false,\"scenarios\":{\"audio_transcribing\":\"gemini-2.5-flash\",\"chat\":\"gemini-2.5-flash\",\"embedding\":\"gemini-embedding-001\",\"image\":\"gpt-image-1\",\"rerank\":\"gpt-4.1\",\"coding\":\"claude-sonnet-4-5@20250929\",\"complex_text_generation\":\"gpt-4o-2024-08-06\",\"quick_decision_making\":\"gpt-5-mini\",\"quick_text_generation\":\"gemini-2.5-flash\",\"polish_and_summarize\":\"gemini-2.5-flash\"}}",
"default": {
"override_enabled": false,
"scenarios": {
"audio_transcribing": "gemini-2.5-flash",
"chat": "gemini-2.5-flash",
"embedding": "gemini-embedding-001",
"image": "gpt-image-1",
"rerank": "gpt-4.1",
"coding": "claude-sonnet-4-5@20250929",
"complex_text_generation": "gpt-4o-2024-08-06",
"quick_decision_making": "gpt-5-mini",
"quick_text_generation": "gemini-2.5-flash",
"polish_and_summarize": "gemini-2.5-flash"
}
}
},
"providers.openai": {
"type": "object",
@@ -1054,14 +996,6 @@
"baseURL": "https://api.openai.com/v1"
}
},
"providers.cloudflareWorkersAi": {
"type": "object",
"description": "The config for the Cloudflare Workers AI provider.\n@default {\"apiToken\":\"\",\"accountId\":\"\"}",
"default": {
"apiToken": "",
"accountId": ""
}
},
"providers.fal": {
"type": "object",
"description": "The config for the fal provider.\n@default {\"apiKey\":\"\"}",
@@ -1112,6 +1046,13 @@
},
"default": {}
},
"providers.perplexity": {
"type": "object",
"description": "The config for the perplexity provider.\n@default {\"apiKey\":\"\"}",
"default": {
"apiKey": ""
}
},
"providers.anthropic": {
"type": "object",
"description": "The config for the anthropic provider.\n@default {\"apiKey\":\"\",\"baseURL\":\"https://api.anthropic.com/v1\"}",
@@ -1155,6 +1096,11 @@
},
"default": {}
},
"providers.morph": {
"type": "object",
"description": "The config for the morph provider.\n@default {}",
"default": {}
},
"unsplash": {
"type": "object",
"description": "The config for the unsplash key.\n@default {\"key\":\"\"}",
@@ -1213,7 +1159,7 @@
"properties": {
"endpoint": {
"type": "string",
"description": "The S3 compatible endpoint (used by aws-s3 provider). Optional; if omitted, endpoint is derived from region."
"description": "The S3 compatible endpoint. Example: \"https://s3.us-east-1.amazonaws.com\" or \"https://<account>.r2.cloudflarestorage.com\"."
},
"region": {
"type": "string",
@@ -1280,6 +1226,10 @@
"type": "object",
"description": "The config for the S3 compatible storage provider.",
"properties": {
"endpoint": {
"type": "string",
"description": "The S3 compatible endpoint. Example: \"https://s3.us-east-1.amazonaws.com\" or \"https://<account>.r2.cloudflarestorage.com\"."
},
"region": {
"type": "string",
"description": "The region for the storage provider. Example: \"us-east-1\" or \"auto\" for R2."
@@ -1329,13 +1279,6 @@
"type": "string",
"description": "The account id for the cloudflare r2 storage provider."
},
"jurisdiction": {
"type": "string",
"enum": [
"eu"
],
"description": "Optional jurisdiction for the cloudflare r2 endpoint. Set to \"eu\" for EU buckets."
},
"usePresignedURL": {
"type": "object",
"description": "The presigned url config for the cloudflare r2 storage provider.",
@@ -1410,6 +1353,22 @@
}
}
},
"customerIo": {
"type": "object",
"description": "Configuration for customerIo module",
"properties": {
"enabled": {
"type": "boolean",
"description": "Enable customer.io integration\n@default false",
"default": false
},
"token": {
"type": "string",
"description": "Customer.io token\n@default \"\"",
"default": ""
}
}
},
"oauth": {
"type": "object",
"description": "Configuration for oauth module",
@@ -1508,6 +1467,16 @@
"description": "Whether enable lifetime price and allow user to pay for it.\n@default true",
"default": true
},
"apiKey": {
"type": "string",
"description": "[Deprecated] Stripe API key. Use payment.stripe.apiKey instead.\n@default \"\"\n@environment `STRIPE_API_KEY`",
"default": ""
},
"webhookKey": {
"type": "string",
"description": "[Deprecated] Stripe webhook key. Use payment.stripe.webhookKey instead.\n@default \"\"\n@environment `STRIPE_WEBHOOK_KEY`",
"default": ""
},
"stripe": {
"type": "object",
"description": "Stripe sdk options and credentials\n@default {\"apiKey\":\"\",\"webhookKey\":\"\"}\n@link https://docs.stripe.com/api",
+4 -17
View File
@@ -50,21 +50,8 @@ runs:
# https://github.com/tree-sitter/tree-sitter/issues/4186
# pass -D_BSD_SOURCE to clang to fix the tree-sitter build issue
run: |
if [[ "${{ inputs.target }}" == "aarch64-unknown-linux-gnu" ]]; then
# napi cross-toolchain 1.0.3 headers miss AT_HWCAP2 in elf.h
echo "CC=clang -D_BSD_SOURCE -DAT_HWCAP2=26" >> "$GITHUB_ENV"
echo "TARGET_CC=clang -D_BSD_SOURCE -DAT_HWCAP2=26" >> "$GITHUB_ENV"
else
echo "CC=clang -D_BSD_SOURCE" >> "$GITHUB_ENV"
echo "TARGET_CC=clang -D_BSD_SOURCE" >> "$GITHUB_ENV"
fi
- name: Prepare cache key
id: cache-key
shell: bash
run: |
shared_key="$(printf '%s' "${{ inputs.target }}-${{ inputs.package }}" | tr -c 'A-Za-z0-9_.-' '-')"
echo "shared-key=$shared_key" >> "$GITHUB_OUTPUT"
echo "CC=clang -D_BSD_SOURCE" >> "$GITHUB_ENV"
echo "TARGET_CC=clang -D_BSD_SOURCE" >> "$GITHUB_ENV"
- name: Cache cargo
uses: Swatinem/rust-cache@v2
@@ -72,7 +59,7 @@ runs:
with:
workspaces: ${{ env.DEV_DRIVE_WORKSPACE }}
save-if: ${{ github.ref_name == 'canary' }}
shared-key: ${{ steps.cache-key.outputs.shared-key }}
shared-key: ${{ inputs.target }}-${{ inputs.package }}
env:
CARGO_HOME: ${{ env.DEV_DRIVE }}/.cargo
RUSTUP_HOME: ${{ env.DEV_DRIVE }}/.rustup
@@ -82,7 +69,7 @@ runs:
if: ${{ runner.os != 'Windows' }}
with:
save-if: ${{ github.ref_name == 'canary' }}
shared-key: ${{ steps.cache-key.outputs.shared-key }}
shared-key: ${{ inputs.target }}-${{ inputs.package }}
- name: Build
shell: bash
+7 -7
View File
@@ -53,7 +53,7 @@ runs:
fi
- name: Setup Node.js
uses: actions/setup-node@v6
uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
registry-url: https://npm.pkg.github.com
@@ -93,7 +93,7 @@ runs:
run: node -e "const p = $(yarn config cacheFolder --json).effective; console.log('yarn_global_cache=' + p)" >> $GITHUB_OUTPUT
- name: Cache non-full yarn cache on Linux
uses: actions/cache@v5
uses: actions/cache@v4
if: ${{ inputs.full-cache != 'true' && runner.os == 'Linux' }}
with:
path: |
@@ -105,7 +105,7 @@ runs:
# and the decompression performance on Windows is very terrible
# so we reduce the number of cached files on non-Linux systems by remove node_modules from cache path.
- name: Cache non-full yarn cache on non-Linux
uses: actions/cache@v5
uses: actions/cache@v4
if: ${{ inputs.full-cache != 'true' && runner.os != 'Linux' }}
with:
path: |
@@ -113,7 +113,7 @@ runs:
key: node_modules-cache-${{ github.job }}-${{ runner.os }}-${{ runner.arch }}-${{ steps.system-info.outputs.name }}-${{ steps.system-info.outputs.release }}-${{ steps.system-info.outputs.version }}
- name: Cache full yarn cache on Linux
uses: actions/cache@v5
uses: actions/cache@v4
if: ${{ inputs.full-cache == 'true' && runner.os == 'Linux' }}
with:
path: |
@@ -122,7 +122,7 @@ runs:
key: node_modules-cache-full-${{ runner.os }}-${{ runner.arch }}-${{ steps.system-info.outputs.name }}-${{ steps.system-info.outputs.release }}-${{ steps.system-info.outputs.version }}
- name: Cache full yarn cache on non-Linux
uses: actions/cache@v5
uses: actions/cache@v4
if: ${{ inputs.full-cache == 'true' && runner.os != 'Linux' }}
with:
path: |
@@ -154,7 +154,7 @@ runs:
# Note: Playwright's cache directory is hard coded because that's what it
# says to do in the docs. There doesn't appear to be a command that prints
# it out for us.
- uses: actions/cache@v5
- uses: actions/cache@v4
id: playwright-cache
if: ${{ inputs.playwright-install == 'true' }}
with:
@@ -189,7 +189,7 @@ runs:
run: |
echo "version=$(yarn why --json electron | grep -h 'workspace:.' | jq --raw-output '.children[].locator' | sed -e 's/@playwright\/test@.*://' | head -n 1)" >> $GITHUB_OUTPUT
- uses: actions/cache@v5
- uses: actions/cache@v4
id: electron-cache
if: ${{ inputs.electron-install == 'true' }}
with:
+1 -1
View File
@@ -3,4 +3,4 @@ name: affine
description: AFFiNE cloud chart
type: application
version: 0.0.0
appVersion: "0.27.0"
appVersion: "0.26.3"
+1 -1
View File
@@ -3,7 +3,7 @@ name: doc
description: AFFiNE doc server
type: application
version: 0.0.0
appVersion: "0.27.0"
appVersion: "0.26.3"
dependencies:
- name: gcloud-sql-proxy
version: 0.0.0
+1 -1
View File
@@ -3,7 +3,7 @@ name: front
description: AFFiNE front server
type: application
version: 0.0.0
appVersion: "0.27.0"
appVersion: "0.26.3"
dependencies:
- name: gcloud-sql-proxy
version: 0.0.0
@@ -25,22 +25,6 @@ spec:
serviceAccountName: {{ include "front.serviceAccountName" . }}
securityContext:
{{- toYaml .Values.podSecurityContext | nindent 8 }}
{{- with .Values.global.database.gcloud }}
{{- if .enabled }}
initContainers:
- name: wait-for-cloud-sql-proxy
image: busybox:1.36.1
imagePullPolicy: IfNotPresent
command:
- /bin/sh
- -ec
- |
until wget -q -T 2 -O /dev/null "http://{{ $.Values.global.database.host }}:9801/startup"; do
echo "waiting for cloud sql proxy to become ready"
sleep 2
done
{{- end }}
{{- end }}
containers:
- name: {{ .Chart.Name }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
+2 -2
View File
@@ -31,10 +31,10 @@ podSecurityContext:
resources:
limits:
cpu: '1'
memory: 6Gi
memory: 4Gi
requests:
cpu: '1'
memory: 4Gi
memory: 2Gi
probe:
initialDelaySeconds: 20
@@ -12,10 +12,6 @@ spec:
targetPort: cloud-sql-proxy
protocol: TCP
name: cloud-sql-proxy
- port: 9801
targetPort: 9801
protocol: TCP
name: health
selector:
{{- include "gcloud-sql-proxy.selectorLabels" . | nindent 4 }}
{{- end }}
@@ -3,7 +3,7 @@ name: graphql
description: AFFiNE GraphQL server
type: application
version: 0.0.0
appVersion: "0.27.0"
appVersion: "0.26.3"
dependencies:
- name: gcloud-sql-proxy
version: 0.0.0
@@ -23,22 +23,6 @@ spec:
{{- toYaml . | nindent 8 }}
{{- end }}
serviceAccountName: {{ include "graphql.serviceAccountName" . }}
{{- with .Values.global.database.gcloud }}
{{- if .enabled }}
initContainers:
- name: wait-for-cloud-sql-proxy
image: busybox:1.36.1
imagePullPolicy: IfNotPresent
command:
- /bin/sh
- -ec
- |
until wget -q -T 2 -O /dev/null "http://{{ $.Values.global.database.host }}:9801/startup"; do
echo "waiting for cloud sql proxy to become ready"
sleep 2
done
{{- end }}
{{- end }}
containers:
- name: {{ .Chart.Name }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
@@ -111,19 +95,11 @@ spec:
path: /info
port: http
initialDelaySeconds: {{ .Values.probe.initialDelaySeconds }}
timeoutSeconds: {{ default .Values.probe.timeoutSeconds .Values.probe.liveness.timeoutSeconds }}
periodSeconds: {{ default .Values.probe.periodSeconds .Values.probe.liveness.periodSeconds }}
failureThreshold: {{ default .Values.probe.failureThreshold .Values.probe.liveness.failureThreshold }}
successThreshold: {{ default .Values.probe.successThreshold .Values.probe.liveness.successThreshold }}
readinessProbe:
httpGet:
path: /info
port: http
initialDelaySeconds: {{ .Values.probe.initialDelaySeconds }}
timeoutSeconds: {{ default .Values.probe.timeoutSeconds .Values.probe.readiness.timeoutSeconds }}
periodSeconds: {{ default .Values.probe.periodSeconds .Values.probe.readiness.periodSeconds }}
failureThreshold: {{ default .Values.probe.failureThreshold .Values.probe.readiness.failureThreshold }}
successThreshold: {{ default .Values.probe.successThreshold .Values.probe.readiness.successThreshold }}
resources:
{{- toYaml .Values.resources | nindent 12 }}
{{- with .Values.nodeSelector }}
@@ -13,22 +13,6 @@ spec:
template:
spec:
serviceAccountName: {{ include "graphql.serviceAccountName" . }}
{{- with .Values.global.database.gcloud }}
{{- if .enabled }}
initContainers:
- name: wait-for-cloud-sql-proxy
image: busybox:1.36.1
imagePullPolicy: IfNotPresent
command:
- /bin/sh
- -ec
- |
until wget -q -T 2 -O /dev/null "http://{{ $.Values.global.database.host }}:9801/startup"; do
echo "waiting for cloud sql proxy to become ready"
sleep 2
done
{{- end }}
{{- end }}
containers:
- name: {{ .Chart.Name }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
@@ -36,13 +36,6 @@ resources:
probe:
initialDelaySeconds: 20
timeoutSeconds: 5
periodSeconds: 10
failureThreshold: 6
successThreshold: 1
liveness:
failureThreshold: 12
readiness: {}
nodeSelector: {}
tolerations: []
+3 -3
View File
@@ -31,7 +31,7 @@
"groupSlug": "all-minor-patch",
"matchUpdateTypes": ["minor", "patch"],
"matchManagers": ["npm"],
"excludePackagePatterns": ["^@blocksuite/", "^oxlint$"]
"matchPackageNames": ["*", "!/^@blocksuite//", "!/oxlint/"]
},
{
"groupName": "all non-major dependencies",
@@ -63,7 +63,7 @@
"groupName": "opentelemetry",
"matchPackageNames": [
"/^@opentelemetry/",
"/^@google-cloud/opentelemetry-/"
"/^@google-cloud\/opentelemetry-/"
]
}
],
@@ -79,7 +79,7 @@
"customManagers": [
{
"customType": "regex",
"managerFilePatterns": ["/^rust-toolchain\\.toml?$/"],
"fileMatch": ["^rust-toolchain\\.toml?$"],
"matchStrings": [
"channel\\s*=\\s*\"(?<currentValue>\\d+\\.\\d+(\\.\\d+)?)\""
],
+2 -2
View File
@@ -13,5 +13,5 @@ jobs:
pull-requests: write
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/labeler@v6
- uses: actions/checkout@v4
- uses: actions/labeler@v5
+8 -8
View File
@@ -24,7 +24,7 @@ jobs:
runs-on: ubuntu-latest
environment: ${{ inputs.build-type }}
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- name: Setup Version
uses: ./.github/actions/setup-version
with:
@@ -57,7 +57,7 @@ jobs:
runs-on: ubuntu-latest
environment: ${{ inputs.build-type }}
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- name: Setup Version
uses: ./.github/actions/setup-version
with:
@@ -89,7 +89,7 @@ jobs:
runs-on: ubuntu-latest
environment: ${{ inputs.build-type }}
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- name: Setup Version
uses: ./.github/actions/setup-version
with:
@@ -118,7 +118,7 @@ jobs:
build-server-native:
name: Build Server native - ${{ matrix.targets.name }}
runs-on: ubuntu-22.04
runs-on: ubuntu-latest
environment: ${{ inputs.build-type }}
strategy:
fail-fast: false
@@ -132,7 +132,7 @@ jobs:
file: server-native.armv7.node
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- name: Setup Version
uses: ./.github/actions/setup-version
with:
@@ -166,7 +166,7 @@ jobs:
needs:
- build-server-native
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- name: Setup Version
uses: ./.github/actions/setup-version
with:
@@ -202,7 +202,7 @@ jobs:
- build-mobile
- build-admin
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- name: Download server dist
uses: actions/download-artifact@v4
with:
@@ -222,7 +222,7 @@ jobs:
# setup node without cache configuration
# Prisma cache is not compatible with docker build cache
- name: Setup Node.js
uses: actions/setup-node@v6
uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
registry-url: https://npm.pkg.github.com
+184 -204
View File
@@ -46,7 +46,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@v4
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
@@ -67,9 +67,9 @@ jobs:
name: Lint
runs-on: ubuntu-24.04-arm
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- name: Setup Go (for actionlint)
uses: actions/setup-go@v6
uses: actions/setup-go@v5
with:
go-version: 'stable'
- name: Install actionlint
@@ -82,13 +82,17 @@ jobs:
run: |
set -euo pipefail
"$(go env GOPATH)/bin/actionlint"
- name: Run oxlint
# oxlint is fast, so wrong code will fail quickly
run: |
set -euo pipefail
oxlint_version="$(node -e "console.log(require('./package.json').devDependencies.oxlint)")"
yarn dlx "oxlint@${oxlint_version}" --deny-warnings
- name: Setup Node.js
uses: ./.github/actions/setup-node
with:
electron-install: false
full-cache: true
- name: Run oxlint
run: yarn lint:ox
- name: Run i18n codegen
run: yarn affine @affine/i18n build
- name: Run ESLint
@@ -107,27 +111,20 @@ jobs:
env:
NODE_OPTIONS: --max-old-space-size=14384
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- name: Setup Node.js
uses: ./.github/actions/setup-node
with:
electron-install: false
full-cache: true
- name: Run i18n codegen
run: |
yarn affine @affine/i18n build
git checkout packages/frontend/i18n/src/i18n-completenesses.json
if git status --porcelain | grep -q .; then
echo "Run 'yarn affine @affine/i18n build' and make sure all generated i18n changes are submitted"
exit 1
else
echo "All generated i18n changes are submitted"
fi
run: yarn affine @affine/i18n build
- name: Run Type Check
run: yarn typecheck
- name: Run BS Docs Build
run: |
yarn affine bs-docs build
git checkout packages/frontend/i18n/src/i18n-completenesses.json
if git status --porcelain | grep -q .; then
echo "Run 'yarn typecheck && yarn affine bs-docs build' and make sure all changes are submitted"
exit 1
@@ -135,166 +132,13 @@ jobs:
echo "All changes are submitted"
fi
mobile-native-build-filter:
name: Mobile native build filter
runs-on: ubuntu-latest
outputs:
run-android: ${{ steps.mobile-native-filter.outputs.android }}
run-ios: ${{ steps.mobile-native-filter.outputs.ios }}
steps:
- uses: actions/checkout@v6
- uses: dorny/paths-filter@v3
id: mobile-native-filter
with:
filters: |
android:
- '.github/workflows/build-test.yml'
- 'packages/frontend/apps/android/**'
- 'packages/frontend/mobile-native/**'
- '.cargo/**'
- 'Cargo.lock'
- 'Cargo.toml'
- 'rust-toolchain*'
ios:
- '.github/workflows/build-test.yml'
- 'packages/frontend/apps/ios/**'
- 'packages/frontend/mobile-native/**'
- '.cargo/**'
- 'Cargo.lock'
- 'Cargo.toml'
- 'rust-toolchain*'
build-android-app:
name: Build Android app
if: ${{ needs.mobile-native-build-filter.outputs.run-android == 'true' }}
runs-on: ubuntu-latest
needs:
- mobile-native-build-filter
steps:
- uses: actions/checkout@v6
- name: Setup Node.js
uses: ./.github/actions/setup-node
with:
extra-flags: workspaces focus @affine/monorepo @affine-tools/cli @affine/android
electron-install: false
- uses: actions/setup-java@v5
with:
distribution: 'temurin'
java-version: '21'
cache: 'gradle'
- name: Setup Rust
uses: ./.github/actions/build-rust
with:
target: 'aarch64-linux-android'
package: 'affine_mobile_native'
no-build: 'true'
- name: Build Android web assets
run: yarn affine @affine/android build
env:
PUBLIC_PATH: '/'
- name: Write CI Firebase config
run: |
cat > packages/frontend/apps/android/App/app/google-services.json <<'JSON'
{
"project_info": {
"project_number": "1",
"project_id": "affine-ci",
"storage_bucket": "affine-ci.appspot.com"
},
"client": [
{
"client_info": {
"mobilesdk_app_id": "1:1:android:0000000000000000",
"android_client_info": {
"package_name": "app.affine.pro"
}
},
"oauth_client": [],
"api_key": [
{
"current_key": "ci-placeholder"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": []
}
}
}
],
"configuration_version": "1"
}
JSON
- name: Cap sync
run: yarn workspace @affine/android cap sync
- name: Build Android debug app
working-directory: packages/frontend/apps/android/App
run: ./gradlew :app:assembleCanaryDebug --no-daemon --stacktrace
build-ios-app:
name: Build iOS app
if: ${{ needs.mobile-native-build-filter.outputs.run-ios == 'true' }}
runs-on: macos-15
needs:
- mobile-native-build-filter
steps:
- uses: actions/checkout@v6
- name: Setup Node.js
uses: ./.github/actions/setup-node
with:
extra-flags: workspaces focus @affine/monorepo @affine-tools/cli @affine/ios
electron-install: false
hard-link-nm: false
- uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: 26.2
- name: Setup Rust
uses: ./.github/actions/build-rust
with:
target: 'aarch64-apple-ios-sim'
package: 'affine_mobile_native'
no-build: 'true'
- name: Build iOS web assets
run: yarn affine @affine/ios build
env:
PUBLIC_PATH: '/'
- name: Cap sync
run: yarn workspace @affine/ios sync
- name: Build iOS simulator app
run: |
xcodebuild \
-workspace packages/frontend/apps/ios/App/App.xcworkspace \
-scheme App \
-configuration Debug \
-sdk iphonesimulator \
-destination 'generic/platform=iOS Simulator' \
ARCHS=arm64 \
ONLY_ACTIVE_ARCH=YES \
CODE_SIGNING_ALLOWED=NO \
CODE_SIGNING_REQUIRED=NO \
build
rust-test-filter:
name: Rust test filter
runs-on: ubuntu-latest
outputs:
run-rust: ${{ steps.rust-filter.outputs.rust }}
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- uses: dorny/paths-filter@v3
id: rust-filter
@@ -315,7 +159,7 @@ jobs:
needs:
- rust-test-filter
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- uses: ./.github/actions/build-rust
with:
target: x86_64-unknown-linux-gnu
@@ -338,7 +182,7 @@ jobs:
needs:
- build-server-native
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- name: Setup Node.js
uses: ./.github/actions/setup-node
with:
@@ -368,7 +212,7 @@ jobs:
name: Check yarn binary
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- name: Run check
run: |
set -euo pipefail
@@ -382,9 +226,9 @@ jobs:
strategy:
fail-fast: false
matrix:
shard: [1, 2, 3, 4, 5]
shard: [1, 2]
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- name: Setup Node.js
uses: ./.github/actions/setup-node
with:
@@ -412,7 +256,7 @@ jobs:
name: E2E BlockSuite Cross Browser Test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- name: Setup Node.js
uses: ./.github/actions/setup-node
with:
@@ -425,13 +269,10 @@ jobs:
- name: Run playground build
run: yarn workspace @blocksuite/playground build
- name: Run integration browser tests
timeout-minutes: 10
run: yarn workspace @blocksuite/integration-test test:unit
- name: Run cross-platform playwright tests
timeout-minutes: 10
run: yarn workspace @affine-test/blocksuite test "cross-platform/" --forbid-only
- name: Run playwright tests
run: |
yarn workspace @blocksuite/integration-test test:unit
yarn workspace @affine-test/blocksuite test "cross-platform/" --forbid-only
- name: Upload test results
if: always()
@@ -441,6 +282,52 @@ jobs:
path: ./test-results
if-no-files-found: ignore
bundler-matrix:
name: Bundler Matrix (${{ matrix.bundler }})
runs-on: ubuntu-24.04-arm
strategy:
fail-fast: false
matrix:
bundler: [webpack, rspack]
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: ./.github/actions/setup-node
with:
playwright-install: false
electron-install: false
full-cache: true
- name: Run frontend build matrix
env:
AFFINE_BUNDLER: ${{ matrix.bundler }}
run: |
set -euo pipefail
packages=(
"@affine/web"
"@affine/mobile"
"@affine/ios"
"@affine/android"
"@affine/admin"
"@affine/electron-renderer"
)
summary="test-results-bundler-${AFFINE_BUNDLER}.txt"
: > "$summary"
for pkg in "${packages[@]}"; do
start=$(date +%s)
yarn affine "$pkg" build
end=$(date +%s)
echo "${pkg},$((end-start))" >> "$summary"
done
- name: Upload bundler timing
if: always()
uses: actions/upload-artifact@v4
with:
name: test-results-bundler-${{ matrix.bundler }}
path: ./test-results-bundler-${{ matrix.bundler }}.txt
if-no-files-found: ignore
e2e-test:
name: E2E Test
runs-on: ubuntu-24.04-arm
@@ -453,7 +340,7 @@ jobs:
matrix:
shard: [1, 2, 3, 4, 5]
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- name: Setup Node.js
uses: ./.github/actions/setup-node
with:
@@ -485,7 +372,7 @@ jobs:
matrix:
shard: [1, 2]
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- name: Setup Node.js
uses: ./.github/actions/setup-node
with:
@@ -515,9 +402,9 @@ jobs:
strategy:
fail-fast: false
matrix:
shard: [1, 2, 3, 4, 5]
shard: [1, 2, 3]
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- name: Setup Node.js
uses: ./.github/actions/setup-node
with:
@@ -550,7 +437,7 @@ jobs:
env:
CARGO_PROFILE_RELEASE_DEBUG: '1'
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- name: Setup Node.js
uses: ./.github/actions/setup-node
with:
@@ -589,7 +476,7 @@ jobs:
- { os: macos-latest, target: aarch64-apple-darwin }
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- name: Setup Node.js
uses: ./.github/actions/setup-node
with:
@@ -630,7 +517,7 @@ jobs:
- { os: windows-latest, target: aarch64-pc-windows-msvc }
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- uses: samypr100/setup-dev-drive@v3
with:
workspace-copy: true
@@ -670,7 +557,7 @@ jobs:
env:
CARGO_PROFILE_RELEASE_DEBUG: '1'
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- name: Setup Node.js
uses: ./.github/actions/setup-node
with:
@@ -693,7 +580,7 @@ jobs:
name: Build @affine/electron renderer
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- name: Setup Node.js
uses: ./.github/actions/setup-node
with:
@@ -720,7 +607,7 @@ jobs:
needs:
- build-native-linux
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- name: Setup Node.js
uses: ./.github/actions/setup-node
with:
@@ -774,7 +661,7 @@ jobs:
ports:
- 9308:9308
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- name: Setup Node.js
uses: ./.github/actions/setup-node
@@ -855,7 +742,7 @@ jobs:
stack-version: 9.0.1
security-enabled: false
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- name: Setup Node.js
uses: ./.github/actions/setup-node
@@ -918,7 +805,7 @@ jobs:
ports:
- 9308:9308
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- name: Setup Node.js
uses: ./.github/actions/setup-node
@@ -948,6 +835,99 @@ jobs:
name: affine
fail_ci_if_error: false
miri:
name: miri code check
if: ${{ needs.rust-test-filter.outputs.run-rust == 'true' }}
runs-on: ubuntu-latest
needs:
- rust-test-filter
env:
RUST_BACKTRACE: full
CARGO_TERM_COLOR: always
MIRIFLAGS: -Zmiri-backtrace=full -Zmiri-tree-borrows
steps:
- uses: actions/checkout@v4
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
with:
toolchain: nightly
components: miri
- name: Install latest nextest release
uses: taiki-e/install-action@v2
with:
tool: nextest@0.9.98
- name: Miri Code Check
continue-on-error: true
run: |
cargo +nightly miri nextest run -p y-octo -j4
loom:
name: loom thread test
if: ${{ needs.rust-test-filter.outputs.run-rust == 'true' }}
runs-on: ubuntu-latest
needs:
- rust-test-filter
env:
RUSTFLAGS: --cfg loom
RUST_BACKTRACE: full
CARGO_TERM_COLOR: always
steps:
- uses: actions/checkout@v4
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
with:
toolchain: stable
- name: Install latest nextest release
uses: taiki-e/install-action@v2
with:
tool: nextest@0.9.98
- name: Loom Thread Test
run: |
cargo nextest run -p y-octo --lib
fuzzing:
name: fuzzing
if: ${{ needs.rust-test-filter.outputs.run-rust == 'true' }}
runs-on: ubuntu-latest
needs:
- rust-test-filter
env:
CARGO_TERM_COLOR: always
steps:
- uses: actions/checkout@v4
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
with:
toolchain: nightly
- name: fuzzing
working-directory: ./packages/common/y-octo/utils
run: |
cargo install cargo-fuzz
cargo +nightly fuzz run apply_update -- -max_total_time=30
cargo +nightly fuzz run codec_doc_any_struct -- -max_total_time=30
cargo +nightly fuzz run codec_doc_any -- -max_total_time=30
cargo +nightly fuzz run decode_bytes -- -max_total_time=30
cargo +nightly fuzz run i32_decode -- -max_total_time=30
cargo +nightly fuzz run i32_encode -- -max_total_time=30
cargo +nightly fuzz run ins_del_text -- -max_total_time=30
cargo +nightly fuzz run sync_message -- -max_total_time=30
cargo +nightly fuzz run u64_decode -- -max_total_time=30
cargo +nightly fuzz run u64_encode -- -max_total_time=30
cargo +nightly fuzz run apply_update -- -max_total_time=30
- name: upload fuzz artifacts
if: ${{ failure() }}
uses: actions/upload-artifact@v4
with:
name: fuzz-artifact
path: packages/common/y-octo/utils/fuzz/artifacts/**/*
rust-test:
name: Run native tests
if: ${{ needs.rust-test-filter.outputs.run-rust == 'true' }}
@@ -957,7 +937,7 @@ jobs:
env:
CARGO_TERM_COLOR: always
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- name: Setup Rust
uses: ./.github/actions/build-rust
with:
@@ -980,7 +960,7 @@ jobs:
run-api: ${{ steps.decision.outputs.run_api }}
run-e2e: ${{ steps.decision.outputs.run_e2e }}
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- uses: dorny/paths-filter@v3
id: copilot-filter
@@ -1049,7 +1029,7 @@ jobs:
ports:
- 9308:9308
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- name: Setup Node.js
uses: ./.github/actions/setup-node
@@ -1122,7 +1102,7 @@ jobs:
ports:
- 9308:9308
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- name: Setup Node.js
uses: ./.github/actions/setup-node
@@ -1205,7 +1185,7 @@ jobs:
ports:
- 9308:9308
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- name: Setup Node.js
uses: ./.github/actions/setup-node
@@ -1286,7 +1266,7 @@ jobs:
test: true,
}
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- name: Setup Node.js
uses: ./.github/actions/setup-node
timeout-minutes: 10
@@ -1388,9 +1368,6 @@ jobs:
- analyze
- lint
- typecheck
- mobile-native-build-filter
- build-android-app
- build-ios-app
- lint-rust
- check-git-status
- check-yarn-binary
@@ -1405,6 +1382,9 @@ jobs:
- build-server-native
- build-electron-renderer
- native-unit-test
- miri
- loom
- fuzzing
- server-test
- server-e2e-test
- rust-test
+4 -4
View File
@@ -10,7 +10,7 @@ jobs:
env:
CARGO_PROFILE_RELEASE_DEBUG: '1'
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- name: Setup Node.js
uses: ./.github/actions/setup-node
with:
@@ -64,7 +64,7 @@ jobs:
ports:
- 9308:9308
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- name: Setup Node.js
uses: ./.github/actions/setup-node
@@ -134,7 +134,7 @@ jobs:
ports:
- 9308:9308
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- name: Setup Node.js
uses: ./.github/actions/setup-node
@@ -167,7 +167,7 @@ jobs:
runs-on: ubuntu-latest
name: Post test result message
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Node.js
+2 -2
View File
@@ -18,9 +18,9 @@ jobs:
runs-on: ubuntu-latest
if: ${{ github.event.action != 'edited' || github.event.changes.title != null }}
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v6
uses: actions/setup-node@v4
with:
cache: 'yarn'
node-version-file: '.nvmrc'
+1 -1
View File
@@ -35,7 +35,7 @@ jobs:
- build-images
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- name: Deploy to ${{ inputs.build-type }}
uses: ./.github/actions/deploy
with:
@@ -69,7 +69,7 @@ jobs:
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
SENTRY_RELEASE: ${{ inputs.app_version }}
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- name: Setup Version
uses: ./.github/actions/setup-version
@@ -101,7 +101,7 @@ jobs:
- name: Signing By Apple Developer ID
if: ${{ inputs.platform == 'darwin' && inputs.apple_codesign }}
uses: apple-actions/import-codesign-certs@v7
uses: apple-actions/import-codesign-certs@v5
with:
p12-file-base64: ${{ secrets.CERTIFICATES_P12 }}
p12-password: ${{ secrets.CERTIFICATES_P12_PASSWORD }}
@@ -178,14 +178,14 @@ jobs:
mv packages/frontend/apps/electron/out/*/make/deb/${{ inputs.arch }}/*.deb ./builds/affine-${{ env.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-linux-${{ inputs.arch }}.deb
mv packages/frontend/apps/electron/out/*/make/flatpak/*/*.flatpak ./builds/affine-${{ env.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-linux-${{ inputs.arch }}.flatpak
- uses: actions/attest-build-provenance@v4
- uses: actions/attest-build-provenance@v2
if: ${{ inputs.platform == 'darwin' }}
with:
subject-path: |
./builds/affine-${{ env.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-macos-${{ inputs.arch }}.zip
./builds/affine-${{ env.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-macos-${{ inputs.arch }}.dmg
- uses: actions/attest-build-provenance@v4
- uses: actions/attest-build-provenance@v2
if: ${{ inputs.platform == 'linux' }}
with:
subject-path: |
+16 -105
View File
@@ -27,11 +27,6 @@ on:
required: false
default: true
type: boolean
require-windows-signing:
description: 'Require all Windows signing steps to succeed before release'
required: false
default: false
type: boolean
permissions:
actions: write
@@ -53,7 +48,7 @@ jobs:
runs-on: ubuntu-latest
environment: ${{ inputs.build-type }}
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- name: Setup Version
uses: ./.github/actions/setup-version
with:
@@ -78,21 +73,6 @@ jobs:
name: desktop-web
path: packages/frontend/apps/electron/resources/web-static
windows-signer-gate:
if: ${{ inputs.desktop_windows }}
runs-on: ubuntu-latest
outputs:
signer_available: ${{ steps.check.outputs.signer_available }}
steps:
- uses: actions/checkout@v6
- name: Check windows signer availability
id: check
run: node ./scripts/check-windows-signer.mjs
env:
BUILD_TYPE: ${{ inputs.build-type }}
GITHUB_TOKEN: ${{ github.token }}
REQUIRE_SIGNER: ${{ inputs.require-windows-signing }}
make-distribution-macos:
if: ${{ inputs.desktop_macos }}
strategy:
@@ -174,45 +154,24 @@ jobs:
enable_scripts: true
sign-packaged-artifacts-windows_x64:
if: ${{ inputs.desktop_windows && needs.windows-signer-gate.outputs.signer_available == 'true' }}
needs:
- windows-signer-gate
- package-distribution-windows-x64
if: ${{ inputs.desktop_windows }}
needs: package-distribution-windows-x64
uses: ./.github/workflows/windows-signer.yml
with:
files: ${{ needs.package-distribution-windows-x64.outputs.files_to_be_signed }}
artifact-name: packaged-win32-x64
sign-packaged-artifacts-windows_arm64:
if: ${{ inputs.desktop_windows && needs.windows-signer-gate.outputs.signer_available == 'true' }}
needs:
- windows-signer-gate
- package-distribution-windows-arm64
if: ${{ inputs.desktop_windows }}
needs: package-distribution-windows-arm64
uses: ./.github/workflows/windows-signer.yml
with:
files: ${{ needs.package-distribution-windows-arm64.outputs.files_to_be_signed }}
artifact-name: packaged-win32-arm64
make-windows-installer:
if: >-
${{
always() &&
inputs.desktop_windows &&
needs.windows-signer-gate.result == 'success' &&
needs.package-distribution-windows-x64.result == 'success' &&
needs.package-distribution-windows-arm64.result == 'success' &&
(
!inputs.require-windows-signing ||
(
needs.sign-packaged-artifacts-windows_x64.result == 'success' &&
needs.sign-packaged-artifacts-windows_arm64.result == 'success'
)
)
}}
if: ${{ inputs.desktop_windows }}
needs:
- windows-signer-gate
- package-distribution-windows-x64
- package-distribution-windows-arm64
- sign-packaged-artifacts-windows_x64
- sign-packaged-artifacts-windows_arm64
strategy:
@@ -228,7 +187,7 @@ jobs:
FILES_TO_BE_SIGNED_x64: ${{ steps.get_files_to_be_signed.outputs.FILES_TO_BE_SIGNED_x64 }}
FILES_TO_BE_SIGNED_arm64: ${{ steps.get_files_to_be_signed.outputs.FILES_TO_BE_SIGNED_arm64 }}
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- name: Setup Version
uses: ./.github/actions/setup-version
with:
@@ -250,13 +209,11 @@ jobs:
- name: unzip packaged artifacts
run: Expand-Archive -Path packaged-unsigned/archive.zip -DestinationPath packages/frontend/apps/electron/out
- name: Download signed packaged file diff
if: ${{ (matrix.spec.arch == 'x64' && needs.sign-packaged-artifacts-windows_x64.result == 'success') || (matrix.spec.arch == 'arm64' && needs.sign-packaged-artifacts-windows_arm64.result == 'success') }}
uses: actions/download-artifact@v4
with:
name: signed-packaged-${{ matrix.spec.platform }}-${{ matrix.spec.arch }}
path: signed-packaged-diff
- name: Apply signed packaged file diff
if: ${{ (matrix.spec.arch == 'x64' && needs.sign-packaged-artifacts-windows_x64.result == 'success') || (matrix.spec.arch == 'arm64' && needs.sign-packaged-artifacts-windows_arm64.result == 'success') }}
shell: pwsh
run: |
$DiffRoot = 'signed-packaged-diff/files'
@@ -306,38 +263,25 @@ jobs:
path: archive.zip
sign-installer-artifacts-windows-x64:
if: ${{ inputs.desktop_windows && needs.windows-signer-gate.outputs.signer_available == 'true' }}
needs:
- windows-signer-gate
- make-windows-installer
if: ${{ inputs.desktop_windows }}
needs: make-windows-installer
uses: ./.github/workflows/windows-signer.yml
with:
files: ${{ needs.make-windows-installer.outputs.FILES_TO_BE_SIGNED_x64 }}
artifact-name: installer-win32-x64
sign-installer-artifacts-windows-arm64:
if: ${{ inputs.desktop_windows && needs.windows-signer-gate.outputs.signer_available == 'true' }}
needs:
- windows-signer-gate
- make-windows-installer
if: ${{ inputs.desktop_windows }}
needs: make-windows-installer
uses: ./.github/workflows/windows-signer.yml
with:
files: ${{ needs.make-windows-installer.outputs.FILES_TO_BE_SIGNED_arm64 }}
artifact-name: installer-win32-arm64
finalize-installer-windows:
if: >-
${{
always() &&
inputs.desktop_windows &&
needs.make-windows-installer.result == 'success'
}}
if: ${{ inputs.desktop_windows }}
needs:
[
windows-signer-gate,
make-windows-installer,
sign-packaged-artifacts-windows_x64,
sign-packaged-artifacts-windows_arm64,
sign-installer-artifacts-windows-x64,
sign-installer-artifacts-windows-arm64,
before-make,
@@ -355,22 +299,18 @@ jobs:
runs-on: ${{ matrix.spec.runner }}
steps:
- name: Download installer artifacts
if: ${{ (matrix.spec.arch == 'x64' && needs.sign-packaged-artifacts-windows_x64.result == 'success' && needs.sign-installer-artifacts-windows-x64.result == 'success') || (matrix.spec.arch == 'arm64' && needs.sign-packaged-artifacts-windows_arm64.result == 'success' && needs.sign-installer-artifacts-windows-arm64.result == 'success') }}
uses: actions/download-artifact@v4
with:
name: installer-${{ matrix.spec.platform }}-${{ matrix.spec.arch }}
path: installer-unsigned
- name: unzip installer artifacts
if: ${{ (matrix.spec.arch == 'x64' && needs.sign-packaged-artifacts-windows_x64.result == 'success' && needs.sign-installer-artifacts-windows-x64.result == 'success') || (matrix.spec.arch == 'arm64' && needs.sign-packaged-artifacts-windows_arm64.result == 'success' && needs.sign-installer-artifacts-windows-arm64.result == 'success') }}
run: Expand-Archive -Path installer-unsigned/archive.zip -DestinationPath packages/frontend/apps/electron/out/${{ env.BUILD_TYPE }}/make
- name: Download signed installer file diff
if: ${{ (matrix.spec.arch == 'x64' && needs.sign-packaged-artifacts-windows_x64.result == 'success' && needs.sign-installer-artifacts-windows-x64.result == 'success') || (matrix.spec.arch == 'arm64' && needs.sign-packaged-artifacts-windows_arm64.result == 'success' && needs.sign-installer-artifacts-windows-arm64.result == 'success') }}
uses: actions/download-artifact@v4
with:
name: signed-installer-${{ matrix.spec.platform }}-${{ matrix.spec.arch }}
path: signed-installer-diff
- name: Apply signed installer file diff
if: ${{ (matrix.spec.arch == 'x64' && needs.sign-packaged-artifacts-windows_x64.result == 'success' && needs.sign-installer-artifacts-windows-x64.result == 'success') || (matrix.spec.arch == 'arm64' && needs.sign-packaged-artifacts-windows_arm64.result == 'success' && needs.sign-installer-artifacts-windows-arm64.result == 'success') }}
shell: pwsh
run: |
$DiffRoot = 'signed-installer-diff/files'
@@ -398,15 +338,13 @@ jobs:
}
- name: Save artifacts
if: ${{ (matrix.spec.arch == 'x64' && needs.sign-packaged-artifacts-windows_x64.result == 'success' && needs.sign-installer-artifacts-windows-x64.result == 'success') || (matrix.spec.arch == 'arm64' && needs.sign-packaged-artifacts-windows_arm64.result == 'success' && needs.sign-installer-artifacts-windows-arm64.result == 'success') }}
run: |
mkdir -p builds
mv packages/frontend/apps/electron/out/*/make/zip/win32/${{ matrix.spec.arch }}/AFFiNE*-win32-${{ matrix.spec.arch }}-*.zip ./builds/affine-${{ env.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-windows-${{ matrix.spec.arch }}.zip
mv packages/frontend/apps/electron/out/*/make/squirrel.windows/${{ matrix.spec.arch }}/*.exe ./builds/affine-${{ env.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-windows-${{ matrix.spec.arch }}.exe
mv packages/frontend/apps/electron/out/*/make/nsis.windows/${{ matrix.spec.arch }}/*.exe ./builds/affine-${{ env.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-windows-${{ matrix.spec.arch }}.nsis.exe
- uses: actions/attest-build-provenance@v4
if: ${{ (matrix.spec.arch == 'x64' && needs.sign-packaged-artifacts-windows_x64.result == 'success' && needs.sign-installer-artifacts-windows-x64.result == 'success') || (matrix.spec.arch == 'arm64' && needs.sign-packaged-artifacts-windows_arm64.result == 'success' && needs.sign-installer-artifacts-windows-arm64.result == 'success') }}
- uses: actions/attest-build-provenance@v2
with:
subject-path: |
./builds/affine-${{ env.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-windows-${{ matrix.spec.arch }}.zip
@@ -414,77 +352,50 @@ jobs:
./builds/affine-${{ env.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-windows-${{ matrix.spec.arch }}.nsis.exe
- name: Upload Artifact
if: ${{ (matrix.spec.arch == 'x64' && needs.sign-packaged-artifacts-windows_x64.result == 'success' && needs.sign-installer-artifacts-windows-x64.result == 'success') || (matrix.spec.arch == 'arm64' && needs.sign-packaged-artifacts-windows_arm64.result == 'success' && needs.sign-installer-artifacts-windows-arm64.result == 'success') }}
uses: actions/upload-artifact@v4
with:
name: affine-${{ matrix.spec.platform }}-${{ matrix.spec.arch }}-builds
path: builds
release:
if: >-
${{
always() &&
(inputs.desktop_macos || inputs.desktop_linux || inputs.desktop_windows) &&
needs.before-make.result == 'success' &&
(!inputs.desktop_macos || needs.make-distribution-macos.result == 'success') &&
(!inputs.desktop_linux || needs.make-distribution-linux.result == 'success') &&
(
!inputs.require-windows-signing ||
(
needs.sign-packaged-artifacts-windows_x64.result == 'success' &&
needs.sign-packaged-artifacts-windows_arm64.result == 'success' &&
needs.sign-installer-artifacts-windows-x64.result == 'success' &&
needs.sign-installer-artifacts-windows-arm64.result == 'success' &&
needs.finalize-installer-windows.result == 'success'
)
)
}}
if: ${{ inputs.desktop_macos && inputs.desktop_linux && inputs.desktop_windows }}
needs:
[
before-make,
make-distribution-macos,
make-distribution-linux,
sign-packaged-artifacts-windows_x64,
sign-packaged-artifacts-windows_arm64,
sign-installer-artifacts-windows-x64,
sign-installer-artifacts-windows-arm64,
finalize-installer-windows,
]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- name: Download Artifacts (macos-x64)
if: ${{ inputs.desktop_macos }}
uses: actions/download-artifact@v4
with:
name: affine-darwin-x64-builds
path: ./release
- name: Download Artifacts (macos-arm64)
if: ${{ inputs.desktop_macos }}
uses: actions/download-artifact@v4
with:
name: affine-darwin-arm64-builds
path: ./release
- name: Download Artifacts (windows-x64)
if: ${{ needs.sign-packaged-artifacts-windows_x64.result == 'success' && needs.sign-installer-artifacts-windows-x64.result == 'success' }}
uses: actions/download-artifact@v4
with:
name: affine-win32-x64-builds
path: ./release
- name: Download Artifacts (windows-arm64)
if: ${{ needs.sign-packaged-artifacts-windows_arm64.result == 'success' && needs.sign-installer-artifacts-windows-arm64.result == 'success' }}
uses: actions/download-artifact@v4
with:
name: affine-win32-arm64-builds
path: ./release
- name: Download Artifacts (linux-x64)
if: ${{ inputs.desktop_linux }}
uses: actions/download-artifact@v4
with:
name: affine-linux-x64-builds
path: ./release
- uses: actions/setup-node@v6
- uses: actions/setup-node@v4
with:
node-version: 20
- name: Copy Selfhost Release Files
+8 -13
View File
@@ -26,7 +26,7 @@ jobs:
runs-on: ubuntu-latest
environment: ${{ inputs.build-type }}
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- name: Setup Version
uses: ./.github/actions/setup-version
with:
@@ -54,7 +54,7 @@ jobs:
build-android-web:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- name: Setup Version
uses: ./.github/actions/setup-version
with:
@@ -83,7 +83,7 @@ jobs:
needs:
- build-ios-web
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- name: Setup Version
uses: ./.github/actions/setup-version
with:
@@ -109,15 +109,12 @@ jobs:
- uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: 26.2
- uses: ruby/setup-ruby@v1
with:
ruby-version: '3.3'
- name: Install Swiftformat
run: brew install swiftformat
- name: Cap sync
run: yarn workspace @affine/ios sync
- name: Signing By Apple Developer ID
uses: apple-actions/import-codesign-certs@v7
uses: apple-actions/import-codesign-certs@v5
id: import-codesign-certs
with:
p12-file-base64: ${{ secrets.CERTIFICATES_P12_MOBILE }}
@@ -134,10 +131,8 @@ jobs:
printf '%s' "$BUILD_PROVISION_PROFILE" | base64 --decode -o "$PP_PATH"
mkdir -p "$HOME/Library/MobileDevice/Provisioning Profiles"
cp "$PP_PATH" "$HOME/Library/MobileDevice/Provisioning Profiles"
bundle install
bundle exec fastlane beta
fastlane beta
env:
BUNDLE_PATH: vendor/bundle
BUILD_TARGET: distribution
BUILD_PROVISION_PROFILE: ${{ secrets.BUILD_PROVISION_PROFILE }}
PP_PATH: ${{ runner.temp }}/build_pp.mobileprovision
@@ -152,7 +147,7 @@ jobs:
needs:
- build-android-web
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- name: Setup Version
uses: ./.github/actions/setup-version
with:
@@ -185,7 +180,7 @@ jobs:
no-build: 'true'
- name: Cap sync
run: yarn workspace @affine/android cap sync
- uses: actions/setup-python@v6
- uses: actions/setup-python@v5
with:
python-version: '3.13'
- name: Auth gcloud
@@ -197,7 +192,7 @@ jobs:
token_format: 'access_token'
project_id: '${{ secrets.GCP_PROJECT_ID }}'
access_token_scopes: 'https://www.googleapis.com/auth/androidpublisher'
- uses: actions/setup-java@v5
- uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '21'
+2 -3
View File
@@ -55,7 +55,7 @@ jobs:
GIT_SHORT_HASH: ${{ steps.prepare.outputs.GIT_SHORT_HASH }}
BUILD_TYPE: ${{ steps.prepare.outputs.BUILD_TYPE }}
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- name: Prepare Release
id: prepare
uses: ./.github/actions/prepare-release
@@ -72,7 +72,7 @@ jobs:
steps:
- name: Decide whether to release
id: decide
uses: actions/github-script@v9
uses: actions/github-script@v7
with:
script: |
const buildType = '${{ needs.prepare.outputs.BUILD_TYPE }}'
@@ -195,7 +195,6 @@ jobs:
desktop_macos: ${{ github.event_name != 'workflow_dispatch' || inputs.desktop_macos }}
desktop_windows: ${{ github.event_name != 'workflow_dispatch' || inputs.desktop_windows }}
desktop_linux: ${{ github.event_name != 'workflow_dispatch' || inputs.desktop_linux }}
require-windows-signing: ${{ needs.prepare.outputs.BUILD_TYPE == 'stable' || (github.event_name == 'workflow_dispatch' && inputs.desktop_windows) }}
mobile:
name: Release Mobile
-10
View File
@@ -6,7 +6,6 @@
!.yarn/releases
!.yarn/sdks
.yarn/versions
.corepack-bin
# compiled output
*dist
@@ -49,9 +48,6 @@ testem.log
/typings
tsconfig.tsbuildinfo
.context
/*.md
.codex
.cursor
# System Files
.DS_Store
@@ -96,9 +92,3 @@ af.cmd
# playwright
storageState.json
/.understand-anything
# local test/browser artifacts
/.playwright-browsers/
**/.vitest-attachments/
/blocksuite/framework/std/src/__tests__/gfx/__screenshots__/
+1 -1
View File
@@ -1 +1 @@
22.23.0
22.22.0
+7 -88
View File
@@ -1,18 +1,10 @@
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"jsPlugins": [
"eslint-plugin-simple-import-sort",
"eslint-plugin-sonarjs",
"eslint-plugin-lit"
],
"plugins": ["import", "react", "typescript", "unicorn", "promise"],
"categories": {
"correctness": "error",
"perf": "error"
},
"options": {
"typeAware": true
},
"env": {
"builtin": true,
"es2026": true
@@ -23,8 +15,6 @@
".github/helm",
".git",
".vscode",
".context",
".codex",
".yarnrc.yml",
".docker",
"**/.storybook",
@@ -53,8 +43,6 @@
"packages/frontend/apps/ios/App/**",
"tests/blocksuite/snapshots",
"blocksuite/docs/api/**",
"blocksuite/docs-site/.vitepress/.temp/**",
"blocksuite/docs-site/api/**",
"packages/frontend/admin/src/config.json",
"**/test-docs.json",
"**/test-blocks.json"
@@ -68,31 +56,9 @@
"react/display-name": "error",
"react/rules-of-hooks": "error",
"react/exhaustive-deps": "warn",
"typescript/prefer-for-of": "error",
"typescript/no-unsafe-function-type": "error",
"typescript/no-wrapper-object-types": "error",
"typescript/unified-signatures": "error",
"typescript/await-thenable": "allow",
"typescript/no-floating-promises": "allow",
"typescript/no-misused-promises": "allow",
"typescript/prefer-readonly": "allow",
"typescript/require-array-sort-compare": "allow",
"typescript/return-await": ["error", "error-handling-correctness-only"],
"typescript/no-array-delete": "allow",
"typescript/no-base-to-string": "allow",
"typescript/no-duplicate-type-constituents": "allow",
"typescript/no-for-in-array": "allow",
"typescript/no-implied-eval": "allow",
"typescript/no-meaningless-void-operator": "allow",
"typescript/no-misused-spread": "allow",
"typescript/no-redundant-type-constituents": "allow",
"typescript/no-unnecessary-parameter-property-assignment": "allow",
"typescript/no-unsafe-unary-minus": "allow",
"typescript/no-useless-empty-export": "allow",
"typescript/prefer-namespace-keyword": "allow",
"typescript/prefer-string-starts-ends-with": "allow",
"typescript/restrict-template-expressions": "allow",
"typescript/unbound-method": "allow",
"@typescript-eslint/prefer-for-of": "error",
"@typescript-eslint/no-unsafe-function-type": "error",
"@typescript-eslint/no-wrapper-object-types": "error",
"no-restricted-imports": [
"error",
{
@@ -201,29 +167,6 @@
"react/jsx-no-target-blank": "error",
"react/jsx-no-comment-textnodes": "error",
"react/no-array-index-key": "off",
"simple-import-sort/imports": "error",
"simple-import-sort/exports": "error",
"sonarjs/no-all-duplicated-branches": "error",
"sonarjs/no-element-overwrite": "error",
"sonarjs/no-empty-collection": "error",
"sonarjs/no-extra-arguments": "error",
"sonarjs/no-identical-conditions": "error",
"sonarjs/no-identical-expressions": "error",
"sonarjs/no-ignored-return": "error",
"sonarjs/no-use-of-empty-return-value": "error",
"sonarjs/non-existent-operator": "error",
"sonarjs/no-collapsible-if": "error",
"sonarjs/no-same-line-conditional": "error",
"sonarjs/no-duplicated-branches": "error",
"sonarjs/no-collection-size-mischeck": "error",
"sonarjs/no-identical-functions": "error",
"sonarjs/no-gratuitous-expressions": "error",
"lit/attribute-value-entities": "error",
"lit/binding-positions": "error",
"lit/no-duplicate-template-bindings": "error",
"lit/no-invalid-html": "error",
"lit/no-legacy-template-syntax": "error",
"lit/no-property-change-update": "error",
"typescript/consistent-type-imports": "error",
"typescript/no-non-null-assertion": "error",
"typescript/triple-slash-reference": "error",
@@ -231,6 +174,7 @@
"typescript/no-duplicate-enum-values": "error",
"typescript/no-extra-non-null-assertion": "error",
"typescript/no-misused-new": "error",
"typescript/prefer-for-of": "error",
"typescript/no-unsafe-declaration-merging": "error",
"typescript/no-unnecessary-type-constraint": "error",
"typescript/no-this-alias": [
@@ -282,10 +226,11 @@
"ignoreTypes": true
}
],
"import/sort-imports": "error",
"import/namespace": "off",
"import/no-webpack-loader-syntax": "error",
"import/no-duplicates": "error",
"no-import-assign": "error",
"import/no-import-assign": "error",
"import/no-self-import": "error"
},
"overrides": [
@@ -324,7 +269,7 @@
{
"files": ["blocksuite/**/*.ts"],
"rules": {
"eqeqeq": "off",
"eslint/eqeqeq": "off",
"typescript/no-non-null-assertion": "off",
"unicorn/prefer-array-some": "off"
}
@@ -345,17 +290,6 @@
"blocksuite/**/*.{ts,tsx}"
],
"rules": {
"typescript/await-thenable": "error",
"typescript/no-floating-promises": [
"error",
{
"ignoreVoid": false,
"ignoreIIFE": false
}
],
"typescript/no-misused-promises": "error",
"typescript/prefer-readonly": "error",
"typescript/require-array-sort-compare": "error",
"react/exhaustive-deps": [
"warn",
{
@@ -376,14 +310,6 @@
"**/e2e/**/*"
],
"rules": {
"typescript/no-floating-promises": [
"error",
{
"ignoreVoid": true,
"ignoreIIFE": false
}
],
"typescript/no-misused-promises": "off",
"no-restricted-imports": "off"
}
},
@@ -392,13 +318,6 @@
"rules": {
"react/rules-of-hooks": "off"
}
},
{
"files": ["blocksuite/framework/std/src/view/element/lit-host.ts"],
"rules": {
"lit/binding-positions": "off",
"lit/no-invalid-html": "off"
}
}
]
}
-4
View File
@@ -4,8 +4,6 @@
.github/helm
.git
.vscode
.context
.codex
.yarnrc.yml
.docker
**/.storybook
@@ -40,8 +38,6 @@ packages/frontend/apps/android/App/**
packages/frontend/apps/ios/App/**
tests/blocksuite/snapshots
blocksuite/docs/api/**
blocksuite/docs-site/.vitepress/.temp/**
blocksuite/docs-site/api/**
packages/frontend/admin/src/config.json
**/test-docs.json
**/test-blocks.json
-2
View File
@@ -32,8 +32,6 @@
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"vitest.include": ["packages/**/*.spec.ts", "packages/**/*.spec.tsx"],
"eslint.runtime": "node",
"eslint.execArgv": ["--max_old_space_size=16384"],
"rust-analyzer.check.extraEnv": {
"DATABASE_URL": "sqlite:affine.db"
},
+942
View File
File diff suppressed because one or more lines are too long
-940
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -12,4 +12,4 @@ npmPublishAccess: public
npmRegistryServer: "https://registry.npmjs.org"
yarnPath: .yarn/releases/yarn-4.13.0.cjs
yarnPath: .yarn/releases/yarn-4.12.0.cjs
Generated
+859 -4714
View File
File diff suppressed because it is too large Load Diff
+25 -36
View File
@@ -2,6 +2,8 @@
members = [
"./packages/backend/native",
"./packages/common/native",
"./packages/common/y-octo/core",
"./packages/common/y-octo/utils",
"./packages/frontend/mobile-native",
"./packages/frontend/native",
"./packages/frontend/native/nbstore",
@@ -14,14 +16,13 @@ resolver = "3"
edition = "2024"
[workspace.dependencies]
aes-gcm = "0.10"
affine_common = { path = "./packages/common/native" }
affine_nbstore = { path = "./packages/frontend/native/nbstore" }
ahash = "0.8"
anyhow = "1"
arbitrary = { version = "1.3", features = ["derive"] }
assert-json-diff = "2.0"
base64 = "0.22.1"
async-lock = { version = "3.4.0", features = ["loom"] }
base64-simd = "0.8"
bitvec = "1.0"
block2 = "0.6"
@@ -35,31 +36,18 @@ resolver = "3"
criterion2 = { version = "3", default-features = false }
crossbeam-channel = "0.5"
dispatch2 = "0.3"
doc_extractor = "0.1.0"
docx-parser = { git = "https://github.com/toeverything/docx-parser" }
dotenvy = "0.15"
file-format = { version = "0.28", features = ["reader"] }
hex = "0.4"
homedir = "0.3"
image = { version = "0.25.9", default-features = false, features = [
"bmp",
"gif",
"jpeg",
"png",
"webp",
] }
infer = { version = "0.19.0" }
lasso = { version = "0.7", features = ["multi-threaded"] }
lib0 = { version = "0.16", features = ["lib0-serde"] }
libc = "0.2"
libwebp-sys = "0.14.2"
little_exif = "0.6.23"
llm_adapter = { version = "0.2", default-features = false }
llm_runtime = { version = "0.2", default-features = false }
log = "0.4"
loom = { version = "0.7", features = ["checkpoint"] }
lru = "0.16"
matroska = "0.30"
memory-indexer = "0.3.1"
mermaid-rs-renderer = { git = "https://github.com/toeverything/mermaid-rs-renderer", rev = "fba9097", default-features = false }
memory-indexer = "0.3.0"
mimalloc = "0.1"
mp4parse = "0.17"
nanoid = "0.4"
@@ -76,11 +64,11 @@ resolver = "3"
notify = { version = "8", features = ["serde"] }
objc2 = "0.6"
objc2-foundation = "0.3"
ogg = "0.9"
once_cell = "1"
ordered-float = "5"
p256 = { version = "0.13", features = ["ecdsa", "pem"] }
parking_lot = "0.12"
path-ext = "0.1.2"
pdf-extract = { git = "https://github.com/toeverything/pdf-extract", branch = "darksky/improve-font-decoding" }
phf = { version = "0.11", features = ["macros"] }
proptest = "1.3"
proptest-derive = "0.5"
@@ -89,14 +77,12 @@ resolver = "3"
rand_chacha = "0.9"
rand_distr = "0.5"
rayon = "1.10"
readability = { version = "0.3.0", default-features = false }
regex = "1.10"
rubato = "0.16"
safefetch = "0.1.0"
schemars = "0.8"
screencapturekit = "0.3"
serde = "1"
serde_json = "1"
sha2 = "0.10"
sha3 = "0.10"
smol_str = "0.3"
sqlx = { version = "0.8", default-features = false, features = [
@@ -105,19 +91,26 @@ resolver = "3"
"migrate",
"runtime-tokio",
"sqlite",
"tls-rustls",
] }
strum_macros = "0.27.0"
symphonia = { version = "0.5", features = ["all", "opt-simd"] }
text-splitter = "0.27"
thiserror = "2"
tiktoken-rs = "0.7"
tokio = "1.45"
typst = "0.14.2"
typst-as-lib = { version = "0.15.4", default-features = false, features = [
"packages",
"typst-kit-embed-fonts",
"typst-kit-fonts",
"ureq",
] }
typst-svg = "0.14.2"
tree-sitter = { version = "0.25" }
tree-sitter-c = { version = "0.24" }
tree-sitter-c-sharp = { version = "0.23" }
tree-sitter-cpp = { version = "0.23" }
tree-sitter-go = { version = "0.23" }
tree-sitter-java = { version = "0.23" }
tree-sitter-javascript = { version = "0.23" }
tree-sitter-kotlin-ng = { version = "1.1" }
tree-sitter-python = { version = "0.23" }
tree-sitter-rust = { version = "0.24" }
tree-sitter-scala = { version = "0.24" }
tree-sitter-typescript = { version = "0.23" }
uniffi = "0.29"
url = { version = "2.5" }
uuid = "1.8"
@@ -135,7 +128,7 @@ resolver = "3"
"Win32_UI_Shell_PropertiesSystem",
] }
windows-core = { version = "0.61" }
y-octo = "0.0.3"
y-octo = { path = "./packages/common/y-octo/core" }
y-sync = { version = "0.4" }
yrs = "0.23.0"
@@ -151,7 +144,3 @@ strip = "symbols"
# android uniffi bindgen requires symbols
[profile.release.package.affine_mobile_native]
strip = "none"
# [patch.crates-io]
# llm_adapter = { path = "../llm_adapter/crates/llm_adapter" }
# llm_runtime = { path = "../llm_adapter/crates/llm_runtime" }
-2
View File
@@ -23,6 +23,4 @@ We welcome you to provide us with bug reports via and email at [security@toevery
Since we are an open source project, we also welcome you to provide corresponding fix PRs, we will determine specific rewards based on the evaluation results.
Due to limited resources, we do not accept and will not review any AI-generated security reports.
If the vulnerability is caused by a library we depend on, we encourage you to submit a security report to the corresponding dependent library at the same time to benefit more users.
+5 -4
View File
@@ -78,7 +78,8 @@
"@blocksuite/global": "workspace:*",
"@blocksuite/std": "workspace:*",
"@blocksuite/store": "workspace:*",
"@blocksuite/sync": "workspace:*"
"@blocksuite/sync": "workspace:*",
"rxjs": "^7.8.2"
},
"exports": {
".": "./src/index.ts",
@@ -295,10 +296,10 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.27.0",
"version": "0.26.3",
"devDependencies": {
"@vanilla-extract/vite-plugin": "^5.0.0",
"msw": "^2.13.2",
"vitest": "^4.1.8"
"msw": "^2.12.4",
"vitest": "^3.2.4"
}
}
@@ -1,94 +0,0 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`snapshot to markdown > imports obsidian vault fixtures 1`] = `
{
"entry": {
"children": [
{
"children": [
{
"children": [
{
"delta": [
{
"insert": "Panel
Body line",
},
],
"flavour": "affine:paragraph",
"type": "text",
},
],
"emoji": "💡",
"flavour": "affine:callout",
},
{
"flavour": "affine:attachment",
"name": "archive.zip",
"style": "horizontalThin",
},
{
"delta": [
{
"footnote": {
"label": "1",
"reference": {
"title": "reference body",
"type": "url",
},
},
"insert": " ",
},
],
"flavour": "affine:paragraph",
"type": "text",
},
{
"flavour": "affine:divider",
},
{
"delta": [
{
"insert": "after note",
},
],
"flavour": "affine:paragraph",
"type": "text",
},
{
"delta": [
{
"insert": " ",
"reference": {
"page": "linked",
"type": "LinkedPage",
},
},
],
"flavour": "affine:paragraph",
"type": "text",
},
{
"delta": [
{
"insert": "Sources",
},
],
"flavour": "affine:paragraph",
"type": "h6",
},
{
"flavour": "affine:bookmark",
},
],
"flavour": "affine:note",
},
],
"flavour": "affine:page",
},
"titles": [
"entry",
"linked",
],
}
`;
@@ -1,14 +0,0 @@
> [!custom] Panel
> Body line
![[archive.zip]]
[^1]
---
after note
[[linked]]
[^1]: reference body
@@ -1 +0,0 @@
plain linked page
@@ -1,10 +1,4 @@
import { readFileSync } from 'node:fs';
import { basename, resolve } from 'node:path';
import {
MarkdownTransformer,
ObsidianTransformer,
} from '@blocksuite/affine/widgets/linked-doc';
import { MarkdownTransformer } from '@blocksuite/affine/widgets/linked-doc';
import {
DefaultTheme,
NoteDisplayMode,
@@ -14,18 +8,13 @@ import {
CalloutAdmonitionType,
CalloutExportStyle,
calloutMarkdownExportMiddleware,
docLinkBaseURLMiddleware,
embedSyncedDocMiddleware,
MarkdownAdapter,
titleMiddleware,
} from '@blocksuite/affine-shared/adapters';
import type { AffineTextAttributes } from '@blocksuite/affine-shared/types';
import type {
BlockSnapshot,
DeltaInsert,
DocSnapshot,
SliceSnapshot,
Store,
TransformerMiddleware,
} from '@blocksuite/store';
import { AssetsManager, MemoryBlobCRUD, Schema } from '@blocksuite/store';
@@ -40,138 +29,6 @@ import { testStoreExtensions } from '../utils/store.js';
const provider = getProvider();
function withRelativePath(file: File, relativePath: string): File {
Object.defineProperty(file, 'webkitRelativePath', {
value: relativePath,
writable: false,
});
return file;
}
function markdownFixture(relativePath: string): File {
return withRelativePath(
new File(
[
readFileSync(
resolve(import.meta.dirname, 'fixtures/obsidian', relativePath),
'utf8'
),
],
basename(relativePath),
{ type: 'text/markdown' }
),
`vault/${relativePath}`
);
}
function exportSnapshot(doc: Store): DocSnapshot {
const job = doc.getTransformer([
docLinkBaseURLMiddleware(doc.workspace.id),
titleMiddleware(doc.workspace.meta.docMetas),
]);
const snapshot = job.docToSnapshot(doc);
expect(snapshot).toBeTruthy();
return snapshot!;
}
function normalizeDeltaForSnapshot(
delta: DeltaInsert<AffineTextAttributes>[],
titleById: ReadonlyMap<string, string>
) {
return delta.map(item => {
const normalized: Record<string, unknown> = {
insert: item.insert,
};
if (item.attributes?.link) {
normalized.link = item.attributes.link;
}
if (item.attributes?.reference?.type === 'LinkedPage') {
normalized.reference = {
type: 'LinkedPage',
page: titleById.get(item.attributes.reference.pageId) ?? '<missing>',
...(item.attributes.reference.title
? { title: item.attributes.reference.title }
: {}),
};
}
if (item.attributes?.footnote) {
const reference = item.attributes.footnote.reference;
normalized.footnote = {
label: item.attributes.footnote.label,
reference:
reference.type === 'doc'
? {
type: 'doc',
page: reference.docId
? (titleById.get(reference.docId) ?? '<missing>')
: '<missing>',
}
: {
type: reference.type,
...(reference.title ? { title: reference.title } : {}),
...(reference.fileName ? { fileName: reference.fileName } : {}),
},
};
}
return normalized;
});
}
function simplifyBlockForSnapshot(
block: BlockSnapshot,
titleById: ReadonlyMap<string, string>
): Record<string, unknown> {
const simplified: Record<string, unknown> = {
flavour: block.flavour,
};
if (block.flavour === 'affine:paragraph' || block.flavour === 'affine:list') {
simplified.type = block.props.type;
const text = block.props.text as
| { delta?: DeltaInsert<AffineTextAttributes>[] }
| undefined;
simplified.delta = normalizeDeltaForSnapshot(text?.delta ?? [], titleById);
}
if (block.flavour === 'affine:callout') {
simplified.emoji = block.props.emoji;
}
if (block.flavour === 'affine:attachment') {
simplified.name = block.props.name;
simplified.style = block.props.style;
}
if (block.flavour === 'affine:image') {
simplified.sourceId = '<asset>';
}
const children = (block.children ?? [])
.filter(child => child.flavour !== 'affine:surface')
.map(child => simplifyBlockForSnapshot(child, titleById));
if (children.length) {
simplified.children = children;
}
return simplified;
}
function snapshotDocByTitle(
collection: TestWorkspace,
title: string,
titleById: ReadonlyMap<string, string>
) {
const meta = collection.meta.docMetas.find(meta => meta.title === title);
expect(meta).toBeTruthy();
const doc = collection.getDoc(meta!.id)?.getStore({ id: meta!.id });
expect(doc).toBeTruthy();
return simplifyBlockForSnapshot(exportSnapshot(doc!).blocks, titleById);
}
describe('snapshot to markdown', () => {
test('code', async () => {
const blockSnapshot: BlockSnapshot = {
@@ -270,94 +127,6 @@ Hello world
expect(meta?.tags).toEqual(['a', 'b']);
});
test('preserves list text inside blockquotes without list blocks', async () => {
const markdown = `> **Shopping List:**
> - Apples
> - Bananas
> - Oranges
`;
const mdAdapter = new MarkdownAdapter(createJob(), provider);
const snapshot = await mdAdapter.toDocSnapshot({
file: markdown,
assets: new AssetsManager({ blob: new MemoryBlobCRUD() }),
});
expect(simplifyBlockForSnapshot(snapshot.blocks, new Map())).toMatchObject({
children: [
{
flavour: 'affine:note',
children: [
{
flavour: 'affine:paragraph',
type: 'quote',
delta: [
{ insert: 'Shopping List:' },
{ insert: '\n' },
{ insert: '- ' },
{ insert: 'Apples' },
{ insert: '\n' },
{ insert: '- ' },
{ insert: 'Bananas' },
{ insert: '\n' },
{ insert: '- ' },
{ insert: 'Oranges' },
],
},
],
},
],
});
const exported = await mdAdapter.fromDocSnapshot({
snapshot,
assets: new AssetsManager({ blob: new MemoryBlobCRUD() }),
});
expect(exported.file).toContain('> **Shopping List:**');
expect(exported.file).toContain('> \\- Apples');
expect(exported.file).toContain('> \\- Bananas');
expect(exported.file).toContain('> \\- Oranges');
});
test('imports obsidian vault fixtures', async () => {
const schema = new Schema().register(AffineSchemas);
const collection = new TestWorkspace();
collection.storeExtensions = testStoreExtensions;
collection.meta.initialize();
const attachment = withRelativePath(
new File([new Uint8Array([80, 75, 3, 4])], 'archive.zip', {
type: 'application/zip',
}),
'vault/archive.zip'
);
const { docIds } = await ObsidianTransformer.importObsidianVault({
collection,
schema,
importedFiles: [
markdownFixture('entry.md'),
markdownFixture('linked.md'),
attachment,
],
extensions: testStoreExtensions,
});
expect(docIds).toHaveLength(2);
const titleById = new Map(
collection.meta.docMetas.map(meta => [
meta.id,
meta.title ?? '<untitled>',
])
);
expect({
titles: collection.meta.docMetas
.map(meta => meta.title)
.sort((a, b) => (a ?? '').localeCompare(b ?? '')),
entry: snapshotDocByTitle(collection, 'entry', titleById),
}).toMatchSnapshot();
});
test('paragraph', async () => {
const blockSnapshot: BlockSnapshot = {
type: 'block',
@@ -1,92 +0,0 @@
import { describe, expect, test } from 'vitest';
import { bilibiliConfig } from '../../../blocks/embed/src/embed-iframe-block/configs/providers/bilibili.js';
import { excalidrawConfig } from '../../../blocks/embed/src/embed-iframe-block/configs/providers/excalidraw.js';
import { genericConfig } from '../../../blocks/embed/src/embed-iframe-block/configs/providers/generic.js';
import { googleDocsConfig } from '../../../blocks/embed/src/embed-iframe-block/configs/providers/google-docs.js';
import { googleDriveConfig } from '../../../blocks/embed/src/embed-iframe-block/configs/providers/google-drive.js';
import { miroConfig } from '../../../blocks/embed/src/embed-iframe-block/configs/providers/miro.js';
import { spotifyConfig } from '../../../blocks/embed/src/embed-iframe-block/configs/providers/spotify.js';
describe('embed iframe provider config', () => {
test('validates final iframe URLs from oEmbed providers', () => {
expect(
spotifyConfig.validateIframeUrl?.(
'https://open.spotify.com/embed/track/0TK2YIli7K1leLovkQiNik'
)
).toBe(true);
expect(
spotifyConfig.validateIframeUrl?.(
'https://example.com/embed/track/0TK2YIli7K1leLovkQiNik'
)
).toBe(false);
});
test('validates provider-specific iframe URL shapes', () => {
expect(
googleDriveConfig.validateIframeUrl?.(
'https://drive.google.com/file/d/file-id/preview?usp=embed_googleplus'
)
).toBe(true);
expect(
googleDriveConfig.validateIframeUrl?.(
'https://drive.google.com/drive/folders/folder-id?usp=sharing'
)
).toBe(false);
expect(
bilibiliConfig.validateIframeUrl?.(
'https://player.bilibili.com/player.html?bvid=BV1xx411c7mD&autoplay=0'
)
).toBe(true);
expect(
bilibiliConfig.match(
'https://player.bilibili.com/player.html?aid=123&autoplay=0'
)
).toBe(true);
expect(
bilibiliConfig.buildOEmbedUrl(
'https://player.bilibili.com/video/BV1xx411c7mD'
)
).toBe(
'https://player.bilibili.com/player.html?bvid=BV1xx411c7mD&autoplay=0'
);
expect(
bilibiliConfig.validateIframeUrl?.(
'https://www.bilibili.com/video/BV1xx411c7mD'
)
).toBe(false);
expect(
googleDocsConfig.validateIframeUrl?.(
'https://docs.google.com/document/d/doc-id/edit?usp=sharing'
)
).toBe(true);
expect(
miroConfig.validateIframeUrl?.(
'https://miro.com/app/live-embed/board-id/'
)
).toBe(true);
expect(
excalidrawConfig.validateIframeUrl?.('https://excalidraw.com/#room-id')
).toBe(true);
});
test('generic iframe validation excludes affine and non-https URLs', () => {
expect(genericConfig.validateIframeUrl?.('https://example.com/embed')).toBe(
true
);
expect(genericConfig.validateIframeUrl?.('http://example.com/embed')).toBe(
false
);
expect(
genericConfig.validateIframeUrl?.('https://app.affine.pro/embed')
).toBe(false);
expect(genericConfig.validateIframeUrl?.('https://127.0.0.1/embed')).toBe(
false
);
expect(genericConfig.validateIframeUrl?.('https://localhost/embed')).toBe(
false
);
});
});
@@ -1,770 +0,0 @@
import { Bound } from '@blocksuite/global/gfx';
import { Viewport, viewportRuntimeConfig } from '@blocksuite/std/gfx';
import { afterEach, describe, expect, test, vi } from 'vitest';
import * as viewportModule from '../../../../../framework/std/src/gfx/viewport.js';
import * as viewportElementModule from '../../../../../framework/std/src/gfx/viewport-element.js';
import * as canvasRendererModule from '../../../../blocks/surface/src/renderer/canvas-renderer.js';
import {
paintPlaceholder,
syncCanvasSize,
} from '../../../../gfx/turbo-renderer/src/renderer-utils.js';
import type { ViewportLayoutTree } from '../../../../gfx/turbo-renderer/src/types.js';
const originalCaps = [...viewportRuntimeConfig.CANVAS_DPR_CAP_BY_ZOOM];
const originalDevicePixelRatio = Object.getOwnPropertyDescriptor(
window,
'devicePixelRatio'
);
function setDevicePixelRatio(value: number) {
Object.defineProperty(window, 'devicePixelRatio', {
configurable: true,
value,
});
}
function createRect(width: number, height: number): DOMRect {
return {
width,
height,
left: 0,
top: 0,
right: width,
bottom: height,
x: 0,
y: 0,
toJSON: () => ({}),
} as DOMRect;
}
function createFakeBlockModel(
id: string,
x: number,
y: number,
w = 10,
h = 10
) {
return {
id,
elementBound: new Bound(x, y, w, h),
};
}
type PaintPlaceholderForTest = (
canvas: HTMLCanvasElement,
layout: ViewportLayoutTree,
viewport: {
zoom: number;
toViewCoord: (x: number, y: number) => [number, number];
}
) => void;
afterEach(() => {
viewportRuntimeConfig.CANVAS_DPR_CAP_BY_ZOOM = [...originalCaps];
if (originalDevicePixelRatio) {
Object.defineProperty(window, 'devicePixelRatio', originalDevicePixelRatio);
}
vi.restoreAllMocks();
});
describe('edgeless canvas budget', () => {
test('requests canvas budget sync when zoom crosses an effective dpr bucket', () => {
viewportRuntimeConfig.CANVAS_DPR_CAP_BY_ZOOM = [
[0.5, 1],
[0.8, 2],
];
expect(
'shouldSyncCanvasBudgetOnViewportUpdate' in canvasRendererModule
).toBe(true);
const shouldSyncCanvasBudgetOnViewportUpdate = (
canvasRendererModule as {
shouldSyncCanvasBudgetOnViewportUpdate: (
previousZoom: number,
nextZoom: number,
rawDpr?: number
) => boolean;
}
).shouldSyncCanvasBudgetOnViewportUpdate;
expect(shouldSyncCanvasBudgetOnViewportUpdate(0.95, 0.4, 2)).toBe(true);
expect(shouldSyncCanvasBudgetOnViewportUpdate(0.95, 0.75, 2)).toBe(false);
expect(shouldSyncCanvasBudgetOnViewportUpdate(0.45, 0.4, 2)).toBe(false);
expect(shouldSyncCanvasBudgetOnViewportUpdate(0.95, 0.4, 1)).toBe(false);
});
test('enables low-zoom survival mode only for active iOS gestures', () => {
expect('shouldUseLowZoomSurvivalMode' in canvasRendererModule).toBe(true);
const shouldUseLowZoomSurvivalMode = (
canvasRendererModule as {
shouldUseLowZoomSurvivalMode: (
isIOS: boolean,
zoom: number,
gestureActive: boolean
) => boolean;
}
).shouldUseLowZoomSurvivalMode;
expect(shouldUseLowZoomSurvivalMode(true, 0.4, true)).toBe(true);
expect(shouldUseLowZoomSurvivalMode(true, 0.6, true)).toBe(false);
expect(shouldUseLowZoomSurvivalMode(true, 0.4, false)).toBe(false);
expect(shouldUseLowZoomSurvivalMode(false, 0.4, true)).toBe(false);
});
test('does not enable canvas placeholders for low-zoom panning without zooming', () => {
expect('shouldRenderCanvasPlaceholders' in canvasRendererModule).toBe(true);
const shouldRenderCanvasPlaceholders = (
canvasRendererModule as {
shouldRenderCanvasPlaceholders: (params: {
isIOS: boolean;
zoom: number;
isPanning: boolean;
isZooming: boolean;
skipRefreshDuringGesture: boolean;
turboEnabled: boolean;
}) => boolean;
}
).shouldRenderCanvasPlaceholders;
expect(
shouldRenderCanvasPlaceholders({
isIOS: true,
zoom: 0.4,
isPanning: true,
isZooming: false,
skipRefreshDuringGesture: true,
turboEnabled: true,
})
).toBe(false);
expect(
shouldRenderCanvasPlaceholders({
isIOS: true,
zoom: 0.4,
isPanning: false,
isZooming: true,
skipRefreshDuringGesture: true,
turboEnabled: true,
})
).toBe(true);
});
test('shares one bypass decision for placeholder and render paths only during the low-zoom iOS landscape gesture or recovery window', () => {
expect('getStackingCanvasBypassState' in canvasRendererModule).toBe(true);
expect(
'shouldBypassStackingCanvasesDuringLowZoomGesture' in canvasRendererModule
).toBe(true);
const getStackingCanvasBypassState = (
canvasRendererModule as {
getStackingCanvasBypassState: (params: {
isIOS: boolean;
zoom: number;
gestureActive: boolean;
recoveryActive: boolean;
viewportWidth: number;
viewportHeight: number;
}) => boolean;
}
).getStackingCanvasBypassState;
const shouldBypassStackingCanvasesDuringLowZoomGesture = (
canvasRendererModule as {
shouldBypassStackingCanvasesDuringLowZoomGesture: (params: {
isIOS: boolean;
zoom: number;
gestureActive: boolean;
recoveryActive: boolean;
viewportWidth: number;
viewportHeight: number;
}) => boolean;
}
).shouldBypassStackingCanvasesDuringLowZoomGesture;
expect(
getStackingCanvasBypassState({
isIOS: true,
zoom: 0.4,
gestureActive: true,
recoveryActive: false,
viewportWidth: 932,
viewportHeight: 430,
})
).toBe(true);
expect(
getStackingCanvasBypassState({
isIOS: true,
zoom: 0.4,
gestureActive: false,
recoveryActive: true,
viewportWidth: 932,
viewportHeight: 430,
})
).toBe(true);
expect(
getStackingCanvasBypassState({
isIOS: true,
zoom: 0.4,
gestureActive: false,
recoveryActive: false,
viewportWidth: 932,
viewportHeight: 430,
})
).toBe(false);
expect(
shouldBypassStackingCanvasesDuringLowZoomGesture({
isIOS: true,
zoom: 0.4,
gestureActive: false,
recoveryActive: false,
viewportWidth: 932,
viewportHeight: 430,
})
).toBe(false);
expect(
getStackingCanvasBypassState({
isIOS: true,
zoom: 0.4,
gestureActive: true,
recoveryActive: false,
viewportWidth: 430,
viewportHeight: 932,
})
).toBe(false);
expect(
getStackingCanvasBypassState({
isIOS: true,
zoom: 0.6,
gestureActive: true,
recoveryActive: false,
viewportWidth: 932,
viewportHeight: 430,
})
).toBe(false);
expect(
getStackingCanvasBypassState({
isIOS: false,
zoom: 0.4,
gestureActive: true,
recoveryActive: false,
viewportWidth: 932,
viewportHeight: 430,
})
).toBe(false);
});
test('gesture low-zoom landscape bypass detaches stacking canvases through the existing attachment path', () => {
expect(
'shouldBypassStackingCanvasesDuringLowZoomGesture' in canvasRendererModule
).toBe(true);
expect('getStackingCanvasAttachmentDiff' in canvasRendererModule).toBe(
true
);
const shouldBypassStackingCanvasesDuringLowZoomGesture = (
canvasRendererModule as {
shouldBypassStackingCanvasesDuringLowZoomGesture: (params: {
isIOS: boolean;
zoom: number;
gestureActive: boolean;
recoveryActive: boolean;
viewportWidth: number;
viewportHeight: number;
}) => boolean;
}
).shouldBypassStackingCanvasesDuringLowZoomGesture;
const getStackingCanvasAttachmentDiff = (
canvasRendererModule as {
getStackingCanvasAttachmentDiff: (params: {
canvases: HTMLCanvasElement[];
wasAttached: boolean;
shouldAttach: boolean;
}) => {
added: HTMLCanvasElement[];
removed: HTMLCanvasElement[];
};
}
).getStackingCanvasAttachmentDiff;
const canvases = [document.createElement('canvas')];
const shouldBypass = shouldBypassStackingCanvasesDuringLowZoomGesture({
isIOS: true,
zoom: 0.4,
gestureActive: true,
recoveryActive: false,
viewportWidth: 932,
viewportHeight: 430,
});
expect(shouldBypass).toBe(true);
expect(
getStackingCanvasAttachmentDiff({
canvases,
wasAttached: true,
shouldAttach: !shouldBypass,
})
).toEqual({
added: [],
removed: canvases,
});
});
test('uses overscan for main-canvas fallback culling and render origin', () => {
expect('getMainCanvasFallbackBounds' in canvasRendererModule).toBe(true);
const getMainCanvasFallbackBounds = (
canvasRendererModule as {
getMainCanvasFallbackBounds: (params: {
viewportBounds: Bound;
overscanViewportBounds: Bound;
}) => {
cullBound: Bound;
renderBound: Bound;
};
}
).getMainCanvasFallbackBounds;
const viewportBounds = new Bound(100, 200, 300, 150);
const overscanViewportBounds = new Bound(40, 170, 420, 210);
expect(
getMainCanvasFallbackBounds({
viewportBounds,
overscanViewportBounds,
})
).toEqual({
cullBound: overscanViewportBounds,
renderBound: overscanViewportBounds,
});
});
test('lays out overscan canvases relative to the exact viewport', () => {
expect('getCanvasViewportLayout' in canvasRendererModule).toBe(true);
const getCanvasViewportLayout = (
canvasRendererModule as {
getCanvasViewportLayout: (params: {
bound: Bound;
viewportBounds: Bound;
zoom: number;
viewScale: number;
dpr: number;
}) => {
actualHeight: number;
actualWidth: number;
height: number;
transform: string;
width: number;
};
}
).getCanvasViewportLayout;
expect(
getCanvasViewportLayout({
bound: new Bound(40, 170, 420, 210),
viewportBounds: new Bound(100, 200, 300, 150),
zoom: 1,
viewScale: 1,
dpr: 2,
})
).toEqual({
actualHeight: 420,
actualWidth: 840,
height: 210,
transform: 'translate(-60px, -30px) scale(1)',
width: 420,
});
});
test('computes stacking canvas DOM attachment diffs when bypass toggles', () => {
expect('getStackingCanvasAttachmentDiff' in canvasRendererModule).toBe(
true
);
const getStackingCanvasAttachmentDiff = (
canvasRendererModule as {
getStackingCanvasAttachmentDiff: (params: {
canvases: HTMLCanvasElement[];
wasAttached: boolean;
shouldAttach: boolean;
}) => {
added: HTMLCanvasElement[];
removed: HTMLCanvasElement[];
};
}
).getStackingCanvasAttachmentDiff;
const canvasA = document.createElement('canvas');
const canvasB = document.createElement('canvas');
const canvases = [canvasA, canvasB];
expect(
getStackingCanvasAttachmentDiff({
canvases,
wasAttached: true,
shouldAttach: false,
})
).toEqual({
added: [],
removed: canvases,
});
expect(
getStackingCanvasAttachmentDiff({
canvases,
wasAttached: false,
shouldAttach: true,
})
).toEqual({
added: canvases,
removed: [],
});
expect(
getStackingCanvasAttachmentDiff({
canvases,
wasAttached: true,
shouldAttach: true,
})
).toEqual({
added: [],
removed: [],
});
});
test('emits a lightweight zoom signal during gesture-skipped zoom updates so canvas budgets can shrink', () => {
viewportRuntimeConfig.CANVAS_DPR_CAP_BY_ZOOM = [
[0.5, 1],
[0.8, 2],
];
const viewport = new Viewport();
viewport.SKIP_REFRESH_DURING_GESTURE = true;
const viewportUpdated = vi.fn();
const zoomUpdates: Array<{ previousZoom: number; zoom: number }> = [];
let lastCanvasBudgetZoom = viewport.zoom;
let budgetSyncCount = 0;
viewport.viewportUpdated.subscribe(viewportUpdated);
expect('zoomUpdated' in viewport).toBe(true);
const zoomUpdated = (
viewport as unknown as {
zoomUpdated: {
subscribe: (
callback: (update: { previousZoom: number; zoom: number }) => void
) => void;
};
}
).zoomUpdated;
zoomUpdated.subscribe(update => {
zoomUpdates.push(update);
if (
(
canvasRendererModule as {
shouldSyncCanvasBudgetOnViewportUpdate: (
previousZoom: number,
nextZoom: number,
rawDpr?: number
) => boolean;
}
).shouldSyncCanvasBudgetOnViewportUpdate(
lastCanvasBudgetZoom,
update.zoom,
2
)
) {
budgetSyncCount += 1;
}
lastCanvasBudgetZoom = update.zoom;
});
viewport.panning$.next(true);
viewport.setZoom(0.4, { x: 0, y: 0 }, false, false, true);
expect(viewportUpdated).not.toHaveBeenCalled();
expect(zoomUpdates).toEqual([{ previousZoom: 1, zoom: 0.4 }]);
expect(budgetSyncCount).toBe(1);
viewport.dispose();
});
test('keeps programmatic setZoom on the normal viewport update path in skip mode', () => {
const viewport = new Viewport();
viewport.SKIP_REFRESH_DURING_GESTURE = true;
const viewportUpdated = vi.fn();
const zoomUpdated = vi.fn();
viewport.viewportUpdated.subscribe(viewportUpdated);
viewport.zoomUpdated.subscribe(zoomUpdated);
viewport.setZoom(0.4, { x: 0, y: 0 });
expect(viewportUpdated).toHaveBeenCalledTimes(1);
expect(zoomUpdated).toHaveBeenCalledWith({ previousZoom: 1, zoom: 0.4 });
expect(viewport.panning$.value).toBe(false);
expect(viewport.zooming$.value).toBe(false);
viewport.dispose();
});
test('enables low-zoom block survival only while the gesture is still active', () => {
expect('shouldUseLowZoomBlockSurvivalMode' in viewportElementModule).toBe(
true
);
const shouldUseLowZoomBlockSurvivalMode = (
viewportElementModule as {
shouldUseLowZoomBlockSurvivalMode: (params: {
zoom: number;
skipRefreshDuringGesture: boolean;
gestureActive: boolean;
}) => boolean;
}
).shouldUseLowZoomBlockSurvivalMode;
expect(
shouldUseLowZoomBlockSurvivalMode({
zoom: 0.4,
skipRefreshDuringGesture: true,
gestureActive: true,
})
).toBe(true);
expect(
shouldUseLowZoomBlockSurvivalMode({
zoom: 0.4,
skipRefreshDuringGesture: true,
gestureActive: false,
})
).toBe(false);
});
test('keeps selected and one nearby viewport block active during low-zoom gesture survival', () => {
expect('getLowZoomGestureActiveModels' in viewportElementModule).toBe(true);
const getLowZoomGestureActiveModels = (
viewportElementModule as {
getLowZoomGestureActiveModels: (params: {
selectedModels: Set<{ id: string; elementBound: Bound }>;
viewportModels: Set<{ id: string; elementBound: Bound }>;
viewportBounds: Bound;
nearbyActiveBlockLimit: number;
nearbyDistanceRatio: number;
}) => Set<{ id: string; elementBound: Bound }>;
}
).getLowZoomGestureActiveModels;
const selected = createFakeBlockModel('selected', 10, 10);
const nearby = createFakeBlockModel('nearby', 28, 12);
const far = createFakeBlockModel('far', 78, 78);
const activeModels = getLowZoomGestureActiveModels({
selectedModels: new Set([selected]),
viewportModels: new Set([selected, nearby, far]),
viewportBounds: new Bound(0, 0, 100, 100),
nearbyActiveBlockLimit: 1,
nearbyDistanceRatio: 0.35,
});
expect([...activeModels].map(model => model.id).sort()).toEqual([
'nearby',
'selected',
]);
});
test('falls back to the nearest viewport block when nothing is selected', () => {
expect('getLowZoomGestureActiveModels' in viewportElementModule).toBe(true);
const getLowZoomGestureActiveModels = (
viewportElementModule as {
getLowZoomGestureActiveModels: (params: {
selectedModels: Set<{ id: string; elementBound: Bound }>;
viewportModels: Set<{ id: string; elementBound: Bound }>;
viewportBounds: Bound;
nearbyActiveBlockLimit: number;
nearbyDistanceRatio: number;
}) => Set<{ id: string; elementBound: Bound }>;
}
).getLowZoomGestureActiveModels;
const nearest = createFakeBlockModel('nearest', 46, 46);
const farther = createFakeBlockModel('farther', 78, 78);
const activeModels = getLowZoomGestureActiveModels({
selectedModels: new Set(),
viewportModels: new Set([nearest, farther]),
viewportBounds: new Bound(0, 0, 100, 100),
nearbyActiveBlockLimit: 1,
nearbyDistanceRatio: 0.35,
});
expect([...activeModels].map(model => model.id)).toEqual(['nearest']);
});
test('starts post-gesture recovery immediately once gesture signals fully settle', () => {
expect('getPostGestureRecoveryDelay' in viewportModule).toBe(true);
const getPostGestureRecoveryDelay = (
viewportModule as {
getPostGestureRecoveryDelay: (params: {
isPanning: boolean;
isZooming: boolean;
fallbackDelayMs: number;
}) => number;
}
).getPostGestureRecoveryDelay;
expect(
getPostGestureRecoveryDelay({
isPanning: false,
isZooming: false,
fallbackDelayMs: 220,
})
).toBe(0);
});
test('keeps fallback post-gesture delay while a gesture signal is still active', () => {
expect('getPostGestureRecoveryDelay' in viewportModule).toBe(true);
const getPostGestureRecoveryDelay = (
viewportModule as {
getPostGestureRecoveryDelay: (params: {
isPanning: boolean;
isZooming: boolean;
fallbackDelayMs: number;
}) => number;
}
).getPostGestureRecoveryDelay;
expect(
getPostGestureRecoveryDelay({
isPanning: true,
isZooming: false,
fallbackDelayMs: 220,
})
).toBe(220);
expect(
getPostGestureRecoveryDelay({
isPanning: false,
isZooming: true,
fallbackDelayMs: 220,
})
).toBe(220);
});
test('sizes turbo renderer canvas with effective dpr at low zoom', () => {
viewportRuntimeConfig.CANVAS_DPR_CAP_BY_ZOOM = [
[0.5, 1],
[0.8, 2],
];
setDevicePixelRatio(2);
const canvas = document.createElement('canvas');
const host = document.createElement('div');
vi.spyOn(host, 'getBoundingClientRect').mockReturnValue(
createRect(200, 100)
);
(
syncCanvasSize as unknown as (
canvas: HTMLCanvasElement,
host: HTMLElement,
zoom: number
) => void
)(canvas, host, 0.4);
expect(canvas.width).toBe(200);
expect(canvas.height).toBe(100);
(
syncCanvasSize as unknown as (
canvas: HTMLCanvasElement,
host: HTMLElement,
zoom: number
) => void
)(canvas, host, 0.95);
expect(canvas.width).toBe(400);
expect(canvas.height).toBe(200);
});
test('paints turbo placeholders with effective dpr at low zoom', () => {
const previousTheme = document.documentElement.dataset.theme;
document.documentElement.dataset.theme = 'light';
try {
viewportRuntimeConfig.CANVAS_DPR_CAP_BY_ZOOM = [
[0.5, 1],
[0.8, 2],
];
setDevicePixelRatio(2);
const canvas = document.createElement('canvas');
const fillRect = vi.fn();
const strokeRect = vi.fn();
let fillStyle = '';
let strokeStyle = '';
vi.spyOn(canvas, 'getContext').mockReturnValue({
get fillStyle() {
return fillStyle;
},
set fillStyle(value: string) {
fillStyle = value;
},
get strokeStyle() {
return strokeStyle;
},
set strokeStyle(value: string) {
strokeStyle = value;
},
fillRect,
strokeRect,
} as unknown as CanvasRenderingContext2D);
const layout: ViewportLayoutTree = {
roots: [
{
blockId: 'root',
type: 'affine:page',
layout: {
blockId: 'root',
type: 'affine:page',
rect: { x: 0, y: 0, w: 50, h: 20 },
},
children: [],
},
],
overallRect: { x: 0, y: 0, w: 50, h: 20 },
};
const paintPlaceholderForTest =
paintPlaceholder as unknown as PaintPlaceholderForTest;
paintPlaceholderForTest(canvas, layout, {
zoom: 0.4,
toViewCoord: () => [0, 0],
});
expect(fillStyle).toBe('rgba(0, 0, 0, 0.04)');
expect(strokeStyle).toBe('rgba(0, 0, 0, 0.02)');
expect(fillRect).toHaveBeenLastCalledWith(0, 0, 20, 8);
paintPlaceholderForTest(canvas, layout, {
zoom: 0.95,
toViewCoord: () => [0, 0],
});
expect(fillRect).toHaveBeenLastCalledWith(0, 0, 95, 38);
} finally {
document.documentElement.dataset.theme = previousTheme;
}
});
});
@@ -1,34 +0,0 @@
import { ColorScheme } from '@blocksuite/affine-model';
import { describe, expect, it } from 'vitest';
import {
getAffinePlaceholderFillColor,
getAffinePlaceholderStrokeColor,
inferColorSchemeFromThemeMode,
} from '../../../../shared/src/theme/placeholder-style.js';
describe('affine placeholder style', () => {
it('returns subtle light placeholder colors', () => {
expect(getAffinePlaceholderFillColor(ColorScheme.Light)).toBe(
'rgba(0, 0, 0, 0.04)'
);
expect(getAffinePlaceholderStrokeColor(ColorScheme.Light)).toBe(
'rgba(0, 0, 0, 0.02)'
);
});
it('returns subtle dark placeholder colors', () => {
expect(getAffinePlaceholderFillColor(ColorScheme.Dark)).toBe(
'rgba(255, 255, 255, 0.08)'
);
expect(getAffinePlaceholderStrokeColor(ColorScheme.Dark)).toBe(
'rgba(255, 255, 255, 0.04)'
);
});
it('infers color scheme from theme mode', () => {
expect(inferColorSchemeFromThemeMode('dark')).toBe(ColorScheme.Dark);
expect(inferColorSchemeFromThemeMode('light')).toBe(ColorScheme.Light);
expect(inferColorSchemeFromThemeMode('')).toBe(ColorScheme.Light);
});
});
@@ -1,66 +0,0 @@
import { describe, expect, test } from 'vitest';
import * as turboRendererModule from '../../../../gfx/turbo-renderer/src/turbo-renderer.js';
describe('viewport turbo renderer policy', () => {
test.each([
{ isIOS: true, zoom: 0.4, hasBitmap: true, expected: true },
{ isIOS: true, zoom: 0.4, hasBitmap: false, expected: false },
{ isIOS: false, zoom: 0.4, hasBitmap: true, expected: false },
{ isIOS: true, zoom: 0.8, hasBitmap: true, expected: false },
])(
'prefers cached bitmap only for iOS low-zoom gestures with a bitmap %#',
({ isIOS, zoom, hasBitmap, expected }) => {
expect(
'shouldPreferBitmapCacheDuringLowZoomGesture' in turboRendererModule
).toBe(true);
const shouldPreferBitmapCacheDuringLowZoomGesture = (
turboRendererModule as {
shouldPreferBitmapCacheDuringLowZoomGesture: (params: {
isIOS: boolean;
zoom: number;
hasBitmap: boolean;
}) => boolean;
}
).shouldPreferBitmapCacheDuringLowZoomGesture;
expect(
shouldPreferBitmapCacheDuringLowZoomGesture({
isIOS,
zoom,
hasBitmap,
})
).toBe(expected);
}
);
test.each([
{ isIOS: true, zoom: 0.4, expected: false },
{ isIOS: true, zoom: 0.8, expected: true },
{ isIOS: false, zoom: 0.4, expected: true },
])(
'idles turbo blocks outside iOS low-zoom survival mode %#',
({ isIOS, zoom, expected }) => {
expect('shouldIdleTurboBlocksDuringZooming' in turboRendererModule).toBe(
true
);
const shouldIdleTurboBlocksDuringZooming = (
turboRendererModule as {
shouldIdleTurboBlocksDuringZooming: (params: {
isIOS: boolean;
zoom: number;
}) => boolean;
}
).shouldIdleTurboBlocksDuringZooming;
expect(
shouldIdleTurboBlocksDuringZooming({
isIOS,
zoom,
})
).toBe(expected);
}
);
});
+1 -1
View File
@@ -1,3 +1,3 @@
/* oxlint-disable no-restricted-imports */
/* eslint-disable @typescript-eslint/no-restricted-imports */
export * from '@blocksuite/store';
+2 -6
View File
@@ -1,5 +1,3 @@
import { fileURLToPath } from 'node:url';
import { vanillaExtractPlugin } from '@vanilla-extract/vite-plugin';
import { defineConfig } from 'vitest/config';
@@ -9,13 +7,11 @@ export default defineConfig({
},
plugins: [vanillaExtractPlugin()],
test: {
globalSetup: fileURLToPath(
new URL('../../../scripts/vitest-global.js', import.meta.url)
),
globalSetup: '../../../scripts/vitest-global.js',
include: ['src/__tests__/**/*.unit.spec.ts'],
testTimeout: 1000,
coverage: {
provider: 'istanbul',
provider: 'istanbul', // or 'c8'
reporter: ['lcov'],
reportsDirectory: '../../../.coverage/blocksuite-affine',
},
@@ -21,10 +21,13 @@
"@blocksuite/std": "workspace:*",
"@blocksuite/store": "workspace:*",
"@floating-ui/dom": "^1.6.13",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.23",
"file-type": "^21.0.0",
"lit": "^3.2.0",
"rxjs": "^7.8.2"
"rxjs": "^7.8.2",
"zod": "^3.25.76"
},
"exports": {
".": "./src/index.ts",
@@ -37,5 +40,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.27.0"
"version": "0.26.3"
}
@@ -5,7 +5,6 @@ import {
import {
BlockMarkdownAdapterExtension,
type BlockMarkdownAdapterMatcher,
createAttachmentBlockSnapshot,
FOOTNOTE_DEFINITION_PREFIX,
getFootnoteDefinitionText,
isFootnoteDefinitionNode,
@@ -57,15 +56,18 @@ export const attachmentBlockMarkdownAdapterMatcher: BlockMarkdownAdapterMatcher
}
walkerContext
.openNode(
createAttachmentBlockSnapshot({
{
type: 'block',
id: nanoid(),
flavour: AttachmentBlockSchema.model.flavour,
props: {
name: fileName,
sourceId: blobId,
footnoteIdentifier,
style: 'citation',
},
}),
children: [],
},
'children'
)
.closeNode();
@@ -54,7 +54,7 @@ type AttachmentResolvedStateInfo = ResolvedStateInfo & {
@Peekable({
enableOn: ({ model }: AttachmentBlockComponent) => {
return model.props.type.endsWith('pdf');
return !model.store.readonly && model.props.type.endsWith('pdf');
},
})
export class AttachmentBlockComponent extends CaptionedBlockComponent<AttachmentBlockModel> {
@@ -144,7 +144,7 @@ export const attachmentViewDropdownMenu = {
@toggle=${onToggle}
.actions=${actions.value}
.context=${ctx}
.viewTypeSignal=${viewType$}
.viewType$=${viewType$}
></affine-view-dropdown-menu>`;
},
} as const satisfies ToolbarActionGroup<ToolbarAction>;
@@ -366,7 +366,7 @@ const builtinSurfaceToolbarConfig = {
@toggle=${onToggle}
.actions=${actions}
.context=${ctx}
.styleSignal=${style$}
.style$=${style$}
></affine-card-style-dropdown-menu>`
)}`;
},
@@ -22,16 +22,16 @@
"@blocksuite/icons": "^2.2.17",
"@blocksuite/std": "workspace:*",
"@blocksuite/store": "workspace:*",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.23",
"lit": "^3.2.0",
"rxjs": "^7.8.2",
"yjs": "^13.6.27"
"yjs": "^13.6.27",
"zod": "^3.25.76"
},
"devDependencies": {
"@vitest/browser-playwright": "^4.1.8",
"playwright": "=1.58.2",
"vitest": "^4.1.8"
"vitest": "^3.2.4"
},
"exports": {
".": "./src/index.ts",
@@ -44,5 +44,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.27.0"
"version": "0.26.3"
}
@@ -108,9 +108,7 @@ export class BookmarkBlockComponent extends CaptionedBlockComponent<BookmarkBloc
}
open = () => {
const link = this.link;
if (!link) return;
window.open(link, '_blank', 'noopener,noreferrer');
window.open(this.link, '_blank');
};
refreshData = () => {
@@ -237,7 +237,7 @@ const builtinToolbarConfig = {
@toggle=${onToggle}
.actions=${actions}
.context=${ctx}
.viewTypeSignal=${viewType$}
.viewType$=${viewType$}
></affine-view-dropdown-menu>`
)}`;
},
@@ -282,7 +282,7 @@ const builtinToolbarConfig = {
@toggle=${onToggle}
.actions=${actions}
.context=${ctx}
.styleSignal=${model.props.style$}
.style$=${model.props.style$}
></affine-card-style-dropdown-menu>`
)}`;
},
@@ -472,7 +472,7 @@ const builtinSurfaceToolbarConfig = {
@toggle=${onToggle}
.actions=${actions}
.context=${ctx}
.viewTypeSignal=${viewType$}
.viewType$=${viewType$}
></affine-view-dropdown-menu>`
)}`;
},
@@ -534,7 +534,7 @@ const builtinSurfaceToolbarConfig = {
@toggle=${onToggle}
.actions=${actions}
.context=${ctx}
.styleSignal=${style$}
.style$=${style$}
></affine-card-style-dropdown-menu>`
)}`;
},
@@ -591,7 +591,7 @@ const builtinSurfaceToolbarConfig = {
@select=${onSelect}
@toggle=${onToggle}
.format=${format}
.sizeSignal=${scale$}
.size$=${scale$}
></affine-size-dropdown-menu>`
)}`;
},
@@ -1,4 +1,3 @@
import { playwright } from '@vitest/browser-playwright';
import { defineConfig } from 'vitest/config';
export default defineConfig({
@@ -9,9 +8,10 @@ export default defineConfig({
browser: {
enabled: true,
headless: true,
instances: [{ browser: 'chromium' }],
provider: playwright(),
name: 'chromium',
provider: 'playwright',
isolate: false,
providerOptions: {},
},
include: ['src/__tests__/**/*.unit.spec.ts'],
testTimeout: 500,
+10 -2
View File
@@ -17,13 +17,21 @@
"@blocksuite/affine-rich-text": "workspace:*",
"@blocksuite/affine-shared": "workspace:*",
"@blocksuite/affine-widget-slash-menu": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.17",
"@blocksuite/std": "workspace:*",
"@blocksuite/store": "workspace:*",
"@emoji-mart/data": "^1.2.1",
"@emotion/css": "^11.13.5",
"@floating-ui/dom": "^1.6.10",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.23",
"lit": "^3.2.0"
"@types/mdast": "^4.0.4",
"emoji-mart": "^5.6.0",
"lit": "^3.2.0",
"rxjs": "^7.8.2",
"zod": "^3.25.76"
},
"exports": {
".": "./src/index.ts",
@@ -36,5 +44,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.27.0"
"version": "0.26.3"
}
@@ -25,4 +25,4 @@ export const calloutTooltip = html`<svg width="170" height="106" viewBox="0 0 17
</clipPath>
</defs>
</svg>
`;
`
@@ -14,6 +14,7 @@
{ "path": "../../rich-text" },
{ "path": "../../shared" },
{ "path": "../../widgets/slash-menu" },
{ "path": "../../../framework/global" },
{ "path": "../../../framework/std" },
{ "path": "../../../framework/store" }
]
+4 -1
View File
@@ -26,9 +26,12 @@
"@blocksuite/std": "workspace:*",
"@blocksuite/store": "workspace:*",
"@floating-ui/dom": "^1.6.13",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.23",
"@types/mdast": "^4.0.4",
"lit": "^3.2.0",
"rxjs": "^7.8.2",
"shiki": "^3.19.0",
"zod": "^3.25.76"
},
@@ -44,5 +47,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.27.0"
"version": "0.26.3"
}
@@ -1,5 +1,4 @@
import { deleteTextCommand } from '@blocksuite/affine-inline-preset';
import type { RichText } from '@blocksuite/affine-rich-text';
import {
HtmlAdapter,
pasteMiddleware,
@@ -19,7 +18,6 @@ import {
LifeCycleWatcher,
LifeCycleWatcherIdentifier,
StdIdentifier,
TextSelection,
type UIEventHandler,
} from '@blocksuite/std';
import type { ExtensionType } from '@blocksuite/store';
@@ -105,30 +103,6 @@ export class CodeBlockClipboardController extends LifeCycleWatcher {
const e = ctx.get('clipboardState').raw;
e.preventDefault();
const textSelection = this.std.selection.find(TextSelection);
const plainText = e.clipboardData
?.getData('text/plain')
?.replace(/\r?\n|\r/g, '\n');
const selectedBlockId = textSelection?.from.blockId;
const codeBlock = selectedBlockId
? this.std.store.getBlock(selectedBlockId)?.model
: null;
if (plainText && codeBlock?.flavour === 'affine:code' && selectedBlockId) {
const richText = this.std.view
.getBlock(selectedBlockId)
?.querySelector<RichText>('rich-text');
const inlineEditor = richText?.inlineEditor;
const inlineRange = inlineEditor?.getInlineRange();
if (inlineEditor && inlineRange) {
inlineEditor.insertText(inlineRange, plainText);
inlineEditor.setInlineRange({
index: inlineRange.index + plainText.length,
length: 0,
});
return true;
}
}
this.std.store.captureSync();
this.std.command
.chain()
@@ -39,7 +39,10 @@ export class CodeBlockHighlighter extends LifeCycleWatcher {
private readonly _loadTheme = async (
highlighter: HighlighterCore
): Promise<void> => {
if (!CodeBlockHighlighter._isHighlighterInUse(highlighter)) {
// It is possible that by the time the highlighter is ready all instances
// have already been unmounted. In that case there is no need to load
// themes or update state.
if (CodeBlockHighlighter._refCount === 0) {
return;
}
@@ -48,17 +51,7 @@ export class CodeBlockHighlighter extends LifeCycleWatcher {
const lightTheme = config?.theme?.light ?? CODE_BLOCK_DEFAULT_LIGHT_THEME;
this._darkThemeKey = (await normalizeGetter(darkTheme)).name;
this._lightThemeKey = (await normalizeGetter(lightTheme)).name;
if (!CodeBlockHighlighter._isHighlighterInUse(highlighter)) {
return;
}
await highlighter.loadTheme(darkTheme, lightTheme);
if (!CodeBlockHighlighter._isHighlighterInUse(highlighter)) {
return;
}
this.highlighter$.value = highlighter;
};
@@ -90,18 +83,30 @@ export class CodeBlockHighlighter extends LifeCycleWatcher {
}
override unmounted(): void {
CodeBlockHighlighter._refCount = Math.max(
0,
CodeBlockHighlighter._refCount - 1
);
this.highlighter$.value = null;
}
CodeBlockHighlighter._refCount--;
private static _isHighlighterInUse(highlighter: HighlighterCore) {
return (
CodeBlockHighlighter._refCount > 0 &&
CodeBlockHighlighter._sharedHighlighter === highlighter
);
// Dispose the shared highlighter **after** any in-flight creation finishes.
if (CodeBlockHighlighter._refCount !== 0) {
return;
}
const doDispose = (highlighter: HighlighterCore | null) => {
if (highlighter) {
highlighter.dispose();
}
CodeBlockHighlighter._sharedHighlighter = null;
CodeBlockHighlighter._highlighterPromise = null;
};
if (CodeBlockHighlighter._sharedHighlighter) {
// Highlighter already created dispose immediately.
doDispose(CodeBlockHighlighter._sharedHighlighter);
} else if (CodeBlockHighlighter._highlighterPromise) {
// Highlighter still being created wait for it, then dispose.
CodeBlockHighlighter._highlighterPromise
.then(doDispose)
.catch(console.error);
}
}
}
@@ -51,10 +51,6 @@ export class CodeBlockComponent extends CaptionedBlockComponent<CodeBlockModel>
return modelPreview;
});
collapsed$: Signal<boolean> = computed(
() => !!this.model.props.collapsed$.value
);
highlightTokens$: Signal<ThemedToken[][]> = signal([]);
languageName$: Signal<string> = computed(() => {
@@ -421,7 +417,6 @@ export class CodeBlockComponent extends CaptionedBlockComponent<CodeBlockModel>
CodeBlockPreviewIdentifier(this.model.props.language ?? '')
);
const shouldRenderPreview = preview && previewContext;
const collapsed = this.collapsed$.value;
return html`
<div
@@ -431,7 +426,6 @@ export class CodeBlockComponent extends CaptionedBlockComponent<CodeBlockModel>
mobile: IS_MOBILE,
wrap: this.model.props.wrap,
'disable-line-numbers': !showLineNumbers,
collapsed,
})}
>
<rich-text
@@ -459,12 +453,9 @@ export class CodeBlockComponent extends CaptionedBlockComponent<CodeBlockModel>
}}
>
</rich-text>
${collapsed
? html`<div class="code-collapsed-fade" aria-hidden="true"></div>`
: nothing}
<div
style=${styleMap({
display: shouldRenderPreview && !collapsed ? undefined : 'none',
display: shouldRenderPreview ? undefined : 'none',
})}
contenteditable="false"
class="affine-code-block-preview"
@@ -480,10 +471,6 @@ export class CodeBlockComponent extends CaptionedBlockComponent<CodeBlockModel>
this.store.updateBlock(this.model, { wrap });
}
setCollapsed(collapsed: boolean) {
this.store.updateBlock(this.model, { collapsed });
}
@query('rich-text')
private accessor _richTextElement: RichText | null = null;
@@ -9,9 +9,9 @@ import { WithDisposable } from '@blocksuite/global/lit';
import { noop } from '@blocksuite/global/utils';
import { MoreVerticalIcon } from '@blocksuite/icons/lit';
import { flip, offset } from '@floating-ui/dom';
import { effect } from '@preact/signals-core';
import { css, html, LitElement } from 'lit';
import { property, query, state } from 'lit/decorators.js';
import { styleMap } from 'lit/directives/style-map.js';
import type { CodeBlockToolbarContext } from '../context.js';
@@ -82,10 +82,18 @@ export class AffineCodeToolbar extends WithDisposable(LitElement) {
createLitPortal({
template: html`
<affine-code-more-menu
.context=${this.context}
.moreGroups=${this.moreGroups}
></affine-code-more-menu>
<editor-menu-content
data-show
class="more-popup-menu"
style=${styleMap({
'--content-padding': '8px',
'--packed-height': '4px',
})}
>
<div data-size="large" data-orientation="vertical">
${renderGroups(this.moreGroups, this.context)}
</div>
</editor-menu-content>
`,
// should be greater than block-selection z-index as selection and popover wil share the same stacking context(editor-host)
portalStyles: {
@@ -109,17 +117,6 @@ export class AffineCodeToolbar extends WithDisposable(LitElement) {
this.closeCurrentMenu();
}
override connectedCallback() {
super.connectedCallback();
// Mirror the collapsed$ signal from the block component into local @state
// so this LitElement re-renders when it changes.
this.disposables.add(
effect(() => {
this._collapsed = this.context.blockComponent.collapsed$.value;
})
);
}
override render() {
return html`
<editor-toolbar class="code-toolbar-container" data-without-bg>
@@ -148,9 +145,6 @@ export class AffineCodeToolbar extends WithDisposable(LitElement) {
@state()
private accessor _moreMenuOpen = false;
@state()
private accessor _collapsed = false;
@property({ attribute: false })
accessor context!: CodeBlockToolbarContext;
@@ -1,52 +0,0 @@
import type { MenuItemGroup } from '@blocksuite/affine-components/toolbar';
import { renderGroups } from '@blocksuite/affine-components/toolbar';
import { SignalWatcher, WithDisposable } from '@blocksuite/global/lit';
import { ShadowlessElement } from '@blocksuite/std';
import { html } from 'lit';
import { property } from 'lit/decorators.js';
import { styleMap } from 'lit/directives/style-map.js';
import type { CodeBlockToolbarContext } from '../context.js';
export class AffineCodeMoreMenu extends SignalWatcher(
WithDisposable(ShadowlessElement)
) {
override firstUpdated() {
this.disposables.add(
this.context.blockComponent.model.propsUpdated.subscribe(({ key }) => {
if (key === 'wrap' || key === 'lineNumber') {
this.requestUpdate();
}
})
);
}
override render() {
return html`
<editor-menu-content
data-show
class="more-popup-menu"
style=${styleMap({
'--content-padding': '8px',
'--packed-height': '4px',
})}
>
<div data-size="large" data-orientation="vertical">
${renderGroups(this.moreGroups, this.context)}
</div>
</editor-menu-content>
`;
}
@property({ attribute: false })
accessor context!: CodeBlockToolbarContext;
@property({ attribute: false })
accessor moreGroups!: MenuItemGroup<CodeBlockToolbarContext>[];
}
declare global {
interface HTMLElementTagNameMap {
'affine-code-more-menu': AffineCodeMoreMenu;
}
}
@@ -1,11 +1,9 @@
import {
CancelWrapIcon,
CaptionIcon,
CollapseCodeIcon,
CopyIcon,
DeleteIcon,
DuplicateIcon,
ExpandCodeIcon,
WrapIcon,
} from '@blocksuite/affine-components/icons';
import type { MenuItemGroup } from '@blocksuite/affine-components/toolbar';
@@ -87,38 +85,6 @@ export const PRIMARY_GROUPS: MenuItemGroup<CodeBlockToolbarContext>[] = [
};
},
},
{
type: 'collapse',
when: ({ doc }) => !doc.readonly,
generate: ({ blockComponent }) => {
return {
action: () => {
blockComponent.setCollapsed(!blockComponent.collapsed$.value);
},
render: item => {
const collapsed = blockComponent.collapsed$.value;
const icon = collapsed ? ExpandCodeIcon : CollapseCodeIcon;
const label = collapsed ? 'Expand code' : 'Collapse code';
return html`
<editor-icon-button
class="code-toolbar-button collapse"
aria-label=${label}
.tooltip=${label}
.tooltipOffset=${4}
.iconSize=${'16px'}
.iconContainerPadding=${4}
@click=${(e: MouseEvent) => {
e.stopPropagation();
item.action();
}}
>
${icon}
</editor-icon-button>
`;
},
};
},
},
{
type: 'caption',
label: 'Caption',
@@ -208,8 +174,7 @@ export const toggleGroup: MenuItemGroup<CodeBlockToolbarContext> = {
return html`
<editor-menu-action
@click=${() => {
const currentWrap = blockComponent.model.props.wrap;
blockComponent.setWrap(!currentWrap);
blockComponent.setWrap(!wrapped);
}}
aria-label=${label}
>
@@ -239,10 +204,8 @@ export const toggleGroup: MenuItemGroup<CodeBlockToolbarContext> = {
return html`
<editor-menu-action
@click=${() => {
const currentLineNumber =
blockComponent.model.props.lineNumber ?? true;
blockComponent.store.updateBlock(blockComponent.model, {
lineNumber: !currentLineNumber,
lineNumber: !lineNumber,
});
}}
aria-label=${label}
@@ -5,14 +5,12 @@ import {
} from './code-toolbar';
import { AffineCodeToolbar } from './code-toolbar/components/code-toolbar';
import { LanguageListButton } from './code-toolbar/components/lang-button';
import { AffineCodeMoreMenu } from './code-toolbar/components/more-menu';
import { PreviewButton } from './code-toolbar/components/preview-button';
import { AffineCodeUnit } from './highlight/affine-code-unit';
export function effects() {
customElements.define('language-list-button', LanguageListButton);
customElements.define('affine-code-toolbar', AffineCodeToolbar);
customElements.define('affine-code-more-menu', AffineCodeMoreMenu);
customElements.define(AFFINE_CODE_TOOLBAR_WIDGET, AffineCodeToolbarWidget);
customElements.define('affine-code-unit', AffineCodeUnit);
customElements.define('affine-code', CodeBlockComponent);
@@ -23,7 +21,6 @@ declare global {
interface HTMLElementTagNameMap {
'language-list-button': LanguageListButton;
'affine-code-toolbar': AffineCodeToolbar;
'affine-code-more-menu': AffineCodeMoreMenu;
'preview-button': PreviewButton;
[AFFINE_CODE_TOOLBAR_WIDGET]: AffineCodeToolbarWidget;
}
@@ -45,10 +45,8 @@ export class AffineCodeUnit extends ShadowlessElement {
if (!codeBlock || !vElement) return plainContent;
const tokens = codeBlock.highlightTokens$.value;
if (tokens.length === 0) return plainContent;
const line = tokens[vElement.lineIndex];
if (!line) return plainContent;
// copy the tokens to avoid modifying the original tokens
const lineTokens = structuredClone(line);
const lineTokens = structuredClone(tokens[vElement.lineIndex]);
if (lineTokens.length === 0) return plainContent;
const startOffset = vElement.startOffset;
@@ -80,35 +80,4 @@ export const codeBlockStyles = css`
affine-code .affine-code-block-preview {
padding: 12px;
}
/* ── Collapsed state ──────────────────────────────────────────────── */
/* Clamp the rich-text to the first 8 lines */
.affine-code-block-container.collapsed rich-text {
display: block;
max-height: calc(8 * var(--affine-line-height));
overflow: hidden;
}
/* Reduce bottom padding so the fade sits flush with the border */
.affine-code-block-container.collapsed {
padding-bottom: 0;
}
/* Gradient overlay that fades to the block background */
.affine-code-block-container .code-collapsed-fade {
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 80px;
background: linear-gradient(
to bottom,
transparent,
var(--affine-background-code-block)
);
border-radius: 0 0 10px 10px;
pointer-events: none;
z-index: 1;
}
`;
@@ -21,9 +21,14 @@
"@blocksuite/icons": "^2.2.17",
"@blocksuite/std": "workspace:*",
"@blocksuite/store": "workspace:*",
"@floating-ui/dom": "^1.6.13",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.23",
"@types/mdast": "^4.0.4",
"lit": "^3.2.0",
"rxjs": "^7.8.2"
"rxjs": "^7.8.2",
"zod": "^3.25.76"
},
"exports": {
".": "./src/index.ts",
@@ -36,5 +41,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.27.0"
"version": "0.26.3"
}
@@ -254,7 +254,6 @@ export class DataViewBlockComponent extends CaptionedBlockComponent<DataViewBloc
dataSource: this.dataSource,
headerWidget: this.headerWidget,
clipboard: this.std.clipboard,
dnd: this.std.dnd,
notification: {
toast: message => {
const notification = this.std.getOptional(NotificationProvider);
@@ -26,11 +26,13 @@
"@blocksuite/store": "workspace:*",
"@emotion/css": "^11.13.5",
"@floating-ui/dom": "^1.6.13",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.23",
"@types/mdast": "^4.0.4",
"date-fns": "^4.4.0",
"date-fns": "^4.0.0",
"lit": "^3.2.0",
"rxjs": "^7.8.2",
"yjs": "^13.6.27",
"zod": "^3.25.76"
},
@@ -45,5 +47,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.27.0"
"version": "0.26.3"
}
@@ -54,9 +54,9 @@ type Cell = {
value: string | { delta: DeltaInsert[] };
};
export const processTable = (
columns: ColumnDataType[] = [],
children: BlockSnapshot[] = [],
cells: SerializedCells = {}
columns: ColumnDataType[],
children: BlockSnapshot[],
cells: SerializedCells
): Table => {
const table: Table = {
headers: columns,
@@ -90,17 +90,13 @@ export const processTable = (
return;
}
let value: string | { delta: DeltaInsert[] };
try {
if (isDelta(cell.value)) {
value = cell.value;
} else {
value = property.config.rawValue.toString({
value: cell.value,
data: col.data,
});
}
} catch {
value = '';
if (isDelta(cell.value)) {
value = cell.value;
} else {
value = property.config.rawValue.toString({
value: cell.value,
data: col.data,
});
}
row.cells.push({
value,
@@ -6,7 +6,6 @@ import { viewPresets } from '@blocksuite/data-view/view-presets';
import {
DatabaseKanbanViewIcon,
DatabaseTableViewIcon,
TodayIcon,
} from '@blocksuite/icons/lit';
import { insertDatabaseBlockCommand } from '../commands';
@@ -48,35 +47,6 @@ export const databaseSlashMenuConfig: SlashMenuConfig = {
},
},
{
name: 'Calendar View',
description: 'Display items by date in a calendar.',
searchAlias: ['database', 'calendar'],
icon: TodayIcon(),
group: '7_Database@1',
when: ({ model }) =>
!isInsideBlockByFlavour(model.store, model, 'affine:edgeless-text'),
action: ({ std }) => {
std.command
.chain()
.pipe(getSelectedModelsCommand)
.pipe(insertDatabaseBlockCommand, {
viewType: viewPresets.calendarViewMeta.type,
place: 'after',
removeEmptyLine: true,
})
.pipe(({ insertedDatabaseBlockId }) => {
if (insertedDatabaseBlockId) {
const telemetry = std.getOptional(TelemetryProvider);
telemetry?.track('BlockCreated', {
blockType: 'affine:database',
});
}
})
.run();
},
},
{
name: 'Kanban View',
description: 'Visualize data in a dashboard.',
@@ -34,7 +34,6 @@ import {
type SingleView,
uniMap,
} from '@blocksuite/data-view';
import { CalendarExternalSourceProvider } from '@blocksuite/data-view/view-presets';
import { widgetPresets } from '@blocksuite/data-view/widget-presets';
import { IS_MOBILE } from '@blocksuite/global/env';
import { Rect } from '@blocksuite/global/gfx';
@@ -151,14 +150,6 @@ export class DatabaseBlockComponent extends CaptionedBlockComponent<DatabaseBloc
config
);
});
this.std.provider
.getAll(CalendarExternalSourceProvider)
.forEach(source => {
dataSource.serviceSet(
CalendarExternalSourceProvider(source.id),
source
);
});
});
const id = currentViewStorage.getCurrentView(this.model.id);
if (id && dataSource.viewManager.viewGet(id)) {
@@ -302,12 +293,6 @@ export class DatabaseBlockComponent extends CaptionedBlockComponent<DatabaseBloc
widgetPresets.tools.viewOptions,
widgetPresets.tools.tableAddRow,
],
calendar: [
widgetPresets.tools.filter,
widgetPresets.tools.search,
widgetPresets.tools.viewOptions,
widgetPresets.tools.tableAddRow,
],
});
private readonly viewSelection$ = computed(() => {
@@ -442,7 +427,6 @@ export class DatabaseBlockComponent extends CaptionedBlockComponent<DatabaseBloc
headerWidget: this.headerWidget,
onDrag: this.onDrag,
clipboard: this.std.clipboard,
dnd: this.std.dnd,
notification: {
toast: message => {
const notification = this.std.getOptional(NotificationProvider);
@@ -47,7 +47,7 @@ export const databasePropertyConverts = [
propertyModelPresets.multiSelectPropertyModelConfig,
(_property, cells) => {
const options: Record<string, SelectTag> = {};
// oxlint-disable-next-line sonarjs/no-identical-functions
// eslint-disable-next-line sonarjs/no-identical-functions
const getTag = (name: string) => {
if (options[name]) return options[name];
const tag: SelectTag = {
@@ -4,7 +4,6 @@ import { viewConverts, viewPresets } from '@blocksuite/data-view/view-presets';
export const databaseBlockViews: ViewMeta[] = [
viewPresets.tableViewMeta,
viewPresets.kanbanViewMeta,
viewPresets.calendarViewMeta,
];
export const databaseBlockViewMap = Object.fromEntries(
@@ -15,10 +15,17 @@
"@blocksuite/affine-model": "workspace:*",
"@blocksuite/affine-rich-text": "workspace:*",
"@blocksuite/affine-shared": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/std": "workspace:*",
"@blocksuite/store": "workspace:*",
"@floating-ui/dom": "^1.6.13",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.23",
"@types/mdast": "^4.0.4",
"lit": "^3.2.0"
"lit": "^3.2.0",
"rxjs": "^7.8.2",
"zod": "^3.25.76"
},
"exports": {
".": "./src/index.ts",
@@ -31,5 +38,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.27.0"
"version": "0.26.3"
}
@@ -12,6 +12,7 @@
{ "path": "../../model" },
{ "path": "../../rich-text" },
{ "path": "../../shared" },
{ "path": "../../../framework/global" },
{ "path": "../../../framework/std" },
{ "path": "../../../framework/store" }
]
@@ -11,17 +11,25 @@
"license": "MIT",
"dependencies": {
"@blocksuite/affine-block-surface": "workspace:*",
"@blocksuite/affine-components": "workspace:*",
"@blocksuite/affine-ext-loader": "workspace:*",
"@blocksuite/affine-gfx-text": "workspace:*",
"@blocksuite/affine-inline-preset": "workspace:*",
"@blocksuite/affine-model": "workspace:*",
"@blocksuite/affine-rich-text": "workspace:*",
"@blocksuite/affine-shared": "workspace:*",
"@blocksuite/affine-widget-edgeless-toolbar": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.17",
"@blocksuite/std": "workspace:*",
"@blocksuite/store": "workspace:*",
"@floating-ui/dom": "^1.6.13",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"lit": "^3.2.0"
"@toeverything/theme": "^1.1.23",
"lit": "^3.2.0",
"rxjs": "^7.8.2",
"zod": "^3.25.76"
},
"exports": {
".": "./src/index.ts",
@@ -34,5 +42,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.27.0"
"version": "0.26.3"
}
@@ -43,11 +43,6 @@ export class EdgelessTextBlockComponent extends GfxBlockComponent<EdgelessTextBl
font-weight: var(--edgeless-text-font-weight);
text-align: var(--edgeless-text-text-align);
}
.edgeless-text-block-container .locked-content a[href] {
pointer-events: auto;
cursor: pointer;
}
`;
private readonly _resizeObserver = new ResizeObserver(() => {
@@ -309,7 +304,6 @@ export class EdgelessTextBlockComponent extends GfxBlockComponent<EdgelessTextBl
style=${styleMap(containerStyle)}
>
<div
class=${!editing && this.model.isLocked() ? 'locked-content' : ''}
style=${styleMap({
pointerEvents: editing ? 'auto' : 'none',
userSelect: editing ? 'auto' : 'none',
@@ -8,12 +8,14 @@
"include": ["./src"],
"references": [
{ "path": "../surface" },
{ "path": "../../components" },
{ "path": "../../ext-loader" },
{ "path": "../../gfx/text" },
{ "path": "../../inlines/preset" },
{ "path": "../../model" },
{ "path": "../../rich-text" },
{ "path": "../../shared" },
{ "path": "../../widgets/edgeless-toolbar" },
{ "path": "../../../framework/global" },
{ "path": "../../../framework/std" },
{ "path": "../../../framework/store" }
@@ -23,12 +23,19 @@
"@blocksuite/icons": "^2.2.17",
"@blocksuite/std": "workspace:*",
"@blocksuite/store": "workspace:*",
"@floating-ui/dom": "^1.6.13",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.23",
"@types/lodash-es": "^4.17.12",
"lit": "^3.2.0",
"lodash-es": "^4.17.23",
"rxjs": "^7.8.2",
"yjs": "^13.6.27"
"yjs": "^13.6.27",
"zod": "^3.25.76"
},
"devDependencies": {
"vitest": "^3.2.4"
},
"exports": {
".": "./src/index.ts",
@@ -41,5 +48,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.27.0"
"version": "0.26.3"
}
@@ -25,7 +25,7 @@ export function renderLinkedDocInCard(
return;
}
// oxlint-disable-next-line sonarjs/no-collapsible-if
// eslint-disable-next-line sonarjs/no-collapsible-if
if ('bannerContainer' in card) {
if (card.editorMode === 'page') {
renderPageAsBanner(card).catch(e => {
@@ -247,7 +247,7 @@ const conversionsActionGroup = {
@toggle=${onToggle}
.actions=${actions}
.context=${ctx}
.viewTypeSignal=${viewType$}
.viewType$=${viewType$}
></affine-view-dropdown-menu>`
)}`;
},
@@ -299,7 +299,7 @@ const builtinToolbarConfig = {
@toggle=${onToggle}
.actions=${actions}
.context=${ctx}
.styleSignal=${model.props.style$}
.style$=${model.props.style$}
></affine-card-style-dropdown-menu>`
)}`;
},
@@ -423,7 +423,7 @@ const builtinSurfaceToolbarConfig = {
@toggle=${onToggle}
.actions=${actions}
.context=${ctx}
.styleSignal=${style$}
.style$=${style$}
></affine-card-style-dropdown-menu>`
)}`;
},
@@ -479,7 +479,7 @@ const builtinSurfaceToolbarConfig = {
@select=${onSelect}
@toggle=${onToggle}
.format=${format}
.sizeSignal=${scale$}
.size$=${scale$}
></affine-size-dropdown-menu>`
)}`;
},
@@ -199,7 +199,7 @@ const conversionsActionGroup = {
@toggle=${onToggle}
.actions=${actions}
.context=${ctx}
.viewTypeSignal=${viewType$}
.viewType$=${viewType$}
></affine-view-dropdown-menu>`
)}`;
},
@@ -466,7 +466,7 @@ const builtinSurfaceToolbarConfig = {
@select=${onSelect}
@toggle=${onToggle}
.format=${format}
.sizeSignal=${scale$}
.size$=${scale$}
></affine-size-dropdown-menu>`
)}`;
},
+13 -2
View File
@@ -13,7 +13,10 @@
"@blocksuite/affine-block-surface": "workspace:*",
"@blocksuite/affine-components": "workspace:*",
"@blocksuite/affine-ext-loader": "workspace:*",
"@blocksuite/affine-gfx-pointer": "workspace:*",
"@blocksuite/affine-inline-reference": "workspace:*",
"@blocksuite/affine-model": "workspace:*",
"@blocksuite/affine-rich-text": "workspace:*",
"@blocksuite/affine-shared": "workspace:*",
"@blocksuite/affine-widget-slash-menu": "workspace:*",
"@blocksuite/global": "workspace:*",
@@ -21,10 +24,18 @@
"@blocksuite/std": "workspace:*",
"@blocksuite/store": "workspace:*",
"@floating-ui/dom": "^1.6.13",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.23",
"@types/lodash-es": "^4.17.12",
"lit": "^3.2.0",
"yjs": "^13.6.27"
"lodash-es": "^4.17.23",
"rxjs": "^7.8.2",
"yjs": "^13.6.27",
"zod": "^3.25.76"
},
"devDependencies": {
"vitest": "^3.2.4"
},
"exports": {
".": "./src/index.ts",
@@ -37,5 +48,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.27.0"
"version": "0.26.3"
}
@@ -281,7 +281,7 @@ function createBuiltinToolbarConfigForExternal(
@toggle=${onToggle}
.actions=${actions}
.context=${ctx}
.viewTypeSignal=${viewType$}
.viewType$=${viewType$}
></affine-view-dropdown-menu>`
)}`;
},
@@ -329,7 +329,7 @@ function createBuiltinToolbarConfigForExternal(
@toggle=${onToggle}
.actions=${actions}
.context=${ctx}
.styleSignal=${model.props.style$}
.style$=${model.props.style$}
></affine-card-style-dropdown-menu>`
)}`;
},
@@ -514,7 +514,7 @@ const createBuiltinSurfaceToolbarConfigForExternal = (
@toggle=${onToggle}
.actions=${actions}
.context=${ctx}
.viewTypeSignal=${viewType$}
.viewType$=${viewType$}
></affine-view-dropdown-menu>`
)}`;
},
@@ -579,7 +579,7 @@ const createBuiltinSurfaceToolbarConfigForExternal = (
@toggle=${onToggle}
.actions=${actions}
.context=${ctx}
.styleSignal=${style$}
.style$=${style$}
></affine-card-style-dropdown-menu>`
)}`;
},
@@ -646,7 +646,7 @@ const createBuiltinSurfaceToolbarConfigForExternal = (
@select=${onSelect}
@toggle=${onToggle}
.format=${format}
.sizeSignal=${scale$}
.size$=${scale$}
></affine-size-dropdown-menu>`
)}`;
},
@@ -117,7 +117,7 @@ export class EmbedGithubBlockComponent extends EmbedBlockComponent<
override renderBlock() {
const {
title,
title = 'GitHub',
githubType,
status,
statusReason,
@@ -139,7 +139,7 @@ export class EmbedGithubBlockComponent extends EmbedBlockComponent<
? getGithubStatusIcon(githubType, status, statusReason)
: nothing;
const statusText = loading ? '' : status;
const titleText = loading ? 'Loading...' : title || 'GitHub';
const titleText = loading ? 'Loading...' : title;
const descriptionText = loading ? '' : description;
const bannerImage =
!loading && image
@@ -35,7 +35,7 @@ const extractBvid = (url: string) => {
const buildBiliPlayerEmbedUrl = (url: string) => {
// If the user pasted the embed URL directly, keep it
if (isValidBiliPlayerUrl(url)) {
if (validateEmbedIframeUrl(url, biliPlayerValidationOptions)) {
return url;
}
const avid = extractAvid(url);
@@ -57,31 +57,13 @@ const buildBiliPlayerEmbedUrl = (url: string) => {
return undefined;
};
function isValidBiliPlayerUrl(url: string) {
try {
if (!validateEmbedIframeUrl(url, biliPlayerValidationOptions)) {
return false;
}
const parsedUrl = new URL(url);
return (
parsedUrl.pathname === '/player.html' &&
(!!parsedUrl.searchParams.get('aid') ||
!!parsedUrl.searchParams.get('bvid'))
);
} catch {
return false;
}
}
export const bilibiliConfig = {
const bilibiliConfig = {
name: 'bilibili',
match: (url: string) =>
isValidBiliPlayerUrl(url) ||
(validateEmbedIframeUrl(url, bilibiliValidationOptions) &&
(!!extractAvid(url) || !!extractBvid(url))),
validateEmbedIframeUrl(url, bilibiliValidationOptions) &&
(!!extractAvid(url) || !!extractBvid(url)),
buildOEmbedUrl: buildBiliPlayerEmbedUrl,
useOEmbedUrlDirectly: true,
validateIframeUrl: (iframeUrl: string) => isValidBiliPlayerUrl(iframeUrl),
options: {
widthInSurface: BILIBILI_DEFAULT_WIDTH_IN_SURFACE,
heightInSurface: BILIBILI_DEFAULT_HEIGHT_IN_SURFACE,
@@ -15,7 +15,7 @@ const excalidrawUrlValidationOptions: EmbedIframeUrlValidationOptions = {
hostnames: ['excalidraw.com'],
};
export const excalidrawConfig = {
const excalidrawConfig = {
name: 'excalidraw',
match: (url: string) =>
validateEmbedIframeUrl(url, excalidrawUrlValidationOptions),
@@ -27,8 +27,6 @@ export const excalidrawConfig = {
return url;
},
useOEmbedUrlDirectly: true,
validateIframeUrl: (iframeUrl: string) =>
validateEmbedIframeUrl(iframeUrl, excalidrawUrlValidationOptions),
options: {
widthInSurface: EXCALIDRAW_DEFAULT_WIDTH_IN_SURFACE,
heightInSurface: EXCALIDRAW_DEFAULT_HEIGHT_IN_SURFACE,
@@ -1,10 +1,5 @@
import { EmbedIframeConfigExtension } from '@blocksuite/affine-shared/services';
import {
type EmbedIframeUrlValidationOptions,
validateEmbedIframeUrl,
} from '../../utils';
const GENERIC_DEFAULT_WIDTH_IN_SURFACE = 800;
const GENERIC_DEFAULT_HEIGHT_IN_SURFACE = 600;
const GENERIC_DEFAULT_WIDTH_PERCENT = 100;
@@ -22,11 +17,6 @@ const AFFINE_DOMAINS = [
'apple.getaffineapp.com', // Cloud domain for Apple app
];
const genericUrlValidationOptions: EmbedIframeUrlValidationOptions = {
protocols: ['https:'],
hostnames: [],
};
/**
* Validates if a URL is suitable for generic iframe embedding
* Allows HTTPS URLs but excludes AFFiNE domains
@@ -37,12 +27,8 @@ function isValidGenericEmbedUrl(url: string): boolean {
try {
const parsedUrl = new URL(url);
if (
!validateEmbedIframeUrl(url, {
...genericUrlValidationOptions,
hostnames: [parsedUrl.hostname],
})
) {
// Only allow HTTPS for security
if (parsedUrl.protocol !== 'https:') {
return false;
}
@@ -63,7 +49,7 @@ function isValidGenericEmbedUrl(url: string): boolean {
}
}
export const genericConfig = {
const genericConfig = {
name: 'generic',
match: (url: string) => isValidGenericEmbedUrl(url),
buildOEmbedUrl: (url: string) => {
@@ -73,7 +59,6 @@ export const genericConfig = {
return url;
},
useOEmbedUrlDirectly: true,
validateIframeUrl: (iframeUrl: string) => isValidGenericEmbedUrl(iframeUrl),
options: {
widthInSurface: GENERIC_DEFAULT_WIDTH_IN_SURFACE,
heightInSurface: GENERIC_DEFAULT_HEIGHT_IN_SURFACE,
@@ -57,7 +57,7 @@ function isValidGoogleDocsUrl(url: string, strictMode = true): boolean {
}
}
export const googleDocsConfig = {
const googleDocsConfig = {
name: 'google-docs',
match: (url: string) => isValidGoogleDocsUrl(url),
buildOEmbedUrl: (url: string) => {
@@ -67,7 +67,6 @@ export const googleDocsConfig = {
return url;
},
useOEmbedUrlDirectly: true,
validateIframeUrl: (iframeUrl: string) => isValidGoogleDocsUrl(iframeUrl),
options: {
widthInSurface: GOOGLE_DOCS_DEFAULT_WIDTH_IN_SURFACE,
heightInSurface: GOOGLE_DOCS_DEFAULT_HEIGHT_IN_SURFACE,
@@ -113,29 +113,6 @@ function isValidGoogleDriveUrl(url: string, strictMode = true): boolean {
}
}
function isValidGoogleDriveIframeUrl(url: string): boolean {
try {
if (!validateEmbedIframeUrl(url, googleDriveUrlValidationOptions)) {
return false;
}
const parsedUrl = new URL(url);
const pathSegments = parsedUrl.pathname.split('/').filter(Boolean);
if (isValidGoogleDriveFileUrl(parsedUrl)) {
return pathSegments[3] === 'preview';
}
return (
parsedUrl.pathname === '/embeddedfolderview' &&
!!parsedUrl.searchParams.get('id')
);
} catch (e) {
console.warn('Invalid Google Drive iframe URL:', e);
return false;
}
}
/**
* Build embed URL for Google Drive files
* @param fileId File ID
@@ -194,7 +171,7 @@ function buildGoogleDriveEmbedUrl(url: string): string | undefined {
}
}
export const googleDriveConfig = {
const googleDriveConfig = {
name: 'google-drive',
match: (url: string) => isValidGoogleDriveUrl(url),
buildOEmbedUrl: (url: string) => {
@@ -206,8 +183,6 @@ export const googleDriveConfig = {
return buildGoogleDriveEmbedUrl(url);
},
useOEmbedUrlDirectly: true,
validateIframeUrl: (iframeUrl: string) =>
isValidGoogleDriveIframeUrl(iframeUrl),
options: {
widthInSurface: GOOGLE_DRIVE_DEFAULT_WIDTH_IN_SURFACE,
heightInSurface: GOOGLE_DRIVE_DEFAULT_HEIGHT_IN_SURFACE,
@@ -18,7 +18,7 @@ const miroUrlValidationOptions: EmbedIframeUrlValidationOptions = {
hostnames: ['miro.com'],
};
export const miroConfig = {
const miroConfig = {
name: 'miro',
match: (url: string) => validateEmbedIframeUrl(url, miroUrlValidationOptions),
buildOEmbedUrl: (url: string) => {
@@ -31,12 +31,6 @@ export const miroConfig = {
return oEmbedUrl;
},
useOEmbedUrlDirectly: false,
validateIframeUrl: (iframeUrl: string) => {
if (!validateEmbedIframeUrl(iframeUrl, miroUrlValidationOptions)) {
return false;
}
return new URL(iframeUrl).pathname.startsWith('/app/live-embed/');
},
options: {
widthInSurface: MIRO_DEFAULT_WIDTH_IN_SURFACE,
heightInSurface: MIRO_DEFAULT_HEIGHT_IN_SURFACE,
@@ -18,12 +18,7 @@ const spotifyUrlValidationOptions: EmbedIframeUrlValidationOptions = {
hostnames: ['open.spotify.com', 'spotify.link'],
};
const spotifyIframeUrlValidationOptions: EmbedIframeUrlValidationOptions = {
protocols: ['https:'],
hostnames: ['open.spotify.com'],
};
export const spotifyConfig = {
const spotifyConfig = {
name: 'spotify',
match: (url: string) =>
validateEmbedIframeUrl(url, spotifyUrlValidationOptions),
@@ -37,13 +32,6 @@ export const spotifyConfig = {
return oEmbedUrl;
},
useOEmbedUrlDirectly: false,
validateIframeUrl: (iframeUrl: string) => {
if (!validateEmbedIframeUrl(iframeUrl, spotifyIframeUrlValidationOptions)) {
return false;
}
const parsedUrl = new URL(iframeUrl);
return parsedUrl.pathname.split('/').find(Boolean) === 'embed';
},
options: {
widthInSurface: SPOTIFY_DEFAULT_WIDTH_IN_SURFACE,
heightInSurface: SPOTIFY_DEFAULT_HEIGHT_IN_SURFACE,
@@ -202,7 +202,7 @@ export const builtinToolbarConfig = {
.actions=${actions}
.context=${ctx}
.toggle=${toggle}
.viewTypeSignal=${signal(actions[2].label)}
.viewType$=${signal(actions[2].label)}
></affine-view-dropdown-menu>`
)}`;
},
@@ -408,7 +408,7 @@ export const builtinSurfaceToolbarConfig = {
@toggle=${onToggle}
.actions=${actions}
.context=${ctx}
.viewTypeSignal=${signal(actions[1].label)}
.viewType$=${signal(actions[1].label)}
></affine-view-dropdown-menu>`
)}`;
},
@@ -465,7 +465,7 @@ export const builtinSurfaceToolbarConfig = {
@select=${onSelect}
@toggle=${onToggle}
.format=${format}
.sizeSignal=${scale$}
.size$=${scale$}
></affine-size-dropdown-menu>`
)}`;
},
@@ -141,7 +141,7 @@ export class EmbedIframeBlockComponent extends CaptionedBlockComponent<EmbedIfra
});
return;
}
window.open(link, '_blank', 'noopener,noreferrer');
window.open(link, '_blank');
};
refreshData = async () => {
@@ -183,12 +183,6 @@ export class EmbedIframeBlockComponent extends CaptionedBlockComponent<EmbedIfra
// update model
const iframeUrl = this._getIframeUrl(embedData) ?? currentIframeUrl;
if (!this._validateIframeUrl(url, iframeUrl)) {
throw new BlockSuiteError(
ErrorCode.ValueNotExists,
'Invalid embed iframe url'
);
}
this.store.updateBlock(this.model, {
iframeUrl,
title: embedData?.title || previewData?.title,
@@ -297,19 +291,6 @@ export class EmbedIframeBlockComponent extends CaptionedBlockComponent<EmbedIfra
}
};
private readonly _validateIframeUrl = (url: string, iframeUrl?: string) => {
if (!iframeUrl) {
return false;
}
const config = this.embedIframeService?.getConfig(url);
if (!config) {
return false;
}
return config.validateIframeUrl
? config.validateIframeUrl(iframeUrl, url)
: config.match(iframeUrl);
};
private readonly _handleDoubleClick = () => {
this.open();
};
@@ -348,16 +329,6 @@ export class EmbedIframeBlockComponent extends CaptionedBlockComponent<EmbedIfra
private readonly _renderIframe = () => {
const { iframeUrl } = this.model.props;
if (!iframeUrl || !this._isIframeUrlAllowed(iframeUrl)) {
return html`<embed-iframe-error-card
.error=${new Error('Invalid iframe URL')}
.model=${this.model}
.onRetry=${this._handleRetry}
.std=${this.std}
.inSurface=${this.inSurface}
.options=${this._statusCardOptions}
></embed-iframe-error-card>`;
}
const {
widthPercent,
heightInNote,
@@ -397,10 +368,6 @@ export class EmbedIframeBlockComponent extends CaptionedBlockComponent<EmbedIfra
: nothing}`;
};
private readonly _isIframeUrlAllowed = (iframeUrl: string) => {
return this._validateIframeUrl(this.model.props.url, iframeUrl);
};
private readonly _getSourceHost = () => {
const url = this.model.props.url ?? this.model.props.iframeUrl;
if (!url) return null;
@@ -470,12 +437,7 @@ export class EmbedIframeBlockComponent extends CaptionedBlockComponent<EmbedIfra
} else {
// update iframe options, to ensure the iframe is rendered with the correct options
this._updateIframeOptions(this.model.props.url);
this.status$.value = this._validateIframeUrl(
this.model.props.url,
this.model.props.iframeUrl
)
? 'success'
: 'error';
this.status$.value = 'success';
}
// refresh data when original url changes
@@ -9,25 +9,6 @@ export interface EmbedIframeUrlValidationOptions {
hostnames: string[]; // Allowed hostnames, e.g. ['docs.google.com']
}
function isLocalOrIpHostname(hostname: string): boolean {
const lower = hostname.toLowerCase();
if (
lower === 'localhost' ||
lower.endsWith('.localhost') ||
lower === '0.0.0.0' ||
lower === '::' ||
lower === '::1'
) {
return true;
}
if (/^\d{1,3}(\.\d{1,3}){3}$/.test(lower)) {
return true;
}
return lower.startsWith('[') && lower.endsWith(']');
}
/**
* Validate the url is allowed to embed in the iframe
* @param url URL to validate
@@ -42,15 +23,6 @@ export function validateEmbedIframeUrl(
const parsedUrl = new URL(url);
const { protocols, hostnames } = options;
if (
parsedUrl.username ||
parsedUrl.password ||
parsedUrl.port ||
isLocalOrIpHostname(parsedUrl.hostname)
) {
return false;
}
return (
protocols.includes(parsedUrl.protocol) &&
hostnames.includes(parsedUrl.hostname)
File diff suppressed because one or more lines are too long

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