feat: init renderer server (#8088)

This commit is contained in:
Brooooooklyn
2024-09-10 04:03:58 +00:00
parent 0add8917f9
commit fe1eefdbb2
52 changed files with 827 additions and 330 deletions

View File

@@ -90,9 +90,14 @@ const createHelmCommand = ({ isDryRun }) => {
const deployCommand = [ const deployCommand = [
`helm upgrade --install affine .github/helm/affine`, `helm upgrade --install affine .github/helm/affine`,
`--namespace ${namespace}`, `--namespace ${namespace}`,
`--set-string global.app.buildType="${buildType}"`,
`--set global.ingress.enabled=true`, `--set global.ingress.enabled=true`,
`--set-json global.ingress.annotations=\"{ \\"kubernetes.io/ingress.class\\": \\"gce\\", \\"kubernetes.io/ingress.allow-http\\": \\"true\\", \\"kubernetes.io/ingress.global-static-ip-name\\": \\"${STATIC_IP_NAME}\\" }\"`, `--set-json global.ingress.annotations=\"{ \\"kubernetes.io/ingress.class\\": \\"gce\\", \\"kubernetes.io/ingress.allow-http\\": \\"true\\", \\"kubernetes.io/ingress.global-static-ip-name\\": \\"${STATIC_IP_NAME}\\" }\"`,
`--set-string global.ingress.host="${host}"`, `--set-string global.ingress.host="${host}"`,
`--set global.objectStorage.r2.enabled=true`,
`--set-string global.objectStorage.r2.accountId="${R2_ACCOUNT_ID}"`,
`--set-string global.objectStorage.r2.accessKeyId="${R2_ACCESS_KEY_ID}"`,
`--set-string global.objectStorage.r2.secretAccessKey="${R2_SECRET_ACCESS_KEY}"`,
`--set-string global.version="${APP_VERSION}"`, `--set-string global.version="${APP_VERSION}"`,
...redisAndPostgres, ...redisAndPostgres,
`--set web.replicaCount=${webReplicaCount}`, `--set web.replicaCount=${webReplicaCount}`,
@@ -106,10 +111,6 @@ const createHelmCommand = ({ isDryRun }) => {
`--set-string graphql.app.copilot.openai.key="${COPILOT_OPENAI_API_KEY}"`, `--set-string graphql.app.copilot.openai.key="${COPILOT_OPENAI_API_KEY}"`,
`--set-string graphql.app.copilot.fal.key="${COPILOT_FAL_API_KEY}"`, `--set-string graphql.app.copilot.fal.key="${COPILOT_FAL_API_KEY}"`,
`--set-string graphql.app.copilot.unsplash.key="${COPILOT_UNSPLASH_API_KEY}"`, `--set-string graphql.app.copilot.unsplash.key="${COPILOT_UNSPLASH_API_KEY}"`,
`--set graphql.app.objectStorage.r2.enabled=true`,
`--set-string graphql.app.objectStorage.r2.accountId="${R2_ACCOUNT_ID}"`,
`--set-string graphql.app.objectStorage.r2.accessKeyId="${R2_ACCESS_KEY_ID}"`,
`--set-string graphql.app.objectStorage.r2.secretAccessKey="${R2_SECRET_ACCESS_KEY}"`,
`--set-string graphql.app.mailer.sender="${MAILER_SENDER}"`, `--set-string graphql.app.mailer.sender="${MAILER_SENDER}"`,
`--set-string graphql.app.mailer.user="${MAILER_USER}"`, `--set-string graphql.app.mailer.user="${MAILER_USER}"`,
`--set-string graphql.app.mailer.password="${MAILER_PASSWORD}"`, `--set-string graphql.app.mailer.password="${MAILER_PASSWORD}"`,
@@ -125,6 +126,8 @@ const createHelmCommand = ({ isDryRun }) => {
`--set graphql.app.features.syncClientVersionCheck=true`, `--set graphql.app.features.syncClientVersionCheck=true`,
`--set sync.replicaCount=${syncReplicaCount}`, `--set sync.replicaCount=${syncReplicaCount}`,
`--set-string sync.image.tag="${imageTag}"`, `--set-string sync.image.tag="${imageTag}"`,
`--set-string renderer.image.tag="${imageTag}"`,
`--set renderer.app.host=${host}`,
...serviceAnnotations, ...serviceAnnotations,
`--timeout 10m`, `--timeout 10m`,
flag, flag,

View File

@@ -6,11 +6,6 @@ server {
try_files $uri/index.html $uri/ $uri /admin/index.html; try_files $uri/index.html $uri/ $uri /admin/index.html;
} }
location ~ ^/(_plugin|assets|imgs|js|plugins|static)/ {
root /app/dist/;
try_files $uri $uri/ =404;
}
set $app_root_path /app/dist/; set $app_root_path /app/dist/;
set $mobile_root /app/dist/; set $mobile_root /app/dist/;
set_by_lua $affine_env 'return os.getenv("AFFINE_ENV")'; set_by_lua $affine_env 'return os.getenv("AFFINE_ENV")';
@@ -28,6 +23,11 @@ server {
set $app_root_path $mobile_root; set $app_root_path $mobile_root;
} }
location ~ ^/(_plugin|assets|imgs|js|plugins|static)/ {
root $app_root_path;
try_files $uri $uri/ =404;
}
location / { location / {
root $app_root_path; root $app_root_path;
index index.html; index index.html;

View File

@@ -3,6 +3,7 @@ FROM node:20-bookworm-slim
COPY ./packages/backend/server /app COPY ./packages/backend/server /app
COPY ./packages/frontend/web/dist /app/static COPY ./packages/frontend/web/dist /app/static
COPY ./packages/frontend/admin/dist /app/static/admin COPY ./packages/frontend/admin/dist /app/static/admin
COPY ./packages/frontend/mobile/dist /app/static/mobile
WORKDIR /app WORKDIR /app
RUN apt-get update && \ RUN apt-get update && \

View File

@@ -76,7 +76,7 @@ spec:
- name: AFFINE_SERVER_HTTPS - name: AFFINE_SERVER_HTTPS
value: "{{ .Values.app.https }}" value: "{{ .Values.app.https }}"
- name: ENABLE_R2_OBJECT_STORAGE - name: ENABLE_R2_OBJECT_STORAGE
value: "{{ .Values.app.objectStorage.r2.enabled }}" value: "{{ .Values.global.objectStorage.r2.enabled }}"
- name: FEATURES_EARLY_ACCESS_PREVIEW - name: FEATURES_EARLY_ACCESS_PREVIEW
value: "{{ .Values.app.features.earlyAccessPreview }}" value: "{{ .Values.app.features.earlyAccessPreview }}"
- name: FEATURES_SYNC_CLIENT_VERSION_CHECK - name: FEATURES_SYNC_CLIENT_VERSION_CHECK
@@ -122,21 +122,21 @@ spec:
- name: DOC_MERGE_USE_JWST_CODEC - name: DOC_MERGE_USE_JWST_CODEC
value: "true" value: "true"
{{ end }} {{ end }}
{{ if .Values.app.objectStorage.r2.enabled }} {{ if .Values.global.objectStorage.r2.enabled }}
- name: R2_OBJECT_STORAGE_ACCOUNT_ID - name: R2_OBJECT_STORAGE_ACCOUNT_ID
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
name: "{{ .Values.app.objectStorage.r2.secretName }}" name: "{{ .Values.global.objectStorage.r2.secretName }}"
key: accountId key: accountId
- name: R2_OBJECT_STORAGE_ACCESS_KEY_ID - name: R2_OBJECT_STORAGE_ACCESS_KEY_ID
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
name: "{{ .Values.app.objectStorage.r2.secretName }}" name: "{{ .Values.global.objectStorage.r2.secretName }}"
key: accessKeyId key: accessKeyId
- name: R2_OBJECT_STORAGE_SECRET_ACCESS_KEY - name: R2_OBJECT_STORAGE_SECRET_ACCESS_KEY
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
name: "{{ .Values.app.objectStorage.r2.secretName }}" name: "{{ .Values.global.objectStorage.r2.secretName }}"
key: secretAccessKey key: secretAccessKey
{{ end }} {{ end }}
{{ if .Values.app.captcha.enabled }} {{ if .Values.app.captcha.enabled }}

View File

@@ -37,21 +37,21 @@ spec:
- name: DATABASE_URL - name: DATABASE_URL
value: postgres://{{ .Values.global.database.user }}:$(DATABASE_PASSWORD)@{{ .Values.global.database.gcloud.cloudSqlInternal }}:{{ .Values.global.database.port }}/{{ .Values.global.database.name }} value: postgres://{{ .Values.global.database.user }}:$(DATABASE_PASSWORD)@{{ .Values.global.database.gcloud.cloudSqlInternal }}:{{ .Values.global.database.port }}/{{ .Values.global.database.name }}
{{ end }} {{ end }}
{{ if .Values.app.objectStorage.r2.enabled }} {{ if .Values.global.objectStorage.r2.enabled }}
- name: R2_OBJECT_STORAGE_ACCOUNT_ID - name: R2_OBJECT_STORAGE_ACCOUNT_ID
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
name: "{{ .Values.app.objectStorage.r2.secretName }}" name: "{{ .Values.global.objectStorage.r2.secretName }}"
key: accountId key: accountId
- name: R2_OBJECT_STORAGE_ACCESS_KEY_ID - name: R2_OBJECT_STORAGE_ACCESS_KEY_ID
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
name: "{{ .Values.app.objectStorage.r2.secretName }}" name: "{{ .Values.global.objectStorage.r2.secretName }}"
key: accessKeyId key: accessKeyId
- name: R2_OBJECT_STORAGE_SECRET_ACCESS_KEY - name: R2_OBJECT_STORAGE_SECRET_ACCESS_KEY
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
name: "{{ .Values.app.objectStorage.r2.secretName }}" name: "{{ .Values.global.objectStorage.r2.secretName }}"
key: secretAccessKey key: secretAccessKey
{{ end }} {{ end }}
resources: resources:

View File

@@ -1,11 +0,0 @@
{{- if .Values.app.objectStorage.r2.enabled -}}
apiVersion: v1
kind: Secret
metadata:
name: "{{ .Values.app.objectStorage.r2.secretName }}"
type: Opaque
data:
accountId: {{ .Values.app.objectStorage.r2.accountId | b64enc }}
accessKeyId: {{ .Values.app.objectStorage.r2.accessKeyId | b64enc }}
secretAccessKey: {{ .Values.app.objectStorage.r2.secretAccessKey | b64enc }}
{{- end }}

View File

@@ -29,14 +29,7 @@ app:
secretName: copilot secretName: copilot
openai: openai:
key: '' key: ''
objectStorage: oauth:
r2:
enabled: false
secretName: r2
accountId: ''
accessKeyId: ''
secretAccessKey: ''
oauth:
google: google:
enabled: false enabled: false
secretName: oauth-google secretName: oauth-google

View File

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

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 "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.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 "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
{{- end }}

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,124 @@
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=4096"
- name: NO_COLOR
value: "1"
- name: DEPLOYMENT_TYPE
value: "affine"
- 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.url }}:{{ .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_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: ENABLE_R2_OBJECT_STORAGE
value: "{{ .Values.global.objectStorage.r2.enabled }}"
{{ if .Values.global.objectStorage.r2.enabled }}
- name: R2_OBJECT_STORAGE_ACCOUNT_ID
valueFrom:
secretKeyRef:
name: "{{ .Values.global.objectStorage.r2.secretName }}"
key: accountId
- name: R2_OBJECT_STORAGE_ACCESS_KEY_ID
valueFrom:
secretKeyRef:
name: "{{ .Values.global.objectStorage.r2.secretName }}"
key: accessKeyId
- name: R2_OBJECT_STORAGE_SECRET_ACCESS_KEY
valueFrom:
secretKeyRef:
name: "{{ .Values.global.objectStorage.r2.secretName }}"
key: secretAccessKey
{{ end }}
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-graphql
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: '4'
memory: 4Gi
probe:
initialDelaySeconds: 20
nodeSelector: {}
tolerations: []
affinity: {}

View File

@@ -0,0 +1,9 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Release.Name }}-runtime-config
data:
web-assets-manifest: |-
{{ .Files.Get "web-assets-manifest.json" | nindent 4 }}
mobile-assets-manifest: |-
{{ .Files.Get "mobile-assets-manifest.json" | nindent 4 }}

View File

@@ -60,6 +60,15 @@ spec:
name: affine-graphql name: affine-graphql
port: port:
number: {{ .Values.graphql.service.port }} number: {{ .Values.graphql.service.port }}
{{- if eq .Values.global.app.buildType "canary" }}
- path: /workspace
pathType: Prefix
backend:
service:
name: affine-renderer
port:
number: {{ .Values.graphql.service.port }}
{{- end }}
- path: / - path: /
pathType: Prefix pathType: Prefix
backend: backend:

View File

@@ -0,0 +1,11 @@
{{- if .Values.global.objectStorage.r2.enabled -}}
apiVersion: v1
kind: Secret
metadata:
name: "{{ .Values.global.objectStorage.r2.secretName }}"
type: Opaque
data:
accountId: {{ .Values.global.objectStorage.r2.accountId | b64enc }}
accessKeyId: {{ .Values.global.objectStorage.r2.accessKeyId | b64enc }}
secretAccessKey: {{ .Values.global.objectStorage.r2.secretAccessKey | b64enc }}
{{- end }}

View File

@@ -1,4 +1,6 @@
global: global:
app:
buildType: 'stable'
ingress: ingress:
enabled: false enabled: false
className: '' className: ''
@@ -28,6 +30,13 @@ global:
username: '' username: ''
password: '' password: ''
database: 0 database: 0
objectStorage:
r2:
enabled: false
secretName: r2
accountId: ''
accessKeyId: ''
secretAccessKey: ''
gke: gke:
enabled: true enabled: true
@@ -45,6 +54,13 @@ sync:
annotations: annotations:
cloud.google.com/backend-config: '{"default": "affine-api-backendconfig"}' 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"}'
web: web:
service: service:
type: ClusterIP type: ClusterIP

View File

@@ -6,11 +6,6 @@ on:
flavor: flavor:
type: string type: string
required: true required: true
workflow_dispatch:
inputs:
flavor:
type: string
required: false
env: env:
NX_CLOUD_ACCESS_TOKEN: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }} NX_CLOUD_ACCESS_TOKEN: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }}
@@ -43,6 +38,103 @@ jobs:
path: ./packages/backend/server/dist path: ./packages/backend/server/dist
if-no-files-found: error if-no-files-found: error
build-web:
name: Build @affine/web
runs-on: ubuntu-latest
environment: ${{ github.event.inputs.flavor }}
steps:
- uses: actions/checkout@v4
- name: Setup Version
id: version
uses: ./.github/actions/setup-version
- name: Setup Node.js
uses: ./.github/actions/setup-node
- name: Build Core
run: yarn nx build @affine/web --skip-nx-cache
env:
R2_ACCOUNT_ID: ${{ secrets.R2_ACCOUNT_ID }}
R2_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
R2_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
BUILD_TYPE: ${{ github.event.inputs.flavor }}
CAPTCHA_SITE_KEY: ${{ secrets.CAPTCHA_SITE_KEY }}
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
SENTRY_PROJECT: 'affine-web'
SENTRY_RELEASE: ${{ steps.version.outputs.APP_VERSION }}
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:
name: web
path: ./packages/frontend/web/dist
if-no-files-found: error
build-admin:
name: Build @affine/admin
runs-on: ubuntu-latest
environment: ${{ github.event.inputs.flavor }}
steps:
- uses: actions/checkout@v4
- name: Setup Version
id: version
uses: ./.github/actions/setup-version
- name: Setup Node.js
uses: ./.github/actions/setup-node
- name: Build Admin
run: yarn nx build @affine/admin --skip-nx-cache
env:
R2_ACCOUNT_ID: ${{ secrets.R2_ACCOUNT_ID }}
R2_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
R2_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
BUILD_TYPE: ${{ github.event.inputs.flavor }}
CAPTCHA_SITE_KEY: ${{ secrets.CAPTCHA_SITE_KEY }}
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
SENTRY_PROJECT: 'affine-admin'
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:
name: admin
path: ./packages/frontend/admin/dist
if-no-files-found: error
build-mobile:
name: Build @affine/mobile
runs-on: ubuntu-latest
environment: ${{ github.event.inputs.flavor }}
steps:
- uses: actions/checkout@v4
- name: Setup Version
id: version
uses: ./.github/actions/setup-version
- name: Setup Node.js
uses: ./.github/actions/setup-node
- name: Build Mobile
run: yarn nx build @affine/mobile --skip-nx-cache
env:
R2_ACCOUNT_ID: ${{ secrets.R2_ACCOUNT_ID }}
R2_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
R2_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
BUILD_TYPE: ${{ github.event.inputs.flavor }}
CAPTCHA_SITE_KEY: ${{ secrets.CAPTCHA_SITE_KEY }}
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
SENTRY_PROJECT: 'affine-mobile'
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:
name: mobile
path: ./packages/frontend/mobile/dist
if-no-files-found: error
build-web-selfhost: build-web-selfhost:
name: Build @affine/web selfhost name: Build @affine/web selfhost
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -70,6 +162,31 @@ jobs:
path: ./packages/frontend/web/dist path: ./packages/frontend/web/dist
if-no-files-found: error if-no-files-found: error
build-mobile-selfhost:
name: Build @affine/mobile selfhost
runs-on: ubuntu-latest
environment: ${{ github.event.inputs.flavor }}
steps:
- uses: actions/checkout@v4
- name: Setup Version
id: version
uses: ./.github/actions/setup-version
- name: Setup Node.js
uses: ./.github/actions/setup-node
- name: Build Mobile
run: yarn nx build @affine/mobile --skip-nx-cache
env:
BUILD_TYPE: ${{ github.event.inputs.flavor }}
PUBLIC_PATH: '/'
SELF_HOSTED: true
MIXPANEL_TOKEN: ${{ secrets.MIXPANEL_TOKEN }}
- name: Upload mobile artifact
uses: actions/upload-artifact@v4
with:
name: selfhost-mobile
path: ./packages/frontend/mobile/dist
if-no-files-found: error
build-admin-selfhost: build-admin-selfhost:
name: Build @affine/admin selfhost name: Build @affine/admin selfhost
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -81,7 +198,7 @@ jobs:
uses: ./.github/actions/setup-version uses: ./.github/actions/setup-version
- name: Setup Node.js - name: Setup Node.js
uses: ./.github/actions/setup-node uses: ./.github/actions/setup-node
- name: Build Core - name: Build admin
run: yarn nx build @affine/admin --skip-nx-cache run: yarn nx build @affine/admin --skip-nx-cache
env: env:
BUILD_TYPE: ${{ github.event.inputs.flavor }} BUILD_TYPE: ${{ github.event.inputs.flavor }}
@@ -131,12 +248,16 @@ jobs:
path: ./packages/backend/native/server-native.node path: ./packages/backend/native/server-native.node
if-no-files-found: error if-no-files-found: error
build-docker: build-images:
name: Build Docker name: Build Images
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: needs:
- build-server - build-server
- build-web
- build-mobile
- build-admin
- build-web-selfhost - build-web-selfhost
- build-mobile-selfhost
- build-admin-selfhost - build-admin-selfhost
- build-server-native - build-server-native
steps: steps:
@@ -195,17 +316,41 @@ jobs:
registry-url: https://npm.pkg.github.com registry-url: https://npm.pkg.github.com
scope: '@toeverything' scope: '@toeverything'
- name: Download web artifact
uses: actions/download-artifact@v4
with:
name: web
path: ./packages/frontend/web/dist
- name: Download mobile artifact
uses: actions/download-artifact@v4
with:
name: mobile
path: ./packages/frontend/mobile/dist
- name: Download admin artifact
uses: actions/download-artifact@v4
with:
name: admin
path: ./packages/frontend/admin/dist
- name: Download selfhost web artifact - name: Download selfhost web artifact
uses: actions/download-artifact@v4 uses: actions/download-artifact@v4
with: with:
name: selfhost-web name: selfhost-web
path: ./packages/frontend/web/dist path: ./packages/frontend/web/dist/selfhost
- name: Download selfhost mobile artifact
uses: actions/download-artifact@v4
with:
name: selfhost-mobile
path: ./packages/frontend/mobile/dist/selfhost
- name: Download selfhost admin artifact - name: Download selfhost admin artifact
uses: actions/download-artifact@v4 uses: actions/download-artifact@v4
with: with:
name: selfhost-admin name: selfhost-admin
path: ./packages/frontend/admin/dist path: ./packages/frontend/admin/dist/selfhost
- name: Install Node.js dependencies - name: Install Node.js dependencies
run: | run: |
@@ -220,6 +365,17 @@ jobs:
id: version id: version
uses: ./.github/actions/setup-version uses: ./.github/actions/setup-version
- 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:${{env.RELEASE_FLAVOR}}-${{ env.GIT_SHORT_HASH }},ghcr.io/toeverything/affine-front:${{env.RELEASE_FLAVOR}}
- name: Build graphql Dockerfile - name: Build graphql Dockerfile
uses: docker/build-push-action@v6 uses: docker/build-push-action@v6
with: with:

