Files
aryx/scripts/package-electron.mjs
T
David KayaandCopilot da72e49f2c feat: add Electron app icon
Move the root logo into app assets, wire the Electron main window to use the branded icon, and stamp the packaged Windows executable icon with rcedit.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-23 21:15:40 +01:00

128 lines
5.2 KiB
JavaScript

import { access, cp, mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
import { constants } from 'node:fs';
import { dirname, join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { rcedit } from 'rcedit';
const productName = 'Kopaya';
const sidecarRuntime = 'win-x64';
const outputDirectoryName = 'win-unpacked';
const scriptDirectory = dirname(fileURLToPath(import.meta.url));
const repositoryRoot = resolve(scriptDirectory, '..');
const assetDirectory = join(repositoryRoot, 'assets');
const windowsIconPath = join(assetDirectory, 'icons', 'app-icon.ico');
const windowIconPath = join(assetDirectory, 'icons', 'app-icon.png');
const electronDistributionDirectory = join(repositoryRoot, 'node_modules', 'electron', 'dist');
const rendererBuildDirectory = join(repositoryRoot, 'dist');
const electronBuildDirectory = join(repositoryRoot, 'dist-electron');
const publishedSidecarDirectory = join(repositoryRoot, 'dist-sidecar', sidecarRuntime);
const outputDirectory = join(repositoryRoot, 'release', outputDirectoryName);
const outputResourcesDirectory = join(outputDirectory, 'resources');
const packagedAppDirectory = join(outputResourcesDirectory, 'app');
const packagedExecutablePath = join(outputDirectory, `${productName}.exe`);
async function ensurePathExists(path, label) {
try {
await access(path, constants.F_OK);
} catch {
throw new Error(`${label} was not found at ${path}.`);
}
}
async function readJson(path) {
return JSON.parse(await readFile(path, 'utf8'));
}
async function collectRuntimeDependencies() {
const rootPackageJson = await readJson(join(repositoryRoot, 'package.json'));
const dependencies = new Set(Object.keys(rootPackageJson.dependencies ?? {}));
const queue = [...dependencies];
while (queue.length > 0) {
const dependencyName = queue.shift();
const dependencyPackageJson = await readJson(
join(repositoryRoot, 'node_modules', ...dependencyName.split('/'), 'package.json'),
);
for (const transitiveDependency of Object.keys({
...(dependencyPackageJson.dependencies ?? {}),
...(dependencyPackageJson.optionalDependencies ?? {}),
})) {
if (!dependencies.has(transitiveDependency)) {
dependencies.add(transitiveDependency);
queue.push(transitiveDependency);
}
}
}
return [...dependencies].sort();
}
async function copyRuntimeDependencies(dependencyNames) {
const packagedNodeModulesDirectory = join(packagedAppDirectory, 'node_modules');
await mkdir(packagedNodeModulesDirectory, { recursive: true });
for (const dependencyName of dependencyNames) {
const dependencyPathParts = dependencyName.split('/');
const sourceDirectory = join(repositoryRoot, 'node_modules', ...dependencyPathParts);
const targetDirectory = join(packagedNodeModulesDirectory, ...dependencyPathParts);
await mkdir(dirname(targetDirectory), { recursive: true });
await cp(sourceDirectory, targetDirectory, { recursive: true });
}
}
async function writePackagedManifest() {
const sourcePackageJson = await readJson(join(repositoryRoot, 'package.json'));
const packagedManifest = {
name: sourcePackageJson.name,
productName,
version: sourcePackageJson.version,
description: sourcePackageJson.description,
main: sourcePackageJson.main,
author: sourcePackageJson.author,
license: sourcePackageJson.license,
};
await writeFile(join(packagedAppDirectory, 'package.json'), `${JSON.stringify(packagedManifest, null, 2)}\n`);
}
async function main() {
if (process.platform !== 'win32') {
throw new Error('This packaging script currently targets Windows only.');
}
await Promise.all([
ensurePathExists(assetDirectory, 'Application assets'),
ensurePathExists(windowIconPath, 'Window icon'),
ensurePathExists(windowsIconPath, 'Windows application icon'),
ensurePathExists(electronDistributionDirectory, 'Electron runtime'),
ensurePathExists(rendererBuildDirectory, 'Renderer build output'),
ensurePathExists(electronBuildDirectory, 'Electron build output'),
ensurePathExists(publishedSidecarDirectory, 'Published sidecar output'),
]);
const runtimeDependencies = await collectRuntimeDependencies();
await rm(outputDirectory, { recursive: true, force: true });
await mkdir(outputDirectory, { recursive: true });
await cp(electronDistributionDirectory, outputDirectory, { recursive: true });
await rename(join(outputDirectory, 'electron.exe'), packagedExecutablePath);
await mkdir(packagedAppDirectory, { recursive: true });
await Promise.all([
writePackagedManifest(),
cp(assetDirectory, join(packagedAppDirectory, 'assets'), { recursive: true }),
cp(rendererBuildDirectory, join(packagedAppDirectory, 'dist'), { recursive: true }),
cp(electronBuildDirectory, join(packagedAppDirectory, 'dist-electron'), { recursive: true }),
cp(publishedSidecarDirectory, join(outputResourcesDirectory, 'sidecar'), { recursive: true }),
]);
await copyRuntimeDependencies(runtimeDependencies);
await rcedit(packagedExecutablePath, { icon: windowsIconPath });
console.log(`Packaged ${productName} to ${outputDirectory}`);
console.log(`Bundled ${runtimeDependencies.length} runtime dependencies and the self-contained .NET sidecar.`);
}
await main();