Skip to content

Commit d05bf80

Browse files
authored
chore: add packaging configuration (#30)
* chore: add packaging configuration
1 parent 2aa46ae commit d05bf80

File tree

8 files changed

+539
-20
lines changed

8 files changed

+539
-20
lines changed

apps/desktop/.npmrc

Lines changed: 0 additions & 1 deletion
This file was deleted.

apps/desktop/forge.config.ts

Lines changed: 247 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,270 @@
11
import type { ForgeConfig } from '@electron-forge/shared-types';
22
import { MakerSquirrel } from '@electron-forge/maker-squirrel';
3-
import { MakerZIP } from '@electron-forge/maker-zip';
3+
import { MakerDMG } from '@electron-forge/maker-dmg';
44
import { MakerDeb } from '@electron-forge/maker-deb';
55
import { MakerRpm } from '@electron-forge/maker-rpm';
66
import { VitePlugin } from '@electron-forge/plugin-vite';
77
import { FusesPlugin } from '@electron-forge/plugin-fuses';
88
import { FuseV1Options, FuseVersion } from '@electron/fuses';
9+
import { readdirSync, rmdirSync, statSync, existsSync, mkdirSync, cpSync } from 'node:fs';
10+
import { join, normalize } from 'node:path';
11+
// Use flora-colossus for finding all dependencies of EXTERNAL_DEPENDENCIES
12+
// flora-colossus is maintained by MarshallOfSound (a top electron-forge contributor)
13+
// already included as a dependency of electron-packager/galactus (so we do NOT have to add it to package.json)
14+
// grabs nested dependencies from tree
15+
import { Walker, DepType, type Module } from 'flora-colossus';
16+
17+
let nativeModuleDependenciesToPackage: string[] = [];
18+
19+
export const EXTERNAL_DEPENDENCIES = [
20+
'electron-squirrel-startup',
21+
'smart-whisper',
22+
'@libsql/client',
23+
'@libsql/darwin-arm64',
24+
'@libsql/darwin-x64',
25+
'@libsql/linux-x64-gnu',
26+
'@libsql/linux-x64-musl',
27+
'@libsql/win32-x64-msvc',
28+
'libsql',
29+
// Add any other native modules you need here
30+
];
931

1032
const config: ForgeConfig = {
33+
hooks: {
34+
prePackage: async () => {
35+
console.error('prePackage');
36+
const projectRoot = normalize(__dirname);
37+
// In a monorepo, node_modules are typically at the root level
38+
const monorepoRoot = join(projectRoot, '../../'); // Go up to monorepo root
39+
40+
const getExternalNestedDependencies = async (
41+
nodeModuleNames: string[],
42+
includeNestedDeps = true
43+
) => {
44+
const foundModules = new Set(nodeModuleNames);
45+
if (includeNestedDeps) {
46+
for (const external of nodeModuleNames) {
47+
type MyPublicClass<T> = {
48+
[P in keyof T]: T[P];
49+
};
50+
type MyPublicWalker = MyPublicClass<Walker> & {
51+
modules: Module[];
52+
walkDependenciesForModule: (
53+
moduleRoot: string,
54+
depType: DepType
55+
) => Promise<void>;
56+
};
57+
const moduleRoot = join(monorepoRoot, 'node_modules', external);
58+
console.log('moduleRoot', moduleRoot);
59+
// Initialize Walker with monorepo root as base path
60+
const walker = new Walker(monorepoRoot) as unknown as MyPublicWalker;
61+
walker.modules = [];
62+
await walker.walkDependenciesForModule(moduleRoot, DepType.PROD);
63+
walker.modules
64+
.filter((dep) => (dep.nativeModuleType as number) === DepType.PROD)
65+
// Remove the problematic name splitting that breaks scoped packages
66+
.map((dep) => dep.name)
67+
.forEach((name) => foundModules.add(name));
68+
}
69+
}
70+
return foundModules;
71+
};
72+
73+
const nativeModuleDependencies = await getExternalNestedDependencies(EXTERNAL_DEPENDENCIES);
74+
nativeModuleDependenciesToPackage = Array.from(nativeModuleDependencies);
75+
76+
// Copy external dependencies to local node_modules
77+
console.error('Copying external dependencies to local node_modules');
78+
const localNodeModules = join(projectRoot, 'node_modules');
79+
const rootNodeModules = join(monorepoRoot, 'node_modules');
80+
81+
// Ensure local node_modules directory exists
82+
if (!existsSync(localNodeModules)) {
83+
mkdirSync(localNodeModules, { recursive: true });
84+
}
85+
86+
console.log(`Found ${nativeModuleDependenciesToPackage.length} dependencies to copy`);
87+
88+
// Copy all required dependencies
89+
for (const dep of nativeModuleDependenciesToPackage) {
90+
const rootDepPath = join(rootNodeModules, dep);
91+
const localDepPath = join(localNodeModules, dep);
92+
93+
try {
94+
// Skip if source doesn't exist
95+
if (!existsSync(rootDepPath)) {
96+
console.log(`Skipping ${dep}: not found in root node_modules`);
97+
continue;
98+
}
99+
100+
// Skip if target already exists (don't override)
101+
if (existsSync(localDepPath)) {
102+
console.log(`Skipping ${dep}: already exists locally`);
103+
continue;
104+
}
105+
106+
// Copy the package
107+
console.log(`Copying ${dep}...`);
108+
cpSync(rootDepPath, localDepPath, { recursive: true });
109+
console.log(`✓ Successfully copied ${dep}`);
110+
111+
} catch (error) {
112+
console.error(`Failed to copy ${dep}:`, error);
113+
}
114+
}
115+
},
116+
packageAfterPrune: async (_forgeConfig, buildPath) => {
117+
try {
118+
function getItemsFromFolder(
119+
path: string,
120+
totalCollection: {
121+
path: string;
122+
type: 'directory' | 'file';
123+
empty: boolean;
124+
}[] = []
125+
) {
126+
try {
127+
const normalizedPath = normalize(path);
128+
const childItems = readdirSync(normalizedPath);
129+
const getItemStats = statSync(normalizedPath);
130+
if (getItemStats.isDirectory()) {
131+
totalCollection.push({
132+
path: normalizedPath,
133+
type: 'directory',
134+
empty: childItems.length === 0,
135+
});
136+
}
137+
childItems.forEach((childItem) => {
138+
const childItemNormalizedPath = join(normalizedPath, childItem);
139+
const childItemStats = statSync(childItemNormalizedPath);
140+
if (childItemStats.isDirectory()) {
141+
getItemsFromFolder(childItemNormalizedPath, totalCollection);
142+
} else {
143+
totalCollection.push({
144+
path: childItemNormalizedPath,
145+
type: 'file',
146+
empty: false,
147+
});
148+
}
149+
});
150+
} catch {
151+
return;
152+
}
153+
return totalCollection;
154+
}
155+
const getItems = getItemsFromFolder(buildPath) ?? [];
156+
for (const item of getItems) {
157+
const DELETE_EMPTY_DIRECTORIES = true;
158+
if (item.empty === true) {
159+
if (DELETE_EMPTY_DIRECTORIES) {
160+
const pathToDelete = normalize(item.path);
161+
// one last check to make sure it is a directory and is empty
162+
const stats = statSync(pathToDelete);
163+
if (!stats.isDirectory()) {
164+
// SKIPPING DELETION: pathToDelete is not a directory
165+
return;
166+
}
167+
const childItems = readdirSync(pathToDelete);
168+
if (childItems.length !== 0) {
169+
// SKIPPING DELETION: pathToDelete is not empty
170+
return;
171+
}
172+
rmdirSync(pathToDelete);
173+
}
174+
}
175+
}
176+
} catch (error) {
177+
console.error('Error in packageAfterPrune:', error);
178+
throw error;
179+
}
180+
},
181+
},
11182
packagerConfig: {
12183
asar: true,
13184
name: 'Amical',
14185
executableName: 'Amical',
15186
icon: './assets/logo', // Path to your icon file (without extension)
16-
extraResource: ['../../packages/native-helpers/swift-helper/bin'],
187+
extraResource: [
188+
'../../packages/native-helpers/swift-helper/bin',
189+
'./src/db/migrations',
190+
],
17191
extendInfo: {
18192
NSMicrophoneUsageDescription:
19193
'This app needs access to your microphone to record audio for transcription.',
20194
},
195+
//! issues with monorepo setup and module resolutions
196+
//! when forge walks paths via flora-colossus
197+
prune: false,
198+
ignore: (file: string) => {
199+
try {
200+
201+
const filePath = file.toLowerCase();
202+
const KEEP_FILE = {
203+
keep: false,
204+
log: true,
205+
};
206+
// NOTE: must return false for empty string or nothing will be packaged
207+
if (filePath === '') KEEP_FILE.keep = true;
208+
if (!KEEP_FILE.keep && filePath === '/package.json') KEEP_FILE.keep = true;
209+
if (!KEEP_FILE.keep && filePath === '/node_modules') KEEP_FILE.keep = true;
210+
if (!KEEP_FILE.keep && filePath === '/.vite') KEEP_FILE.keep = true;
211+
if (!KEEP_FILE.keep && filePath.startsWith('/.vite/')) KEEP_FILE.keep = true;
212+
if (!KEEP_FILE.keep && filePath.startsWith('/node_modules/')) {
213+
// check if matches any of the external dependencies
214+
for (const dep of nativeModuleDependenciesToPackage) {
215+
if (
216+
filePath === `/node_modules/${dep}/` ||
217+
filePath === `/node_modules/${dep}`
218+
) {
219+
KEEP_FILE.keep = true;
220+
break;
221+
}
222+
if (filePath === `/node_modules/${dep}/package.json`) {
223+
KEEP_FILE.keep = true;
224+
break;
225+
}
226+
if (filePath.startsWith(`/node_modules/${dep}/`)) {
227+
KEEP_FILE.keep = true;
228+
KEEP_FILE.log = false;
229+
break;
230+
}
231+
232+
// Handle scoped packages: if dep is @scope/package, also keep @scope/ directory
233+
if (dep.includes('/') && dep.startsWith('@')) {
234+
const scopeDir = dep.split('/')[0]; // @libsql/client -> @libsql
235+
if (
236+
filePath === `/node_modules/${scopeDir}/` ||
237+
filePath === `/node_modules/${scopeDir}` ||
238+
filePath.startsWith(`/node_modules/${scopeDir}/`)
239+
) {
240+
KEEP_FILE.keep = true;
241+
KEEP_FILE.log = filePath === `/node_modules/${scopeDir}/` || filePath === `/node_modules/${scopeDir}`;
242+
break;
243+
}
244+
}
245+
}
246+
}
247+
if (KEEP_FILE.keep) {
248+
if (KEEP_FILE.log) console.log('Keeping:', file);
249+
return false;
250+
}
251+
return true;
252+
} catch (error) {
253+
console.error('Error in ignore:', error);
254+
throw error;
255+
}
256+
},
21257
},
22258
rebuildConfig: {},
23-
makers: [new MakerSquirrel({}), new MakerZIP({}, ['darwin']), new MakerRpm({}), new MakerDeb({})],
259+
makers: [
260+
new MakerSquirrel({}),
261+
new MakerDMG({
262+
name: 'Amical',
263+
icon: './assets/logo.svg'
264+
}, ['darwin']),
265+
new MakerRpm({}),
266+
new MakerDeb({})
267+
],
24268
plugins: [
25269
new VitePlugin({
26270
// `build` can specify multiple entry builds, which can be Main process, Preload scripts, Worker process, etc.

apps/desktop/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
"start": "pnpm build:swift-helper && electron-forge start",
99
"package": "pnpm build:swift-helper && electron-forge package",
1010
"make": "pnpm build:swift-helper && electron-forge make",
11+
"make:dmg": "pnpm build:swift-helper && electron-forge make --targets=@electron-forge/maker-dmg --platform=darwin --arch=arm64",
1112
"publish": "electron-forge publish",
1213
"lint": "eslint --ext .ts,.tsx .",
1314
"format": "prettier --write \"**/*.{js,jsx,ts,tsx,json,md}\"",
@@ -23,6 +24,7 @@
2324
"devDependencies": {
2425
"@electron-forge/cli": "^7.8.1",
2526
"@electron-forge/maker-deb": "^7.8.1",
27+
"@electron-forge/maker-dmg": "^7.8.1",
2628
"@electron-forge/maker-rpm": "^7.8.1",
2729
"@electron-forge/maker-squirrel": "^7.8.1",
2830
"@electron-forge/maker-zip": "^7.8.1",
@@ -39,6 +41,7 @@
3941
"eslint-config-prettier": "^9.1.0",
4042
"eslint-plugin-import": "^2.31.0",
4143
"eslint-plugin-prettier": "^5.4.0",
44+
"flora-colossus": "^2.0.0",
4245
"prettier": "^3.5.3",
4346
"tailwindcss": "^4.1.6",
4447
"tsx": "^4.19.4",

apps/desktop/src/db/config.ts

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -31,15 +31,8 @@ export async function initializeDatabase() {
3131
// Development: use source path relative to the app's working directory
3232
migrationsPath = path.join(process.cwd(), 'src', 'db', 'migrations');
3333
} else {
34-
// Production: migrations should be in app resources
35-
migrationsPath = path.join(
36-
process.resourcesPath,
37-
'app.asar.unpacked',
38-
'dist',
39-
'main',
40-
'db',
41-
'migrations'
42-
);
34+
// Production: migrations are copied to resources via extraResource
35+
migrationsPath = path.join(process.resourcesPath, 'migrations');
4336
}
4437

4538
console.log('Attempting to run migrations from:', migrationsPath);

apps/desktop/src/main/main.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,7 @@ if (started) {
3737

3838
declare const MAIN_WINDOW_VITE_DEV_SERVER_URL: string;
3939
declare const MAIN_WINDOW_VITE_NAME: string;
40-
41-
const WIDGET_WINDOW_VITE_NAME = 'widget_window';
40+
declare const WIDGET_WINDOW_VITE_NAME: string;
4241

4342
let mainWindow: BrowserWindow | null = null;
4443
let floatingButtonWindow: BrowserWindow | null = null;
@@ -158,7 +157,7 @@ const createFloatingButtonWindow = () => {
158157
floatingButtonWindow.loadURL(devUrl.toString());
159158
} else {
160159
floatingButtonWindow.loadFile(
161-
path.join(__dirname, `../renderer/${MAIN_WINDOW_VITE_NAME}/fab.html`)
160+
path.join(__dirname, `../renderer/${WIDGET_WINDOW_VITE_NAME}/fab.html`)
162161
);
163162
}
164163

apps/desktop/vite.main.config.mts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ export default defineConfig({
66
build: {
77
rollupOptions: {
88
external: [
9-
'better-sqlite3',
109
'smart-whisper',
1110
'@libsql/client',
1211
'@libsql/darwin-arm64',

package.json

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,15 +28,16 @@
2828
"protobufjs",
2929
"sharp",
3030
"smart-whisper",
31-
"drizzle-orm/libsql",
32-
"@libsql"
31+
"drizzle-orm/libsql"
3332
],
3433
"onlyBuiltDependencies": [
3534
"electron",
3635
"electron-winstaller",
3736
"smart-whisper",
3837
"drizzle-orm/libsql",
39-
"@libsql"
38+
"@libsql",
39+
"macos-alias",
40+
"fs-xattr"
4041
]
4142
}
4243
}

0 commit comments

Comments
 (0)