chore: upgrade frontend tooling and speed up checks

This commit is contained in:
t8y2 2026-06-03 12:20:27 +08:00
parent cc74fabad7
commit e67894bea7
78 changed files with 1382 additions and 1177 deletions

View File

@ -33,14 +33,8 @@ jobs:
- name: Install frontend dependencies
run: pnpm install --frozen-lockfile
- name: Frontend format check
run: npx oxfmt --check "apps/desktop/src/**/*.{ts,vue}"
- name: TypeScript check
run: npx vue-tsc --noEmit --project apps/desktop/tsconfig.json
- name: Frontend tests
run: pnpm test
- name: Frontend check
run: pnpm check
- name: Node package tests
run: pnpm test:packages

View File

@ -1,4 +1,4 @@
npx lint-staged
pnpm exec lint-staged
# Only format and re-add Rust files that are already staged
TOPLEVEL=$(git rev-parse --show-toplevel)

View File

@ -2,7 +2,7 @@
import { ref, computed, watch, onMounted, onUnmounted, nextTick, defineAsyncComponent } from "vue";
import { useI18n } from "vue-i18n";
import { invoke } from "@tauri-apps/api/core";
import { ChevronsRight } from "lucide-vue-next";
import { ChevronsRight } from "@lucide/vue";
import { TooltipProvider } from "@/components/ui/tooltip";
import AppToolbar from "@/components/layout/AppToolbar.vue";
import AppTabBar from "@/components/layout/AppTabBar.vue";

View File

@ -3,7 +3,7 @@ import { ref } from "vue";
import { useI18n } from "vue-i18n";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Lock, Loader2, ShieldCheck } from "lucide-vue-next";
import { Lock, Loader2, ShieldCheck } from "@lucide/vue";
import AppLogo from "@/components/icons/AppLogo.vue";
const props = withDefaults(

View File

@ -6,7 +6,7 @@ import { CanvasRenderer } from "echarts/renderers";
import { LineChart, BarChart, PieChart } from "echarts/charts";
import { GridComponent, TooltipComponent, LegendComponent } from "echarts/components";
import VChart from "vue-echarts";
import { BarChart3 } from "lucide-vue-next";
import { BarChart3 } from "@lucide/vue";
import { Button } from "@/components/ui/button";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import type { QueryResult } from "@/types/database";

View File

@ -1,7 +1,7 @@
<script setup lang="ts">
import { computed, ref, watch } from "vue";
import { useI18n } from "vue-i18n";
import { Lock } from "lucide-vue-next";
import { Lock } from "@lucide/vue";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";

View File

@ -1,7 +1,7 @@
<script setup lang="ts">
import { ref, onMounted, onUnmounted, computed } from "vue";
import { useI18n } from "vue-i18n";
import { FolderOpen, Trash2, Download, RotateCcw, Loader2, RefreshCw, Check, Clock3, FileUp } from "lucide-vue-next";
import { FolderOpen, Trash2, Download, RotateCcw, Loader2, RefreshCw, Check, Clock3, FileUp } from "@lucide/vue";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";

View File

@ -39,7 +39,7 @@ import {
Search,
ShieldCheck,
Trash2,
} from "lucide-vue-next";
} from "@lucide/vue";
type DbOption = { value: string; label: string };
type DbCategory = { key: string; title: string; options: DbOption[] };

View File

@ -1,7 +1,7 @@
<script setup lang="ts">
import { computed } from "vue";
import { useI18n } from "vue-i18n";
import { AlertTriangle, X } from "lucide-vue-next";
import { AlertTriangle, X } from "@lucide/vue";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { useConnectionStore } from "@/stores/connectionStore";

View File

@ -33,7 +33,7 @@ import {
Table2,
ZoomIn,
ZoomOut,
} from "lucide-vue-next";
} from "@lucide/vue";
import { useToast } from "@/composables/useToast";
import { isTauriRuntime } from "@/lib/tauriRuntime";

View File

@ -31,7 +31,7 @@ import {
Loader2,
Play,
Square,
} from "lucide-vue-next";
} from "@lucide/vue";
type CompareColumn = ColumnInfo;

View File

@ -15,7 +15,7 @@ import type { TableDiff, TableSchemaDetail } from "@/lib/schemaDiff";
import type { TableInfo } from "@/types/database";
import { sqlMetadataRefreshTarget } from "@/lib/sqlMetadataRefresh";
import { useToast } from "@/composables/useToast";
import { Loader2, Copy, Play, GitCompareArrows, ArrowLeftRight } from "lucide-vue-next";
import { Loader2, Copy, Play, GitCompareArrows, ArrowLeftRight } from "@lucide/vue";
import { useSqlHighlighter } from "@/composables/useSqlHighlighter";
interface SelectableTableDiff extends TableDiff {

View File

@ -30,7 +30,7 @@ import {
X,
Zap,
TestTube,
} from "lucide-vue-next";
} from "@lucide/vue";
import { Button } from "@/components/ui/button";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";

View File

@ -1,5 +1,5 @@
<script setup lang="ts">
import { X, Copy } from "lucide-vue-next";
import { X, Copy } from "@lucide/vue";
import { useI18n } from "vue-i18n";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";

View File

@ -1,7 +1,7 @@
<script setup lang="ts">
import { computed } from "vue";
import { useI18n } from "vue-i18n";
import { AlertTriangle } from "lucide-vue-next";
import { AlertTriangle } from "@lucide/vue";
import { Button } from "@/components/ui/button";
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
import { useSqlHighlighter } from "@/composables/useSqlHighlighter";

View File

