From 2d451dc1f0c31901b2af5517d7d8ef96ad94b3a4 Mon Sep 17 00:00:00 2001 From: hugo Date: Sun, 22 Feb 2026 18:17:30 +0100 Subject: [PATCH] feat: dist building --- README.md | 20 ++++- package-lock.json | 108 +++++++++++++++------------ package.json | 45 ++++++++++- scripts/notarize.mjs | 30 ++++++++ tests/engine/PackagingConfig.test.ts | 45 +++++++++++ 5 files changed, 195 insertions(+), 53 deletions(-) create mode 100644 scripts/notarize.mjs create mode 100644 tests/engine/PackagingConfig.test.ts diff --git a/README.md b/README.md index b4692ed..a155ded 100644 --- a/README.md +++ b/README.md @@ -120,10 +120,26 @@ npm run test:coverage # Generates drizzle artifacts, builds main process and renderer npm run build -# Package distributables -npx electron-builder +# Package app directory only (no installer) +npm run package + +# Package distributables for all supported systems +npm run dist + +# Package by target platform +npm run dist:mac +npm run dist:win +npm run dist:linux ``` +### Distribution Notes + +- Custom protocol `bds://` is declared in electron-builder metadata for packaged apps. +- macOS signing/notarization is scaffolded via `scripts/notarize.mjs` and runs when these env vars are set: + - `APPLE_ID` + - `APPLE_APP_SPECIFIC_PASSWORD` + - `APPLE_TEAM_ID` + ### Database Utilities ```bash diff --git a/package-lock.json b/package-lock.json index 0263828..5bff9a4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -45,11 +45,13 @@ "simple-git": "^3.31.1", "snowball-stemmers": "^0.6.0", "turndown": "^7.2.2", + "uuid": "^13.0.0", "vanilla-calendar-pro": "^3.1.0", "zod": "^4.3.6", "zustand": "^5.0.11" }, "devDependencies": { + "@electron/notarize": "^3.1.0", "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.2", "@testing-library/user-event": "^14.6.1", @@ -75,7 +77,6 @@ "memfs": "^4.6.0", "tsx": "^4.6.0", "typescript": "^5.3.0", - "uuid": "^13.0.0", "vite": "^7.3.1", "vitest": "^4.0.18", "wait-on": "^9.0.3" @@ -1134,57 +1135,17 @@ } }, "node_modules/@electron/notarize": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@electron/notarize/-/notarize-2.5.0.tgz", - "integrity": "sha512-jNT8nwH1f9X5GEITXaQ8IF/KdskvIkOFfB2CvwumsveVidzpSc+mvhhTMdAGSYF3O+Nq49lJ7y+ssODRXu06+A==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@electron/notarize/-/notarize-3.1.1.tgz", + "integrity": "sha512-uQQSlOiJnqRkTL1wlEBAxe90nVN/Fc/hEmk0bqpKk8nKjV1if/tXLHKUPePtv9Xsx90PtZU8aidx5lAiOpjkQQ==", "dev": true, "license": "MIT", "dependencies": { - "debug": "^4.1.1", - "fs-extra": "^9.0.1", + "debug": "^4.4.0", "promise-retry": "^2.0.1" }, "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/@electron/notarize/node_modules/fs-extra": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "at-least-node": "^1.0.0", - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@electron/notarize/node_modules/jsonfile": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", - "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", - "dev": true, - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/@electron/notarize/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.0.0" + "node": ">= 22.12.0" } }, "node_modules/@electron/osx-sign": { @@ -6350,6 +6311,60 @@ "semver": "bin/semver.js" } }, + "node_modules/app-builder-lib/node_modules/@electron/notarize": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@electron/notarize/-/notarize-2.5.0.tgz", + "integrity": "sha512-jNT8nwH1f9X5GEITXaQ8IF/KdskvIkOFfB2CvwumsveVidzpSc+mvhhTMdAGSYF3O+Nq49lJ7y+ssODRXu06+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "fs-extra": "^9.0.1", + "promise-retry": "^2.0.1" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/app-builder-lib/node_modules/@electron/notarize/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/app-builder-lib/node_modules/@electron/notarize/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/app-builder-lib/node_modules/@electron/notarize/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/app-builder-lib/node_modules/ci-info": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.1.tgz", @@ -14913,7 +14928,6 @@ "version": "13.0.0", "resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz", "integrity": "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==", - "dev": true, "funding": [ "https://github.com/sponsors/broofa", "https://github.com/sponsors/ctavan" diff --git a/package.json b/package.json index e755127..1382b85 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,11 @@ "start": "concurrently --kill-others \"npm run dev:renderer\" \"npm run start:electron\"", "start:electron": "wait-on http://localhost:5173 && cross-env NODE_ENV=development node ./node_modules/electron/cli.js .", "build": "npm run lint && npm run db:generate && npm run build:main && npm run build:renderer", + "package": "npm run build && electron-builder --dir", + "dist": "npm run build && electron-builder", + "dist:mac": "npm run build && electron-builder --mac", + "dist:win": "npm run build && electron-builder --win", + "dist:linux": "npm run build && electron-builder --linux", "build:main": "node ./node_modules/typescript/bin/tsc -p tsconfig.main.json", "build:renderer": "node ./node_modules/vite/bin/vite.js build", "start:prod": "node ./node_modules/electron/cli.js .", @@ -29,6 +34,7 @@ "author": "", "license": "MIT", "devDependencies": { + "@electron/notarize": "^3.1.0", "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.2", "@testing-library/user-event": "^14.6.1", @@ -54,7 +60,6 @@ "memfs": "^4.6.0", "tsx": "^4.6.0", "typescript": "^5.3.0", - "uuid": "^13.0.0", "vite": "^7.3.1", "vitest": "^4.0.18", "wait-on": "^9.0.3" @@ -96,6 +101,7 @@ "simple-git": "^3.31.1", "snowball-stemmers": "^0.6.0", "turndown": "^7.2.2", + "uuid": "^13.0.0", "vanilla-calendar-pro": "^3.1.0", "zod": "^4.3.6", "zustand": "^5.0.11" @@ -108,6 +114,8 @@ "build": { "appId": "com.bds.blogging-desktop-server", "productName": "Blogging Desktop Server", + "asar": true, + "afterSign": "scripts/notarize.mjs", "directories": { "output": "release" }, @@ -122,14 +130,43 @@ "to": "drizzle" } ], + "protocols": [ + { + "name": "bDS Blogmark Links", + "schemes": [ + "bds" + ] + } + ], "win": { - "target": "nsis" + "target": [ + { + "target": "nsis", + "arch": [ + "x64", + "arm64" + ] + } + ] }, "mac": { - "target": "dmg" + "target": [ + "dmg", + "zip" + ], + "category": "public.app-category.productivity", + "hardenedRuntime": true, + "gatekeeperAssess": false, + "entitlements": "build/entitlements.mac.plist", + "entitlementsInherit": "build/entitlements.mac.inherit.plist" }, "linux": { - "target": "AppImage" + "target": [ + "AppImage", + "deb", + "rpm" + ], + "category": "Utility" } } } diff --git a/scripts/notarize.mjs b/scripts/notarize.mjs new file mode 100644 index 0000000..415e1d4 --- /dev/null +++ b/scripts/notarize.mjs @@ -0,0 +1,30 @@ +import { notarize } from '@electron/notarize'; + +export default async function notarizeIfConfigured(context) { + const { electronPlatformName, appOutDir, packager } = context; + + if (electronPlatformName !== 'darwin') { + return; + } + + const appleId = process.env.APPLE_ID; + const appleIdPassword = process.env.APPLE_APP_SPECIFIC_PASSWORD; + const teamId = process.env.APPLE_TEAM_ID; + + if (!appleId || !appleIdPassword || !teamId) { + console.log('[notarize] Skipped: APPLE_ID / APPLE_APP_SPECIFIC_PASSWORD / APPLE_TEAM_ID not set'); + return; + } + + const appName = packager.appInfo.productFilename; + const appPath = `${appOutDir}/${appName}.app`; + + console.log(`[notarize] Submitting ${appPath}`); + await notarize({ + appPath, + appleId, + appleIdPassword, + teamId, + }); + console.log('[notarize] Completed'); +} diff --git a/tests/engine/PackagingConfig.test.ts b/tests/engine/PackagingConfig.test.ts new file mode 100644 index 0000000..aafec1d --- /dev/null +++ b/tests/engine/PackagingConfig.test.ts @@ -0,0 +1,45 @@ +import { describe, expect, it } from 'vitest'; +import fs from 'fs'; +import path from 'path'; + +describe('package.json packaging configuration', () => { + const packageJsonPath = path.resolve(process.cwd(), 'package.json'); + const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')) as Record; + + it('defines cross-platform distribution scripts', () => { + const scripts = packageJson.scripts as Record; + + expect(scripts['dist']).toBeTypeOf('string'); + expect(scripts['dist:mac']).toBeTypeOf('string'); + expect(scripts['dist:win']).toBeTypeOf('string'); + expect(scripts['dist:linux']).toBeTypeOf('string'); + }); + + it('registers bds protocol for packaged apps', () => { + const build = packageJson.build as Record; + const protocols = build.protocols as Array<{ name: string; schemes: string[] }>; + + expect(Array.isArray(protocols)).toBe(true); + expect(protocols.some((entry) => Array.isArray(entry.schemes) && entry.schemes.includes('bds'))).toBe(true); + }); + + it('configures macOS, Windows, and Linux targets', () => { + const build = packageJson.build as Record; + + expect(build.mac).toBeTruthy(); + expect(build.win).toBeTruthy(); + expect(build.linux).toBeTruthy(); + + expect(build.mac.target).toBeTruthy(); + expect(build.win.target).toBeTruthy(); + expect(build.linux.target).toBeTruthy(); + }); + + it('keeps runtime modules in dependencies (not devDependencies)', () => { + const dependencies = packageJson.dependencies as Record; + const devDependencies = packageJson.devDependencies as Record; + + expect(dependencies.uuid).toBeTypeOf('string'); + expect(devDependencies.uuid).toBeUndefined(); + }); +});