fix(desktop): make get-windows optional dep so Linux build doesn't break (#85377)

(cherry picked from commit f25e3467e6a28911067b416bf6647c8c0b3254a6)
This commit is contained in:
webtecnica
2026-08-13 13:09:32 -03:00
committed by brooklyn!
parent 5269c8f69b
commit 58e4bb04d6
6 changed files with 111 additions and 14 deletions

23
apps/desktop/electron/get-windows.d.ts vendored Normal file
View File

@@ -0,0 +1,23 @@
// Type declarations for the get-windows optionalDependency.
//
// get-windows ships no bundled types and is an optionalDependency: on Linux
// `npm ci` skips it when its node-pre-gyp install script fails (no Linux
// prebuilt; the node-gyp fallback needs `gyp` in the active Python), so the
// package is legitimately absent from node_modules on Linux builds. Declaring
// the module here keeps the typecheck independent of whether the package
// installed — the runtime import in window-below.ts degrades to null when it
// is absent.
declare module 'get-windows' {
export interface GetWindowsWindow {
bounds?: { height?: number; width?: number; x?: number; y?: number }
id?: number
owner?: { name?: string; processId?: number }
title?: string
}
export function openWindows(options?: {
accessibilityPermission?: boolean
screenRecordingPermission?: boolean
}): Promise<GetWindowsWindow[]>
}

View File