View File

@@ -20,6 +20,6 @@ permissions:
jobs: jobs:
build-image: build-image:
name: Build Image name: Build Image
uses: ./.github/workflows/build-server-image.yml uses: ./.github/workflows/build-images.yml
with: with:
flavor: ${{ github.event.inputs.flavor }} flavor: ${{ github.event.inputs.flavor }}

View File

@@ -117,7 +117,7 @@ jobs:
name: E2E Test name: E2E Test
runs-on: ubuntu-latest runs-on: ubuntu-latest
env: env:
DISTRIBUTION: browser DISTRIBUTION: web
IN_CI_TEST: true IN_CI_TEST: true
strategy: strategy:
fail-fast: false fail-fast: false
@@ -177,7 +177,7 @@ jobs:
name: E2E Migration Test name: E2E Migration Test
runs-on: ubuntu-latest runs-on: ubuntu-latest
env: env:
DISTRIBUTION: browser DISTRIBUTION: web
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Setup Node.js - name: Setup Node.js
@@ -204,7 +204,7 @@ jobs:
needs: needs:
- build-native - build-native
env: env:
DISTRIBUTION: browser DISTRIBUTION: web
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Setup Node.js - name: Setup Node.js
@@ -311,7 +311,7 @@ jobs:
# always skip cache because its fast, and cache configuration is always changing # always skip cache because its fast, and cache configuration is always changing
run: yarn nx build @affine/web --skip-nx-cache run: yarn nx build @affine/web --skip-nx-cache
env: env:
DISTRIBUTION: 'desktop' DISTRIBUTION: desktop
- name: zip web - name: zip web
run: tar -czf dist.tar.gz --directory=packages/frontend/electron/renderer/dist . run: tar -czf dist.tar.gz --directory=packages/frontend/electron/renderer/dist .
- name: Upload web artifact - name: Upload web artifact
@@ -327,7 +327,7 @@ jobs:
needs: build-server-native needs: build-server-native
env: env:
NODE_ENV: test NODE_ENV: test
DISTRIBUTION: browser DISTRIBUTION: web
services: services:
postgres: postgres:
image: postgres image: postgres
@@ -396,7 +396,7 @@ jobs:
name: ${{ matrix.tests.name }} name: ${{ matrix.tests.name }}
runs-on: ubuntu-latest runs-on: ubuntu-latest
env: env:
DISTRIBUTION: browser DISTRIBUTION: web
DATABASE_URL: postgresql://affine:affine@localhost:5432/affine DATABASE_URL: postgresql://affine:affine@localhost:5432/affine
IN_CI_TEST: true IN_CI_TEST: true
strategy: strategy:

