Compare commits

..

2 Commits

Author SHA1 Message Date
L-Sun
ac76e5b949 chore: enable webview debugging for Android 2025-10-02 21:48:29 +08:00
L-Sun
0bc1005b96 fix(core): infinitied loop 2025-09-26 15:48:24 +08:00
1535 changed files with 25344 additions and 122712 deletions

View File

@@ -5,14 +5,7 @@ rustflags = ["-C", "target-feature=+crt-static"]
[target.'cfg(target_os = "linux")']
rustflags = ["-C", "link-args=-Wl,--warn-unresolved-symbols"]
[target.'cfg(target_os = "macos")']
rustflags = [
"-C",
"link-args=-Wl,-undefined,dynamic_lookup,-no_fixup_chains",
"-C",
"link-args=-all_load",
"-C",
"link-args=-weak_framework ScreenCaptureKit",
]
rustflags = ["-C", "link-args=-Wl,-undefined,dynamic_lookup,-no_fixup_chains", "-C", "link-args=-all_load", "-C", "link-args=-weak_framework ScreenCaptureKit"]
# https://sourceware.org/bugzilla/show_bug.cgi?id=21032
# https://sourceware.org/bugzilla/show_bug.cgi?id=21031
# https://github.com/rust-lang/rust/issues/134820

View File

@@ -6,6 +6,7 @@ yarn install
# Build Server Dependencies
yarn affine @affine/server-native build
yarn affine @affine/reader build
# Create database
yarn affine @affine/server prisma migrate reset -f

View File

@@ -2,8 +2,6 @@ version: '3.8'
services:
app:
security_opt:
- no-new-privileges:true
image: mcr.microsoft.com/devcontainers/base:bookworm
volumes:
- ../..:/workspaces:cached

View File

@@ -148,11 +148,6 @@
"description": "Whether allow new registrations.\n@default true",
"default": true
},
"allowSignupForOauth": {
"type": "boolean",
"description": "Whether allow new registrations via configured oauth.\n@default true",
"default": true
},
"requireEmailDomainVerification": {
"type": "boolean",
"description": "Whether require email domain record verification before accessing restricted resources.\n@default false",
@@ -195,11 +190,6 @@
"type": "object",
"description": "Configuration for mailer module",
"properties": {
"SMTP.name": {
"type": "string",
"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",
"description": "Host of the email server (e.g. smtp.gmail.com)\n@default \"\"\n@environment `MAILER_HOST`",
@@ -222,12 +212,12 @@
},
"SMTP.sender": {
"type": "string",
"description": "Sender of all the emails (e.g. \"AFFiNE Self Hosted <noreply@example.com>\")\n@default \"AFFiNE Self Hosted <noreply@example.com>\"\n@environment `MAILER_SENDER`",
"default": "AFFiNE Self Hosted <noreply@example.com>"
"description": "Sender of all the emails (e.g. \"AFFiNE Team <noreply@affine.pro>\")\n@default \"\"\n@environment `MAILER_SENDER`",
"default": ""
},
"SMTP.ignoreTLS": {
"type": "boolean",
"description": "Whether ignore email server's TLS certificate verification. Enable it for self-signed certificates.\n@default false\n@environment `MAILER_IGNORE_TLS`",
"description": "Whether ignore email server's TSL certification verification. Enable it for self-signed certificates.\n@default false\n@environment `MAILER_IGNORE_TLS`",
"default": false
},
"fallbackDomains": {
@@ -235,11 +225,6 @@
"description": "The emails from these domains are always sent using the fallback SMTP server.\n@default []",
"default": []
},
"fallbackSMTP.name": {
"type": "string",
"description": "Name of the fallback email server (e.g. your domain name)\n@default \"AFFiNE Server\"",
"default": "AFFiNE Server"
},
"fallbackSMTP.host": {
"type": "string",
"description": "Host of the email server (e.g. smtp.gmail.com)\n@default \"\"",
@@ -262,12 +247,12 @@
},
"fallbackSMTP.sender": {
"type": "string",
"description": "Sender of all the emails (e.g. \"AFFiNE Self Hosted <noreply@example.com>\")\n@default \"\"",
"description": "Sender of all the emails (e.g. \"AFFiNE Team <noreply@affine.pro>\")\n@default \"\"",
"default": ""
},
"fallbackSMTP.ignoreTLS": {
"type": "boolean",
"description": "Whether ignore email server's TLS certificate verification. Enable it for self-signed certificates.\n@default false",
"description": "Whether ignore email server's TSL certification verification. Enable it for self-signed certificates.\n@default false",
"default": false
}
}
@@ -337,42 +322,8 @@
},
"config": {
"type": "object",
"description": "The config for the S3 compatible storage provider.",
"description": "The config for the s3 compatible storage provider. directly passed to aws-sdk client.\n@link https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html",
"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."
},
"forcePathStyle": {
"type": "boolean",
"description": "Whether to use path-style bucket addressing."
},
"requestTimeoutMs": {
"type": "number",
"description": "Request timeout in milliseconds."
},
"minPartSize": {
"type": "number",
"description": "Minimum multipart part size in bytes."
},
"presign": {
"type": "object",
"description": "Presigned URL behavior configuration.",
"properties": {
"expiresInSeconds": {
"type": "number",
"description": "Expiration time in seconds for presigned URLs."
},
"signContentTypeForPut": {
"type": "boolean",
"description": "Whether to sign Content-Type for presigned PUT."
}
}
},
"credentials": {
"type": "object",
"description": "The credentials for the s3 compatible storage provider.",
@@ -382,9 +333,6 @@
},
"secretAccessKey": {
"type": "string"
},
"sessionToken": {
"type": "string"
}
}
}
@@ -406,42 +354,8 @@
},
"config": {
"type": "object",
"description": "The config for the S3 compatible storage provider.",
"description": "The config for the s3 compatible storage provider. directly passed to aws-sdk client.\n@link https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html",
"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."
},
"forcePathStyle": {
"type": "boolean",
"description": "Whether to use path-style bucket addressing."
},
"requestTimeoutMs": {
"type": "number",
"description": "Request timeout in milliseconds."
},
"minPartSize": {
"type": "number",
"description": "Minimum multipart part size in bytes."
},
"presign": {
"type": "object",
"description": "Presigned URL behavior configuration.",
"properties": {
"expiresInSeconds": {
"type": "number",
"description": "Expiration time in seconds for presigned URLs."
},
"signContentTypeForPut": {
"type": "boolean",
"description": "Whether to sign Content-Type for presigned PUT."
}
}
},
"credentials": {
"type": "object",
"description": "The credentials for the s3 compatible storage provider.",
@@ -451,9 +365,6 @@
},
"secretAccessKey": {
"type": "string"
},
"sessionToken": {
"type": "string"
}
}
},
@@ -471,7 +382,7 @@
},
"urlPrefix": {
"type": "string",
"description": "The custom domain URL prefix for the cloudflare r2 storage provider.\nWhen `enabled=true` and `urlPrefix` + `signKey` are provided, the server will:\n- Redirect GET requests to this custom domain with an HMAC token.\n- Return upload URLs under `/api/storage/*` for uploads.\nPresigned/upload proxy TTL is 1 hour.\nsee https://developers.cloudflare.com/waf/custom-rules/use-cases/configure-token-authentication/ to configure it.\nExample value: \"https://storage.example.com\"\nExample rule: is_timed_hmac_valid_v0(\"your_secret\", http.request.uri, 10800, http.request.timestamp.sec, 6)"
"description": "The presigned url prefix for the cloudflare r2 storage provider.\nsee https://developers.cloudflare.com/waf/custom-rules/use-cases/configure-token-authentication/ to configure it.\nExample value: \"https://storage.example.com\"\nExample rule: is_timed_hmac_valid_v0(\"your_secret\", http.request.uri, 10800, http.request.timestamp.sec, 6)"
},
"signKey": {
"type": "string",
@@ -532,42 +443,8 @@
},
"config": {
"type": "object",
"description": "The config for the S3 compatible storage provider.",
"description": "The config for the s3 compatible storage provider. directly passed to aws-sdk client.\n@link https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html",
"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."
},
"forcePathStyle": {
"type": "boolean",
"description": "Whether to use path-style bucket addressing."
},
"requestTimeoutMs": {
"type": "number",
"description": "Request timeout in milliseconds."
},
"minPartSize": {
"type": "number",
"description": "Minimum multipart part size in bytes."
},
"presign": {
"type": "object",
"description": "Presigned URL behavior configuration.",
"properties": {
"expiresInSeconds": {
"type": "number",
"description": "Expiration time in seconds for presigned URLs."
},
"signContentTypeForPut": {
"type": "boolean",
"description": "Whether to sign Content-Type for presigned PUT."
}
}
},
"credentials": {
"type": "object",
"description": "The credentials for the s3 compatible storage provider.",
@@ -577,9 +454,6 @@
},
"secretAccessKey": {
"type": "string"
},
"sessionToken": {
"type": "string"
}
}
}
@@ -601,42 +475,8 @@
},
"config": {
"type": "object",
"description": "The config for the S3 compatible storage provider.",
"description": "The config for the s3 compatible storage provider. directly passed to aws-sdk client.\n@link https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html",
"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."
},
"forcePathStyle": {
"type": "boolean",
"description": "Whether to use path-style bucket addressing."
},
"requestTimeoutMs": {
"type": "number",
"description": "Request timeout in milliseconds."
},
"minPartSize": {
"type": "number",
"description": "Minimum multipart part size in bytes."
},
"presign": {
"type": "object",
"description": "Presigned URL behavior configuration.",
"properties": {
"expiresInSeconds": {
"type": "number",
"description": "Expiration time in seconds for presigned URLs."
},
"signContentTypeForPut": {
"type": "boolean",
"description": "Whether to sign Content-Type for presigned PUT."
}
}
},
"credentials": {
"type": "object",
"description": "The credentials for the s3 compatible storage provider.",
@@ -646,9 +486,6 @@
},
"secretAccessKey": {
"type": "string"
},
"sessionToken": {
"type": "string"
}
}
},
@@ -666,7 +503,7 @@
},
"urlPrefix": {
"type": "string",
"description": "The custom domain URL prefix for the cloudflare r2 storage provider.\nWhen `enabled=true` and `urlPrefix` + `signKey` are provided, the server will:\n- Redirect GET requests to this custom domain with an HMAC token.\n- Return upload URLs under `/api/storage/*` for uploads.\nPresigned/upload proxy TTL is 1 hour.\nsee https://developers.cloudflare.com/waf/custom-rules/use-cases/configure-token-authentication/ to configure it.\nExample value: \"https://storage.example.com\"\nExample rule: is_timed_hmac_valid_v0(\"your_secret\", http.request.uri, 10800, http.request.timestamp.sec, 6)"
"description": "The presigned url prefix for the cloudflare r2 storage provider.\nsee https://developers.cloudflare.com/waf/custom-rules/use-cases/configure-token-authentication/ to configure it.\nExample value: \"https://storage.example.com\"\nExample rule: is_timed_hmac_valid_v0(\"your_secret\", http.request.uri, 10800, http.request.timestamp.sec, 6)"
},
"signKey": {
"type": "string",
@@ -743,11 +580,6 @@
"description": "Multiple hosts the server will accept requests from.\n@default []",
"default": []
},
"listenAddr": {
"type": "string",
"description": "The address to listen on (e.g., 0.0.0.0 for IPv4, :: for IPv6).\n@default \"0.0.0.0\"\n@environment `LISTEN_ADDR`",
"default": "0.0.0.0"
},
"port": {
"type": "number",
"description": "Which port the server will listen on.\n@default 3010\n@environment `AFFINE_SERVER_PORT`",
@@ -764,6 +596,11 @@
"type": "object",
"description": "Configuration for flags module",
"properties": {
"earlyAccessControl": {
"type": "boolean",
"description": "Only allow users with early access features to access the app\n@default false",
"default": false
},
"allowGuestDemoWorkspace": {
"type": "boolean",
"description": "Whether allow guest users to create demo workspaces.\n@default true",
@@ -782,45 +619,6 @@
}
}
},
"telemetry": {
"type": "object",
"description": "Configuration for telemetry module",
"properties": {
"allowedOrigin": {
"type": "array",
"description": "Allowed origins for telemetry collection.\n@default [\"localhost\",\"127.0.0.1\"]",
"default": [
"localhost",
"127.0.0.1"
]
},
"ga4.measurementId": {
"type": "string",
"description": "GA4 Measurement ID for Measurement Protocol.\n@default \"\"\n@environment `GA4_MEASUREMENT_ID`",
"default": ""
},
"ga4.apiSecret": {
"type": "string",
"description": "GA4 API secret for Measurement Protocol.\n@default \"\"\n@environment `GA4_API_SECRET`",
"default": ""
},
"dedupe.ttlHours": {
"type": "number",
"description": "Telemetry dedupe TTL in hours.\n@default 24",
"default": 24
},
"dedupe.maxEntries": {
"type": "number",
"description": "Telemetry dedupe max entries.\n@default 100000",
"default": 100000
},
"batch.maxEvents": {
"type": "number",
"description": "Max events per telemetry batch.\n@default 25",
"default": 25
}
}
},
"client": {
"type": "object",
"description": "Configuration for client module",
@@ -832,108 +630,8 @@
},
"versionControl.requiredVersion": {
"type": "string",
"description": "Allowed version range of the app that allowed to access the server. Requires 'client/versionControl.enabled' to be true to take effect.\n@default \">=0.25.0\"",
"default": ">=0.25.0"
}
}
},
"calendar": {
"type": "object",
"description": "Configuration for calendar module",
"properties": {
"google": {
"type": "object",
"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"
},
"clientId": {
"type": "string"
},
"clientSecret": {
"type": "string"
},
"externalWebhookUrl": {
"type": "string"
},
"webhookVerificationToken": {
"type": "string"
}
},
"default": {
"enabled": false,
"clientId": "",
"clientSecret": "",
"externalWebhookUrl": "",
"webhookVerificationToken": ""
}
},
"caldav": {
"type": "object",
"description": "CalDAV integration config\n@default {\"enabled\":false,\"allowCustomProvider\":false,\"providers\":[],\"allowInsecureHttp\":false,\"allowedHosts\":[],\"blockPrivateNetwork\":true,\"requestTimeoutMs\":10000,\"maxRedirects\":5}",
"properties": {
"enabled": {
"type": "boolean"
},
"allowCustomProvider": {
"type": "boolean"
},
"providers": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"label": {
"type": "string"
},
"serverUrl": {
"type": "string"
},
"authType": {
"type": "string"
},
"requiresAppPassword": {
"type": "boolean"
},
"docsUrl": {
"type": "string"
}
}
}
},
"allowInsecureHttp": {
"type": "boolean"
},
"allowedHosts": {
"type": "array",
"items": {
"type": "string"
}
},
"blockPrivateNetwork": {
"type": "boolean"
},
"requestTimeoutMs": {
"type": "number"
},
"maxRedirects": {
"type": "number"
}
},
"default": {
"enabled": false,
"allowCustomProvider": false,
"providers": [],
"allowInsecureHttp": false,
"allowedHosts": [],
"blockPrivateNetwork": true,
"requestTimeoutMs": 10000,
"maxRedirects": 5
}
"description": "Allowed version range of the app that allowed to access the server. Requires 'client/versionControl.enabled' to be true to take effect.\n@default \">=0.20.0\"",
"default": ">=0.20.0"
}
}
},
@@ -971,16 +669,16 @@
},
"scenarios": {
"type": "object",
"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\"}}",
"description": "Use custom models in scenarios and override default settings.\n@default {\"override_enabled\":false,\"scenarios\":{\"audio_transcribing\":\"gemini-2.5-flash\",\"chat\":\"claude-sonnet-4@20250514\",\"embedding\":\"gemini-embedding-001\",\"image\":\"gpt-image-1\",\"rerank\":\"gpt-4.1\",\"coding\":\"claude-sonnet-4@20250514\",\"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",
"chat": "claude-sonnet-4@20250514",
"embedding": "gemini-embedding-001",
"image": "gpt-image-1",
"rerank": "gpt-4.1",
"coding": "claude-sonnet-4-5@20250929",
"coding": "claude-sonnet-4@20250514",
"complex_text_generation": "gpt-4o-2024-08-06",
"quick_decision_making": "gpt-5-mini",
"quick_text_generation": "gemini-2.5-flash",
@@ -1155,42 +853,8 @@
},
"config": {
"type": "object",
"description": "The config for the S3 compatible storage provider.",
"description": "The config for the s3 compatible storage provider. directly passed to aws-sdk client.\n@link https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html",
"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."
},
"forcePathStyle": {
"type": "boolean",
"description": "Whether to use path-style bucket addressing."
},
"requestTimeoutMs": {
"type": "number",
"description": "Request timeout in milliseconds."
},
"minPartSize": {
"type": "number",
"description": "Minimum multipart part size in bytes."
},
"presign": {
"type": "object",
"description": "Presigned URL behavior configuration.",
"properties": {
"expiresInSeconds": {
"type": "number",
"description": "Expiration time in seconds for presigned URLs."
},
"signContentTypeForPut": {
"type": "boolean",
"description": "Whether to sign Content-Type for presigned PUT."
}
}
},
"credentials": {
"type": "object",
"description": "The credentials for the s3 compatible storage provider.",
@@ -1200,9 +864,6 @@
},
"secretAccessKey": {
"type": "string"
},
"sessionToken": {
"type": "string"
}
}
}
@@ -1224,42 +885,8 @@
},
"config": {
"type": "object",
"description": "The config for the S3 compatible storage provider.",
"description": "The config for the s3 compatible storage provider. directly passed to aws-sdk client.\n@link https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html",
"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."
},
"forcePathStyle": {
"type": "boolean",
"description": "Whether to use path-style bucket addressing."
},
"requestTimeoutMs": {
"type": "number",
"description": "Request timeout in milliseconds."
},
"minPartSize": {
"type": "number",
"description": "Minimum multipart part size in bytes."
},
"presign": {
"type": "object",
"description": "Presigned URL behavior configuration.",
"properties": {
"expiresInSeconds": {
"type": "number",
"description": "Expiration time in seconds for presigned URLs."
},
"signContentTypeForPut": {
"type": "boolean",
"description": "Whether to sign Content-Type for presigned PUT."
}
}
},
"credentials": {
"type": "object",
"description": "The credentials for the s3 compatible storage provider.",
@@ -1269,9 +896,6 @@
},
"secretAccessKey": {
"type": "string"
},
"sessionToken": {
"type": "string"
}
}
},
@@ -1289,7 +913,7 @@
},
"urlPrefix": {
"type": "string",
"description": "The custom domain URL prefix for the cloudflare r2 storage provider.\nWhen `enabled=true` and `urlPrefix` + `signKey` are provided, the server will:\n- Redirect GET requests to this custom domain with an HMAC token.\n- Return upload URLs under `/api/storage/*` for uploads.\nPresigned/upload proxy TTL is 1 hour.\nsee https://developers.cloudflare.com/waf/custom-rules/use-cases/configure-token-authentication/ to configure it.\nExample value: \"https://storage.example.com\"\nExample rule: is_timed_hmac_valid_v0(\"your_secret\", http.request.uri, 10800, http.request.timestamp.sec, 6)"
"description": "The presigned url prefix for the cloudflare r2 storage provider.\nsee https://developers.cloudflare.com/waf/custom-rules/use-cases/configure-token-authentication/ to configure it.\nExample value: \"https://storage.example.com\"\nExample rule: is_timed_hmac_valid_v0(\"your_secret\", http.request.uri, 10800, http.request.timestamp.sec, 6)"
},
"signKey": {
"type": "string",
@@ -1469,33 +1093,18 @@
},
"apiKey": {
"type": "string",
"description": "[Deprecated] Stripe API key. Use payment.stripe.apiKey instead.\n@default \"\"\n@environment `STRIPE_API_KEY`",
"description": "Stripe API key to enable payment service.\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`",
"description": "Stripe webhook key to enable payment service.\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",
"default": {
"apiKey": "",
"webhookKey": ""
}
},
"revenuecat": {
"type": "object",
"description": "RevenueCat integration configs\n@default {\"enabled\":false,\"apiKey\":\"\",\"projectId\":\"\",\"webhookAuth\":\"\",\"environment\":\"production\",\"productMap\":{}}\n@link https://www.revenuecat.com/docs/",
"default": {
"enabled": false,
"apiKey": "",
"projectId": "",
"webhookAuth": "",
"environment": "production",
"productMap": {}
}
"description": "Stripe sdk options\n@default {}\n@link https://docs.stripe.com/api",
"default": {}
}
}
},