@ -12,7 +12,7 @@ import {
replaceNext as cmReplaceNext,
replaceAll as cmReplaceAll,
} from "@codemirror/search";
import { ChevronUp, ChevronDown, ChevronRight, X } from "lucide-vue-next";
import { ChevronUp, ChevronDown, ChevronRight, X } from "@lucide/vue";
const props = defineProps<{
view: EditorView | null;

View File

@ -19,7 +19,7 @@ import {
Trash2,
Upload,
X,
} from "lucide-vue-next";
} from "@lucide/vue";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";

View File

@ -1,6 +1,6 @@
<script setup lang="ts">
import { ref, onMounted, onBeforeUnmount, onActivated, onDeactivated, watch, shallowRef, computed } from "vue";
import { Play, Copy, TextSelect } from "lucide-vue-next";
import { Play, Copy, TextSelect } from "@lucide/vue";
import { useI18n } from "vue-i18n";
import type { CompletionContext } from "@codemirror/autocomplete";
import type { EditorView as EditorViewType } from "@codemirror/view";

View File

@ -2,7 +2,7 @@
import { ref, computed, onMounted } from "vue";
import { useI18n } from "vue-i18n";
import { useSqlHighlighter } from "@/composables/useSqlHighlighter";
import { Clock, Copy, Database, RotateCcw, Search, Sparkles, Trash2, X } from "lucide-vue-next";
import { Clock, Copy, Database, RotateCcw, Search, Sparkles, Trash2, X } from "@lucide/vue";
import { RecycleScroller } from "vue-virtual-scroller";
import { Button } from "@/components/ui/button";
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";

View File

@ -1,5 +1,5 @@
<script setup lang="ts">
import { ChevronRight } from "lucide-vue-next";
import { ChevronRight } from "@lucide/vue";
import type { ExplainPlanNode } from "@/lib/explainPlan";
defineProps<{

View File

@ -1,7 +1,7 @@
<script setup lang="ts">
import { computed, ref } from "vue";
import { useI18n } from "vue-i18n";
import { AlertCircle, Braces, GitBranch, Table2 } from "lucide-vue-next";
import { AlertCircle, Braces, GitBranch, Table2 } from "@lucide/vue";
import type { ParsedExplainPlan, ExplainPlanNode } from "@/lib/explainPlan";
import { flattenExplainPlanNodes } from "@/lib/explainPlan";
import { Button } from "@/components/ui/button";

View File

@ -15,7 +15,7 @@ import { buildSelectedTablesPayload } from "@/lib/databaseExportSelection";
import { isTauriRuntime } from "@/lib/tauriRuntime";
import { useToast } from "@/composables/useToast";
import { Input } from "@/components/ui/input";
import { Download, Square, CheckSquare, Search, X } from "lucide-vue-next";
import { Download, Square, CheckSquare, Search, X } from "@lucide/vue";
const { t } = useI18n();
const { toast } = useToast();

View File

@ -3,7 +3,7 @@ import { computed } from "vue";
import { useI18n } from "vue-i18n";
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Loader2, CheckCircle2, XCircle, AlertCircle, X } from "lucide-vue-next";
import { Loader2, CheckCircle2, XCircle, AlertCircle, X } from "@lucide/vue";
const { t } = useI18n();
const open = defineModel<boolean>("open", { default: false });

View File