@@ -122,10 +122,15 @@ type GetWindowsModule = {
>
}
let getWindowsModule: Promise<GetWindowsModule> | null = null
let getWindowsModule: Promise<GetWindowsModule | null> | null = null
const loadGetWindows = (): Promise<GetWindowsModule> => {
getWindowsModule ??= import('get-windows')
const loadGetWindows = (): Promise<GetWindowsModule | null> => {
// get-windows is an optionalDependency: on Linux `npm ci` skips it when its
// node-pre-gyp install script fails (no Linux prebuilt, and the node-gyp
// fallback needs `gyp` in the active Python). A missing module is therefore
// a normal state, so the lazy import resolves to null instead of rejecting;
// enumeration then degrades to the failure note instead of an uncaught error.
getWindowsModule ??= import('get-windows').catch(() => null)
return getWindowsModule
}
@@ -143,7 +148,13 @@ async function enumerateViaGetWindows(titlesAvailable: boolean): Promise<Enumera
let raw
try {
const { openWindows } = await loadGetWindows()
const getWindows = await loadGetWindows()
if (!getWindows) {
return null
}
const { openWindows } = getWindows
raw = await openWindows(
process.platform === 'darwin'
? { accessibilityPermission: false, screenRecordingPermission: titlesAvailable }

View File

@@ -114,7 +114,6 @@
"emojibase-data": "16.0.3",
"fflate": "0.8.3",
"frimousse": "0.3.0",
"get-windows": "9.3.0",
"hast-util-from-html-isomorphic": "2.0.0",
"hast-util-to-text": "4.0.2",
"ignore": "7.0.6",
@@ -145,6 +144,9 @@
"vfile": "6.0.3",
"web-haptics": "0.0.6"
},
"optionalDependencies": {
"get-windows": "9.3.0"
},
"devDependencies": {
"@electron/rebuild": "4.2.0",
"@playwright/test": "1.58.2",

View File

@@ -407,12 +407,20 @@ export function openWindowsSync() {
`
function resolveGetWindowsRoot() {
// get-windows' exports map doesn't expose ./package.json; resolve the entry
// (index.js sits at the package root) and take its directory.
const entryPath = require.resolve('get-windows', {
paths: [projectRoot]
})
return dirname(entryPath)
// get-windows is an optionalDependency (its node-pre-gyp install script has
// no Linux prebuilt and the node-gyp fallback needs `gyp` in the active
// Python, so `npm ci` may skip it entirely on Linux). Return null when it is
// absent; the caller decides whether that is fatal per platform.
try {
// get-windows' exports map doesn't expose ./package.json; resolve the entry
// (index.js sits at the package root) and take its directory.
const entryPath = require.resolve('get-windows', {
paths: [projectRoot]
})
return dirname(entryPath)
} catch {
return null
}
}
/**
@@ -537,9 +545,32 @@ function rebuildGetWindowsViaNpm() {
}
}
export function stageGetWindows({ platform = process.platform } = {}) {
const srcRoot = resolveGetWindowsRoot()
export function stageGetWindows(
{ platform = process.platform, resolveRoot = resolveGetWindowsRoot } = {}
) {
const srcRoot = resolveRoot()
const destRoot = resolve(projectRoot, 'dist/node_modules/get-windows')
if (!srcRoot) {
// get-windows is an optionalDependency: `npm ci` on Linux skips it when the
// install script fails (no Linux prebuilt, node-gyp needs `gyp` in the
// active Python), so absence here is a normal state, not a broken checkout.
// On Linux it only backs read_window_below over X11 (lib/linux.js shells
// out to xprop); skipping it degrades that tool to Hyprland IPC or an
// "unavailable" note instead of failing the whole desktop build. On
// darwin/win32 the native payload is required, so fail loudly.
if (platform === 'linux') {
console.warn(
'[stage-native-deps] get-windows not installed (optional dep skipped on ' +
'Linux); read_window_below will be unavailable in this build'
)
return undefined
}
throw new Error(
`[stage-native-deps] get-windows is not installed; cannot stage its ${platform} native payload`
)
}
// Only a win32 host can produce the win32 binding, so a cross-platform pack
// has nothing to gain from the rebuild.
const rebuild =

View File

@@ -6,6 +6,7 @@ import { pathToFileURL } from 'node:url'
import { test } from 'vitest'
import {
stageGetWindows,
stageGetWindowsInto,
stageNodePtyInto,
classifyNativeBinary
@@ -519,3 +520,29 @@ test('darwin staging ships the Swift helper executable and the rewritten windows
fs.rmSync(tmp, { recursive: true, force: true })
}
})
// ─── stageGetWindows (optionalDependency gate) ──────────────────────
//
// get-windows is an optionalDependency: on Linux its node-pre-gyp install
// script fails (no Linux prebuilt; the node-gyp fallback needs `gyp` in the
// active Python), so npm skips the package and `npm ci` must still succeed.
// The staging step mirrors that: absent package on Linux is a skip, on
// darwin/win32 it is a hard failure because the native payload is required.
test('linux staging skips when get-windows is absent (optional dep skipped by npm)', () => {
assert.equal(stageGetWindows({ platform: 'linux', resolveRoot: () => null }), undefined)
})
test('darwin staging fails when get-windows is absent', () => {
assert.throws(
() => stageGetWindows({ platform: 'darwin', resolveRoot: () => null }),
/get-windows is not installed/
)
})
test('win32 staging fails when get-windows is absent', () => {
assert.throws(
() => stageGetWindows({ platform: 'win32', resolveRoot: () => null }),
/get-windows is not installed/
)
})

5
package-lock.json generated
View File

@@ -108,7 +108,6 @@
"emojibase-data": "16.0.3",
"fflate": "0.8.3",
"frimousse": "0.3.0",
"get-windows": "9.3.0",
"hast-util-from-html-isomorphic": "2.0.0",
"hast-util-to-text": "4.0.2",
"ignore": "7.0.6",
@@ -167,6 +166,9 @@
},
"engines": {
"node": ">=22.22.0"
},
"optionalDependencies": {
"get-windows": "9.3.0"
}
},
"apps/desktop/node_modules/@electron/get": {
@@ -11004,6 +11006,7 @@
"integrity": "sha512-DrOfQSmIcsFax28FfSUjbLTfeOkAG7yeh6NCb/9zzRkDuClXaqYqHuQBPUqcqd1uYS70ygYERSooacMAwvbyVw==",
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"engines": {
"node": ">=18.18"
},