View File

@@ -1,26 +0,0 @@
.git
.github/**/*.md
.gitignore
# Local dependency/build artifacts
/node_modules
/target
# Yarn v4 artifacts (not needed for image packaging)
/.yarn/cache
/.yarn/unplugged
/.yarn/install-state.gz
/.pnp.*
# Test artifacts
/test-results
/playwright-report
/coverage
/.coverage
# OS noise
.DS_Store
# Sourcemaps (keep server sourcemap for backend stacktraces)
**/*.map
!packages/backend/server/dist/main.js.map

View File

@@ -1,8 +1,6 @@
# Editor configuration, see http://editorconfig.org
root = true
[*.rs]
max_line_length = 120
[*]
charset = utf-8
indent_style = space

View File

@@ -74,11 +74,3 @@ body:
description: |
Links? References? Anything that will give us more context about the issue you are encountering!
Tip: You can attach images here
- type: checkboxes
attributes:
label: Is your content generated by AI?
description: >
(Required) Please confirm that the content you submit was not generated by AI or only minimally edited by AI.
If an administrator believes the post contains a large amount of AI-generated content, they may directly close the question.
options:
- label: I confirm that the content I submitted was **not** generated by AI / **merely contained minimal** AI edits.

View File

@@ -2,6 +2,7 @@ name: Feature Request
description: Suggest a feature or improvement
title: '[Feature Request]: '
labels: ['feat', 'story']
assignees: ['hwangdev97']
body:
- type: markdown
attributes:
@@ -34,11 +35,3 @@ body:
See the AFFiNE [Contributing Guide](https://github.com/toeverything/affine/blob/canary/CONTRIBUTING.md) to get started.
options:
- label: Yes I'd like to help by submitting a PR!
- type: checkboxes
attributes:
label: Is your content generated by AI?
description: >
(Required) Please confirm that the content you submit was not generated by AI or only minimally edited by AI.
If an administrator believes the post contains a large amount of AI-generated content, they may directly close the question.
options:
- label: I confirm that the content I submitted was **not** generated by AI / **merely contained minimal** AI edits.

View File

@@ -75,11 +75,7 @@ runs:
shell: bash
if: ${{ runner.os != 'Windows' && inputs.no-build != 'true' }}
run: |
if [[ "${{ inputs.target }}" == "x86_64-unknown-linux-gnu" ]]; then
yarn workspace ${{ inputs.package }} build --target ${{ inputs.target }}
else
yarn workspace ${{ inputs.package }} build --target ${{ inputs.target }} --use-napi-cross
fi
yarn workspace ${{ inputs.package }} build --target ${{ inputs.target }} --use-napi-cross
env:
DEBUG: 'napi:*'

View File

@@ -29,26 +29,43 @@ const isInternal = buildType === 'internal';
const replicaConfig = {
stable: {
front: Number(process.env.PRODUCTION_FRONT_REPLICA) || 2,
web: 2,
graphql: Number(process.env.PRODUCTION_GRAPHQL_REPLICA) || 2,
sync: Number(process.env.PRODUCTION_SYNC_REPLICA) || 2,
renderer: Number(process.env.PRODUCTION_RENDERER_REPLICA) || 2,
doc: Number(process.env.PRODUCTION_DOC_REPLICA) || 2,
},
beta: {
front: Number(process.env.BETA_FRONT_REPLICA) || 1,
web: 1,
graphql: Number(process.env.BETA_GRAPHQL_REPLICA) || 1,
sync: Number(process.env.BETA_SYNC_REPLICA) || 1,
renderer: Number(process.env.BETA_RENDERER_REPLICA) || 1,
doc: Number(process.env.BETA_DOC_REPLICA) || 1,
},
canary: { front: 1, graphql: 1, doc: 1 },
canary: {
web: 1,
graphql: 1,
sync: 1,
renderer: 1,
doc: 1,
},
};
const cpuConfig = {
beta: { front: '1', graphql: '1', doc: '1' },
canary: { front: '500m', graphql: '1', doc: '500m' },
};
const memoryConfig = {
beta: { front: '1Gi', graphql: '1Gi', doc: '1Gi' },
canary: { front: '512Mi', graphql: '512Mi', doc: '512Mi' },
beta: {
web: '300m',
graphql: '1',
sync: '1',
doc: '1',
renderer: '300m',
},
canary: {
web: '300m',
graphql: '1',
sync: '1',
doc: '1',
renderer: '300m',
},
};
const createHelmCommand = ({ isDryRun }) => {
@@ -73,16 +90,16 @@ const createHelmCommand = ({ isDryRun }) => {
`--set-string global.indexer.apiKey="${AFFINE_INDEXER_SEARCH_API_KEY}"`,
];
const serviceAnnotations = [
`--set-json front.serviceAccount.annotations="{ \\"iam.gke.io/gcp-service-account\\": \\"${APP_IAM_ACCOUNT}\\" }"`,
`--set-json web.serviceAccount.annotations="{ \\"iam.gke.io/gcp-service-account\\": \\"${APP_IAM_ACCOUNT}\\" }"`,
`--set-json graphql.serviceAccount.annotations="{ \\"iam.gke.io/gcp-service-account\\": \\"${APP_IAM_ACCOUNT}\\" }"`,
`--set-json sync.serviceAccount.annotations="{ \\"iam.gke.io/gcp-service-account\\": \\"${APP_IAM_ACCOUNT}\\" }"`,
`--set-json doc.serviceAccount.annotations="{ \\"iam.gke.io/gcp-service-account\\": \\"${APP_IAM_ACCOUNT}\\" }"`,
].concat(
isProduction || isBeta || isInternal
? [
`--set-json front.services.web.annotations="{ \\"cloud.google.com/neg\\": \\"{\\\\\\"ingress\\\\\\": true}\\" }"`,
`--set-json front.services.sync.annotations="{ \\"cloud.google.com/neg\\": \\"{\\\\\\"ingress\\\\\\": true}\\" }"`,
`--set-json front.services.renderer.annotations="{ \\"cloud.google.com/neg\\": \\"{\\\\\\"ingress\\\\\\": true}\\" }"`,
`--set-json web.service.annotations="{ \\"cloud.google.com/neg\\": \\"{\\\\\\"ingress\\\\\\": true}\\" }"`,
`--set-json graphql.service.annotations="{ \\"cloud.google.com/neg\\": \\"{\\\\\\"ingress\\\\\\": true}\\" }"`,
`--set-json sync.service.annotations="{ \\"cloud.google.com/neg\\": \\"{\\\\\\"ingress\\\\\\": true}\\" }"`,
`--set-json cloud-sql-proxy.serviceAccount.annotations="{ \\"iam.gke.io/gcp-service-account\\": \\"${CLOUD_SQL_IAM_ACCOUNT}\\" }"`,
`--set-json cloud-sql-proxy.nodeSelector="{ \\"iam.gke.io/gke-metadata-server-enabled\\": \\"true\\" }"`,
]
@@ -90,22 +107,14 @@ const createHelmCommand = ({ isDryRun }) => {
);
const cpu = cpuConfig[buildType];
const memory = memoryConfig[buildType];
let resources = [];
if (cpu) {
resources = resources.concat([
`--set front.resources.requests.cpu="${cpu.front}"`,
`--set graphql.resources.requests.cpu="${cpu.graphql}"`,
`--set doc.resources.requests.cpu="${cpu.doc}"`,
]);
}
if (memory) {
resources = resources.concat([
`--set front.resources.requests.memory="${memory.front}"`,
`--set graphql.resources.requests.memory="${memory.graphql}"`,
`--set doc.resources.requests.memory="${memory.doc}"`,
]);
}
const resources = cpu
? [
`--set web.resources.requests.cpu="${cpu.web}"`,
`--set graphql.resources.requests.cpu="${cpu.graphql}"`,
`--set sync.resources.requests.cpu="${cpu.sync}"`,
`--set doc.resources.requests.cpu="${cpu.doc}"`,
]
: [];
const replica = replicaConfig[buildType] || replicaConfig.canary;
@@ -121,7 +130,6 @@ const createHelmCommand = ({ isDryRun }) => {
.split(',')
.map(host => host.trim())
.filter(host => host);
const primaryHost = hosts[0] || '0.0.0.0';
const deployCommand = [
`helm upgrade --install affine .github/helm/affine`,
`--namespace ${namespace}`,
@@ -136,14 +144,18 @@ const createHelmCommand = ({ isDryRun }) => {
`--set-string global.version="${APP_VERSION}"`,
...redisAndPostgres,
...indexerOptions,
`--set front.replicaCount=${replica.front}`,
`--set-string front.image.tag="${imageTag}"`,
`--set-string front.app.host="${primaryHost}"`,
`--set web.replicaCount=${replica.web}`,
`--set-string web.image.tag="${imageTag}"`,
`--set graphql.replicaCount=${replica.graphql}`,
`--set-string graphql.image.tag="${imageTag}"`,
`--set-string graphql.app.host="${primaryHost}"`,
`--set graphql.app.host=${hosts[0]}`,
`--set sync.replicaCount=${replica.sync}`,
`--set-string sync.image.tag="${imageTag}"`,
`--set-string renderer.image.tag="${imageTag}"`,
`--set renderer.app.host=${hosts[0]}`,
`--set renderer.replicaCount=${replica.renderer}`,
`--set-string doc.image.tag="${imageTag}"`,
`--set-string doc.app.host="${primaryHost}"`,
`--set doc.app.host=${hosts[0]}`,
`--set doc.replicaCount=${replica.doc}`,
...serviceAnnotations,
...resources,

View File

@@ -4,6 +4,11 @@ description: 'Prepare Server Test Environment'
runs:
using: 'composite'
steps:
- name: Bundle @affine/reader
shell: bash
run: |
yarn affine @affine/reader build
- name: Initialize database
shell: bash
run: |

13
.github/deployment/front/Dockerfile vendored Normal file
View File

@@ -0,0 +1,13 @@
FROM openresty/openresty:1.27.1.1-0-buster
WORKDIR /app
COPY ./packages/frontend/apps/web/dist ./dist
COPY ./packages/frontend/admin/dist ./admin
COPY ./packages/frontend/apps/mobile/dist ./mobile
COPY ./.github/deployment/front/nginx.conf /usr/local/openresty/nginx/conf/nginx.conf
COPY ./.github/deployment/front/affine.nginx.conf /etc/nginx/conf.d/affine.nginx.conf
RUN mkdir -p /var/log/nginx && \
rm /etc/nginx/conf.d/default.conf
EXPOSE 8080
CMD ["/usr/local/openresty/bin/openresty", "-g", "daemon off;"]

View File

@@ -0,0 +1,42 @@
server {
listen 8080;
location /admin {
root /app/;
index index.html;
try_files $uri/index.html $uri/ $uri /admin/index.html;
}
set $app_root_path /app/dist/;
set $mobile_root /app/dist/;
set_by_lua $affine_env 'return os.getenv("AFFINE_ENV")';
if ($affine_env = "dev") {
set $mobile_root /app/mobile/;
}
# https://gist.github.com/mariusom/6683dc52b1cad1a1f372e908bdb209d0
if ($http_user_agent ~* "(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino") {
set $app_root_path $mobile_root;
}
if ($http_user_agent ~* "^(1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-)") {
set $app_root_path $mobile_root;
}
location ~ ^/(_plugin|assets|imgs|js|plugins|static)/ {
root $app_root_path;
try_files $uri $uri/ =404;
}
location / {
root $app_root_path;
index index.html;
try_files $uri $uri/ /index.html;
add_header Cache-Control "private, no-cache, no-store, max-age=0, must-revalidate";
}
error_page 404 /404.html;
location = /404.html {
internal;
}
}

15
.github/deployment/front/nginx.conf vendored Normal file
View File

@@ -0,0 +1,15 @@
worker_processes 4;
error_log /var/log/nginx/error.log warn;
pcre_jit on;
env AFFINE_ENV;
events {
worker_connections 1024;
}
http {
include mime.types;
log_format main '$remote_addr [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
include /etc/nginx/conf.d/*.conf;
}

View File

@@ -1,28 +1,11 @@
# syntax=docker/dockerfile:1.7
FROM node:22-bookworm-slim AS assets
WORKDIR /app
FROM node:22-bookworm-slim
COPY ./packages/backend/server /app
COPY ./packages/frontend/apps/web/dist /app/static
COPY ./packages/frontend/admin/dist /app/static/admin
COPY ./packages/frontend/apps/mobile/dist /app/static/mobile
# Keep server sourcemap for stacktraces, but don't ship frontend/node_modules sourcemaps.
ARG TARGETARCH
ARG TARGETVARIANT
# Needed for Prisma engine resolution (and potential engine download during cleanup).
RUN apt-get update && \
apt-get install -y --no-install-recommends openssl ca-certificates && \
rm -rf /var/lib/apt/lists/*
RUN AFFINE_DOCKER_CLEAN=1 TARGETARCH="${TARGETARCH}" TARGETVARIANT="${TARGETVARIANT}" node ./scripts/docker-clean.mjs
FROM node:22-bookworm-slim
WORKDIR /app
COPY --from=assets /app /app
RUN apt-get update && \
apt-get install -y --no-install-recommends openssl libjemalloc2 && \
rm -rf /var/lib/apt/lists/*
@@ -30,6 +13,4 @@ RUN apt-get update && \
# Enable jemalloc by preloading the library
ENV LD_PRELOAD=libjemalloc.so.2
EXPOSE 3010
CMD ["node", "./dist/main.js"]

View File

@@ -3,4 +3,4 @@ name: affine
description: AFFiNE cloud chart
type: application
version: 0.0.0
appVersion: "0.26.1"
appVersion: "0.22.4"

View File

@@ -3,7 +3,7 @@ name: doc
description: AFFiNE doc server
type: application
version: 0.0.0
appVersion: "0.26.1"
appVersion: "0.22.4"
dependencies:
- name: gcloud-sql-proxy
version: 0.0.0

View File

@@ -1,120 +0,0 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "front.fullname" . }}
labels:
{{- include "front.labels" . | nindent 4 }}
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
{{- include "front.selectorLabels" . | nindent 6 }}
template:
metadata:
{{- with .Values.podAnnotations }}
annotations:
{{- toYaml . | nindent 8 }}
{{- end }}
labels:
{{- include "front.selectorLabels" . | nindent 8 }}
spec:
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
serviceAccountName: {{ include "front.serviceAccountName" . }}
securityContext:
{{- toYaml .Values.podSecurityContext | nindent 8 }}
containers:
- name: {{ .Chart.Name }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
env:
- name: AFFINE_PRIVATE_KEY
valueFrom:
secretKeyRef:
name: "{{ .Values.global.secret.secretName }}"
key: key
- name: NODE_ENV
value: "{{ .Values.env }}"
- name: NODE_OPTIONS
value: "{{ .Values.nodeOptions }}"
- name: NO_COLOR
value: "1"
- name: DEPLOYMENT_TYPE
value: "{{ .Values.global.deployment.type }}"
- name: DEPLOYMENT_PLATFORM
value: "{{ .Values.global.deployment.platform }}"
- name: SERVER_FLAVOR
value: "front"
- name: AFFINE_ENV
value: "{{ .Release.Namespace }}"
- name: DATABASE_PASSWORD
valueFrom:
secretKeyRef:
name: pg-postgresql
key: postgres-password
- name: DATABASE_URL
value: postgres://{{ .Values.global.database.user }}:$(DATABASE_PASSWORD)@{{ .Values.global.database.host }}:{{ .Values.global.database.port }}/{{ .Values.global.database.name }}
- name: REDIS_SERVER_ENABLED
value: "true"
- name: REDIS_SERVER_HOST
value: "{{ .Values.global.redis.host }}"
- name: REDIS_SERVER_PORT
value: "{{ .Values.global.redis.port }}"
- name: REDIS_SERVER_USER
value: "{{ .Values.global.redis.username }}"
- name: REDIS_SERVER_PASSWORD
valueFrom:
secretKeyRef:
name: redis
key: redis-password
- name: REDIS_SERVER_DATABASE
value: "{{ .Values.global.redis.database }}"
- name: AFFINE_INDEXER_SEARCH_PROVIDER
value: "{{ .Values.global.indexer.provider }}"
- name: AFFINE_INDEXER_SEARCH_ENDPOINT
value: "{{ .Values.global.indexer.endpoint }}"
- name: AFFINE_INDEXER_SEARCH_API_KEY
valueFrom:
secretKeyRef:
name: indexer
key: indexer-apiKey
- name: AFFINE_SERVER_PORT
value: "{{ .Values.app.port }}"
- name: AFFINE_SERVER_SUB_PATH
value: "{{ .Values.app.path }}"
- name: AFFINE_SERVER_HOST
value: "{{ .Values.app.host }}"
- name: AFFINE_SERVER_HTTPS
value: "{{ .Values.app.https }}"
- name: DOC_SERVICE_ENDPOINT
value: "http://{{ .Values.global.docService.name }}:{{ .Values.global.docService.port }}"
ports:
- name: http
containerPort: {{ .Values.app.port }}
protocol: TCP
livenessProbe:
httpGet:
path: /info
port: http
initialDelaySeconds: {{ .Values.probe.initialDelaySeconds }}
readinessProbe:
httpGet:
path: /info
port: http
initialDelaySeconds: {{ .Values.probe.initialDelaySeconds }}
resources:
{{- toYaml .Values.resources | nindent 12 }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}

View File

@@ -1,19 +0,0 @@
apiVersion: v1
kind: Service
metadata:
name: {{ .Values.services.renderer.name }}
labels:
{{- include "front.labels" . | nindent 4 }}
{{- with .Values.services.renderer.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
type: {{ .Values.services.renderer.type }}
ports:
- port: {{ .Values.services.renderer.port }}
targetPort: http
protocol: TCP
name: http
selector:
{{- include "front.selectorLabels" . | nindent 4 }}

View File

@@ -1,19 +0,0 @@
apiVersion: v1
kind: Service
metadata:
name: {{ .Values.services.sync.name }}
labels:
{{- include "front.labels" . | nindent 4 }}
{{- with .Values.services.sync.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
type: {{ .Values.services.sync.type }}
ports:
- port: {{ .Values.services.sync.port }}
targetPort: http
protocol: TCP
name: http
selector:
{{- include "front.selectorLabels" . | nindent 4 }}

View File

@@ -1,19 +0,0 @@
apiVersion: v1
kind: Service
metadata:
name: {{ .Values.services.web.name }}
labels:
{{- include "front.labels" . | nindent 4 }}
{{- with .Values.services.web.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
type: {{ .Values.services.web.type }}
ports:
- port: {{ .Values.services.web.port }}
targetPort: http
protocol: TCP
name: http
selector:
{{- include "front.selectorLabels" . | nindent 4 }}

View File

@@ -1,60 +0,0 @@
replicaCount: 1
image:
repository: ghcr.io/toeverything/affine
pullPolicy: IfNotPresent
tag: ''
imagePullSecrets: []
nameOverride: ''
fullnameOverride: ''
# map to NODE_ENV environment variable
env: 'production'
nodeOptions: '--max-old-space-size=3072'
app:
# AFFINE_SERVER_PORT
port: 3010
# AFFINE_SERVER_SUB_PATH
path: ''
# AFFINE_SERVER_HOST
host: '0.0.0.0'
https: true
serviceAccount:
create: true
annotations: {}
name: 'affine-front'
podAnnotations: {}
podSecurityContext:
fsGroup: 2000
resources:
requests:
cpu: '2'
memory: 4Gi
probe:
initialDelaySeconds: 20
services:
sync:
name: affine-sync
type: ClusterIP
port: 3010
annotations:
cloud.google.com/backend-config: '{"default": "affine-api-backendconfig"}'
renderer:
name: affine-renderer
type: ClusterIP
port: 3000
annotations:
cloud.google.com/backend-config: '{"default": "affine-api-backendconfig"}'
web:
name: affine-web
type: ClusterIP
port: 8080
annotations: {}
nodeSelector: {}
tolerations: []
affinity: {}

View File

@@ -3,7 +3,7 @@ name: graphql
description: AFFiNE GraphQL server
type: application
version: 0.0.0
appVersion: "0.26.1"
appVersion: "0.22.4"
dependencies:
- name: gcloud-sql-proxy
version: 0.0.0

View File

@@ -0,0 +1,11 @@
apiVersion: v2
name: renderer
description: AFFiNE renderer server
type: application
version: 0.0.0
appVersion: "0.22.4"
dependencies:
- name: gcloud-sql-proxy
version: 0.0.0
repository: "file://../gcloud-sql-proxy"
condition: .global.database.gcloud.enabled

View File

@@ -1,15 +1,15 @@
1. Get the application URL by running these commands:
{{- if contains "NodePort" .Values.services.sync.type }}
export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ .Values.services.sync.name }})
{{- if contains "NodePort" .Values.service.type }}
export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "renderer.fullname" . }})
export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}")
echo http://$NODE_IP:$NODE_PORT
{{- else if contains "LoadBalancer" .Values.services.sync.type }}
{{- else if contains "LoadBalancer" .Values.service.type }}
NOTE: It may take a few minutes for the LoadBalancer IP to be available.
You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ .Values.services.sync.name }}'
export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ .Values.services.sync.name }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}")
echo http://$SERVICE_IP:{{ .Values.services.sync.port }}
{{- else if contains "ClusterIP" .Values.services.sync.type }}
export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "front.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}")
You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "renderer.fullname" . }}'
export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "renderer.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}")
echo http://$SERVICE_IP:{{ .Values.service.port }}
{{- else if contains "ClusterIP" .Values.service.type }}
export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "renderer.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}")
export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}")
echo "Visit http://127.0.0.1:8080 to use your application"
kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT

View File

@@ -0,0 +1,63 @@
{{/*
Expand the name of the chart.
*/}}
{{- define "renderer.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Create a default fully qualified app name.
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
If release name contains chart name it will be used as a full name.
*/}}
{{- define "renderer.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- if contains $name .Release.Name }}
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{- end }}
{{/*
Create chart name and version as used by the chart label.
*/}}
{{- define "renderer.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Common labels
*/}}
{{- define "renderer.labels" -}}
helm.sh/chart: {{ include "renderer.chart" . }}
{{ include "renderer.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
monitoring: enabled
{{- end }}
{{/*
Selector labels
*/}}
{{- define "renderer.selectorLabels" -}}
app.kubernetes.io/name: {{ include "renderer.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
{{/*
Create the name of the service account to use
*/}}
{{- define "renderer.serviceAccountName" -}}
{{- if .Values.serviceAccount.create }}
{{- default (include "renderer.fullname" .) .Values.serviceAccount.name }}
{{- else }}
{{- default "default" .Values.serviceAccount.name }}
{{- end }}
{{- end }}

View File

@@ -0,0 +1,118 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "renderer.fullname" . }}
labels:
{{- include "renderer.labels" . | nindent 4 }}
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
{{- include "renderer.selectorLabels" . | nindent 6 }}
template:
metadata:
{{- with .Values.podAnnotations }}
annotations:
{{- toYaml . | nindent 8 }}
{{- end }}
labels:
{{- include "renderer.selectorLabels" . | nindent 8 }}
spec:
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
serviceAccountName: {{ include "renderer.serviceAccountName" . }}
containers:
- name: {{ .Chart.Name }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
env:
- name: AFFINE_PRIVATE_KEY
valueFrom:
secretKeyRef:
name: "{{ .Values.global.secret.secretName }}"
key: key
- name: NODE_ENV
value: "{{ .Values.env }}"
- name: NODE_OPTIONS
value: "--max-old-space-size=2048"
- name: NO_COLOR
value: "1"
- name: DEPLOYMENT_TYPE
value: "{{ .Values.global.deployment.type }}"
- name: DEPLOYMENT_PLATFORM
value: "{{ .Values.global.deployment.platform }}"
- name: SERVER_FLAVOR
value: "renderer"
- name: AFFINE_ENV
value: "{{ .Release.Namespace }}"
- name: DATABASE_PASSWORD
valueFrom:
secretKeyRef:
name: pg-postgresql
key: postgres-password
- name: DATABASE_URL
value: postgres://{{ .Values.global.database.user }}:$(DATABASE_PASSWORD)@{{ .Values.global.database.host }}:{{ .Values.global.database.port }}/{{ .Values.global.database.name }}
- name: REDIS_SERVER_ENABLED
value: "true"
- name: REDIS_SERVER_HOST
value: "{{ .Values.global.redis.host }}"
- name: REDIS_SERVER_PORT
value: "{{ .Values.global.redis.port }}"
- name: REDIS_SERVER_USER
value: "{{ .Values.global.redis.username }}"
- name: REDIS_SERVER_PASSWORD
valueFrom:
secretKeyRef:
name: redis
key: redis-password
- name: REDIS_SERVER_DATABASE
value: "{{ .Values.global.redis.database }}"
- name: AFFINE_INDEXER_SEARCH_PROVIDER
value: "{{ .Values.global.indexer.provider }}"
- name: AFFINE_INDEXER_SEARCH_ENDPOINT
value: "{{ .Values.global.indexer.endpoint }}"
- name: AFFINE_INDEXER_SEARCH_API_KEY
valueFrom:
secretKeyRef:
name: indexer
key: indexer-apiKey
- name: AFFINE_SERVER_PORT
value: "{{ .Values.service.port }}"
- name: AFFINE_SERVER_SUB_PATH
value: "{{ .Values.app.path }}"
- name: AFFINE_SERVER_HOST
value: "{{ .Values.app.host }}"
- name: AFFINE_SERVER_HTTPS
value: "{{ .Values.app.https }}"
- name: DOC_SERVICE_ENDPOINT
value: "http://{{ .Values.global.docService.name }}:{{ .Values.global.docService.port }}"
ports:
- name: http
containerPort: {{ .Values.service.port }}
protocol: TCP
livenessProbe:
httpGet:
path: /info
port: http
initialDelaySeconds: {{ .Values.probe.initialDelaySeconds }}
readinessProbe:
httpGet:
path: /info
port: http
initialDelaySeconds: {{ .Values.probe.initialDelaySeconds }}
resources:
{{- toYaml .Values.resources | nindent 12 }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}

View File

@@ -0,0 +1,19 @@
apiVersion: v1
kind: Service
metadata:
name: {{ include "graphql.fullname" . }}
labels:
{{- include "graphql.labels" . | nindent 4 }}
{{- with .Values.service.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
type: {{ .Values.service.type }}
ports:
- port: {{ .Values.service.port }}
targetPort: http
protocol: TCP
name: http
selector:
{{- include "graphql.selectorLabels" . | nindent 4 }}

View File

@@ -0,0 +1,12 @@
{{- if .Values.serviceAccount.create -}}
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ include "graphql.serviceAccountName" . }}
labels:
{{- include "graphql.labels" . | nindent 4 }}
{{- with .Values.serviceAccount.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
{{- end }}

View File

@@ -0,0 +1,15 @@
apiVersion: v1
kind: Pod
metadata:
name: "{{ include "renderer.fullname" . }}-test-connection"
labels:
{{- include "renderer.labels" . | nindent 4 }}
annotations:
"helm.sh/hook": test
spec:
containers:
- name: wget
image: busybox
command: ['wget']
args: ['{{ include "renderer.fullname" . }}:{{ .Values.service.port }}']
restartPolicy: Never

View File

@@ -0,0 +1,38 @@
replicaCount: 1
image:
repository: ghcr.io/toeverything/affine
pullPolicy: IfNotPresent
tag: ''
imagePullSecrets: []
nameOverride: ''
fullnameOverride: ''
# map to NODE_ENV environment variable
env: 'production'
app:
# AFFINE_SERVER_SUB_PATH
path: ''
# AFFINE_SERVER_HOST
host: '0.0.0.0'
https: true
serviceAccount:
create: true
annotations: {}
name: 'affine-renderer'
podAnnotations: {}
podSecurityContext:
fsGroup: 2000
resources:
requests:
cpu: '1'
memory: 2Gi
probe:
initialDelaySeconds: 20
nodeSelector: {}
tolerations: []
affinity: {}

View File

@@ -0,0 +1,23 @@
# Patterns to ignore when building packages.
# This supports shell glob matching, relative path matching, and
# negation (prefixed with !). Only one pattern per line.
.DS_Store
# Common VCS dirs
.git/
.gitignore
.bzr/
.bzrignore
.hg/
.hgignore
.svn/
# Common backup files
*.swp
*.bak
*.tmp
*.orig
*~
# Various IDEs
.project
.idea/
*.tmproj
.vscode/

View File

@@ -1,9 +1,9 @@
apiVersion: v2
name: front
description: AFFiNE front server
name: sync
description: AFFiNE Sync Server
type: application
version: 0.0.0
appVersion: "0.26.1"
appVersion: "0.22.4"
dependencies:
- name: gcloud-sql-proxy
version: 0.0.0

View File

@@ -0,0 +1,16 @@
1. Get the application URL by running these commands:
{{- if contains "NodePort" .Values.service.type }}
export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "sync.fullname" . }})
export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}")
echo http://$NODE_IP:$NODE_PORT
{{- else if contains "LoadBalancer" .Values.service.type }}
NOTE: It may take a few minutes for the LoadBalancer IP to be available.
You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "sync.fullname" . }}'
export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "sync.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}")
echo http://$SERVICE_IP:{{ .Values.service.port }}
{{- else if contains "ClusterIP" .Values.service.type }}
export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "sync.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}")
export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}")
echo "Visit http://127.0.0.1:8080 to use your application"
kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT
{{- end }}