@ -56,7 +56,7 @@ import {
PanelBottom,
PanelRight,
TableProperties,
} from "lucide-vue-next";
} from "@lucide/vue";
import { Button } from "@/components/ui/button";
import CustomContextMenu, { type ContextMenuItem } from "@/components/ui/CustomContextMenu.vue";
import {
@ -1627,7 +1627,14 @@ interface RenderedGridColumn {
name: string;
}
const horizontalColumnWindow = computed(() => {
interface HorizontalColumnWindow {
start: number;
end: number;
beforeWidth: number;
afterWidth: number;
}
function getHorizontalColumnWindow(): HorizontalColumnWindow {
const widths = renderedColumnWidths.value;
const offsets = renderedColumnOffsets.value;
const totalColumns = visibleColumnIndexes.value.length;
@ -1666,7 +1673,9 @@ const horizontalColumnWindow = computed(() => {
beforeWidth: offset,
afterWidth: Math.max(0, columnsWidth - visibleWidth),
};
});
}
const horizontalColumnWindow = computed(getHorizontalColumnWindow);
const renderedGridColumns = computed<RenderedGridColumn[]>(() => {
const window = horizontalColumnWindow.value;

View File

@ -1,7 +1,7 @@
<script setup lang="ts">
import { computed, ref, watch } from "vue";
import { useI18n } from "vue-i18n";
import { ExternalLink, Maximize2, X, ZoomIn, ZoomOut } from "lucide-vue-next";
import { ExternalLink, Maximize2, X, ZoomIn, ZoomOut } from "@lucide/vue";
import { Button } from "@/components/ui/button";
import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog";
import { imagePreviewFitScale, imagePreviewTransform, nextImagePreviewScale } from "@/lib/imagePreviewViewer";

View File

@ -1,7 +1,7 @@
<script setup lang="ts">
import { computed, nextTick, onMounted, ref } from "vue";
import type { FocusOutsideEvent, PointerDownOutsideEvent } from "reka-ui";
import { CalendarClock, ChevronDown, ChevronUp, CircleSlash } from "lucide-vue-next";
import { CalendarClock, ChevronDown, ChevronUp, CircleSlash } from "@lucide/vue";
import { Button } from "@/components/ui/button";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { formatTemporalInputValue, type TemporalCellEditorKind } from "@/lib/dataGridTemporalEditor";

View File

@ -1,6 +1,6 @@
<script setup lang="ts">
import { computed, ref, watch } from "vue";
import { Settings2 } from "lucide-vue-next";
import { Settings2 } from "@lucide/vue";
import type { AiProvider } from "@/stores/settingsStore";
import { useTheme } from "@/composables/useTheme";

View File

@ -1,6 +1,6 @@
<script setup lang="ts">
import { computed } from "vue";
import { Database } from "lucide-vue-next";
import { Database } from "@lucide/vue";
const props = defineProps<{
dbType: string;

View File

@ -8,7 +8,7 @@ import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Check, FileUp, Loader2, Square, Upload, X } from "lucide-vue-next";
import { Check, FileUp, Loader2, Square, Upload, X } from "@lucide/vue";
import { useConnectionStore } from "@/stores/connectionStore";
import { useToast } from "@/composables/useToast";
import { autoMapImportColumns } from "@/lib/tableImport";

View File

@ -2,7 +2,7 @@
import { computed, ref } from "vue";
import { useI18n } from "vue-i18n";
import { translateBackendError } from "@/i18n/backend-errors";
import { Upload, Download, FolderPlus, RefreshCw, ChevronsLeft } from "lucide-vue-next";
import { Upload, Download, FolderPlus, RefreshCw, ChevronsLeft } from "@lucide/vue";
import { Button } from "@/components/ui/button";
import { Tooltip, TooltipTrigger, TooltipContent } from "@/components/ui/tooltip";
import LightDropdown from "@/components/ui/LightDropdown.vue";

View File

@ -15,7 +15,7 @@ import {
Pencil,
Package,
Check,
} from "lucide-vue-next";
} from "@lucide/vue";
import CustomContextMenu, { type ContextMenuItem } from "@/components/ui/CustomContextMenu.vue";
import LightDropdown from "@/components/ui/LightDropdown.vue";
import { Tooltip, TooltipTrigger, TooltipContent } from "@/components/ui/tooltip";

View File

@ -18,7 +18,7 @@ import {
Settings,
CloudDownload,
Package,
} from "lucide-vue-next";
} from "@lucide/vue";
import { Button } from "@/components/ui/button";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import LightDropdown from "@/components/ui/LightDropdown.vue";

View File

@ -16,7 +16,7 @@ import {
ChevronUp,
RefreshCcw,
Wrench,
} from "lucide-vue-next";
} from "@lucide/vue";
import { Splitpanes, Pane } from "splitpanes";
import "splitpanes/dist/splitpanes.css";
import { Button } from "@/components/ui/button";

View File

@ -13,7 +13,7 @@ import {
Save,
FolderOpen,
Layers,
} from "lucide-vue-next";
} from "@lucide/vue";
import { Button } from "@/components/ui/button";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { SearchableSelect } from "@/components/ui/searchable-select";

View File

@ -1,7 +1,7 @@
<script setup lang="ts">
import { ref, watch } from "vue";
import { useI18n } from "vue-i18n";
import { Loader2 } from "lucide-vue-next";
import { Loader2 } from "@lucide/vue";
import { Button } from "@/components/ui/button";
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import type { UpdateInfo } from "@/lib/api";

View File

@ -1,6 +1,6 @@
<script setup lang="ts">
import { useI18n } from "vue-i18n";
import { FilePlus2, Plus, History, Upload, Database, Search, ShieldCheck, Sparkles } from "lucide-vue-next";
import { FilePlus2, Plus, History, Upload, Database, Search, ShieldCheck, Sparkles } from "@lucide/vue";
import DatabaseIcon from "@/components/icons/DatabaseIcon.vue";
import {
connectionDriverLabel,

View File

@ -1,5 +1,5 @@
<script setup lang="ts">
import { Minus, Square, Copy, X } from "lucide-vue-next";
import { Minus, Square, Copy, X } from "@lucide/vue";
defineProps<{
isMaximized: boolean;

View File

@ -16,7 +16,7 @@ import {
Search,
SearchX,
X,
} from "lucide-vue-next";
} from "@lucide/vue";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Dialog, DialogFooter, DialogHeader, DialogScrollContent, DialogTitle } from "@/components/ui/dialog";

View File

@ -2,7 +2,7 @@
import { computed, ref } from "vue";
import { uuid } from "@/lib/utils";
import { useI18n } from "vue-i18n";
import { Plus, Trash2 } from "lucide-vue-next";
import { Plus, Trash2 } from "@lucide/vue";
import { Button } from "@/components/ui/button";
import DangerConfirmDialog from "@/components/editor/DangerConfirmDialog.vue";
import type { EditNode, EditNodeKind } from "@/types/editor";

View File

@ -2,7 +2,7 @@
import { computed, ref, onMounted } from "vue";
import { uuid } from "@/lib/utils";
import { useI18n } from "vue-i18n";
import { RefreshCw, Trash2, Plus, Save, ChevronLeft, ChevronRight, Table2, Braces, X } from "lucide-vue-next";
import { RefreshCw, Trash2, Plus, Save, ChevronLeft, ChevronRight, Table2, Braces, X } from "@lucide/vue";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import DangerConfirmDialog from "@/components/editor/DangerConfirmDialog.vue";

View File

@ -34,7 +34,7 @@ import {
TerminalSquare,
Trash2,
X,
} from "lucide-vue-next";
} from "@lucide/vue";
import { useI18n } from "vue-i18n";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";

View File

@ -1,6 +1,6 @@
<script setup lang="ts">
import { computed, ref, watch } from "vue";
import { CircleHelp, Loader2 } from "lucide-vue-next";
import { CircleHelp, Loader2 } from "@lucide/vue";
import { useI18n } from "vue-i18n";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";

