Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions apps/desktop/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@zennotes/desktop",
"productName": "ZenNotes",
"version": "2.39.0",
"version": "2.40.0",
"description": "ZenNotes desktop shell",
"private": true,
"main": "./out/main/index.js",
Expand Down Expand Up @@ -96,15 +96,15 @@
"@types/ws": "^8.18.1",
"@vitejs/plugin-react": "^4.3.4",
"autoprefixer": "^10.4.20",
"electron": "41.2.1",
"electron-builder": "^25.1.8",
"electron-vite": "^2.3.0",
"electron": "41.10.7",
"electron-builder": "26.15.7",
"electron-vite": "^3.1.0",
"jsdom": "^29.0.2",
"postcss": "^8.5.10",
"tailwindcss": "^3.4.17",
"typescript": "^5.7.2",
"vite": "^5.4.11",
"vitest": "^2.1.8"
"vite": "^6.4.3",
"vitest": "^3.2.6"
},
"build": {
"appId": "com.adibhanna.zennotes",
Expand Down Expand Up @@ -200,7 +200,7 @@
"mimeType": "text/markdown"
}
],
"electronVersion": "41.2.1",
"electronVersion": "41.10.7",
"electronUpdaterCompatibility": ">=2.16",
"mac": {
"icon": "build/icon.icns",
Expand Down
40 changes: 40 additions & 0 deletions apps/desktop/patches/app-builder-lib+26.15.7.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
diff --git a/node_modules/app-builder-lib/out/node-module-collector/nodeModulesCollector.js b/node_modules/app-builder-lib/out/node-module-collector/nodeModulesCollector.js
index 31798e4..15bb8ce 100644
--- a/node_modules/app-builder-lib/out/node-module-collector/nodeModulesCollector.js
+++ b/node_modules/app-builder-lib/out/node-module-collector/nodeModulesCollector.js
@@ -238,17 +238,22 @@ class NodeModulesCollector {
const deps = (obj[key] || {}).dependencies || [];
for (const dep of deps) {
const child = this.transformToHoisterTree(obj, dep, nodes);
- node.dependencies.add(child);
+ if (child !== node) {
+ node.dependencies.add(child);
+ }
}
}
return node;
}
- async _getNodeModules(dependencies, result) {
+ async _getNodeModules(dependencies, result, ancestors = new Set()) {
var _a;
if (dependencies.size === 0) {
return;
}
for (const d of dependencies.values()) {
+ if (ancestors.has(d)) {
+ continue;
+ }
const reference = [...d.references][0];
const key = `${d.name}@${reference}`;
// Normalize the path to handle mixed separators from pnpm JSON output on Windows
@@ -272,7 +277,9 @@ class NodeModulesCollector {
result.push(node);
if (d.dependencies.size > 0) {
node.dependencies = [];
- await this._getNodeModules(d.dependencies, node.dependencies);
+ ancestors.add(d);
+ await this._getNodeModules(d.dependencies, node.dependencies, ancestors);
+ ancestors.delete(d);
}
}
result.sort((a, b) => a.name.localeCompare(b.name));
23 changes: 23 additions & 0 deletions apps/desktop/src/main/packaging.test.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,37 @@
import { readFileSync } from 'node:fs'
import { createRequire } from 'node:module'
import { describe, expect, it } from 'vitest'
import { PACKAGED_CLI_RUNTIME_PACKAGES } from '../../electron.vite.config'
import desktopPackage from '../../package.json'

const require = createRequire(import.meta.url)

interface ExtraResource {
from: string
to: string
filter?: string[]
}

describe('desktop packaging', () => {
it('uses Electron and electron-builder releases with the current security fixes', () => {
expect(desktopPackage.devDependencies.electron).toBe('41.10.7')
expect(desktopPackage.build.electronVersion).toBe('41.10.7')
expect(desktopPackage.devDependencies['electron-builder']).toBe('26.15.7')
})

it('applies the merged upstream cycle guard to electron-builder dependency collection', () => {
// 26.15.7 includes the AppImage security fixes, but its new module
// collector can recurse until OOM on cyclic package graphs. This is the
// exact guard merged upstream in electron-builder#10070; remove the patch
// after the first v26 release containing that change.
const collector = readFileSync(
require.resolve('app-builder-lib/out/node-module-collector/nodeModulesCollector.js'),
'utf8'
)
expect(collector).toContain('if (child !== node)')
expect(collector).toContain('if (ancestors.has(d))')
})

it('ships the CLI chunks beside the unpacked CLI launcher', () => {
const resources = desktopPackage.build.extraResources as ExtraResource[]

Expand Down
185 changes: 182 additions & 3 deletions apps/desktop/src/main/updater.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { describe, expect, it, vi } from 'vitest'
import { afterEach, describe, expect, it, vi } from 'vitest'

// updater.ts imports electron and electron-updater at module load. Stub both so
// we can unit-test the pure Linux-install helpers without an Electron runtime.
Expand All @@ -9,14 +9,29 @@ vi.mock('electron', () => ({
shell: {}
}))
vi.mock('electron-updater', () => ({
default: { autoUpdater: {} }
default: {
autoUpdater: {},
AppImageUpdater: class {},
DebUpdater: class {},
RpmUpdater: class {},
PacmanUpdater: class {}
}
}))

import FpmTarget from 'app-builder-lib/out/targets/FpmTarget'
import electronUpdater from 'electron-updater'
import {
elevatedInstallScript,
installedLinuxFormat,
isOfficialLinuxSystemPackage,
linuxFormatFromOsRelease,
linuxInstallMismatch,
linuxNeedsRootInstall,
linuxPackageFormat,
manualInstallHint
linuxUpdaterForFormat,
linuxUpdaterFormat,
manualInstallHint,
mismatchedUpdateMessage
} from './updater'

describe('linuxPackageFormat', () => {
Expand Down Expand Up @@ -69,3 +84,167 @@ describe('manualInstallHint', () => {
expect(manualInstallHint('rpm', '/tmp/a.rpm')).toBe('sudo rpm -U "/tmp/a.rpm"')
})
})

describe('linuxFormatFromOsRelease', () => {
it('reads the distro family, derivatives included', () => {
expect(linuxFormatFromOsRelease('ID=arch\n')).toBe('pacman')
expect(linuxFormatFromOsRelease('ID=ubuntu\nID_LIKE=debian\n')).toBe('deb')
expect(linuxFormatFromOsRelease('ID=fedora\nVERSION_ID=42\n')).toBe('rpm')
// The reporter's distro: unknown by name, but it declares its ancestor.
expect(linuxFormatFromOsRelease('NAME="CachyOS Linux"\nID=cachyos\nID_LIKE=arch\n')).toBe(
'pacman'
)
expect(linuxFormatFromOsRelease('ID=neon\nID_LIKE="ubuntu debian"\n')).toBe('deb')
})

it('tolerates quotes, spacing and files it cannot place', () => {
expect(linuxFormatFromOsRelease('ID = "manjaro"')).toBe('pacman')
expect(linuxFormatFromOsRelease('ID=nixos\nID_LIKE=\n')).toBe('unknown')
expect(linuxFormatFromOsRelease('')).toBe('unknown')
})

it('gives ID priority over ID_LIKE regardless of declaration order', () => {
expect(linuxFormatFromOsRelease('ID_LIKE=debian\nID=arch\n')).toBe('pacman')
})
})

describe('installedLinuxFormat', () => {
afterEach(() => {
delete process.env.APPIMAGE
})

it('trusts the AppImage env var before anything on disk', () => {
process.env.APPIMAGE = '/home/kelv/Apps/ZenNotes.AppImage'
expect(
installedLinuxFormat(() => {
throw new Error('must not be read')
})
).toBe('appimage')
})

it('falls back to os-release, and to unknown when it cannot be read', () => {
expect(installedLinuxFormat(() => 'ID=arch\n')).toBe('pacman')
expect(
installedLinuxFormat(() => {
throw new Error('ENOENT')
})
).toBe('unknown')
})
})

describe('linuxInstallMismatch', () => {
it('catches the Arch-gets-a-deb case that shipped', () => {
expect(linuxInstallMismatch('deb', 'pacman')).toBe(true)
expect(linuxInstallMismatch('rpm', 'deb')).toBe(true)
})

it('never blocks a match, or a system it could not identify', () => {
expect(linuxInstallMismatch('deb', 'deb')).toBe(false)
expect(linuxInstallMismatch('pacman', 'pacman')).toBe(false)
expect(linuxInstallMismatch('deb', 'unknown')).toBe(false)
expect(linuxInstallMismatch('unknown', 'pacman')).toBe(false)
})
})

describe('mismatchedUpdateMessage', () => {
it('names both formats and points at the right download', () => {
const message = mismatchedUpdateMessage('deb', 'pacman', '2.40.0')
expect(message).toContain('ZenNotes 2.40.0')
expect(message).toContain('.deb package')
expect(message).toContain('https://zennotes.org/download/linux-pacman')
})
})

describe('linuxUpdaterFormat', () => {
const arch = 'NAME="CachyOS Linux"\nID=cachyos\nID_LIKE=arch\n'

it('sends an Arch system package to the pacman updater, whatever the stamp said', () => {
// The shipped case: the .pacman carried a `deb` stamp, so electron-updater
// had picked the deb updater. The stamp's value is never consulted here.
expect(
linuxUpdaterFormat({ isAppImage: false, isOfficialSystemPackage: true, osRelease: arch })
).toBe('pacman')
expect(
linuxUpdaterFormat({
isAppImage: false,
isOfficialSystemPackage: true,
osRelease: 'ID=ubuntu\nID_LIKE=debian\n'
})
).toBe('deb')
expect(
linuxUpdaterFormat({
isAppImage: false,
isOfficialSystemPackage: true,
osRelease: 'ID=fedora\n'
})
).toBe('rpm')
})

it('leaves an AppImage alone even where a stamp leaked into it', () => {
expect(
linuxUpdaterFormat({ isAppImage: true, isOfficialSystemPackage: false, osRelease: arch })
).toBe('appimage')
})

it('forces the safe AppImage updater for AUR and tar installs even if the stamp leaked', () => {
expect(
linuxUpdaterFormat({
isAppImage: false,
isOfficialSystemPackage: false,
osRelease: arch
})
).toBe('appimage')
})

it('stays with the default updater when the distro cannot be identified', () => {
expect(
linuxUpdaterFormat({ isAppImage: false, isOfficialSystemPackage: true, osRelease: null })
).toBe('unknown')
expect(
linuxUpdaterFormat({
isAppImage: false,
isOfficialSystemPackage: true,
osRelease: 'ID=nixos\n'
})
).toBe('unknown')
})
})

describe('isOfficialLinuxSystemPackage', () => {
it('accepts only a stamped electron-builder system-package install', () => {
expect(isOfficialLinuxSystemPackage('/opt/ZenNotes/resources', true)).toBe(true)
expect(isOfficialLinuxSystemPackage('/opt/ZenNotes/resources', false)).toBe(false)
})

it('rejects AUR and tar installs even if a racing target leaked the stamp', () => {
expect(isOfficialLinuxSystemPackage('/opt/zennotes-bin/resources', true)).toBe(false)
expect(
isOfficialLinuxSystemPackage('/tmp/ZenNotes-2.40.0-linux-x64/resources', true)
).toBe(false)
})
})

describe('linuxUpdaterForFormat', () => {
it('creates a fresh AppImage updater instead of reusing the stamp-derived singleton', () => {
const selected = linuxUpdaterForFormat('appimage')
expect(selected).toBeInstanceOf(electronUpdater.AppImageUpdater)
expect(selected).not.toBe(electronUpdater.autoUpdater)
})

it('keeps the existing updater only when the system package format is unknown', () => {
expect(linuxUpdaterForFormat('unknown')).toBe(electronUpdater.autoUpdater)
})
})

describe('Linux updater build support', () => {
// 25.x omits pacman metadata. The pinned 26.15.7 has the current AppImage
// security fixes; apps/desktop/patches carries the cycle guard already
// merged upstream for its module collector. Upgrade only after this
// assertion and an electron-builder --dir package check pass.
it('emits pacman packages into latest-linux.yml', () => {
const supportsAutoUpdate = Reflect.get(FpmTarget.prototype, 'supportsAutoUpdate') as (
target: string
) => boolean
expect(supportsAutoUpdate.call(Object.create(FpmTarget.prototype), 'pacman')).toBe(true)
})
})
Loading