View File

@@ -1,7 +1,7 @@
{{/*
Expand the name of the chart.
*/}}
{{- define "front.name" -}}
{{- define "sync.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
@@ -10,7 +10,7 @@ Create a default fully qualified app name.
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
If release name contains chart name it will be used as a full name.
*/}}
{{- define "front.fullname" -}}
{{- define "sync.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
@@ -26,16 +26,16 @@ If release name contains chart name it will be used as a full name.
{{/*
Create chart name and version as used by the chart label.
*/}}
{{- define "front.chart" -}}
{{- define "sync.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Common labels
*/}}
{{- define "front.labels" -}}
helm.sh/chart: {{ include "front.chart" . }}
{{ include "front.selectorLabels" . }}
{{- define "sync.labels" -}}
helm.sh/chart: {{ include "sync.chart" . }}
{{ include "sync.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
@@ -46,17 +46,17 @@ monitoring: enabled
{{/*
Selector labels
*/}}
{{- define "front.selectorLabels" -}}
app.kubernetes.io/name: {{ include "front.name" . }}
{{- define "sync.selectorLabels" -}}
app.kubernetes.io/name: {{ include "sync.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
{{/*
Create the name of the service account to use
*/}}
{{- define "front.serviceAccountName" -}}
{{- define "sync.serviceAccountName" -}}
{{- if .Values.serviceAccount.create }}
{{- default (include "front.fullname" .) .Values.serviceAccount.name }}
{{- default (include "sync.fullname" .) .Values.serviceAccount.name }}
{{- else }}
{{- default "default" .Values.serviceAccount.name }}
{{- end }}

View File

@@ -0,0 +1,112 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "sync.fullname" . }}
labels:
{{- include "sync.labels" . | nindent 4 }}
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
{{- include "sync.selectorLabels" . | nindent 6 }}
template:
metadata:
{{- with .Values.podAnnotations }}
annotations:
{{- toYaml . | nindent 8 }}
{{- end }}
labels:
{{- include "sync.selectorLabels" . | nindent 8 }}
spec:
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
serviceAccountName: {{ include "sync.serviceAccountName" . }}
securityContext:
{{- toYaml .Values.podSecurityContext | nindent 8 }}
containers:
- name: {{ .Chart.Name }}
securityContext:
{{- toYaml .Values.securityContext | nindent 12 }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
env:
- name: AFFINE_PRIVATE_KEY
valueFrom:
secretKeyRef:
name: "{{ .Values.global.secret.secretName }}"
key: key
- name: NODE_ENV
value: "{{ .Values.env }}"
- name: NO_COLOR
value: "1"
- name: DEPLOYMENT_TYPE
value: "{{ .Values.global.deployment.type }}"
- name: DEPLOYMENT_PLATFORM
value: "{{ .Values.global.deployment.platform }}"
- name: SERVER_FLAVOR
value: "sync"
- name: AFFINE_ENV
value: "{{ .Release.Namespace }}"
- name: DATABASE_PASSWORD
valueFrom:
secretKeyRef:
name: pg-postgresql
key: postgres-password
- name: DATABASE_URL
value: postgres://{{ .Values.global.database.user }}:$(DATABASE_PASSWORD)@{{ .Values.global.database.host }}:{{ .Values.global.database.port }}/{{ .Values.global.database.name }}
- name: REDIS_SERVER_HOST
value: "{{ .Values.global.redis.host }}"
- name: REDIS_SERVER_PORT
value: "{{ .Values.global.redis.port }}"
- name: REDIS_SERVER_USER
value: "{{ .Values.global.redis.username }}"
- name: REDIS_SERVER_PASSWORD
valueFrom:
secretKeyRef:
name: redis
key: redis-password
- name: REDIS_SERVER_DATABASE
value: "{{ .Values.global.redis.database }}"
- name: AFFINE_INDEXER_SEARCH_PROVIDER
value: "{{ .Values.global.indexer.provider }}"
- name: AFFINE_INDEXER_SEARCH_ENDPOINT
value: "{{ .Values.global.indexer.endpoint }}"
- name: AFFINE_INDEXER_SEARCH_API_KEY
valueFrom:
secretKeyRef:
name: indexer
key: indexer-apiKey
- name: AFFINE_SERVER_PORT
value: "{{ .Values.service.port }}"
- name: AFFINE_SERVER_HOST
value: "{{ .Values.app.host }}"
- name: DOC_SERVICE_ENDPOINT
value: "http://{{ .Values.global.docService.name }}:{{ .Values.global.docService.port }}"
ports:
- name: http
containerPort: {{ .Values.service.port }}
protocol: TCP
livenessProbe:
tcpSocket:
port: http
initialDelaySeconds: {{ .Values.probe.initialDelaySeconds }}
readinessProbe:
tcpSocket:
port: http
initialDelaySeconds: {{ .Values.probe.initialDelaySeconds }}
resources:
{{- toYaml .Values.resources | nindent 12 }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}

View File

@@ -0,0 +1,19 @@
apiVersion: v1
kind: Service
metadata:
name: {{ include "sync.fullname" . }}
labels:
{{- include "sync.labels" . | nindent 4 }}
{{- with .Values.service.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
type: {{ .Values.service.type }}
ports:
- port: {{ .Values.service.port }}
targetPort: http
protocol: TCP
name: http
selector:
{{- include "sync.selectorLabels" . | nindent 4 }}

View File

@@ -2,9 +2,9 @@
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ include "front.serviceAccountName" . }}
name: {{ include "sync.serviceAccountName" . }}
labels:
{{- include "front.labels" . | nindent 4 }}
{{- include "sync.labels" . | nindent 4 }}
{{- with .Values.serviceAccount.annotations }}
annotations:
{{- toYaml . | nindent 4 }}

View File

@@ -1,9 +1,9 @@
apiVersion: v1
kind: Pod
metadata:
name: "{{ include "front.fullname" . }}-test-connection"
name: "{{ include "sync.fullname" . }}-test-connection"
labels:
{{- include "front.labels" . | nindent 4 }}
{{- include "sync.labels" . | nindent 4 }}
annotations:
"helm.sh/hook": test
spec:
@@ -11,5 +11,5 @@ spec:
- name: wget
image: busybox
command: ['wget']
args: ['{{ .Values.services.sync.name }}:{{ .Values.services.sync.port }}']
args: ['{{ include "sync.fullname" . }}:{{ .Values.service.port }}']
restartPolicy: Never

View File

@@ -0,0 +1,38 @@
replicaCount: 1
image:
repository: ghcr.io/toeverything/affine
pullPolicy: IfNotPresent
tag: ''
imagePullSecrets: []
nameOverride: ''
fullnameOverride: ''
# map to NODE_ENV environment variable
env: 'production'
app:
# AFFINE_SERVER_HOST
host: '0.0.0.0'
serviceAccount:
create: true
annotations: {}
name: 'affine-sync'
podAnnotations: {}
podSecurityContext:
fsGroup: 2000
resources:
limits:
cpu: '2'
memory: 4Gi
requests:
cpu: '1'
memory: 2Gi
probe:
initialDelaySeconds: 20
nodeSelector: {}
tolerations: []
affinity: {}

View File

@@ -0,0 +1,23 @@
# Patterns to ignore when building packages.
# This supports shell glob matching, relative path matching, and
# negation (prefixed with !). Only one pattern per line.
.DS_Store
# Common VCS dirs
.git/
.gitignore
.bzr/
.bzrignore
.hg/
.hgignore
.svn/
# Common backup files
*.swp
*.bak
*.tmp
*.orig
*~
# Various IDEs
.project
.idea/
*.tmproj
.vscode/

View File

@@ -0,0 +1,6 @@
apiVersion: v2
name: web
description: A Helm chart for Kubernetes
type: application
version: 0.0.0
appVersion: "0.7.0-canary.18"

View File

@@ -0,0 +1,16 @@
1. Get the application URL by running these commands:
{{- if contains "NodePort" .Values.service.type }}
export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "web.fullname" . }})
export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}")
echo http://$NODE_IP:$NODE_PORT
{{- else if contains "LoadBalancer" .Values.service.type }}
NOTE: It may take a few minutes for the LoadBalancer IP to be available.
You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "web.fullname" . }}'
export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "web.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}")
echo http://$SERVICE_IP:{{ .Values.service.port }}
{{- else if contains "ClusterIP" .Values.service.type }}
export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "web.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}")
export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}")
echo "Visit http://127.0.0.1:8080 to use your application"
kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT
{{- end }}

View File

