fix: eliminate screen share audio feedback loop + upgrade Electron 33→40
Screen sharing with audio captured the app's own voice playback, causing users to hear themselves echoed back. Fixed via two layers: - Add restrictOwnAudio constraint (Chrome 141+/Chromium 144) to exclude the app's own audio from system audio capture - Add shareAudio toggle so users can disable system audio entirely - Remove outdated macOS audio block (now supported via ScreenCaptureKit) - Upgrade Electron 33→40 (Chromium 130→144) so restrictOwnAudio works natively in the desktop app - Add NSAudioCaptureUsageDescription for macOS 14.2+ audio capture - Add GTK 3 fallback for Linux GNOME compatibility (Electron 36+)
This commit is contained in:
@@ -17,6 +17,9 @@ protocols:
|
||||
- backspace
|
||||
mac:
|
||||
category: public.app-category.social-networking
|
||||
minimumSystemVersion: "12.0"
|
||||
extendInfo:
|
||||
NSAudioCaptureUsageDescription: "Backspace needs access to system audio to share sound during screen sharing."
|
||||
target:
|
||||
- dmg
|
||||
- zip
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
"electron-updater": "^6.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"electron": "^33.2.0",
|
||||
"electron": "^40.0.0",
|
||||
"electron-builder": "^25.1.8",
|
||||
"typescript": "^5.7.2"
|
||||
}
|
||||
|
||||
@@ -432,7 +432,7 @@ function registerIpcHandlers(): void {
|
||||
});
|
||||
|
||||
// Screen share picker coordination (used by setDisplayMediaRequestHandler)
|
||||
ipcMain.on('screen-share-selected', (_event, sourceId: string | null) => {
|
||||
ipcMain.on('screen-share-selected', (_event, _sourceId: string | null, _shareAudio?: boolean) => {
|
||||
// Handled via ipcMain.once in the display media handler — this is just
|
||||
// a safety net to prevent unhandled-message warnings
|
||||
});
|
||||
@@ -487,6 +487,12 @@ function handleDeepLink(url: string): void {
|
||||
}
|
||||
}
|
||||
|
||||
// Electron 36+ defaults to GTK 4 on GNOME, which crashes if GTK 2/3
|
||||
// libraries are loaded in the same process. Force GTK 3 for compatibility.
|
||||
if (process.platform === 'linux') {
|
||||
app.commandLine.appendSwitch('gtk-version', '3');
|
||||
}
|
||||
|
||||
// Set as default protocol handler
|
||||
app.setAsDefaultProtocolClient('backspace');
|
||||
|
||||
@@ -621,12 +627,12 @@ if (!gotTheLock) {
|
||||
// Send sources to renderer, wait for user selection
|
||||
mainWindow?.webContents.send('screen-share-sources', serialized);
|
||||
|
||||
const sourceId = await new Promise<string | null>((resolve) => {
|
||||
ipcMain.once('screen-share-selected', (_event, id: string | null) => {
|
||||
resolve(id);
|
||||
const { sourceId, shareAudio } = await new Promise<{ sourceId: string | null; shareAudio: boolean }>((resolve) => {
|
||||
ipcMain.once('screen-share-selected', (_event, id: string | null, wantAudio?: boolean) => {
|
||||
resolve({ sourceId: id, shareAudio: wantAudio ?? true });
|
||||
});
|
||||
});
|
||||
console.log('[Main:ScreenShare] User selected:', sourceId);
|
||||
console.log('[Main:ScreenShare] User selected:', sourceId, 'audio:', shareAudio);
|
||||
|
||||
if (!sourceId) {
|
||||
// @ts-ignore — deny the request without crashing
|
||||
@@ -642,12 +648,9 @@ if (!gotTheLock) {
|
||||
}
|
||||
|
||||
// Provide the selected source — Electron creates the MediaStream
|
||||
// Enable system audio loopback on Windows/Linux (macOS blocks at OS level)
|
||||
if (process.platform === 'darwin') {
|
||||
callback({ video: selected });
|
||||
} else {
|
||||
callback({ video: selected, audio: 'loopback' });
|
||||
}
|
||||
// System audio loopback: Windows/Linux native, macOS 13+ via ScreenCaptureKit
|
||||
// Controlled by user's shareAudio preference from the picker UI
|
||||
callback({ video: selected, ...(shareAudio ? { audio: 'loopback' } : {}) });
|
||||
} catch (err) {
|
||||
console.error('[Main:ScreenShare] Handler error:', err);
|
||||
// @ts-ignore — deny the request without crashing
|
||||
|
||||
@@ -54,8 +54,8 @@ contextBridge.exposeInMainWorld('backspace', {
|
||||
onScreenShareSources: (callback: (sources: unknown[]) => void) => {
|
||||
ipcRenderer.on('screen-share-sources', (_event, sources) => callback(sources));
|
||||
},
|
||||
selectScreenSource: (sourceId: string | null) => {
|
||||
ipcRenderer.send('screen-share-selected', sourceId);
|
||||
selectScreenSource: (sourceId: string | null, shareAudio?: boolean) => {
|
||||
ipcRenderer.send('screen-share-selected', sourceId, shareAudio ?? true);
|
||||
},
|
||||
|
||||
// Instance URL management
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useState, useEffect, useCallback, useMemo } from 'react';
|
||||
import { create } from 'zustand';
|
||||
import { getElectronAPI } from '../../platform/platform';
|
||||
import { useVoiceStore } from '../../stores/voiceStore';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Zustand micro-store — bridges the event-driven API to React state
|
||||
@@ -20,9 +21,9 @@ const useScreenPickerStore = create<ScreenPickerState>(() => ({
|
||||
// Close helper — sends selection back to main process
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function closePicker(sourceId: string | null) {
|
||||
function closePicker(sourceId: string | null, shareAudio?: boolean) {
|
||||
const api = getElectronAPI();
|
||||
if (api) api.selectScreenSource(sourceId);
|
||||
if (api) api.selectScreenSource(sourceId, shareAudio);
|
||||
useScreenPickerStore.setState({ isOpen: false, sources: [] });
|
||||
}
|
||||
|
||||
@@ -37,6 +38,8 @@ export function ScreenSharePicker() {
|
||||
const [activeTab, setActiveTab] = useState<Tab>('screens');
|
||||
const [selectedId, setSelectedId] = useState<string | null>(null);
|
||||
const [search, setSearch] = useState('');
|
||||
const shareAudio = useVoiceStore((s) => s.screenShareConfig.shareAudio);
|
||||
const setScreenShareConfig = useVoiceStore((s) => s.setScreenShareConfig);
|
||||
|
||||
// Register listener for sources from main process (once on mount)
|
||||
useEffect(() => {
|
||||
@@ -93,8 +96,6 @@ export function ScreenSharePicker() {
|
||||
return wins.filter((w) => w.name.toLowerCase().includes(q));
|
||||
}, [sources, search]);
|
||||
|
||||
const isMac = getElectronAPI()?.platform === 'darwin';
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
const activeSources = activeTab === 'screens' ? screens : windows;
|
||||
@@ -168,7 +169,7 @@ export function ScreenSharePicker() {
|
||||
source={source}
|
||||
selected={selectedId === source.id}
|
||||
onClick={() => setSelectedId(source.id)}
|
||||
onDoubleClick={() => closePicker(source.id)}
|
||||
onDoubleClick={() => closePicker(source.id, shareAudio)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -177,11 +178,22 @@ export function ScreenSharePicker() {
|
||||
|
||||
{/* Footer */}
|
||||
<div className="flex-shrink-0 flex flex-col items-center px-5 pt-2 pb-4">
|
||||
{isMac ? (
|
||||
<div className="text-[11px] text-txt-tertiary mb-2">System audio is not available on macOS</div>
|
||||
) : (
|
||||
<div className="text-[11px] text-txt-tertiary mb-2">System audio will be shared</div>
|
||||
)}
|
||||
<div className="flex flex-col items-center gap-1 mb-2">
|
||||
<label className="flex items-center gap-2 cursor-pointer select-none">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={shareAudio}
|
||||
onChange={(e) => setScreenShareConfig({ shareAudio: e.target.checked })}
|
||||
className="w-3.5 h-3.5 rounded accent-accent-primary cursor-pointer"
|
||||
/>
|
||||
<span className="text-[12px] text-txt-secondary">Share system audio</span>
|
||||
</label>
|
||||
{shareAudio && (
|
||||
<div className="text-[11px] text-accent-amber/80">
|
||||
Headphones recommended to prevent echo
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="glass-bubble rounded-full px-3 py-2 flex items-center gap-3">
|
||||
<button
|
||||
onClick={() => closePicker(null)}
|
||||
@@ -190,7 +202,7 @@ export function ScreenSharePicker() {
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={() => closePicker(selectedId)}
|
||||
onClick={() => closePicker(selectedId, shareAudio)}
|
||||
disabled={!selectedId}
|
||||
className="px-3 py-1.5 bg-accent-primary hover:bg-accent-primary-hover text-white text-sm font-medium rounded-full transition-colors disabled:opacity-40 disabled:cursor-not-allowed"
|
||||
>
|
||||
|
||||
@@ -5,6 +5,8 @@ import type { ScreenShareConfig } from '../../stores/voiceStore';
|
||||
import { useSettingsStore } from '../../stores/settingsStore';
|
||||
import { buildScreenShareOptions } from '../../utils/screenShare';
|
||||
import { useFloatingPosition } from '../../hooks/useFloatingPosition';
|
||||
import { Toggle } from '../ui/Toggle';
|
||||
import { isElectron } from '../../platform/platform';
|
||||
|
||||
interface ScreenShareSettingsPopoverProps {
|
||||
open: boolean;
|
||||
@@ -217,6 +219,26 @@ export function ScreenShareSettingsPopover({ open, onClose, anchorRef }: ScreenS
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* System Audio */}
|
||||
<div>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<div className="text-[11px] text-txt-tertiary font-semibold uppercase tracking-wider">
|
||||
System Audio
|
||||
</div>
|
||||
{isElectron() && config.shareAudio && (
|
||||
<div className="text-[10px] text-accent-amber/80 mt-0.5">
|
||||
Headphones recommended
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Toggle
|
||||
enabled={config.shareAudio}
|
||||
onChange={(enabled) => setConfig({ shareAudio: enabled })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer — computed stats */}
|
||||
|
||||
+1
-1
@@ -36,7 +36,7 @@ interface BackspaceElectronAPI {
|
||||
|
||||
// Screen share picker coordination
|
||||
onScreenShareSources: (callback: (sources: ElectronScreenSource[]) => void) => void;
|
||||
selectScreenSource: (sourceId: string | null) => void;
|
||||
selectScreenSource: (sourceId: string | null, shareAudio?: boolean) => void;
|
||||
|
||||
// Instance URL management
|
||||
getInstanceUrl: () => Promise<string | null>;
|
||||
|
||||
@@ -10,6 +10,7 @@ export interface ScreenShareConfig {
|
||||
fps: 60 | 45 | 30;
|
||||
mode: 'gaming' | 'text';
|
||||
customBitrateKbps: number | null;
|
||||
shareAudio: boolean;
|
||||
}
|
||||
|
||||
interface VoiceState {
|
||||
@@ -134,7 +135,7 @@ export const useVoiceStore = create<VoiceState>()(
|
||||
inputDeviceId: 'default',
|
||||
outputDeviceId: 'default',
|
||||
focusedParticipantId: null,
|
||||
screenShareConfig: { height: 720, fps: 60, mode: 'gaming', customBitrateKbps: null },
|
||||
screenShareConfig: { height: 720, fps: 60, mode: 'gaming', customBitrateKbps: null, shareAudio: true },
|
||||
participantVolumes: new Map(),
|
||||
setParticipantVolume: (userId, volume) => {
|
||||
set((state) => {
|
||||
@@ -527,7 +528,7 @@ export const useVoiceStore = create<VoiceState>()(
|
||||
}),
|
||||
{
|
||||
name: 'backspace-voice-settings',
|
||||
version: 9,
|
||||
version: 10,
|
||||
migrate: (persistedState: any, version: number) => {
|
||||
if (version === 0) {
|
||||
persistedState.streamAttenuationEnabled = false;
|
||||
@@ -566,6 +567,11 @@ export const useVoiceStore = create<VoiceState>()(
|
||||
if (version < 9) {
|
||||
delete persistedState.currentVoiceChannelId;
|
||||
}
|
||||
if (version < 10) {
|
||||
if (persistedState.screenShareConfig) {
|
||||
persistedState.screenShareConfig.shareAudio = true;
|
||||
}
|
||||
}
|
||||
return persistedState;
|
||||
},
|
||||
storage: createJSONStorage(() => localStorage),
|
||||
|
||||
@@ -131,16 +131,22 @@ export async function applyOverdrive(
|
||||
|
||||
export async function startScreenShare(room: Room): Promise<boolean> {
|
||||
console.log('[SS] startScreenShare called, room state:', room.state);
|
||||
const opts = buildScreenShareOptions(useVoiceStore.getState().screenShareConfig);
|
||||
const config = useVoiceStore.getState().screenShareConfig;
|
||||
const opts = buildScreenShareOptions(config);
|
||||
|
||||
try {
|
||||
const track = await room.localParticipant.setScreenShareEnabled(true, {
|
||||
audio: {
|
||||
audio: config.shareAudio ? {
|
||||
// Chrome 141+: exclude this tab's own audio from system audio capture
|
||||
// Prevents feedback loop where remote voices are captured and echoed back
|
||||
// Silently ignored by older browsers / Electron's Chromium 130
|
||||
// @ts-ignore — restrictOwnAudio is not yet in all TS type definitions
|
||||
restrictOwnAudio: true,
|
||||
echoCancellation: false,
|
||||
noiseSuppression: false,
|
||||
autoGainControl: false,
|
||||
channelCount: 2,
|
||||
},
|
||||
} : false,
|
||||
resolution: { width: opts.capture.width, height: opts.capture.height },
|
||||
// @ts-ignore — LiveKit accepts frameRate at capture level
|
||||
frameRate: opts.capture.frameRate,
|
||||
|
||||
Generated
+34
-22
@@ -15,8 +15,8 @@ importers:
|
||||
version: 6.8.3
|
||||
devDependencies:
|
||||
electron:
|
||||
specifier: ^33.2.0
|
||||
version: 33.4.11
|
||||
specifier: ^40.0.0
|
||||
version: 40.8.2
|
||||
electron-builder:
|
||||
specifier: ^25.1.8
|
||||
version: 25.1.8(electron-builder-squirrel-windows@25.1.8)
|
||||
@@ -165,7 +165,7 @@ importers:
|
||||
version: 18.3.7(@types/react@18.3.28)
|
||||
'@vitejs/plugin-react':
|
||||
specifier: ^4.3.4
|
||||
version: 4.7.0(vite@6.4.1(@types/node@20.19.33)(jiti@1.21.7)(terser@5.46.0)(tsx@4.21.0))
|
||||
version: 4.7.0(vite@6.4.1(@types/node@24.12.0)(jiti@1.21.7)(terser@5.46.0)(tsx@4.21.0))
|
||||
autoprefixer:
|
||||
specifier: ^10.4.20
|
||||
version: 10.4.24(postcss@8.5.6)
|
||||
@@ -183,13 +183,13 @@ importers:
|
||||
version: 5.9.3
|
||||
vite:
|
||||
specifier: ^6.0.3
|
||||
version: 6.4.1(@types/node@20.19.33)(jiti@1.21.7)(terser@5.46.0)(tsx@4.21.0)
|
||||
version: 6.4.1(@types/node@24.12.0)(jiti@1.21.7)(terser@5.46.0)(tsx@4.21.0)
|
||||
vite-plugin-pwa:
|
||||
specifier: ^1.2.0
|
||||
version: 1.2.0(vite@6.4.1(@types/node@20.19.33)(jiti@1.21.7)(terser@5.46.0)(tsx@4.21.0))(workbox-build@7.4.0(@types/babel__core@7.20.5))(workbox-window@7.4.0)
|
||||
version: 1.2.0(vite@6.4.1(@types/node@24.12.0)(jiti@1.21.7)(terser@5.46.0)(tsx@4.21.0))(workbox-build@7.4.0(@types/babel__core@7.20.5))(workbox-window@7.4.0)
|
||||
vitest:
|
||||
specifier: ^4.0.18
|
||||
version: 4.0.18(@types/node@20.19.33)(jiti@1.21.7)(jsdom@28.1.0)(terser@5.46.0)(tsx@4.21.0)
|
||||
version: 4.0.18(@types/node@24.12.0)(jiti@1.21.7)(jsdom@28.1.0)(terser@5.46.0)(tsx@4.21.0)
|
||||
workbox-window:
|
||||
specifier: ^7.4.0
|
||||
version: 7.4.0
|
||||
@@ -1992,6 +1992,9 @@ packages:
|
||||
'@types/node@20.19.33':
|
||||
resolution: {integrity: sha512-Rs1bVAIdBs5gbTIKza/tgpMuG1k3U/UMJLWecIMxNdJFDMzcM5LOiLVRYh3PilWEYDIeUDv7bpiHPLPsbydGcw==}
|
||||
|
||||
'@types/node@24.12.0':
|
||||
resolution: {integrity: sha512-GYDxsZi3ChgmckRT9HPU0WEhKLP08ev/Yfcq2AstjrDASOYCSXeyjDsHg4v5t4jOj7cyDX3vmprafKlWIG9MXQ==}
|
||||
|
||||
'@types/plist@3.0.5':
|
||||
resolution: {integrity: sha512-E6OCaRmAe4WDmWNsL/9RMqdkkzDCY1etutkflWk4c+AcjDU07Pcz1fQwTX0TQz+Pxqn9i4L1TU3UFpjnrcDgxA==}
|
||||
|
||||
@@ -2858,8 +2861,8 @@ packages:
|
||||
electron-updater@6.8.3:
|
||||
resolution: {integrity: sha512-Z6sgw3jgbikWKXei1ENdqFOxBP0WlXg3TtKfz0rgw2vIZFJUyI4pD7ZN7jrkm7EoMK+tcm/qTnPUdqfZukBlBQ==}
|
||||
|
||||
electron@33.4.11:
|
||||
resolution: {integrity: sha512-xmdAs5QWRkInC7TpXGNvzo/7exojubk+72jn1oJL7keNeIlw7xNglf8TGtJtkR4rWC5FJq0oXiIXPS9BcK2Irg==}
|
||||
electron@40.8.2:
|
||||
resolution: {integrity: sha512-EFgQHG0GBO9glpY/x2v4e7xH5uGuoKOQyXeleljlXZeThbjFsNu0NTUyTCVBOkoKT0F5xCwAOAHcI83b3b8jzA==}
|
||||
engines: {node: '>= 12.20.55'}
|
||||
hasBin: true
|
||||
|
||||
@@ -4993,6 +4996,9 @@ packages:
|
||||
undici-types@6.21.0:
|
||||
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
|
||||
|
||||
undici-types@7.16.0:
|
||||
resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==}
|
||||
|
||||
undici@7.22.0:
|
||||
resolution: {integrity: sha512-RqslV2Us5BrllB+JeiZnK4peryVTndy9Dnqq62S3yYRRTj0tFQCwEniUy2167skdGOy3vqRzEvl1Dm4sV2ReDg==}
|
||||
engines: {node: '>=20.18.1'}
|
||||
@@ -7021,6 +7027,10 @@ snapshots:
|
||||
dependencies:
|
||||
undici-types: 6.21.0
|
||||
|
||||
'@types/node@24.12.0':
|
||||
dependencies:
|
||||
undici-types: 7.16.0
|
||||
|
||||
'@types/plist@3.0.5':
|
||||
dependencies:
|
||||
'@types/node': 20.19.33
|
||||
@@ -7066,7 +7076,7 @@ snapshots:
|
||||
|
||||
'@ungap/structured-clone@1.3.0': {}
|
||||
|
||||
'@vitejs/plugin-react@4.7.0(vite@6.4.1(@types/node@20.19.33)(jiti@1.21.7)(terser@5.46.0)(tsx@4.21.0))':
|
||||
'@vitejs/plugin-react@4.7.0(vite@6.4.1(@types/node@24.12.0)(jiti@1.21.7)(terser@5.46.0)(tsx@4.21.0))':
|
||||
dependencies:
|
||||
'@babel/core': 7.29.0
|
||||
'@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.29.0)
|
||||
@@ -7074,7 +7084,7 @@ snapshots:
|
||||
'@rolldown/pluginutils': 1.0.0-beta.27
|
||||
'@types/babel__core': 7.20.5
|
||||
react-refresh: 0.17.0
|
||||
vite: 6.4.1(@types/node@20.19.33)(jiti@1.21.7)(terser@5.46.0)(tsx@4.21.0)
|
||||
vite: 6.4.1(@types/node@24.12.0)(jiti@1.21.7)(terser@5.46.0)(tsx@4.21.0)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
@@ -7087,13 +7097,13 @@ snapshots:
|
||||
chai: 6.2.2
|
||||
tinyrainbow: 3.0.3
|
||||
|
||||
'@vitest/mocker@4.0.18(vite@6.4.1(@types/node@20.19.33)(jiti@1.21.7)(terser@5.46.0)(tsx@4.21.0))':
|
||||
'@vitest/mocker@4.0.18(vite@6.4.1(@types/node@24.12.0)(jiti@1.21.7)(terser@5.46.0)(tsx@4.21.0))':
|
||||
dependencies:
|
||||
'@vitest/spy': 4.0.18
|
||||
estree-walker: 3.0.3
|
||||
magic-string: 0.30.21
|
||||
optionalDependencies:
|
||||
vite: 6.4.1(@types/node@20.19.33)(jiti@1.21.7)(terser@5.46.0)(tsx@4.21.0)
|
||||
vite: 6.4.1(@types/node@24.12.0)(jiti@1.21.7)(terser@5.46.0)(tsx@4.21.0)
|
||||
|
||||
'@vitest/pretty-format@4.0.18':
|
||||
dependencies:
|
||||
@@ -7980,10 +7990,10 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
electron@33.4.11:
|
||||
electron@40.8.2:
|
||||
dependencies:
|
||||
'@electron/get': 2.0.3
|
||||
'@types/node': 20.19.33
|
||||
'@types/node': 24.12.0
|
||||
extract-zip: 2.0.1
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
@@ -10716,6 +10726,8 @@ snapshots:
|
||||
|
||||
undici-types@6.21.0: {}
|
||||
|
||||
undici-types@7.16.0: {}
|
||||
|
||||
undici@7.22.0: {}
|
||||
|
||||
unicode-canonical-property-names-ecmascript@2.0.1: {}
|
||||
@@ -10816,18 +10828,18 @@ snapshots:
|
||||
'@types/unist': 3.0.3
|
||||
vfile-message: 4.0.3
|
||||
|
||||
vite-plugin-pwa@1.2.0(vite@6.4.1(@types/node@20.19.33)(jiti@1.21.7)(terser@5.46.0)(tsx@4.21.0))(workbox-build@7.4.0(@types/babel__core@7.20.5))(workbox-window@7.4.0):
|
||||
vite-plugin-pwa@1.2.0(vite@6.4.1(@types/node@24.12.0)(jiti@1.21.7)(terser@5.46.0)(tsx@4.21.0))(workbox-build@7.4.0(@types/babel__core@7.20.5))(workbox-window@7.4.0):
|
||||
dependencies:
|
||||
debug: 4.4.3
|
||||
pretty-bytes: 6.1.1
|
||||
tinyglobby: 0.2.15
|
||||
vite: 6.4.1(@types/node@20.19.33)(jiti@1.21.7)(terser@5.46.0)(tsx@4.21.0)
|
||||
vite: 6.4.1(@types/node@24.12.0)(jiti@1.21.7)(terser@5.46.0)(tsx@4.21.0)
|
||||
workbox-build: 7.4.0(@types/babel__core@7.20.5)
|
||||
workbox-window: 7.4.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
vite@6.4.1(@types/node@20.19.33)(jiti@1.21.7)(terser@5.46.0)(tsx@4.21.0):
|
||||
vite@6.4.1(@types/node@24.12.0)(jiti@1.21.7)(terser@5.46.0)(tsx@4.21.0):
|
||||
dependencies:
|
||||
esbuild: 0.25.12
|
||||
fdir: 6.5.0(picomatch@4.0.3)
|
||||
@@ -10836,16 +10848,16 @@ snapshots:
|
||||
rollup: 4.57.1
|
||||
tinyglobby: 0.2.15
|
||||
optionalDependencies:
|
||||
'@types/node': 20.19.33
|
||||
'@types/node': 24.12.0
|
||||
fsevents: 2.3.3
|
||||
jiti: 1.21.7
|
||||
terser: 5.46.0
|
||||
tsx: 4.21.0
|
||||
|
||||
vitest@4.0.18(@types/node@20.19.33)(jiti@1.21.7)(jsdom@28.1.0)(terser@5.46.0)(tsx@4.21.0):
|
||||
vitest@4.0.18(@types/node@24.12.0)(jiti@1.21.7)(jsdom@28.1.0)(terser@5.46.0)(tsx@4.21.0):
|
||||
dependencies:
|
||||
'@vitest/expect': 4.0.18
|
||||
'@vitest/mocker': 4.0.18(vite@6.4.1(@types/node@20.19.33)(jiti@1.21.7)(terser@5.46.0)(tsx@4.21.0))
|
||||
'@vitest/mocker': 4.0.18(vite@6.4.1(@types/node@24.12.0)(jiti@1.21.7)(terser@5.46.0)(tsx@4.21.0))
|
||||
'@vitest/pretty-format': 4.0.18
|
||||
'@vitest/runner': 4.0.18
|
||||
'@vitest/snapshot': 4.0.18
|
||||
@@ -10862,10 +10874,10 @@ snapshots:
|
||||
tinyexec: 1.0.2
|
||||
tinyglobby: 0.2.15
|
||||
tinyrainbow: 3.0.3
|
||||
vite: 6.4.1(@types/node@20.19.33)(jiti@1.21.7)(terser@5.46.0)(tsx@4.21.0)
|
||||
vite: 6.4.1(@types/node@24.12.0)(jiti@1.21.7)(terser@5.46.0)(tsx@4.21.0)
|
||||
why-is-node-running: 2.3.0
|
||||
optionalDependencies:
|
||||
'@types/node': 20.19.33
|
||||
'@types/node': 24.12.0
|
||||
jsdom: 28.1.0
|
||||
transitivePeerDependencies:
|
||||
- jiti
|
||||
|
||||
Reference in New Issue
Block a user