feat: improve test & bundler (#14434)

#### PR Dependency Tree


* **PR #14434** 👈

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Introduced rspack bundler as an alternative to webpack for optimized
builds.

* **Tests & Quality**
* Added comprehensive editor semantic tests covering markdown, hotkeys,
and slash-menu operations.
* Expanded CI cross-browser testing to Chromium, Firefox, and WebKit;
improved shape-rendering tests to account for zoom.

* **Bug Fixes**
  * Corrected CSS overlay styling for development servers.
  * Fixed TypeScript typings for build tooling.

* **Other**
  * Document duplication now produces consistent "(n)" suffixes.
  * French i18n completeness increased to 100%.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
DarkSky
2026-02-14 16:09:09 +08:00
committed by GitHub
parent 3bc28ba78c
commit 2b71b3f345
25 changed files with 1913 additions and 333 deletions

View File

@@ -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;
}

View 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}`;
}

View File

@@ -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'),
},
],

View File

@@ -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();

View File

@@ -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})`
);
}

View File

@@ -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 => {

View File

@@ -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)];
}

View File

@@ -1,4 +1,5 @@
/// <reference types="@webpack/env"" />
/// <reference types="@webpack/env" />
/// <reference types="@rspack/core/module" />
declare module '*.md' {
const text: string;

View File

@@ -9,7 +9,7 @@
"es-CL": 98,
"es": 96,
"fa": 96,
"fr": 98,
"fr": 100,
"hi": 1,
"it-IT": 98,
"it": 1,

View File

@@ -2311,4 +2311,3 @@
"error.RESPONSE_TOO_LARGE_ERROR": "Réponse trop volumineuse ({{receivedBytes}} octets), la limite est de {{limitBytes}} octets",
"error.SSRF_BLOCKED_ERROR": "URL invalide"
}