View File

@ -1,6 +1,6 @@
<script setup lang="ts">
import { computed, defineComponent, h, ref, type VNodeChild } from "vue";
import { ChevronDown, ChevronRight } from "lucide-vue-next";
import { ChevronDown, ChevronRight } from "@lucide/vue";
import { Button } from "@/components/ui/button";
defineOptions({ name: "RedisJsonTree" });

View File

@ -14,7 +14,7 @@ import {
KeyRound,
TerminalSquare,
Asterisk,
} from "lucide-vue-next";
} from "@lucide/vue";
import { RecycleScroller } from "vue-virtual-scroller";
import "vue-virtual-scroller/dist/vue-virtual-scroller.css";
import { Splitpanes, Pane } from "splitpanes";

View File

@ -2,7 +2,7 @@
import { computed, ref, onBeforeUnmount, onMounted } from "vue";
import { useI18n } from "vue-i18n";
import { DynamicScroller, DynamicScrollerItem, RecycleScroller } from "vue-virtual-scroller";
import { Braces, Copy, Eye, FileText, Trash2, Save, RefreshCw, Plus, Loader2, Pencil, WrapText } from "lucide-vue-next";
import { Braces, Copy, Eye, FileText, Trash2, Save, RefreshCw, Plus, Loader2, Pencil, WrapText } from "@lucide/vue";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Badge } from "@/components/ui/badge";

View File

@ -2,7 +2,7 @@
import { computed, ref, watch } from "vue";
import { uuid } from "@/lib/utils";
import { useI18n } from "vue-i18n";
import { AlertCircle, Loader2, Search, Square, Table2 } from "lucide-vue-next";
import { AlertCircle, Loader2, Search, Square, Table2 } from "@lucide/vue";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Dialog, DialogFooter, DialogHeader, DialogScrollContent, DialogTitle } from "@/components/ui/dialog";

View File

@ -1,18 +1,7 @@
<script setup lang="ts">
import { ref, computed, nextTick, watch, type Component } from "vue";
import { useI18n } from "vue-i18n";
import {
Search,
X,
ListFilter,
Crosshair,
Server,
Database,
FolderTree,
Table2,
Eye,
RotateCcw,
} from "lucide-vue-next";
import { Search, X, ListFilter, Crosshair, Server, Database, FolderTree, Table2, Eye, RotateCcw } from "@lucide/vue";
import { useConnectionStore } from "@/stores/connectionStore";
import { useQueryStore } from "@/stores/queryStore";
import { useSettingsStore } from "@/stores/settingsStore";

View File

@ -47,7 +47,7 @@ import {
ListFilter,
Package,
Clipboard,
} from "lucide-vue-next";
} from "@lucide/vue";
import CustomContextMenu, { type ContextMenuItem } from "@/components/ui/CustomContextMenu.vue";
import { useConnectionStore } from "@/stores/connectionStore";
import { useQueryStore } from "@/stores/queryStore";

View File

@ -1,7 +1,7 @@
<script setup lang="ts">
import { computed, ref, watch } from "vue";
import { useI18n } from "vue-i18n";
import { CheckSquare, Loader2, Search, Square } from "lucide-vue-next";
import { CheckSquare, Loader2, Search, Square } from "@lucide/vue";
import { Button } from "@/components/ui/button";
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";

View File

@ -22,7 +22,7 @@ import {
type SqlFileProgress,
type SqlFileStatus,
} from "@/lib/api";
import { Check, CheckSquare, FileCode, FolderOpen, Loader2, Play, Square, X } from "lucide-vue-next";
import { Check, CheckSquare, FileCode, FolderOpen, Loader2, Play, Square, X } from "@lucide/vue";
const { t } = useI18n();
const { toast } = useToast();

View File

