refactor: enhance manual chunking in Vite config for better code splitting

This commit is contained in:
shuaiplus
2026-03-15 18:15:28 +08:00
parent f0ace28bf2
commit 722d3db0e9
49 changed files with 10021 additions and 8531 deletions
@@ -0,0 +1,324 @@
import { useState } from 'preact/hooks';
import {
CreditCard,
FileKey2,
Globe,
KeyRound,
ShieldUser,
StickyNote,
} from 'lucide-preact';
import { t } from '@/lib/i18n';
import type { Cipher, CipherAttachment, CustomFieldType, VaultDraft, VaultDraftField } from '@/lib/types';
export type TypeFilter = 'login' | 'card' | 'identity' | 'note' | 'ssh';
export type VaultSortMode = 'edited' | 'created' | 'name';
export type SidebarFilter =
| { kind: 'all' }
| { kind: 'favorite' }
| { kind: 'trash' }
| { kind: 'type'; value: TypeFilter }
| { kind: 'folder'; folderId: string | null };
interface TypeOption {
type: number;
label: string;
}
export const CREATE_TYPE_OPTIONS: TypeOption[] = [
{ type: 1, label: t('txt_login') },
{ type: 3, label: t('txt_card') },
{ type: 4, label: t('txt_identity') },
{ type: 2, label: t('txt_note') },
{ type: 5, label: t('txt_ssh_key') },
];
export const VAULT_SORT_STORAGE_KEY = 'nodewarden.vault.sort.v1';
export const MOBILE_LAYOUT_QUERY = '(max-width: 900px)';
export const VAULT_LIST_ROW_HEIGHT = 66;
export const VAULT_LIST_OVERSCAN = 10;
export const VAULT_SORT_OPTIONS: Array<{ value: VaultSortMode; label: string }> = [
{ value: 'edited', label: t('txt_sort_last_edited') },
{ value: 'created', label: t('txt_sort_created') },
{ value: 'name', label: t('txt_sort_name') },
];
export const FIELD_TYPE_OPTIONS: Array<{ value: CustomFieldType; label: string }> = [
{ value: 0, label: t('txt_text') },
{ value: 1, label: t('txt_hidden') },
{ value: 2, label: t('txt_boolean') },
];
export const TOTP_PERIOD_SECONDS = 30;
export const TOTP_RING_RADIUS = 14;
export const TOTP_RING_CIRCUMFERENCE = 2 * Math.PI * TOTP_RING_RADIUS;
export function CreateTypeIcon({ type }: { type: number }) {
if (type === 1) return <Globe size={15} />;
if (type === 3) return <CreditCard size={15} />;
if (type === 4) return <ShieldUser size={15} />;
if (type === 2) return <StickyNote size={15} />;
if (type === 5) return <KeyRound size={15} />;
return <FileKey2 size={15} />;
}
export function cipherTypeKey(type: number): TypeFilter {
if (type === 1) return 'login';
if (type === 3) return 'card';
if (type === 4) return 'identity';
if (type === 2) return 'note';
return 'ssh';
}
export function cipherTypeLabel(type: number): string {
if (type === 1) return t('txt_login');
if (type === 3) return t('txt_card');
if (type === 4) return t('txt_identity');
if (type === 2) return t('txt_secure_note');
if (type === 5) return t('txt_ssh_key');
return t('txt_item');
}
export function TypeIcon({ type }: { type: number }) {
if (type === 1) return <Globe size={18} />;
if (type === 3) return <CreditCard size={18} />;
if (type === 4) return <ShieldUser size={18} />;
if (type === 2) return <StickyNote size={18} />;
if (type === 5) return <KeyRound size={18} />;
return <FileKey2 size={18} />;
}
export function parseFieldType(value: number | string | null | undefined): CustomFieldType {
if (value === 1 || value === 2 || value === 3) return value;
if (value === '1' || String(value).toLowerCase() === 'hidden') return 1;
if (value === '2' || String(value).toLowerCase() === 'boolean') return 2;
if (value === '3' || String(value).toLowerCase() === 'linked') return 3;
return 0;
}
export function toBooleanFieldValue(raw: string): boolean {
const v = String(raw || '').trim().toLowerCase();
return v === '1' || v === 'true' || v === 'yes' || v === 'on';
}
export function firstCipherUri(cipher: Cipher): string {
const uris = cipher.login?.uris || [];
for (const uri of uris) {
const raw = uri.decUri || uri.uri || '';
if (raw.trim()) return raw.trim();
}
return '';
}
export function hostFromUri(uri: string): string {
if (!uri.trim()) return '';
try {
const normalized = /^https?:\/\//i.test(uri) ? uri : `https://${uri}`;
return new URL(normalized).hostname || '';
} catch {
return '';
}
}
export function createEmptyDraft(type: number): VaultDraft {
return {
type,
favorite: false,
name: '',
folderId: '',
notes: '',
reprompt: false,
loginUsername: '',
loginPassword: '',
loginTotp: '',
loginUris: [''],
loginFido2Credentials: [],
cardholderName: '',
cardNumber: '',
cardBrand: '',
cardExpMonth: '',
cardExpYear: '',
cardCode: '',
identTitle: '',
identFirstName: '',
identMiddleName: '',
identLastName: '',
identUsername: '',
identCompany: '',
identSsn: '',
identPassportNumber: '',
identLicenseNumber: '',
identEmail: '',
identPhone: '',
identAddress1: '',
identAddress2: '',
identAddress3: '',
identCity: '',
identState: '',
identPostalCode: '',
identCountry: '',
sshPrivateKey: '',
sshPublicKey: '',
sshFingerprint: '',
customFields: [],
};
}
export function draftFromCipher(cipher: Cipher): VaultDraft {
const draft = createEmptyDraft(Number(cipher.type || 1));
draft.id = cipher.id;
draft.favorite = !!cipher.favorite;
draft.name = cipher.decName || '';
draft.folderId = cipher.folderId || '';
draft.notes = cipher.decNotes || '';
draft.reprompt = Number(cipher.reprompt || 0) === 1;
if (cipher.login) {
draft.loginUsername = cipher.login.decUsername || '';
draft.loginPassword = cipher.login.decPassword || '';
draft.loginTotp = cipher.login.decTotp || '';
draft.loginUris = (cipher.login.uris || []).map((x) => x.decUri || x.uri || '');
draft.loginFido2Credentials = Array.isArray(cipher.login.fido2Credentials)
? cipher.login.fido2Credentials.map((credential) => ({ ...credential }))
: [];
if (!draft.loginUris.length) draft.loginUris = [''];
}
if (cipher.card) {
draft.cardholderName = cipher.card.decCardholderName || '';
draft.cardNumber = cipher.card.decNumber || '';
draft.cardBrand = cipher.card.decBrand || '';
draft.cardExpMonth = cipher.card.decExpMonth || '';
draft.cardExpYear = cipher.card.decExpYear || '';
draft.cardCode = cipher.card.decCode || '';
}
if (cipher.identity) {
draft.identTitle = cipher.identity.decTitle || '';
draft.identFirstName = cipher.identity.decFirstName || '';
draft.identMiddleName = cipher.identity.decMiddleName || '';
draft.identLastName = cipher.identity.decLastName || '';
draft.identUsername = cipher.identity.decUsername || '';
draft.identCompany = cipher.identity.decCompany || '';
draft.identSsn = cipher.identity.decSsn || '';
draft.identPassportNumber = cipher.identity.decPassportNumber || '';
draft.identLicenseNumber = cipher.identity.decLicenseNumber || '';
draft.identEmail = cipher.identity.decEmail || '';
draft.identPhone = cipher.identity.decPhone || '';
draft.identAddress1 = cipher.identity.decAddress1 || '';
draft.identAddress2 = cipher.identity.decAddress2 || '';
draft.identAddress3 = cipher.identity.decAddress3 || '';
draft.identCity = cipher.identity.decCity || '';
draft.identState = cipher.identity.decState || '';
draft.identPostalCode = cipher.identity.decPostalCode || '';
draft.identCountry = cipher.identity.decCountry || '';
}
if (cipher.sshKey) {
draft.sshPrivateKey = cipher.sshKey.decPrivateKey || '';
draft.sshPublicKey = cipher.sshKey.decPublicKey || '';
draft.sshFingerprint = cipher.sshKey.decFingerprint || '';
}
draft.customFields = (cipher.fields || []).map((field) => ({
type: parseFieldType(field.type),
label: field.decName || '',
value: field.decValue || '',
}));
return draft;
}
export function maskSecret(value: string): string {
if (!value) return '';
return '*'.repeat(Math.max(8, Math.min(24, value.length)));
}
export function formatTotp(code: string): string {
if (!code || code.length < 6) return code;
return `${code.slice(0, 3)} ${code.slice(3, 6)}`;
}
export function formatHistoryTime(value: string | null | undefined): string {
if (!value) return t('txt_dash');
const date = new Date(value);
if (!Number.isFinite(date.getTime())) return value;
return date.toLocaleString();
}
export function parseAttachmentSizeBytes(attachment: CipherAttachment): number {
const raw = attachment?.size;
if (typeof raw === 'number' && Number.isFinite(raw) && raw >= 0) return raw;
const parsed = Number(raw);
if (Number.isFinite(parsed) && parsed >= 0) return parsed;
return 0;
}
export function formatAttachmentSize(attachment: CipherAttachment): string {
const sizeName = String(attachment?.sizeName || '').trim();
if (sizeName) return sizeName;
const bytes = parseAttachmentSizeBytes(attachment);
if (bytes <= 0) return '0 B';
if (bytes < 1024) return `${bytes} B`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(2)} KB`;
if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
}
export function sortTimeValue(cipher: Cipher): number {
const candidates = [cipher.revisionDate, cipher.creationDate];
for (const value of candidates) {
const time = new Date(String(value || '')).getTime();
if (Number.isFinite(time)) return time;
}
return 0;
}
export function creationTimeValue(cipher: Cipher): number {
const time = new Date(String(cipher.creationDate || '')).getTime();
return Number.isFinite(time) ? time : 0;
}
export function firstPasskeyCreationTime(cipher: Cipher | null): string | null {
const credentials = cipher?.login?.fido2Credentials;
if (!Array.isArray(credentials) || credentials.length === 0) return null;
for (const credential of credentials) {
const raw = String(credential?.creationDate || '').trim();
if (raw) return raw;
}
return null;
}
const failedIconHosts = new Set<string>();
export function VaultListIcon({ cipher }: { cipher: Cipher }) {
const uri = firstCipherUri(cipher);
const host = hostFromUri(uri);
const [errored, setErrored] = useState(() => (host ? failedIconHosts.has(host) : false));
if (host && !errored) {
return (
<img
className="list-icon"
src={`/icons/${host}/icon.png?v=2`}
alt=""
loading="lazy"
onError={() => {
failedIconHosts.add(host);
setErrored(true);
}}
/>
);
}
return (
<span className="list-icon-fallback">
<TypeIcon type={Number(cipher.type || 1)} />
</span>
);
}
export function copyToClipboard(value: string): void {
if (!value.trim()) return;
void navigator.clipboard.writeText(value);
}
export function openUri(raw: string): void {
const value = raw.trim();
if (!value) return;
const url = /^https?:\/\//i.test(value) ? value : `https://${value}`;
window.open(url, '_blank', 'noopener');
}