View File

@@ -62,171 +62,19 @@ jobs:
echo "version=$prev_version" >> $GITHUB_OUTPUT echo "version=$prev_version" >> $GITHUB_OUTPUT
echo "namesapce=$namespace" >> $GITHUB_OUTPUT echo "namesapce=$namespace" >> $GITHUB_OUTPUT
build-server-image: build-images:
name: Build Server Image name: Build Images
uses: ./.github/workflows/build-server-image.yml uses: ./.github/workflows/build-images.yml
secrets: inherit
with: with:
flavor: ${{ github.event.inputs.flavor }} flavor: ${{ github.event.inputs.flavor }}
build-web:
name: Build @affine/web
runs-on: ubuntu-latest
environment: ${{ github.event.inputs.flavor }}
steps:
- uses: actions/checkout@v4
- name: Setup Version
id: version
uses: ./.github/actions/setup-version
- name: Setup Node.js
uses: ./.github/actions/setup-node
- name: Build Core
run: yarn nx build @affine/web --skip-nx-cache
env:
R2_ACCOUNT_ID: ${{ secrets.R2_ACCOUNT_ID }}
R2_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
R2_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
BUILD_TYPE: ${{ github.event.inputs.flavor }}
CAPTCHA_SITE_KEY: ${{ secrets.CAPTCHA_SITE_KEY }}
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
SENTRY_PROJECT: 'affine-web'
SENTRY_RELEASE: ${{ steps.version.outputs.APP_VERSION }}
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:
name: web
path: ./packages/frontend/web/dist
if-no-files-found: error
build-admin:
name: Build @affine/admin
runs-on: ubuntu-latest
environment: ${{ github.event.inputs.flavor }}
steps:
- uses: actions/checkout@v4
- name: Setup Version
id: version
uses: ./.github/actions/setup-version
- name: Setup Node.js
uses: ./.github/actions/setup-node
- name: Build Admin
run: yarn nx build @affine/admin --skip-nx-cache
env:
R2_ACCOUNT_ID: ${{ secrets.R2_ACCOUNT_ID }}
R2_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
R2_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
BUILD_TYPE: ${{ github.event.inputs.flavor }}
CAPTCHA_SITE_KEY: ${{ secrets.CAPTCHA_SITE_KEY }}
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
SENTRY_PROJECT: 'affine-admin'
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:
name: admin
path: ./packages/frontend/admin/dist
if-no-files-found: error
build-mobile:
name: Build @affine/mobile
runs-on: ubuntu-latest
environment: ${{ github.event.inputs.flavor }}
steps:
- uses: actions/checkout@v4
- name: Setup Version
id: version
uses: ./.github/actions/setup-version
- name: Setup Node.js
uses: ./.github/actions/setup-node
- name: Build Mobile
run: yarn nx build @affine/mobile --skip-nx-cache
env:
R2_ACCOUNT_ID: ${{ secrets.R2_ACCOUNT_ID }}
R2_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
R2_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
BUILD_TYPE: ${{ github.event.inputs.flavor }}
CAPTCHA_SITE_KEY: ${{ secrets.CAPTCHA_SITE_KEY }}
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
SENTRY_PROJECT: 'affine-mobile'
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:
name: mobile
path: ./packages/frontend/mobile/dist
if-no-files-found: error
build-frontend-image:
name: Build Frontend Image
runs-on: ubuntu-latest
needs:
- build-web
- build-admin
- build-mobile
steps:
- uses: actions/checkout@v4
- name: Download web artifact
uses: actions/download-artifact@v4
with:
name: web
path: ./packages/frontend/web/dist
- name: Download admin artifact
uses: actions/download-artifact@v4
with:
name: admin
path: ./packages/frontend/admin/dist
- name: Download mobile artifact
uses: actions/download-artifact@v4
with:
name: mobile
path: ./packages/frontend/mobile/dist
- name: Setup env
run: |
echo "GIT_SHORT_HASH=$(git rev-parse --short HEAD)" >> "$GITHUB_ENV"
if [ -z "${{ inputs.flavor }}" ]
then
echo "RELEASE_FLAVOR=canary" >> "$GITHUB_ENV"
else
echo "RELEASE_FLAVOR=${{ inputs.flavor }}" >> "$GITHUB_ENV"
fi
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
logout: false
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- 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:${{env.RELEASE_FLAVOR}}-${{ env.GIT_SHORT_HASH }},ghcr.io/toeverything/affine-front:${{env.RELEASE_FLAVOR}}
deploy: deploy:
name: Deploy to cluster name: Deploy to cluster
if: ${{ github.event_name == 'workflow_dispatch' }} if: ${{ github.event_name == 'workflow_dispatch' }}
environment: ${{ github.event.inputs.flavor }} environment: ${{ github.event.inputs.flavor }}
needs: needs:
- build-frontend-image - build-images
- build-server-image
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
@@ -276,11 +124,7 @@ jobs:
deploy-done: deploy-done:
needs: needs:
- output-prev-version - output-prev-version
- build-web - build-images
- build-admin
- build-mobile
- build-frontend-image
- build-server-image
- deploy - deploy
if: always() if: always()
runs-on: ubuntu-latest runs-on: ubuntu-latest