@ -23,7 +23,7 @@ import {
SlidersHorizontal,
Trash2,
X,
} from "lucide-vue-next";
} from "@lucide/vue";
import {
DropdownMenu,
DropdownMenuCheckboxItem,

View File

@ -14,7 +14,7 @@ import type { TransferProgress, TransferMode } from "@/lib/api";
import type { DatabaseType } from "@/types/database";
import { isSchemaAware, supportsTransfer } from "@/lib/databaseCapabilities";
import { nextTransferTerminalState } from "@/lib/transferProgressState";
import { ArrowRightLeft, Check, X, Loader2, Square, CheckSquare } from "lucide-vue-next";
import { ArrowRightLeft, Check, X, Loader2, Square, CheckSquare } from "@lucide/vue";
const { t } = useI18n();
const open = defineModel<boolean>("open", { default: false });

View File

@ -1,6 +1,6 @@
<script setup lang="ts">
import { ref, watch, onBeforeUnmount, nextTick, type Component } from "vue";
import { ChevronRight } from "lucide-vue-next";
import { ChevronRight } from "@lucide/vue";
export interface ContextMenuItem {
label: string;

View File

@ -1,6 +1,6 @@
<script setup lang="ts">
import { computed, nextTick, onBeforeUnmount, ref, type Component } from "vue";
import { Check, ChevronDown } from "lucide-vue-next";
import { Check, ChevronDown } from "@lucide/vue";
export interface LightDropdownItem {
label: string;

View File

@ -3,7 +3,7 @@ import type { ContextMenuCheckboxItemEmits, ContextMenuCheckboxItemProps } from
import type { HTMLAttributes } from "vue";
import { reactiveOmit } from "@vueuse/core";
import { CheckIcon } from "lucide-vue-next";
import { CheckIcon } from "@lucide/vue";
import { ContextMenuCheckboxItem, ContextMenuItemIndicator, useForwardPropsEmits } from "reka-ui";
import { shouldSuppressRepeatedActivation, suppressEvent, type ActionActivationGuard } from "@/lib/actionActivation";
import { cn } from "@/lib/utils";

View File

@ -3,7 +3,7 @@ import type { ContextMenuRadioItemEmits, ContextMenuRadioItemProps } from "reka-
import type { HTMLAttributes } from "vue";
import { reactiveOmit } from "@vueuse/core";
import { CheckIcon } from "lucide-vue-next";
import { CheckIcon } from "@lucide/vue";
import { ContextMenuItemIndicator, ContextMenuRadioItem, useForwardPropsEmits } from "reka-ui";
import { shouldSuppressRepeatedActivation, suppressEvent, type ActionActivationGuard } from "@/lib/actionActivation";
import { cn } from "@/lib/utils";

View File

@ -3,7 +3,7 @@ import type { ContextMenuSubTriggerProps } from "reka-ui";
import type { HTMLAttributes } from "vue";
import { reactiveOmit } from "@vueuse/core";
import { ChevronRightIcon } from "lucide-vue-next";
import { ChevronRightIcon } from "@lucide/vue";
import { ContextMenuSubTrigger, useForwardProps } from "reka-ui";
import { cn } from "@/lib/utils";

View File

@ -3,7 +3,7 @@ import type { DialogContentEmits, DialogContentProps } from "reka-ui";
import type { HTMLAttributes } from "vue";
import { reactiveOmit } from "@vueuse/core";
import { XIcon } from "lucide-vue-next";
import { XIcon } from "@lucide/vue";
import {
DialogClose,
DialogContent,

View File

@ -3,7 +3,7 @@ import type { DialogContentEmits, DialogContentProps } from "reka-ui";
import type { HTMLAttributes } from "vue";
import { reactiveOmit } from "@vueuse/core";
import { XIcon } from "lucide-vue-next";
import { XIcon } from "@lucide/vue";
import { DialogClose, DialogContent, DialogOverlay, DialogPortal, useForwardPropsEmits } from "reka-ui";
import { cn } from "@/lib/utils";

View File

@ -3,7 +3,7 @@ import type { DropdownMenuCheckboxItemEmits, DropdownMenuCheckboxItemProps } fro
import type { HTMLAttributes } from "vue";
import { reactiveOmit } from "@vueuse/core";
import { CheckIcon } from "lucide-vue-next";
import { CheckIcon } from "@lucide/vue";
import { DropdownMenuCheckboxItem, DropdownMenuItemIndicator, useForwardPropsEmits } from "reka-ui";
import { shouldSuppressRepeatedActivation, suppressEvent, type ActionActivationGuard } from "@/lib/actionActivation";
import { cn } from "@/lib/utils";

View File

@ -3,7 +3,7 @@ import type { DropdownMenuRadioItemEmits, DropdownMenuRadioItemProps } from "rek
import type { HTMLAttributes } from "vue";
import { reactiveOmit } from "@vueuse/core";
import { CheckIcon } from "lucide-vue-next";
import { CheckIcon } from "@lucide/vue";
import { DropdownMenuItemIndicator, DropdownMenuRadioItem, useForwardPropsEmits } from "reka-ui";
import { shouldSuppressRepeatedActivation, suppressEvent, type ActionActivationGuard } from "@/lib/actionActivation";
import { cn } from "@/lib/utils";

View File

@ -3,7 +3,7 @@ import type { DropdownMenuSubTriggerProps } from "reka-ui";
import type { HTMLAttributes } from "vue";
import { reactiveOmit } from "@vueuse/core";
import { ChevronRightIcon } from "lucide-vue-next";
import { ChevronRightIcon } from "@lucide/vue";
import { DropdownMenuSubTrigger, useForwardProps } from "reka-ui";
import { cn } from "@/lib/utils";

View File

@ -1,7 +1,7 @@
<script setup lang="ts">
import { computed, nextTick, ref, watch } from "vue";
import type { HTMLAttributes } from "vue";
import { Check, ChevronDown, Search } from "lucide-vue-next";
import { Check, ChevronDown, Search } from "@lucide/vue";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";

View File

@ -3,7 +3,7 @@ import type { SelectItemProps } from "reka-ui";
import type { HTMLAttributes } from "vue";
import { reactiveOmit } from "@vueuse/core";
import { CheckIcon } from "lucide-vue-next";
import { CheckIcon } from "@lucide/vue";
import { SelectItem, SelectItemIndicator, SelectItemText, useForwardProps } from "reka-ui";
import { cn } from "@/lib/utils";

View File

@ -3,7 +3,7 @@ import type { SelectScrollDownButtonProps } from "reka-ui";
import type { HTMLAttributes } from "vue";
import { reactiveOmit } from "@vueuse/core";
import { ChevronDownIcon } from "lucide-vue-next";
import { ChevronDownIcon } from "@lucide/vue";
import { SelectScrollDownButton, useForwardProps } from "reka-ui";
import { cn } from "@/lib/utils";

View File

@ -3,7 +3,7 @@ import type { SelectScrollUpButtonProps } from "reka-ui";
import type { HTMLAttributes } from "vue";
import { reactiveOmit } from "@vueuse/core";
import { ChevronUpIcon } from "lucide-vue-next";
import { ChevronUpIcon } from "@lucide/vue";
import { SelectScrollUpButton, useForwardProps } from "reka-ui";
import { cn } from "@/lib/utils";

View File

@ -3,7 +3,7 @@ import type { SelectTriggerProps } from "reka-ui";
import type { HTMLAttributes } from "vue";
import { reactiveOmit } from "@vueuse/core";
import { ChevronDownIcon } from "lucide-vue-next";
import { ChevronDownIcon } from "@lucide/vue";
import { SelectIcon, SelectTrigger, useForwardProps } from "reka-ui";
import { cn } from "@/lib/utils";

View File

@ -3,7 +3,7 @@ import type { DialogContentEmits, DialogContentProps } from "reka-ui";
import type { HTMLAttributes } from "vue";
import { reactiveOmit } from "@vueuse/core";
import { XIcon } from "lucide-vue-next";
import { XIcon } from "@lucide/vue";
import { DialogClose, DialogContent, DialogPortal, useForwardPropsEmits } from "reka-ui";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";

View File

@ -21,6 +21,10 @@ const HEX_VALUE_RE = /^(?:0[xX]|\\x)([0-9a-fA-F\s]+)$/;
const BINARY_TYPE_RE =
/^(?:blob|tinyblob|mediumblob|longblob|bytea|bytes|binary|varbinary|image|raw|long\s+raw)(?:\b|\()/i;
function copyBytesForBlob(bytes: Uint8Array): Uint8Array<ArrayBuffer> {
return new Uint8Array(bytes);
}
export function parseBinaryCellHexValue(value: CellValue): Uint8Array | null {
if (typeof value !== "string") return null;
const match = value.trim().match(HEX_VALUE_RE);
@ -105,7 +109,9 @@ export async function downloadBinaryCellPayload(
return { kind: "saved", path };
}
const blob = new Blob([payload.data], { type: payload.mimeType });
const blob = new Blob([typeof payload.data === "string" ? payload.data : copyBytesForBlob(payload.data)], {
type: payload.mimeType,
});
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;

View File

@ -14,7 +14,7 @@ export interface PlainConfigPayload {
const PBKDF2_ITERATIONS = 100_000;
async function deriveKey(passphrase: string, salt: Uint8Array): Promise<CryptoKey> {
async function deriveKey(passphrase: string, salt: Uint8Array<ArrayBuffer>): Promise<CryptoKey> {
const encoder = new TextEncoder();
const baseKey = await crypto.subtle.importKey("raw", encoder.encode(passphrase), "PBKDF2", false, ["deriveKey"]);
return crypto.subtle.deriveKey(
@ -26,8 +26,8 @@ async function deriveKey(passphrase: string, salt: Uint8Array): Promise<CryptoKe
);
}
function toBase64(buffer: ArrayBuffer): string {
const bytes = new Uint8Array(buffer);
function toBase64(data: ArrayBuffer | Uint8Array<ArrayBuffer>): string {
const bytes = data instanceof Uint8Array ? data : new Uint8Array(data);
let binary = "";
for (let i = 0; i < bytes.length; i++) {
binary += String.fromCharCode(bytes[i]);
@ -35,7 +35,7 @@ function toBase64(buffer: ArrayBuffer): string {
return btoa(binary);
}
function fromBase64(base64: string): Uint8Array {
function fromBase64(base64: string): Uint8Array<ArrayBuffer> {
const binary = atob(base64);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) {

View File

@ -1188,7 +1188,7 @@ export async function exportQueryResultXlsx(
rows,
});
const fileName = filePath.split(/[\\/]/).pop() || "export.xlsx";
const blob = new Blob([workbook], {
const blob = new Blob([new Uint8Array(workbook)], {
type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
});
const url = URL.createObjectURL(blob);

View File

@ -38,6 +38,22 @@ const STORAGE_KEY = "dbx-open-tabs";
const ACTIVE_TAB_KEY = "dbx-active-tab";
const ORACLE_LIKE_METADATA_TYPES = new Set<string>(["oracle", "dameng", "oceanbase-oracle"]);
async function withFrontendQueryTimeout<T>(promise: Promise<T>, timeoutSecs: number, message: string): Promise<T> {
if (timeoutSecs === 0) return promise;
let timer: ReturnType<typeof setTimeout> | undefined;
try {
return await Promise.race([
promise,
new Promise<never>((_, reject) => {
timer = setTimeout(() => reject(new Error(message)), timeoutSecs * 1000);
}),
]);
} finally {
if (timer) clearTimeout(timer);
}
}
function normalizeOracleLikeMetadataIdentifier(dbType: string, identifier: string | undefined, quoted?: boolean) {
if (!identifier || quoted || !ORACLE_LIKE_METADATA_TYPES.has(dbType)) return identifier;
return identifier.toUpperCase();
@ -961,19 +977,12 @@ export const useQueryStore = defineStore("query", () => {
executionId,
executionOptions,
);
const results =
queryTimeoutSecs === 0
? await executionPromise
: await Promise.race([
executionPromise,
new Promise<never>((_, reject) => {
const frontendTimeoutSecs = Math.max(queryTimeoutSecs * 2, 60);
setTimeout(
() => reject(new Error(t("editor.queryTimeoutError", { seconds: frontendTimeoutSecs }))),
frontendTimeoutSecs * 1000,
);
}),
]);
const frontendTimeoutSecs = Math.max(queryTimeoutSecs * 2, 60);
const results = await withFrontendQueryTimeout(
executionPromise,
queryTimeoutSecs === 0 ? 0 : frontendTimeoutSecs,
t("editor.queryTimeoutError", { seconds: frontendTimeoutSecs }),
);
console.info("[DBX][executeTabSql:execute-multi:done]", {
traceId,
resultCount: results.length,

View File

@ -1,8 +1,7 @@
declare module "vue-virtual-scroller" {
import type { DefineComponent } from "vue";
const VueVirtualScroller: any;
export const RecycleScroller: DefineComponent<any, any, any>;
export const DynamicScroller: DefineComponent<any, any, any>;
export const DynamicScrollerItem: DefineComponent<any, any, any>;
export const RecycleScroller: any;
export const DynamicScroller: any;
export const DynamicScrollerItem: any;
export default VueVirtualScroller;
}

View File

@ -8,6 +8,8 @@
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"incremental": true,
"tsBuildInfoFile": "../../node_modules/.cache/vue-tsc/desktop.tsbuildinfo",
"moduleDetection": "force",
"noEmit": true,
"jsx": "preserve",
@ -15,7 +17,6 @@
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}

View File

@ -5,6 +5,55 @@ import path from "path";
const host = process.env.TAURI_DEV_HOST;
const isTauri = !!host || !!process.env.TAURI_ENV_ARCH;
const manualChunks: Record<string, string[]> = {
codemirror: [
"codemirror",
"@codemirror/lang-sql",
"@codemirror/view",
"@codemirror/state",
"@codemirror/autocomplete",
"@codemirror/commands",
"@codemirror/theme-one-dark",
],
"sql-formatter": ["sql-formatter"],
"vue-echarts": ["vue-echarts"],
ui: ["reka-ui"],
marked: ["marked"],
};
function chunkNameForEchartsModule(id: string): string {
const echartsPath = id.split("/node_modules/echarts/").pop() ?? "";
if (echartsPath === "charts.js" || echartsPath.startsWith("lib/chart/")) {
return "echarts-charts";
}
if (echartsPath === "components.js" || echartsPath.startsWith("lib/component/")) {
return "echarts-components";
}
if (echartsPath === "renderers.js" || echartsPath.startsWith("lib/renderer/")) {
return "echarts-renderers";
}
return "echarts-core";
}
function chunkNameForModule(id: string): string | undefined {
const normalizedId = id.replaceAll("\\", "/");
if (normalizedId.includes("/node_modules/echarts/")) {
return chunkNameForEchartsModule(normalizedId);
}
for (const [chunkName, packages] of Object.entries(manualChunks)) {
if (packages.some((pkg) => normalizedId.includes(`/node_modules/${pkg}/`))) {
return chunkName;
}
}
return undefined;
}
export default defineConfig(async () => ({
root: __dirname,
@ -20,20 +69,7 @@ export default defineConfig(async () => ({
emptyOutDir: true,
rollupOptions: {
output: {
manualChunks: {
codemirror: [
"codemirror",
"@codemirror/lang-sql",
"@codemirror/view",
"@codemirror/state",
"@codemirror/autocomplete",
"@codemirror/commands",
"@codemirror/theme-one-dark",
],
"sql-formatter": ["sql-formatter"],
ui: ["reka-ui"],
marked: ["marked"],
},
manualChunks: chunkNameForModule,
},
},
},

View File

@ -16,8 +16,10 @@
"test:packages": "pnpm --filter @dbx-app/node-core test && pnpm --filter @dbx-app/cli test && pnpm --filter @dbx-app/mcp-server test",
"pack:packages": "rm -rf /tmp/dbx-pack-check && mkdir -p /tmp/dbx-pack-check && pnpm --filter @dbx-app/node-core pack --pack-destination /tmp/dbx-pack-check && pnpm --filter @dbx-app/cli pack --pack-destination /tmp/dbx-pack-check && pnpm --filter @dbx-app/mcp-server pack --pack-destination /tmp/dbx-pack-check",
"publish:dry-run": "pnpm build:packages && pnpm pack:packages",
"build": "vue-tsc --noEmit --project apps/desktop/tsconfig.json && vite build --config apps/desktop/vite.config.ts",
"check": "oxfmt --check \"apps/desktop/src/**/*.{ts,vue}\" && pnpm lint && vue-tsc --noEmit --project apps/desktop/tsconfig.json && pnpm test",
"typecheck": "vue-tsc --noEmit --project apps/desktop/tsconfig.json",
"build": "vite build --config apps/desktop/vite.config.ts",
"build:checked": "pnpm typecheck && pnpm build",
"check": "node scripts/run-check.mjs",
"lint": "oxlint --vue-plugin apps/desktop/src",
"fmt": "oxfmt \"apps/desktop/src/**/*.{ts,vue}\"",
"test": "tsx --tsconfig apps/desktop/tsconfig.json --test packages/app-tests/*.test.ts",
@ -26,8 +28,8 @@
"prepare": "husky"
},
"dependencies": {
"@babel/runtime": "^7.29.2",
"@codemirror/autocomplete": "^6.20.1",
"@babel/runtime": "^7.29.7",
"@codemirror/autocomplete": "^6.20.2",
"@codemirror/commands": "^6.10.3",
"@codemirror/lang-json": "^6.0.2",
"@codemirror/lang-sql": "^6.10.0",
@ -35,7 +37,8 @@
"@codemirror/search": "^6.7.0",
"@codemirror/state": "^6.6.0",
"@codemirror/theme-one-dark": "^6.1.3",
"@codemirror/view": "^6.41.1",
"@codemirror/view": "^6.43.0",
"@lucide/vue": "1.17.0",
"@tauri-apps/api": "^2.11.0",
"@tauri-apps/plugin-clipboard-manager": "^2.3.2",
"@tauri-apps/plugin-dialog": "^2.7.1",
@ -43,49 +46,48 @@
"@tauri-apps/plugin-process": "^2.3.1",
"@tauri-apps/plugin-shell": "^2.3.5",
"@tauri-apps/plugin-updater": "^2.10.1",
"@uiw/codemirror-theme-duotone": "^4.25.9",
"@uiw/codemirror-theme-material": "^4.25.9",
"@uiw/codemirror-theme-nord": "^4.25.9",
"@uiw/codemirror-theme-okaidia": "^4.25.9",
"@uiw/codemirror-theme-vscode": "^4.25.9",
"@uiw/codemirror-theme-xcode": "^4.25.9",
"@uiw/codemirror-theme-duotone": "^4.25.10",
"@uiw/codemirror-theme-material": "^4.25.10",
"@uiw/codemirror-theme-nord": "^4.25.10",
"@uiw/codemirror-theme-okaidia": "^4.25.10",
"@uiw/codemirror-theme-vscode": "^4.25.10",
"@uiw/codemirror-theme-xcode": "^4.25.10",
"@vueuse/core": "^14.2.1",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"codemirror": "^6.0.2",
"echarts": "^6.0.0",
"lucide-vue-next": "^1.0.0",
"marked": "^18.0.3",
"echarts": "^6.1.0",
"marked": "^18.0.4",
"pinia": "^3.0.0",
"reka-ui": "^2.9.8",
"shadcn-vue": "^2.7.3",
"shiki": "^4.1.0",
"splitpanes": "^4.0.4",
"sql-formatter": "^15.7.3",
"tailwind-merge": "^3.5.0",
"shiki": "^4.2.0",
"splitpanes": "^4.1.2",
"sql-formatter": "^15.8.0",
"tailwind-merge": "^3.6.0",
"tw-animate-css": "^1.4.0",
"vue": "^3.5.0",
"vue": "^3.5.35",
"vue-echarts": "^8.0.1",
"vue-i18n": "^11.4.0",
"vue-virtual-scroller": "^3.0.1"
"vue-i18n": "^11.4.4",
"vue-virtual-scroller": "^3.0.4"
},
"devDependencies": {
"@oxfmt/binding-darwin-arm64": "0.47.0",
"@oxlint/binding-darwin-arm64": "1.62.0",
"@tailwindcss/vite": "^4.2.4",
"@tauri-apps/cli": "^2.11.0",
"@types/node": "^25.6.0",
"@oxfmt/binding-darwin-arm64": "0.53.0",
"@oxlint/binding-darwin-arm64": "1.68.0",
"@tailwindcss/vite": "^4.3.0",
"@tauri-apps/cli": "^2.11.2",
"@types/node": "^25.9.1",
"@types/splitpanes": "^2.2.6",
"@vitejs/plugin-vue": "^5.2.0",
"@vitejs/plugin-vue": "^6.0.7",
"husky": "^9.1.7",
"lint-staged": "^16.4.0",
"oxfmt": "0.47.0",
"oxlint": "^1.62.0",
"tailwindcss": "^4.2.4",
"tsx": "^4.21.0",
"typescript": "~5.6.0",
"vite": "^6.0.0",
"vue-tsc": "^2.2.0"
"oxfmt": "0.53.0",
"oxlint": "^1.68.0",
"tailwindcss": "^4.3.0",
"tsx": "^4.22.4",
"typescript": "~6.0.3",
"vite": "^8.0.16",
"vue-tsc": "^3.3.3"
},
"pnpm": {
"onlyBuiltDependencies": [

File diff suppressed because it is too large Load Diff

85
scripts/run-check.mjs Normal file
View File

@ -0,0 +1,85 @@
import { spawn } from "node:child_process";
import { performance } from "node:perf_hooks";
const tasks = [
{
name: "format",
command: "oxfmt",
args: ["--check", "apps/desktop/src/**/*.{ts,vue}"],
},
{
name: "lint",
command: "oxlint",
args: ["--vue-plugin", "apps/desktop/src"],
},
{
name: "typecheck",
command: "vue-tsc",
args: ["--noEmit", "--project", "apps/desktop/tsconfig.json"],
},
{
name: "test",
command: "tsx",
args: ["--tsconfig", "apps/desktop/tsconfig.json", "--test", "packages/app-tests/*.test.ts"],
},
];
function runTask(task) {
const startedAt = performance.now();
const child = spawn(task.command, task.args, {
cwd: process.cwd(),
env: process.env,
stdio: ["ignore", "pipe", "pipe"],
});
const stdout = [];
const stderr = [];
child.stdout.on("data", (chunk) => stdout.push(chunk));
child.stderr.on("data", (chunk) => stderr.push(chunk));
return new Promise((resolve) => {
child.on("error", (error) => {
resolve({
...task,
code: 1,
durationMs: performance.now() - startedAt,
output: "",
errorOutput: error.stack ?? String(error),
});
});
child.on("close", (code) => {
resolve({
...task,
code,
durationMs: performance.now() - startedAt,
output: Buffer.concat(stdout).toString(),
errorOutput: Buffer.concat(stderr).toString(),
});
});
});
}
const results = await Promise.all(tasks.map(runTask));
for (const result of results) {
const seconds = (result.durationMs / 1000).toFixed(2);
const status = result.code === 0 ? "ok" : "failed";
console.log(`${status.padEnd(6)} ${result.name.padEnd(9)} ${seconds}s`);
}
const failures = results.filter((result) => result.code !== 0);
for (const failure of failures) {
console.error(`\n${failure.name} output:`);
if (failure.output) {
console.error(failure.output.trimEnd());
}
if (failure.errorOutput) {
console.error(failure.errorOutput.trimEnd());
}
}
if (failures.length > 0) {
process.exit(1);
}