@@ -0,0 +1,63 @@
{{/*
Expand the name of the chart.
*/}}
{{- define "web.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Create a default fully qualified app name.
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
If release name contains chart name it will be used as a full name.
*/}}
{{- define "web.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- if contains $name .Release.Name }}
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{- end }}
{{/*
Create chart name and version as used by the chart label.
*/}}
{{- define "web.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Common labels
*/}}
{{- define "web.labels" -}}
helm.sh/chart: {{ include "web.chart" . }}
{{ include "web.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
monitoring: enabled
{{- end }}
{{/*
Selector labels
*/}}
{{- define "web.selectorLabels" -}}
app.kubernetes.io/name: {{ include "web.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
{{/*
Create the name of the service account to use
*/}}
{{- define "web.serviceAccountName" -}}
{{- if .Values.serviceAccount.create }}
{{- default (include "web.fullname" .) .Values.serviceAccount.name }}
{{- else }}
{{- default "default" .Values.serviceAccount.name }}
{{- end }}
{{- end }}

View File

@@ -0,0 +1,60 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "web.fullname" . }}
labels:
{{- include "web.labels" . | nindent 4 }}
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
{{- include "web.selectorLabels" . | nindent 6 }}
template:
metadata:
{{- with .Values.podAnnotations }}
annotations:
{{- toYaml . | nindent 8 }}
{{- end }}
labels:
{{- include "web.selectorLabels" . | nindent 8 }}
spec:
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
serviceAccountName: {{ include "web.serviceAccountName" . }}
containers:
- name: {{ .Chart.Name }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
env:
- name: AFFINE_ENV
value: "{{ .Release.Namespace }}"
ports:
- name: http
containerPort: {{ .Values.service.port }}
protocol: TCP
livenessProbe:
httpGet:
path: /
port: http
initialDelaySeconds: {{ .Values.probe.initialDelaySeconds }}
readinessProbe:
httpGet:
path: /
port: http
initialDelaySeconds: {{ .Values.probe.initialDelaySeconds }}
resources:
{{- toYaml .Values.resources | nindent 12 }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}

View File

@@ -0,0 +1,15 @@
apiVersion: v1
kind: Service
metadata:
name: {{ include "web.fullname" . }}
labels:
{{- include "web.labels" . | nindent 4 }}
spec:
type: {{ .Values.service.type }}
ports:
- port: {{ .Values.service.port }}
targetPort: http
protocol: TCP
name: http
selector:
{{- include "web.selectorLabels" . | nindent 4 }}

View File

@@ -0,0 +1,12 @@
{{- if .Values.serviceAccount.create -}}
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ include "web.serviceAccountName" . }}
labels:
{{- include "web.labels" . | nindent 4 }}
{{- with .Values.serviceAccount.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
{{- end }}

View File

@@ -0,0 +1,15 @@
apiVersion: v1
kind: Pod
metadata:
name: "{{ include "web.fullname" . }}-test-connection"
labels:
{{- include "web.labels" . | nindent 4 }}
annotations:
"helm.sh/hook": test
spec:
containers:
- name: wget
image: busybox
command: ['wget']
args: ['{{ include "web.fullname" . }}:{{ .Values.service.port }}']
restartPolicy: Never

View File

@@ -0,0 +1,37 @@
replicaCount: 1
image:
repository: ghcr.io/toeverything/affine-front
pullPolicy: IfNotPresent
tag: ""
imagePullSecrets: []
nameOverride: ""
fullnameOverride: ""
serviceAccount:
create: true
annotations: {}
name: "affine-web"
podAnnotations: {}
podSecurityContext:
fsGroup: 2000
resources:
limits:
cpu: '500m'
memory: 2Gi
requests:
cpu: '500m'
memory: 2Gi
nodeSelector: {}
tolerations: []
affinity: {}
probe:
initialDelaySeconds: 1

View File

@@ -44,9 +44,9 @@ spec:
pathType: Prefix
backend:
service:
name: {{ $.Values.front.services.sync.name }}
name: affine-sync
port:
number: {{ $.Values.front.services.sync.port }}
number: {{ $.Values.sync.service.port }}
- path: /graphql
pathType: Prefix
backend:
@@ -65,15 +65,15 @@ spec:
pathType: Prefix
backend:
service:
name: {{ $.Values.front.services.renderer.name }}
name: affine-renderer
port:
number: {{ $.Values.front.services.renderer.port }}
number: {{ $.Values.renderer.service.port }}
- path: /
pathType: Prefix
backend:
service:
name: {{ $.Values.front.services.web.name }}
name: affine-web
port:
number: {{ $.Values.front.services.web.port }}
number: {{ $.Values.web.service.port }}
{{- end }}
{{- end }}

View File

@@ -47,27 +47,27 @@ graphql:
annotations:
cloud.google.com/backend-config: '{"default": "affine-api-backendconfig"}'
sync:
service:
type: ClusterIP
port: 3010
annotations:
cloud.google.com/backend-config: '{"default": "affine-api-backendconfig"}'
renderer:
service:
type: ClusterIP
port: 3000
annotations:
cloud.google.com/backend-config: '{"default": "affine-api-backendconfig"}'
doc:
service:
type: ClusterIP
annotations:
cloud.google.com/backend-config: '{"default": "affine-api-backendconfig"}'
front:
services:
sync:
name: affine-sync
type: ClusterIP
port: 3010
annotations:
cloud.google.com/backend-config: '{"default": "affine-api-backendconfig"}'
renderer:
name: affine-renderer
type: ClusterIP
port: 3000
annotations:
cloud.google.com/backend-config: '{"default": "affine-api-backendconfig"}'
web:
name: affine-web
type: ClusterIP
port: 8080
web:
service:
type: ClusterIP
port: 8080

View File

@@ -45,6 +45,7 @@ jobs:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
PERFSEE_TOKEN: ${{ secrets.PERFSEE_TOKEN }}
MIXPANEL_TOKEN: ${{ secrets.MIXPANEL_TOKEN }}
- name: Upload web artifact
uses: actions/upload-artifact@v4
with:
@@ -77,6 +78,7 @@ jobs:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
PERFSEE_TOKEN: ${{ secrets.PERFSEE_TOKEN }}
MIXPANEL_TOKEN: ${{ secrets.MIXPANEL_TOKEN }}
- name: Upload admin artifact
uses: actions/upload-artifact@v4
with:
@@ -109,6 +111,7 @@ jobs:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
PERFSEE_TOKEN: ${{ secrets.PERFSEE_TOKEN }}
MIXPANEL_TOKEN: ${{ secrets.MIXPANEL_TOKEN }}
- name: Upload mobile artifact
uses: actions/upload-artifact@v4
with:
@@ -184,6 +187,8 @@ jobs:
path: ./packages/backend/native
- name: List server-native files
run: ls -alh ./packages/backend/native
- name: Build @affine/reader
run: yarn workspace @affine/reader build
- name: Build Server
run: yarn workspace @affine/server build
- name: Upload server dist
@@ -263,7 +268,18 @@ jobs:
with:
app-version: ${{ inputs.app-version }}
- name: Build backend Dockerfile
- name: Build front Dockerfile
uses: docker/build-push-action@v6
with:
context: .
push: true
pull: true
platforms: linux/amd64,linux/arm64
provenance: true
file: .github/deployment/front/Dockerfile
tags: ghcr.io/toeverything/affine-front:${{inputs.build-type}}-${{ inputs.git-short-hash }}
- name: Build graphql Dockerfile
uses: docker/build-push-action@v6
with:
context: .

View File

@@ -19,7 +19,7 @@ env:
APP_NAME: affine
AFFINE_ENV: dev
COVERAGE: true
MACOSX_DEPLOYMENT_TARGET: '11.6'
MACOSX_DEPLOYMENT_TARGET: '10.13'
DEPLOYMENT_TYPE: affine
AFFINE_INDEXER_ENABLED: true
@@ -152,6 +152,11 @@ jobs:
name: server-native.node
path: ./packages/backend/native
- name: Bundle @affine/reader
shell: bash
run: |
yarn workspace @affine/reader build
- name: Run Check
run: |
yarn affine init
@@ -182,7 +187,7 @@ jobs:
strategy:
fail-fast: false
matrix:
shard: [1, 2]
shard: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
@@ -213,7 +218,7 @@ jobs:
strategy:
fail-fast: false
matrix:
shard: [1]
shard: [1, 2]
browser: ['chromium', 'firefox', 'webkit']
steps:
- uses: actions/checkout@v4
@@ -251,7 +256,7 @@ jobs:
strategy:
fail-fast: false
matrix:
shard: [1, 2, 3, 4, 5]
shard: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
@@ -282,7 +287,7 @@ jobs:
strategy:
fail-fast: false
matrix:
shard: [1, 2]
shard: [1, 2, 3, 4, 5]
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
@@ -313,7 +318,7 @@ jobs:
strategy:
fail-fast: false
matrix:
shard: [1, 2, 3]
shard: [1, 2, 3, 4, 5]
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
@@ -507,8 +512,8 @@ jobs:
strategy:
fail-fast: false
matrix:
node_index: [0, 1, 2, 3]
total_nodes: [4]
node_index: [0, 1, 2, 3, 4, 5, 6, 7]
total_nodes: [8]
env:
NODE_ENV: test
DATABASE_URL: postgresql://affine:affine@localhost:5432/affine
@@ -798,6 +803,49 @@ jobs:
name: fuzz-artifact
path: packages/common/y-octo/utils/fuzz/artifacts/**/*
y-octo-binding-test:
name: y-octo binding test on ${{ matrix.settings.target }}
runs-on: ${{ matrix.settings.os }}
strategy:
fail-fast: false
matrix:
settings:
- { target: 'x86_64-unknown-linux-gnu', os: 'ubuntu-latest' }
- { target: 'aarch64-unknown-linux-gnu', os: 'ubuntu-24.04-arm' }
- { target: 'x86_64-apple-darwin', os: 'macos-13' }
- { target: 'aarch64-apple-darwin', os: 'macos-latest' }
- { target: 'x86_64-pc-windows-msvc', os: 'windows-latest' }
- { target: 'aarch64-pc-windows-msvc', os: 'windows-11-arm' }
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: ./.github/actions/setup-node
with:
extra-flags: workspaces focus @affine-tools/cli @affine/monorepo @y-octo/node
electron-install: false
- name: Install rustup (Windows 11 ARM)
if: matrix.settings.os == 'windows-11-arm'
shell: pwsh
run: |
Invoke-WebRequest -Uri "https://static.rust-lang.org/rustup/dist/aarch64-pc-windows-msvc/rustup-init.exe" -OutFile rustup-init.exe
.\rustup-init.exe --default-toolchain none -y
"$env:USERPROFILE\.cargo\bin" | Out-File -Append -Encoding ascii $env:GITHUB_PATH
"CARGO_HOME=$env:USERPROFILE\.cargo" | Out-File -Append -Encoding ascii $env:GITHUB_ENV
- name: Install Rust (Windows 11 ARM)
if: matrix.settings.os == 'windows-11-arm'
shell: pwsh
run: |
rustup install stable
rustup target add ${{ matrix.settings.target }}
cargo --version
- name: Build Rust
uses: ./.github/actions/build-rust
with:
target: ${{ matrix.settings.target }}
package: '@y-octo/node'
- name: Run tests
run: yarn affine @y-octo/node test
rust-test:
name: Run native tests
runs-on: ubuntu-latest
@@ -924,8 +972,8 @@ jobs:
strategy:
fail-fast: false
matrix:
shardIndex: [1, 2, 3, 4, 5]
shardTotal: [5]
shardIndex: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
shardTotal: [10]
needs:
- build-server-native
services:
@@ -1016,12 +1064,36 @@ jobs:
fail-fast: false
matrix:
tests:
- name: 'Cloud E2E Test 1/2'
- name: 'Cloud E2E Test 1/10'
shard: 1
script: yarn affine @affine-test/affine-cloud e2e --forbid-only --shard=1/2
- name: 'Cloud E2E Test 2/2'
script: yarn affine @affine-test/affine-cloud e2e --forbid-only --shard=1/10
- name: 'Cloud E2E Test 2/10'
shard: 2
script: yarn affine @affine-test/affine-cloud e2e --forbid-only --shard=2/2
script: yarn affine @affine-test/affine-cloud e2e --forbid-only --shard=2/10
- name: 'Cloud E2E Test 3/10'
shard: 3
script: yarn affine @affine-test/affine-cloud e2e --forbid-only --shard=3/10
- name: 'Cloud E2E Test 4/10'
shard: 4
script: yarn affine @affine-test/affine-cloud e2e --forbid-only --shard=4/10
- name: 'Cloud E2E Test 5/10'
shard: 5
script: yarn affine @affine-test/affine-cloud e2e --forbid-only --shard=5/10
- name: 'Cloud E2E Test 6/10'
shard: 6
script: yarn affine @affine-test/affine-cloud e2e --forbid-only --shard=6/10
- name: 'Cloud E2E Test 7/10'
shard: 7
script: yarn affine @affine-test/affine-cloud e2e --forbid-only --shard=7/10
- name: 'Cloud E2E Test 8/10'
shard: 8
script: yarn affine @affine-test/affine-cloud e2e --forbid-only --shard=8/10
- name: 'Cloud E2E Test 9/10'
shard: 9
script: yarn affine @affine-test/affine-cloud e2e --forbid-only --shard=9/10
- name: 'Cloud E2E Test 10/10'
shard: 10
script: yarn affine @affine-test/affine-cloud e2e --forbid-only --shard=10/10
- name: 'Cloud Desktop E2E Test'
shard: desktop
script: |
@@ -1284,7 +1356,7 @@ jobs:
run: |
sudo add-apt-repository universe
sudo apt install -y libfuse2 elfutils flatpak flatpak-builder
flatpak remote-add --user --if-not-exists flathub https://dl.flathub.org/repo/flathub.flatpakrepo
flatpak remote-add --user --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
flatpak update
# some flatpak deps need git protocol.file.allow
git config --global protocol.file.allow always
@@ -1320,6 +1392,7 @@ jobs:
- miri
- loom
- fuzzing
- y-octo-binding-test
- server-test
- server-e2e-test
- rust-test

View File

@@ -1,225 +0,0 @@
name: Release Desktop Platform
on:
workflow_call:
inputs:
build_type:
required: true
type: string
app_version:
required: true
type: string
git_short_hash:
required: true
type: string
runner:
required: true
type: string
platform:
required: true
type: string
arch:
required: true
type: string
target:
required: true
type: string
apple_codesign:
required: false
default: false
type: boolean
install_linux_deps:
required: false
default: false
type: boolean
enable_scripts:
required: false
default: false
type: boolean
outputs:
files_to_be_signed:
description: Files to be signed (Windows only)
value: ${{ jobs.build.outputs.files_to_be_signed }}
permissions:
actions: write
contents: write
security-events: write
id-token: write
attestations: write
jobs:
build:
runs-on: ${{ inputs.runner }}
outputs:
files_to_be_signed: ${{ steps.get_files_to_be_signed.outputs.FILES_TO_BE_SIGNED }}
env:
BUILD_TYPE: ${{ inputs.build_type }}
RELEASE_VERSION: ${{ inputs.app_version }}
DEBUG: 'affine:*,napi:*'
APP_NAME: affine
MACOSX_DEPLOYMENT_TARGET: '12.0'
SKIP_GENERATE_ASSETS: 1
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
SENTRY_PROJECT: 'affine'
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
SENTRY_RELEASE: ${{ inputs.app_version }}
steps:
- uses: actions/checkout@v4
- name: Setup Version
uses: ./.github/actions/setup-version
with:
app-version: ${{ inputs.app_version }}
- name: Setup Node.js
timeout-minutes: 10
uses: ./.github/actions/setup-node
with:
extra-flags: workspaces focus @affine/electron @affine/monorepo @affine/nbstore @toeverything/infra
hard-link-nm: false
nmHoistingLimits: workspaces
enableScripts: ${{ inputs.enable_scripts }}
- name: Build AFFiNE native
uses: ./.github/actions/build-rust
with:
target: ${{ inputs.target }}
package: '@affine/native'
- uses: actions/download-artifact@v4
with:
name: desktop-web
path: packages/frontend/apps/electron/resources/web-static
- name: Build Desktop Layers
run: yarn affine @affine/electron build
- name: Signing By Apple Developer ID
if: ${{ inputs.platform == 'darwin' && inputs.apple_codesign }}
uses: apple-actions/import-codesign-certs@v5
with:
p12-file-base64: ${{ secrets.CERTIFICATES_P12 }}
p12-password: ${{ secrets.CERTIFICATES_P12_PASSWORD }}
- name: Install additional dependencies on Linux
if: ${{ inputs.platform == 'linux' && inputs.install_linux_deps }}
run: |
df -h
sudo add-apt-repository universe
sudo apt install -y libfuse2 elfutils flatpak flatpak-builder
flatpak remote-add --user --if-not-exists flathub https://dl.flathub.org/repo/flathub.flatpakrepo
flatpak update
# some flatpak deps need git protocol.file.allow
git config --global protocol.file.allow always
# clean up apt cache to save disk space
sudo -E apt-get -y purge azure-cli* zulu* hhvm* llvm* firefox* google* dotnet* aspnetcore* powershell* adoptopenjdk* mysql* php* mongodb* moby* snap* || true
sudo -E apt-get -qq autoremove --purge
sudo rm -rf /usr/share/dotnet /opt/ghc /opt/hostedtoolcache/CodeQL /usr/local/lib/android
sudo apt-get clean
rm -rf ~/.cache/yarn ~/.npm
df -h
- name: Remove nbstore node_modules (darwin/linux)
if: ${{ inputs.platform != 'win32' }}
shell: bash
# node_modules of nbstore is not needed for building, and it will make the build process out of memory
run: |
cargo clean
rm -rf packages/frontend/apps/electron/node_modules/@affine/nbstore/node_modules/@blocksuite
rm -rf packages/frontend/apps/electron/node_modules/@affine/native/node_modules
- name: Remove nbstore node_modules (windows)
if: ${{ inputs.platform == 'win32' }}
shell: bash
run: |
rm -rf packages/frontend/apps/electron/node_modules/@affine/nbstore/node_modules/@blocksuite/affine/node_modules
rm -rf packages/frontend/apps/electron/node_modules/@affine/native/node_modules
- name: make
if: ${{ inputs.platform != 'win32' }}
run: yarn affine @affine/electron make --platform=${{ inputs.platform }} --arch=${{ inputs.arch }}
env:
SKIP_WEB_BUILD: 1
HOIST_NODE_MODULES: 1
NODE_OPTIONS: --max-old-space-size=14384
- name: package
if: ${{ inputs.platform == 'win32' }}
run: |
yarn affine @affine/electron package --platform=${{ inputs.platform }} --arch=${{ inputs.arch }}
env:
SKIP_WEB_BUILD: 1
HOIST_NODE_MODULES: 1
NODE_OPTIONS: --max-old-space-size=14384
- name: signing DMG
if: ${{ inputs.platform == 'darwin' && inputs.apple_codesign }}
run: |
codesign --force --sign "Developer ID Application: TOEVERYTHING PTE. LTD." packages/frontend/apps/electron/out/${{ env.BUILD_TYPE }}/make/AFFiNE.dmg
- name: Save artifacts (mac)
if: ${{ inputs.platform == 'darwin' }}
run: |
mkdir -p builds
mv packages/frontend/apps/electron/out/*/make/*.dmg ./builds/affine-${{ env.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-macos-${{ inputs.arch }}.dmg
mv packages/frontend/apps/electron/out/*/make/zip/darwin/${{ inputs.arch }}/*.zip ./builds/affine-${{ env.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-macos-${{ inputs.arch }}.zip
- name: Save artifacts (linux)
if: ${{ inputs.platform == 'linux' }}
run: |
mkdir -p builds
mv packages/frontend/apps/electron/out/*/make/zip/linux/${{ inputs.arch }}/*.zip ./builds/affine-${{ env.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-linux-${{ inputs.arch }}.zip
mv packages/frontend/apps/electron/out/*/make/*.AppImage ./builds/affine-${{ env.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-linux-${{ inputs.arch }}.appimage
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@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@v2
if: ${{ inputs.platform == 'linux' }}
with:
subject-path: |
./builds/affine-${{ env.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-linux-${{ inputs.arch }}.zip
./builds/affine-${{ env.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-linux-${{ inputs.arch }}.appimage
./builds/affine-${{ env.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-linux-${{ inputs.arch }}.deb
./builds/affine-${{ env.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-linux-${{ inputs.arch }}.flatpak
- name: Upload Artifact
if: ${{ inputs.platform == 'darwin' || inputs.platform == 'linux' }}
uses: actions/upload-artifact@v4
with:
name: affine-${{ inputs.platform }}-${{ inputs.arch }}-builds
path: builds
- name: get all files to be signed
id: get_files_to_be_signed
if: ${{ inputs.platform == 'win32' }}
shell: pwsh
run: |
Set-Variable -Name FILES_TO_BE_SIGNED -Value ((Get-ChildItem -Path packages/frontend/apps/electron/out -Recurse -File | Where-Object { $_.Extension -in @(".exe", ".node", ".dll", ".msi") } | ForEach-Object { '"' + $_.FullName.Replace((Get-Location).Path + '\packages\frontend\apps\electron\out\', '') + '"' }) -join ' ')
"FILES_TO_BE_SIGNED=$FILES_TO_BE_SIGNED" >> $env:GITHUB_OUTPUT
echo $FILES_TO_BE_SIGNED
- name: Zip artifacts for faster upload
if: ${{ inputs.platform == 'win32' }}
shell: pwsh
run: Compress-Archive -CompressionLevel Fastest -Path packages/frontend/apps/electron/out/* -DestinationPath archive.zip
- name: Save packaged artifacts for signing
if: ${{ inputs.platform == 'win32' }}
uses: actions/upload-artifact@v4
with:
name: packaged-${{ inputs.platform }}-${{ inputs.arch }}
path: |
archive.zip
!**/*.map

View File

@@ -12,21 +12,6 @@ on:
git-short-hash:
required: true
type: string
desktop_macos:
description: 'Desktop - macOS'
required: false
default: true
type: boolean
desktop_windows:
description: 'Desktop - Windows'
required: false
default: true
type: boolean
desktop_linux:
description: 'Desktop - Linux'
required: false
default: true
type: boolean
permissions:
actions: write
@@ -40,11 +25,10 @@ env:
RELEASE_VERSION: ${{ inputs.app-version }}
DEBUG: 'affine:*,napi:*'
APP_NAME: affine
MACOSX_DEPLOYMENT_TARGET: '11.6'
MACOSX_DEPLOYMENT_TARGET: '10.13'
jobs:
before-make:
if: ${{ inputs.desktop_macos || inputs.desktop_windows || inputs.desktop_linux }}
runs-on: ubuntu-latest
environment: ${{ inputs.build-type }}
steps:
@@ -66,6 +50,7 @@ jobs:
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
SENTRY_RELEASE: ${{ inputs.app-version }}
RELEASE_VERSION: ${{ inputs.app-version }}
MIXPANEL_TOKEN: ${{ secrets.MIXPANEL_TOKEN }}
- name: Upload web artifact
uses: actions/upload-artifact@v4
@@ -73,8 +58,7 @@ jobs:
name: desktop-web
path: packages/frontend/apps/electron/resources/web-static
make-distribution-macos:
if: ${{ inputs.desktop_macos }}
make-distribution:
strategy:
fail-fast: false
matrix:
@@ -87,90 +71,223 @@ jobs:
platform: darwin
arch: arm64
target: aarch64-apple-darwin
needs: before-make
uses: ./.github/workflows/release-desktop-platform.yml
secrets: inherit
with:
build_type: ${{ inputs.build-type }}
app_version: ${{ inputs.app-version }}
git_short_hash: ${{ inputs.git-short-hash }}
runner: ${{ matrix.spec.runner }}
platform: ${{ matrix.spec.platform }}
arch: ${{ matrix.spec.arch }}
target: ${{ matrix.spec.target }}
apple_codesign: true
make-distribution-linux:
if: ${{ inputs.desktop_linux }}
strategy:
fail-fast: false
matrix:
spec:
- runner: ubuntu-latest
platform: linux
arch: x64
target: x86_64-unknown-linux-gnu
runs-on: ${{ matrix.spec.runner }}
needs: before-make
uses: ./.github/workflows/release-desktop-platform.yml
secrets: inherit
with:
build_type: ${{ inputs.build-type }}
app_version: ${{ inputs.app-version }}
git_short_hash: ${{ inputs.git-short-hash }}
runner: ${{ matrix.spec.runner }}
platform: ${{ matrix.spec.platform }}
arch: ${{ matrix.spec.arch }}
target: ${{ matrix.spec.target }}
install_linux_deps: true
environment: ${{ inputs.build-type }}
env:
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
SKIP_GENERATE_ASSETS: 1
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
SENTRY_PROJECT: 'affine'
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
SENTRY_RELEASE: ${{ inputs.app-version }}
MIXPANEL_TOKEN: ${{ secrets.MIXPANEL_TOKEN }}
steps:
- uses: actions/checkout@v4
- name: Setup Version
uses: ./.github/actions/setup-version
with:
app-version: ${{ inputs.app-version }}
- name: Setup Node.js
timeout-minutes: 10
uses: ./.github/actions/setup-node
with:
extra-flags: workspaces focus @affine/electron @affine/monorepo @affine/nbstore @toeverything/infra
hard-link-nm: false
nmHoistingLimits: workspaces
enableScripts: false
- name: Build AFFiNE native
uses: ./.github/actions/build-rust
with:
target: ${{ matrix.spec.target }}
package: '@affine/native'
- uses: actions/download-artifact@v4
with:
name: desktop-web
path: packages/frontend/apps/electron/resources/web-static
package-distribution-windows-x64:
if: ${{ inputs.desktop_windows }}
needs: before-make
uses: ./.github/workflows/release-desktop-platform.yml
secrets: inherit
with:
build_type: ${{ inputs.build-type }}
app_version: ${{ inputs.app-version }}
git_short_hash: ${{ inputs.git-short-hash }}
runner: windows-latest
platform: win32
arch: x64
target: x86_64-pc-windows-msvc
enable_scripts: true
- name: Build Desktop Layers
run: yarn affine @affine/electron build
package-distribution-windows-arm64:
if: ${{ inputs.desktop_windows }}
- name: Signing By Apple Developer ID
if: ${{ matrix.spec.platform == 'darwin' }}
uses: apple-actions/import-codesign-certs@v5
with:
p12-file-base64: ${{ secrets.CERTIFICATES_P12 }}
p12-password: ${{ secrets.CERTIFICATES_P12_PASSWORD }}
- name: Install additional dependencies on Linux
if: ${{ matrix.spec.platform == 'linux' }}
run: |
sudo add-apt-repository universe
sudo apt install -y libfuse2 elfutils flatpak flatpak-builder
flatpak remote-add --user --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
flatpak update
# some flatpak deps need git protocol.file.allow
git config --global protocol.file.allow always
- name: Remove nbstore node_modules
shell: bash
# node_modules of nbstore is not needed for building, and it will make the build process out of memory
run: |
rm -rf packages/frontend/apps/electron/node_modules/@affine/nbstore/node_modules/@blocksuite
rm -rf packages/frontend/apps/electron/node_modules/@affine/native/node_modules
- name: make
run: yarn affine @affine/electron make --platform=${{ matrix.spec.platform }} --arch=${{ matrix.spec.arch }}
env:
SKIP_WEB_BUILD: 1
HOIST_NODE_MODULES: 1
NODE_OPTIONS: --max-old-space-size=14384
- name: signing DMG
if: ${{ matrix.spec.platform == 'darwin' }}
run: |
codesign --force --sign "Developer ID Application: TOEVERYTHING PTE. LTD." packages/frontend/apps/electron/out/${{ env.BUILD_TYPE }}/make/AFFiNE.dmg
- name: Save artifacts (mac)
if: ${{ matrix.spec.platform == 'darwin' }}
run: |
mkdir -p builds
mv packages/frontend/apps/electron/out/*/make/*.dmg ./builds/affine-${{ env.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-macos-${{ matrix.spec.arch }}.dmg
mv packages/frontend/apps/electron/out/*/make/zip/darwin/${{ matrix.spec.arch }}/*.zip ./builds/affine-${{ env.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-macos-${{ matrix.spec.arch }}.zip
- name: Save artifacts (linux)
if: ${{ matrix.spec.platform == 'linux' }}
run: |
mkdir -p builds
mv packages/frontend/apps/electron/out/*/make/zip/linux/${{ matrix.spec.arch }}/*.zip ./builds/affine-${{ env.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-linux-${{ matrix.spec.arch }}.zip
mv packages/frontend/apps/electron/out/*/make/*.AppImage ./builds/affine-${{ env.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-linux-${{ matrix.spec.arch }}.appimage
mv packages/frontend/apps/electron/out/*/make/deb/${{ matrix.spec.arch }}/*.deb ./builds/affine-${{ env.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-linux-${{ matrix.spec.arch }}.deb
mv packages/frontend/apps/electron/out/*/make/flatpak/*/*.flatpak ./builds/affine-${{ env.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-linux-${{ matrix.spec.arch }}.flatpak
- uses: actions/attest-build-provenance@v2
if: ${{ matrix.spec.platform == 'darwin' }}
with:
subject-path: |
./builds/affine-${{ env.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-macos-${{ matrix.spec.arch }}.zip
./builds/affine-${{ env.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-macos-${{ matrix.spec.arch }}.dmg
- uses: actions/attest-build-provenance@v2
if: ${{ matrix.spec.platform == 'linux' }}
with:
subject-path: |
./builds/affine-${{ env.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-linux-x64.zip
./builds/affine-${{ env.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-linux-x64.appimage
./builds/affine-${{ env.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-linux-x64.deb
- name: Upload Artifact
uses: actions/upload-artifact@v4
with:
name: affine-${{ matrix.spec.platform }}-${{ matrix.spec.arch }}-builds
path: builds
package-distribution-windows:
environment: ${{ inputs.build-type }}
strategy:
fail-fast: false
matrix:
spec:
- runner: windows-latest
platform: win32
arch: x64
target: x86_64-pc-windows-msvc
- runner: windows-latest
platform: win32
arch: arm64
target: aarch64-pc-windows-msvc
runs-on: ${{ matrix.spec.runner }}
needs: before-make
uses: ./.github/workflows/release-desktop-platform.yml
secrets: inherit
with:
build_type: ${{ inputs.build-type }}
app_version: ${{ inputs.app-version }}
git_short_hash: ${{ inputs.git-short-hash }}
runner: windows-latest
platform: win32
arch: arm64
target: aarch64-pc-windows-msvc
enable_scripts: true
outputs:
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 }}
env:
SKIP_GENERATE_ASSETS: 1
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
SENTRY_PROJECT: 'affine'
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
SENTRY_RELEASE: ${{ inputs.app-version }}
MIXPANEL_TOKEN: ${{ secrets.MIXPANEL_TOKEN }}
steps:
- uses: actions/checkout@v4
- name: Setup Version
uses: ./.github/actions/setup-version
with:
app-version: ${{ inputs.app-version }}
- name: Setup Node.js
timeout-minutes: 10
uses: ./.github/actions/setup-node
with:
extra-flags: workspaces focus @affine/electron @affine/monorepo @affine/nbstore @toeverything/infra
hard-link-nm: false
nmHoistingLimits: workspaces
- name: Build AFFiNE native
uses: ./.github/actions/build-rust
with:
target: ${{ matrix.spec.target }}
package: '@affine/native'
- uses: actions/download-artifact@v4
with:
name: desktop-web
path: packages/frontend/apps/electron/resources/web-static
- name: Build Desktop Layers
run: yarn affine @affine/electron build
- name: Remove nbstore node_modules
shell: bash
# node_modules of nbstore is not needed for building, and it will make the build process out of memory
run: |
rm -rf packages/frontend/apps/electron/node_modules/@affine/nbstore/node_modules/@blocksuite/affine/node_modules
rm -rf packages/frontend/apps/electron/node_modules/@affine/native/node_modules
- name: package
run: |
yarn affine @affine/electron package --platform=${{ matrix.spec.platform }} --arch=${{ matrix.spec.arch }}
env:
SKIP_WEB_BUILD: 1
HOIST_NODE_MODULES: 1
NODE_OPTIONS: --max-old-space-size=14384
- name: get all files to be signed
id: get_files_to_be_signed
run: |
Set-Variable -Name FILES_TO_BE_SIGNED -Value ((Get-ChildItem -Path packages/frontend/apps/electron/out -Recurse -File | Where-Object { $_.Extension -in @(".exe", ".node", ".dll", ".msi") } | ForEach-Object { '"' + $_.FullName.Replace((Get-Location).Path + '\packages\frontend\apps\electron\out\', '') + '"' }) -join ' ')
"FILES_TO_BE_SIGNED_${{ matrix.spec.arch }}=$FILES_TO_BE_SIGNED" >> $env:GITHUB_OUTPUT
echo $FILES_TO_BE_SIGNED
- name: Zip artifacts for faster upload
run: Compress-Archive -CompressionLevel Fastest -Path packages/frontend/apps/electron/out/* -DestinationPath archive.zip
- name: Save packaged artifacts for signing
uses: actions/upload-artifact@v4
with:
name: packaged-${{ matrix.spec.platform }}-${{ matrix.spec.arch }}
path: |
archive.zip
!**/*.map
sign-packaged-artifacts-windows_x64:
if: ${{ inputs.desktop_windows }}
needs: package-distribution-windows-x64
needs: package-distribution-windows
uses: ./.github/workflows/windows-signer.yml
with:
files: ${{ needs.package-distribution-windows-x64.outputs.files_to_be_signed }}
files: ${{ needs.package-distribution-windows.outputs.FILES_TO_BE_SIGNED_x64 }}
artifact-name: packaged-win32-x64
sign-packaged-artifacts-windows_arm64:
if: ${{ inputs.desktop_windows }}
needs: package-distribution-windows-arm64
needs: package-distribution-windows
uses: ./.github/workflows/windows-signer.yml
with:
files: ${{ needs.package-distribution-windows-arm64.outputs.files_to_be_signed }}
files: ${{ needs.package-distribution-windows.outputs.FILES_TO_BE_SIGNED_arm64 }}
artifact-name: packaged-win32-arm64
make-windows-installer:
if: ${{ inputs.desktop_windows }}
needs:
- sign-packaged-artifacts-windows_x64
- sign-packaged-artifacts-windows_arm64
@@ -232,7 +349,6 @@ jobs:
path: archive.zip
sign-installer-artifacts-windows-x64:
if: ${{ inputs.desktop_windows }}
needs: make-windows-installer
uses: ./.github/workflows/windows-signer.yml
with:
@@ -240,7 +356,6 @@ jobs:
artifact-name: installer-win32-x64
sign-installer-artifacts-windows-arm64:
if: ${{ inputs.desktop_windows }}
needs: make-windows-installer
uses: ./.github/workflows/windows-signer.yml
with:
@@ -248,7 +363,6 @@ jobs:
artifact-name: installer-win32-arm64
finalize-installer-windows:
if: ${{ inputs.desktop_windows }}
needs:
[
sign-installer-artifacts-windows-x64,
@@ -296,18 +410,17 @@ jobs:
path: builds
release:
if: ${{ inputs.desktop_macos && inputs.desktop_linux && inputs.desktop_windows }}
needs:
[
before-make,
make-distribution-macos,
make-distribution-linux,
finalize-installer-windows,
]
needs: [before-make, make-distribution, finalize-installer-windows]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v4
with:
name: desktop-web
path: web-static
- name: Zip web-static
run: zip -r web-static.zip web-static
- name: Download Artifacts (macos-x64)
uses: actions/download-artifact@v4
with:
@@ -353,4 +466,6 @@ jobs:
draft: ${{ inputs.build-type == 'stable' }}
prerelease: ${{ inputs.build-type != 'stable' }}
tag_name: v${{ env.RELEASE_VERSION}}
files: ./release/*
files: |
./release/*
./release/.env.example

View File

@@ -39,6 +39,7 @@ jobs:
run: yarn affine @affine/ios build
env:
PUBLIC_PATH: '/'
MIXPANEL_TOKEN: ${{ secrets.MIXPANEL_TOKEN }}
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
SENTRY_PROJECT: 'affine'
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
@@ -67,6 +68,7 @@ jobs:
run: yarn affine @affine/android build
env:
PUBLIC_PATH: '/'
MIXPANEL_TOKEN: ${{ secrets.MIXPANEL_TOKEN }}
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
SENTRY_PROJECT: 'affine'
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
@@ -108,7 +110,7 @@ jobs:
enableScripts: false
- uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: 26.2
xcode-version: 16.4
- name: Install Swiftformat
run: brew install swiftformat
- name: Cap sync

View File

@@ -11,18 +11,8 @@ on:
required: true
type: boolean
default: false
desktop_macos:
description: 'Desktop - macOS'
required: true
type: boolean
default: false
desktop_windows:
description: 'Desktop - Windows'
required: true
type: boolean
default: false
desktop_linux:
description: 'Desktop - Linux'
desktop:
description: 'Release Desktop?'
required: true
type: boolean
default: false
@@ -60,68 +50,6 @@ jobs:
id: prepare
uses: ./.github/actions/prepare-release
canary-gate:
name: Canary Gate
runs-on: ubuntu-latest
needs:
- prepare
outputs:
SHOULD_RELEASE: ${{ steps.decide.outputs.SHOULD_RELEASE }}
LAST_CANARY_TAG: ${{ steps.decide.outputs.LAST_CANARY_TAG }}
LAST_CANARY_SHA: ${{ steps.decide.outputs.LAST_CANARY_SHA }}
steps:
- name: Decide whether to release
id: decide
uses: actions/github-script@v7
with:
script: |
const buildType = '${{ needs.prepare.outputs.BUILD_TYPE }}'
if (buildType !== 'canary') {
core.setOutput('SHOULD_RELEASE', 'true')
return
}
const owner = context.repo.owner
const repo = context.repo.repo
const currentSha = context.sha
const canaryTagRe = /^v\d+\.\d+\.\d+-canary\.[0-9a-f]+$/i
let page = 1
const perPage = 100
let lastCanary = null
while (!lastCanary && page <= 10) {
const { data } = await github.rest.repos.listTags({
owner,
repo,
per_page: perPage,
page,
})
for (const tag of data) {
if (canaryTagRe.test(tag.name)) {
lastCanary = tag
break
}
}
if (data.length < perPage) break
page++
}
if (!lastCanary) {
core.warning('No canary tags found; proceeding with canary release.')
core.setOutput('SHOULD_RELEASE', 'true')
return
}
core.setOutput('LAST_CANARY_TAG', lastCanary.name)
core.setOutput('LAST_CANARY_SHA', lastCanary.commit.sha)
const shouldRelease = lastCanary.commit.sha !== currentSha
core.info(`Latest canary tag ${lastCanary.name} -> ${lastCanary.commit.sha}; current ${currentSha}; should_release=${shouldRelease}`)
core.setOutput('SHOULD_RELEASE', shouldRelease ? 'true' : 'false')
cloud:
name: Release Cloud
if: ${{ inputs.web || github.event_name != 'workflow_dispatch' }}
@@ -136,11 +64,9 @@ jobs:
image:
name: Release Docker Image
if: ${{ needs.canary-gate.outputs.SHOULD_RELEASE == 'true' }}
runs-on: ubuntu-latest
needs:
- prepare
- canary-gate
- cloud
steps:
- uses: trstringer/manual-approval@v1
@@ -148,7 +74,7 @@ jobs:
name: Wait for approval
with:
secret: ${{ secrets.GITHUB_TOKEN }}
approvers: darkskygit,pengx17,L-Sun,EYHN
approvers: forehalo,fengmk2,darkskygit
minimum-approvals: 1
fail-on-denial: true
issue-title: Please confirm to release docker image
@@ -176,25 +102,15 @@ jobs:
desktop:
name: Release Desktop
if: >-
${{
(github.event_name != 'workflow_dispatch' && needs.canary-gate.outputs.SHOULD_RELEASE == 'true') ||
inputs.desktop_macos ||
inputs.desktop_windows ||
inputs.desktop_linux
}}
if: ${{ inputs.desktop || github.event_name != 'workflow_dispatch' }}
needs:
- prepare
- canary-gate
uses: ./.github/workflows/release-desktop.yml
secrets: inherit
with:
build-type: ${{ needs.prepare.outputs.BUILD_TYPE }}
app-version: ${{ needs.prepare.outputs.APP_VERSION }}
git-short-hash: ${{ needs.prepare.outputs.GIT_SHORT_HASH }}
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 }}
mobile:
name: Release Mobile

4
.gitignore vendored
View File

@@ -33,9 +33,6 @@ node_modules
!.vscode/launch.template.json
!.vscode/extensions.json
# Kiro
.kiro
# misc
/.sass-cache
/connect.lock
@@ -47,7 +44,6 @@ testem.log
.pnpm-debug.log
/typings
tsconfig.tsbuildinfo
.context
# System Files
.DS_Store

2
.nvmrc
View File

@@ -1 +1 @@
22.22.0
22.16.0

View File

@@ -1,11 +1,7 @@
exclude = [
"node_modules/**/*.toml",
"target/**/*.toml",
"packages/frontend/apps/ios/App/Packages/AffineGraphQL/**/*.toml",
]
include = ["./*.toml", "./packages/**/*.toml"]
# https://taplo.tamasfe.dev/configuration/formatter-options.html
[formatting]
align_entries = true
indent_tables = true
reorder_keys = true
align_entries = true
column_width = 180
reorder_arrays = true
reorder_keys = true

File diff suppressed because one or more lines are too long

View File

@@ -12,4 +12,4 @@ npmPublishAccess: public
npmRegistryServer: "https://registry.npmjs.org"
yarnPath: .yarn/releases/yarn-4.12.0.cjs
yarnPath: .yarn/releases/yarn-4.9.1.cjs

2160
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -3,6 +3,7 @@ members = [
"./packages/backend/native",
"./packages/common/native",
"./packages/common/y-octo/core",
"./packages/common/y-octo/node",
"./packages/common/y-octo/utils",
"./packages/frontend/mobile-native",
"./packages/frontend/native",
@@ -12,124 +13,108 @@ members = [
]
resolver = "3"
[workspace.package]
edition = "2024"
[workspace.package]
edition = "2024"
[workspace.dependencies]
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"
async-lock = { version = "3.4.0", features = ["loom"] }
base64-simd = "0.8"
bitvec = "1.0"
block2 = "0.6"
byteorder = "1.5"
chrono = "0.4"
clap = { version = "4.4", features = ["derive"] }
core-foundation = "0.10"
coreaudio-rs = "0.12"
cpal = "0.15"
criterion = { version = "0.5", features = ["html_reports"] }
criterion2 = { version = "3", default-features = false }
crossbeam-channel = "0.5"
dispatch2 = "0.3"
docx-parser = { git = "https://github.com/toeverything/docx-parser" }
dotenvy = "0.15"
file-format = { version = "0.28", features = ["reader"] }
homedir = "0.3"
infer = { version = "0.19.0" }
lasso = { version = "0.7", features = ["multi-threaded"] }
lib0 = { version = "0.16", features = ["lib0-serde"] }
libc = "0.2"
log = "0.4"
loom = { version = "0.7", features = ["checkpoint"] }
memory-indexer = "0.3.0"
mimalloc = "0.1"
mp4parse = "0.17"
nanoid = "0.4"
napi = { version = "3.7.0", features = [
"async",
"chrono_date",
"error_anyhow",
"napi9",
"serde",
] }
napi-build = { version = "2" }
napi-derive = { version = "3.4" }
nom = "8"
notify = { version = "8", features = ["serde"] }
objc2 = "0.6"
objc2-foundation = "0.3"
once_cell = "1"
ordered-float = "5"
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"
pulldown-cmark = "0.13"
rand = "0.9"
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"
screencapturekit = "0.3"
serde = "1"
serde_json = "1"
sha3 = "0.10"
smol_str = "0.3"
sqlx = { version = "0.8", default-features = false, features = [
"chrono",
"macros",
"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"
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"
v_htmlescape = "0.15"
windows = { version = "0.61", features = [
"Win32_Devices_FunctionDiscovery",
"Win32_Foundation",
"Win32_Media_Audio",
"Win32_System_Com",
"Win32_System_Com_StructuredStorage",
"Win32_System_Diagnostics_ToolHelp",
"Win32_System_ProcessStatus",
"Win32_System_Threading",
"Win32_System_Variant",
"Win32_UI_Shell_PropertiesSystem",
] }
windows-core = { version = "0.61" }
y-octo = { path = "./packages/common/y-octo/core" }
y-sync = { version = "0.4" }
yrs = "0.23.0"
[workspace.dependencies]
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"
async-lock = { version = "3.4.0", features = ["loom"] }
base64-simd = "0.8"
bitvec = "1.0"
block2 = "0.6"
byteorder = "1.5"
cpal = "0.15"
chrono = "0.4"
clap = { version = "4.4", features = ["derive"] }
core-foundation = "0.10"
coreaudio-rs = "0.12"
criterion = { version = "0.5", features = ["html_reports"] }
criterion2 = { version = "3", default-features = false }
crossbeam-channel = "0.5"
dispatch2 = "0.3"
docx-parser = { git = "https://github.com/toeverything/docx-parser" }
dotenvy = "0.15"
file-format = { version = "0.26", features = ["reader"] }
homedir = "0.3"
infer = { version = "0.19.0" }
lasso = { version = "0.7", features = ["multi-threaded"] }
lib0 = { version = "0.16", features = ["lib0-serde"] }
libc = "0.2"
log = "0.4"
loom = { version = "0.7", features = ["checkpoint"] }
mimalloc = "0.1"
nanoid = "0.4"
napi = { version = "3.0.0-beta.3", features = ["async", "chrono_date", "error_anyhow", "napi9", "serde"] }
napi-build = { version = "2" }
napi-derive = { version = "3.0.0-beta.3" }
nom = "8"
notify = { version = "8", features = ["serde"] }
objc2 = "0.6"
objc2-foundation = "0.3"
once_cell = "1"
ordered-float = "5"
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"
rand = "0.9"
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"
screencapturekit = "0.3"
serde = "1"
serde_json = "1"
sha3 = "0.10"
smol_str = "0.3"
sqlx = { version = "0.8", default-features = false, features = ["chrono", "macros", "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"
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"
v_htmlescape = "0.15"
windows = { version = "0.61", features = [
"Win32_Devices_FunctionDiscovery",
"Win32_UI_Shell_PropertiesSystem",
"Win32_Media_Audio",
"Win32_System_Variant",
"Win32_System_Com_StructuredStorage",
"Win32_System_Threading",
"Win32_System_ProcessStatus",
"Win32_Foundation",
"Win32_System_Com",
"Win32_System_Diagnostics_ToolHelp",
] }
windows-core = { version = "0.61" }
y-octo = { path = "./packages/common/y-octo/core" }
y-sync = { version = "0.4" }
yrs = "0.23.0"
[profile.dev.package.sqlx-macros]
opt-level = 3
@@ -140,6 +125,6 @@ lto = true
opt-level = 3
strip = "symbols"
# android uniffi bindgen requires symbols
[profile.release.package.affine_mobile_native]
strip = "none"
# android uniffi bindgen requires symbols
[profile.release.package.affine_mobile_native]
strip = "none"

View File

@@ -2,7 +2,7 @@ Copyright (c) 2022-present TOEVERYTHING PTE. LTD. and its affiliates.
Portions of this software are licensed as follows:
- All content that resides under the "packages/backend" and "packages/common/native" directory of this repository, if that directory exists, is licensed under the license defined in "packages/backend/server/LICENSE".
- All content that resides under the "packages/backend/server" directory of this repository, if that directory exists, is licensed under the license defined in "packages/backend/server/LICENSE".
- All third party components incorporated into the AFFiNE Software are licensed under the original license provided by the owner of the applicable component.
- Content outside of the above mentioned directories or restrictions above is available under the "MIT" license as defined in "LICENSE-MIT".

View File

@@ -6,7 +6,7 @@
<br>
</h1>
<a href="https://affine.pro/download">
<img alt="affine logo" src="https://cdn.affine.pro/Github_hero_image2.png" style="width: 100%">
<img alt="affine logo" src="https://cdn.affine.pro/Github_hero_image1.png" style="width: 100%">
</a>
<br/>
<p align="center">
@@ -81,7 +81,7 @@ Star us, and you will receive all release notifications from GitHub without any
**Multimodal AI partner ready to kick in any work**
- Write up professional work report? Turn an outline into expressive and presentable slides? Summary an article into a well-structured mindmap? Sorting your job plan and backlog for tasks? Or... draw and code prototype apps and web pages directly all with one prompt? With you, [AFFiNE AI](https://affine.pro/ai) pushes your creativity to the edge of your imagination, just like [Canvas AI](https://affine.pro/blog/best-canvas-ai) to generate mind map for brainstorming.
- Write up professional work report? Turn an outline into expressive and presentable slides? Summary an article into a well-structured mindmap? Sorting your job plan and backlog for tasks? Or... draw and code prototype apps and web pages directly all with one prompt? With you, [AFFiNE AI](https://affine.pro/ai) pushes your creativity to the edge of your imagination,just like [Canvas AI](https://affine.pro/blog/best-canvas-ai) to generate mind map for brainstorming.
**Local-first & Real-time collaborative**
@@ -193,8 +193,6 @@ We would like to express our gratitude to all the individuals who have already c
Begin with Docker to deploy your own feature-rich, unrestricted version of AFFiNE. Our team is diligently updating to the latest version. For more information on how to self-host AFFiNE, please refer to our [documentation](https://docs.affine.pro/self-host-affine).
[![Run on Sealos](https://sealos.io/Deploy-on-Sealos.svg)](https://sealos.io/products/app-store/affine)
[![Run on ClawCloud](https://raw.githubusercontent.com/ClawCloud/Run-Template/refs/heads/main/Run-on-ClawCloud.svg)](https://template.run.claw.cloud/?openapp=system-fastdeploy%3FtemplateName%3Daffine)
## Hiring

View File

@@ -6,14 +6,15 @@ We recommend users to always use the latest major version. Security updates will
| Version | Supported |
| --------------- | ------------------ |
| 0.26.x (stable) | :white_check_mark: |
| < 0.26.x | :x: |
| 0.17.x (stable) | :white_check_mark: |
| < 0.17.x | :x: |
## Reporting a Vulnerability
We welcome you to provide us with bug reports via and email at [security@toeverything.info](mailto:security@toeverything.info) or submit directly on [GitHub](https://github.com/toeverything/AFFiNE/security), **we encourage you to submit the relevant information directly via GitHub**. We expect your report to contain at least the following for us to evaluate and reproduce:
We welcome you to provide us with bug reports via and email at [security@toeverything.info](mailto:security@toeverything.info). We expect your report to contain at least the following for us to evaluate and reproduce:
1. Using platform and version, for example:
- macos arm64 0.12.0-canary-202402220729-0868ac6
- app.affine.pro 0.12.0-canary-202402220729-0868ac6
@@ -21,6 +22,8 @@ We welcome you to provide us with bug reports via and email at [security@toevery
3. Your classification or analysis of the vulnerability (optional)
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.
Since we are an open source project, we also welcome you to provide corresponding fix PRs.
We will provide bounties for vulnerabilities involving user information leakage, permission leakage, and unauthorized code execution. For other types of vulnerabilities, we will determine specific rewards based on the evaluation results.
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.

View File

@@ -79,7 +79,7 @@
"@blocksuite/std": "workspace:*",
"@blocksuite/store": "workspace:*",
"@blocksuite/sync": "workspace:*",
"rxjs": "^7.8.2"
"rxjs": "^7.8.1"
},
"exports": {
".": "./src/index.ts",
@@ -296,10 +296,10 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.26.1",
"version": "0.22.4",
"devDependencies": {
"@vanilla-extract/vite-plugin": "^5.0.0",
"msw": "^2.12.4",
"vitest": "^3.2.4"
"msw": "^2.8.4",
"vitest": "3.1.3"
}
}

View File

@@ -2214,7 +2214,7 @@ describe('html to snapshot', () => {
test('iframe', async () => {
const html = template(
`<iframe width="560" height="315" src="https://www.youtube.com/embed/QDsd0nyzwz0?start=&amp;end=" title="YouTube video player" frameborder="0" allow="fullscreen; autoplay; clipboard-write; encrypted-media; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin"></iframe>`
`<iframe width="560" height="315" src="https://www.youtube.com/embed/QDsd0nyzwz0?start=&amp;end=" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>`
);
const blockSnapshot: BlockSnapshot = {

View File

@@ -1,4 +1,3 @@
import { MarkdownTransformer } from '@blocksuite/affine/widgets/linked-doc';
import {
DefaultTheme,
NoteDisplayMode,
@@ -17,15 +16,12 @@ import type {
SliceSnapshot,
TransformerMiddleware,
} from '@blocksuite/store';
import { AssetsManager, MemoryBlobCRUD, Schema } from '@blocksuite/store';
import { TestWorkspace } from '@blocksuite/store/test';
import { AssetsManager, MemoryBlobCRUD } from '@blocksuite/store';
import { describe, expect, test } from 'vitest';
import { AffineSchemas } from '../../schemas.js';
import { createJob } from '../utils/create-job.js';
import { getProvider } from '../utils/get-provider.js';
import { nanoidReplacement } from '../utils/nanoid-replacement.js';
import { testStoreExtensions } from '../utils/store.js';
const provider = getProvider();
@@ -94,39 +90,6 @@ describe('snapshot to markdown', () => {
expect(target.file).toBe(markdown);
});
test('imports frontmatter metadata into doc meta', async () => {
const schema = new Schema().register(AffineSchemas);
const collection = new TestWorkspace();
collection.storeExtensions = testStoreExtensions;
collection.meta.initialize();
const markdown = `---
title: Web developer
created: 2018-04-12T09:51:00
updated: 2018-04-12T10:00:00
tags: [a, b]
favorite: true
---
Hello world
`;
const docId = await MarkdownTransformer.importMarkdownToDoc({
collection,
schema,
markdown,
fileName: 'fallback-title',
extensions: testStoreExtensions,
});
expect(docId).toBeTruthy();
const meta = collection.meta.getDocMeta(docId!);
expect(meta?.title).toBe('Web developer');
expect(meta?.createDate).toBe(Date.parse('2018-04-12T09:51:00'));
expect(meta?.updatedDate).toBe(Date.parse('2018-04-12T10:00:00'));
expect(meta?.favorite).toBe(true);
expect(meta?.tags).toEqual(['a', 'b']);
});
test('paragraph', async () => {
const blockSnapshot: BlockSnapshot = {
type: 'block',
@@ -3033,50 +2996,6 @@ describe('markdown to snapshot', () => {
});
});
test('html inline color span imports to nearest supported text color', async () => {
const markdown = `<span style="color: #00afde;">Hello</span>`;
const blockSnapshot: BlockSnapshot = {
type: 'block',
id: 'matchesReplaceMap[0]',
flavour: 'affine:note',
props: {
xywh: '[0,0,800,95]',
background: DefaultTheme.noteBackgrounColor,
index: 'a0',
hidden: false,
displayMode: NoteDisplayMode.DocAndEdgeless,
},
children: [
{
type: 'block',
id: 'matchesReplaceMap[1]',
flavour: 'affine:paragraph',
props: {
type: 'text',
text: {
'$blocksuite:internal:text$': true,
delta: [
{
insert: 'Hello',
attributes: {
color: 'var(--affine-v2-text-highlight-fg-blue)',
},
},
],
},
},
children: [],
},
],
};
const mdAdapter = new MarkdownAdapter(createJob(), provider);
const rawBlockSnapshot = await mdAdapter.toBlockSnapshot({
file: markdown,
});
expect(nanoidReplacement(rawBlockSnapshot)).toEqual(blockSnapshot);
});
test('paragraph', async () => {
const markdown = `aaa

File diff suppressed because it is too large Load Diff

View File

@@ -17,18 +17,18 @@
"@blocksuite/affine-shared": "workspace:*",
"@blocksuite/affine-widget-slash-menu": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.17",
"@blocksuite/icons": "^2.2.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",
"@toeverything/theme": "^1.1.16",
"file-type": "^21.0.0",
"lit": "^3.2.0",
"minimatch": "^10.1.1",
"rxjs": "^7.8.2",
"zod": "^3.25.76"
"minimatch": "^10.0.1",
"rxjs": "^7.8.1",
"zod": "^3.23.8"
},
"exports": {
".": "./src/index.ts",
@@ -41,5 +41,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.26.1"
"version": "0.22.4"
}

View File

@@ -19,20 +19,20 @@
"@blocksuite/affine-shared": "workspace:*",
"@blocksuite/affine-widget-slash-menu": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.17",
"@blocksuite/icons": "^2.2.12",
"@blocksuite/std": "workspace:*",
"@blocksuite/store": "workspace:*",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.23",
"@toeverything/theme": "^1.1.16",
"lit": "^3.2.0",
"minimatch": "^10.1.1",
"rxjs": "^7.8.2",
"yjs": "^13.6.27",
"zod": "^3.25.76"
"minimatch": "^10.0.1",
"rxjs": "^7.8.1",
"yjs": "^13.6.23",
"zod": "^3.23.8"
},
"devDependencies": {
"vitest": "^3.2.4"
"vitest": "3.1.3"
},
"exports": {
".": "./src/index.ts",
@@ -45,5 +45,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.26.1"
"version": "0.22.4"
}

View File

@@ -18,21 +18,20 @@
"@blocksuite/affine-shared": "workspace:*",
"@blocksuite/affine-widget-slash-menu": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.17",
"@blocksuite/icons": "^2.2.12",
"@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",
"@toeverything/theme": "^1.1.16",
"@types/mdast": "^4.0.4",
"emoji-mart": "^5.6.0",
"lit": "^3.2.0",
"minimatch": "^10.1.1",
"rxjs": "^7.8.2",
"zod": "^3.25.76"
"minimatch": "^10.0.1",
"rxjs": "^7.8.1",
"zod": "^3.23.8"
},
"exports": {
".": "./src/index.ts",
@@ -45,5 +44,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.26.1"
"version": "0.22.4"
}

View File

@@ -1,56 +0,0 @@
import { css } from '@emotion/css';
export const calloutHostStyles = css({
display: 'block',
margin: '8px 0',
});
export const calloutBlockContainerStyles = css({
display: 'flex',
alignItems: 'flex-start',
padding: '5px 10px',
borderRadius: '8px',
});
export const calloutEmojiContainerStyles = css({
userSelect: 'none',
fontSize: '1.2em',
width: '24px',
height: '24px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
// marginTop is dynamically set by JavaScript based on first child's height
marginBottom: '10px',
flexShrink: 0,
position: 'relative',
});
export const calloutEmojiStyles = css({
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
':hover': {
cursor: 'pointer',
opacity: 0.7,
},
});
export const calloutChildrenStyles = css({
flex: 1,
minWidth: 0,
paddingLeft: '10px',
});
export const iconPickerContainerStyles = css({
position: 'absolute',
top: '100%',
left: 0,
zIndex: 1000,
background: 'white',
border: '1px solid #ccc',
borderRadius: '8px',
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)',
width: '390px',
height: '400px',
});

View File

@@ -1,191 +1,84 @@
import { CaptionedBlockComponent } from '@blocksuite/affine-components/caption';
import {
createPopup,
popupTargetFromElement,
} from '@blocksuite/affine-components/context-menu';
import { createLitPortal } from '@blocksuite/affine-components/portal';
import { DefaultInlineManagerExtension } from '@blocksuite/affine-inline-preset';
import {
type CalloutBlockModel,
type ParagraphBlockModel,
} from '@blocksuite/affine-model';
import { focusTextModel } from '@blocksuite/affine-rich-text';
import { type CalloutBlockModel } from '@blocksuite/affine-model';
import { EDGELESS_TOP_CONTENTEDITABLE_SELECTOR } from '@blocksuite/affine-shared/consts';
import {
DocModeProvider,
type IconData,
IconPickerServiceIdentifier,
IconType,
ThemeProvider,
} from '@blocksuite/affine-shared/services';
import type { UniComponent } from '@blocksuite/affine-shared/types';
import * as icons from '@blocksuite/icons/lit';
import { unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme';
import type { BlockComponent } from '@blocksuite/std';
import { type Signal } from '@preact/signals-core';
import { cssVarV2 } from '@toeverything/theme/v2';
import type { TemplateResult } from 'lit';
import { html } from 'lit';
import { type StyleInfo, styleMap } from 'lit/directives/style-map.js';
import {
calloutBlockContainerStyles,
calloutChildrenStyles,
calloutEmojiContainerStyles,
calloutEmojiStyles,
calloutHostStyles,
} from './callout-block-styles.js';
import { IconPickerWrapper } from './icon-picker-wrapper.js';
// Copy of renderUniLit and UniLit from affine-data-view
export const renderUniLit = <Props, Expose extends NonNullable<unknown>>(
uni: UniComponent<Props, Expose> | undefined,
props?: Props,
options?: {
ref?: Signal<Expose | undefined>;
style?: Readonly<StyleInfo>;
class?: string;
}
): TemplateResult => {
return html` <uni-lit
.uni="${uni}"
.props="${props}"
.ref="${options?.ref}"
style=${options?.style ? styleMap(options?.style) : ''}
></uni-lit>`;
};
const getIcon = (icon?: IconData) => {
if (!icon) {
return null;
}
if (icon.type === IconType.Emoji) {
return icon.unicode;
}
if (icon.type === IconType.AffineIcon) {
return (
icons as Record<string, (props: { style: string }) => TemplateResult>
)[`${icon.name}Icon`]?.({ style: `color:${icon.color}` });
}
return null;
};
import { flip, offset } from '@floating-ui/dom';
import { css, html } from 'lit';
import { query } from 'lit/decorators.js';
import { styleMap } from 'lit/directives/style-map.js';
export class CalloutBlockComponent extends CaptionedBlockComponent<CalloutBlockModel> {
private _popupCloseHandler: (() => void) | null = null;
override connectedCallback() {
super.connectedCallback();
this.classList.add(calloutHostStyles);
}
private _getEmojiMarginTop(): string {
if (this.model.children.length === 0) {
return '10px';
static override styles = css`
:host {
display: block;
margin: 8px 0;
}
const firstChild = this.model.children[0];
const flavour = firstChild.flavour;
const marginTopMap: Record<string, string> = {
'affine:paragraph:h1': '23px',
'affine:paragraph:h2': '20px',
'affine:paragraph:h3': '16px',
'affine:paragraph:h4': '15px',
'affine:paragraph:h5': '14px',
'affine:paragraph:h6': '13px',
};
// For heading blocks, use the type to determine margin
if (flavour === 'affine:paragraph') {
const paragraph = firstChild as ParagraphBlockModel;
const type = paragraph.props.type$.value;
const key = `${flavour}:${type}`;
return marginTopMap[key] || '10px';
.affine-callout-block-container {
display: flex;
padding: 5px 10px;
border-radius: 8px;
background-color: ${unsafeCSSVarV2('block/callout/background/grey')};
}
// Default for all other block types
return '10px';
}
private _closeIconPicker() {
if (this._popupCloseHandler) {
this._popupCloseHandler();
this._popupCloseHandler = null;
.affine-callout-emoji-container {
margin-right: 10px;
margin-top: 14px;
user-select: none;
font-size: 1.2em;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
}
}
private _toggleIconPicker(event: MouseEvent) {
// If popup is already open, close it
if (this._popupCloseHandler) {
this._closeIconPicker();
return;
.affine-callout-emoji:hover {
cursor: pointer;
opacity: 0.7;
}
// Get IconPickerService from the framework
const iconPickerService = this.std.getOptional(IconPickerServiceIdentifier);
if (!iconPickerService) {
console.warn('IconPickerService not found');
return;
.affine-callout-children {
flex: 1;
min-width: 0;
padding-left: 10px;
}
`;
// Get the uni-component from the service
const iconPickerComponent = iconPickerService.iconPickerComponent;
private _emojiMenuAbortController: AbortController | null = null;
private readonly _toggleEmojiMenu = () => {
if (this._emojiMenuAbortController) {
this._emojiMenuAbortController.abort();
}
this._emojiMenuAbortController = new AbortController();
// Create props for the icon picker
const props = {
onSelect: (iconData?: IconData) => {
this.model.props.icon$.value = iconData;
this._closeIconPicker(); // Close the picker after selection
const theme = this.std.get(ThemeProvider).theme$.value;
createLitPortal({
template: html`<affine-emoji-menu
.theme=${theme}
.onEmojiSelect=${(data: any) => {
this.model.props.emoji = data.native;
}}
></affine-emoji-menu>`,
portalStyles: {
zIndex: 'var(--affine-z-index-popover)',
},
onClose: () => {
this._closeIconPicker();
},
};
// Create IconPickerWrapper instance
const wrapper = new IconPickerWrapper();
wrapper.iconPickerComponent = iconPickerComponent;
wrapper.props = props;
wrapper.style.position = 'absolute';
wrapper.style.backgroundColor = cssVarV2.layer.background.overlayPanel;
wrapper.style.boxShadow = 'var(--affine-menu-shadow)';
wrapper.style.borderRadius = '8px';
// Create popup target from the clicked element
const target = popupTargetFromElement(event.currentTarget as HTMLElement);
// Create popup
this._popupCloseHandler = createPopup(target, wrapper, {
onClose: () => {
this._popupCloseHandler = null;
container: this.host,
computePosition: {
referenceElement: this._emojiButton,
placement: 'bottom-start',
middleware: [flip(), offset(4)],
autoUpdate: { animationFrame: true },
},
abortController: this._emojiMenuAbortController,
closeOnClickAway: true,
});
}
private readonly _handleBlockClick = (event: MouseEvent) => {
// Check if the click target is emoji related element
const target = event.target as HTMLElement;
if (
target.closest('.affine-callout-emoji-container') ||
target.classList.contains('affine-callout-emoji')
) {
return;
}
// If there's no icon, open icon picker on click
const icon = this.model.props.icon$.value;
if (!icon) {
this._toggleIconPicker(event);
return;
}
// Only handle clicks when there are no children
if (this.model.children.length > 0) {
return;
}
// Prevent event bubbling
event.stopPropagation();
// Create a new paragraph block
const paragraphId = this.store.addBlock('affine:paragraph', {}, this.model);
// Focus the new paragraph
focusTextModel(this.std, paragraphId);
};
get attributeRenderer() {
@@ -204,6 +97,9 @@ export class CalloutBlockComponent extends CaptionedBlockComponent<CalloutBlockM
return this.std.get(DefaultInlineManagerExtension.identifier);
}
@query('.affine-callout-emoji')
private accessor _emojiButton!: HTMLElement;
override get topContenteditableElement() {
if (this.std.get(DocModeProvider).getEditorMode() === 'edgeless') {
return this.closest<BlockComponent>(
@@ -214,39 +110,20 @@ export class CalloutBlockComponent extends CaptionedBlockComponent<CalloutBlockM
}
override renderBlock() {
const icon = this.model.props.icon$.value;
const backgroundColorName = this.model.props.backgroundColorName$.value;
const backgroundColor = (
cssVarV2.block.callout.background as Record<string, string>
)[backgroundColorName ?? ''];
const iconContent = getIcon(icon);
const emoji = this.model.props.emoji$.value;
return html`
<div
class="${calloutBlockContainerStyles}"
@click=${this._handleBlockClick}
style=${styleMap({
backgroundColor: backgroundColor ?? 'transparent',
})}
>
${iconContent
? html`
<div
@click=${this._toggleIconPicker}
contenteditable="false"
class="${calloutEmojiContainerStyles}"
style=${styleMap({
marginTop: this._getEmojiMarginTop(),
})}
>
<span class="${calloutEmojiStyles}" data-testid="callout-emoji"
>${iconContent}</span
>
</div>
`
: ''}
<div class="${calloutChildrenStyles}">
<div class="affine-callout-block-container">
<div
@click=${this._toggleEmojiMenu}
contenteditable="false"
class="affine-callout-emoji-container"
style=${styleMap({
display: emoji.length === 0 ? 'none' : undefined,
})}
>
<span class="affine-callout-emoji">${emoji}</span>
</div>
<div class="affine-callout-children">
${this.renderChildren(this.model)}
</div>
</div>

View File

@@ -1,7 +1,4 @@
import {
CalloutBlockModel,
ParagraphBlockModel,
} from '@blocksuite/affine-model';
import { CalloutBlockModel } from '@blocksuite/affine-model';
import { matchModels } from '@blocksuite/affine-shared/utils';
import {
BlockSelection,
@@ -9,46 +6,13 @@ import {
TextSelection,
} from '@blocksuite/std';
import { calloutToParagraphCommand } from './commands/callout-to-paragraph.js';
import { splitCalloutCommand } from './commands/split-callout.js';
export const CalloutKeymapExtension = KeymapExtension(std => {
return {
Enter: ctx => {
const text = std.selection.find(TextSelection);
if (!text) return false;
const currentBlock = std.store.getBlock(text.from.blockId);
if (!currentBlock) return false;
// Check if current block is a callout block
let calloutBlock = currentBlock;
if (!matchModels(currentBlock.model, [CalloutBlockModel])) {
// If not, check if the parent is a callout block
const parent = std.store.getParent(currentBlock.model);
if (!parent || !matchModels(parent, [CalloutBlockModel])) {
return false;
}
const parentBlock = std.store.getBlock(parent.id);
if (!parentBlock) return false;
calloutBlock = parentBlock;
}
ctx.get('keyboardState').raw.preventDefault();
std.command
.chain()
.pipe(splitCalloutCommand, {
blockId: calloutBlock.model.id,
inlineIndex: text.from.index,
currentBlockId: text.from.blockId,
})
.run();
return true;
},
Backspace: ctx => {
const text = std.selection.find(TextSelection);
if (text && text.isCollapsed() && text.from.index === 0) {
const event = ctx.get('defaultState').event;
event.preventDefault();
const block = std.store.getBlock(text.from.blockId);
if (!block) return false;
@@ -56,22 +20,6 @@ export const CalloutKeymapExtension = KeymapExtension(std => {
if (!parent) return false;
if (!matchModels(parent, [CalloutBlockModel])) return false;
// Check if current block is a paragraph inside callout
if (matchModels(block.model, [ParagraphBlockModel])) {
event.preventDefault();
std.command
.chain()
.pipe(calloutToParagraphCommand, {
id: block.model.id,
})
.run();
return true;
}
// Fallback to selecting the callout block
event.preventDefault();
std.selection.setGroup('note', [
std.selection.create(BlockSelection, {
blockId: parent.id,

View File

@@ -1,86 +0,0 @@
import {
CalloutBlockModel,
ParagraphBlockModel,
} from '@blocksuite/affine-model';
import { focusTextModel } from '@blocksuite/affine-rich-text';
import { matchModels } from '@blocksuite/affine-shared/utils';
import type { Command } from '@blocksuite/std';
import { BlockSelection } from '@blocksuite/std';
import { Text } from '@blocksuite/store';
export const calloutToParagraphCommand: Command<
{
id: string;
stopCapturing?: boolean;
},
{
success: boolean;
}
> = (ctx, next) => {
const { id, stopCapturing = true } = ctx;
const std = ctx.std;
const doc = std.store;
const model = doc.getBlock(id)?.model;
if (!model || !matchModels(model, [ParagraphBlockModel])) return false;
const parent = doc.getParent(model);
if (!parent || !matchModels(parent, [CalloutBlockModel])) return false;
if (stopCapturing) std.store.captureSync();
// Get current block index in callout
const currentIndex = parent.children.indexOf(model);
const hasText = model.text && model.text.length > 0;
// Find previous paragraph block in callout
let previousBlock = null;
for (let i = currentIndex - 1; i >= 0; i--) {
const sibling = parent.children[i];
if (matchModels(sibling, [ParagraphBlockModel])) {
previousBlock = sibling;
break;
}
}
if (previousBlock && hasText) {
// Clone current text content before any operations to prevent data loss
const currentText = model.text || new Text();
// Get previous block text and merge index
const previousText = previousBlock.text || new Text();
const mergeIndex = previousText.length;
// Apply each delta from cloned current text to previous block to preserve formatting
previousText.join(currentText);
// Remove current block after text has been merged
doc.deleteBlock(model, {
deleteChildren: false,
});
// Focus at merge point in previous block
focusTextModel(std, previousBlock.id, mergeIndex);
} else if (previousBlock && !hasText) {
// Move cursor to end of previous block
doc.deleteBlock(model, {
deleteChildren: false,
});
const previousText = previousBlock.text || new Text();
focusTextModel(std, previousBlock.id, previousText.length);
} else {
// No previous block, select the entire callout
doc.deleteBlock(model, {
deleteChildren: false,
});
std.selection.setGroup('note', [
std.selection.create(BlockSelection, {
blockId: parent.id,
}),
]);
}
return next({ success: true });
};

View File

@@ -1,85 +0,0 @@
import {
CalloutBlockModel,
ParagraphBlockModel,
} from '@blocksuite/affine-model';
import { focusTextModel } from '@blocksuite/affine-rich-text';
import { matchModels } from '@blocksuite/affine-shared/utils';
import type { Command, EditorHost } from '@blocksuite/std';
export const splitCalloutCommand: Command<{
blockId: string;
inlineIndex: number;
currentBlockId: string;
}> = (ctx, next) => {
const { blockId, inlineIndex, currentBlockId, std } = ctx;
const host = std.host as EditorHost;
const doc = host.store;
const calloutModel = doc.getBlock(blockId)?.model;
if (!calloutModel || !matchModels(calloutModel, [CalloutBlockModel])) {
console.error(`block ${blockId} is not a callout block`);
return;
}
const currentModel = doc.getBlock(currentBlockId)?.model;
if (!currentModel) {
console.error(`current block ${currentBlockId} not found`);
return;
}
doc.captureSync();
if (matchModels(currentModel, [ParagraphBlockModel])) {
// User is in a paragraph within the callout's children
const afterText = currentModel.props.text.split(inlineIndex);
// Update the current paragraph's text to keep only the part before cursor
doc.transact(() => {
currentModel.props.text.delete(
inlineIndex,
currentModel.props.text.length - inlineIndex
);
});
// Create a new paragraph block after the current one
const parent = doc.getParent(currentModel);
if (parent) {
const currentIndex = parent.children.indexOf(currentModel);
const newParagraphId = doc.addBlock(
'affine:paragraph',
{
text: afterText,
},
parent,
currentIndex + 1
);
if (newParagraphId) {
host.updateComplete
.then(() => {
focusTextModel(std, newParagraphId);
})
.catch(console.error);
}
}
} else {
// If current block is not a paragraph, create a new paragraph in callout
const newParagraphId = doc.addBlock(
'affine:paragraph',
{
text: new Text(),
},
calloutModel
);
if (newParagraphId) {
host.updateComplete
.then(() => {
focusTextModel(std, newParagraphId);
})
.catch(console.error);
}
}
next();
};

View File

@@ -1,11 +1,24 @@
import { CalloutBlockModel } from '@blocksuite/affine-model';
import { focusBlockEnd } from '@blocksuite/affine-shared/commands';
import { isInsideBlockByFlavour } from '@blocksuite/affine-shared/utils';
import { FeatureFlagService } from '@blocksuite/affine-shared/services';
import {
findAncestorModel,
isInsideBlockByFlavour,
matchModels,
} from '@blocksuite/affine-shared/utils';
import { type SlashMenuConfig } from '@blocksuite/affine-widget-slash-menu';
import { FontIcon } from '@blocksuite/icons/lit';
import { calloutTooltip } from './tooltips';
export const calloutSlashMenuConfig: SlashMenuConfig = {
disableWhen: ({ model }) => {
return (
findAncestorModel(model, ancestor =>
matchModels(ancestor, [CalloutBlockModel])
) !== null
);
},
items: [
{
name: 'Callout',
@@ -17,11 +30,10 @@ export const calloutSlashMenuConfig: SlashMenuConfig = {
},
searchAlias: ['callout'],
group: '0_Basic@9',
when: ({ model }) => {
return !isInsideBlockByFlavour(
model.store,
model,
'affine:edgeless-text'
when: ({ std, model }) => {
return (
std.get(FeatureFlagService).getFlag('enable_callout') &&
!isInsideBlockByFlavour(model.store, model, 'affine:edgeless-text')
);
},
action: ({ model, std }) => {

View File

@@ -1,204 +0,0 @@
import {
createPopup,
popupTargetFromElement,
} from '@blocksuite/affine-components/context-menu';
import { EditorChevronDown } from '@blocksuite/affine-components/toolbar';
import { CalloutBlockModel } from '@blocksuite/affine-model';
import {
ActionPlacement,
type IconData,
IconPickerServiceIdentifier,
type ToolbarAction,
type ToolbarActionGroup,
type ToolbarModuleConfig,
ToolbarModuleExtension,
} from '@blocksuite/affine-shared/services';
import { DeleteIcon, PaletteIcon, SmileIcon } from '@blocksuite/icons/lit';
import { BlockFlavourIdentifier } from '@blocksuite/std';
import type { ExtensionType } from '@blocksuite/store';
import { cssVarV2 } from '@toeverything/theme/v2';
import { html } from 'lit';
import { repeat } from 'lit/directives/repeat.js';
import { styleMap } from 'lit/directives/style-map.js';
import { IconPickerWrapper } from '../icon-picker-wrapper.js';
const colors = [
'default',
'red',
'orange',
'yellow',
'green',
'teal',
'blue',
'purple',
'grey',
] as const;
const backgroundColorAction = {
id: 'background-color',
label: 'Background Color',
tooltip: 'Change background color',
icon: PaletteIcon(),
run() {
// This will be handled by the content function
},
content(ctx) {
const model = ctx.getCurrentModelByType(CalloutBlockModel);
if (!model) return null;
const updateBackground = (color: string) => {
ctx.store.updateBlock(model, { backgroundColorName: color });
};
return html`
<editor-menu-button
.contentPadding=${'8px'}
.button=${html`
<editor-icon-button
aria-label="background"
.tooltip=${'Background Color'}
>
${PaletteIcon()} ${EditorChevronDown}
</editor-icon-button>
`}
>
<div data-size="large" data-orientation="vertical">
<div class="highlight-heading">Background</div>
${repeat(colors, color => {
const isDefault = color === 'default';
const value = isDefault
? null
: `var(--affine-text-highlight-${color})`;
const displayName = `${color} Background`;
return html`
<editor-menu-action
data-testid="background-${color}"
@click=${() => updateBackground(color)}
>
<affine-text-duotone-icon
style=${styleMap({
'--color': 'var(--affine-text-primary-color)',
'--background': value ?? 'transparent',
})}
></affine-text-duotone-icon>
<span class="label capitalize">${displayName}</span>
</editor-menu-action>
`;
})}
</div>
</editor-menu-button>
`;
},
} satisfies ToolbarAction;
const iconPickerAction = {
id: 'icon-picker',
label: 'Icon Picker',
tooltip: 'Change icon',
icon: SmileIcon(),
run() {
// This will be handled by the content function
},
content(ctx) {
const model = ctx.getCurrentModelByType(CalloutBlockModel);
if (!model) return null;
const handleIconPickerClick = (event: MouseEvent) => {
// Get IconPickerService from the framework
const iconPickerService = ctx.std.getOptional(
IconPickerServiceIdentifier
);
if (!iconPickerService) {
console.warn('IconPickerService not found');
return;
}
// Get the uni-component from the service
const iconPickerComponent = iconPickerService.iconPickerComponent;
// Create props for the icon picker
const props = {
onSelect: (iconData?: IconData) => {
// When iconData is undefined (delete icon), set icon to undefined
ctx.store.updateBlock(model, { icon: iconData });
closeHandler(); // Close the picker after selection
},
onClose: () => {
closeHandler();
},
};
// Create IconPickerWrapper instance
const wrapper = new IconPickerWrapper();
wrapper.iconPickerComponent = iconPickerComponent;
wrapper.props = props;
wrapper.style.position = 'absolute';
wrapper.style.backgroundColor = cssVarV2.layer.background.overlayPanel;
wrapper.style.boxShadow = 'var(--affine-menu-shadow)';
wrapper.style.borderRadius = '8px';
// Create popup target from the clicked element
const target = popupTargetFromElement(event.currentTarget as HTMLElement);
// Create popup
const closeHandler = createPopup(target, wrapper, {
onClose: () => {
// Cleanup if needed
},
});
};
return html`
<editor-icon-button
aria-label="icon-picker"
.tooltip=${'Change Icon'}
@click=${handleIconPickerClick}
>
${SmileIcon()} ${EditorChevronDown}
</editor-icon-button>
`;
},
} satisfies ToolbarAction;
const builtinToolbarConfig = {
actions: [
{
id: 'style',
actions: [backgroundColorAction],
} satisfies ToolbarActionGroup<ToolbarAction>,
{
id: 'icon',
actions: [iconPickerAction],
} satisfies ToolbarActionGroup<ToolbarAction>,
{
placement: ActionPlacement.More,
id: 'c.delete',
label: 'Delete',
icon: DeleteIcon(),
variant: 'destructive',
run(ctx) {
const model = ctx.getCurrentModelByType(CalloutBlockModel);
if (!model) return;
ctx.store.deleteBlock(model);
// Clears
ctx.select('note');
ctx.reset();
},
} satisfies ToolbarAction,
],
} as const satisfies ToolbarModuleConfig;
export const createBuiltinToolbarConfigExtension = (
flavour: string
): ExtensionType[] => {
return [
ToolbarModuleExtension({
id: BlockFlavourIdentifier(flavour),
config: builtinToolbarConfig,
}),
];
};

View File

@@ -1,14 +1,14 @@
import { CalloutBlockComponent } from './callout-block';
import { IconPickerWrapper } from './icon-picker-wrapper';
import { EmojiMenu } from './emoji-menu';
export function effects() {
customElements.define('affine-callout', CalloutBlockComponent);
customElements.define('icon-picker-wrapper', IconPickerWrapper);
customElements.define('affine-emoji-menu', EmojiMenu);
}
declare global {
interface HTMLElementTagNameMap {
'affine-callout': CalloutBlockComponent;
'icon-picker-wrapper': IconPickerWrapper;
'affine-emoji-menu': EmojiMenu;
}
}

View File

@@ -0,0 +1,34 @@
import { WithDisposable } from '@blocksuite/global/lit';
import data from '@emoji-mart/data';
import { Picker } from 'emoji-mart';
import { html, LitElement, type PropertyValues } from 'lit';
import { property, query } from 'lit/decorators.js';
export class EmojiMenu extends WithDisposable(LitElement) {
override firstUpdated(props: PropertyValues) {
const result = super.firstUpdated(props);
const picker = new Picker({
data,
onEmojiSelect: this.onEmojiSelect,
autoFocus: true,
theme: this.theme,
});
this.emojiMenu.append(picker as unknown as Node);
return result;
}
@property({ attribute: false })
accessor onEmojiSelect: (data: any) => void = () => {};
@property({ attribute: false })
accessor theme: 'light' | 'dark' = 'light';
@query('.affine-emoji-menu')
accessor emojiMenu!: HTMLElement;
override render() {
return html`<div class="affine-emoji-menu"></div>`;
}
}

View File

@@ -1,52 +0,0 @@
import type { IconData } from '@blocksuite/affine-shared/services';
import type { UniComponent } from '@blocksuite/affine-shared/types';
import { ShadowlessElement } from '@blocksuite/std';
import { type Signal } from '@preact/signals-core';
import { html, type TemplateResult } from 'lit';
import { type StyleInfo, styleMap } from 'lit/directives/style-map.js';
// Copy of renderUniLit from callout-block.ts
const renderUniLit = <Props, Expose extends NonNullable<unknown>>(
uni: UniComponent<Props, Expose> | undefined,
props?: Props,
options?: {
ref?: Signal<Expose | undefined>;
style?: Readonly<StyleInfo>;
class?: string;
}
): TemplateResult => {
return html` <uni-lit
.uni="${uni}"
.props="${props}"
.ref="${options?.ref}"
style=${options?.style ? styleMap(options?.style) : ''}
></uni-lit>`;
};
export interface IconPickerWrapperProps {
onSelect?: (iconData?: IconData) => void;
onClose?: () => void;
}
export class IconPickerWrapper extends ShadowlessElement {
iconPickerComponent?: UniComponent<IconPickerWrapperProps, any>;
props?: IconPickerWrapperProps;
constructor() {
super();
}
override render() {
if (!this.iconPickerComponent) {
return html``;
}
return renderUniLit(this.iconPickerComponent, this.props);
}
}
declare global {
interface HTMLElementTagNameMap {
'icon-picker-wrapper': IconPickerWrapper;
}
}

View File

@@ -8,7 +8,6 @@ import { literal } from 'lit/static-html.js';
import { CalloutKeymapExtension } from './callout-keymap';
import { calloutSlashMenuConfig } from './configs/slash-menu';
import { createBuiltinToolbarConfigExtension } from './configs/toolbar';
import { effects } from './effects';
export class CalloutViewExtension extends ViewExtensionProvider {
@@ -26,7 +25,6 @@ export class CalloutViewExtension extends ViewExtensionProvider {
BlockViewExtension('affine:callout', literal`affine-callout`),
CalloutKeymapExtension,
SlashMenuConfigExtension('affine:callout', calloutSlashMenuConfig),
...createBuiltinToolbarConfigExtension('affine:callout'),
]);
}
}

View File

@@ -22,19 +22,19 @@
"@blocksuite/affine-shared": "workspace:*",
"@blocksuite/affine-widget-slash-menu": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.17",
"@blocksuite/icons": "^2.2.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",
"@toeverything/theme": "^1.1.16",
"@types/mdast": "^4.0.4",
"lit": "^3.2.0",
"minimatch": "^10.1.1",
"rxjs": "^7.8.2",
"shiki": "^3.19.0",
"zod": "^3.25.76"
"minimatch": "^10.0.1",
"rxjs": "^7.8.1",
"shiki": "^3.0.0",
"zod": "^3.23.8"
},
"exports": {
".": "./src/index.ts",
@@ -48,5 +48,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.26.1"
"version": "0.22.4"
}

View File

@@ -1,4 +1,6 @@
export const CODE_BLOCK_DEFAULT_DARK_THEME =
import('shiki/themes/dark-plus.mjs');
export const CODE_BLOCK_DEFAULT_LIGHT_THEME =
import('shiki/themes/light-plus.mjs');
export const CODE_BLOCK_DEFAULT_DARK_THEME = import(
'shiki/themes/dark-plus.mjs'
);
export const CODE_BLOCK_DEFAULT_LIGHT_THEME = import(
'shiki/themes/light-plus.mjs'
);

View File

@@ -18,18 +18,18 @@
"@blocksuite/affine-widget-slash-menu": "workspace:*",
"@blocksuite/data-view": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.17",
"@blocksuite/icons": "^2.2.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",
"@toeverything/theme": "^1.1.16",
"@types/mdast": "^4.0.4",
"lit": "^3.2.0",
"minimatch": "^10.1.1",
"rxjs": "^7.8.2",
"zod": "^3.25.76"
"minimatch": "^10.0.1",
"rxjs": "^7.8.1",
"zod": "^3.23.8"
},
"exports": {
".": "./src/index.ts",
@@ -42,5 +42,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.26.1"
"version": "0.22.4"
}

View File

@@ -21,21 +21,21 @@
"@blocksuite/affine-widget-slash-menu": "workspace:*",
"@blocksuite/data-view": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.17",
"@blocksuite/icons": "^2.2.12",
"@blocksuite/std": "workspace:*",
"@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",
"@toeverything/theme": "^1.1.16",
"@types/mdast": "^4.0.4",
"date-fns": "^4.0.0",
"lit": "^3.2.0",
"minimatch": "^10.1.1",
"rxjs": "^7.8.2",
"yjs": "^13.6.27",
"zod": "^3.25.76"
"minimatch": "^10.0.1",
"rxjs": "^7.8.1",
"yjs": "^13.6.21",
"zod": "^3.23.8"
},
"exports": {
".": "./src/index.ts",
@@ -48,5 +48,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.26.1"
"version": "0.22.4"
}

View File

@@ -15,7 +15,6 @@ const ColumnClassMap: Record<string, string> = {
typesCheckbox: 'checkbox',
typesText: 'rich-text',
typesTitle: 'title',
typesDate: 'date',
};
const NotionDatabaseToken = '.collection-content';
@@ -166,36 +165,7 @@ export const databaseBlockNotionHtmlAdapterMatcher: BlockNotionHtmlAdapterMatche
if (!column) {
return;
}
// Check for <time> element to find date field from Notion.
if (HastUtils.querySelector(child, 'time')) {
const timeElement = HastUtils.querySelector(child, 'time');
let rawColumnData =
HastUtils.getTextContent(timeElement).trim();
if (rawColumnData.startsWith('@')) {
rawColumnData = rawColumnData.slice(1);
}
const columnDate = new Date(rawColumnData);
const timestamp = columnDate.getTime();
if (!Number.isNaN(timestamp)) {
column.data = {};
if (column.type !== 'date') {
column.type = 'date';
}
row[column.id] = {
columnId: column.id,
value: timestamp,
};
} else {
row[column.id] = {
columnId: column.id,
value: HastUtils.getTextContent(child),
};
}
} else if (HastUtils.querySelector(child, '.selected-value')) {
if (HastUtils.querySelector(child, '.selected-value')) {
if (!('options' in column.data)) {
column.data.options = [];
}

View File

@@ -176,7 +176,7 @@ export class DatabaseTitle extends SignalWatcher(
private readonly isFocus$ = signal(false);
private onPressEnterKey() {
this.input.blur();
this.dataViewLogic.addRow?.('start');
}
get readonly$() {

View File

@@ -21,12 +21,12 @@
"@floating-ui/dom": "^1.6.13",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.23",
"@toeverything/theme": "^1.1.16",
"@types/mdast": "^4.0.4",
"lit": "^3.2.0",
"minimatch": "^10.1.1",
"rxjs": "^7.8.2",
"zod": "^3.25.76"
"minimatch": "^10.0.1",
"rxjs": "^7.8.1",
"zod": "^3.23.8"
},
"exports": {
".": "./src/index.ts",
@@ -39,5 +39,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.26.1"
"version": "0.22.4"
}

View File

@@ -20,17 +20,17 @@
"@blocksuite/affine-shared": "workspace:*",
"@blocksuite/affine-widget-edgeless-toolbar": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.17",
"@blocksuite/icons": "^2.2.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",
"@toeverything/theme": "^1.1.16",
"lit": "^3.2.0",
"minimatch": "^10.1.1",
"rxjs": "^7.8.2",
"zod": "^3.25.76"
"minimatch": "^10.0.1",
"rxjs": "^7.8.1",
"zod": "^3.23.8"
},
"exports": {
".": "./src/index.ts",
@@ -43,5 +43,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.26.1"
"version": "0.22.4"
}

View File

@@ -20,23 +20,23 @@
"@blocksuite/affine-shared": "workspace:*",
"@blocksuite/affine-widget-slash-menu": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.17",
"@blocksuite/icons": "^2.2.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",
"@toeverything/theme": "^1.1.16",
"@types/lodash-es": "^4.17.12",
"lit": "^3.2.0",
"lodash-es": "^4.17.23",
"minimatch": "^10.1.1",
"rxjs": "^7.8.2",
"yjs": "^13.6.27",
"zod": "^3.25.76"
"lodash-es": "^4.17.21",
"minimatch": "^10.0.1",
"rxjs": "^7.8.1",
"yjs": "^13.6.21",
"zod": "^3.23.8"
},
"devDependencies": {
"vitest": "^3.2.4"
"vitest": "3.1.3"
},
"exports": {
".": "./src/index.ts",
@@ -49,5 +49,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.26.1"
"version": "0.22.4"
}

View File

@@ -323,8 +323,7 @@ export class EmbedLinkedDocBlockComponent extends EmbedBlockComponent<EmbedLinke
private readonly _renderEmbedView = () => {
const linkedDoc = this.linkedDoc;
const trash = linkedDoc?.meta?.trash;
const isDeleted = trash || !linkedDoc;
const isDeleted = !linkedDoc;
const isLoading = this._loading;
const isError = this.isError;
const isEmpty = this._isDocEmpty() && this.isBannerEmpty;
@@ -522,6 +521,11 @@ export class EmbedLinkedDocBlockComponent extends EmbedBlockComponent<EmbedLinke
);
this._setDocUpdatedAt();
this.disposables.add(
this.store.workspace.slots.docListUpdated.subscribe(() => {
this._setDocUpdatedAt();
})
);
if (this._referenceToNode) {
this._linkedDocMode = this.model.props.params?.mode ?? 'page';
@@ -550,13 +554,6 @@ export class EmbedLinkedDocBlockComponent extends EmbedBlockComponent<EmbedLinke
})
);
this.disposables.add(
this.store.workspace.slots.docListUpdated.subscribe(() => {
this._setDocUpdatedAt();
this.refreshData();
})
);
this._trackCitationDeleteEvent();
}

View File

@@ -56,9 +56,6 @@ export class EmbedSyncedDocBlockComponent extends EmbedBlockComponent<EmbedSynce
// Caches total bounds, includes all blocks and elements.
private _cachedBounds: Bound | null = null;
private _hasRenderedSyncedView = false;
private _hasInitedFitEffect = false;
private readonly _initEdgelessFitEffect = () => {
const fitToContent = () => {
if (this.isPageMode) return;
@@ -360,14 +357,10 @@ export class EmbedSyncedDocBlockComponent extends EmbedBlockComponent<EmbedSynce
};
refreshData = () => {
this._load()
.then(() => {
this._isEmptySyncedDoc = isEmptyDoc(this.syncedDoc, this.editorMode);
})
.catch(e => {
console.error(e);
this._error = true;
});
this._load().catch(e => {
console.error(e);
this._error = true;
});
};
title$ = computed(() => {
@@ -452,8 +445,7 @@ export class EmbedSyncedDocBlockComponent extends EmbedBlockComponent<EmbedSynce
this._cycle = false;
const syncedDoc = this.syncedDoc;
const trash = syncedDoc?.meta?.trash;
if (trash || !syncedDoc) {
if (!syncedDoc) {
this._deleted = true;
this._loading = false;
return;
@@ -529,7 +521,6 @@ export class EmbedSyncedDocBlockComponent extends EmbedBlockComponent<EmbedSynce
this.disposables.add(
this.store.workspace.slots.docListUpdated.subscribe(() => {
this._setDocUpdatedAt();
this.refreshData();
})
);
@@ -561,6 +552,8 @@ export class EmbedSyncedDocBlockComponent extends EmbedBlockComponent<EmbedSynce
this._selectBlock();
}
});
this._initEdgelessFitEffect();
}
override renderBlock() {
@@ -588,21 +581,12 @@ export class EmbedSyncedDocBlockComponent extends EmbedBlockComponent<EmbedSynce
);
}
!this._hasRenderedSyncedView && (this._hasRenderedSyncedView = true);
return this._renderSyncedView();
}
override updated(changedProperties: PropertyValues) {
super.updated(changedProperties);
this.syncedDocCard?.requestUpdate();
if (!this._hasInitedFitEffect && this._hasRenderedSyncedView) {
/* Register the resizeObserver AFTER syncdView viewport's own resizeObserver
* so that viewport.onResize() use up-to-date boundingClientRect values */
this._hasInitedFitEffect = true;
this._initEdgelessFitEffect();
}
}
@state()

View File

@@ -20,23 +20,23 @@
"@blocksuite/affine-shared": "workspace:*",
"@blocksuite/affine-widget-slash-menu": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.17",
"@blocksuite/icons": "^2.2.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",
"@toeverything/theme": "^1.1.16",
"@types/lodash-es": "^4.17.12",
"lit": "^3.2.0",
"lodash-es": "^4.17.23",
"minimatch": "^10.1.1",
"rxjs": "^7.8.2",
"yjs": "^13.6.27",
"zod": "^3.25.76"
"lodash-es": "^4.17.21",
"minimatch": "^10.0.1",
"rxjs": "^7.8.1",
"yjs": "^13.6.21",
"zod": "^3.23.8"
},
"devDependencies": {
"vitest": "^3.2.4"
"vitest": "3.1.3"
},
"exports": {
".": "./src/index.ts",
@@ -49,5 +49,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.26.1"
"version": "0.22.4"
}

View File

@@ -82,8 +82,7 @@ export class EmbedFigmaBlockComponent extends EmbedBlockComponent<EmbedFigmaMode
<div class="affine-embed-figma-iframe-container">
<iframe
src=${`https://www.figma.com/embed?embed_host=blocksuite&url=${url}`}
sandbox="allow-same-origin allow-scripts allow-presentation"
allow="fullscreen"
allowfullscreen
loading="lazy"
credentialless
></iframe>

View File

@@ -1,79 +0,0 @@
import { EmbedIframeConfigExtension } from '@blocksuite/affine-shared/services';
import {
type EmbedIframeUrlValidationOptions,
validateEmbedIframeUrl,
} from '../../utils';
const BILIBILI_DEFAULT_WIDTH_IN_SURFACE = 800;
const BILIBILI_DEFAULT_HEIGHT_IN_SURFACE = 450;
const BILIBILI_DEFAULT_HEIGHT_IN_NOTE = 450;
const BILIBILI_DEFAULT_WIDTH_PERCENT = 100;
const bilibiliValidationOptions: EmbedIframeUrlValidationOptions = {
protocols: ['https:'],
hostnames: ['player.bilibili.com', 'www.bilibili.com', 'bilibili.com'],
};
const biliPlayerValidationOptions: EmbedIframeUrlValidationOptions = {
protocols: ['https:'],
hostnames: ['player.bilibili.com'],
};
const AV_REGEX = /av([0-9]+)/i;
const BV_REGEX = /(BV[0-9A-Za-z]{10})/;
const extractAvid = (url: string) => {
const match = url.match(AV_REGEX);
return match ? match[1] : undefined;
};
const extractBvid = (url: string) => {
const match = url.match(BV_REGEX);
return match ? match[1] : undefined;
};
const buildBiliPlayerEmbedUrl = (url: string) => {
// If the user pasted the embed URL directly, keep it
if (validateEmbedIframeUrl(url, biliPlayerValidationOptions)) {
return url;
}
const avid = extractAvid(url);
if (avid) {
const params = new URLSearchParams({
aid: avid,
autoplay: '0',
});
return `https://player.bilibili.com/player.html?${params.toString()}`;
}
const bvid = extractBvid(url);
if (bvid) {
const params = new URLSearchParams({
bvid,
autoplay: '0',
});
return `https://player.bilibili.com/player.html?${params.toString()}`;
}
return undefined;
};
const bilibiliConfig = {
name: 'bilibili',
match: (url: string) =>
validateEmbedIframeUrl(url, bilibiliValidationOptions) &&
(!!extractAvid(url) || !!extractBvid(url)),
buildOEmbedUrl: buildBiliPlayerEmbedUrl,
useOEmbedUrlDirectly: true,
options: {
widthInSurface: BILIBILI_DEFAULT_WIDTH_IN_SURFACE,
heightInSurface: BILIBILI_DEFAULT_HEIGHT_IN_SURFACE,
heightInNote: BILIBILI_DEFAULT_HEIGHT_IN_NOTE,
widthPercent: BILIBILI_DEFAULT_WIDTH_PERCENT,
allow: 'clipboard-write; encrypted-media; picture-in-picture',
sandbox: 'allow-same-origin allow-scripts',
style: 'border: none; border-radius: 8px;',
allowFullscreen: true,
},
};
export const BilibiliEmbedConfig = EmbedIframeConfigExtension(bilibiliConfig);

View File

@@ -10,6 +10,7 @@ const GENERIC_DEFAULT_HEIGHT_IN_NOTE = 400;
* These are based on the centralized cloud constants and known AFFiNE domains
*/
const AFFINE_DOMAINS = [
'affine.pro', // Main AFFiNE domain
'app.affine.pro', // Stable cloud domain
'insider.affine.pro', // Beta/internal cloud domain
'affine.fail', // Canary cloud domain
@@ -66,9 +67,8 @@ const genericConfig = {
heightInNote: GENERIC_DEFAULT_HEIGHT_IN_NOTE,
allowFullscreen: true,
style: 'border: none; border-radius: 8px;',
allow: '',
allow: 'clipboard-read; clipboard-write; picture-in-picture;',
referrerpolicy: 'no-referrer-when-downgrade',
sandbox: 'allow-scripts',
},
};

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