mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-18 23:07:02 +08:00
Compare commits
6 Commits
v2026.2.13
...
v2026.2.14
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
819402d9f1 | ||
|
|
33bc3e2fe9 | ||
|
|
2b71b3f345 | ||
|
|
3bc28ba78c | ||
|
|
72df9cb457 | ||
|
|
98e5747fdc |
19
.github/actions/deploy/deploy.mjs
vendored
19
.github/actions/deploy/deploy.mjs
vendored
@@ -33,24 +33,22 @@ const replicaConfig = {
|
||||
stable: {
|
||||
front: Number(process.env.PRODUCTION_FRONT_REPLICA) || 2,
|
||||
graphql: Number(process.env.PRODUCTION_GRAPHQL_REPLICA) || 2,
|
||||
doc: Number(process.env.PRODUCTION_DOC_REPLICA) || 2,
|
||||
},
|
||||
beta: {
|
||||
front: Number(process.env.BETA_FRONT_REPLICA) || 1,
|
||||
graphql: Number(process.env.BETA_GRAPHQL_REPLICA) || 1,
|
||||
doc: Number(process.env.BETA_DOC_REPLICA) || 1,
|
||||
},
|
||||
canary: { front: 1, graphql: 1, doc: 1 },
|
||||
canary: { front: 1, graphql: 1 },
|
||||
};
|
||||
|
||||
const cpuConfig = {
|
||||
beta: { front: '1', graphql: '1', doc: '1' },
|
||||
canary: { front: '500m', graphql: '1', doc: '500m' },
|
||||
beta: { front: '1', graphql: '1' },
|
||||
canary: { front: '500m', graphql: '1' },
|
||||
};
|
||||
|
||||
const memoryConfig = {
|
||||
beta: { front: '1Gi', graphql: '1Gi', doc: '1Gi' },
|
||||
canary: { front: '512Mi', graphql: '512Mi', doc: '512Mi' },
|
||||
beta: { front: '2Gi', graphql: '1Gi' },
|
||||
canary: { front: '512Mi', graphql: '512Mi' },
|
||||
};
|
||||
|
||||
const createHelmCommand = ({ isDryRun }) => {
|
||||
@@ -80,7 +78,6 @@ const createHelmCommand = ({ isDryRun }) => {
|
||||
const serviceAnnotations = [
|
||||
`--set-json front.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 doc.serviceAccount.annotations="{ \\"iam.gke.io/gcp-service-account\\": \\"${APP_IAM_ACCOUNT}\\" }"`,
|
||||
].concat(
|
||||
isProduction || isBeta || isInternal
|
||||
? [
|
||||
@@ -98,7 +95,6 @@ const createHelmCommand = ({ isDryRun }) => {
|
||||
? [
|
||||
`--set-json front.nodeSelector="${spotNodeSelector}"`,
|
||||
`--set-json graphql.nodeSelector="${spotNodeSelector}"`,
|
||||
`--set-json doc.nodeSelector="${spotNodeSelector}"`,
|
||||
]
|
||||
: [];
|
||||
|
||||
@@ -109,14 +105,12 @@ const createHelmCommand = ({ isDryRun }) => {
|
||||
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}"`,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -155,9 +149,6 @@ const createHelmCommand = ({ isDryRun }) => {
|
||||
`--set graphql.replicaCount=${replica.graphql}`,
|
||||
`--set-string graphql.image.tag="${imageTag}"`,
|
||||
`--set-string graphql.app.host="${primaryHost}"`,
|
||||
`--set-string doc.image.tag="${imageTag}"`,
|
||||
`--set-string doc.app.host="${primaryHost}"`,
|
||||
`--set doc.replicaCount=${replica.doc}`,
|
||||
...serviceAnnotations,
|
||||
...spotScheduling,
|
||||
...resources,
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
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 "doc.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 "doc.fullname" . }}'
|
||||
export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "doc.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 "doc.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 }}
|
||||
@@ -1,63 +0,0 @@
|
||||
{{/*
|
||||
Expand the name of the chart.
|
||||
*/}}
|
||||
{{- define "doc.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 "doc.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 "doc.chart" -}}
|
||||
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Common labels
|
||||
*/}}
|
||||
{{- define "doc.labels" -}}
|
||||
helm.sh/chart: {{ include "doc.chart" . }}
|
||||
{{ include "doc.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 "doc.selectorLabels" -}}
|
||||
app.kubernetes.io/name: {{ include "doc.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Create the name of the service account to use
|
||||
*/}}
|
||||
{{- define "doc.serviceAccountName" -}}
|
||||
{{- if .Values.serviceAccount.create }}
|
||||
{{- default (include "doc.fullname" .) .Values.global.docService.name }}
|
||||
{{- else }}
|
||||
{{- default "default" .Values.global.docService.name }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@@ -1,118 +0,0 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "doc.fullname" . }}
|
||||
labels:
|
||||
{{- include "doc.labels" . | nindent 4 }}
|
||||
spec:
|
||||
replicas: {{ .Values.replicaCount }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "doc.selectorLabels" . | nindent 6 }}
|
||||
template:
|
||||
metadata:
|
||||
{{- with .Values.podAnnotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
labels:
|
||||
{{- include "doc.selectorLabels" . | nindent 8 }}
|
||||
spec:
|
||||
{{- with .Values.imagePullSecrets }}
|
||||
imagePullSecrets:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
serviceAccountName: {{ include "doc.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: "{{ .Values.global.deployment.type }}"
|
||||
- name: DEPLOYMENT_PLATFORM
|
||||
value: "{{ .Values.global.deployment.platform }}"
|
||||
- name: SERVER_FLAVOR
|
||||
value: "doc"
|
||||
- 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.global.docService.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 }}"
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: {{ .Values.global.docService.port }}
|
||||
protocol: TCP
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /info
|
||||
port: http
|
||||
initialDelaySeconds: {{ .Values.probe.initialDelaySeconds }}
|
||||
timeoutSeconds: {{ .Values.probe.timeoutSeconds }}
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /info
|
||||
port: http
|
||||
initialDelaySeconds: {{ .Values.probe.initialDelaySeconds }}
|
||||
timeoutSeconds: {{ .Values.probe.timeoutSeconds }}
|
||||
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 }}
|
||||
@@ -1,12 +0,0 @@
|
||||
{{- if .Values.serviceAccount.create -}}
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: {{ include "doc.serviceAccountName" . }}
|
||||
labels:
|
||||
{{- include "doc.labels" . | nindent 4 }}
|
||||
{{- with .Values.serviceAccount.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@@ -1,15 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: "{{ include "doc.fullname" . }}-test-connection"
|
||||
labels:
|
||||
{{- include "doc.labels" . | nindent 4 }}
|
||||
annotations:
|
||||
"helm.sh/hook": test
|
||||
spec:
|
||||
containers:
|
||||
- name: wget
|
||||
image: busybox
|
||||
command: ['wget']
|
||||
args: ['{{ include "doc.fullname" . }}:{{ .Values.global.docService.port }}']
|
||||
restartPolicy: Never
|
||||
@@ -88,8 +88,6 @@ spec:
|
||||
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 }}
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ include "doc.fullname" . }}
|
||||
name: {{ .Values.global.docService.name }}
|
||||
labels:
|
||||
{{- include "doc.labels" . | nindent 4 }}
|
||||
{{- with .Values.service.annotations }}
|
||||
{{- include "front.labels" . | nindent 4 }}
|
||||
{{- with .Values.services.doc.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
type: {{ .Values.service.type }}
|
||||
type: {{ .Values.services.doc.type }}
|
||||
ports:
|
||||
- port: {{ .Values.global.docService.port }}
|
||||
targetPort: http
|
||||
protocol: TCP
|
||||
name: http
|
||||
selector:
|
||||
{{- include "doc.selectorLabels" . | nindent 4 }}
|
||||
{{- include "front.selectorLabels" . | nindent 4 }}
|
||||
3
.github/helm/affine/charts/front/values.yaml
vendored
3
.github/helm/affine/charts/front/values.yaml
vendored
@@ -57,6 +57,9 @@ services:
|
||||
type: ClusterIP
|
||||
port: 8080
|
||||
annotations: {}
|
||||
doc:
|
||||
type: ClusterIP
|
||||
annotations: {}
|
||||
|
||||
nodeSelector: {}
|
||||
tolerations: []
|
||||
|
||||
10
.github/helm/affine/values.yaml
vendored
10
.github/helm/affine/values.yaml
vendored
@@ -47,12 +47,6 @@ graphql:
|
||||
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:
|
||||
@@ -71,3 +65,7 @@ front:
|
||||
name: affine-web
|
||||
type: ClusterIP
|
||||
port: 8080
|
||||
doc:
|
||||
type: ClusterIP
|
||||
annotations:
|
||||
cloud.google.com/backend-config: '{"default": "affine-api-backendconfig"}'
|
||||
|
||||
6
.github/workflows/auto-labeler.yml
vendored
6
.github/workflows/auto-labeler.yml
vendored
@@ -1,6 +1,10 @@
|
||||
name: 'Pull Request Labeler'
|
||||
on:
|
||||
- pull_request_target
|
||||
pull_request_target:
|
||||
types:
|
||||
- opened
|
||||
- reopened
|
||||
- synchronize
|
||||
|
||||
jobs:
|
||||
triage:
|
||||
|
||||
296
.github/workflows/build-test.yml
vendored
296
.github/workflows/build-test.yml
vendored
@@ -210,18 +210,13 @@ jobs:
|
||||
e2e-blocksuite-cross-browser-test:
|
||||
name: E2E BlockSuite Cross Browser Test
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
shard: [1]
|
||||
browser: ['chromium', 'firefox', 'webkit']
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Setup Node.js
|
||||
uses: ./.github/actions/setup-node
|
||||
with:
|
||||
playwright-install: true
|
||||
playwright-platform: ${{ matrix.browser }}
|
||||
playwright-platform: 'chromium,firefox,webkit'
|
||||
electron-install: false
|
||||
full-cache: true
|
||||
|
||||
@@ -229,18 +224,64 @@ jobs:
|
||||
run: yarn workspace @blocksuite/playground build
|
||||
|
||||
- name: Run playwright tests
|
||||
env:
|
||||
BROWSER: ${{ matrix.browser }}
|
||||
run: yarn workspace @affine-test/blocksuite test "cross-platform/" --forbid-only --shard=${{ matrix.shard }}/${{ strategy.job-total }}
|
||||
run: |
|
||||
yarn workspace @blocksuite/integration-test test:unit
|
||||
yarn workspace @affine-test/blocksuite test "cross-platform/" --forbid-only
|
||||
|
||||
- name: Upload test results
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: test-results-e2e-bs-cross-browser-${{ matrix.browser }}-${{ matrix.shard }}
|
||||
name: test-results-e2e-bs-cross-browser
|
||||
path: ./test-results
|
||||
if-no-files-found: ignore
|
||||
|
||||
bundler-matrix:
|
||||
name: Bundler Matrix (${{ matrix.bundler }})
|
||||
runs-on: ubuntu-24.04-arm
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
bundler: [webpack, rspack]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Setup Node.js
|
||||
uses: ./.github/actions/setup-node
|
||||
with:
|
||||
playwright-install: false
|
||||
electron-install: false
|
||||
full-cache: true
|
||||
|
||||
- name: Run frontend build matrix
|
||||
env:
|
||||
AFFINE_BUNDLER: ${{ matrix.bundler }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
packages=(
|
||||
"@affine/web"
|
||||
"@affine/mobile"
|
||||
"@affine/ios"
|
||||
"@affine/android"
|
||||
"@affine/admin"
|
||||
"@affine/electron-renderer"
|
||||
)
|
||||
summary="test-results-bundler-${AFFINE_BUNDLER}.txt"
|
||||
: > "$summary"
|
||||
for pkg in "${packages[@]}"; do
|
||||
start=$(date +%s)
|
||||
yarn affine "$pkg" build
|
||||
end=$(date +%s)
|
||||
echo "${pkg},$((end-start))" >> "$summary"
|
||||
done
|
||||
|
||||
- name: Upload bundler timing
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: test-results-bundler-${{ matrix.bundler }}
|
||||
path: ./test-results-bundler-${{ matrix.bundler }}.txt
|
||||
if-no-files-found: ignore
|
||||
|
||||
e2e-test:
|
||||
name: E2E Test
|
||||
runs-on: ubuntu-24.04-arm
|
||||
@@ -307,7 +348,7 @@ jobs:
|
||||
name: Unit Test
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- build-native
|
||||
- build-native-linux
|
||||
env:
|
||||
DISTRIBUTION: web
|
||||
strategy:
|
||||
@@ -321,6 +362,7 @@ jobs:
|
||||
with:
|
||||
electron-install: true
|
||||
playwright-install: true
|
||||
playwright-platform: 'chromium,firefox,webkit'
|
||||
full-cache: true
|
||||
|
||||
- name: Download affine.linux-x64-gnu.node
|
||||
@@ -341,7 +383,39 @@ jobs:
|
||||
name: affine
|
||||
fail_ci_if_error: false
|
||||
|
||||
build-native:
|
||||
build-native-linux:
|
||||
name: Build AFFiNE native (x86_64-unknown-linux-gnu)
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
CARGO_PROFILE_RELEASE_DEBUG: '1'
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Setup Node.js
|
||||
uses: ./.github/actions/setup-node
|
||||
with:
|
||||
extra-flags: workspaces focus @affine/native
|
||||
electron-install: false
|
||||
- name: Setup filename
|
||||
id: filename
|
||||
working-directory: ${{ github.workspace }}
|
||||
shell: bash
|
||||
run: |
|
||||
export PLATFORM_ARCH_ABI=$(node -e "console.log(require('@napi-rs/cli').parseTriple('x86_64-unknown-linux-gnu').platformArchABI)")
|
||||
echo "filename=affine.$PLATFORM_ARCH_ABI.node" >> "$GITHUB_OUTPUT"
|
||||
- name: Build AFFiNE native
|
||||
uses: ./.github/actions/build-rust
|
||||
with:
|
||||
target: x86_64-unknown-linux-gnu
|
||||
package: '@affine/native'
|
||||
- name: Upload ${{ steps.filename.outputs.filename }}
|
||||
uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: ${{ steps.filename.outputs.filename }}
|
||||
path: ${{ github.workspace }}/packages/frontend/native/${{ steps.filename.outputs.filename }}
|
||||
if-no-files-found: error
|
||||
|
||||
build-native-macos:
|
||||
name: Build AFFiNE native (${{ matrix.spec.target }})
|
||||
runs-on: ${{ matrix.spec.os }}
|
||||
env:
|
||||
@@ -350,7 +424,6 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
spec:
|
||||
- { os: ubuntu-latest, target: x86_64-unknown-linux-gnu }
|
||||
- { os: macos-latest, target: x86_64-apple-darwin }
|
||||
- { os: macos-latest, target: aarch64-apple-darwin }
|
||||
|
||||
@@ -383,7 +456,7 @@ jobs:
|
||||
|
||||
# Split Windows build because it's too slow
|
||||
# and other ci jobs required linux native
|
||||
build-windows-native:
|
||||
build-native-windows:
|
||||
name: Build AFFiNE native (${{ matrix.spec.target }})
|
||||
runs-on: ${{ matrix.spec.os }}
|
||||
env:
|
||||
@@ -483,7 +556,7 @@ jobs:
|
||||
name: Native Unit Test
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- build-native
|
||||
- build-native-linux
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Setup Node.js
|
||||
@@ -577,8 +650,6 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- build-server-native
|
||||
strategy:
|
||||
fail-fast: false
|
||||
env:
|
||||
NODE_ENV: test
|
||||
DATABASE_URL: postgresql://affine:affine@localhost:5432/affine
|
||||
@@ -819,11 +890,51 @@ jobs:
|
||||
- name: Run tests
|
||||
run: cargo nextest run --workspace --exclude affine_server_native --features use-as-lib --release --no-fail-fast
|
||||
|
||||
copilot-test-filter:
|
||||
name: Copilot test filter
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
run-api: ${{ steps.decision.outputs.run_api }}
|
||||
run-e2e: ${{ steps.decision.outputs.run_e2e }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: dorny/paths-filter@v3
|
||||
id: copilot-filter
|
||||
with:
|
||||
filters: |
|
||||
api:
|
||||
- 'packages/backend/server/src/plugins/copilot/**'
|
||||
- 'packages/backend/server/tests/copilot.*'
|
||||
e2e:
|
||||
- 'packages/backend/server/src/plugins/copilot/**'
|
||||
- 'packages/backend/server/tests/copilot.*'
|
||||
- 'packages/frontend/core/src/blocksuite/ai/**'
|
||||
- 'packages/frontend/core/src/modules/workspace-indexer-embedding/**'
|
||||
- 'tests/affine-cloud-copilot/**'
|
||||
|
||||
- name: Decide test scope
|
||||
id: decision
|
||||
run: |
|
||||
if [[ "${{ steps.copilot-filter.outputs.api }}" == "true" ]]; then
|
||||
echo "run_api=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "run_api=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
if [[ "${{ steps.copilot-filter.outputs.e2e }}" == "true" ]]; then
|
||||
echo "run_e2e=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "run_e2e=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
copilot-api-test:
|
||||
name: Server Copilot Api Test
|
||||
if: ${{ needs.copilot-test-filter.outputs.run-api == 'true' }}
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- build-server-native
|
||||
- copilot-test-filter
|
||||
env:
|
||||
NODE_ENV: test
|
||||
DISTRIBUTION: web
|
||||
@@ -857,53 +968,29 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Check blocksuite update
|
||||
id: check-blocksuite-update
|
||||
env:
|
||||
BASE_REF: ${{ github.base_ref }}
|
||||
run: |
|
||||
if node ./scripts/detect-blocksuite-update.mjs "$BASE_REF"; then
|
||||
echo "skip=false" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "skip=true" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- uses: dorny/paths-filter@v3
|
||||
id: apifilter
|
||||
with:
|
||||
filters: |
|
||||
changed:
|
||||
- 'packages/backend/server/src/plugins/copilot/**'
|
||||
- 'packages/backend/server/tests/copilot.*'
|
||||
|
||||
- name: Setup Node.js
|
||||
if: ${{ steps.check-blocksuite-update.outputs.skip != 'true' || steps.apifilter.outputs.changed == 'true' }}
|
||||
uses: ./.github/actions/setup-node
|
||||
with:
|
||||
electron-install: false
|
||||
full-cache: true
|
||||
|
||||
- name: Download server-native.node
|
||||
if: ${{ steps.check-blocksuite-update.outputs.skip != 'true' || steps.apifilter.outputs.changed == 'true' }}
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: server-native.node
|
||||
path: ./packages/backend/native
|
||||
|
||||
- name: Prepare Server Test Environment
|
||||
if: ${{ steps.check-blocksuite-update.outputs.skip != 'true' || steps.apifilter.outputs.changed == 'true' }}
|
||||
env:
|
||||
SERVER_CONFIG: ${{ secrets.TEST_SERVER_CONFIG }}
|
||||
uses: ./.github/actions/server-test-env
|
||||
|
||||
- name: Run server tests
|
||||
if: ${{ steps.check-blocksuite-update.outputs.skip != 'true' || steps.apifilter.outputs.changed == 'true' }}
|
||||
run: yarn affine @affine/server test:copilot:coverage --forbid-only
|
||||
env:
|
||||
CARGO_TARGET_DIR: '${{ github.workspace }}/target'
|
||||
|
||||
- name: Upload server test coverage results
|
||||
if: ${{ steps.check-blocksuite-update.outputs.skip != 'true' || steps.apifilter.outputs.changed == 'true' }}
|
||||
uses: codecov/codecov-action@v5
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
@@ -914,6 +1001,7 @@ jobs:
|
||||
|
||||
copilot-e2e-test:
|
||||
name: Frontend Copilot E2E Test
|
||||
if: ${{ needs.copilot-test-filter.outputs.run-e2e == 'true' }}
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
DISTRIBUTION: web
|
||||
@@ -928,6 +1016,7 @@ jobs:
|
||||
shardTotal: [5]
|
||||
needs:
|
||||
- build-server-native
|
||||
- copilot-test-filter
|
||||
services:
|
||||
postgres:
|
||||
image: pgvector/pgvector:pg16
|
||||
@@ -951,30 +1040,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Check blocksuite update
|
||||
id: check-blocksuite-update
|
||||
env:
|
||||
BASE_REF: ${{ github.base_ref }}
|
||||
run: |
|
||||
if node ./scripts/detect-blocksuite-update.mjs "$BASE_REF"; then
|
||||
echo "skip=false" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "skip=true" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- uses: dorny/paths-filter@v3
|
||||
id: e2efilter
|
||||
with:
|
||||
filters: |
|
||||
changed:
|
||||
- 'packages/backend/server/src/plugins/copilot/**'
|
||||
- 'packages/backend/server/tests/copilot.*'
|
||||
- 'packages/frontend/core/src/blocksuite/ai/**'
|
||||
- 'packages/frontend/core/src/modules/workspace-indexer-embedding/**'
|
||||
- 'tests/affine-cloud-copilot/**'
|
||||
|
||||
- name: Setup Node.js
|
||||
if: ${{ steps.check-blocksuite-update.outputs.skip != 'true' || steps.e2efilter.outputs.changed == 'true' }}
|
||||
uses: ./.github/actions/setup-node
|
||||
with:
|
||||
playwright-install: true
|
||||
@@ -983,20 +1049,17 @@ jobs:
|
||||
hard-link-nm: false
|
||||
|
||||
- name: Download server-native.node
|
||||
if: ${{ steps.check-blocksuite-update.outputs.skip != 'true' || steps.e2efilter.outputs.changed == 'true' }}
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: server-native.node
|
||||
path: ./packages/backend/native
|
||||
|
||||
- name: Prepare Server Test Environment
|
||||
if: ${{ steps.check-blocksuite-update.outputs.skip != 'true' || steps.e2efilter.outputs.changed == 'true' }}
|
||||
env:
|
||||
SERVER_CONFIG: ${{ secrets.TEST_SERVER_CONFIG }}
|
||||
uses: ./.github/actions/server-test-env
|
||||
|
||||
- name: Run Copilot E2E Test ${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
|
||||
if: ${{ steps.check-blocksuite-update.outputs.skip != 'true' || steps.e2efilter.outputs.changed == 'true' }}
|
||||
uses: ./.github/actions/copilot-test
|
||||
with:
|
||||
script: yarn affine @affine-test/affine-cloud-copilot e2e --forbid-only --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
|
||||
@@ -1006,7 +1069,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- build-server-native
|
||||
- build-native
|
||||
- build-native-linux
|
||||
env:
|
||||
DISTRIBUTION: web
|
||||
DATABASE_URL: postgresql://affine:affine@localhost:5432/affine
|
||||
@@ -1099,7 +1162,9 @@ jobs:
|
||||
runs-on: ${{ matrix.spec.os }}
|
||||
needs:
|
||||
- build-electron-renderer
|
||||
- build-native
|
||||
- build-native-linux
|
||||
- build-native-macos
|
||||
- build-native-windows
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -1182,84 +1247,6 @@ jobs:
|
||||
if: ${{ matrix.spec.test && matrix.spec.os != 'ubuntu-latest' }}
|
||||
run: yarn affine @affine-test/affine-desktop e2e
|
||||
|
||||
- name: Upload test results
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: test-results-e2e-${{ matrix.spec.os }}-${{ matrix.spec.arch }}
|
||||
path: ./test-results
|
||||
if-no-files-found: ignore
|
||||
|
||||
desktop-bundle-check:
|
||||
name: Desktop bundle check (${{ matrix.spec.os }}, ${{ matrix.spec.platform }}, ${{ matrix.spec.arch }}, ${{ matrix.spec.target }}, ${{ matrix.spec.test }})
|
||||
runs-on: ${{ matrix.spec.os }}
|
||||
needs:
|
||||
- build-electron-renderer
|
||||
- build-native
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
spec:
|
||||
- {
|
||||
os: macos-latest,
|
||||
platform: macos,
|
||||
arch: x64,
|
||||
target: x86_64-apple-darwin,
|
||||
test: false,
|
||||
}
|
||||
- {
|
||||
os: macos-latest,
|
||||
platform: macos,
|
||||
arch: arm64,
|
||||
target: aarch64-apple-darwin,
|
||||
test: true,
|
||||
}
|
||||
- {
|
||||
os: ubuntu-latest,
|
||||
platform: linux,
|
||||
arch: x64,
|
||||
target: x86_64-unknown-linux-gnu,
|
||||
test: true,
|
||||
}
|
||||
- {
|
||||
os: windows-latest,
|
||||
platform: windows,
|
||||
arch: x64,
|
||||
target: x86_64-pc-windows-msvc,
|
||||
test: true,
|
||||
}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Setup Node.js
|
||||
uses: ./.github/actions/setup-node
|
||||
timeout-minutes: 10
|
||||
with:
|
||||
extra-flags: workspaces focus @affine/electron @affine/monorepo @affine-test/affine-desktop @affine/nbstore @toeverything/infra
|
||||
playwright-install: true
|
||||
hard-link-nm: false
|
||||
enableScripts: false
|
||||
|
||||
- name: Setup filename
|
||||
id: filename
|
||||
shell: bash
|
||||
run: |
|
||||
export PLATFORM_ARCH_ABI=$(node -e "console.log(require('@napi-rs/cli').parseTriple('${{ matrix.spec.target }}').platformArchABI)")
|
||||
echo "filename=affine.$PLATFORM_ARCH_ABI.node" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Download ${{ steps.filename.outputs.filename }}
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: ${{ steps.filename.outputs.filename }}
|
||||
path: ./packages/frontend/native
|
||||
|
||||
- name: Download web artifact
|
||||
uses: ./.github/actions/download-web
|
||||
with:
|
||||
path: packages/frontend/apps/electron/resources/web-static
|
||||
|
||||
- name: Build Desktop Layers
|
||||
run: yarn affine @affine/electron build
|
||||
|
||||
- name: Make bundle (macOS)
|
||||
if: ${{ matrix.spec.target == 'aarch64-apple-darwin' }}
|
||||
env:
|
||||
@@ -1299,6 +1286,14 @@ jobs:
|
||||
run: |
|
||||
yarn affine @affine/electron node ./scripts/macos-arm64-output-check.ts
|
||||
|
||||
- name: Upload test results
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: test-results-e2e-${{ matrix.spec.os }}-${{ matrix.spec.arch }}
|
||||
path: ./test-results
|
||||
if-no-files-found: ignore
|
||||
|
||||
test-done:
|
||||
needs:
|
||||
- analyze
|
||||
@@ -1312,8 +1307,9 @@ jobs:
|
||||
- e2e-blocksuite-cross-browser-test
|
||||
- e2e-mobile-test
|
||||
- unit-test
|
||||
- build-native
|
||||
- build-windows-native
|
||||
- build-native-linux
|
||||
- build-native-macos
|
||||
- build-native-windows
|
||||
- build-server-native
|
||||
- build-electron-renderer
|
||||
- native-unit-test
|
||||
@@ -1323,10 +1319,10 @@ jobs:
|
||||
- server-test
|
||||
- server-e2e-test
|
||||
- rust-test
|
||||
- copilot-test-filter
|
||||
- copilot-api-test
|
||||
- copilot-e2e-test
|
||||
- desktop-test
|
||||
- desktop-bundle-check
|
||||
- cloud-e2e-test
|
||||
if: always()
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
1
.github/workflows/pr-title-lint.yml
vendored
1
.github/workflows/pr-title-lint.yml
vendored
@@ -16,6 +16,7 @@ jobs:
|
||||
check-pull-request-title:
|
||||
name: Check pull request title
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event.action != 'edited' || github.event.changes.title != null }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Setup Node.js
|
||||
|
||||
@@ -2101,6 +2101,157 @@ describe('html to snapshot', () => {
|
||||
expect(nanoidReplacement(rawBlockSnapshot)).toEqual(blockSnapshot);
|
||||
});
|
||||
|
||||
test('paragraph with br should split into multiple blocks', async () => {
|
||||
const html = template(`<p>aaa<br>bbb<br>ccc</p>`);
|
||||
|
||||
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: 'aaa' }],
|
||||
},
|
||||
},
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
type: 'block',
|
||||
id: 'matchesReplaceMap[2]',
|
||||
flavour: 'affine:paragraph',
|
||||
props: {
|
||||
type: 'text',
|
||||
text: {
|
||||
'$blocksuite:internal:text$': true,
|
||||
delta: [{ insert: 'bbb' }],
|
||||
},
|
||||
},
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
type: 'block',
|
||||
id: 'matchesReplaceMap[3]',
|
||||
flavour: 'affine:paragraph',
|
||||
props: {
|
||||
type: 'text',
|
||||
text: {
|
||||
'$blocksuite:internal:text$': true,
|
||||
delta: [{ insert: 'ccc' }],
|
||||
},
|
||||
},
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const htmlAdapter = new HtmlAdapter(createJob(), provider);
|
||||
const rawBlockSnapshot = await htmlAdapter.toBlockSnapshot({
|
||||
file: html,
|
||||
});
|
||||
expect(nanoidReplacement(rawBlockSnapshot)).toEqual(blockSnapshot);
|
||||
});
|
||||
|
||||
test('paragraph with br should keep inline styles in each split line', async () => {
|
||||
const html = template(
|
||||
`<p><strong>aaa</strong><br><a href="https://www.google.com/">bbb</a><br><em>ccc</em></p>`
|
||||
);
|
||||
|
||||
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: 'aaa',
|
||||
attributes: {
|
||||
bold: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
type: 'block',
|
||||
id: 'matchesReplaceMap[2]',
|
||||
flavour: 'affine:paragraph',
|
||||
props: {
|
||||
type: 'text',
|
||||
text: {
|
||||
'$blocksuite:internal:text$': true,
|
||||
delta: [
|
||||
{
|
||||
insert: 'bbb',
|
||||
attributes: {
|
||||
link: 'https://www.google.com/',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
type: 'block',
|
||||
id: 'matchesReplaceMap[3]',
|
||||
flavour: 'affine:paragraph',
|
||||
props: {
|
||||
type: 'text',
|
||||
text: {
|
||||
'$blocksuite:internal:text$': true,
|
||||
delta: [
|
||||
{
|
||||
insert: 'ccc',
|
||||
attributes: {
|
||||
italic: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const htmlAdapter = new HtmlAdapter(createJob(), provider);
|
||||
const rawBlockSnapshot = await htmlAdapter.toBlockSnapshot({
|
||||
file: html,
|
||||
});
|
||||
expect(nanoidReplacement(rawBlockSnapshot)).toEqual(blockSnapshot);
|
||||
});
|
||||
|
||||
test('nested list', async () => {
|
||||
const html = template(`<ul><li>111<ul><li>222</li></ul></li></ul>`);
|
||||
|
||||
|
||||
@@ -37,6 +37,126 @@ const tagsInAncestor = (o: NodeProps<HtmlAST>, tagNames: Array<string>) => {
|
||||
return false;
|
||||
};
|
||||
|
||||
const splitDeltaByNewline = (delta: DeltaInsert[]) => {
|
||||
const lines: DeltaInsert[][] = [[]];
|
||||
const pending = [...delta];
|
||||
|
||||
while (pending.length > 0) {
|
||||
const op = pending.shift();
|
||||
if (!op) continue;
|
||||
|
||||
const insert = op.insert;
|
||||
if (typeof insert !== 'string') {
|
||||
lines[lines.length - 1].push(op);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!insert.includes('\n')) {
|
||||
if (insert.length === 0) {
|
||||
continue;
|
||||
}
|
||||
lines[lines.length - 1].push(op);
|
||||
continue;
|
||||
}
|
||||
|
||||
const splitIndex = insert.indexOf('\n');
|
||||
const linePart = insert.slice(0, splitIndex);
|
||||
const remainPart = insert.slice(splitIndex + 1);
|
||||
if (linePart.length > 0) {
|
||||
lines[lines.length - 1].push({ ...op, insert: linePart });
|
||||
}
|
||||
lines.push([]);
|
||||
if (remainPart) {
|
||||
pending.unshift({ ...op, insert: remainPart });
|
||||
}
|
||||
}
|
||||
|
||||
return lines;
|
||||
};
|
||||
|
||||
const hasBlockElementDescendant = (node: HtmlAST): boolean => {
|
||||
if (!HastUtils.isElement(node)) {
|
||||
return false;
|
||||
}
|
||||
return node.children.some(child => {
|
||||
if (!HastUtils.isElement(child)) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
(HastUtils.isTagBlock(child.tagName) && child.tagName !== 'br') ||
|
||||
hasBlockElementDescendant(child)
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const getParagraphDeltas = (
|
||||
node: HtmlAST,
|
||||
delta: DeltaInsert[]
|
||||
): DeltaInsert[][] => {
|
||||
if (!HastUtils.isElement(node)) return [delta];
|
||||
if (hasBlockElementDescendant(node)) return [delta];
|
||||
|
||||
const hasBr = !!HastUtils.querySelector(node, 'br');
|
||||
if (!hasBr) return [delta];
|
||||
|
||||
const hasNewline = delta.some(
|
||||
op => typeof op.insert === 'string' && op.insert.includes('\n')
|
||||
);
|
||||
if (!hasNewline) return [delta];
|
||||
|
||||
return splitDeltaByNewline(delta);
|
||||
};
|
||||
|
||||
const openParagraphBlocks = (
|
||||
deltas: DeltaInsert[][],
|
||||
type: string,
|
||||
// AST walker context from html adapter transform pipeline.
|
||||
walkerContext: any
|
||||
) => {
|
||||
for (const delta of deltas) {
|
||||
walkerContext
|
||||
.openNode(
|
||||
{
|
||||
type: 'block',
|
||||
id: nanoid(),
|
||||
flavour: 'affine:paragraph',
|
||||
props: { type, text: { '$blocksuite:internal:text$': true, delta } },
|
||||
children: [],
|
||||
},
|
||||
'children'
|
||||
)
|
||||
.closeNode();
|
||||
}
|
||||
};
|
||||
|
||||
const MULTI_PARAGRAPH_EMITTED_NODES_CONTEXT_KEY =
|
||||
'affine:paragraph:multi-emitted-nodes';
|
||||
|
||||
const markMultiParagraphEmitted = (walkerContext: any, node: HtmlAST) => {
|
||||
const emittedNodes =
|
||||
(walkerContext.getGlobalContext(
|
||||
MULTI_PARAGRAPH_EMITTED_NODES_CONTEXT_KEY
|
||||
) as WeakSet<object> | undefined) ?? new WeakSet<object>();
|
||||
emittedNodes.add(node as object);
|
||||
walkerContext.setGlobalContext(
|
||||
MULTI_PARAGRAPH_EMITTED_NODES_CONTEXT_KEY,
|
||||
emittedNodes
|
||||
);
|
||||
};
|
||||
|
||||
const consumeMultiParagraphEmittedMark = (
|
||||
walkerContext: any,
|
||||
node: HtmlAST
|
||||
) => {
|
||||
const emittedNodes = walkerContext.getGlobalContext(
|
||||
MULTI_PARAGRAPH_EMITTED_NODES_CONTEXT_KEY
|
||||
) as WeakSet<object> | undefined;
|
||||
if (!emittedNodes) {
|
||||
return false;
|
||||
}
|
||||
return emittedNodes.delete(node as object);
|
||||
};
|
||||
|
||||
export const paragraphBlockHtmlAdapterMatcher: BlockHtmlAdapterMatcher = {
|
||||
flavour: ParagraphBlockSchema.model.flavour,
|
||||
toMatch: o =>
|
||||
@@ -88,41 +208,37 @@ export const paragraphBlockHtmlAdapterMatcher: BlockHtmlAdapterMatcher = {
|
||||
!tagsInAncestor(o, ['p', 'li']) &&
|
||||
HastUtils.isParagraphLike(o.node)
|
||||
) {
|
||||
walkerContext
|
||||
.openNode(
|
||||
{
|
||||
type: 'block',
|
||||
id: nanoid(),
|
||||
flavour: 'affine:paragraph',
|
||||
props: {
|
||||
type: 'text',
|
||||
text: {
|
||||
'$blocksuite:internal:text$': true,
|
||||
delta: deltaConverter.astToDelta(o.node),
|
||||
},
|
||||
},
|
||||
children: [],
|
||||
},
|
||||
'children'
|
||||
)
|
||||
.closeNode();
|
||||
const delta = deltaConverter.astToDelta(o.node);
|
||||
const deltas = getParagraphDeltas(o.node, delta);
|
||||
openParagraphBlocks(deltas, 'text', walkerContext);
|
||||
walkerContext.skipAllChildren();
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'p': {
|
||||
const type = walkerContext.getGlobalContext('hast:blockquote')
|
||||
? 'quote'
|
||||
: 'text';
|
||||
const delta = deltaConverter.astToDelta(o.node);
|
||||
const deltas = getParagraphDeltas(o.node, delta);
|
||||
|
||||
if (deltas.length > 1) {
|
||||
openParagraphBlocks(deltas, type, walkerContext);
|
||||
markMultiParagraphEmitted(walkerContext, o.node);
|
||||
walkerContext.skipAllChildren();
|
||||
break;
|
||||
}
|
||||
|
||||
walkerContext.openNode(
|
||||
{
|
||||
type: 'block',
|
||||
id: nanoid(),
|
||||
flavour: 'affine:paragraph',
|
||||
props: {
|
||||
type: walkerContext.getGlobalContext('hast:blockquote')
|
||||
? 'quote'
|
||||
: 'text',
|
||||
type,
|
||||
text: {
|
||||
'$blocksuite:internal:text$': true,
|
||||
delta: deltaConverter.astToDelta(o.node),
|
||||
delta,
|
||||
},
|
||||
},
|
||||
children: [],
|
||||
@@ -192,6 +308,9 @@ export const paragraphBlockHtmlAdapterMatcher: BlockHtmlAdapterMatcher = {
|
||||
break;
|
||||
}
|
||||
case 'p': {
|
||||
if (consumeMultiParagraphEmittedMark(walkerContext, o.node)) {
|
||||
break;
|
||||
}
|
||||
if (
|
||||
o.next?.type === 'element' &&
|
||||
o.next.tagName === 'div' &&
|
||||
|
||||
@@ -86,6 +86,7 @@ export class PageClipboard extends ReadOnlyClipboard {
|
||||
|
||||
if (this.std.store.readonly) return;
|
||||
this.std.store.captureSync();
|
||||
let hasPasteTarget = false;
|
||||
this.std.command
|
||||
.chain()
|
||||
.try<{}>(cmd => [
|
||||
@@ -144,18 +145,39 @@ export class PageClipboard extends ReadOnlyClipboard {
|
||||
if (!ctx.parentBlock) {
|
||||
return;
|
||||
}
|
||||
hasPasteTarget = true;
|
||||
this.std.clipboard
|
||||
.paste(
|
||||
e,
|
||||
this.std.store,
|
||||
ctx.parentBlock.model.id,
|
||||
ctx.blockIndex ? ctx.blockIndex + 1 : 1
|
||||
ctx.blockIndex !== undefined ? ctx.blockIndex + 1 : 1
|
||||
)
|
||||
.catch(console.error);
|
||||
|
||||
return next();
|
||||
})
|
||||
.run();
|
||||
|
||||
if (hasPasteTarget) return;
|
||||
|
||||
// If no valid selection target exists (for example, stale block selection
|
||||
// right after cut), create/focus the default paragraph and paste after it.
|
||||
const firstParagraphId = document
|
||||
.querySelector('affine-page-root')
|
||||
?.focusFirstParagraph?.()?.id;
|
||||
const parentModel = firstParagraphId
|
||||
? this.std.store.getParent(firstParagraphId)
|
||||
: null;
|
||||
const paragraphIndex =
|
||||
firstParagraphId && parentModel
|
||||
? parentModel.children.findIndex(child => child.id === firstParagraphId)
|
||||
: -1;
|
||||
const insertIndex = paragraphIndex >= 0 ? paragraphIndex + 1 : undefined;
|
||||
|
||||
this.std.clipboard
|
||||
.paste(e, this.std.store, parentModel?.id, insertIndex)
|
||||
.catch(console.error);
|
||||
};
|
||||
|
||||
override mounted() {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { FontFamily, FontStyle, FontWeight } from '@blocksuite/affine-model';
|
||||
import { createIdentifier } from '@blocksuite/global/di';
|
||||
import { IS_FIREFOX } from '@blocksuite/global/env';
|
||||
import { LifeCycleWatcher } from '@blocksuite/std';
|
||||
@@ -20,33 +21,171 @@ const initFontFace = IS_FIREFOX
|
||||
export class FontLoaderService extends LifeCycleWatcher {
|
||||
static override readonly key = 'font-loader';
|
||||
|
||||
private static readonly DEFERRED_LOAD_DELAY_MS = 5000;
|
||||
|
||||
private static readonly DEFERRED_LOAD_BATCH_SIZE = 4;
|
||||
|
||||
private static readonly DEFERRED_LOAD_BATCH_INTERVAL_MS = 1000;
|
||||
|
||||
private _idleLoadTaskId: number | null = null;
|
||||
|
||||
private _lazyLoadTimeoutId: number | null = null;
|
||||
|
||||
private _deferredFontsQueue: FontConfig[] = [];
|
||||
|
||||
private _deferredFontsCursor = 0;
|
||||
|
||||
private readonly _loadedFontKeys = new Set<string>();
|
||||
|
||||
readonly fontFaces: FontFace[] = [];
|
||||
|
||||
get ready() {
|
||||
return Promise.all(this.fontFaces.map(fontFace => fontFace.loaded));
|
||||
}
|
||||
|
||||
private readonly _fontKey = ({ font, weight, style, url }: FontConfig) => {
|
||||
return `${font}:${weight}:${style}:${url}`;
|
||||
};
|
||||
|
||||
private readonly _isCriticalCanvasFont = ({
|
||||
font,
|
||||
weight,
|
||||
style,
|
||||
}: FontConfig) => {
|
||||
if (style !== FontStyle.Normal) return false;
|
||||
|
||||
if (font === FontFamily.Poppins) {
|
||||
return (
|
||||
weight === FontWeight.Regular ||
|
||||
weight === FontWeight.Medium ||
|
||||
weight === FontWeight.SemiBold
|
||||
);
|
||||
}
|
||||
|
||||
if (font === FontFamily.Inter) {
|
||||
return weight === FontWeight.Regular || weight === FontWeight.SemiBold;
|
||||
}
|
||||
|
||||
if (font === FontFamily.Kalam) {
|
||||
// Mindmap style four uses bold Kalam text.
|
||||
// We map to SemiBold because this is the strongest shipped Kalam weight.
|
||||
return weight === FontWeight.SemiBold;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
private readonly _scheduleDeferredLoad = (fonts: FontConfig[]) => {
|
||||
if (fonts.length === 0 || typeof window === 'undefined') {
|
||||
return;
|
||||
}
|
||||
this._deferredFontsQueue = fonts;
|
||||
this._deferredFontsCursor = 0;
|
||||
|
||||
const win = window as Window & {
|
||||
requestIdleCallback?: (
|
||||
callback: () => void,
|
||||
options?: { timeout?: number }
|
||||
) => number;
|
||||
cancelIdleCallback?: (handle: number) => void;
|
||||
};
|
||||
|
||||
const scheduleBatch = (delayMs: number) => {
|
||||
this._lazyLoadTimeoutId = window.setTimeout(() => {
|
||||
this._lazyLoadTimeoutId = null;
|
||||
const runBatch = () => {
|
||||
this._idleLoadTaskId = null;
|
||||
|
||||
const start = this._deferredFontsCursor;
|
||||
const end = Math.min(
|
||||
start + FontLoaderService.DEFERRED_LOAD_BATCH_SIZE,
|
||||
this._deferredFontsQueue.length
|
||||
);
|
||||
const batch = this._deferredFontsQueue.slice(start, end);
|
||||
this._deferredFontsCursor = end;
|
||||
this.load(batch);
|
||||
|
||||
if (this._deferredFontsCursor < this._deferredFontsQueue.length) {
|
||||
scheduleBatch(FontLoaderService.DEFERRED_LOAD_BATCH_INTERVAL_MS);
|
||||
}
|
||||
};
|
||||
|
||||
if (typeof win.requestIdleCallback === 'function') {
|
||||
this._idleLoadTaskId = win.requestIdleCallback(runBatch, {
|
||||
timeout: 2000,
|
||||
});
|
||||
return;
|
||||
}
|
||||
runBatch();
|
||||
}, delayMs);
|
||||
};
|
||||
|
||||
scheduleBatch(FontLoaderService.DEFERRED_LOAD_DELAY_MS);
|
||||
};
|
||||
|
||||
private readonly _cancelDeferredLoad = () => {
|
||||
if (typeof window === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
const win = window as Window & {
|
||||
cancelIdleCallback?: (handle: number) => void;
|
||||
};
|
||||
|
||||
if (
|
||||
this._idleLoadTaskId !== null &&
|
||||
typeof win.cancelIdleCallback === 'function'
|
||||
) {
|
||||
win.cancelIdleCallback(this._idleLoadTaskId);
|
||||
this._idleLoadTaskId = null;
|
||||
}
|
||||
if (this._lazyLoadTimeoutId !== null) {
|
||||
window.clearTimeout(this._lazyLoadTimeoutId);
|
||||
this._lazyLoadTimeoutId = null;
|
||||
}
|
||||
this._deferredFontsQueue = [];
|
||||
this._deferredFontsCursor = 0;
|
||||
};
|
||||
|
||||
load(fonts: FontConfig[]) {
|
||||
this.fontFaces.push(
|
||||
...fonts.map(font => {
|
||||
const fontFace = initFontFace(font);
|
||||
document.fonts.add(fontFace);
|
||||
fontFace.load().catch(console.error);
|
||||
return fontFace;
|
||||
})
|
||||
);
|
||||
for (const font of fonts) {
|
||||
const key = this._fontKey(font);
|
||||
if (this._loadedFontKeys.has(key)) {
|
||||
continue;
|
||||
}
|
||||
this._loadedFontKeys.add(key);
|
||||
const fontFace = initFontFace(font);
|
||||
document.fonts.add(fontFace);
|
||||
fontFace.load().catch(console.error);
|
||||
this.fontFaces.push(fontFace);
|
||||
}
|
||||
}
|
||||
|
||||
override mounted() {
|
||||
const config = this.std.getOptional(FontConfigIdentifier);
|
||||
if (config) {
|
||||
this.load(config);
|
||||
if (!config || config.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const criticalFonts = config.filter(this._isCriticalCanvasFont);
|
||||
const eagerFonts =
|
||||
criticalFonts.length > 0 ? criticalFonts : config.slice(0, 3);
|
||||
const eagerFontKeySet = new Set(eagerFonts.map(this._fontKey));
|
||||
const deferredFonts = config.filter(
|
||||
font => !eagerFontKeySet.has(this._fontKey(font))
|
||||
);
|
||||
|
||||
this.load(eagerFonts);
|
||||
this._scheduleDeferredLoad(deferredFonts);
|
||||
}
|
||||
|
||||
override unmounted() {
|
||||
this.fontFaces.forEach(fontFace => document.fonts.delete(fontFace));
|
||||
this._cancelDeferredLoad();
|
||||
for (const fontFace of this.fontFaces) {
|
||||
document.fonts.delete(fontFace);
|
||||
}
|
||||
this.fontFaces.splice(0, this.fontFaces.length);
|
||||
this._loadedFontKeys.clear();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,17 @@ import {
|
||||
} from '../config.js';
|
||||
import type { AffineDragHandleWidget } from '../drag-handle.js';
|
||||
|
||||
type HoveredElemArea = {
|
||||
left: number;
|
||||
top: number;
|
||||
right: number;
|
||||
bottom: number;
|
||||
width: number;
|
||||
height: number;
|
||||
padding: number;
|
||||
containerWidth: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Used to control the drag handle visibility in edgeless mode
|
||||
*
|
||||
@@ -21,6 +32,52 @@ import type { AffineDragHandleWidget } from '../drag-handle.js';
|
||||
* 2. Multiple selection is not supported
|
||||
*/
|
||||
export class EdgelessWatcher {
|
||||
private _pendingHoveredElemArea: HoveredElemArea | null = null;
|
||||
|
||||
private _lastAppliedHoveredElemArea: HoveredElemArea | null = null;
|
||||
|
||||
private _showDragHandleRafId: number | null = null;
|
||||
|
||||
private _surfaceElementUpdatedRafId: number | null = null;
|
||||
|
||||
private readonly _cloneArea = (area: HoveredElemArea): HoveredElemArea => ({
|
||||
left: area.left,
|
||||
top: area.top,
|
||||
right: area.right,
|
||||
bottom: area.bottom,
|
||||
width: area.width,
|
||||
height: area.height,
|
||||
padding: area.padding,
|
||||
containerWidth: area.containerWidth,
|
||||
});
|
||||
|
||||
private readonly _isAreaEqual = (
|
||||
left: HoveredElemArea | null,
|
||||
right: HoveredElemArea | null
|
||||
) => {
|
||||
if (!left || !right) return false;
|
||||
return (
|
||||
left.left === right.left &&
|
||||
left.top === right.top &&
|
||||
left.right === right.right &&
|
||||
left.bottom === right.bottom &&
|
||||
left.width === right.width &&
|
||||
left.height === right.height &&
|
||||
left.padding === right.padding &&
|
||||
left.containerWidth === right.containerWidth
|
||||
);
|
||||
};
|
||||
|
||||
private readonly _scheduleShowDragHandleFromSurfaceUpdate = () => {
|
||||
if (this._surfaceElementUpdatedRafId !== null) return;
|
||||
|
||||
this._surfaceElementUpdatedRafId = requestAnimationFrame(() => {
|
||||
this._surfaceElementUpdatedRafId = null;
|
||||
if (!this.widget.isGfxDragHandleVisible) return;
|
||||
this._showDragHandle();
|
||||
});
|
||||
};
|
||||
|
||||
private readonly _handleEdgelessToolUpdated = (
|
||||
newTool: ToolOptionWithType
|
||||
) => {
|
||||
@@ -43,46 +100,123 @@ export class EdgelessWatcher {
|
||||
}
|
||||
|
||||
if (
|
||||
this.widget.center[0] !== center[0] &&
|
||||
this.widget.center[0] !== center[0] ||
|
||||
this.widget.center[1] !== center[1]
|
||||
) {
|
||||
this.widget.center = [...center];
|
||||
}
|
||||
|
||||
if (this.widget.isGfxDragHandleVisible) {
|
||||
this._showDragHandle();
|
||||
this._updateDragHoverRectTopLevelBlock();
|
||||
const area = this.hoveredElemArea;
|
||||
this._showDragHandle(area);
|
||||
this._updateDragHoverRectTopLevelBlock(area);
|
||||
} else if (this.widget.activeDragHandle) {
|
||||
this.widget.hide();
|
||||
}
|
||||
};
|
||||
|
||||
private readonly _showDragHandle = () => {
|
||||
if (!this.widget.anchorBlockId) return;
|
||||
private readonly _flushShowDragHandle = () => {
|
||||
this._showDragHandleRafId = null;
|
||||
|
||||
if (!this.widget.anchorBlockId.peek()) return;
|
||||
|
||||
const container = this.widget.dragHandleContainer;
|
||||
const grabber = this.widget.dragHandleGrabber;
|
||||
if (!container || !grabber) return;
|
||||
|
||||
const area = this.hoveredElemArea;
|
||||
const area = this._pendingHoveredElemArea ?? this.hoveredElemArea;
|
||||
this._pendingHoveredElemArea = null;
|
||||
if (!area) return;
|
||||
|
||||
container.style.transition = 'none';
|
||||
container.style.paddingTop = `0px`;
|
||||
container.style.paddingBottom = `0px`;
|
||||
container.style.left = `${area.left}px`;
|
||||
container.style.top = `${area.top}px`;
|
||||
container.style.display = 'flex';
|
||||
if (
|
||||
this.widget.isGfxDragHandleVisible &&
|
||||
this._isAreaEqual(this._lastAppliedHoveredElemArea, area)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (container.style.transition !== 'none') {
|
||||
container.style.transition = 'none';
|
||||
}
|
||||
const nextPaddingTop = '0px';
|
||||
if (container.style.paddingTop !== nextPaddingTop) {
|
||||
container.style.paddingTop = nextPaddingTop;
|
||||
}
|
||||
const nextPaddingBottom = '0px';
|
||||
if (container.style.paddingBottom !== nextPaddingBottom) {
|
||||
container.style.paddingBottom = nextPaddingBottom;
|
||||
}
|
||||
const nextLeft = `${area.left}px`;
|
||||
if (container.style.left !== nextLeft) {
|
||||
container.style.left = nextLeft;
|
||||
}
|
||||
const nextTop = `${area.top}px`;
|
||||
if (container.style.top !== nextTop) {
|
||||
container.style.top = nextTop;
|
||||
}
|
||||
if (container.style.display !== 'flex') {
|
||||
container.style.display = 'flex';
|
||||
}
|
||||
|
||||
this.widget.handleAnchorModelDisposables();
|
||||
|
||||
this.widget.activeDragHandle = 'gfx';
|
||||
this._lastAppliedHoveredElemArea = this._cloneArea(area);
|
||||
};
|
||||
|
||||
private readonly _updateDragHoverRectTopLevelBlock = () => {
|
||||
private readonly _showDragHandle = (area?: HoveredElemArea | null) => {
|
||||
const nextArea = area ?? this.hoveredElemArea;
|
||||
this._pendingHoveredElemArea = nextArea;
|
||||
if (!this._pendingHoveredElemArea) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
this.widget.isGfxDragHandleVisible &&
|
||||
this._showDragHandleRafId === null &&
|
||||
this._isAreaEqual(
|
||||
this._lastAppliedHoveredElemArea,
|
||||
this._pendingHoveredElemArea
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (this._showDragHandleRafId !== null) {
|
||||
return;
|
||||
}
|
||||
this._showDragHandleRafId = requestAnimationFrame(
|
||||
this._flushShowDragHandle
|
||||
);
|
||||
};
|
||||
|
||||
private readonly _updateDragHoverRectTopLevelBlock = (
|
||||
area?: HoveredElemArea | null
|
||||
) => {
|
||||
if (!this.widget.dragHoverRect) return;
|
||||
|
||||
this.widget.dragHoverRect = this.hoveredElemAreaRect;
|
||||
const nextArea = area ?? this.hoveredElemArea;
|
||||
if (!nextArea) {
|
||||
this.widget.dragHoverRect = null;
|
||||
return;
|
||||
}
|
||||
|
||||
const nextRect = new Rect(
|
||||
nextArea.left,
|
||||
nextArea.top,
|
||||
nextArea.right,
|
||||
nextArea.bottom
|
||||
);
|
||||
const prevRect = this.widget.dragHoverRect;
|
||||
if (
|
||||
prevRect &&
|
||||
prevRect.left === nextRect.left &&
|
||||
prevRect.top === nextRect.top &&
|
||||
prevRect.width === nextRect.width &&
|
||||
prevRect.height === nextRect.height
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.widget.dragHoverRect = nextRect;
|
||||
};
|
||||
|
||||
get gfx() {
|
||||
@@ -123,7 +257,7 @@ export class EdgelessWatcher {
|
||||
return new Rect(area.left, area.top, area.right, area.bottom);
|
||||
}
|
||||
|
||||
get hoveredElemArea() {
|
||||
get hoveredElemArea(): HoveredElemArea | null {
|
||||
const edgelessElement = this.widget.anchorEdgelessElement.peek();
|
||||
|
||||
if (!edgelessElement) return null;
|
||||
@@ -174,6 +308,19 @@ export class EdgelessWatcher {
|
||||
viewport.viewportUpdated.subscribe(this._handleEdgelessViewPortUpdated)
|
||||
);
|
||||
|
||||
disposables.add(() => {
|
||||
if (this._showDragHandleRafId !== null) {
|
||||
cancelAnimationFrame(this._showDragHandleRafId);
|
||||
this._showDragHandleRafId = null;
|
||||
}
|
||||
if (this._surfaceElementUpdatedRafId !== null) {
|
||||
cancelAnimationFrame(this._surfaceElementUpdatedRafId);
|
||||
this._surfaceElementUpdatedRafId = null;
|
||||
}
|
||||
this._pendingHoveredElemArea = null;
|
||||
this._lastAppliedHoveredElemArea = null;
|
||||
});
|
||||
|
||||
disposables.add(
|
||||
selection.slots.updated.subscribe(() => {
|
||||
this.updateAnchorElement();
|
||||
@@ -216,7 +363,7 @@ export class EdgelessWatcher {
|
||||
this.widget.hide();
|
||||
}
|
||||
if (payload.type === 'update') {
|
||||
this._showDragHandle();
|
||||
this._scheduleShowDragHandleFromSurfaceUpdate();
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -224,9 +371,10 @@ export class EdgelessWatcher {
|
||||
|
||||
if (surface) {
|
||||
disposables.add(
|
||||
surface.elementUpdated.subscribe(() => {
|
||||
surface.elementUpdated.subscribe(({ id }) => {
|
||||
if (this.widget.isGfxDragHandleVisible) {
|
||||
this._showDragHandle();
|
||||
if (id !== this.widget.anchorBlockId.peek()) return;
|
||||
this._scheduleShowDragHandleFromSurfaceUpdate();
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
@@ -153,6 +153,10 @@ export class PointerEventWatcher {
|
||||
|
||||
private _lastShowedBlock: { id: string; el: BlockComponent } | null = null;
|
||||
|
||||
private _lastPointerHitBlockId: string | null = null;
|
||||
|
||||
private _lastPointerHitBlockElement: Element | null = null;
|
||||
|
||||
/**
|
||||
* When pointer move on block, should show drag handle
|
||||
* And update hover block id and path
|
||||
@@ -169,6 +173,7 @@ export class PointerEventWatcher {
|
||||
point
|
||||
);
|
||||
if (!closestBlock) {
|
||||
this._lastPointerHitBlockId = null;
|
||||
this.widget.anchorBlockId.value = null;
|
||||
return;
|
||||
}
|
||||
@@ -237,19 +242,38 @@ export class PointerEventWatcher {
|
||||
|
||||
const state = ctx.get('pointerState');
|
||||
|
||||
// When pointer is moving, should do nothing
|
||||
if (state.delta.x !== 0 && state.delta.y !== 0) return;
|
||||
|
||||
const { target } = state.raw;
|
||||
const element = captureEventTarget(target);
|
||||
// When pointer not on block or on dragging, should do nothing
|
||||
if (!element) return;
|
||||
if (!element) {
|
||||
this._lastPointerHitBlockId = null;
|
||||
this._lastPointerHitBlockElement = null;
|
||||
return;
|
||||
}
|
||||
|
||||
// When pointer on drag handle, should do nothing
|
||||
if (element.closest('.affine-drag-handle-container')) return;
|
||||
|
||||
if (!this.widget.rootComponent) return;
|
||||
|
||||
const hitBlock = element.closest(`[${BLOCK_ID_ATTR}]`);
|
||||
const hitBlockId = hitBlock?.getAttribute(BLOCK_ID_ATTR) ?? null;
|
||||
|
||||
// Pointer move events are high-frequency. If hovered block identity is
|
||||
// unchanged and the underlying block element is the same, skip the
|
||||
// closest-note lookup.
|
||||
if (
|
||||
hitBlockId &&
|
||||
this.widget.isBlockDragHandleVisible &&
|
||||
hitBlockId === this._lastPointerHitBlockId &&
|
||||
hitBlock === this._lastPointerHitBlockElement &&
|
||||
isBlockIdEqual(this.widget.anchorBlockId.peek(), hitBlockId)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
this._lastPointerHitBlockId = hitBlockId;
|
||||
this._lastPointerHitBlockElement = hitBlock;
|
||||
|
||||
// When pointer out of note block hover area or inside database, should hide drag handle
|
||||
const point = new Point(state.raw.x, state.raw.y);
|
||||
|
||||
@@ -354,6 +378,8 @@ export class PointerEventWatcher {
|
||||
reset() {
|
||||
this._lastHoveredBlockId = null;
|
||||
this._lastShowedBlock = null;
|
||||
this._lastPointerHitBlockId = null;
|
||||
this._lastPointerHitBlockElement = null;
|
||||
}
|
||||
|
||||
watch() {
|
||||
|
||||
@@ -10,25 +10,15 @@ import type { InlineRange } from '../types.js';
|
||||
import { deltaInsertsToChunks } from '../utils/delta-convert.js';
|
||||
|
||||
export class RenderService<TextAttributes extends BaseTextAttributes> {
|
||||
private readonly _onYTextChange = (
|
||||
_: Y.YTextEvent,
|
||||
transaction: Y.Transaction
|
||||
) => {
|
||||
this.editor.slots.textChange.next();
|
||||
private _pendingRemoteInlineRangeSync = false;
|
||||
|
||||
const yText = this.editor.yText;
|
||||
private _carriageReturnValidationCounter = 0;
|
||||
|
||||
if (yText.toString().includes('\r')) {
|
||||
throw new BlockSuiteError(
|
||||
ErrorCode.InlineEditorError,
|
||||
'yText must not contain "\\r" because it will break the range synchronization'
|
||||
);
|
||||
}
|
||||
|
||||
this.render();
|
||||
private _renderVersion = 0;
|
||||
|
||||
private readonly _syncRemoteInlineRange = () => {
|
||||
const inlineRange = this.editor.inlineRange$.peek();
|
||||
if (!inlineRange || transaction.local) return;
|
||||
if (!inlineRange) return;
|
||||
|
||||
const lastStartRelativePosition = this.editor.lastStartRelativePosition;
|
||||
const lastEndRelativePosition = this.editor.lastEndRelativePosition;
|
||||
@@ -50,7 +40,7 @@ export class RenderService<TextAttributes extends BaseTextAttributes> {
|
||||
|
||||
const startIndex = absoluteStart?.index;
|
||||
const endIndex = absoluteEnd?.index;
|
||||
if (!startIndex || !endIndex) return;
|
||||
if (startIndex == null || endIndex == null) return;
|
||||
|
||||
const newInlineRange: InlineRange = {
|
||||
index: startIndex,
|
||||
@@ -59,7 +49,31 @@ export class RenderService<TextAttributes extends BaseTextAttributes> {
|
||||
if (!this.editor.isValidInlineRange(newInlineRange)) return;
|
||||
|
||||
this.editor.setInlineRange(newInlineRange);
|
||||
this.editor.syncInlineRange();
|
||||
};
|
||||
|
||||
private readonly _onYTextChange = (
|
||||
_: Y.YTextEvent,
|
||||
transaction: Y.Transaction
|
||||
) => {
|
||||
this.editor.slots.textChange.next();
|
||||
|
||||
const yText = this.editor.yText;
|
||||
|
||||
if (
|
||||
(this._carriageReturnValidationCounter++ & 0x3f) === 0 &&
|
||||
yText.toString().includes('\r')
|
||||
) {
|
||||
throw new BlockSuiteError(
|
||||
ErrorCode.InlineEditorError,
|
||||
'yText must not contain "\\r" because it will break the range synchronization'
|
||||
);
|
||||
}
|
||||
|
||||
if (!transaction.local) {
|
||||
this._pendingRemoteInlineRangeSync = true;
|
||||
}
|
||||
|
||||
this.render();
|
||||
};
|
||||
|
||||
mount = () => {
|
||||
@@ -70,6 +84,7 @@ export class RenderService<TextAttributes extends BaseTextAttributes> {
|
||||
editor.disposables.add({
|
||||
dispose: () => {
|
||||
yText.unobserve(this._onYTextChange);
|
||||
this._pendingRemoteInlineRangeSync = false;
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -82,6 +97,7 @@ export class RenderService<TextAttributes extends BaseTextAttributes> {
|
||||
render = () => {
|
||||
if (!this.editor.rootElement) return;
|
||||
|
||||
const renderVersion = ++this._renderVersion;
|
||||
this._rendering = true;
|
||||
|
||||
const rootElement = this.editor.rootElement;
|
||||
@@ -152,11 +168,21 @@ export class RenderService<TextAttributes extends BaseTextAttributes> {
|
||||
this.editor
|
||||
.waitForUpdate()
|
||||
.then(() => {
|
||||
if (renderVersion !== this._renderVersion) return;
|
||||
if (this._pendingRemoteInlineRangeSync) {
|
||||
this._pendingRemoteInlineRangeSync = false;
|
||||
this._syncRemoteInlineRange();
|
||||
}
|
||||
this._rendering = false;
|
||||
this.editor.slots.renderComplete.next();
|
||||
this.editor.syncInlineRange();
|
||||
})
|
||||
.catch(console.error);
|
||||
.catch(error => {
|
||||
if (renderVersion === this._renderVersion) {
|
||||
this._rendering = false;
|
||||
}
|
||||
console.error(error);
|
||||
});
|
||||
};
|
||||
|
||||
rerenderWholeEditor = () => {
|
||||
|
||||
@@ -9,7 +9,12 @@ import {
|
||||
isVElement,
|
||||
isVLine,
|
||||
} from './guard.js';
|
||||
import { calculateTextLength, getTextNodesFromElement } from './text.js';
|
||||
import {
|
||||
calculateTextLength,
|
||||
getInlineRootTextCache,
|
||||
getTextNodesFromElement,
|
||||
invalidateInlineRootTextCache,
|
||||
} from './text.js';
|
||||
|
||||
export function nativePointToTextPoint(
|
||||
node: unknown,
|
||||
@@ -67,19 +72,6 @@ export function textPointToDomPoint(
|
||||
|
||||
if (!rootElement.contains(text)) return null;
|
||||
|
||||
const texts = getTextNodesFromElement(rootElement);
|
||||
if (texts.length === 0) return null;
|
||||
|
||||
const goalIndex = texts.indexOf(text);
|
||||
let index = 0;
|
||||
for (const text of texts.slice(0, goalIndex)) {
|
||||
index += calculateTextLength(text);
|
||||
}
|
||||
|
||||
if (text.wholeText !== ZERO_WIDTH_FOR_EMPTY_LINE) {
|
||||
index += offset;
|
||||
}
|
||||
|
||||
const textParentElement = text.parentElement;
|
||||
if (!textParentElement) {
|
||||
throw new BlockSuiteError(
|
||||
@@ -97,9 +89,44 @@ export function textPointToDomPoint(
|
||||
);
|
||||
}
|
||||
|
||||
const textOffset = text.wholeText === ZERO_WIDTH_FOR_EMPTY_LINE ? 0 : offset;
|
||||
|
||||
for (let attempt = 0; attempt < 2; attempt++) {
|
||||
const { textNodes, textNodeIndexMap, prefixLengths, lineIndexMap } =
|
||||
getInlineRootTextCache(rootElement);
|
||||
if (textNodes.length === 0) return null;
|
||||
|
||||
const goalIndex = textNodeIndexMap.get(text);
|
||||
const lineIndex = lineIndexMap.get(lineElement);
|
||||
if (goalIndex !== undefined && lineIndex !== undefined) {
|
||||
const index = (prefixLengths[goalIndex] ?? 0) + textOffset;
|
||||
return { text, index: index + lineIndex };
|
||||
}
|
||||
|
||||
if (attempt === 0) {
|
||||
// MutationObserver marks cache dirty asynchronously; force one sync retry
|
||||
// when a newly-added node is queried within the same task.
|
||||
invalidateInlineRootTextCache(rootElement);
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to linear scan when cache still misses. This keeps behavior
|
||||
// stable even if MutationObserver-based invalidation lags behind.
|
||||
const texts = getTextNodesFromElement(rootElement);
|
||||
if (texts.length === 0) return null;
|
||||
|
||||
const goalIndex = texts.indexOf(text);
|
||||
if (goalIndex < 0) return null;
|
||||
|
||||
let index = textOffset;
|
||||
for (const beforeText of texts.slice(0, goalIndex)) {
|
||||
index += calculateTextLength(beforeText);
|
||||
}
|
||||
|
||||
const lineIndex = Array.from(rootElement.querySelectorAll('v-line')).indexOf(
|
||||
lineElement
|
||||
);
|
||||
if (lineIndex < 0) return null;
|
||||
|
||||
return { text, index: index + lineIndex };
|
||||
}
|
||||
|
||||
@@ -8,6 +8,92 @@ export function calculateTextLength(text: Text): number {
|
||||
}
|
||||
}
|
||||
|
||||
type InlineRootTextCache = {
|
||||
dirty: boolean;
|
||||
observer: MutationObserver | null;
|
||||
textNodes: Text[];
|
||||
textNodeIndexMap: WeakMap<Text, number>;
|
||||
prefixLengths: number[];
|
||||
lineIndexMap: WeakMap<Element, number>;
|
||||
};
|
||||
|
||||
const inlineRootTextCaches = new WeakMap<HTMLElement, InlineRootTextCache>();
|
||||
|
||||
const buildInlineRootTextCache = (
|
||||
rootElement: HTMLElement,
|
||||
cache: InlineRootTextCache
|
||||
) => {
|
||||
const textSpanElements = Array.from(
|
||||
rootElement.querySelectorAll('[data-v-text="true"]')
|
||||
);
|
||||
const textNodes: Text[] = [];
|
||||
const textNodeIndexMap = new WeakMap<Text, number>();
|
||||
const prefixLengths: number[] = [];
|
||||
let prefixLength = 0;
|
||||
|
||||
for (const textSpanElement of textSpanElements) {
|
||||
const textNode = Array.from(textSpanElement.childNodes).find(
|
||||
(node): node is Text => node instanceof Text
|
||||
);
|
||||
if (!textNode) continue;
|
||||
prefixLengths.push(prefixLength);
|
||||
textNodeIndexMap.set(textNode, textNodes.length);
|
||||
textNodes.push(textNode);
|
||||
prefixLength += calculateTextLength(textNode);
|
||||
}
|
||||
|
||||
const lineIndexMap = new WeakMap<Element, number>();
|
||||
const lineElements = Array.from(rootElement.querySelectorAll('v-line'));
|
||||
for (const [index, line] of lineElements.entries()) {
|
||||
lineIndexMap.set(line, index);
|
||||
}
|
||||
|
||||
cache.textNodes = textNodes;
|
||||
cache.textNodeIndexMap = textNodeIndexMap;
|
||||
cache.prefixLengths = prefixLengths;
|
||||
cache.lineIndexMap = lineIndexMap;
|
||||
cache.dirty = false;
|
||||
};
|
||||
|
||||
export function invalidateInlineRootTextCache(rootElement: HTMLElement) {
|
||||
const cache = inlineRootTextCaches.get(rootElement);
|
||||
if (cache) {
|
||||
cache.dirty = true;
|
||||
}
|
||||
}
|
||||
|
||||
export function getInlineRootTextCache(rootElement: HTMLElement) {
|
||||
let cache = inlineRootTextCaches.get(rootElement);
|
||||
if (!cache) {
|
||||
cache = {
|
||||
dirty: true,
|
||||
observer: null,
|
||||
textNodes: [],
|
||||
textNodeIndexMap: new WeakMap(),
|
||||
prefixLengths: [],
|
||||
lineIndexMap: new WeakMap(),
|
||||
};
|
||||
inlineRootTextCaches.set(rootElement, cache);
|
||||
}
|
||||
|
||||
if (!cache.observer && typeof MutationObserver !== 'undefined') {
|
||||
cache.observer = new MutationObserver(() => {
|
||||
cache!.dirty = true;
|
||||
});
|
||||
cache.observer.observe(rootElement, {
|
||||
subtree: true,
|
||||
childList: true,
|
||||
characterData: true,
|
||||
});
|
||||
}
|
||||
|
||||
if (cache.dirty) {
|
||||
buildInlineRootTextCache(rootElement, cache);
|
||||
}
|
||||
|
||||
return cache;
|
||||
}
|
||||
|
||||
export function getTextNodesFromElement(element: Element): Text[] {
|
||||
const textSpanElements = Array.from(
|
||||
element.querySelectorAll('[data-v-text="true"]')
|
||||
|
||||
@@ -47,7 +47,10 @@ describe('frame', () => {
|
||||
expect(rect!.width).toBeGreaterThan(0);
|
||||
expect(rect!.height).toBeGreaterThan(0);
|
||||
|
||||
const [titleX, titleY] = service.viewport.toModelCoord(rect!.x, rect!.y);
|
||||
const [titleX, titleY] = service.viewport.toModelCoordFromClientCoord([
|
||||
rect!.x,
|
||||
rect!.y,
|
||||
]);
|
||||
expect(titleX).toBeCloseTo(0);
|
||||
expect(titleY).toBeLessThan(0);
|
||||
|
||||
@@ -66,10 +69,11 @@ describe('frame', () => {
|
||||
if (!nestedTitle) return;
|
||||
|
||||
const nestedTitleRect = nestedTitle.getBoundingClientRect()!;
|
||||
const [nestedTitleX, nestedTitleY] = service.viewport.toModelCoord(
|
||||
nestedTitleRect.x,
|
||||
nestedTitleRect.y
|
||||
);
|
||||
const [nestedTitleX, nestedTitleY] =
|
||||
service.viewport.toModelCoordFromClientCoord([
|
||||
nestedTitleRect.x,
|
||||
nestedTitleRect.y,
|
||||
]);
|
||||
|
||||
expect(nestedTitleX).toBeGreaterThan(20);
|
||||
expect(nestedTitleY).toBeGreaterThan(20);
|
||||
|
||||
@@ -5,6 +5,14 @@ import { wait } from '../utils/common.js';
|
||||
import { getSurface } from '../utils/edgeless.js';
|
||||
import { setupEditor } from '../utils/setup.js';
|
||||
|
||||
function expectPxCloseTo(
|
||||
value: string,
|
||||
expected: number,
|
||||
precision: number = 2
|
||||
) {
|
||||
expect(Number.parseFloat(value)).toBeCloseTo(expected, precision);
|
||||
}
|
||||
|
||||
describe('Shape rendering with DOM renderer', () => {
|
||||
beforeEach(async () => {
|
||||
const cleanup = await setupEditor('edgeless', [], {
|
||||
@@ -59,7 +67,8 @@ describe('Shape rendering with DOM renderer', () => {
|
||||
);
|
||||
|
||||
expect(shapeElement).not.toBeNull();
|
||||
expect(shapeElement?.style.borderRadius).toBe('6px');
|
||||
const zoom = surfaceView.renderer.viewport.zoom;
|
||||
expectPxCloseTo(shapeElement!.style.borderRadius, 6 * zoom);
|
||||
});
|
||||
|
||||
test('should remove shape DOM node when element is deleted', async () => {
|
||||
@@ -110,8 +119,9 @@ describe('Shape rendering with DOM renderer', () => {
|
||||
);
|
||||
|
||||
expect(shapeElement).not.toBeNull();
|
||||
expect(shapeElement?.style.width).toBe('80px');
|
||||
expect(shapeElement?.style.height).toBe('60px');
|
||||
const zoom = surfaceView.renderer.viewport.zoom;
|
||||
expectPxCloseTo(shapeElement!.style.width, 80 * zoom);
|
||||
expectPxCloseTo(shapeElement!.style.height, 60 * zoom);
|
||||
});
|
||||
|
||||
test('should correctly render triangle shape', async () => {
|
||||
@@ -132,7 +142,8 @@ describe('Shape rendering with DOM renderer', () => {
|
||||
);
|
||||
|
||||
expect(shapeElement).not.toBeNull();
|
||||
expect(shapeElement?.style.width).toBe('80px');
|
||||
expect(shapeElement?.style.height).toBe('60px');
|
||||
const zoom = surfaceView.renderer.viewport.zoom;
|
||||
expectPxCloseTo(shapeElement!.style.width, 80 * zoom);
|
||||
expectPxCloseTo(shapeElement!.style.height, 60 * zoom);
|
||||
});
|
||||
});
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 24 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 8.2 KiB After Width: | Height: | Size: 24 KiB |
@@ -0,0 +1,363 @@
|
||||
import { LinkExtension } from '@blocksuite/affine-inline-link';
|
||||
import { textKeymap } from '@blocksuite/affine-inline-preset';
|
||||
import type {
|
||||
ListBlockModel,
|
||||
ParagraphBlockModel,
|
||||
} from '@blocksuite/affine-model';
|
||||
import { insertContent } from '@blocksuite/affine-rich-text';
|
||||
import { REFERENCE_NODE } from '@blocksuite/affine-shared/consts';
|
||||
import { createDefaultDoc } from '@blocksuite/affine-shared/utils';
|
||||
import { TextSelection } from '@blocksuite/std';
|
||||
import type { InlineMarkdownMatch } from '@blocksuite/std/inline';
|
||||
import { Text } from '@blocksuite/store';
|
||||
import { beforeEach, describe, expect, test, vi } from 'vitest';
|
||||
|
||||
import { defaultSlashMenuConfig } from '../../../../affine/widgets/slash-menu/src/config.js';
|
||||
import type {
|
||||
SlashMenuActionItem,
|
||||
SlashMenuItem,
|
||||
} from '../../../../affine/widgets/slash-menu/src/types.js';
|
||||
import { wait } from '../utils/common.js';
|
||||
import { addNote } from '../utils/edgeless.js';
|
||||
import { setupEditor } from '../utils/setup.js';
|
||||
|
||||
type RichTextElement = HTMLElement & {
|
||||
inlineEditor: {
|
||||
getFormat: (range: {
|
||||
index: number;
|
||||
length: number;
|
||||
}) => Record<string, unknown>;
|
||||
getInlineRange: () => { index: number; length: number } | null;
|
||||
setInlineRange: (range: { index: number; length: number }) => void;
|
||||
yTextString: string;
|
||||
};
|
||||
markdownMatches: InlineMarkdownMatch[];
|
||||
undoManager: {
|
||||
stopCapturing: () => void;
|
||||
};
|
||||
};
|
||||
|
||||
function findSlashActionItem(
|
||||
items: SlashMenuItem[],
|
||||
name: string
|
||||
): SlashMenuActionItem {
|
||||
const item = items.find(entry => entry.name === name);
|
||||
if (!item || !('action' in item)) {
|
||||
throw new Error(`Cannot find slash-menu action: ${name}`);
|
||||
}
|
||||
return item;
|
||||
}
|
||||
|
||||
function getRichTextByBlockId(blockId: string): RichTextElement {
|
||||
const block = editor.host?.view.getBlock(blockId) as HTMLElement | null;
|
||||
if (!block) {
|
||||
throw new Error(`Cannot find block view: ${blockId}`);
|
||||
}
|
||||
const richText = block.querySelector('rich-text') as RichTextElement | null;
|
||||
if (!richText) {
|
||||
throw new Error(`Cannot find rich-text for block: ${blockId}`);
|
||||
}
|
||||
return richText;
|
||||
}
|
||||
|
||||
async function createParagraph(text = '') {
|
||||
const noteId = addNote(doc);
|
||||
const note = doc.getBlock(noteId)?.model;
|
||||
if (!note) {
|
||||
throw new Error('Cannot find note model');
|
||||
}
|
||||
const paragraph = note.children[0] as ParagraphBlockModel | undefined;
|
||||
if (!paragraph) {
|
||||
throw new Error('Cannot find paragraph model');
|
||||
}
|
||||
if (text) {
|
||||
doc.updateBlock(paragraph, {
|
||||
text: new Text(text),
|
||||
});
|
||||
}
|
||||
await wait();
|
||||
return {
|
||||
noteId,
|
||||
paragraphId: paragraph.id,
|
||||
};
|
||||
}
|
||||
|
||||
function setTextSelection(blockId: string, index: number, length: number) {
|
||||
const to = length
|
||||
? {
|
||||
blockId,
|
||||
index: index + length,
|
||||
length: 0,
|
||||
}
|
||||
: null;
|
||||
const selection = editor.host?.selection.create(TextSelection, {
|
||||
from: {
|
||||
blockId,
|
||||
index,
|
||||
length: 0,
|
||||
},
|
||||
to,
|
||||
});
|
||||
if (!selection) {
|
||||
throw new Error('Cannot create text selection');
|
||||
}
|
||||
editor.host?.selection.setGroup('note', [selection]);
|
||||
const richText = getRichTextByBlockId(blockId);
|
||||
richText.inlineEditor.setInlineRange({ index, length });
|
||||
}
|
||||
|
||||
async function triggerMarkdown(
|
||||
blockId: string,
|
||||
input: string,
|
||||
matcherName: string
|
||||
) {
|
||||
const model = doc.getBlock(blockId)?.model as ParagraphBlockModel | undefined;
|
||||
if (!model) {
|
||||
throw new Error(`Cannot find paragraph model: ${blockId}`);
|
||||
}
|
||||
doc.updateBlock(model, {
|
||||
text: new Text(input),
|
||||
});
|
||||
await wait();
|
||||
|
||||
const richText = getRichTextByBlockId(blockId);
|
||||
const matcher = richText.markdownMatches.find(
|
||||
item => item.name === matcherName
|
||||
);
|
||||
if (!matcher) {
|
||||
throw new Error(`Cannot find markdown matcher: ${matcherName}`);
|
||||
}
|
||||
const inlineRange = { index: input.length, length: 0 };
|
||||
setTextSelection(blockId, inlineRange.index, 0);
|
||||
|
||||
matcher.action({
|
||||
inlineEditor: richText.inlineEditor as any,
|
||||
prefixText: input,
|
||||
inlineRange,
|
||||
pattern: matcher.pattern,
|
||||
undoManager: richText.undoManager as any,
|
||||
});
|
||||
|
||||
await wait();
|
||||
}
|
||||
|
||||
function mockKeyboardContext() {
|
||||
const preventDefault = vi.fn();
|
||||
const ctx = {
|
||||
get(key: string) {
|
||||
if (key === 'keyboardState') {
|
||||
return { raw: { preventDefault } };
|
||||
}
|
||||
throw new Error(`Unexpected state key: ${key}`);
|
||||
},
|
||||
};
|
||||
return { ctx: ctx as any, preventDefault };
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
const cleanup = await setupEditor('page', [LinkExtension]);
|
||||
return cleanup;
|
||||
});
|
||||
|
||||
describe('markdown/list/paragraph/quote/code/link', () => {
|
||||
test('markdown list shortcut converts to todo list and keeps checked state', async () => {
|
||||
const { noteId, paragraphId } = await createParagraph();
|
||||
await triggerMarkdown(paragraphId, '[x] ', 'list');
|
||||
|
||||
const note = doc.getBlock(noteId)?.model;
|
||||
if (!note) {
|
||||
throw new Error('Cannot find note model');
|
||||
}
|
||||
const model = note.children[0] as ListBlockModel;
|
||||
expect(model.flavour).toBe('affine:list');
|
||||
expect(model.props.type).toBe('todo');
|
||||
expect(model.props.checked).toBe(true);
|
||||
});
|
||||
|
||||
test('markdown heading and quote shortcuts convert paragraph type', async () => {
|
||||
const { noteId: headingNoteId, paragraphId: headingParagraphId } =
|
||||
await createParagraph();
|
||||
await triggerMarkdown(headingParagraphId, '# ', 'heading');
|
||||
const headingNote = doc.getBlock(headingNoteId)?.model;
|
||||
if (!headingNote) {
|
||||
throw new Error('Cannot find heading note model');
|
||||
}
|
||||
const headingModel = headingNote.children[0] as ParagraphBlockModel;
|
||||
expect(headingModel.flavour).toBe('affine:paragraph');
|
||||
expect(headingModel.props.type).toBe('h1');
|
||||
|
||||
const { noteId: quoteNoteId, paragraphId: quoteParagraphId } =
|
||||
await createParagraph();
|
||||
await triggerMarkdown(quoteParagraphId, '> ', 'heading');
|
||||
const quoteNote = doc.getBlock(quoteNoteId)?.model;
|
||||
if (!quoteNote) {
|
||||
throw new Error('Cannot find quote note model');
|
||||
}
|
||||
const quoteModel = quoteNote.children[0] as ParagraphBlockModel;
|
||||
expect(quoteModel.flavour).toBe('affine:paragraph');
|
||||
expect(quoteModel.props.type).toBe('quote');
|
||||
});
|
||||
|
||||
test('markdown code shortcut converts paragraph to code block with language', async () => {
|
||||
const { noteId, paragraphId } = await createParagraph();
|
||||
await triggerMarkdown(paragraphId, '```ts ', 'code-block');
|
||||
|
||||
const note = doc.getBlock(noteId)?.model;
|
||||
if (!note) {
|
||||
throw new Error('Cannot find note model');
|
||||
}
|
||||
const model = note.children[0];
|
||||
expect(model.flavour).toBe('affine:code');
|
||||
expect((model as any).props.language).toBe('typescript');
|
||||
});
|
||||
|
||||
test('inline markdown converts style and link attributes', async () => {
|
||||
const { paragraphId: boldParagraphId } = await createParagraph();
|
||||
await triggerMarkdown(boldParagraphId, '**bold** ', 'bold');
|
||||
const boldRichText = getRichTextByBlockId(boldParagraphId);
|
||||
expect(boldRichText.inlineEditor.yTextString).toBe('bold');
|
||||
expect(
|
||||
boldRichText.inlineEditor.getFormat({ index: 1, length: 0 })
|
||||
).toMatchObject({
|
||||
bold: true,
|
||||
});
|
||||
|
||||
const { paragraphId: codeParagraphId } = await createParagraph();
|
||||
await triggerMarkdown(codeParagraphId, '`code` ', 'code');
|
||||
const codeRichText = getRichTextByBlockId(codeParagraphId);
|
||||
expect(codeRichText.inlineEditor.yTextString).toBe('code');
|
||||
expect(
|
||||
codeRichText.inlineEditor.getFormat({ index: 1, length: 0 })
|
||||
).toMatchObject({
|
||||
code: true,
|
||||
});
|
||||
|
||||
const { paragraphId: linkParagraphId } = await createParagraph();
|
||||
await triggerMarkdown(
|
||||
linkParagraphId,
|
||||
'[AFFiNE](https://affine.pro) ',
|
||||
'link'
|
||||
);
|
||||
const linkRichText = getRichTextByBlockId(linkParagraphId);
|
||||
expect(linkRichText.inlineEditor.yTextString).toBe('AFFiNE');
|
||||
expect(
|
||||
linkRichText.inlineEditor.getFormat({ index: 1, length: 0 })
|
||||
).toMatchObject({
|
||||
link: 'https://affine.pro',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('hotkey/bracket/linked-page', () => {
|
||||
test('bracket keymap skips redundant right bracket in code block', async () => {
|
||||
const { noteId, paragraphId } = await createParagraph();
|
||||
await triggerMarkdown(paragraphId, '```ts ', 'code-block');
|
||||
const note = doc.getBlock(noteId)?.model;
|
||||
const codeId = note?.children[0]?.id;
|
||||
if (!codeId) {
|
||||
throw new Error('Cannot find code block id');
|
||||
}
|
||||
const codeModel = doc.getBlock(codeId)?.model;
|
||||
if (!codeModel) {
|
||||
throw new Error('Cannot find code block model');
|
||||
}
|
||||
const keymap = textKeymap(editor.std);
|
||||
const leftHandler = keymap['('];
|
||||
const rightHandler = keymap[')'];
|
||||
expect(leftHandler).toBeDefined();
|
||||
if (!rightHandler) {
|
||||
throw new Error('Cannot find bracket key handlers');
|
||||
}
|
||||
|
||||
doc.updateBlock(codeModel, {
|
||||
text: new Text('()'),
|
||||
});
|
||||
await wait();
|
||||
const codeRichText = getRichTextByBlockId(codeId);
|
||||
setTextSelection(codeId, 1, 0);
|
||||
const rightContext = mockKeyboardContext();
|
||||
rightHandler(rightContext.ctx);
|
||||
expect(rightContext.preventDefault).not.toHaveBeenCalled();
|
||||
expect(codeRichText.inlineEditor.yTextString).toBe('()');
|
||||
});
|
||||
|
||||
test('consecutive linked-page reference nodes render as separate references', async () => {
|
||||
const { paragraphId } = await createParagraph();
|
||||
const paragraphModel = doc.getBlock(paragraphId)?.model as
|
||||
| ParagraphBlockModel
|
||||
| undefined;
|
||||
if (!paragraphModel) {
|
||||
throw new Error('Cannot find paragraph model');
|
||||
}
|
||||
const linkedDoc = createDefaultDoc(collection, {
|
||||
title: 'Linked page',
|
||||
});
|
||||
|
||||
setTextSelection(paragraphId, 0, 0);
|
||||
insertContent(editor.std, paragraphModel, REFERENCE_NODE, {
|
||||
reference: {
|
||||
type: 'LinkedPage',
|
||||
pageId: linkedDoc.id,
|
||||
},
|
||||
});
|
||||
insertContent(editor.std, paragraphModel, REFERENCE_NODE, {
|
||||
reference: {
|
||||
type: 'LinkedPage',
|
||||
pageId: linkedDoc.id,
|
||||
},
|
||||
});
|
||||
await wait();
|
||||
expect(collection.docs.has(linkedDoc.id)).toBe(true);
|
||||
|
||||
const richText = getRichTextByBlockId(paragraphId);
|
||||
expect(richText.querySelectorAll('affine-reference').length).toBe(2);
|
||||
expect(richText.inlineEditor.yTextString.length).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('slash-menu action semantics', () => {
|
||||
test('date and move actions mutate block content/order as expected', async () => {
|
||||
const noteId = addNote(doc);
|
||||
const note = doc.getBlock(noteId)?.model;
|
||||
if (!note) {
|
||||
throw new Error('Cannot find note model');
|
||||
}
|
||||
const first = note.children[0] as ParagraphBlockModel;
|
||||
const secondId = doc.addBlock(
|
||||
'affine:paragraph',
|
||||
{ text: new Text('second') },
|
||||
noteId
|
||||
);
|
||||
const second = doc.getBlock(secondId)?.model as
|
||||
| ParagraphBlockModel
|
||||
| undefined;
|
||||
if (!second) {
|
||||
throw new Error('Cannot find second paragraph model');
|
||||
}
|
||||
doc.updateBlock(first, { text: new Text('first') });
|
||||
await wait();
|
||||
|
||||
const slashItems = defaultSlashMenuConfig.items;
|
||||
const items =
|
||||
typeof slashItems === 'function'
|
||||
? slashItems({ std: editor.std, model: first })
|
||||
: slashItems;
|
||||
const today = findSlashActionItem(items, 'Today');
|
||||
const moveDown = findSlashActionItem(items, 'Move Down');
|
||||
const moveUp = findSlashActionItem(items, 'Move Up');
|
||||
|
||||
moveDown.action({ std: editor.std, model: first });
|
||||
await wait();
|
||||
expect(note.children.map(child => child.id)).toEqual([second.id, first.id]);
|
||||
|
||||
moveUp.action({ std: editor.std, model: first });
|
||||
await wait();
|
||||
expect(note.children.map(child => child.id)).toEqual([first.id, second.id]);
|
||||
|
||||
setTextSelection(first.id, 0, 0);
|
||||
today.action({ std: editor.std, model: first });
|
||||
await wait();
|
||||
const richText = getRichTextByBlockId(first.id);
|
||||
expect(richText.inlineEditor.yTextString).toMatch(/\d{4}-\d{2}-\d{2}/);
|
||||
});
|
||||
});
|
||||
@@ -19,7 +19,11 @@ export default defineConfig(_configEnv =>
|
||||
browser: {
|
||||
enabled: true,
|
||||
headless: process.env.CI === 'true',
|
||||
instances: [{ browser: 'chromium' }],
|
||||
instances: [
|
||||
{ browser: 'chromium' },
|
||||
{ browser: 'firefox' },
|
||||
{ browser: 'webkit' },
|
||||
],
|
||||
provider: 'playwright',
|
||||
isolate: false,
|
||||
viewport: {
|
||||
|
||||
@@ -1,12 +1,28 @@
|
||||
import { getCurrentUserQuery } from '@affine/graphql';
|
||||
|
||||
import { JobExecutor } from '../../../base/job/queue/executor';
|
||||
import { DatabaseDocReader, DocReader } from '../../../core/doc';
|
||||
import { createApp } from '../create-app';
|
||||
import { e2e } from '../test';
|
||||
|
||||
e2e('should init doc service', async t => {
|
||||
type TestFlavor = 'doc' | 'graphql' | 'sync' | 'renderer' | 'front';
|
||||
|
||||
const createFlavorApp = async (flavor: TestFlavor) => {
|
||||
// @ts-expect-error override
|
||||
globalThis.env.FLAVOR = 'doc';
|
||||
await using app = await createApp();
|
||||
globalThis.env.FLAVOR = flavor;
|
||||
return await createApp({
|
||||
tapModule(module) {
|
||||
module.overrideProvider(JobExecutor).useValue({
|
||||
onConfigInit: async () => {},
|
||||
onConfigChanged: async () => {},
|
||||
onModuleDestroy: async () => {},
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
e2e('should init doc service', async t => {
|
||||
await using app = await createFlavorApp('doc');
|
||||
|
||||
const res = await app.GET('/info').expect(200);
|
||||
t.is(res.body.flavor, 'doc');
|
||||
@@ -15,9 +31,7 @@ e2e('should init doc service', async t => {
|
||||
});
|
||||
|
||||
e2e('should init graphql service', async t => {
|
||||
// @ts-expect-error override
|
||||
globalThis.env.FLAVOR = 'graphql';
|
||||
await using app = await createApp();
|
||||
await using app = await createFlavorApp('graphql');
|
||||
|
||||
const res = await app.GET('/info').expect(200);
|
||||
|
||||
@@ -28,28 +42,25 @@ e2e('should init graphql service', async t => {
|
||||
});
|
||||
|
||||
e2e('should init sync service', async t => {
|
||||
// @ts-expect-error override
|
||||
globalThis.env.FLAVOR = 'sync';
|
||||
await using app = await createApp();
|
||||
await using app = await createFlavorApp('sync');
|
||||
|
||||
const res = await app.GET('/info').expect(200);
|
||||
t.is(res.body.flavor, 'sync');
|
||||
});
|
||||
|
||||
e2e('should init renderer service', async t => {
|
||||
// @ts-expect-error override
|
||||
globalThis.env.FLAVOR = 'renderer';
|
||||
await using app = await createApp();
|
||||
await using app = await createFlavorApp('renderer');
|
||||
|
||||
const res = await app.GET('/info').expect(200);
|
||||
t.is(res.body.flavor, 'renderer');
|
||||
});
|
||||
|
||||
e2e('should init front service', async t => {
|
||||
// @ts-expect-error override
|
||||
globalThis.env.FLAVOR = 'front';
|
||||
await using app = await createApp();
|
||||
await using app = await createFlavorApp('front');
|
||||
|
||||
const res = await app.GET('/info').expect(200);
|
||||
t.is(res.body.flavor, 'front');
|
||||
|
||||
const docReader = app.get(DocReader);
|
||||
t.true(docReader instanceof DatabaseDocReader);
|
||||
});
|
||||
|
||||
@@ -159,8 +159,11 @@ export function buildAppModule(env: Env) {
|
||||
// basic
|
||||
.use(...FunctionalityModules)
|
||||
|
||||
// enable indexer module on graphql server and doc service
|
||||
.useIf(() => env.flavors.graphql || env.flavors.doc, IndexerModule)
|
||||
// enable indexer module on graphql, doc and front service
|
||||
.useIf(
|
||||
() => env.flavors.graphql || env.flavors.doc || env.flavors.front,
|
||||
IndexerModule
|
||||
)
|
||||
|
||||
// auth
|
||||
.use(UserModule, AuthModule, PermissionModule)
|
||||
@@ -202,8 +205,8 @@ export function buildAppModule(env: Env) {
|
||||
AccessTokenModule,
|
||||
QueueDashboardModule
|
||||
)
|
||||
// doc service only
|
||||
.useIf(() => env.flavors.doc, DocServiceModule)
|
||||
// doc service and front service
|
||||
.useIf(() => env.flavors.doc || env.flavors.front, DocServiceModule)
|
||||
// worker for and self-hosted API only for self-host and local development only
|
||||
.useIf(() => env.dev || env.selfhosted, WorkerModule, SelfhostModule)
|
||||
// static frontend routes for front flavor
|
||||
|
||||
@@ -31,8 +31,8 @@ export class JobExecutor implements OnModuleDestroy {
|
||||
? difference(QUEUES, [Queue.DOC, Queue.INDEXER])
|
||||
: [];
|
||||
|
||||
// NOTE(@forehalo): only enable doc queue in doc service
|
||||
if (env.flavors.doc) {
|
||||
// Enable doc/indexer queues in both doc and front service.
|
||||
if (env.flavors.doc || env.flavors.front) {
|
||||
queues.push(Queue.DOC);
|
||||
// NOTE(@fengmk2): Once the index task cannot be processed in time, it needs to be separated from the doc service and deployed independently.
|
||||
queues.push(Queue.INDEXER);
|
||||
|
||||
@@ -447,7 +447,7 @@ export class RpcDocReader extends DatabaseDocReader {
|
||||
export const DocReaderProvider: FactoryProvider = {
|
||||
provide: DocReader,
|
||||
useFactory: (ref: ModuleRef) => {
|
||||
if (env.flavors.doc) {
|
||||
if (env.flavors.doc || env.flavors.front) {
|
||||
return ref.create(DatabaseDocReader);
|
||||
}
|
||||
return ref.create(RpcDocReader);
|
||||
|
||||
@@ -6,7 +6,8 @@ textarea
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
|
||||
#webpack-dev-server-client-overlay {
|
||||
#webpack-dev-server-client-overlay,
|
||||
#rspack-dev-server-client-overlay {
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||
import { style } from '@vanilla-extract/css';
|
||||
import { globalStyle, style } from '@vanilla-extract/css';
|
||||
|
||||
export const mainContainer = style({
|
||||
containerType: 'inline-size',
|
||||
@@ -47,6 +47,12 @@ export const affineDocViewport = style({
|
||||
},
|
||||
});
|
||||
|
||||
export const pageModeViewportContentBox = style({});
|
||||
globalStyle(`${pageModeViewportContentBox} >:first-child`, {
|
||||
display: 'table !important',
|
||||
minWidth: '100%',
|
||||
});
|
||||
|
||||
export const scrollbar = style({
|
||||
marginRight: '4px',
|
||||
});
|
||||
|
||||
@@ -347,7 +347,8 @@ const DetailPageImpl = memo(function DetailPageImpl() {
|
||||
className={clsx(
|
||||
'affine-page-viewport',
|
||||
styles.affineDocViewport,
|
||||
styles.editorContainer
|
||||
styles.editorContainer,
|
||||
{ [styles.pageModeViewportContentBox]: mode === 'page' }
|
||||
)}
|
||||
>
|
||||
<PageDetailEditor onLoad={onLoad} readonly={readonly} />
|
||||
|
||||
8
packages/frontend/core/src/desktop/route-paths.ts
Normal file
8
packages/frontend/core/src/desktop/route-paths.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export const WORKSPACE_ROUTE_PATH = '/workspace/:workspaceId/*';
|
||||
export const SHARE_ROUTE_PATH = '/share/:workspaceId/:pageId';
|
||||
export const NOT_FOUND_ROUTE_PATH = '/404';
|
||||
export const CATCH_ALL_ROUTE_PATH = '*';
|
||||
|
||||
export function getWorkspaceDocPath(workspaceId: string, docId: string) {
|
||||
return `/workspace/${workspaceId}/${docId}`;
|
||||
}
|
||||
@@ -10,6 +10,13 @@ import {
|
||||
import { AffineErrorComponent } from '../components/affine/affine-error-boundary/affine-error-fallback';
|
||||
import { NavigateContext } from '../components/hooks/use-navigate-helper';
|
||||
import { RootWrapper } from './pages/root';
|
||||
import {
|
||||
CATCH_ALL_ROUTE_PATH,
|
||||
getWorkspaceDocPath,
|
||||
NOT_FOUND_ROUTE_PATH,
|
||||
SHARE_ROUTE_PATH,
|
||||
WORKSPACE_ROUTE_PATH,
|
||||
} from './route-paths';
|
||||
|
||||
export function RootRouter() {
|
||||
const navigate = useNavigate();
|
||||
@@ -38,17 +45,19 @@ export const topLevelRoutes = [
|
||||
lazy: () => import('./pages/index'),
|
||||
},
|
||||
{
|
||||
path: '/workspace/:workspaceId/*',
|
||||
path: WORKSPACE_ROUTE_PATH,
|
||||
lazy: () => import('./pages/workspace/index'),
|
||||
},
|
||||
{
|
||||
path: '/share/:workspaceId/:pageId',
|
||||
path: SHARE_ROUTE_PATH,
|
||||
loader: ({ params }) => {
|
||||
return redirect(`/workspace/${params.workspaceId}/${params.pageId}`);
|
||||
return redirect(
|
||||
getWorkspaceDocPath(params.workspaceId ?? '', params.pageId ?? '')
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/404',
|
||||
path: NOT_FOUND_ROUTE_PATH,
|
||||
lazy: () => import('./pages/404'),
|
||||
},
|
||||
{
|
||||
@@ -175,7 +184,7 @@ export const topLevelRoutes = [
|
||||
lazy: () => import('./pages/open-app'),
|
||||
},
|
||||
{
|
||||
path: '*',
|
||||
path: CATCH_ALL_ROUTE_PATH,
|
||||
lazy: () => import('./pages/404'),
|
||||
},
|
||||
],
|
||||
|
||||
@@ -18,6 +18,7 @@ import type { DocPropertiesStore } from '../stores/doc-properties';
|
||||
import type { DocsStore } from '../stores/docs';
|
||||
import type { DocCreateOptions } from '../types';
|
||||
import { DocService } from './doc';
|
||||
import { getDuplicatedDocTitle } from './duplicate-title';
|
||||
|
||||
const logger = new DebugLogger('DocsService');
|
||||
|
||||
@@ -286,13 +287,7 @@ export class DocsService extends Service {
|
||||
});
|
||||
|
||||
// duplicate doc title
|
||||
const originalTitle = sourceDoc.title$.value;
|
||||
const lastDigitRegex = /\((\d+)\)$/;
|
||||
const match = originalTitle.match(lastDigitRegex);
|
||||
const newNumber = match ? parseInt(match[1], 10) + 1 : 1;
|
||||
const newPageTitle =
|
||||
originalTitle.replace(lastDigitRegex, '') + `(${newNumber})`;
|
||||
targetDoc.changeDocTitle(newPageTitle);
|
||||
targetDoc.changeDocTitle(getDuplicatedDocTitle(sourceDoc.title$.value));
|
||||
|
||||
// duplicate doc properties
|
||||
const properties = sourceDoc.getProperties();
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
const DUPLICATED_DOC_TITLE_SUFFIX = /\((\d+)\)$/;
|
||||
|
||||
export function getDuplicatedDocTitle(originalTitle: string) {
|
||||
const match = originalTitle.match(DUPLICATED_DOC_TITLE_SUFFIX);
|
||||
const nextSequence = match ? parseInt(match[1], 10) + 1 : 1;
|
||||
return (
|
||||
originalTitle.replace(DUPLICATED_DOC_TITLE_SUFFIX, '') + `(${nextSequence})`
|
||||
);
|
||||
}
|
||||
@@ -46,6 +46,10 @@ import type {
|
||||
} from '../../workspace';
|
||||
import { WorkspaceImpl } from '../../workspace/impls/workspace';
|
||||
import { getWorkspaceProfileWorker } from './out-worker';
|
||||
import {
|
||||
dedupeWorkspaceIds,
|
||||
normalizeWorkspaceIds,
|
||||
} from './workspace-id-utils';
|
||||
|
||||
export const LOCAL_WORKSPACE_LOCAL_STORAGE_KEY = 'affine-local-workspace';
|
||||
export const LOCAL_WORKSPACE_GLOBAL_STATE_KEY =
|
||||
@@ -61,13 +65,6 @@ type GlobalStateStorageLike = {
|
||||
set<T>(key: string, value: T): void;
|
||||
};
|
||||
|
||||
function normalizeWorkspaceIds(ids: unknown): string[] {
|
||||
if (!Array.isArray(ids)) {
|
||||
return [];
|
||||
}
|
||||
return ids.filter((id): id is string => typeof id === 'string');
|
||||
}
|
||||
|
||||
function getElectronGlobalStateStorage(): GlobalStateStorageLike | null {
|
||||
if (!BUILD_CONFIG.isElectron) {
|
||||
return null;
|
||||
@@ -113,7 +110,7 @@ export function setLocalWorkspaceIds(
|
||||
? idsOrUpdater(getLocalWorkspaceIds())
|
||||
: idsOrUpdater
|
||||
);
|
||||
const deduplicated = [...new Set(next)];
|
||||
const deduplicated = dedupeWorkspaceIds(next);
|
||||
|
||||
const globalState = getElectronGlobalStateStorage();
|
||||
if (globalState) {
|
||||
@@ -168,14 +165,12 @@ class LocalWorkspaceFlavourProvider implements WorkspaceFlavourProvider {
|
||||
}
|
||||
|
||||
setLocalWorkspaceIds(currentIds => {
|
||||
return [
|
||||
...new Set([
|
||||
...currentIds,
|
||||
...persistedIds,
|
||||
...legacyIds,
|
||||
...scannedIds,
|
||||
]),
|
||||
];
|
||||
return dedupeWorkspaceIds([
|
||||
...currentIds,
|
||||
...persistedIds,
|
||||
...legacyIds,
|
||||
...scannedIds,
|
||||
]);
|
||||
});
|
||||
})()
|
||||
.catch(e => {
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
export function normalizeWorkspaceIds(ids: unknown): string[] {
|
||||
if (!Array.isArray(ids)) return [];
|
||||
return ids.filter((id): id is string => typeof id === 'string');
|
||||
}
|
||||
|
||||
export function dedupeWorkspaceIds(ids: string[]): string[] {
|
||||
return [...new Set(ids)];
|
||||
}
|
||||
3
packages/frontend/core/src/types/types.d.ts
vendored
3
packages/frontend/core/src/types/types.d.ts
vendored
@@ -1,4 +1,5 @@
|
||||
/// <reference types="@webpack/env"" />
|
||||
/// <reference types="@webpack/env" />
|
||||
/// <reference types="@rspack/core/module" />
|
||||
|
||||
declare module '*.md' {
|
||||
const text: string;
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"es-CL": 98,
|
||||
"es": 96,
|
||||
"fa": 96,
|
||||
"fr": 98,
|
||||
"fr": 100,
|
||||
"hi": 1,
|
||||
"it-IT": 98,
|
||||
"it": 1,
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
"DB_FILE_MIGRATION_FAILED": "La migration du fichier de base de données a échoué",
|
||||
"DB_FILE_PATH_INVALID": "Le chemin d'accès du fichier de base de données est invalide",
|
||||
"Date": "Date",
|
||||
"Delete": "Supprimer objet",
|
||||
"Delete": "Supprimer",
|
||||
"Deleted": "Supprimer",
|
||||
"Disable": "Désactiver",
|
||||
"Disable Public Sharing": "Désactiver le Partage Public",
|
||||
@@ -44,7 +44,7 @@
|
||||
"Full Backup Hint": "Synchroniser toutes les données cloud et exporter une sauvegarde complète de l'espace de travail",
|
||||
"Quick Export": "Exportation rapide",
|
||||
"Quick Export Description": "Ignorer la synchronisation cloud et exporter rapidement les données actuelles (certaines pièces jointes ou documents peuvent manquer)",
|
||||
"Export failed": "L'exportation à échouer",
|
||||
"Export failed": "L'exportation a échoué",
|
||||
"Export success": "Exporté avec succès",
|
||||
"Export to HTML": "Exporter en HTML",
|
||||
"Export to Markdown": "Exporter en Markdown",
|
||||
@@ -68,7 +68,7 @@
|
||||
"Invite Members": "Inviter des membres",
|
||||
"Invite Members Message": "Les membres invités collaboreront avec vous dans l'espace de travail actuel",
|
||||
"insufficient-team-seat": "Place d'équipe insuffisante",
|
||||
"Joined Workspace": "L'espace de travail a été rejoint",
|
||||
"Joined Workspace": "Espace de travail rejoint",
|
||||
"Leave": "Quitter",
|
||||
"Link": "Lien hypertexte (avec le texte sélectionné)",
|
||||
"Loading": "Chargement...",
|
||||
@@ -94,7 +94,7 @@
|
||||
"Remove from workspace": "Retirer de l'espace de travail",
|
||||
"Remove photo": "Supprimer la photo",
|
||||
"Remove special filter": "Retirer le filtre spécial",
|
||||
"Removed successfully": "Supprimer avec succès",
|
||||
"Removed successfully": "Supprimé avec succès",
|
||||
"Rename": "Renommer",
|
||||
"Retry": "Réessayer",
|
||||
"Save": "Enregistrer",
|
||||
@@ -120,15 +120,15 @@
|
||||
"Undo": "Annuler",
|
||||
"Unpin": "Désépingler",
|
||||
"Untitled": "Sans titre",
|
||||
"Update workspace name success": "L'espace de travail à été renommé avec succès",
|
||||
"Update workspace name success": "L'espace de travail a été renommé avec succès",
|
||||
"Updated": "Mis à jour",
|
||||
"Upload": "Uploader",
|
||||
"Users": "Utilisateur",
|
||||
"Upload": "Téléverser",
|
||||
"Users": "Utilisateurs",
|
||||
"Version": "Version",
|
||||
"Visit Workspace": "Visiter l'espace de travail",
|
||||
"Workspace Name": "Nom de l'espace de travail",
|
||||
"Workspace Owner": "Propriétaire de l’espace de travail",
|
||||
"Workspace Profile": "Profil de l'Espace de travail",
|
||||
"Workspace Profile": "Profil de l'espace de travail",
|
||||
"Workspace Settings": "Paramètres de l'espace de travail",
|
||||
"Workspace Settings with name": "Paramètres de {{name}}",
|
||||
"Workspace saved locally": "{{name}} est sauvegardé localement",
|
||||
@@ -140,7 +140,7 @@
|
||||
"current": "actuel",
|
||||
"created at": "créé à {{time}}",
|
||||
"updated at": "dernière mise à jour à {{time}}",
|
||||
"com.affine.aboutAFFiNE.autoCheckUpdate.description": "Vérifiez automatiquement pour de nouvelles mises à jour régulièrement.",
|
||||
"com.affine.aboutAFFiNE.autoCheckUpdate.description": "Vérifie automatiquement la disponibilité de nouvelles mises à jour.",
|
||||
"com.affine.aboutAFFiNE.autoCheckUpdate.title": "Vérifier automatiquement les mises à jours",
|
||||
"com.affine.aboutAFFiNE.autoDownloadUpdate.description": "Télécharger les mises à jour automatiquement (pour cet appareil)",
|
||||
"com.affine.aboutAFFiNE.autoDownloadUpdate.title": "Télécharger les mises à jour automatiquement",
|
||||
@@ -188,7 +188,7 @@
|
||||
"com.affine.ai-onboarding.general.get-started": "Commencer",
|
||||
"com.affine.ai-onboarding.general.next": "Suivant",
|
||||
"com.affine.ai-onboarding.general.prev": "Retour",
|
||||
"com.affine.ai-onboarding.general.privacy": "En continuant, vous acceptez nos <a>Conditions AI</a>.",
|
||||
"com.affine.ai-onboarding.general.privacy": "En continuant, vous acceptez nos <a>Conditions IA</a>.",
|
||||
"com.affine.ai-onboarding.general.purchase": "Obtenir une utilisation illimitée",
|
||||
"com.affine.ai-onboarding.general.skip": "Rappelez-moi plus tard",
|
||||
"com.affine.ai-onboarding.general.try-for-free": "Essayer gratuitement",
|
||||
@@ -205,7 +205,7 @@
|
||||
"com.affine.ai.login-required.dialog-confirm": "Se connecter",
|
||||
"com.affine.ai.login-required.dialog-content": "Pour utiliser AFFiNE IA, veuillez vous connecter à votre compte AFFiNE Cloud.",
|
||||
"com.affine.ai.login-required.dialog-title": "Connectez-vous pour continuer",
|
||||
"com.affine.ai.template-insert.failed": "Échec lors de l'insertion du modele, veuillez réessayer",
|
||||
"com.affine.ai.template-insert.failed": "Échec lors de l'insertion du modèle, veuillez réessayer.",
|
||||
"com.affine.all-pages.header": "Tous les documents",
|
||||
"com.affine.app-sidebar.learn-more": "En savoir plus",
|
||||
"com.affine.app-sidebar.star-us": "Étoilez-nous",
|
||||
@@ -271,7 +271,7 @@
|
||||
"com.affine.auth.open.affine.doc.edit-settings": "Modifier les paramètres",
|
||||
"com.affine.auth.open.affine.doc.footer-text": "Nécessite la version 0.18 ou plus de l'application de bureau AFFiNE.",
|
||||
"com.affine.auth.page.sent.email.subtitle": "Merci de rentrer un mot de passe de {{min}}-{{max}} caractères avec des lettres et des numéros pour continuer à vous créer un compte",
|
||||
"com.affine.auth.page.sent.email.title": "Bienvenu au AFFiNE Cloud, vous êtes presque là !",
|
||||
"com.affine.auth.page.sent.email.title": "Bienvenue sur AFFiNE Cloud, vous y êtes presque !",
|
||||
"com.affine.auth.password": "Mot de passe",
|
||||
"com.affine.auth.password.error": "Mot de passe invalide",
|
||||
"com.affine.auth.password.set-failed": "Échec de la définition du mot de passe",
|
||||
@@ -293,15 +293,15 @@
|
||||
"com.affine.auth.sent.change.email.hint": "Le lien de vérification a été envoyé",
|
||||
"com.affine.auth.sent.change.password.hint": "Le lien de réinitialisation de mot de passe a été envoyé",
|
||||
"com.affine.auth.sent.reset.password.success.message": "Votre mot de passe a été mis à jour ! Vous pouvez vous connecter à AFFiNE Cloud avec le nouveau mot de passe !",
|
||||
"com.affine.auth.sent.set.password.hint": "Le lien pour définir votre mot de passe à été envoyé",
|
||||
"com.affine.auth.sent.set.password.success.message": "Votre mot de passe est enregistré! Vous pouvez vous connecter sur AFFINE Cloud avec votre email et votre mot de passe!",
|
||||
"com.affine.auth.sent.set.password.hint": "Le lien pour définir votre mot de passe a été envoyé.",
|
||||
"com.affine.auth.sent.set.password.success.message": "Votre mot de passe a été enregistré ! Vous pouvez vous connecter à AFFiNE Cloud avec votre e-mail et votre mot de passe.",
|
||||
"com.affine.auth.sent.verify.email.hint": "Le lien de vérification a été envoyé",
|
||||
"com.affine.auth.set.email.save": "Enregistrer le mail",
|
||||
"com.affine.auth.set.password": "Définir le mot de passe",
|
||||
"com.affine.auth.set.password.message": "Merci de rentrer un mot de passe de {{min}}-{{max}} caractères avec des lettres et des numéros pour continuer à vous créer un compte",
|
||||
"com.affine.auth.set.password.message.maxlength": "Maximum {{max}} caractères",
|
||||
"com.affine.auth.set.password.message.minlength": "Minimum {{max}} caractères",
|
||||
"com.affine.auth.set.password.page.success": "Mot de passe définit avec succès",
|
||||
"com.affine.auth.set.password.message.minlength": "Minimum {{min}} caractères",
|
||||
"com.affine.auth.set.password.page.success": "Mot de passe défini avec succès",
|
||||
"com.affine.auth.set.password.page.title": "Définir votre mot de passe pour AFFiNE Cloud",
|
||||
"com.affine.auth.set.password.placeholder": "Définissez un mot de passe d'au moins {{min}} caractères",
|
||||
"com.affine.auth.set.password.placeholder.confirm": "Confirmer votre mot de passe",
|
||||
@@ -423,7 +423,7 @@
|
||||
"com.affine.collection.allCollections": "Toutes les collections",
|
||||
"com.affine.collection.emptyCollection": "Collection vide",
|
||||
"com.affine.collection.emptyCollectionDescription": "La collection est un dossier intelligent auquel vous pouvez ajouter des documents manuellement ou automatiquement à l'aide de règles.",
|
||||
"com.affine.collection.helpInfo": "AIDE INFO",
|
||||
"com.affine.collection.helpInfo": "Informations",
|
||||
"com.affine.collection.menu.edit": "Modifier la collection",
|
||||
"com.affine.collection.menu.rename": "Renommer",
|
||||
"com.affine.collection.removePage.success": "Supprimer avec succès",
|
||||
@@ -581,7 +581,7 @@
|
||||
"com.affine.history.confirm-restore-modal.hint": "Vous êtes sur le point de restaurer la version actuelle du document vers la dernière version disponible. Cette action écrasera toutes les modifications apportées à la dernière version.",
|
||||
"com.affine.history.confirm-restore-modal.load-more": "Charger plus",
|
||||
"com.affine.history.confirm-restore-modal.plan-prompt.limited-title": "HISTORIQUE DES DOCUMENTS LIMITÉS",
|
||||
"com.affine.history.confirm-restore-modal.plan-prompt.title": "AIDE INFO",
|
||||
"com.affine.history.confirm-restore-modal.plan-prompt.title": "Informations",
|
||||
"com.affine.history.confirm-restore-modal.pro-plan-prompt.description": "Avec le compte payant du créateur de l'espace de travail, tous les membres ont le privilège d'accéder à jusqu'à <1>30 jours<1> d'historique des versions.",
|
||||
"com.affine.history.confirm-restore-modal.pro-plan-prompt.upgrade": "Passer à la version Pro",
|
||||
"com.affine.history.confirm-restore-modal.restore": "Restaurer",
|
||||
@@ -833,7 +833,7 @@
|
||||
"com.affine.pageMode.all": "tout",
|
||||
"com.affine.pageMode.edgeless": "Mode sans bords",
|
||||
"com.affine.pageMode.page": "Page",
|
||||
"com.affine.payment.ai-upgrade-success-page.text": "Félicitations pour votre achat d'AFFiNE IA ! Vous avez désormais la possibilité de perfectionner votre contenu, de générer des images et de créer des cartes mentales complètes directement avec AFFiNE AI, améliorant considérablement votre productivité.",
|
||||
"com.affine.payment.ai-upgrade-success-page.text": "Félicitations pour votre achat d'AFFiNE IA ! Vous avez désormais la possibilité de perfectionner votre contenu, de générer des images et de créer des cartes mentales complètes directement avec AFFiNE IA, améliorant considérablement votre productivité.",
|
||||
"com.affine.payment.ai-upgrade-success-page.title": "Achat réussi !",
|
||||
"com.affine.payment.ai.action.cancel.button-label": "Annuler l'abonnement",
|
||||
"com.affine.payment.ai.action.cancel.confirm.cancel-text": "Garder AFFiNE IA",
|
||||
@@ -871,7 +871,7 @@
|
||||
"com.affine.payment.ai.pricing-plan.title-caption-2": "Un véritable copilote intelligent multimodale.",
|
||||
"com.affine.payment.ai.subscribe.billed-annually": "Facturé annuellement",
|
||||
"com.affine.payment.ai.usage-description-purchased": "Vous avez acheté AFFiNE IA.",
|
||||
"com.affine.payment.ai.usage-title": "Utilisation d'AFFiNE AI",
|
||||
"com.affine.payment.ai.usage-title": "Utilisation d'AFFiNE IA",
|
||||
"com.affine.payment.ai.usage.change-button-label": "Passé à la version Pro",
|
||||
"com.affine.payment.ai.usage.purchase-button-label": "Passer à la version Pro",
|
||||
"com.affine.payment.ai.usage.used-caption": "Nombre d'utilisation",
|
||||
@@ -1323,12 +1323,12 @@
|
||||
"com.affine.settings.editorSettings.edgeless.text.font-style": "Style de police",
|
||||
"com.affine.settings.editorSettings.edgeless.text.font-weight": "Poids de la police",
|
||||
"com.affine.settings.editorSettings.general": "Général",
|
||||
"com.affine.settings.editorSettings.general.ai.description": "Activer l'assistant AI puissant, AFFiNE AI.",
|
||||
"com.affine.settings.editorSettings.general.ai.disable.confirm": "Désactiver l'AI et recharger",
|
||||
"com.affine.settings.editorSettings.general.ai.disable.description": "Êtes-vous sûr de vouloir désactiver l'AI ? Nous apprécions votre productivité et notre AI peut l'améliorer. Réfléchissez-y à deux fois !",
|
||||
"com.affine.settings.editorSettings.general.ai.description": "Activer l'assistant IA puissant, AFFiNE IA.",
|
||||
"com.affine.settings.editorSettings.general.ai.disable.confirm": "Désactiver l'IA et recharger",
|
||||
"com.affine.settings.editorSettings.general.ai.disable.description": "Êtes-vous sûr de vouloir désactiver l'IA ? Nous apprécions votre productivité et notre IA peut l'améliorer. Réfléchissez-y à deux fois !",
|
||||
"com.affine.settings.editorSettings.general.ai.disable.title": "Désactiver l'IA ?",
|
||||
"com.affine.settings.editorSettings.general.ai.enable.confirm": "Activer l'AI et recharger",
|
||||
"com.affine.settings.editorSettings.general.ai.enable.description": "Souhaitez-vous activer l'AI ? Notre assistant AI est prêt à améliorer votre productivité et à offrir une assistance intelligente. Commençons ! Nous devons recharger la page pour effectuer ce changement.",
|
||||
"com.affine.settings.editorSettings.general.ai.enable.confirm": "Activer l'IA et recharger",
|
||||
"com.affine.settings.editorSettings.general.ai.enable.description": "Souhaitez-vous activer l'IA ? Notre assistant IA est prêt à améliorer votre productivité et à offrir une assistance intelligente. Commençons ! Nous devons recharger la page pour effectuer ce changement.",
|
||||
"com.affine.settings.editorSettings.general.ai.enable.title": "Activer l'IA ?",
|
||||
"com.affine.settings.editorSettings.general.ai.title": "AFFiNE IA",
|
||||
"com.affine.settings.editorSettings.general.default-code-block.language.description": "Définir un langage de programmation par défaut.",
|
||||
@@ -1396,22 +1396,22 @@
|
||||
"com.affine.settings.translucent-style": "UI translucide sur la barre latérale",
|
||||
"com.affine.settings.translucent-style-description": "Utiliser l'effet translucide sur la barre latérale",
|
||||
"com.affine.settings.meetings": "Réunions",
|
||||
"com.affine.settings.meetings.setting.welcome": "Au-delà de l'enregistrement\nVotre assistant de réunion AI est ici",
|
||||
"com.affine.settings.meetings.setting.welcome": "Au-delà de l'enregistrement\nVotre assistant de réunion IA est disponible",
|
||||
"com.affine.settings.meetings.setting.prompt": "Capture audio native, pas de robots requis - Directement de votre Mac à l'intelligence des réunions.",
|
||||
"com.affine.settings.meetings.setting.prompt.2": "Fonctionnalités de réunion disponibles <strong>gratuitement</strong> en phase bêta",
|
||||
"com.affine.settings.meetings.setting.welcome.hints": "<strong> Où l'AI rencontre vos réunions - affine votre collaboration.</strong>\n<ul><li>Extraire instantanément les éléments d'action et les idées clés</li><li>La capture automatique intelligente commence avec votre réunion</li><li>Intégration transparente sur toutes les plateformes de réunion</li><li>Un espace unifié pour tout le contexte de votre réunion</li><li>Votre assistant AI avec chaque contexte de réunion préservé</li></ul>",
|
||||
"com.affine.settings.meetings.setting.welcome.hints": "<strong>L'IA au service de vos réunions pour fluidifier la collaboration.</strong>\n<ul><li>Extraction instantanée des actions à mener et des points clés</li><li>Capture intelligente automatique au démarrage de la réunion</li><li>Intégration transparente avec les principales plateformes de réunion</li><li>Espace unifié pour centraliser le contexte de vos réunions</li><li>Assistant IA avec conservation de l'historique de chaque réunion</li></ul>",
|
||||
"com.affine.settings.meetings.setting.welcome.learn-more": "En savoir plus",
|
||||
"com.affine.settings.meetings.enable.title": "Activer les notes de réunion",
|
||||
"com.affine.settings.meetings.enable.description": "Utilisez les notes de réunion et les fonctionnalités de résumé AI fournies par AFFiNE. <1>Discutez-en plus dans la communauté</1>.",
|
||||
"com.affine.settings.meetings.enable.description": "Utilisez les notes de réunion et les fonctionnalités de synthèse IA fournies par AFFiNE. <1>En savoir plus dans la communauté</1>.",
|
||||
"com.affine.settings.meetings.record.header": "Enregistrement de la réunion",
|
||||
"com.affine.settings.meetings.record.recording-mode": "Quand la réunion commence",
|
||||
"com.affine.settings.meetings.record.recording-mode.description": "Choisissez le comportement au démarrage de la réunion.",
|
||||
"com.affine.settings.meetings.record.open-saved-file": "Ouvrir les enregistrements sauvegardés",
|
||||
"com.affine.settings.meetings.record.open-saved-file.description": "Ouvrez les fichiers d'enregistrement stockés localement.",
|
||||
"com.affine.settings.meetings.transcription.header": "Transcription avec IA",
|
||||
"com.affine.settings.meetings.transcription.auto-summary": "Résumé automatique de l'AI",
|
||||
"com.affine.settings.meetings.transcription.auto-summary": "Résumé automatique de l'IA",
|
||||
"com.affine.settings.meetings.transcription.auto-summary.description": "Générez automatiquement un résumé des notes de réunion.",
|
||||
"com.affine.settings.meetings.transcription.auto-todo": "Liste de tâches automatique AI",
|
||||
"com.affine.settings.meetings.transcription.auto-todo": "Liste de tâches automatique IA",
|
||||
"com.affine.settings.meetings.transcription.auto-todo.description": "Générez automatiquement une liste de tâches des notes de réunion.",
|
||||
"com.affine.settings.meetings.privacy.header": "Confidentialité et sécurité",
|
||||
"com.affine.settings.meetings.privacy.screen-system-audio-recording": "Enregistrement de l'écran et de l'audio système",
|
||||
@@ -1444,10 +1444,10 @@
|
||||
"com.affine.settings.workspace.experimental-features.enable-ai.description": "Activer ou désactiver toutes les fonctionnalités d'IA.",
|
||||
"com.affine.settings.workspace.experimental-features.enable-ai-network-search.name": "Activer la recherche de réseau IA",
|
||||
"com.affine.settings.workspace.experimental-features.enable-ai-network-search.description": "Activer ou désactiver la fonction de recherche de réseau IA.",
|
||||
"com.affine.settings.workspace.experimental-features.enable-ai-model-switch.name": "Activer l'interrupteur du modèle AI",
|
||||
"com.affine.settings.workspace.experimental-features.enable-ai-model-switch.description": "Activer ou désactiver la fonction de commutation de modèle AI.",
|
||||
"com.affine.settings.workspace.experimental-features.enable-ai-playground.name": "Activer AI Playground",
|
||||
"com.affine.settings.workspace.experimental-features.enable-ai-playground.description": "Activer ou désactiver la fonction AI Playground.",
|
||||
"com.affine.settings.workspace.experimental-features.enable-ai-model-switch.name": "Activer le sélecteur de modèle IA",
|
||||
"com.affine.settings.workspace.experimental-features.enable-ai-model-switch.description": "Activer ou désactiver la fonctionnalité de sélection de modèle IA.",
|
||||
"com.affine.settings.workspace.experimental-features.enable-ai-playground.name": "Activer le bac à sable IA",
|
||||
"com.affine.settings.workspace.experimental-features.enable-ai-playground.description": "Activer ou désactiver la fonctionnalité de bac à sable IA.",
|
||||
"com.affine.settings.workspace.experimental-features.enable-database-full-width.name": "Largeur complète de la base de données",
|
||||
"com.affine.settings.workspace.experimental-features.enable-database-full-width.description": "La base de données sera affichée en mode pleine largeur.",
|
||||
"com.affine.settings.workspace.experimental-features.enable-database-attachment-note.name": "Note de l'attachement de la base de données",
|
||||
@@ -1591,8 +1591,8 @@
|
||||
"com.affine.settings.workspace.sharing.url-preview.description": "Autoriser le dépliage d'URL par Slack et d'autres applications sociales, même si un document est uniquement accessible par les membres de l'espace de travail.",
|
||||
"com.affine.settings.workspace.sharing.url-preview.title": "Toujours activer l'aperçu de l'URL",
|
||||
"com.affine.settings.workspace.affine-ai.title": "AFFiNE IA",
|
||||
"com.affine.settings.workspace.affine-ai.label": "Autoriser l'assistant AFFiNE AI",
|
||||
"com.affine.settings.workspace.affine-ai.description": "Permettre aux membres de l'espace de travail d'utiliser les fonctionnalités AFFiNE AI. Ce paramètre n'affecte pas la facturation. Les membres de l'espace de travail utilisent AFFiNE AI à travers leurs comptes personnels.",
|
||||
"com.affine.settings.workspace.affine-ai.label": "Autoriser l'assistant AFFiNE IA",
|
||||
"com.affine.settings.workspace.affine-ai.description": "Permettre aux membres de l'espace de travail d'utiliser les fonctionnalités AFFiNE IA. Ce paramètre n'affecte pas la facturation. Les membres de l'espace de travail utilisent AFFiNE IA via leurs comptes personnels.",
|
||||
"com.affine.settings.workspace.backup": "Espaces de travail archivés",
|
||||
"com.affine.settings.workspace.backup.subtitle": "Gérer les fichiers d'espaces de travail locaux archivés",
|
||||
"com.affine.settings.workspace.backup.empty": "Aucun fichier d'espace de travail archivé trouvé",
|
||||
@@ -1604,7 +1604,7 @@
|
||||
"com.affine.settings.workspace.backup.import.success.action": "Ouvrir",
|
||||
"com.affine.settings.workspace.backup.delete-at": "Supprimé le {{date}} à {{time}}",
|
||||
"com.affine.settings.workspace.indexer-embedding.title": "Indexeur & Intégration",
|
||||
"com.affine.settings.workspace.indexer-embedding.description": "Gérer l'indexation AFFiNE et l'intégration AI pour le traitement de contenu local",
|
||||
"com.affine.settings.workspace.indexer-embedding.description": "Gérer l'indexation AFFiNE et l'intégration IA pour le traitement du contenu local",
|
||||
"com.affine.settings.workspace.indexer-embedding.embedding.title": "Intégration",
|
||||
"com.affine.settings.workspace.indexer-embedding.embedding.description": "L'intégration permet à l'IA de récupérer votre contenu. Si l'indexeur utilise des paramètres locaux, cela peut affecter certains résultats de l'intégration.",
|
||||
"com.affine.settings.workspace.indexer-embedding.embedding.disabled-tooltip": "Seul le propriétaire de l'espace de travail peut activer l'intégration de l'espace de travail.",
|
||||
@@ -2071,8 +2071,8 @@
|
||||
"com.affine.integration.mcp-server.desc": "Permettre à d'autres clients MCP de rechercher et de consulter la documentation d'AFFiNE.",
|
||||
"com.affine.audio.notes": "Notes",
|
||||
"com.affine.audio.transcribing": "Transcription en cours",
|
||||
"com.affine.audio.transcribe.non-owner.confirm.title": "Impossible de récupérer les résultats AI pour les autres",
|
||||
"com.affine.audio.transcribe.non-owner.confirm.message": "Veuillez contacter <1>{{user}}</1> pour mettre à niveau les droits AI ou renvoyer la pièce jointe.",
|
||||
"com.affine.audio.transcribe.non-owner.confirm.title": "Impossible de récupérer les résultats IA pour les autres",
|
||||
"com.affine.audio.transcribe.non-owner.confirm.message": "Veuillez contacter <1>{{user}}</1> pour mettre à niveau les droits IA ou renvoyer la pièce jointe.",
|
||||
"com.affine.recording.new": "Activité audio",
|
||||
"com.affine.recording.success.prompt": "Terminé",
|
||||
"com.affine.recording.success.button": "Ouvrir l'application",
|
||||
@@ -2099,7 +2099,7 @@
|
||||
"com.affine.comment.filter.only-current-mode": "Seul le mode actuel",
|
||||
"com.affine.payment.subscription.title": "Débloquer plus de fonctionnalités",
|
||||
"com.affine.payment.subscription.description": "L'éditeur universel qui vous permet de travailler, de vous divertir, de présenter ou de créer presque tout.",
|
||||
"com.affine.payment.subscription.button": "Mettre a niveau",
|
||||
"com.affine.payment.subscription.button": "Mettre à niveau",
|
||||
"com.affine.comment.reply": "Réponse",
|
||||
"com.affine.comment.copy-link": "Copier le lien",
|
||||
"com.affine.context-menu.copy": "Copier",
|
||||
@@ -2159,9 +2159,9 @@
|
||||
"error.BLOB_NOT_FOUND": "Blob {{blobId}} introuvable dans l'Espace {{spaceId}}.",
|
||||
"error.EXPECT_TO_PUBLISH_DOC": "Attendu publier un document, pas un espace.",
|
||||
"error.EXPECT_TO_REVOKE_PUBLIC_DOC": "Attendu révoquer un document public, pas un espace.",
|
||||
"error.EXPECT_TO_GRANT_DOC_USER_ROLES": "Attendu accorder des rôles sur le doc {{docId}} sous l'espace {{spaceId}}, pas un espace.",
|
||||
"error.EXPECT_TO_REVOKE_DOC_USER_ROLES": "Attendu révoquer des rôles sur le doc {{docId}} sous l'espace {{spaceId}}, pas un espace.",
|
||||
"error.EXPECT_TO_UPDATE_DOC_USER_ROLE": "Attendu mettre à jour des rôles sur le doc {{docId}} sous l'espace {{spaceId}}, pas un espace.",
|
||||
"error.EXPECT_TO_GRANT_DOC_USER_ROLES": "Accord de rôles attendu sur le document {{docId}} dans l'espace {{spaceId}}, pas sur un espace.",
|
||||
"error.EXPECT_TO_REVOKE_DOC_USER_ROLES": "Révocation de rôles attendue sur le document {{docId}} dans l'espace {{spaceId}}, pas sur un espace.",
|
||||
"error.EXPECT_TO_UPDATE_DOC_USER_ROLE": "Mise à jour de rôles attendue sur le document {{docId}} dans l'espace {{spaceId}}, pas sur un espace.",
|
||||
"error.DOC_IS_NOT_PUBLIC": "Le document n'est pas public.",
|
||||
"error.FAILED_TO_SAVE_UPDATES": "Échec de l'enregistrement des mises à jour du document.",
|
||||
"error.FAILED_TO_UPSERT_SNAPSHOT": "Échec de l'enregistrement du snapshot du document.",
|
||||
@@ -2233,7 +2233,7 @@
|
||||
"error.LICENSE_EXPIRED": "La licence a expiré.",
|
||||
"error.UNSUPPORTED_CLIENT_VERSION": "Client non pris en charge avec la version [{{clientVersion}}], la version requise est [{{requiredVersion}}].",
|
||||
"error.NOTIFICATION_NOT_FOUND": "Notification non trouvée.",
|
||||
"error.MENTION_USER_DOC_ACCESS_DENIED": "L'utilisateur mentionné ne peut pas accéder au doc {{docId}}.",
|
||||
"error.MENTION_USER_DOC_ACCESS_DENIED": "L'utilisateur mentionné ne peut pas accéder au document {{docId}}.",
|
||||
"error.MENTION_USER_ONESELF_DENIED": "Vous ne pouvez pas vous mentionner vous-même.",
|
||||
"error.INVALID_APP_CONFIG": "Configuration d'application invalide pour le module `{{module}}` avec la clé `{{key}}`. {{hint}}.",
|
||||
"error.INVALID_APP_CONFIG_INPUT": "Entrée de configuration d'application invalide : {{message}}",
|
||||
@@ -2243,5 +2243,71 @@
|
||||
"error.COMMENT_NOT_FOUND": "Commentaire non trouvé.",
|
||||
"error.REPLY_NOT_FOUND": "Réponse non trouvée.",
|
||||
"error.COMMENT_ATTACHMENT_NOT_FOUND": "Pièce jointe du commentaire introuvable.",
|
||||
"error.COMMENT_ATTACHMENT_QUOTA_EXCEEDED": "Vous avez dépassé le quota de taille de pièce jointe de commentaire."
|
||||
"error.COMMENT_ATTACHMENT_QUOTA_EXCEEDED": "Vous avez dépassé le quota de taille de pièce jointe de commentaire.",
|
||||
"com.affine.ai.chat-panel.embedding-progress": "Intégration {{done}}/{{total}}",
|
||||
"com.affine.ai.chat-panel.loading-history": "AFFiNE IA charge l'historique...",
|
||||
"com.affine.ai.chat-panel.session.delete.confirm.message": "Voulez-vous supprimer cet historique de conversation IA ? Une fois supprimé, il ne pourra pas être récupéré.",
|
||||
"com.affine.ai.chat-panel.session.delete.confirm.title": "Supprimer cet historique ?",
|
||||
"com.affine.ai.chat-panel.session.delete.toast.failed": "Échec de la suppression de l'historique",
|
||||
"com.affine.ai.chat-panel.session.delete.toast.success": "Historique supprimé",
|
||||
"com.affine.ai.chat-panel.title": "AFFiNE IA",
|
||||
"com.affine.appearanceSettings.images.antialiasing.description": "Lorsqu'elle est désactivée, les images sont rendues avec un redimensionnement au plus proche voisin pour des pixels nets.",
|
||||
"com.affine.appearanceSettings.images.antialiasing.title": "Rendu lissé des images",
|
||||
"com.affine.appearanceSettings.images.title": "Images",
|
||||
"com.affine.doc.analytics.chart.total-views": "Vues totales",
|
||||
"com.affine.doc.analytics.chart.unique-views": "Vues uniques",
|
||||
"com.affine.doc.analytics.empty.no-page-views": "Aucune vue de page sur cette période.",
|
||||
"com.affine.doc.analytics.empty.no-viewers": "Aucun lecteur sur cette période.",
|
||||
"com.affine.doc.analytics.error.load-analytics": "Impossible de charger les statistiques.",
|
||||
"com.affine.doc.analytics.error.load-viewers": "Impossible de charger les lecteurs.",
|
||||
"com.affine.doc.analytics.metric.guest": "Invité",
|
||||
"com.affine.doc.analytics.metric.total": "Total",
|
||||
"com.affine.doc.analytics.metric.unique": "Unique",
|
||||
"com.affine.doc.analytics.paywall.open-pricing": "Voir les offres tarifaires",
|
||||
"com.affine.doc.analytics.paywall.toast": "Les statistiques de document au-delà de 7 jours nécessitent un abonnement AFFiNE Team.",
|
||||
"com.affine.doc.analytics.summary.total": "({{count}} au total)",
|
||||
"com.affine.doc.analytics.title": "Statistiques de vues",
|
||||
"com.affine.doc.analytics.viewers.show-all": "Afficher tous les lecteurs",
|
||||
"com.affine.doc.analytics.viewers.title": "Lecteurs",
|
||||
"com.affine.doc.analytics.window.last-days": "{{days}} derniers jours",
|
||||
"com.affine.docIconPicker.placeholder": "Ajouter une icône",
|
||||
"com.affine.import.docx": "Docx",
|
||||
"com.affine.integration.calendar.account.count": "{{count}} calendrier",
|
||||
"com.affine.integration.calendar.account.link": "Lier",
|
||||
"com.affine.integration.calendar.account.linked-empty": "Aucun compte calendrier lié pour le moment.",
|
||||
"com.affine.integration.calendar.account.load-error": "Échec du chargement des comptes calendrier",
|
||||
"com.affine.integration.calendar.account.status.failed": "Autorisation échouée : {{error}}",
|
||||
"com.affine.integration.calendar.account.status.failed-reconnect": "Autorisation échouée. Veuillez reconnecter votre compte.",
|
||||
"com.affine.integration.calendar.account.unlink": "Dissocier",
|
||||
"com.affine.integration.calendar.account.unlink-error": "Échec de la dissociation du compte calendrier",
|
||||
"com.affine.integration.calendar.auth.start-error": "Échec du démarrage de l'autorisation calendrier",
|
||||
"com.affine.integration.calendar.caldav.field.displayName": "Nom d'affichage (optionnel)",
|
||||
"com.affine.integration.calendar.caldav.field.displayName.placeholder": "Mon CalDAV",
|
||||
"com.affine.integration.calendar.caldav.field.password": "Mot de passe",
|
||||
"com.affine.integration.calendar.caldav.field.password.error": "Le mot de passe est requis.",
|
||||
"com.affine.integration.calendar.caldav.field.password.placeholder": "Mot de passe ou mot de passe spécifique à l'application",
|
||||
"com.affine.integration.calendar.caldav.field.provider": "Fournisseur",
|
||||
"com.affine.integration.calendar.caldav.field.provider.error": "Veuillez sélectionner un fournisseur.",
|
||||
"com.affine.integration.calendar.caldav.field.provider.placeholder": "Sélectionner un fournisseur",
|
||||
"com.affine.integration.calendar.caldav.field.username": "Nom d'utilisateur",
|
||||
"com.affine.integration.calendar.caldav.field.username.error": "Le nom d'utilisateur est requis.",
|
||||
"com.affine.integration.calendar.caldav.field.username.placeholder": "email@example.com",
|
||||
"com.affine.integration.calendar.caldav.hint.app-password": "Un mot de passe spécifique à l'application est requis.",
|
||||
"com.affine.integration.calendar.caldav.hint.guide": "Guide de configuration du fournisseur",
|
||||
"com.affine.integration.calendar.caldav.hint.learn-more": "En savoir plus",
|
||||
"com.affine.integration.calendar.caldav.link.failed": "Échec de la liaison du compte CalDAV",
|
||||
"com.affine.integration.calendar.caldav.link.title": "Lier un compte CalDAV",
|
||||
"com.affine.integration.calendar.no-calendar": "Aucun calendrier abonné pour le moment.",
|
||||
"com.affine.integration.calendar.no-journal": "Aucune page journal trouvée pour le {{date}}. Veuillez d'abord créer une page journal.",
|
||||
"com.affine.integration.calendar.provider.load-error": "Échec du chargement des fournisseurs de calendrier",
|
||||
"com.affine.integration.calendar.save-error": "Une erreur est survenue lors de l'enregistrement des paramètres de calendrier",
|
||||
"com.affine.settings.workspace.sharing.workspace-sharing.description": "Contrôlez si les pages de cet espace de travail peuvent être partagées publiquement. Désactivez pour bloquer les nouveaux partages et l'accès externe aux partages existants.",
|
||||
"com.affine.settings.workspace.sharing.workspace-sharing.title": "Autoriser le partage des pages de l'espace de travail",
|
||||
"com.affine.share-menu.workspace-sharing.disabled.tooltip": "Le partage est désactivé pour cet espace de travail. Veuillez contacter un administrateur pour l'activer.",
|
||||
"com.affine.workspaceSubPath.chat": "Intelligence",
|
||||
"error.BLOB_INVALID": "Le blob est invalide.",
|
||||
"error.CALENDAR_PROVIDER_REQUEST_ERROR": "Erreur de requête du fournisseur de calendrier, statut : {{status}}, message : {{message}}",
|
||||
"error.MANAGED_BY_APP_STORE_OR_PLAY": "Cet abonnement est géré par l'App Store ou Google Play. Veuillez le gérer dans la boutique correspondante.",
|
||||
"error.RESPONSE_TOO_LARGE_ERROR": "Réponse trop volumineuse ({{receivedBytes}} octets), la limite est de {{limitBytes}} octets",
|
||||
"error.SSRF_BLOCKED_ERROR": "URL invalide"
|
||||
}
|
||||
|
||||
@@ -231,7 +231,7 @@ test('items in favourites can be reordered by dragging', async ({ page }) => {
|
||||
});
|
||||
|
||||
// some how this test always timeout, so we skip it
|
||||
test.skip('drag a page link in editor to favourites', async ({ page }) => {
|
||||
test('drag a page link in editor to favourites', async ({ page }) => {
|
||||
await clickNewPageButton(page);
|
||||
await page.waitForTimeout(500);
|
||||
await page.keyboard.press('Enter');
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { Page } from '@playwright/test';
|
||||
import { expect } from '@playwright/test';
|
||||
|
||||
export let coreUrl = 'http://localhost:8080';
|
||||
|
||||
@@ -21,6 +20,7 @@ export async function confirmCreateJournal(page: Page) {
|
||||
}
|
||||
|
||||
export async function openJournalsPage(page: Page) {
|
||||
const { expect } = await import('@playwright/test');
|
||||
await page.getByTestId('slider-bar-journals-button').click();
|
||||
await confirmCreateJournal(page);
|
||||
await expect(
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
"@affine/s3-compat": "workspace:*",
|
||||
"@napi-rs/simple-git": "^0.1.22",
|
||||
"@perfsee/webpack": "^1.13.0",
|
||||
"@rspack/core": "^1.7.6",
|
||||
"@rspack/dev-server": "^1.1.3",
|
||||
"@sentry/webpack-plugin": "^4.0.0",
|
||||
"@swc/core": "^1.10.1",
|
||||
"@tailwindcss/postcss": "^4.0.0",
|
||||
|
||||
77
tools/cli/src/bundle-shared.ts
Normal file
77
tools/cli/src/bundle-shared.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import type { Configuration as WebpackDevServerConfiguration } from 'webpack-dev-server';
|
||||
|
||||
export const RSPACK_SUPPORTED_PACKAGES = [
|
||||
'@affine/admin',
|
||||
'@affine/web',
|
||||
'@affine/mobile',
|
||||
'@affine/ios',
|
||||
'@affine/android',
|
||||
'@affine/electron-renderer',
|
||||
'@affine/server',
|
||||
] as const;
|
||||
|
||||
const rspackSupportedPackageSet = new Set<string>(RSPACK_SUPPORTED_PACKAGES);
|
||||
|
||||
export function isRspackSupportedPackageName(name: string) {
|
||||
return rspackSupportedPackageSet.has(name);
|
||||
}
|
||||
|
||||
export function assertRspackSupportedPackageName(name: string) {
|
||||
if (isRspackSupportedPackageName(name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`AFFINE_BUNDLER=rspack currently supports: ${Array.from(RSPACK_SUPPORTED_PACKAGES).join(', ')}. Use AFFINE_BUNDLER=webpack for ${name}.`
|
||||
);
|
||||
}
|
||||
|
||||
const IN_CI = !!process.env.CI;
|
||||
const httpProxyMiddlewareLogLevel = IN_CI ? 'silent' : 'error';
|
||||
|
||||
export const DEFAULT_DEV_SERVER_CONFIG: WebpackDevServerConfiguration = {
|
||||
host: '0.0.0.0',
|
||||
allowedHosts: 'all',
|
||||
hot: false,
|
||||
liveReload: true,
|
||||
compress: !process.env.CI,
|
||||
setupExitSignals: true,
|
||||
client: {
|
||||
overlay: process.env.DISABLE_DEV_OVERLAY === 'true' ? false : undefined,
|
||||
logging: process.env.CI ? 'none' : 'error',
|
||||
// see: https://webpack.js.org/configuration/dev-server/#websocketurl
|
||||
// must be an explicit ws/wss URL because custom protocols (e.g. assets://)
|
||||
// cannot be used to construct WebSocket endpoints in Electron
|
||||
webSocketURL: 'ws://0.0.0.0:8080/ws',
|
||||
},
|
||||
historyApiFallback: {
|
||||
rewrites: [
|
||||
{
|
||||
from: /.*/,
|
||||
to: () => {
|
||||
return process.env.SELF_HOSTED === 'true'
|
||||
? '/selfhost.html'
|
||||
: '/index.html';
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
proxy: [
|
||||
{
|
||||
context: '/api',
|
||||
target: 'http://localhost:3010',
|
||||
logLevel: httpProxyMiddlewareLogLevel,
|
||||
},
|
||||
{
|
||||
context: '/socket.io',
|
||||
target: 'http://localhost:3010',
|
||||
ws: true,
|
||||
logLevel: httpProxyMiddlewareLogLevel,
|
||||
},
|
||||
{
|
||||
context: '/graphql',
|
||||
target: 'http://localhost:3010',
|
||||
logLevel: httpProxyMiddlewareLogLevel,
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -3,20 +3,65 @@ import { cpus } from 'node:os';
|
||||
|
||||
import { Logger } from '@affine-tools/utils/logger';
|
||||
import { Package } from '@affine-tools/utils/workspace';
|
||||
import rspack, { type MultiRspackOptions } from '@rspack/core';
|
||||
import {
|
||||
type Configuration as RspackDevServerConfiguration,
|
||||
RspackDevServer,
|
||||
} from '@rspack/dev-server';
|
||||
import { merge } from 'lodash-es';
|
||||
import webpack from 'webpack';
|
||||
import WebpackDevServer, {
|
||||
type Configuration as DevServerConfiguration,
|
||||
type Configuration as WebpackDevServerConfiguration,
|
||||
} from 'webpack-dev-server';
|
||||
|
||||
import {
|
||||
assertRspackSupportedPackageName,
|
||||
DEFAULT_DEV_SERVER_CONFIG,
|
||||
isRspackSupportedPackageName,
|
||||
} from './bundle-shared';
|
||||
import { type Bundler, getBundler } from './bundler';
|
||||
import { Option, PackageCommand } from './command';
|
||||
import {
|
||||
createHTMLTargetConfig,
|
||||
createNodeTargetConfig,
|
||||
createWorkerTargetConfig,
|
||||
createHTMLTargetConfig as createRspackHTMLTargetConfig,
|
||||
createNodeTargetConfig as createRspackNodeTargetConfig,
|
||||
createWorkerTargetConfig as createRspackWorkerTargetConfig,
|
||||
} from './rspack';
|
||||
import {
|
||||
createHTMLTargetConfig as createWebpackHTMLTargetConfig,
|
||||
createNodeTargetConfig as createWebpackNodeTargetConfig,
|
||||
createWorkerTargetConfig as createWebpackWorkerTargetConfig,
|
||||
} from './webpack';
|
||||
import {
|
||||
shouldUploadReleaseAssets,
|
||||
uploadDistAssetsToS3,
|
||||
} from './webpack/s3-plugin.js';
|
||||
|
||||
function getBaseWorkerConfigs(pkg: Package) {
|
||||
type WorkerConfig = { name: string };
|
||||
type CreateWorkerTargetConfig = (pkg: Package, entry: string) => WorkerConfig;
|
||||
|
||||
function assertRspackSupportedPackage(pkg: Package) {
|
||||
assertRspackSupportedPackageName(pkg.name);
|
||||
}
|
||||
|
||||
function shouldUploadAssetsForPackage(pkg: Package): boolean {
|
||||
return (
|
||||
!!process.env.R2_SECRET_ACCESS_KEY && shouldUploadReleaseAssets(pkg.name)
|
||||
);
|
||||
}
|
||||
|
||||
async function uploadAssetsForPackage(pkg: Package, logger: Logger) {
|
||||
if (!shouldUploadAssetsForPackage(pkg)) {
|
||||
return;
|
||||
}
|
||||
logger.info('Uploading dist assets to R2...');
|
||||
await uploadDistAssetsToS3(pkg.distPath.value);
|
||||
logger.info('Uploaded dist assets to R2.');
|
||||
}
|
||||
|
||||
function getBaseWorkerConfigs(
|
||||
pkg: Package,
|
||||
createWorkerTargetConfig: CreateWorkerTargetConfig
|
||||
) {
|
||||
const core = new Package('@affine/core');
|
||||
|
||||
return [
|
||||
@@ -39,27 +84,30 @@ function getBaseWorkerConfigs(pkg: Package) {
|
||||
];
|
||||
}
|
||||
|
||||
function getBundleConfigs(pkg: Package): webpack.MultiConfiguration {
|
||||
function getWebpackBundleConfigs(pkg: Package): webpack.MultiConfiguration {
|
||||
switch (pkg.name) {
|
||||
case '@affine/admin': {
|
||||
return [
|
||||
createHTMLTargetConfig(pkg, pkg.srcPath.join('index.tsx').value),
|
||||
createWebpackHTMLTargetConfig(pkg, pkg.srcPath.join('index.tsx').value),
|
||||
] as webpack.MultiConfiguration;
|
||||
}
|
||||
case '@affine/web':
|
||||
case '@affine/mobile':
|
||||
case '@affine/ios':
|
||||
case '@affine/android': {
|
||||
const workerConfigs = getBaseWorkerConfigs(pkg);
|
||||
const workerConfigs = getBaseWorkerConfigs(
|
||||
pkg,
|
||||
createWebpackWorkerTargetConfig
|
||||
);
|
||||
workerConfigs.push(
|
||||
createWorkerTargetConfig(
|
||||
createWebpackWorkerTargetConfig(
|
||||
pkg,
|
||||
pkg.srcPath.join('nbstore.worker.ts').value
|
||||
)
|
||||
);
|
||||
|
||||
return [
|
||||
createHTMLTargetConfig(
|
||||
createWebpackHTMLTargetConfig(
|
||||
pkg,
|
||||
pkg.srcPath.join('index.tsx').value,
|
||||
{},
|
||||
@@ -69,10 +117,13 @@ function getBundleConfigs(pkg: Package): webpack.MultiConfiguration {
|
||||
] as webpack.MultiConfiguration;
|
||||
}
|
||||
case '@affine/electron-renderer': {
|
||||
const workerConfigs = getBaseWorkerConfigs(pkg);
|
||||
const workerConfigs = getBaseWorkerConfigs(
|
||||
pkg,
|
||||
createWebpackWorkerTargetConfig
|
||||
);
|
||||
|
||||
return [
|
||||
createHTMLTargetConfig(
|
||||
createWebpackHTMLTargetConfig(
|
||||
pkg,
|
||||
{
|
||||
index: pkg.srcPath.join('app/index.tsx').value,
|
||||
@@ -93,7 +144,7 @@ function getBundleConfigs(pkg: Package): webpack.MultiConfiguration {
|
||||
}
|
||||
case '@affine/server': {
|
||||
return [
|
||||
createNodeTargetConfig(pkg, pkg.srcPath.join('index.ts').value),
|
||||
createWebpackNodeTargetConfig(pkg, pkg.srcPath.join('index.ts').value),
|
||||
] as webpack.MultiConfiguration;
|
||||
}
|
||||
}
|
||||
@@ -101,55 +152,75 @@ function getBundleConfigs(pkg: Package): webpack.MultiConfiguration {
|
||||
throw new Error(`Unsupported package: ${pkg.name}`);
|
||||
}
|
||||
|
||||
const IN_CI = !!process.env.CI;
|
||||
const httpProxyMiddlewareLogLevel = IN_CI ? 'silent' : 'error';
|
||||
function getRspackBundleConfigs(pkg: Package): MultiRspackOptions {
|
||||
assertRspackSupportedPackage(pkg);
|
||||
|
||||
const defaultDevServerConfig: DevServerConfiguration = {
|
||||
host: '0.0.0.0',
|
||||
allowedHosts: 'all',
|
||||
hot: false,
|
||||
liveReload: true,
|
||||
compress: !process.env.CI,
|
||||
setupExitSignals: true,
|
||||
client: {
|
||||
overlay: process.env.DISABLE_DEV_OVERLAY === 'true' ? false : undefined,
|
||||
logging: process.env.CI ? 'none' : 'error',
|
||||
// see: https://webpack.js.org/configuration/dev-server/#websocketurl
|
||||
// must be an explicit ws/wss URL because custom protocols (e.g. assets://)
|
||||
// cannot be used to construct WebSocket endpoints in Electron
|
||||
webSocketURL: 'ws://0.0.0.0:8080/ws',
|
||||
},
|
||||
historyApiFallback: {
|
||||
rewrites: [
|
||||
{
|
||||
from: /.*/,
|
||||
to: () => {
|
||||
return process.env.SELF_HOSTED === 'true'
|
||||
? '/selfhost.html'
|
||||
: '/index.html';
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
proxy: [
|
||||
{
|
||||
context: '/api',
|
||||
target: 'http://localhost:3010',
|
||||
logLevel: httpProxyMiddlewareLogLevel,
|
||||
},
|
||||
{
|
||||
context: '/socket.io',
|
||||
target: 'http://localhost:3010',
|
||||
ws: true,
|
||||
logLevel: httpProxyMiddlewareLogLevel,
|
||||
},
|
||||
{
|
||||
context: '/graphql',
|
||||
target: 'http://localhost:3010',
|
||||
logLevel: httpProxyMiddlewareLogLevel,
|
||||
},
|
||||
],
|
||||
};
|
||||
switch (pkg.name) {
|
||||
case '@affine/admin': {
|
||||
return [
|
||||
createRspackHTMLTargetConfig(pkg, pkg.srcPath.join('index.tsx').value),
|
||||
] as MultiRspackOptions;
|
||||
}
|
||||
case '@affine/web':
|
||||
case '@affine/mobile':
|
||||
case '@affine/ios':
|
||||
case '@affine/android': {
|
||||
const workerConfigs = getBaseWorkerConfigs(
|
||||
pkg,
|
||||
createRspackWorkerTargetConfig
|
||||
);
|
||||
workerConfigs.push(
|
||||
createRspackWorkerTargetConfig(
|
||||
pkg,
|
||||
pkg.srcPath.join('nbstore.worker.ts').value
|
||||
)
|
||||
);
|
||||
|
||||
return [
|
||||
createRspackHTMLTargetConfig(
|
||||
pkg,
|
||||
pkg.srcPath.join('index.tsx').value,
|
||||
{},
|
||||
workerConfigs.map(config => config.name)
|
||||
),
|
||||
...workerConfigs,
|
||||
] as MultiRspackOptions;
|
||||
}
|
||||
case '@affine/electron-renderer': {
|
||||
const workerConfigs = getBaseWorkerConfigs(
|
||||
pkg,
|
||||
createRspackWorkerTargetConfig
|
||||
);
|
||||
|
||||
return [
|
||||
createRspackHTMLTargetConfig(
|
||||
pkg,
|
||||
{
|
||||
index: pkg.srcPath.join('app/index.tsx').value,
|
||||
shell: pkg.srcPath.join('shell/index.tsx').value,
|
||||
popup: pkg.srcPath.join('popup/index.tsx').value,
|
||||
backgroundWorker: pkg.srcPath.join('background-worker/index.ts')
|
||||
.value,
|
||||
},
|
||||
{
|
||||
additionalEntryForSelfhost: false,
|
||||
injectGlobalErrorHandler: false,
|
||||
emitAssetsManifest: false,
|
||||
},
|
||||
workerConfigs.map(config => config.name)
|
||||
),
|
||||
...workerConfigs,
|
||||
] as MultiRspackOptions;
|
||||
}
|
||||
case '@affine/server': {
|
||||
return [
|
||||
createRspackNodeTargetConfig(pkg, pkg.srcPath.join('index.ts').value),
|
||||
] as MultiRspackOptions;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`Unsupported package: ${pkg.name}`);
|
||||
}
|
||||
|
||||
export class BundleCommand extends PackageCommand {
|
||||
static override paths = [['bundle'], ['webpack'], ['pack'], ['bun']];
|
||||
@@ -164,22 +235,36 @@ export class BundleCommand extends PackageCommand {
|
||||
|
||||
async execute() {
|
||||
const pkg = this.workspace.getPackage(this.package);
|
||||
const bundler = getBundler();
|
||||
|
||||
if (this.dev) {
|
||||
await BundleCommand.dev(pkg);
|
||||
await BundleCommand.dev(pkg, bundler);
|
||||
} else {
|
||||
await BundleCommand.build(pkg);
|
||||
await BundleCommand.build(pkg, bundler);
|
||||
}
|
||||
}
|
||||
|
||||
static async build(pkg: Package) {
|
||||
static async build(pkg: Package, bundler: Bundler = getBundler()) {
|
||||
if (bundler === 'rspack' && !isRspackSupportedPackageName(pkg.name)) {
|
||||
return BundleCommand.buildWithWebpack(pkg);
|
||||
}
|
||||
|
||||
switch (bundler) {
|
||||
case 'webpack':
|
||||
return BundleCommand.buildWithWebpack(pkg);
|
||||
case 'rspack':
|
||||
return BundleCommand.buildWithRspack(pkg);
|
||||
}
|
||||
}
|
||||
|
||||
static async buildWithWebpack(pkg: Package) {
|
||||
process.env.NODE_ENV = 'production';
|
||||
const logger = new Logger('bundle');
|
||||
logger.info(`Packing package ${pkg.name}...`);
|
||||
logger.info(`Packing package ${pkg.name} with webpack...`);
|
||||
logger.info('Cleaning old output...');
|
||||
rmSync(pkg.distPath.value, { recursive: true, force: true });
|
||||
|
||||
const config = getBundleConfigs(pkg);
|
||||
const config = getWebpackBundleConfigs(pkg);
|
||||
config.parallelism = cpus().length;
|
||||
|
||||
const compiler = webpack(config);
|
||||
@@ -187,28 +272,73 @@ export class BundleCommand extends PackageCommand {
|
||||
throw new Error('Failed to create webpack compiler');
|
||||
}
|
||||
|
||||
compiler.run((error, stats) => {
|
||||
if (error) {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
}
|
||||
if (stats) {
|
||||
if (stats.hasErrors()) {
|
||||
console.error(stats.toString('errors-only'));
|
||||
process.exit(1);
|
||||
} else {
|
||||
console.log(stats.toString('minimal'));
|
||||
try {
|
||||
const stats = await new Promise<webpack.Stats | webpack.MultiStats>(
|
||||
(resolve, reject) => {
|
||||
compiler.run((error, stats) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
if (!stats) {
|
||||
reject(new Error('Failed to get webpack stats'));
|
||||
return;
|
||||
}
|
||||
resolve(stats);
|
||||
});
|
||||
}
|
||||
);
|
||||
if (stats.hasErrors()) {
|
||||
console.error(stats.toString('errors-only'));
|
||||
process.exit(1);
|
||||
return;
|
||||
}
|
||||
});
|
||||
console.log(stats.toString('minimal'));
|
||||
await uploadAssetsForPackage(pkg, logger);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
static async dev(pkg: Package, devServerConfig?: DevServerConfiguration) {
|
||||
static async dev(
|
||||
pkg: Package,
|
||||
bundler: Bundler = getBundler(),
|
||||
devServerConfig?:
|
||||
| WebpackDevServerConfiguration
|
||||
| RspackDevServerConfiguration
|
||||
) {
|
||||
if (bundler === 'rspack' && !isRspackSupportedPackageName(pkg.name)) {
|
||||
return BundleCommand.devWithWebpack(
|
||||
pkg,
|
||||
devServerConfig as WebpackDevServerConfiguration | undefined
|
||||
);
|
||||
}
|
||||
|
||||
switch (bundler) {
|
||||
case 'webpack':
|
||||
return BundleCommand.devWithWebpack(
|
||||
pkg,
|
||||
devServerConfig as WebpackDevServerConfiguration | undefined
|
||||
);
|
||||
case 'rspack':
|
||||
return BundleCommand.devWithRspack(
|
||||
pkg,
|
||||
devServerConfig as RspackDevServerConfiguration | undefined
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
static async devWithWebpack(
|
||||
pkg: Package,
|
||||
devServerConfig?: WebpackDevServerConfiguration
|
||||
) {
|
||||
process.env.NODE_ENV = 'development';
|
||||
const logger = new Logger('bundle');
|
||||
logger.info(`Starting dev server for ${pkg.name}...`);
|
||||
logger.info(`Starting webpack dev server for ${pkg.name}...`);
|
||||
|
||||
const config = getBundleConfigs(pkg);
|
||||
const config = getWebpackBundleConfigs(pkg);
|
||||
config.parallelism = cpus().length;
|
||||
|
||||
const compiler = webpack(config);
|
||||
@@ -217,7 +347,78 @@ export class BundleCommand extends PackageCommand {
|
||||
}
|
||||
|
||||
const devServer = new WebpackDevServer(
|
||||
merge({}, defaultDevServerConfig, devServerConfig),
|
||||
merge({}, DEFAULT_DEV_SERVER_CONFIG, devServerConfig),
|
||||
compiler
|
||||
);
|
||||
|
||||
await devServer.start();
|
||||
}
|
||||
|
||||
static async buildWithRspack(pkg: Package) {
|
||||
process.env.NODE_ENV = 'production';
|
||||
assertRspackSupportedPackage(pkg);
|
||||
|
||||
const logger = new Logger('bundle');
|
||||
logger.info(`Packing package ${pkg.name} with rspack...`);
|
||||
logger.info('Cleaning old output...');
|
||||
rmSync(pkg.distPath.value, { recursive: true, force: true });
|
||||
|
||||
const config = getRspackBundleConfigs(pkg);
|
||||
config.parallelism = cpus().length;
|
||||
|
||||
const compiler = rspack(config);
|
||||
if (!compiler) {
|
||||
throw new Error('Failed to create rspack compiler');
|
||||
}
|
||||
|
||||
try {
|
||||
const stats = await new Promise<any>((resolve, reject) => {
|
||||
compiler.run((error, stats) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
if (!stats) {
|
||||
reject(new Error('Failed to get rspack stats'));
|
||||
return;
|
||||
}
|
||||
resolve(stats);
|
||||
});
|
||||
});
|
||||
if (stats.hasErrors()) {
|
||||
console.error(stats.toString('errors-only'));
|
||||
process.exit(1);
|
||||
return;
|
||||
}
|
||||
console.log(stats.toString('minimal'));
|
||||
await uploadAssetsForPackage(pkg, logger);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
static async devWithRspack(
|
||||
pkg: Package,
|
||||
devServerConfig?: RspackDevServerConfiguration
|
||||
) {
|
||||
process.env.NODE_ENV = 'development';
|
||||
assertRspackSupportedPackage(pkg);
|
||||
|
||||
const logger = new Logger('bundle');
|
||||
logger.info(`Starting rspack dev server for ${pkg.name}...`);
|
||||
|
||||
const config = getRspackBundleConfigs(pkg);
|
||||
config.parallelism = cpus().length;
|
||||
|
||||
const compiler = rspack(config);
|
||||
if (!compiler) {
|
||||
throw new Error('Failed to create rspack compiler');
|
||||
}
|
||||
|
||||
const devServer = new RspackDevServer(
|
||||
merge({}, DEFAULT_DEV_SERVER_CONFIG, devServerConfig),
|
||||
compiler
|
||||
);
|
||||
|
||||
|
||||
27
tools/cli/src/bundler.ts
Normal file
27
tools/cli/src/bundler.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
export const SUPPORTED_BUNDLERS = ['webpack', 'rspack'] as const;
|
||||
|
||||
export type Bundler = (typeof SUPPORTED_BUNDLERS)[number];
|
||||
|
||||
export const DEFAULT_BUNDLER: Bundler = 'rspack';
|
||||
|
||||
function isBundler(value: string): value is Bundler {
|
||||
return SUPPORTED_BUNDLERS.includes(value as Bundler);
|
||||
}
|
||||
|
||||
export function normalizeBundler(input: string | undefined | null): Bundler {
|
||||
const value = input?.trim().toLowerCase();
|
||||
if (!value) {
|
||||
return DEFAULT_BUNDLER;
|
||||
}
|
||||
if (isBundler(value)) {
|
||||
return value;
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`Unsupported AFFINE_BUNDLER: "${input}". Expected one of: ${SUPPORTED_BUNDLERS.join(', ')}.`
|
||||
);
|
||||
}
|
||||
|
||||
export function getBundler(env: NodeJS.ProcessEnv = process.env): Bundler {
|
||||
return normalizeBundler(env.AFFINE_BUNDLER);
|
||||
}
|
||||
633
tools/cli/src/rspack/index.ts
Normal file
633
tools/cli/src/rspack/index.ts
Normal file
@@ -0,0 +1,633 @@
|
||||
import { createRequire } from 'node:module';
|
||||
import path from 'node:path';
|
||||
|
||||
import { getBuildConfig } from '@affine-tools/utils/build-config';
|
||||
import { Path, ProjectRoot } from '@affine-tools/utils/path';
|
||||
import { Package } from '@affine-tools/utils/workspace';
|
||||
import rspack, {
|
||||
type Configuration as RspackConfiguration,
|
||||
} from '@rspack/core';
|
||||
import { sentryWebpackPlugin } from '@sentry/webpack-plugin';
|
||||
import { VanillaExtractPlugin } from '@vanilla-extract/webpack-plugin';
|
||||
import cssnano from 'cssnano';
|
||||
import { compact, merge } from 'lodash-es';
|
||||
|
||||
import { productionCacheGroups } from '../webpack/cache-group.js';
|
||||
import {
|
||||
type CreateHTMLPluginConfig,
|
||||
createHTMLPlugins as createWebpackCompatibleHTMLPlugins,
|
||||
} from '../webpack/html-plugin.js';
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
|
||||
const IN_CI = !!process.env.CI;
|
||||
|
||||
const availableChannels = ['canary', 'beta', 'stable', 'internal'];
|
||||
function getBuildConfigFromEnv(pkg: Package) {
|
||||
const channel = process.env.BUILD_TYPE ?? 'canary';
|
||||
const dev = process.env.NODE_ENV === 'development';
|
||||
if (!availableChannels.includes(channel)) {
|
||||
throw new Error(
|
||||
`BUILD_TYPE must be one of ${availableChannels.join(', ')}, received [${channel}]`
|
||||
);
|
||||
}
|
||||
|
||||
return getBuildConfig(pkg, {
|
||||
// @ts-expect-error checked
|
||||
channel,
|
||||
mode: dev ? 'development' : 'production',
|
||||
});
|
||||
}
|
||||
|
||||
export function createHTMLTargetConfig(
|
||||
pkg: Package,
|
||||
entry: string | Record<string, string>,
|
||||
htmlConfig: Partial<CreateHTMLPluginConfig> = {},
|
||||
deps?: string[]
|
||||
): RspackConfiguration {
|
||||
entry = typeof entry === 'string' ? { index: entry } : entry;
|
||||
|
||||
htmlConfig = merge(
|
||||
{},
|
||||
{
|
||||
filename: 'index.html',
|
||||
additionalEntryForSelfhost: true,
|
||||
injectGlobalErrorHandler: true,
|
||||
emitAssetsManifest: true,
|
||||
},
|
||||
htmlConfig
|
||||
);
|
||||
|
||||
const buildConfig = getBuildConfigFromEnv(pkg);
|
||||
|
||||
console.log(
|
||||
`Building [${pkg.name}] for [${buildConfig.appBuildType}] channel in [${buildConfig.debug ? 'development' : 'production'}] mode.`
|
||||
);
|
||||
console.log(
|
||||
`Entry points: ${Object.entries(entry)
|
||||
.map(([name, path]) => `${name}: ${path}`)
|
||||
.join(', ')}`
|
||||
);
|
||||
console.log(`Output path: ${pkg.distPath.value}`);
|
||||
console.log(`Config: ${JSON.stringify(buildConfig, null, 2)}`);
|
||||
|
||||
const config: RspackConfiguration = {
|
||||
//#region basic webpack config
|
||||
name: entry['index'],
|
||||
dependencies: deps,
|
||||
context: ProjectRoot.value,
|
||||
experiments: {
|
||||
topLevelAwait: true,
|
||||
outputModule: false,
|
||||
asyncWebAssembly: true,
|
||||
},
|
||||
entry,
|
||||
output: {
|
||||
environment: { module: true, dynamicImport: true },
|
||||
filename: buildConfig.debug
|
||||
? 'js/[name].js'
|
||||
: 'js/[name].[contenthash:8].js',
|
||||
assetModuleFilename: buildConfig.debug
|
||||
? '[name].[contenthash:8][ext]'
|
||||
: 'assets/[name].[contenthash:8][ext][query]',
|
||||
path: pkg.distPath.value,
|
||||
clean: false,
|
||||
globalObject: 'globalThis',
|
||||
// NOTE: always keep it '/'
|
||||
publicPath: '/',
|
||||
},
|
||||
target: ['web', 'es2022'],
|
||||
mode: buildConfig.debug ? 'development' : 'production',
|
||||
devtool: buildConfig.debug ? 'cheap-module-source-map' : 'source-map',
|
||||
resolve: {
|
||||
symlinks: true,
|
||||
extensionAlias: {
|
||||
'.js': ['.js', '.tsx', '.ts'],
|
||||
'.mjs': ['.mjs', '.mts'],
|
||||
},
|
||||
extensions: ['.js', '.ts', '.tsx'],
|
||||
alias: {
|
||||
yjs: ProjectRoot.join('node_modules', 'yjs').value,
|
||||
lit: ProjectRoot.join('node_modules', 'lit').value,
|
||||
'@preact/signals-core': ProjectRoot.join(
|
||||
'node_modules',
|
||||
'@preact',
|
||||
'signals-core'
|
||||
).value,
|
||||
},
|
||||
},
|
||||
//#endregion
|
||||
|
||||
//#region module config
|
||||
module: {
|
||||
parser: {
|
||||
javascript: {
|
||||
// Do not mock Node.js globals
|
||||
node: false,
|
||||
requireJs: false,
|
||||
import: true,
|
||||
// Treat as missing export as error
|
||||
strictExportPresence: true,
|
||||
},
|
||||
},
|
||||
//#region rules
|
||||
rules: [
|
||||
{ test: /\.m?js?$/, resolve: { fullySpecified: false } },
|
||||
{
|
||||
test: /\.js$/,
|
||||
enforce: 'pre',
|
||||
include: /@blocksuite/,
|
||||
use: ['source-map-loader'],
|
||||
},
|
||||
{
|
||||
oneOf: [
|
||||
{
|
||||
test: /\.ts$/,
|
||||
exclude: /node_modules/,
|
||||
loader: 'swc-loader',
|
||||
options: {
|
||||
// https://swc.rs/docs/configuring-swc/
|
||||
jsc: {
|
||||
preserveAllComments: true,
|
||||
parser: {
|
||||
syntax: 'typescript',
|
||||
dynamicImport: true,
|
||||
topLevelAwait: false,
|
||||
tsx: false,
|
||||
decorators: true,
|
||||
},
|
||||
target: 'es2022',
|
||||
externalHelpers: false,
|
||||
transform: {
|
||||
useDefineForClassFields: false,
|
||||
decoratorVersion: '2022-03',
|
||||
},
|
||||
},
|
||||
sourceMaps: true,
|
||||
inlineSourcesContent: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
test: /\.tsx$/,
|
||||
exclude: /node_modules/,
|
||||
loader: 'swc-loader',
|
||||
options: {
|
||||
// https://swc.rs/docs/configuring-swc/
|
||||
jsc: {
|
||||
preserveAllComments: true,
|
||||
parser: {
|
||||
syntax: 'typescript',
|
||||
dynamicImport: true,
|
||||
topLevelAwait: false,
|
||||
tsx: true,
|
||||
decorators: true,
|
||||
},
|
||||
target: 'es2022',
|
||||
externalHelpers: false,
|
||||
transform: {
|
||||
react: { runtime: 'automatic' },
|
||||
useDefineForClassFields: false,
|
||||
decoratorVersion: '2022-03',
|
||||
},
|
||||
},
|
||||
sourceMaps: true,
|
||||
inlineSourcesContent: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
test: /\.(png|jpg|gif|svg|webp|mp4|zip)$/,
|
||||
type: 'asset/resource',
|
||||
},
|
||||
{ test: /\.(ttf|eot|woff|woff2)$/, type: 'asset/resource' },
|
||||
{ test: /\.txt$/, type: 'asset/source' },
|
||||
{ test: /\.inline\.svg$/, type: 'asset/inline' },
|
||||
{
|
||||
test: /\.css$/,
|
||||
use: [
|
||||
buildConfig.debug
|
||||
? 'style-loader'
|
||||
: rspack.CssExtractRspackPlugin.loader,
|
||||
{
|
||||
loader: 'css-loader',
|
||||
options: {
|
||||
url: true,
|
||||
sourceMap: false,
|
||||
modules: false,
|
||||
import: true,
|
||||
importLoaders: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
loader: 'postcss-loader',
|
||||
options: {
|
||||
postcssOptions: {
|
||||
plugins: pkg.join('tailwind.config.js').exists()
|
||||
? [
|
||||
[
|
||||
'@tailwindcss/postcss',
|
||||
require(pkg.join('tailwind.config.js').value),
|
||||
],
|
||||
['autoprefixer'],
|
||||
]
|
||||
: [
|
||||
cssnano({
|
||||
preset: ['default', { convertValues: false }],
|
||||
}),
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
//#endregion
|
||||
},
|
||||
//#endregion
|
||||
|
||||
//#region plugins
|
||||
plugins: compact([
|
||||
!IN_CI && new rspack.ProgressPlugin(),
|
||||
...createWebpackCompatibleHTMLPlugins(buildConfig, htmlConfig),
|
||||
new rspack.DefinePlugin({
|
||||
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
|
||||
...Object.entries(buildConfig).reduce(
|
||||
(def, [k, v]) => {
|
||||
def[`BUILD_CONFIG.${k}`] = JSON.stringify(v);
|
||||
return def;
|
||||
},
|
||||
{} as Record<string, string>
|
||||
),
|
||||
}),
|
||||
!buildConfig.debug &&
|
||||
// todo: support multiple entry points
|
||||
new rspack.CssExtractRspackPlugin({
|
||||
filename: `[name].[contenthash:8].css`,
|
||||
ignoreOrder: true,
|
||||
}),
|
||||
new VanillaExtractPlugin(),
|
||||
!buildConfig.isAdmin &&
|
||||
new rspack.CopyRspackPlugin({
|
||||
patterns: [
|
||||
{
|
||||
// copy the shared public assets into dist
|
||||
from: new Package('@affine/core').join('public').value,
|
||||
},
|
||||
],
|
||||
}),
|
||||
process.env.SENTRY_AUTH_TOKEN &&
|
||||
process.env.SENTRY_ORG &&
|
||||
process.env.SENTRY_PROJECT &&
|
||||
sentryWebpackPlugin({
|
||||
org: process.env.SENTRY_ORG,
|
||||
project: process.env.SENTRY_PROJECT,
|
||||
authToken: process.env.SENTRY_AUTH_TOKEN,
|
||||
}),
|
||||
// sourcemap url like # sourceMappingURL=76-6370cd185962bc89.js.map wont load in electron
|
||||
// this is because the default file:// protocol will be ignored by Chromium
|
||||
// so we need to replace the sourceMappingURL to assets:// protocol
|
||||
// for example:
|
||||
// replace # sourceMappingURL=76-6370cd185962bc89.js.map
|
||||
// to # sourceMappingURL=assets://./{dir}/76-6370cd185962bc89.js.map
|
||||
buildConfig.isElectron &&
|
||||
new rspack.SourceMapDevToolPlugin({
|
||||
append: (pathData: { filename?: string }) => {
|
||||
return `\n//# sourceMappingURL=assets://./${pathData.filename ?? ''}.map`;
|
||||
},
|
||||
filename: '[file].map',
|
||||
}),
|
||||
]),
|
||||
//#endregion
|
||||
|
||||
stats: { errorDetails: true },
|
||||
|
||||
//#region optimization
|
||||
optimization: {
|
||||
minimize: !buildConfig.debug,
|
||||
minimizer: [
|
||||
new rspack.SwcJsMinimizerRspackPlugin({
|
||||
extractComments: true,
|
||||
minimizerOptions: {
|
||||
ecma: 2020,
|
||||
compress: { unused: true },
|
||||
mangle: { keep_classnames: true },
|
||||
},
|
||||
}),
|
||||
],
|
||||
removeEmptyChunks: true,
|
||||
providedExports: true,
|
||||
usedExports: true,
|
||||
sideEffects: true,
|
||||
removeAvailableModules: true,
|
||||
runtimeChunk: { name: 'runtime' },
|
||||
splitChunks: {
|
||||
chunks: 'all',
|
||||
minSize: 1,
|
||||
minChunks: 1,
|
||||
maxInitialRequests: Number.MAX_SAFE_INTEGER,
|
||||
maxAsyncRequests: Number.MAX_SAFE_INTEGER,
|
||||
cacheGroups: {
|
||||
...productionCacheGroups,
|
||||
// Rspack tends to pull async node_modules into the initial vendor chunk
|
||||
// when `vendor` is configured as `chunks: 'all'`.
|
||||
vendor: {
|
||||
...productionCacheGroups.vendor,
|
||||
chunks: 'initial',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
//#endregion
|
||||
};
|
||||
|
||||
if (buildConfig.debug && !IN_CI) {
|
||||
config.optimization = {
|
||||
...config.optimization,
|
||||
minimize: false,
|
||||
runtimeChunk: false,
|
||||
splitChunks: {
|
||||
maxInitialRequests: Infinity,
|
||||
chunks: 'all',
|
||||
cacheGroups: {
|
||||
defaultVendors: {
|
||||
test: `[\\/]node_modules[\\/](?!.*vanilla-extract)`,
|
||||
priority: -10,
|
||||
reuseExistingChunk: true,
|
||||
},
|
||||
default: { minChunks: 2, priority: -20, reuseExistingChunk: true },
|
||||
styles: {
|
||||
name: 'styles',
|
||||
type: 'css/mini-extract',
|
||||
chunks: 'all',
|
||||
enforce: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
export function createWorkerTargetConfig(
|
||||
pkg: Package,
|
||||
entry: string
|
||||
): Omit<RspackConfiguration, 'name'> & { name: string } {
|
||||
const workerName = path.basename(entry).replace(/\.worker\.ts$/, '');
|
||||
const buildConfig = getBuildConfigFromEnv(pkg);
|
||||
|
||||
return {
|
||||
name: entry,
|
||||
context: ProjectRoot.value,
|
||||
experiments: {
|
||||
topLevelAwait: true,
|
||||
outputModule: false,
|
||||
asyncWebAssembly: true,
|
||||
},
|
||||
entry: { [workerName]: entry },
|
||||
output: {
|
||||
filename: `js/${workerName}-${buildConfig.appVersion}.worker.js`,
|
||||
path: pkg.distPath.value,
|
||||
clean: false,
|
||||
globalObject: 'globalThis',
|
||||
// NOTE: always keep it '/'
|
||||
publicPath: '/',
|
||||
},
|
||||
target: ['webworker', 'es2022'],
|
||||
mode: buildConfig.debug ? 'development' : 'production',
|
||||
devtool: buildConfig.debug ? 'cheap-module-source-map' : 'source-map',
|
||||
resolve: {
|
||||
symlinks: true,
|
||||
extensionAlias: { '.js': ['.js', '.ts'], '.mjs': ['.mjs', '.mts'] },
|
||||
extensions: ['.js', '.ts'],
|
||||
alias: { yjs: ProjectRoot.join('node_modules', 'yjs').value },
|
||||
},
|
||||
|
||||
module: {
|
||||
parser: {
|
||||
javascript: {
|
||||
// Do not mock Node.js globals
|
||||
node: false,
|
||||
requireJs: false,
|
||||
import: true,
|
||||
// Treat as missing export as error
|
||||
strictExportPresence: true,
|
||||
},
|
||||
},
|
||||
rules: [
|
||||
{ test: /\.m?js?$/, resolve: { fullySpecified: false } },
|
||||
{
|
||||
test: /\.js$/,
|
||||
enforce: 'pre',
|
||||
include: /@blocksuite/,
|
||||
use: ['source-map-loader'],
|
||||
},
|
||||
{
|
||||
oneOf: [
|
||||
{
|
||||
test: /\.ts$/,
|
||||
exclude: /node_modules/,
|
||||
loader: 'swc-loader',
|
||||
options: {
|
||||
// https://swc.rs/docs/configuring-swc/
|
||||
jsc: {
|
||||
preserveAllComments: true,
|
||||
parser: {
|
||||
syntax: 'typescript',
|
||||
dynamicImport: true,
|
||||
topLevelAwait: false,
|
||||
tsx: false,
|
||||
decorators: true,
|
||||
},
|
||||
target: 'es2022',
|
||||
externalHelpers: false,
|
||||
transform: {
|
||||
useDefineForClassFields: false,
|
||||
decoratorVersion: '2022-03',
|
||||
},
|
||||
},
|
||||
sourceMaps: true,
|
||||
inlineSourcesContent: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
plugins: compact([
|
||||
new rspack.DefinePlugin(
|
||||
Object.entries(buildConfig).reduce(
|
||||
(def, [k, v]) => {
|
||||
def[`BUILD_CONFIG.${k}`] = JSON.stringify(v);
|
||||
return def;
|
||||
},
|
||||
{} as Record<string, string>
|
||||
)
|
||||
),
|
||||
new rspack.optimize.LimitChunkCountPlugin({ maxChunks: 1 }),
|
||||
process.env.SENTRY_AUTH_TOKEN &&
|
||||
process.env.SENTRY_ORG &&
|
||||
process.env.SENTRY_PROJECT &&
|
||||
sentryWebpackPlugin({
|
||||
org: process.env.SENTRY_ORG,
|
||||
project: process.env.SENTRY_PROJECT,
|
||||
authToken: process.env.SENTRY_AUTH_TOKEN,
|
||||
}),
|
||||
]),
|
||||
stats: { errorDetails: true },
|
||||
optimization: {
|
||||
minimize: !buildConfig.debug,
|
||||
minimizer: [
|
||||
new rspack.SwcJsMinimizerRspackPlugin({
|
||||
extractComments: true,
|
||||
minimizerOptions: {
|
||||
ecma: 2020,
|
||||
compress: { unused: true },
|
||||
mangle: { keep_classnames: true },
|
||||
},
|
||||
}),
|
||||
],
|
||||
removeEmptyChunks: true,
|
||||
providedExports: true,
|
||||
usedExports: true,
|
||||
sideEffects: true,
|
||||
removeAvailableModules: true,
|
||||
runtimeChunk: false,
|
||||
splitChunks: false,
|
||||
},
|
||||
performance: { hints: false },
|
||||
};
|
||||
}
|
||||
|
||||
export function createNodeTargetConfig(
|
||||
pkg: Package,
|
||||
entry: string
|
||||
): Omit<RspackConfiguration, 'name'> & { name: string } {
|
||||
const dev = process.env.NODE_ENV === 'development';
|
||||
return {
|
||||
name: entry,
|
||||
context: ProjectRoot.value,
|
||||
experiments: {
|
||||
topLevelAwait: true,
|
||||
outputModule: pkg.packageJson.type === 'module',
|
||||
asyncWebAssembly: true,
|
||||
},
|
||||
entry: { index: entry },
|
||||
output: {
|
||||
filename: `main.js`,
|
||||
path: pkg.distPath.value,
|
||||
clean: true,
|
||||
globalObject: 'globalThis',
|
||||
},
|
||||
target: ['node', 'es2022'],
|
||||
externals: ((data: any, callback: (err: null, value: boolean) => void) => {
|
||||
if (
|
||||
data.request &&
|
||||
// import ... from 'module'
|
||||
/^[a-zA-Z@]/.test(data.request) &&
|
||||
// not workspace deps
|
||||
!pkg.deps.some(dep => data.request!.startsWith(dep.name))
|
||||
) {
|
||||
callback(null, true);
|
||||
} else {
|
||||
callback(null, false);
|
||||
}
|
||||
}) as any,
|
||||
externalsPresets: { node: true },
|
||||
node: { __dirname: false, __filename: false },
|
||||
mode: dev ? 'development' : 'production',
|
||||
devtool: 'source-map',
|
||||
resolve: {
|
||||
symlinks: true,
|
||||
extensionAlias: { '.js': ['.js', '.ts'], '.mjs': ['.mjs', '.mts'] },
|
||||
extensions: ['.js', '.ts', '.tsx', '.node'],
|
||||
alias: { yjs: ProjectRoot.join('node_modules', 'yjs').value },
|
||||
},
|
||||
module: {
|
||||
parser: {
|
||||
javascript: { url: false, importMeta: false, createRequire: false },
|
||||
},
|
||||
rules: [
|
||||
{
|
||||
test: /\.js$/,
|
||||
enforce: 'pre',
|
||||
include: /@blocksuite/,
|
||||
use: ['source-map-loader'],
|
||||
},
|
||||
{
|
||||
test: /\.node$/,
|
||||
loader: Path.dir(import.meta.url).join('../webpack/node-loader.js')
|
||||
.value,
|
||||
},
|
||||
{
|
||||
test: /\.tsx?$/,
|
||||
exclude: /node_modules/,
|
||||
loader: 'swc-loader',
|
||||
options: {
|
||||
// https://swc.rs/docs/configuring-swc/
|
||||
jsc: {
|
||||
preserveAllComments: true,
|
||||
parser: {
|
||||
syntax: 'typescript',
|
||||
dynamicImport: true,
|
||||
topLevelAwait: true,
|
||||
tsx: true,
|
||||
decorators: true,
|
||||
},
|
||||
target: 'es2022',
|
||||
externalHelpers: false,
|
||||
transform: {
|
||||
legacyDecorator: true,
|
||||
decoratorMetadata: true,
|
||||
react: { runtime: 'automatic' },
|
||||
},
|
||||
},
|
||||
sourceMaps: true,
|
||||
inlineSourcesContent: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
plugins: compact([
|
||||
new rspack.optimize.LimitChunkCountPlugin({ maxChunks: 1 }),
|
||||
new rspack.IgnorePlugin({
|
||||
checkResource(resource) {
|
||||
const lazyImports = [
|
||||
'@nestjs/microservices',
|
||||
'@nestjs/websockets/socket-module',
|
||||
'@apollo/subgraph',
|
||||
'@apollo/gateway',
|
||||
'@as-integrations/fastify',
|
||||
'ts-morph',
|
||||
'class-validator',
|
||||
'class-transformer',
|
||||
];
|
||||
return lazyImports.some(lazyImport =>
|
||||
resource.startsWith(lazyImport)
|
||||
);
|
||||
},
|
||||
}),
|
||||
new rspack.DefinePlugin({
|
||||
'process.env.NODE_ENV': '"production"',
|
||||
}),
|
||||
]),
|
||||
stats: { errorDetails: true },
|
||||
optimization: {
|
||||
nodeEnv: false,
|
||||
minimize: !dev,
|
||||
minimizer: [
|
||||
new rspack.SwcJsMinimizerRspackPlugin({
|
||||
extractComments: true,
|
||||
minimizerOptions: {
|
||||
ecma: 2020,
|
||||
compress: { unused: true },
|
||||
mangle: { keep_classnames: true },
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
performance: { hints: false },
|
||||
ignoreWarnings: [/^(?!CriticalDependenciesWarning$)/],
|
||||
};
|
||||
}
|
||||
@@ -21,11 +21,18 @@ export const productionCacheGroups = {
|
||||
asyncVendor: {
|
||||
test: /[\\/]node_modules[\\/]/,
|
||||
name(module: any) {
|
||||
const modulePath =
|
||||
module?.nameForCondition?.() || module?.context || module?.resource;
|
||||
|
||||
if (!modulePath || typeof modulePath !== 'string') {
|
||||
return 'app-async';
|
||||
}
|
||||
|
||||
// monorepo linked in node_modules, so it's not a npm package
|
||||
if (!module.context.includes('node_modules')) {
|
||||
if (!modulePath.includes('node_modules')) {
|
||||
return `app-async`;
|
||||
}
|
||||
const name = module.context.match(
|
||||
const name = modulePath.match(
|
||||
/[\\/]node_modules[\\/](.*?)([\\/]|$)/
|
||||
)?.[1];
|
||||
return `npm-async-${name}`;
|
||||
|
||||
@@ -5,8 +5,31 @@ import { Path, ProjectRoot } from '@affine-tools/utils/path';
|
||||
import { Repository } from '@napi-rs/simple-git';
|
||||
import HTMLPlugin from 'html-webpack-plugin';
|
||||
import { once } from 'lodash-es';
|
||||
import type { Compiler, WebpackPluginInstance } from 'webpack';
|
||||
import webpack from 'webpack';
|
||||
import type { WebpackPluginInstance } from 'webpack';
|
||||
|
||||
type CompilerLike = {
|
||||
webpack?: {
|
||||
sources?: {
|
||||
RawSource?: new (source: string) => unknown;
|
||||
};
|
||||
};
|
||||
hooks: {
|
||||
compilation: {
|
||||
tap: (name: string, callback: (compilation: any) => void) => void;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
function createRawSource(compiler: CompilerLike, source: string) {
|
||||
const RawSource = compiler.webpack?.sources?.RawSource;
|
||||
if (!RawSource) {
|
||||
throw new Error(
|
||||
'compiler.webpack.sources.RawSource is required for html plugin assets emission'
|
||||
);
|
||||
}
|
||||
|
||||
return new RawSource(source);
|
||||
}
|
||||
|
||||
export const getPublicPath = (BUILD_CONFIG: BUILD_CONFIG_TYPE) => {
|
||||
const { BUILD_TYPE } = process.env;
|
||||
@@ -86,7 +109,7 @@ function getHTMLPluginOptions(BUILD_CONFIG: BUILD_CONFIG_TYPE) {
|
||||
}
|
||||
|
||||
const AssetsManifestPlugin = {
|
||||
apply(compiler: Compiler) {
|
||||
apply(compiler: CompilerLike) {
|
||||
compiler.hooks.compilation.tap('assets-manifest-plugin', compilation => {
|
||||
HTMLPlugin.getHooks(compilation).beforeAssetTagGeneration.tap(
|
||||
'assets-manifest-plugin',
|
||||
@@ -94,7 +117,8 @@ const AssetsManifestPlugin = {
|
||||
if (!compilation.getAsset('assets-manifest.json')) {
|
||||
compilation.emitAsset(
|
||||
`assets-manifest.json`,
|
||||
new webpack.sources.RawSource(
|
||||
createRawSource(
|
||||
compiler,
|
||||
JSON.stringify(
|
||||
{
|
||||
...arg.assets,
|
||||
@@ -125,7 +149,7 @@ const AssetsManifestPlugin = {
|
||||
};
|
||||
|
||||
const GlobalErrorHandlerPlugin = {
|
||||
apply(compiler: Compiler) {
|
||||
apply(compiler: CompilerLike) {
|
||||
const globalErrorHandler = [
|
||||
'js/global-error-handler.js',
|
||||
readFileSync(currentDir.join('./error-handler.js').toString(), 'utf-8'),
|
||||
@@ -140,7 +164,7 @@ const GlobalErrorHandlerPlugin = {
|
||||
if (!compilation.getAsset(globalErrorHandler[0])) {
|
||||
compilation.emitAsset(
|
||||
globalErrorHandler[0],
|
||||
new webpack.sources.RawSource(globalErrorHandler[1])
|
||||
createRawSource(compiler, globalErrorHandler[1])
|
||||
);
|
||||
arg.assets.js.unshift(
|
||||
arg.assets.publicPath + globalErrorHandler[0]
|
||||
@@ -156,7 +180,7 @@ const GlobalErrorHandlerPlugin = {
|
||||
};
|
||||
|
||||
const CorsPlugin = {
|
||||
apply(compiler: Compiler) {
|
||||
apply(compiler: CompilerLike) {
|
||||
compiler.hooks.compilation.tap('html-js-cors-plugin', compilation => {
|
||||
HTMLPlugin.getHooks(compilation).alterAssetTags.tap(
|
||||
'html-js-cors-plugin',
|
||||
|
||||
@@ -18,7 +18,6 @@ import {
|
||||
type CreateHTMLPluginConfig,
|
||||
createHTMLPlugins,
|
||||
} from './html-plugin.js';
|
||||
import { WebpackS3Plugin } from './s3-plugin.js';
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
const cssnano = require('cssnano');
|
||||
@@ -279,10 +278,6 @@ export function createHTMLTargetConfig(
|
||||
},
|
||||
],
|
||||
}),
|
||||
!buildConfig.debug &&
|
||||
(buildConfig.isWeb || buildConfig.isMobileWeb || buildConfig.isAdmin) &&
|
||||
process.env.R2_SECRET_ACCESS_KEY &&
|
||||
new WebpackS3Plugin(),
|
||||
!buildConfig.debug &&
|
||||
process.env.PERFSEE_TOKEN &&
|
||||
new PerfseePlugin({ project: 'affine-toeverything' }),
|
||||
|
||||
@@ -1,43 +1,141 @@
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { join } from 'node:path';
|
||||
import { readdir, readFile } from 'node:fs/promises';
|
||||
import { join, relative, sep } from 'node:path';
|
||||
|
||||
import { createS3CompatClient } from '@affine/s3-compat';
|
||||
import { lookup } from 'mime-types';
|
||||
import type { Compiler, WebpackPluginInstance } from 'webpack';
|
||||
|
||||
export const R2_BUCKET =
|
||||
process.env.R2_BUCKET ??
|
||||
(process.env.BUILD_TYPE === 'canary' ? 'assets-dev' : 'assets-prod');
|
||||
|
||||
export class WebpackS3Plugin implements WebpackPluginInstance {
|
||||
private readonly s3 = createS3CompatClient(
|
||||
const S3_UPLOAD_PACKAGE_NAMES = new Set([
|
||||
'@affine/web',
|
||||
'@affine/mobile',
|
||||
'@affine/admin',
|
||||
]);
|
||||
const MAX_UPLOAD_RETRIES = 3;
|
||||
const UPLOAD_RETRY_BASE_DELAY_MS = 500;
|
||||
|
||||
function createR2Client() {
|
||||
const { R2_ACCOUNT_ID, R2_ACCESS_KEY_ID, R2_SECRET_ACCESS_KEY } = process.env;
|
||||
if (!R2_ACCOUNT_ID || !R2_ACCESS_KEY_ID || !R2_SECRET_ACCESS_KEY) {
|
||||
throw new Error('Missing R2 credentials for uploading release assets');
|
||||
}
|
||||
|
||||
return createS3CompatClient(
|
||||
{
|
||||
region: 'auto',
|
||||
bucket: R2_BUCKET,
|
||||
forcePathStyle: true,
|
||||
endpoint: `https://${process.env.R2_ACCOUNT_ID}.r2.cloudflarestorage.com`,
|
||||
endpoint: `https://${R2_ACCOUNT_ID}.r2.cloudflarestorage.com`,
|
||||
},
|
||||
{
|
||||
accessKeyId: process.env.R2_ACCESS_KEY_ID!,
|
||||
secretAccessKey: process.env.R2_SECRET_ACCESS_KEY!,
|
||||
accessKeyId: R2_ACCESS_KEY_ID,
|
||||
secretAccessKey: R2_SECRET_ACCESS_KEY,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async function collectFiles(dir: string): Promise<string[]> {
|
||||
const dirs = [dir];
|
||||
const files: string[] = [];
|
||||
|
||||
while (dirs.length > 0) {
|
||||
const current = dirs.pop()!;
|
||||
const entries = await readdir(current, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
const fullPath = join(current, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
dirs.push(fullPath);
|
||||
} else if (entry.isFile()) {
|
||||
files.push(fullPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
function toAssetKey(outputPath: string, filePath: string): string {
|
||||
return relative(outputPath, filePath).split(sep).join('/');
|
||||
}
|
||||
|
||||
function sleep(ms: number) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
async function putObjectWithRetry(
|
||||
s3: ReturnType<typeof createR2Client>,
|
||||
asset: string,
|
||||
assetSource: Buffer,
|
||||
contentType: string | false | undefined
|
||||
) {
|
||||
let retries = 0;
|
||||
while (true) {
|
||||
try {
|
||||
await s3.putObject(asset, assetSource, {
|
||||
contentType: contentType || undefined,
|
||||
contentLength: assetSource.byteLength,
|
||||
});
|
||||
return;
|
||||
} catch (error) {
|
||||
if (retries >= MAX_UPLOAD_RETRIES) {
|
||||
throw error;
|
||||
}
|
||||
retries += 1;
|
||||
const delay = UPLOAD_RETRY_BASE_DELAY_MS * 2 ** (retries - 1);
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : String(error);
|
||||
console.warn(
|
||||
`[s3-upload] Retry ${retries}/${MAX_UPLOAD_RETRIES} for ${asset}: ${errorMessage}`
|
||||
);
|
||||
await sleep(delay);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function runInParallel<T>(
|
||||
values: T[],
|
||||
worker: (value: T) => Promise<void>,
|
||||
concurrency = 16
|
||||
) {
|
||||
if (values.length === 0) {
|
||||
return;
|
||||
}
|
||||
let nextIndex = 0;
|
||||
const workers = Array.from(
|
||||
{ length: Math.min(concurrency, values.length) },
|
||||
async () => {
|
||||
while (true) {
|
||||
const index = nextIndex++;
|
||||
if (index >= values.length) {
|
||||
return;
|
||||
}
|
||||
await worker(values[index]!);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
apply(compiler: Compiler) {
|
||||
compiler.hooks.assetEmitted.tapPromise(
|
||||
'WebpackS3Plugin',
|
||||
async (asset, { outputPath }) => {
|
||||
if (asset.endsWith('.html')) {
|
||||
return;
|
||||
}
|
||||
const assetPath = join(outputPath, asset);
|
||||
const assetSource = await readFile(assetPath);
|
||||
const contentType = lookup(asset) || undefined;
|
||||
await this.s3.putObject(asset, assetSource, {
|
||||
contentType,
|
||||
contentLength: assetSource.byteLength,
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
await Promise.all(workers);
|
||||
}
|
||||
|
||||
export function shouldUploadReleaseAssets(pkgName: string): boolean {
|
||||
return S3_UPLOAD_PACKAGE_NAMES.has(pkgName);
|
||||
}
|
||||
|
||||
export async function uploadDistAssetsToS3(outputPath: string) {
|
||||
const allFiles = await collectFiles(outputPath);
|
||||
const uploadFiles = allFiles.filter(file => !file.endsWith('.html'));
|
||||
|
||||
if (uploadFiles.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const s3 = createR2Client();
|
||||
await runInParallel(uploadFiles, async filePath => {
|
||||
const asset = toAssetKey(outputPath, filePath);
|
||||
const assetSource = await readFile(filePath);
|
||||
const contentType = lookup(asset);
|
||||
await putObjectWithRetry(s3, asset, assetSource, contentType);
|
||||
});
|
||||
}
|
||||
|
||||
614
yarn.lock
614
yarn.lock
@@ -119,6 +119,8 @@ __metadata:
|
||||
"@affine/s3-compat": "workspace:*"
|
||||
"@napi-rs/simple-git": "npm:^0.1.22"
|
||||
"@perfsee/webpack": "npm:^1.13.0"
|
||||
"@rspack/core": "npm:^1.7.6"
|
||||
"@rspack/dev-server": "npm:^1.1.3"
|
||||
"@sentry/webpack-plugin": "npm:^4.0.0"
|
||||
"@swc/core": "npm:^1.10.1"
|
||||
"@tailwindcss/postcss": "npm:^4.0.0"
|
||||
@@ -4899,13 +4901,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@emnapi/core@npm:^1.4.0, @emnapi/core@npm:^1.7.1":
|
||||
version: 1.7.1
|
||||
resolution: "@emnapi/core@npm:1.7.1"
|
||||
"@emnapi/core@npm:^1.4.0, @emnapi/core@npm:^1.5.0, @emnapi/core@npm:^1.7.1":
|
||||
version: 1.8.1
|
||||
resolution: "@emnapi/core@npm:1.8.1"
|
||||
dependencies:
|
||||
"@emnapi/wasi-threads": "npm:1.1.0"
|
||||
tslib: "npm:^2.4.0"
|
||||
checksum: 10/260841f6dd2a7823a964d9de6da3a5e6f565dac8d21a5bd8f6215b87c45c22a4dc371b9ad877961579ee3cca8a76e55e3dd033ae29cba1998999cda6d794bdab
|
||||
checksum: 10/904ea60c91fc7d8aeb4a8f2c433b8cfb47c50618f2b6f37429fc5093c857c6381c60628a5cfbc3a7b0d75b0a288f21d4ed2d4533e82f92c043801ef255fd6a5c
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -8247,6 +8249,61 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@module-federation/error-codes@npm:0.22.0":
|
||||
version: 0.22.0
|
||||
resolution: "@module-federation/error-codes@npm:0.22.0"
|
||||
checksum: 10/4edb269e9f3039899f879788c84d2bfecff94ca8e87ffcd80dbf8589d8543ec32558b3fa05c8549a8abd3ac33e856ff2aacf458dea5c0d7bea608bf12bb13359
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@module-federation/runtime-core@npm:0.22.0":
|
||||
version: 0.22.0
|
||||
resolution: "@module-federation/runtime-core@npm:0.22.0"
|
||||
dependencies:
|
||||
"@module-federation/error-codes": "npm:0.22.0"
|
||||
"@module-federation/sdk": "npm:0.22.0"
|
||||
checksum: 10/d21969198322b6f79e0513b702d0af5097613d47819724c849b6c677c163cd10fb8c89e3ff62b798bec498ee4d8e95dec71861071bc4ed74bd86a7e43193bc05
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@module-federation/runtime-tools@npm:0.22.0":
|
||||
version: 0.22.0
|
||||
resolution: "@module-federation/runtime-tools@npm:0.22.0"
|
||||
dependencies:
|
||||
"@module-federation/runtime": "npm:0.22.0"
|
||||
"@module-federation/webpack-bundler-runtime": "npm:0.22.0"
|
||||
checksum: 10/0e7693c1ec02fc5bef770b478c8757cad9cfefb2310d1943151d0ad079b72472d9b2c8a087299e9124dfcd6b649c83290c7fdfa333865baab4ba193f39e7b6bd
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@module-federation/runtime@npm:0.22.0":
|
||||
version: 0.22.0
|
||||
resolution: "@module-federation/runtime@npm:0.22.0"
|
||||
dependencies:
|
||||
"@module-federation/error-codes": "npm:0.22.0"
|
||||
"@module-federation/runtime-core": "npm:0.22.0"
|
||||
"@module-federation/sdk": "npm:0.22.0"
|
||||
checksum: 10/eca608be999d7d2e83abc1169643c2f795a5ed950f9e2bdf7000400a30b3e1e0ca4bdaa5daa09f55e44868383d444707e40236cec1aaa7b40432b0cce800b7f3
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@module-federation/sdk@npm:0.22.0":
|
||||
version: 0.22.0
|
||||
resolution: "@module-federation/sdk@npm:0.22.0"
|
||||
checksum: 10/d7085d883730a33145052520787a7e59cf9c54b51b2946bebc7c63a6bb668bcc6cbdc27fa0b7354a62f5a7ee4e8829a66b84e644607498f2e37cfd5eb4ded0da
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@module-federation/webpack-bundler-runtime@npm:0.22.0":
|
||||
version: 0.22.0
|
||||
resolution: "@module-federation/webpack-bundler-runtime@npm:0.22.0"
|
||||
dependencies:
|
||||
"@module-federation/runtime": "npm:0.22.0"
|
||||
"@module-federation/sdk": "npm:0.22.0"
|
||||
checksum: 10/afd24406817dfc6474ebcf5be714ccf26690eb3f6f5172bda711c8f23dba149fe47293f7aa2d0733dfed0334c98d4d3d9e7c2da2be78750cae5a72d72f32ce93
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@monaco-editor/loader@npm:^1.5.0":
|
||||
version: 1.7.0
|
||||
resolution: "@monaco-editor/loader@npm:1.7.0"
|
||||
@@ -9131,6 +9188,17 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@napi-rs/wasm-runtime@npm:1.0.7":
|
||||
version: 1.0.7
|
||||
resolution: "@napi-rs/wasm-runtime@npm:1.0.7"
|
||||
dependencies:
|
||||
"@emnapi/core": "npm:^1.5.0"
|
||||
"@emnapi/runtime": "npm:^1.5.0"
|
||||
"@tybys/wasm-util": "npm:^0.10.1"
|
||||
checksum: 10/6bc32d32d486d07b83220a9b7b2b715e39acacbacef0011ebca05c00b41d80a0535123da10fea7a7d6d7e206712bb50dc50ac3cf88b770754d44378570fb5c05
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@napi-rs/wasm-runtime@npm:^0.2.5, @napi-rs/wasm-runtime@npm:^0.2.9":
|
||||
version: 0.2.9
|
||||
resolution: "@napi-rs/wasm-runtime@npm:0.2.9"
|
||||
@@ -15058,6 +15126,178 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@rspack/binding-darwin-arm64@npm:1.7.6":
|
||||
version: 1.7.6
|
||||
resolution: "@rspack/binding-darwin-arm64@npm:1.7.6"
|
||||
conditions: os=darwin & cpu=arm64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@rspack/binding-darwin-x64@npm:1.7.6":
|
||||
version: 1.7.6
|
||||
resolution: "@rspack/binding-darwin-x64@npm:1.7.6"
|
||||
conditions: os=darwin & cpu=x64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@rspack/binding-linux-arm64-gnu@npm:1.7.6":
|
||||
version: 1.7.6
|
||||
resolution: "@rspack/binding-linux-arm64-gnu@npm:1.7.6"
|
||||
conditions: os=linux & cpu=arm64 & libc=glibc
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@rspack/binding-linux-arm64-musl@npm:1.7.6":
|
||||
version: 1.7.6
|
||||
resolution: "@rspack/binding-linux-arm64-musl@npm:1.7.6"
|
||||
conditions: os=linux & cpu=arm64 & libc=musl
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@rspack/binding-linux-x64-gnu@npm:1.7.6":
|
||||
version: 1.7.6
|
||||
resolution: "@rspack/binding-linux-x64-gnu@npm:1.7.6"
|
||||
conditions: os=linux & cpu=x64 & libc=glibc
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@rspack/binding-linux-x64-musl@npm:1.7.6":
|
||||
version: 1.7.6
|
||||
resolution: "@rspack/binding-linux-x64-musl@npm:1.7.6"
|
||||
conditions: os=linux & cpu=x64 & libc=musl
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@rspack/binding-wasm32-wasi@npm:1.7.6":
|
||||
version: 1.7.6
|
||||
resolution: "@rspack/binding-wasm32-wasi@npm:1.7.6"
|
||||
dependencies:
|
||||
"@napi-rs/wasm-runtime": "npm:1.0.7"
|
||||
conditions: cpu=wasm32
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@rspack/binding-win32-arm64-msvc@npm:1.7.6":
|
||||
version: 1.7.6
|
||||
resolution: "@rspack/binding-win32-arm64-msvc@npm:1.7.6"
|
||||
conditions: os=win32 & cpu=arm64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@rspack/binding-win32-ia32-msvc@npm:1.7.6":
|
||||
version: 1.7.6
|
||||
resolution: "@rspack/binding-win32-ia32-msvc@npm:1.7.6"
|
||||
conditions: os=win32 & cpu=ia32
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@rspack/binding-win32-x64-msvc@npm:1.7.6":
|
||||
version: 1.7.6
|
||||
resolution: "@rspack/binding-win32-x64-msvc@npm:1.7.6"
|
||||
conditions: os=win32 & cpu=x64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@rspack/binding@npm:1.7.6":
|
||||
version: 1.7.6
|
||||
resolution: "@rspack/binding@npm:1.7.6"
|
||||
dependencies:
|
||||
"@rspack/binding-darwin-arm64": "npm:1.7.6"
|
||||
"@rspack/binding-darwin-x64": "npm:1.7.6"
|
||||
"@rspack/binding-linux-arm64-gnu": "npm:1.7.6"
|
||||
"@rspack/binding-linux-arm64-musl": "npm:1.7.6"
|
||||
"@rspack/binding-linux-x64-gnu": "npm:1.7.6"
|
||||
"@rspack/binding-linux-x64-musl": "npm:1.7.6"
|
||||
"@rspack/binding-wasm32-wasi": "npm:1.7.6"
|
||||
"@rspack/binding-win32-arm64-msvc": "npm:1.7.6"
|
||||
"@rspack/binding-win32-ia32-msvc": "npm:1.7.6"
|
||||
"@rspack/binding-win32-x64-msvc": "npm:1.7.6"
|
||||
dependenciesMeta:
|
||||
"@rspack/binding-darwin-arm64":
|
||||
optional: true
|
||||
"@rspack/binding-darwin-x64":
|
||||
optional: true
|
||||
"@rspack/binding-linux-arm64-gnu":
|
||||
optional: true
|
||||
"@rspack/binding-linux-arm64-musl":
|
||||
optional: true
|
||||
"@rspack/binding-linux-x64-gnu":
|
||||
optional: true
|
||||
"@rspack/binding-linux-x64-musl":
|
||||
optional: true
|
||||
"@rspack/binding-wasm32-wasi":
|
||||
optional: true
|
||||
"@rspack/binding-win32-arm64-msvc":
|
||||
optional: true
|
||||
"@rspack/binding-win32-ia32-msvc":
|
||||
optional: true
|
||||
"@rspack/binding-win32-x64-msvc":
|
||||
optional: true
|
||||
checksum: 10/fec6c978e51f20471e278a07018b414125cf3bccf9c6bd7032ca65603cfe5bf0fdd7f58c156c0640b5dfab05e82a1e1170ac6d1aacaf4f46b61564be77dbe41b
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@rspack/core@npm:^1.7.6":
|
||||
version: 1.7.6
|
||||
resolution: "@rspack/core@npm:1.7.6"
|
||||
dependencies:
|
||||
"@module-federation/runtime-tools": "npm:0.22.0"
|
||||
"@rspack/binding": "npm:1.7.6"
|
||||
"@rspack/lite-tapable": "npm:1.1.0"
|
||||
peerDependencies:
|
||||
"@swc/helpers": ">=0.5.1"
|
||||
peerDependenciesMeta:
|
||||
"@swc/helpers":
|
||||
optional: true
|
||||
checksum: 10/9f23c4849926d9ddff34f703ab2be41878bca9e877c130d16d20d911ba4b13f15dfe96d7e86225d7f5a1e48034ab92cccec89f3765f84ff518538f6bb07f1f06
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@rspack/dev-server@npm:^1.1.3":
|
||||
version: 1.2.1
|
||||
resolution: "@rspack/dev-server@npm:1.2.1"
|
||||
dependencies:
|
||||
"@types/bonjour": "npm:^3.5.13"
|
||||
"@types/connect-history-api-fallback": "npm:^1.5.4"
|
||||
"@types/express": "npm:^4.17.25"
|
||||
"@types/express-serve-static-core": "npm:^4.17.21"
|
||||
"@types/serve-index": "npm:^1.9.4"
|
||||
"@types/serve-static": "npm:^1.15.5"
|
||||
"@types/sockjs": "npm:^0.3.36"
|
||||
"@types/ws": "npm:^8.5.10"
|
||||
ansi-html-community: "npm:^0.0.8"
|
||||
bonjour-service: "npm:^1.2.1"
|
||||
chokidar: "npm:^3.6.0"
|
||||
colorette: "npm:^2.0.10"
|
||||
compression: "npm:^1.8.1"
|
||||
connect-history-api-fallback: "npm:^2.0.0"
|
||||
express: "npm:^4.22.1"
|
||||
graceful-fs: "npm:^4.2.6"
|
||||
http-proxy-middleware: "npm:^2.0.9"
|
||||
ipaddr.js: "npm:^2.1.0"
|
||||
launch-editor: "npm:^2.6.1"
|
||||
open: "npm:^10.0.3"
|
||||
p-retry: "npm:^6.2.0"
|
||||
schema-utils: "npm:^4.2.0"
|
||||
selfsigned: "npm:^2.4.1"
|
||||
serve-index: "npm:^1.9.1"
|
||||
sockjs: "npm:^0.3.24"
|
||||
spdy: "npm:^4.0.2"
|
||||
webpack-dev-middleware: "npm:^7.4.2"
|
||||
ws: "npm:^8.18.0"
|
||||
peerDependencies:
|
||||
"@rspack/core": "*"
|
||||
checksum: 10/154808faef8079dc1d6eae1712455864cc7bc1ec686f3020f7117ad3e5f2906940f27ec514eb40230276132371570ecdf6b47f7ab117ad209462bcba7c2b0692
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@rspack/lite-tapable@npm:1.1.0":
|
||||
version: 1.1.0
|
||||
resolution: "@rspack/lite-tapable@npm:1.1.0"
|
||||
checksum: 10/41ff73fe5e1b8dccaad746c9c1bd36dd67649e1ad35776f311b5ba94333a397704e11158579e25a6a7e677c51abe35e66987b1b000faef48d4e4ad2470fea150
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@scarf/scarf@npm:=1.4.0":
|
||||
version: 1.4.0
|
||||
resolution: "@scarf/scarf@npm:1.4.0"
|
||||
@@ -17079,15 +17319,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/express@npm:^4.17.13, @types/express@npm:^4.17.21":
|
||||
version: 4.17.22
|
||||
resolution: "@types/express@npm:4.17.22"
|
||||
"@types/express@npm:^4.17.13, @types/express@npm:^4.17.21, @types/express@npm:^4.17.25":
|
||||
version: 4.17.25
|
||||
resolution: "@types/express@npm:4.17.25"
|
||||
dependencies:
|
||||
"@types/body-parser": "npm:*"
|
||||
"@types/express-serve-static-core": "npm:^4.17.33"
|
||||
"@types/qs": "npm:*"
|
||||
"@types/serve-static": "npm:*"
|
||||
checksum: 10/9497634fc341ff4ac966ec0c529ded03bdacd2c3dae164f10a060ff250c66591b873aedce92d0239869cf3d05615ae9bcad584c7349fe68780242f6fef010c62
|
||||
"@types/serve-static": "npm:^1"
|
||||
checksum: 10/c309fdb79fb8569b5d8d8f11268d0160b271f8b38f0a82c20a0733e526baf033eb7a921cd51d54fe4333c616de9e31caf7d4f3ef73baaf212d61f23f460b0369
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -17610,13 +17850,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/send@npm:*":
|
||||
version: 0.17.4
|
||||
resolution: "@types/send@npm:0.17.4"
|
||||
"@types/send@npm:*, @types/send@npm:<1":
|
||||
version: 0.17.6
|
||||
resolution: "@types/send@npm:0.17.6"
|
||||
dependencies:
|
||||
"@types/mime": "npm:^1"
|
||||
"@types/node": "npm:*"
|
||||
checksum: 10/28320a2aa1eb704f7d96a65272a07c0bf3ae7ed5509c2c96ea5e33238980f71deeed51d3631927a77d5250e4091b3e66bce53b42d770873282c6a20bb8b0280d
|
||||
checksum: 10/4948ab32ab84a81a0073f8243dd48ee766bc80608d5391060360afd1249f83c08a7476f142669ac0b0b8831c89d909a88bcb392d1b39ee48b276a91b50f3d8d1
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -17629,14 +17869,14 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/serve-static@npm:*, @types/serve-static@npm:^1.15.5":
|
||||
version: 1.15.7
|
||||
resolution: "@types/serve-static@npm:1.15.7"
|
||||
"@types/serve-static@npm:*, @types/serve-static@npm:^1, @types/serve-static@npm:^1.15.5":
|
||||
version: 1.15.10
|
||||
resolution: "@types/serve-static@npm:1.15.10"
|
||||
dependencies:
|
||||
"@types/http-errors": "npm:*"
|
||||
"@types/node": "npm:*"
|
||||
"@types/send": "npm:*"
|
||||
checksum: 10/c5a7171d5647f9fbd096ed1a26105759f3153ccf683824d99fee4c7eb9cde2953509621c56a070dd9fb1159e799e86d300cbe4e42245ebc5b0c1767e8ca94a67
|
||||
"@types/send": "npm:<1"
|
||||
checksum: 10/d9be72487540b9598e7d77260d533f241eb2e5db5181bb885ef2d6bc4592dad1c9e8c0e27f465d59478b2faf90edd2d535e834f20fbd9dd3c0928d43dc486404
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -19555,26 +19795,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"body-parser@npm:1.20.3":
|
||||
version: 1.20.3
|
||||
resolution: "body-parser@npm:1.20.3"
|
||||
dependencies:
|
||||
bytes: "npm:3.1.2"
|
||||
content-type: "npm:~1.0.5"
|
||||
debug: "npm:2.6.9"
|
||||
depd: "npm:2.0.0"
|
||||
destroy: "npm:1.2.0"
|
||||
http-errors: "npm:2.0.0"
|
||||
iconv-lite: "npm:0.4.24"
|
||||
on-finished: "npm:2.4.1"
|
||||
qs: "npm:6.13.0"
|
||||
raw-body: "npm:2.5.2"
|
||||
type-is: "npm:~1.6.18"
|
||||
unpipe: "npm:1.0.0"
|
||||
checksum: 10/8723e3d7a672eb50854327453bed85ac48d045f4958e81e7d470c56bf111f835b97e5b73ae9f6393d0011cc9e252771f46fd281bbabc57d33d3986edf1e6aeca
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"body-parser@npm:^2.2.0, body-parser@npm:^2.2.1":
|
||||
version: 2.2.2
|
||||
resolution: "body-parser@npm:2.2.2"
|
||||
@@ -19592,6 +19812,26 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"body-parser@npm:~1.20.3":
|
||||
version: 1.20.4
|
||||
resolution: "body-parser@npm:1.20.4"
|
||||
dependencies:
|
||||
bytes: "npm:~3.1.2"
|
||||
content-type: "npm:~1.0.5"
|
||||
debug: "npm:2.6.9"
|
||||
depd: "npm:2.0.0"
|
||||
destroy: "npm:~1.2.0"
|
||||
http-errors: "npm:~2.0.1"
|
||||
iconv-lite: "npm:~0.4.24"
|
||||
on-finished: "npm:~2.4.1"
|
||||
qs: "npm:~6.14.0"
|
||||
raw-body: "npm:~2.5.3"
|
||||
type-is: "npm:~1.6.18"
|
||||
unpipe: "npm:~1.0.0"
|
||||
checksum: 10/ff67e28d3f426707be8697a75fdf8d564dc50c341b41f054264d8ab6e2924e519c7ce8acc9d0de05328fdc41e1d9f3f200aec9c1cfb1867d6b676a410d97c689
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"bonjour-service@npm:^1.2.1":
|
||||
version: 1.3.0
|
||||
resolution: "bonjour-service@npm:1.3.0"
|
||||
@@ -21034,18 +21274,18 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"compression@npm:^1.7.4":
|
||||
version: 1.8.0
|
||||
resolution: "compression@npm:1.8.0"
|
||||
"compression@npm:^1.7.4, compression@npm:^1.8.1":
|
||||
version: 1.8.1
|
||||
resolution: "compression@npm:1.8.1"
|
||||
dependencies:
|
||||
bytes: "npm:3.1.2"
|
||||
compressible: "npm:~2.0.18"
|
||||
debug: "npm:2.6.9"
|
||||
negotiator: "npm:~0.6.4"
|
||||
on-headers: "npm:~1.0.2"
|
||||
on-headers: "npm:~1.1.0"
|
||||
safe-buffer: "npm:5.2.1"
|
||||
vary: "npm:~1.1.2"
|
||||
checksum: 10/ca213b9bd03e56c7c3596399d846237b5f0b31ca4cdeaa76a9547cd3c1465fbcfcb0fe93a5d7ff64eff28383fc65b53f1ef8bb2720d11bb48ad8c0836c502506
|
||||
checksum: 10/e7552bfbd780f2003c6fe8decb44561f5cc6bc82f0c61e81122caff5ec656f37824084f52155b1e8ef31d7656cecbec9a2499b7a68e92e20780ffb39b479abb7
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -21140,15 +21380,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"content-disposition@npm:0.5.4":
|
||||
version: 0.5.4
|
||||
resolution: "content-disposition@npm:0.5.4"
|
||||
dependencies:
|
||||
safe-buffer: "npm:5.2.1"
|
||||
checksum: 10/b7f4ce176e324f19324be69b05bf6f6e411160ac94bc523b782248129eb1ef3be006f6cff431aaea5e337fe5d176ce8830b8c2a1b721626ead8933f0cbe78720
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"content-disposition@npm:^1.0.0":
|
||||
version: 1.0.0
|
||||
resolution: "content-disposition@npm:1.0.0"
|
||||
@@ -21158,6 +21389,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"content-disposition@npm:~0.5.4":
|
||||
version: 0.5.4
|
||||
resolution: "content-disposition@npm:0.5.4"
|
||||
dependencies:
|
||||
safe-buffer: "npm:5.2.1"
|
||||
checksum: 10/b7f4ce176e324f19324be69b05bf6f6e411160ac94bc523b782248129eb1ef3be006f6cff431aaea5e337fe5d176ce8830b8c2a1b721626ead8933f0cbe78720
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"content-type@npm:^1.0.5, content-type@npm:~1.0.4, content-type@npm:~1.0.5":
|
||||
version: 1.0.5
|
||||
resolution: "content-type@npm:1.0.5"
|
||||
@@ -21249,14 +21489,14 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"cookie@npm:0.7.1":
|
||||
version: 0.7.1
|
||||
resolution: "cookie@npm:0.7.1"
|
||||
checksum: 10/aec6a6aa0781761bf55d60447d6be08861d381136a0fe94aa084fddd4f0300faa2b064df490c6798adfa1ebaef9e0af9b08a189c823e0811b8b313b3d9a03380
|
||||
"cookie-signature@npm:~1.0.6":
|
||||
version: 1.0.7
|
||||
resolution: "cookie-signature@npm:1.0.7"
|
||||
checksum: 10/1a62808cd30d15fb43b70e19829b64d04b0802d8ef00275b57d152de4ae6a3208ca05c197b6668d104c4d9de389e53ccc2d3bc6bcaaffd9602461417d8c40710
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"cookie@npm:0.7.2, cookie@npm:^0.7.1, cookie@npm:~0.7.2":
|
||||
"cookie@npm:0.7.2, cookie@npm:^0.7.1, cookie@npm:~0.7.1, cookie@npm:~0.7.2":
|
||||
version: 0.7.2
|
||||
resolution: "cookie@npm:0.7.2"
|
||||
checksum: 10/24b286c556420d4ba4e9bc09120c9d3db7d28ace2bd0f8ccee82422ce42322f73c8312441271e5eefafbead725980e5996cc02766dbb89a90ac7f5636ede608f
|
||||
@@ -22470,7 +22710,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"destroy@npm:1.2.0":
|
||||
"destroy@npm:1.2.0, destroy@npm:~1.2.0":
|
||||
version: 1.2.0
|
||||
resolution: "destroy@npm:1.2.0"
|
||||
checksum: 10/0acb300b7478a08b92d810ab229d5afe0d2f4399272045ab22affa0d99dbaf12637659411530a6fcd597a9bdac718fc94373a61a95b4651bbc7b83684a565e38
|
||||
@@ -23133,13 +23373,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"encodeurl@npm:~1.0.2":
|
||||
version: 1.0.2
|
||||
resolution: "encodeurl@npm:1.0.2"
|
||||
checksum: 10/e50e3d508cdd9c4565ba72d2012e65038e5d71bdc9198cb125beb6237b5b1ade6c0d343998da9e170fb2eae52c1bed37d4d6d98a46ea423a0cddbed5ac3f780c
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"encoding@npm:^0.1.13":
|
||||
version: 0.1.13
|
||||
resolution: "encoding@npm:0.1.13"
|
||||
@@ -24075,42 +24308,42 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"express@npm:^4.21.1, express@npm:^4.21.2":
|
||||
version: 4.21.2
|
||||
resolution: "express@npm:4.21.2"
|
||||
"express@npm:^4.21.1, express@npm:^4.21.2, express@npm:^4.22.1":
|
||||
version: 4.22.1
|
||||
resolution: "express@npm:4.22.1"
|
||||
dependencies:
|
||||
accepts: "npm:~1.3.8"
|
||||
array-flatten: "npm:1.1.1"
|
||||
body-parser: "npm:1.20.3"
|
||||
content-disposition: "npm:0.5.4"
|
||||
body-parser: "npm:~1.20.3"
|
||||
content-disposition: "npm:~0.5.4"
|
||||
content-type: "npm:~1.0.4"
|
||||
cookie: "npm:0.7.1"
|
||||
cookie-signature: "npm:1.0.6"
|
||||
cookie: "npm:~0.7.1"
|
||||
cookie-signature: "npm:~1.0.6"
|
||||
debug: "npm:2.6.9"
|
||||
depd: "npm:2.0.0"
|
||||
encodeurl: "npm:~2.0.0"
|
||||
escape-html: "npm:~1.0.3"
|
||||
etag: "npm:~1.8.1"
|
||||
finalhandler: "npm:1.3.1"
|
||||
fresh: "npm:0.5.2"
|
||||
http-errors: "npm:2.0.0"
|
||||
finalhandler: "npm:~1.3.1"
|
||||
fresh: "npm:~0.5.2"
|
||||
http-errors: "npm:~2.0.0"
|
||||
merge-descriptors: "npm:1.0.3"
|
||||
methods: "npm:~1.1.2"
|
||||
on-finished: "npm:2.4.1"
|
||||
on-finished: "npm:~2.4.1"
|
||||
parseurl: "npm:~1.3.3"
|
||||
path-to-regexp: "npm:0.1.12"
|
||||
path-to-regexp: "npm:~0.1.12"
|
||||
proxy-addr: "npm:~2.0.7"
|
||||
qs: "npm:6.13.0"
|
||||
qs: "npm:~6.14.0"
|
||||
range-parser: "npm:~1.2.1"
|
||||
safe-buffer: "npm:5.2.1"
|
||||
send: "npm:0.19.0"
|
||||
serve-static: "npm:1.16.2"
|
||||
send: "npm:~0.19.0"
|
||||
serve-static: "npm:~1.16.2"
|
||||
setprototypeof: "npm:1.2.0"
|
||||
statuses: "npm:2.0.1"
|
||||
statuses: "npm:~2.0.1"
|
||||
type-is: "npm:~1.6.18"
|
||||
utils-merge: "npm:1.0.1"
|
||||
vary: "npm:~1.1.2"
|
||||
checksum: 10/34571c442fc8c9f2c4b442d2faa10ea1175cf8559237fc6a278f5ce6254a8ffdbeb9a15d99f77c1a9f2926ab183e3b7ba560e3261f1ad4149799e3412ab66bd1
|
||||
checksum: 10/f33c1bd0c7d36e2a1f18de9cdc176469d32f68e20258d2941b8d296ab9a4fd9011872c246391bf87714f009fac5114c832ec5ac65cbee39421f1258801eb8470
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -24483,21 +24716,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"finalhandler@npm:1.3.1":
|
||||
version: 1.3.1
|
||||
resolution: "finalhandler@npm:1.3.1"
|
||||
dependencies:
|
||||
debug: "npm:2.6.9"
|
||||
encodeurl: "npm:~2.0.0"
|
||||
escape-html: "npm:~1.0.3"
|
||||
on-finished: "npm:2.4.1"
|
||||
parseurl: "npm:~1.3.3"
|
||||
statuses: "npm:2.0.1"
|
||||
unpipe: "npm:~1.0.0"
|
||||
checksum: 10/4babe72969b7373b5842bc9f75c3a641a4d0f8eb53af6b89fa714d4460ce03fb92b28de751d12ba415e96e7e02870c436d67412120555e2b382640535697305b
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"finalhandler@npm:^2.1.0":
|
||||
version: 2.1.0
|
||||
resolution: "finalhandler@npm:2.1.0"
|
||||
@@ -24512,6 +24730,21 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"finalhandler@npm:~1.3.1":
|
||||
version: 1.3.2
|
||||
resolution: "finalhandler@npm:1.3.2"
|
||||
dependencies:
|
||||
debug: "npm:2.6.9"
|
||||
encodeurl: "npm:~2.0.0"
|
||||
escape-html: "npm:~1.0.3"
|
||||
on-finished: "npm:~2.4.1"
|
||||
parseurl: "npm:~1.3.3"
|
||||
statuses: "npm:~2.0.2"
|
||||
unpipe: "npm:~1.0.0"
|
||||
checksum: 10/6cb4f9f80eaeb5a0fac4fdbd27a65d39271f040a0034df16556d896bfd855fd42f09da886781b3102117ea8fceba97b903c1f8b08df1fb5740576d5e0f481eed
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"find-cache-dir@npm:^3.3.2":
|
||||
version: 3.3.2
|
||||
resolution: "find-cache-dir@npm:3.3.2"
|
||||
@@ -24725,13 +24958,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"fresh@npm:0.5.2":
|
||||
version: 0.5.2
|
||||
resolution: "fresh@npm:0.5.2"
|
||||
checksum: 10/64c88e489b5d08e2f29664eb3c79c705ff9a8eb15d3e597198ef76546d4ade295897a44abb0abd2700e7ef784b2e3cbf1161e4fbf16f59129193fd1030d16da1
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"fresh@npm:^2.0.0":
|
||||
version: 2.0.0
|
||||
resolution: "fresh@npm:2.0.0"
|
||||
@@ -24739,6 +24965,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"fresh@npm:~0.5.2":
|
||||
version: 0.5.2
|
||||
resolution: "fresh@npm:0.5.2"
|
||||
checksum: 10/64c88e489b5d08e2f29664eb3c79c705ff9a8eb15d3e597198ef76546d4ade295897a44abb0abd2700e7ef784b2e3cbf1161e4fbf16f59129193fd1030d16da1
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"fromentries@npm:^1.3.2":
|
||||
version: 1.3.2
|
||||
resolution: "fromentries@npm:1.3.2"
|
||||
@@ -25985,20 +26218,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"http-errors@npm:2.0.0":
|
||||
version: 2.0.0
|
||||
resolution: "http-errors@npm:2.0.0"
|
||||
dependencies:
|
||||
depd: "npm:2.0.0"
|
||||
inherits: "npm:2.0.4"
|
||||
setprototypeof: "npm:1.2.0"
|
||||
statuses: "npm:2.0.1"
|
||||
toidentifier: "npm:1.0.1"
|
||||
checksum: 10/0e7f76ee8ff8a33e58a3281a469815b893c41357378f408be8f6d4aa7d1efafb0da064625518e7078381b6a92325949b119dc38fcb30bdbc4e3a35f78c44c439
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"http-errors@npm:^2.0.0, http-errors@npm:~2.0.1":
|
||||
"http-errors@npm:^2.0.0, http-errors@npm:~2.0.0, http-errors@npm:~2.0.1":
|
||||
version: 2.0.1
|
||||
resolution: "http-errors@npm:2.0.1"
|
||||
dependencies:
|
||||
@@ -26051,7 +26271,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"http-proxy-middleware@npm:^2.0.7":
|
||||
"http-proxy-middleware@npm:^2.0.7, http-proxy-middleware@npm:^2.0.9":
|
||||
version: 2.0.9
|
||||
resolution: "http-proxy-middleware@npm:2.0.9"
|
||||
dependencies:
|
||||
@@ -26196,15 +26416,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"iconv-lite@npm:0.4.24, iconv-lite@npm:^0.4.24":
|
||||
version: 0.4.24
|
||||
resolution: "iconv-lite@npm:0.4.24"
|
||||
dependencies:
|
||||
safer-buffer: "npm:>= 2.1.2 < 3"
|
||||
checksum: 10/6d3a2dac6e5d1fb126d25645c25c3a1209f70cceecc68b8ef51ae0da3cdc078c151fade7524a30b12a3094926336831fca09c666ef55b37e2c69638b5d6bd2e3
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"iconv-lite@npm:0.6, iconv-lite@npm:^0.6.2, iconv-lite@npm:^0.6.3":
|
||||
version: 0.6.3
|
||||
resolution: "iconv-lite@npm:0.6.3"
|
||||
@@ -26214,6 +26425,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"iconv-lite@npm:^0.4.24, iconv-lite@npm:~0.4.24":
|
||||
version: 0.4.24
|
||||
resolution: "iconv-lite@npm:0.4.24"
|
||||
dependencies:
|
||||
safer-buffer: "npm:>= 2.1.2 < 3"
|
||||
checksum: 10/6d3a2dac6e5d1fb126d25645c25c3a1209f70cceecc68b8ef51ae0da3cdc078c151fade7524a30b12a3094926336831fca09c666ef55b37e2c69638b5d6bd2e3
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"iconv-lite@npm:^0.7.0, iconv-lite@npm:~0.7.0":
|
||||
version: 0.7.2
|
||||
resolution: "iconv-lite@npm:0.7.2"
|
||||
@@ -26401,7 +26621,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"inherits@npm:2, inherits@npm:2.0.4, inherits@npm:^2.0.1, inherits@npm:^2.0.3, inherits@npm:^2.0.4, inherits@npm:~2.0.3, inherits@npm:~2.0.4":
|
||||
"inherits@npm:2, inherits@npm:^2.0.1, inherits@npm:^2.0.3, inherits@npm:^2.0.4, inherits@npm:~2.0.3, inherits@npm:~2.0.4":
|
||||
version: 2.0.4
|
||||
resolution: "inherits@npm:2.0.4"
|
||||
checksum: 10/cd45e923bee15186c07fa4c89db0aace24824c482fb887b528304694b2aa6ff8a898da8657046a5dcf3e46cd6db6c61629551f9215f208d7c3f157cf9b290521
|
||||
@@ -30735,7 +30955,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"on-finished@npm:2.4.1, on-finished@npm:^2.4.1":
|
||||
"on-finished@npm:^2.4.1, on-finished@npm:~2.4.1":
|
||||
version: 2.4.1
|
||||
resolution: "on-finished@npm:2.4.1"
|
||||
dependencies:
|
||||
@@ -31406,13 +31626,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"path-to-regexp@npm:0.1.12":
|
||||
version: 0.1.12
|
||||
resolution: "path-to-regexp@npm:0.1.12"
|
||||
checksum: 10/2e30f6a0144679c1f95c98e166b96e6acd1e72be9417830fefc8de7ac1992147eb9a4c7acaa59119fb1b3c34eec393b2129ef27e24b2054a3906fc4fb0d1398e
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"path-to-regexp@npm:3.3.0":
|
||||
version: 3.3.0
|
||||
resolution: "path-to-regexp@npm:3.3.0"
|
||||
@@ -31441,6 +31654,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"path-to-regexp@npm:~0.1.12":
|
||||
version: 0.1.12
|
||||
resolution: "path-to-regexp@npm:0.1.12"
|
||||
checksum: 10/2e30f6a0144679c1f95c98e166b96e6acd1e72be9417830fefc8de7ac1992147eb9a4c7acaa59119fb1b3c34eec393b2129ef27e24b2054a3906fc4fb0d1398e
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"path-type@npm:^2.0.0":
|
||||
version: 2.0.0
|
||||
resolution: "path-type@npm:2.0.0"
|
||||
@@ -32457,21 +32677,12 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"qs@npm:6.13.0":
|
||||
version: 6.13.0
|
||||
resolution: "qs@npm:6.13.0"
|
||||
dependencies:
|
||||
side-channel: "npm:^1.0.6"
|
||||
checksum: 10/f548b376e685553d12e461409f0d6e5c59ec7c7d76f308e2a888fd9db3e0c5e89902bedd0754db3a9038eda5f27da2331a6f019c8517dc5e0a16b3c9a6e9cef8
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"qs@npm:^6.11.0, qs@npm:^6.11.2, qs@npm:^6.14.0, qs@npm:^6.14.1, qs@npm:^6.7.0":
|
||||
version: 6.14.1
|
||||
resolution: "qs@npm:6.14.1"
|
||||
"qs@npm:^6.11.0, qs@npm:^6.11.2, qs@npm:^6.14.0, qs@npm:^6.14.1, qs@npm:^6.7.0, qs@npm:~6.14.0":
|
||||
version: 6.14.2
|
||||
resolution: "qs@npm:6.14.2"
|
||||
dependencies:
|
||||
side-channel: "npm:^1.1.0"
|
||||
checksum: 10/34b5ab00a910df432d55180ef39c1d1375e550f098b5ec153b41787f1a6a6d7e5f9495593c3b112b77dbc6709d0ae18e55b82847a4c2bbbb0de1e8ccbb1794c5
|
||||
checksum: 10/682933a85bb4b7bd0d66e13c0a40d9e612b5e4bcc2cb9238f711a9368cd22d91654097a74fff93551e58146db282c56ac094957dfdc60ce64ea72c3c9d7779ac
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -32573,18 +32784,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"raw-body@npm:2.5.2":
|
||||
version: 2.5.2
|
||||
resolution: "raw-body@npm:2.5.2"
|
||||
dependencies:
|
||||
bytes: "npm:3.1.2"
|
||||
http-errors: "npm:2.0.0"
|
||||
iconv-lite: "npm:0.4.24"
|
||||
unpipe: "npm:1.0.0"
|
||||
checksum: 10/863b5171e140546a4d99f349b720abac4410338e23df5e409cfcc3752538c9caf947ce382c89129ba976f71894bd38b5806c774edac35ebf168d02aa1ac11a95
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"raw-body@npm:^3.0.0, raw-body@npm:^3.0.1":
|
||||
version: 3.0.2
|
||||
resolution: "raw-body@npm:3.0.2"
|
||||
@@ -32597,6 +32796,18 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"raw-body@npm:~2.5.3":
|
||||
version: 2.5.3
|
||||
resolution: "raw-body@npm:2.5.3"
|
||||
dependencies:
|
||||
bytes: "npm:~3.1.2"
|
||||
http-errors: "npm:~2.0.1"
|
||||
iconv-lite: "npm:~0.4.24"
|
||||
unpipe: "npm:~1.0.0"
|
||||
checksum: 10/f35759fe5a6548e7c529121ead1de4dd163f899749a5896c42e278479df2d9d7f98b5bb17312737c03617765e5a1433e586f717616e5cfbebc13b4738b820601
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"rc9@npm:^2.1.2":
|
||||
version: 2.1.2
|
||||
resolution: "rc9@npm:2.1.2"
|
||||
@@ -34159,27 +34370,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"send@npm:0.19.0":
|
||||
version: 0.19.0
|
||||
resolution: "send@npm:0.19.0"
|
||||
dependencies:
|
||||
debug: "npm:2.6.9"
|
||||
depd: "npm:2.0.0"
|
||||
destroy: "npm:1.2.0"
|
||||
encodeurl: "npm:~1.0.2"
|
||||
escape-html: "npm:~1.0.3"
|
||||
etag: "npm:~1.8.1"
|
||||
fresh: "npm:0.5.2"
|
||||
http-errors: "npm:2.0.0"
|
||||
mime: "npm:1.6.0"
|
||||
ms: "npm:2.1.3"
|
||||
on-finished: "npm:2.4.1"
|
||||
range-parser: "npm:~1.2.1"
|
||||
statuses: "npm:2.0.1"
|
||||
checksum: 10/1f6064dea0ae4cbe4878437aedc9270c33f2a6650a77b56a16b62d057527f2766d96ee282997dd53ec0339082f2aad935bc7d989b46b48c82fc610800dc3a1d0
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"send@npm:^1.1.0, send@npm:^1.2.0":
|
||||
version: 1.2.0
|
||||
resolution: "send@npm:1.2.0"
|
||||
@@ -34199,6 +34389,27 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"send@npm:~0.19.0, send@npm:~0.19.1":
|
||||
version: 0.19.2
|
||||
resolution: "send@npm:0.19.2"
|
||||
dependencies:
|
||||
debug: "npm:2.6.9"
|
||||
depd: "npm:2.0.0"
|
||||
destroy: "npm:1.2.0"
|
||||
encodeurl: "npm:~2.0.0"
|
||||
escape-html: "npm:~1.0.3"
|
||||
etag: "npm:~1.8.1"
|
||||
fresh: "npm:~0.5.2"
|
||||
http-errors: "npm:~2.0.1"
|
||||
mime: "npm:1.6.0"
|
||||
ms: "npm:2.1.3"
|
||||
on-finished: "npm:~2.4.1"
|
||||
range-parser: "npm:~1.2.1"
|
||||
statuses: "npm:~2.0.2"
|
||||
checksum: 10/e932a592f62c58560b608a402d52333a8ae98a5ada076feb5db1d03adaa77c3ca32a7befa1c4fd6dedc186e88f342725b0cb4b3d86835eaf834688b259bef18d
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"sentence-case@npm:^3.0.4":
|
||||
version: 3.0.4
|
||||
resolution: "sentence-case@npm:3.0.4"
|
||||
@@ -34258,18 +34469,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"serve-static@npm:1.16.2":
|
||||
version: 1.16.2
|
||||
resolution: "serve-static@npm:1.16.2"
|
||||
dependencies:
|
||||
encodeurl: "npm:~2.0.0"
|
||||
escape-html: "npm:~1.0.3"
|
||||
parseurl: "npm:~1.3.3"
|
||||
send: "npm:0.19.0"
|
||||
checksum: 10/7fa9d9c68090f6289976b34fc13c50ac8cd7f16ae6bce08d16459300f7fc61fbc2d7ebfa02884c073ec9d6ab9e7e704c89561882bbe338e99fcacb2912fde737
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"serve-static@npm:^2.2.0":
|
||||
version: 2.2.0
|
||||
resolution: "serve-static@npm:2.2.0"
|
||||
@@ -34282,6 +34481,18 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"serve-static@npm:~1.16.2":
|
||||
version: 1.16.3
|
||||
resolution: "serve-static@npm:1.16.3"
|
||||
dependencies:
|
||||
encodeurl: "npm:~2.0.0"
|
||||
escape-html: "npm:~1.0.3"
|
||||
parseurl: "npm:~1.3.3"
|
||||
send: "npm:~0.19.1"
|
||||
checksum: 10/149d6718dd9e53166784d0a65535e21a7c01249d9c51f57224b786a7306354c6807e7811a9f6c143b45c863b1524721fca2f52b5c81a8b5194e3dde034a03b9c
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"serve@npm:^14.2.4":
|
||||
version: 14.2.4
|
||||
resolution: "serve@npm:14.2.4"
|
||||
@@ -35143,13 +35354,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"statuses@npm:2.0.1":
|
||||
version: 2.0.1
|
||||
resolution: "statuses@npm:2.0.1"
|
||||
checksum: 10/18c7623fdb8f646fb213ca4051be4df7efb3484d4ab662937ca6fbef7ced9b9e12842709872eb3020cc3504b93bde88935c9f6417489627a7786f24f8031cbcb
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"statuses@npm:>= 1.4.0 < 2":
|
||||
version: 1.5.0
|
||||
resolution: "statuses@npm:1.5.0"
|
||||
@@ -35157,7 +35361,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"statuses@npm:^2.0.1, statuses@npm:^2.0.2, statuses@npm:~2.0.2":
|
||||
"statuses@npm:^2.0.1, statuses@npm:^2.0.2, statuses@npm:~2.0.1, statuses@npm:~2.0.2":
|
||||
version: 2.0.2
|
||||
resolution: "statuses@npm:2.0.2"
|
||||
checksum: 10/6927feb50c2a75b2a4caab2c565491f7a93ad3d8dbad7b1398d52359e9243a20e2ebe35e33726dee945125ef7a515e9097d8a1b910ba2bbd818265a2f6c39879
|
||||
@@ -36120,7 +36324,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"toidentifier@npm:1.0.1, toidentifier@npm:~1.0.1":
|
||||
"toidentifier@npm:~1.0.1":
|
||||
version: 1.0.1
|
||||
resolution: "toidentifier@npm:1.0.1"
|
||||
checksum: 10/952c29e2a85d7123239b5cfdd889a0dde47ab0497f0913d70588f19c53f7e0b5327c95f4651e413c74b785147f9637b17410ac8c846d5d4a20a5a33eb6dc3a45
|
||||
@@ -36787,7 +36991,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"unpipe@npm:1.0.0, unpipe@npm:~1.0.0":
|
||||
"unpipe@npm:~1.0.0":
|
||||
version: 1.0.0
|
||||
resolution: "unpipe@npm:1.0.0"
|
||||
checksum: 10/4fa18d8d8d977c55cb09715385c203197105e10a6d220087ec819f50cb68870f02942244f1017565484237f1f8c5d3cd413631b1ae104d3096f24fdfde1b4aa2
|
||||
|
||||
Reference in New Issue
Block a user