7
Cargo.lock generated
View File

@@ -58,6 +58,7 @@ dependencies = [
"sha3", "sha3",
"tiktoken-rs", "tiktoken-rs",
"tokio", "tokio",
"v_htmlescape",
"y-octo", "y-octo",
] ]
@@ -2218,6 +2219,12 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "v_htmlescape"
version = "0.15.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e8257fbc510f0a46eb602c10215901938b5c2a7d5e70fc11483b1d3c9b5b18c"
[[package]] [[package]]
name = "valuable" name = "valuable"
version = "0.1.0" version = "0.1.0"

View File

@@ -3,26 +3,27 @@ members = ["./packages/backend/native", "./packages/frontend/native", "./packag
resolver = "2" resolver = "2"
[workspace.dependencies] [workspace.dependencies]
anyhow = "1" anyhow = "1"
chrono = "0.4" chrono = "0.4"
dotenv = "0.15" dotenv = "0.15"
file-format = { version = "0.25", features = ["reader"] } file-format = { version = "0.25", features = ["reader"] }
mimalloc = "0.1" mimalloc = "0.1"
napi = { version = "3.0.0-alpha.1", features = ["async", "chrono_date", "error_anyhow", "napi9", "serde"] } napi = { version = "3.0.0-alpha.1", features = ["async", "chrono_date", "error_anyhow", "napi9", "serde"] }
napi-build = { version = "2" } napi-build = { version = "2" }
napi-derive = { version = "3.0.0-alpha.1" } napi-derive = { version = "3.0.0-alpha.1" }
notify = { version = "6", features = ["serde"] } notify = { version = "6", features = ["serde"] }
once_cell = "1" once_cell = "1"
parking_lot = "0.12" parking_lot = "0.12"
rand = "0.8" rand = "0.8"
serde = "1" serde = "1"
serde_json = "1" serde_json = "1"
sha3 = "0.10" sha3 = "0.10"
sqlx = { version = "0.8", default-features = false, features = ["chrono", "macros", "migrate", "runtime-tokio", "sqlite", "tls-rustls"] } sqlx = { version = "0.8", default-features = false, features = ["chrono", "macros", "migrate", "runtime-tokio", "sqlite", "tls-rustls"] }
tiktoken-rs = "0.5" tiktoken-rs = "0.5"
tokio = "1.37" tokio = "1.37"
uuid = "1.8" uuid = "1.8"
y-octo = { git = "https://github.com/y-crdt/y-octo.git", branch = "main" } v_htmlescape = "0.15"
y-octo = { git = "https://github.com/y-crdt/y-octo.git", branch = "main" }
[profile.dev.package.sqlx-macros] [profile.dev.package.sqlx-macros]
opt-level = 3 opt-level = 3

View File

@@ -7,14 +7,15 @@ version = "1.0.0"
crate-type = ["cdylib"] crate-type = ["cdylib"]
[dependencies] [dependencies]
chrono = { workspace = true } chrono = { workspace = true }
file-format = { workspace = true } file-format = { workspace = true }
napi = { workspace = true } napi = { workspace = true }
napi-derive = { workspace = true } napi-derive = { workspace = true }
rand = { workspace = true } rand = { workspace = true }
sha3 = { workspace = true } sha3 = { workspace = true }
tiktoken-rs = { workspace = true } tiktoken-rs = { workspace = true }
y-octo = { workspace = true } v_htmlescape = { workspace = true }
y-octo = { workspace = true }
[target.'cfg(not(target_os = "linux"))'.dependencies] [target.'cfg(not(target_os = "linux"))'.dependencies]
mimalloc = { workspace = true } mimalloc = { workspace = true }

View File

@@ -8,6 +8,8 @@ export declare function fromModelName(modelName: string): Tokenizer | null
export declare function getMime(input: Uint8Array): string export declare function getMime(input: Uint8Array): string
export declare function htmlSanitize(input: string): string
/** /**
* Merge updates in form like `Y.applyUpdate(doc, update)` way and return the * Merge updates in form like `Y.applyUpdate(doc, update)` way and return the
* result binary. * result binary.

View File

@@ -11,3 +11,4 @@ export const mintChallengeResponse = binding.mintChallengeResponse;
export const getMime = binding.getMime; export const getMime = binding.getMime;
export const Tokenizer = binding.Tokenizer; export const Tokenizer = binding.Tokenizer;
export const fromModelName = binding.fromModelName; export const fromModelName = binding.fromModelName;
export const htmlSanitize = binding.htmlSanitize;

View File

@@ -0,0 +1,4 @@
#[napi]
pub fn html_sanitize(input: String) -> String {
v_htmlescape::escape(&input).to_string()
}

View File

@@ -2,6 +2,7 @@
pub mod file_type; pub mod file_type;
pub mod hashcash; pub mod hashcash;
pub mod html_sanitize;
pub mod tiktoken; pub mod tiktoken;
use std::fmt::{Debug, Display}; use std::fmt::{Debug, Display};

View File

@@ -70,6 +70,7 @@
"graphql-upload": "^16.0.2", "graphql-upload": "^16.0.2",
"html-validate": "^8.20.1", "html-validate": "^8.20.1",
"ioredis": "^5.3.2", "ioredis": "^5.3.2",
"is-mobile": "^4.0.0",
"keyv": "^5.0.0", "keyv": "^5.0.0",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"mixpanel": "^0.18.0", "mixpanel": "^0.18.0",
@@ -94,7 +95,6 @@
"ts-node": "^10.9.2", "ts-node": "^10.9.2",
"typescript": "^5.4.5", "typescript": "^5.4.5",
"ws": "^8.16.0", "ws": "^8.16.0",
"xss": "^1.0.15",
"yjs": "patch:yjs@npm%3A13.6.18#~/.yarn/patches/yjs-npm-13.6.18-ad0d5f7c43.patch", "yjs": "patch:yjs@npm%3A13.6.18#~/.yarn/patches/yjs-npm-13.6.18-ad0d5f7c43.patch",
"zod": "^3.22.4" "zod": "^3.22.4"
}, },

View File

@@ -1,93 +1,191 @@
import { Controller, Get, Param, Res } from '@nestjs/common'; import { readFileSync } from 'node:fs';
import type { Response } from 'express'; import { join } from 'node:path';
import xss from 'xss';
import { DocNotFound } from '../../fundamentals'; import { Controller, Get, Logger, Param, Req, Res } from '@nestjs/common';
import type { Request, Response } from 'express';
import isMobile from 'is-mobile';
import { Config, metrics, URLHelper } from '../../fundamentals';
import { htmlSanitize } from '../../native';
import { Public } from '../auth';
import { PermissionService } from '../permission'; import { PermissionService } from '../permission';
import { PageDocContent } from '../utils/blocksuite';
import { DocContentService } from './service'; import { DocContentService } from './service';
interface RenderOptions { interface RenderOptions {
og: boolean; title: string;
content: boolean; summary: string;
avatar?: string;
} }
interface HtmlAssets {
css: string[];
js: string[];
publicPath: string;
gitHash: string;
description: string;
}
const defaultAssets: HtmlAssets = {
css: [],
js: [],
publicPath: '/',
gitHash: '',
description: '',
};
@Controller('/workspace/:workspaceId/:docId') @Controller('/workspace/:workspaceId/:docId')
export class DocRendererController { export class DocRendererController {
private readonly logger = new Logger(DocRendererController.name);
private readonly webAssets: HtmlAssets = defaultAssets;
private readonly mobileAssets: HtmlAssets = defaultAssets;
constructor( constructor(
private readonly doc: DocContentService, private readonly doc: DocContentService,
private readonly permission: PermissionService private readonly permission: PermissionService,
) {} private readonly config: Config,
private readonly url: URLHelper
) {
try {
const webConfigMapsPath = join(
this.config.projectRoot,
this.config.isSelfhosted ? 'static/selfhost' : 'static',
'assets-manifest.json'
);
const mobileConfigMapsPath = join(
this.config.projectRoot,
this.config.isSelfhosted ? 'static/mobile/selfhost' : 'static/mobile',
'assets-manifest.json'
);
this.webAssets = JSON.parse(readFileSync(webConfigMapsPath, 'utf-8'));
this.mobileAssets = JSON.parse(
readFileSync(mobileConfigMapsPath, 'utf-8')
);
} catch (e) {
if (this.config.node.prod) {
throw e;
}
}
}
@Public()
@Get() @Get()
async render( async render(
@Req() req: Request,
@Res() res: Response, @Res() res: Response,
@Param('workspaceId') workspaceId: string, @Param('workspaceId') workspaceId: string,
@Param('docId') docId: string @Param('docId') docId: string
) { ) {
if (workspaceId === docId) { const assets: HtmlAssets =
throw new DocNotFound({ spaceId: workspaceId, docId }); this.config.affine.canary &&
} isMobile({
ua: req.headers['user-agent'] ?? undefined,
})
? this.mobileAssets
: this.webAssets;
// if page is public, show all let opts: RenderOptions | null = null;
// if page is private, but workspace public og is on, show og but not content try {
const opts: RenderOptions = { opts =
og: false, workspaceId === docId
content: false, ? await this.renderWorkspace(workspaceId)
}; : await this.getPageContent(workspaceId, docId);
const isPagePublic = await this.permission.isPublicPage(workspaceId, docId); metrics.doc.counter('render').add(1);
} catch (e) {
if (isPagePublic) { this.logger.error('failed to render page', e);
opts.og = true;
opts.content = true;
} else {
const allowPreview = await this.permission.allowUrlPreview(workspaceId);
if (allowPreview) {
opts.og = true;
}
}
let docContent = opts.og
? await this.doc.getPageContent(workspaceId, docId)
: null;
if (!docContent) {
docContent = { title: 'untitled', summary: '' };
} }
res.setHeader('Content-Type', 'text/html'); res.setHeader('Content-Type', 'text/html');
if (!opts.og) { if (!opts) {
res.setHeader('X-Robots-Tag', 'noindex'); res.setHeader('X-Robots-Tag', 'noindex');
} }
res.send(this._render(docContent, opts));
res.send(this._render(opts, assets));
} }
_render(doc: PageDocContent, { og }: RenderOptions): string { private async getPageContent(
const title = xss(doc.title); workspaceId: string,
const summary = xss(doc.summary); docId: string
): Promise<RenderOptions | null> {
let allowUrlPreview = await this.permission.isPublicPage(
workspaceId,
docId
);
if (!allowUrlPreview) {
// if page is private, but workspace url preview is on
allowUrlPreview = await this.permission.allowUrlPreview(workspaceId);
}
if (allowUrlPreview) {
return this.doc.getPageContent(workspaceId, docId);
}
return null;
}
private async renderWorkspace(
workspaceId: string
): Promise<RenderOptions | null> {
const allowUrlPreview = await this.permission.allowUrlPreview(workspaceId);
if (allowUrlPreview) {
const workspaceContent = await this.doc.getWorkspaceContent(workspaceId);
if (workspaceContent) {
return {
title: workspaceContent.name,
summary: '',
avatar: workspaceContent.avatarKey
? this.url.link(
`/api/workspaces/${workspaceId}/blobs/${workspaceContent.avatarKey}`
)
: undefined,
};
}
}
return null;
}
_render(opts: RenderOptions | null, assets: HtmlAssets): string {
const title = opts?.title
? htmlSanitize(`${opts.title} | AFFiNE`)
: 'AFFiNE';
const summary = opts ? htmlSanitize(opts.summary) : assets.description;
const image = opts?.avatar ?? 'https://affine.pro/og.jpeg';
return ` return `
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<title>${title} | AFFiNE</title> <meta charset="utf-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, maximum-scale=1"
/>
<title>${title}</title>
<meta name="theme-color" content="#fafafa" /> <meta name="theme-color" content="#fafafa" />
<link rel="preconnect" href="${assets.publicPath}">
<link rel="manifest" href="/manifest.json" /> <link rel="manifest" href="/manifest.json" />
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" /> <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
<link rel="icon" sizes="192x192" href="/favicon-192.png" /> <link rel="icon" sizes="192x192" href="/favicon-192.png" />
${!og ? '<meta name="robots" content="noindex, nofollow" />' : ''} <meta name="emotion-insertion-point" content="" />
${!opts ? '<meta name="robots" content="noindex, nofollow" />' : ''}
<meta <meta
name="twitter:title" name="twitter:title"
content="AFFiNE: There can be more than Notion and Miro." content="${title}"
/> />
<meta name="twitter:description" content="${title}" /> <meta name="twitter:description" content="${summary}" />
<meta name="twitter:site" content="@AffineOfficial" /> <meta name="twitter:site" content="@AffineOfficial" />
<meta name="twitter:image" content="https://affine.pro/og.jpeg" /> <meta name="twitter:image" content="${image}" />
<meta property="og:title" content="${title}" /> <meta property="og:title" content="${title}" />
<meta property="og:description" content="${summary}" /> <meta property="og:description" content="${summary}" />
<meta property="og:image" content="https://affine.pro/og.jpeg" /> <meta property="og:image" content="${image}" />
${assets.css.map(url => `<link rel="stylesheet" href="${url}" />`).join('\n')}
</head> </head>
<body> <body>
<div id="app" data-version="${assets.gitHash}"></div>
${assets.js.map(url => `<script type="module" src="${url}"></script>`).join('\n')}
</body> </body>
</html> </html>
`; `;

View File

@@ -1,7 +1,7 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { applyUpdate, Doc } from 'yjs'; import { applyUpdate, Doc } from 'yjs';
import { Cache } from '../../fundamentals'; import { Cache, type EventPayload, OnEvent } from '../../fundamentals';
import { PgWorkspaceDocStorageAdapter } from '../doc'; import { PgWorkspaceDocStorageAdapter } from '../doc';
import { import {
type PageDocContent, type PageDocContent,
@@ -78,11 +78,15 @@ export class DocContentService {
return content; return content;
} }
async markDocContentCacheStale(workspaceId: string, guid: string) { @OnEvent('snapshot.updated')
async markDocContentCacheStale({
workspaceId,
id,
}: EventPayload<'snapshot.updated'>) {
const key = const key =
workspaceId === guid workspaceId === id
? `workspace:${workspaceId}:content` ? `workspace:${workspaceId}:content`
: `workspace:${workspaceId}:doc:${guid}:content`; : `workspace:${workspaceId}:doc:${id}:content`;
await this.cache.delete(key); await this.cache.delete(key);
} }
} }

View File

@@ -6,6 +6,7 @@ import {
Cache, Cache,
DocHistoryNotFound, DocHistoryNotFound,
DocNotFound, DocNotFound,
EventEmitter,
FailedToSaveUpdates, FailedToSaveUpdates,
FailedToUpsertSnapshot, FailedToUpsertSnapshot,
metrics, metrics,
@@ -30,6 +31,7 @@ export class PgWorkspaceDocStorageAdapter extends DocStorageAdapter {
private readonly db: PrismaClient, private readonly db: PrismaClient,
private readonly mutex: Mutex, private readonly mutex: Mutex,
private readonly cache: Cache, private readonly cache: Cache,
private readonly event: EventEmitter,
protected override readonly options: DocStorageOptions protected override readonly options: DocStorageOptions
) { ) {
super(options); super(options);
@@ -97,7 +99,6 @@ export class PgWorkspaceDocStorageAdapter extends DocStorageAdapter {
metrics.doc.counter('doc_update_insert_failed').add(1); metrics.doc.counter('doc_update_insert_failed').add(1);
throw new FailedToSaveUpdates(); throw new FailedToSaveUpdates();
} }
return timestamp; return timestamp;
} }
@@ -463,6 +464,14 @@ export class PgWorkspaceDocStorageAdapter extends DocStorageAdapter {
// the updates has been applied to current `doc` must have been seen by the other process as well. // the updates has been applied to current `doc` must have been seen by the other process as well.
// The `updatedSnapshot` will be `undefined` in this case. // The `updatedSnapshot` will be `undefined` in this case.
const updatedSnapshot = result.at(0); const updatedSnapshot = result.at(0);
if (updatedSnapshot) {
this.event.emit('snapshot.updated', {
workspaceId: snapshot.spaceId,
id: snapshot.docId,
});
}
return !!updatedSnapshot; return !!updatedSnapshot;
} catch (e) { } catch (e) {
metrics.doc.counter('snapshot_upsert_failed').add(1); metrics.doc.counter('snapshot_upsert_failed').add(1);

View File

@@ -22,7 +22,6 @@ export class SetupMiddleware implements NestMiddleware {
use = (req: Request, res: Response, next: (error?: Error | any) => void) => { use = (req: Request, res: Response, next: (error?: Error | any) => void) => {
// never throw // never throw
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.server this.server
.initialized() .initialized()
.then(initialized => { .then(initialized => {
@@ -59,6 +58,10 @@ export class SelfhostModule implements OnModuleInit {
) {} ) {}
onModuleInit() { onModuleInit() {
// selfhost static file location
// web => 'static/selfhost'
// admin => 'static/admin/selfhost'
// mobile => 'static/mobile/selfhost'
const staticPath = join(this.config.projectRoot, 'static'); const staticPath = join(this.config.projectRoot, 'static');
// in command line mode // in command line mode
if (!this.adapterHost.httpAdapter) { if (!this.adapterHost.httpAdapter) {
@@ -73,7 +76,7 @@ export class SelfhostModule implements OnModuleInit {
}); });
app.use( app.use(
basePath + '/admin', basePath + '/admin',
serveStatic(join(staticPath, 'admin'), { serveStatic(join(staticPath, 'admin', 'selfhost'), {
redirect: false, redirect: false,
index: false, index: false,
}) })
@@ -83,7 +86,7 @@ export class SelfhostModule implements OnModuleInit {
[basePath + '/admin', basePath + '/admin/*'], [basePath + '/admin', basePath + '/admin/*'],
this.check.use, this.check.use,
(_req, res) => { (_req, res) => {
res.sendFile(join(staticPath, 'admin', 'index.html')); res.sendFile(join(staticPath, 'admin', 'selfhost', 'index.html'));
} }
); );
@@ -92,13 +95,13 @@ export class SelfhostModule implements OnModuleInit {
}); });
app.use( app.use(
basePath, basePath,
serveStatic(staticPath, { serveStatic(join(staticPath, 'selfhost'), {
redirect: false, redirect: false,
index: false, index: false,
}) })
); );
app.get('*', this.check.use, (_req, res) => { app.get('*', this.check.use, (_req, res) => {
res.sendFile(join(staticPath, 'index.html')); res.sendFile(join(staticPath, 'selfhost', 'index.html'));
}); });
} }
} }

View File

@@ -13,12 +13,8 @@ export interface WorkspaceEvents {
} }
export interface DocEvents { export interface DocEvents {
updated: Payload<
Pick<Snapshot, 'id' | 'workspaceId'> & {
previous: Pick<Snapshot, 'blob' | 'state' | 'updatedAt'>;
}
>;
deleted: Payload<Pick<Snapshot, 'id' | 'workspaceId'>>; deleted: Payload<Pick<Snapshot, 'id' | 'workspaceId'>>;
updated: Payload<Pick<Snapshot, 'id' | 'workspaceId'>>;
} }
export interface UserEvents { export interface UserEvents {

View File

@@ -32,3 +32,4 @@ export const mintChallengeResponse = async (resource: string, bits: number) => {
export const getMime = serverNativeModule.getMime; export const getMime = serverNativeModule.getMime;
export const Tokenizer = serverNativeModule.Tokenizer; export const Tokenizer = serverNativeModule.Tokenizer;
export const fromModelName = serverNativeModule.fromModelName; export const fromModelName = serverNativeModule.fromModelName;
export const htmlSanitize = serverNativeModule.htmlSanitize;

View File

@@ -18,16 +18,17 @@ const test = ava as TestFn<{
}>; }>;
function initTestStaticFiles(staticPath: string) { function initTestStaticFiles(staticPath: string) {
mkdirSync(path.join(staticPath, 'admin'), { recursive: true });
const files = { const files = {
'index.html': `<!DOCTYPE html><html><body>AFFiNE</body><script src="main.js"/></html>`, 'selfhost/index.html': `<!DOCTYPE html><html><body>AFFiNE</body><script src="main.js"/></html>`,
'main.js': `const name = 'affine'`, 'selfhost/main.js': `const name = 'affine'`,
'admin/index.html': `<!DOCTYPE html><html><body>AFFiNE Admin</body><script src="/admin/main.js"/></html>`, 'admin/selfhost/index.html': `<!DOCTYPE html><html><body>AFFiNE Admin</body><script src="/admin/main.js"/></html>`,
'admin/main.js': `const name = 'affine-admin'`, 'admin/selfhost/main.js': `const name = 'affine-admin'`,
}; };
for (const [filename, content] of Object.entries(files)) { for (const [filename, content] of Object.entries(files)) {
writeFileSync(path.join(staticPath, filename), content); const filePath = path.join(staticPath, filename);
mkdirSync(path.dirname(filePath), { recursive: true });
writeFileSync(filePath, content);
} }
} }

View File

@@ -10,7 +10,7 @@ export const runtimeFlagsSchema = z.object({
serverUrlPrefix: z.string(), serverUrlPrefix: z.string(),
appVersion: z.string(), appVersion: z.string(),
editorVersion: z.string(), editorVersion: z.string(),
distribution: z.enum(['browser', 'desktop', 'admin', 'mobile']), distribution: z.enum(['web', 'desktop', 'admin', 'mobile']),
appBuildType: z.union([ appBuildType: z.union([
z.literal('stable'), z.literal('stable'),
z.literal('beta'), z.literal('beta'),
@@ -104,7 +104,7 @@ export function setupGlobal() {
environment = { environment = {
isDesktopEdition: runtimeConfig.distribution !== 'mobile', isDesktopEdition: runtimeConfig.distribution !== 'mobile',
isMobileEdition: runtimeConfig.distribution === 'mobile', isMobileEdition: runtimeConfig.distribution === 'mobile',
isDesktopWeb: runtimeConfig.distribution === 'browser', isDesktopWeb: runtimeConfig.distribution === 'web',
isMobileWeb: runtimeConfig.distribution === 'mobile', isMobileWeb: runtimeConfig.distribution === 'mobile',
isElectron, isElectron,
isDebug, isDebug,

View File

@@ -55,9 +55,10 @@ export default {
define: { define: {
'process.env.CAPTCHA_SITE_KEY': `"${process.env.CAPTCHA_SITE_KEY}"`, 'process.env.CAPTCHA_SITE_KEY': `"${process.env.CAPTCHA_SITE_KEY}"`,
runtimeConfig: getRuntimeConfig({ runtimeConfig: getRuntimeConfig({
distribution: 'browser', distribution: 'web',
mode: 'development', mode: 'development',
channel: 'canary', channel: 'canary',
static: false,
coverage: false, coverage: false,
static: false, static: false,
}), }),

View File

@@ -1074,6 +1074,8 @@ export interface UpdateUserInput {
} }
export interface UpdateWorkspaceInput { export interface UpdateWorkspaceInput {
/** Enable url previous when sharing */
enableUrlPreview: InputMaybe<Scalars['Boolean']['input']>;
id: Scalars['ID']['input']; id: Scalars['ID']['input'];
/** is Public workspace */ /** is Public workspace */
public: InputMaybe<Scalars['Boolean']['input']>; public: InputMaybe<Scalars['Boolean']['input']>;
@@ -1217,6 +1219,8 @@ export interface WorkspaceType {
blobsSize: Scalars['Int']['output']; blobsSize: Scalars['Int']['output'];
/** Workspace created date */ /** Workspace created date */
createdAt: Scalars['DateTime']['output']; createdAt: Scalars['DateTime']['output'];
/** Enable url previous when sharing */
enableUrlPreview: Scalars['Boolean']['output'];
/** Enabled features of workspace */ /** Enabled features of workspace */
features: Array<FeatureType>; features: Array<FeatureType>;
histories: Array<DocHistoryType>; histories: Array<DocHistoryType>;

View File

@@ -5,7 +5,7 @@
"private": true, "private": true,
"browser": "src/index.tsx", "browser": "src/index.tsx",
"scripts": { "scripts": {
"build": "cross-env DISTRIBUTION=browser yarn workspace @affine/cli build", "build": "cross-env DISTRIBUTION=web yarn workspace @affine/cli build",
"dev": "yarn workspace @affine/cli dev", "dev": "yarn workspace @affine/cli dev",
"static-server": "yarn workspace @affine/cli dev --static" "static-server": "yarn workspace @affine/cli dev --static"
}, },

View File

@@ -3,7 +3,7 @@ import { setupGlobal } from '@affine/env/global';
process.env.RUNTIME_CONFIG = JSON.stringify( process.env.RUNTIME_CONFIG = JSON.stringify(
getRuntimeConfig({ getRuntimeConfig({
distribution: 'browser', distribution: 'web',
mode: 'development', mode: 'development',
channel: 'canary', channel: 'canary',
static: false, static: false,

View File

@@ -31,7 +31,7 @@ const getChannel = () => {
let entry: BuildFlags['entry']; let entry: BuildFlags['entry'];
const { DISTRIBUTION } = process.env; const { DISTRIBUTION = 'web' } = process.env;
const cwd = getCwdFromDistribution(DISTRIBUTION); const cwd = getCwdFromDistribution(DISTRIBUTION);

View File

@@ -13,7 +13,7 @@ import { createWebpackConfig } from '../webpack/webpack.config.js';
const flags: BuildFlags = { const flags: BuildFlags = {
distribution: distribution:
(process.env.DISTRIBUTION as BuildFlags['distribution']) ?? 'browser', (process.env.DISTRIBUTION as BuildFlags['distribution']) ?? 'web',
mode: 'development', mode: 'development',
static: false, static: false,
channel: 'canary', channel: 'canary',
@@ -42,7 +42,7 @@ const buildFlags = process.argv.includes('--static')
message: 'Distribution', message: 'Distribution',
options: [ options: [
{ {
value: 'browser', value: 'web',
}, },
{ {
value: 'desktop', value: 'desktop',
@@ -54,7 +54,7 @@ const buildFlags = process.argv.includes('--static')
value: 'mobile', value: 'mobile',
}, },
], ],
initialValue: 'browser', initialValue: 'web',
}), }),
mode: () => mode: () =>
p.select({ p.select({

View File

@@ -15,7 +15,7 @@ module.exports.getCwdFromDistribution = function getCwdFromDistribution(
distribution distribution
) { ) {
switch (distribution) { switch (distribution) {
case 'browser': case 'web':
case undefined: case undefined:
case null: case null:
return join(projectRoot, 'packages/frontend/web'); return join(projectRoot, 'packages/frontend/web');
@@ -26,7 +26,9 @@ module.exports.getCwdFromDistribution = function getCwdFromDistribution(
case 'mobile': case 'mobile':
return join(projectRoot, 'packages/frontend/mobile'); return join(projectRoot, 'packages/frontend/mobile');
default: { default: {
throw new Error('DISTRIBUTION must be one of browser, desktop'); throw new Error(
'DISTRIBUTION must be one of web, desktop, admin, mobile'
);
} }
} }
}; };

View File

@@ -1,5 +1,5 @@
export type BuildFlags = { export type BuildFlags = {
distribution: 'browser' | 'desktop' | 'admin' | 'mobile'; distribution: 'web' | 'desktop' | 'admin' | 'mobile';
mode: 'development' | 'production'; mode: 'development' | 'production';
channel: 'stable' | 'beta' | 'canary' | 'internal'; channel: 'stable' | 'beta' | 'canary' | 'internal';
static: boolean; static: boolean;

View File

@@ -8,6 +8,7 @@
/> />
<title>AFFiNE</title> <title>AFFiNE</title>
<meta name="theme-color" content="#fafafa" /> <meta name="theme-color" content="#fafafa" />
<link rel="preconnect" href="<%= PUBLIC_PATH %>" />
<link rel="manifest" href="/manifest.json" /> <link rel="manifest" href="/manifest.json" />
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" /> <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
<link rel="icon" sizes="192x192" href="/favicon-192.png" /> <link rel="icon" sizes="192x192" href="/favicon-192.png" />

View File

@@ -5,6 +5,7 @@ import type { BuildFlags } from '@affine/cli/config';
import { Repository } from '@napi-rs/simple-git'; import { Repository } from '@napi-rs/simple-git';
import HTMLPlugin from 'html-webpack-plugin'; import HTMLPlugin from 'html-webpack-plugin';
import { once } from 'lodash-es'; import { once } from 'lodash-es';
import webpack from 'webpack';
import { merge } from 'webpack-merge'; import { merge } from 'webpack-merge';
import { createConfiguration, rootPath, workspaceRoot } from './config.js'; import { createConfiguration, rootPath, workspaceRoot } from './config.js';
@@ -48,9 +49,32 @@ export function createWebpackConfig(cwd: string, flags: BuildFlags) {
minify: false, minify: false,
chunks: [entryName], chunks: [entryName],
filename: `${entryName === 'app' ? 'index' : entryName}.html`, // main entry should take name index.html filename: `${entryName === 'app' ? 'index' : entryName}.html`, // main entry should take name index.html
templateParameters: { templateParameters: (compilation, assets) => {
GIT_SHORT_SHA: gitShortHash(), if (entryName === 'app') {
DESCRIPTION, // emit assets manifest for ssr
compilation.emitAsset(
`assets-manifest.json`,
new webpack.sources.RawSource(
JSON.stringify(
{
...assets,
gitHash: gitShortHash(),
description: DESCRIPTION,
},
null,
2
)
),
{
immutable: true,
}
);
}
return {
GIT_SHORT_SHA: gitShortHash(),
DESCRIPTION,
PUBLIC_PATH: config.output?.publicPath,
};
}, },
}); });
}; };

View File

@@ -869,6 +869,7 @@ __metadata:
graphql-upload: "npm:^16.0.2" graphql-upload: "npm:^16.0.2"
html-validate: "npm:^8.20.1" html-validate: "npm:^8.20.1"
ioredis: "npm:^5.3.2" ioredis: "npm:^5.3.2"
is-mobile: "npm:^4.0.0"
keyv: "npm:^5.0.0" keyv: "npm:^5.0.0"
lodash-es: "npm:^4.17.21" lodash-es: "npm:^4.17.21"
mixpanel: "npm:^0.18.0" mixpanel: "npm:^0.18.0"
@@ -896,7 +897,6 @@ __metadata:
ts-node: "npm:^10.9.2" ts-node: "npm:^10.9.2"
typescript: "npm:^5.4.5" typescript: "npm:^5.4.5"
ws: "npm:^8.16.0" ws: "npm:^8.16.0"
xss: "npm:^1.0.15"
yjs: "patch:yjs@npm%3A13.6.18#~/.yarn/patches/yjs-npm-13.6.18-ad0d5f7c43.patch" yjs: "patch:yjs@npm%3A13.6.18#~/.yarn/patches/yjs-npm-13.6.18-ad0d5f7c43.patch"
zod: "npm:^3.22.4" zod: "npm:^3.22.4"
bin: bin:
@@ -24792,6 +24792,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"is-mobile@npm:^4.0.0":
version: 4.0.0
resolution: "is-mobile@npm:4.0.0"
checksum: 10/1c4f32ab030ac6c203d63b547ef23933eacfebe81fd9d800c86739d5a73afad7983aea4c5e832c3d9c0a63d1e68cd318637490e6406bdda1cbadc8f701d5d557
languageName: node
linkType: hard
"is-my-ip-valid@npm:^1.0.0": "is-my-ip-valid@npm:^1.0.0":
version: 1.0.1 version: 1.0.1
resolution: "is-my-ip-valid@npm:1.0.1" resolution: "is-my-ip-valid@npm:1.0.1"
@@ -36501,7 +36508,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"xss@npm:^1.0.15, xss@npm:^1.0.8": "xss@npm:^1.0.8":
version: 1.0.15 version: 1.0.15
resolution: "xss@npm:1.0.15" resolution: "xss@npm:1.0.15"
dependencies: dependencies: