diff --git a/.ember-cli b/.ember-cli
index 4defd284e..a37e45fab 100644
--- a/.ember-cli
+++ b/.ember-cli
@@ -3,5 +3,17 @@
Setting `isTypeScriptProject` to true will force the blueprint generators to generate TypeScript
rather than JavaScript by default, when a TypeScript version of a given blueprint is available.
*/
- "isTypeScriptProject": true
+ "isTypeScriptProject": true,
+
+ /**
+ Setting `componentAuthoringFormat` to "strict" will force the blueprint generators to generate GJS
+ or GTS files for the component and the component rendering test. "loose" is the default.
+ */
+ "componentAuthoringFormat": "strict",
+
+ /**
+ Setting `routeAuthoringFormat` to "strict" will force the blueprint generators to generate GJS
+ or GTS templates for routes. "loose" is the default
+ */
+ "routeAuthoringFormat": "strict"
}
diff --git a/.gitignore b/.gitignore
index e09b5781b..1bc95a508 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,12 +3,14 @@
# compiled output
/dist/
/declarations/
+/tmp/
# dependencies
/node_modules/
# misc
/.env*
+*.local
/.pnp*
/.sass-cache
/.eslintcache
@@ -19,8 +21,24 @@
/testem.log
/yarn-error.log
+# ember-try
+/.node_modules.ember-try/
+/npm-shrinkwrap.json.ember-try
+/package.json.ember-try
+/package-lock.json.ember-try
+/yarn.lock.ember-try
+
# broccoli-debug
/DEBUG/
electron-out/
.idea/
+
+# Vite
+.vite/
+
+# Electron-Forge
+out/
+
+# TypeScript
+*.tsbuildinfo
diff --git a/.prettierignore b/.prettierignore
index d36183eaf..16b37f37b 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -18,8 +18,6 @@ ember-cli-update.json
# ember-electron
/electron-app/node_modules/
/electron-app/out/
-/electron-app/ember-dist/
-/electron-app/ember-test/
# Sentry
/electron-app/sentry-symbols.js
diff --git a/.prettierrc.js b/.prettierrc.mjs
similarity index 60%
rename from .prettierrc.js
rename to .prettierrc.mjs
index 2152f97bd..86f40a841 100644
--- a/.prettierrc.js
+++ b/.prettierrc.mjs
@@ -1,5 +1,3 @@
-'use strict';
-
const testing = [
'^ember-cli-htmlbars($|\\/)',
'^qunit',
@@ -34,18 +32,45 @@ const importOrder = [
];
const importOrderParserPlugins = ['typescript', 'decorators-legacy'];
-module.exports = {
+export default {
plugins: [
'prettier-plugin-ember-template-tag',
'@ianvs/prettier-plugin-sort-imports',
],
importOrder,
importOrderParserPlugins,
+ singleQuote: true,
overrides: [
{
- files: '*.{js,gjs,ts,gts,mjs,mts,cjs,cts}',
- options: { singleQuote: true, templateSingleQuote: false },
+ files: ['*.js', '*.ts', '*.cjs', '.mjs', '.cts', '.mts', '.cts'],
+ options: {
+ trailingComma: 'es5',
+ },
+ },
+ {
+ files: ['*.html'],
+ options: {
+ singleQuote: false,
+ },
+ },
+ {
+ files: ['*.json'],
+ options: {
+ singleQuote: false,
+ },
+ },
+ {
+ files: ['*.hbs'],
+ options: {
+ singleQuote: false,
+ },
+ },
+ {
+ files: ['*.gjs', '*.gts'],
+ options: {
+ templateSingleQuote: false,
+ trailingComma: 'es5',
+ },
},
- { files: '*.{yaml,yml}', options: { singleQuote: true } },
],
};
diff --git a/.stylelintrc.cjs b/.stylelintrc.cjs
new file mode 100644
index 000000000..414fe6a72
--- /dev/null
+++ b/.stylelintrc.cjs
@@ -0,0 +1,16 @@
+'use strict';
+
+module.exports = {
+ extends: ['stylelint-config-standard'],
+ rules: {
+ 'at-rule-no-deprecated': [true, { ignoreAtRules: ['/^view/', 'apply'] }],
+ 'at-rule-no-unknown': [
+ true,
+ { ignoreAtRules: ['plugin', 'reference', 'theme'] },
+ ],
+ 'custom-property-empty-line-before': null,
+ 'custom-property-pattern': null,
+ 'declaration-empty-line-before': null,
+ 'import-notation': null,
+ },
+};
diff --git a/.stylelintrc.js b/.stylelintrc.js
deleted file mode 100644
index d650b3713..000000000
--- a/.stylelintrc.js
+++ /dev/null
@@ -1,8 +0,0 @@
-'use strict';
-
-module.exports = {
- extends: ['stylelint-config-standard'],
- rules: {
- 'at-rule-no-deprecated': [true, { ignoreAtRules: ['/^view/', 'apply'] }],
- },
-};
diff --git a/.template-lintrc.js b/.template-lintrc.mjs
similarity index 87%
rename from .template-lintrc.js
rename to .template-lintrc.mjs
index 450080d2c..d112f877c 100644
--- a/.template-lintrc.js
+++ b/.template-lintrc.mjs
@@ -1,7 +1,5 @@
-'use strict';
-
-module.exports = {
- extends: ['recommended'],
+export default {
+ extends: 'recommended',
rules: {
'no-at-ember-render-modifiers': false,
'no-builtin-form-components': false,
diff --git a/.tool-versions b/.tool-versions
index 3c20bf8c1..8862775c6 100644
--- a/.tool-versions
+++ b/.tool-versions
@@ -1,2 +1,2 @@
nodejs 20.19.0
-pnpm 10.11.0
\ No newline at end of file
+pnpm 10.18.2
\ No newline at end of file
diff --git a/.vscode/settings.json b/.vscode/settings.json
index 634cedc77..7a73a41bf 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -1,6 +1,2 @@
{
- "eslint.validate": [
- "glimmer-ts",
- "glimmer-js"
- ]
}
\ No newline at end of file
diff --git a/app/app.ts b/app/app.ts
index 30522c199..5a1e25a72 100644
--- a/app/app.ts
+++ b/app/app.ts
@@ -3,7 +3,9 @@ import { InitSentryForEmber } from '@sentry/ember';
import loadInitializers from 'ember-load-initializers';
import Resolver from 'ember-resolver';
import { importSync, isDevelopingApp, macroCondition } from '@embroider/macros';
+import compatModules from '@embroider/virtual/compat-modules';
import config from 'swach/config/environment';
+import './styles/all.css';
if (macroCondition(isDevelopingApp())) {
importSync('./deprecation-workflow');
@@ -14,7 +16,7 @@ InitSentryForEmber();
export default class App extends Application {
modulePrefix = config.modulePrefix;
podModulePrefix = config.podModulePrefix;
- Resolver = Resolver;
+ Resolver = Resolver.withModules(compatModules);
}
-loadInitializers(App, config.modulePrefix);
+loadInitializers(App, config.modulePrefix, compatModules);
diff --git a/app/components/about.gts b/app/components/about.gts
index 2038f216d..36f202114 100644
--- a/app/components/about.gts
+++ b/app/components/about.gts
@@ -3,7 +3,6 @@ import { action } from '@ember/object';
import type Owner from '@ember/owner';
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
-import type { IpcRenderer } from 'electron';
interface AboutSignature {
Element: HTMLDivElement;
@@ -38,7 +37,7 @@ export default class AboutComponent extends Component
`. When the current URL is a `file:`
-// URL, that ends up resolving to the absolute filesystem path `/images/foo.jpg`
-// rather than being relative to the root of the Ember app. So, we intercept
-// `file:` URL request and look to see if they point to an asset when
-// interpreted as being relative to the root of the Ember app. If so, we return
-// that path, and if not we leave them as-is, as their absolute path.
-//
-async function getAssetPath(emberAppDir, url) {
- let urlPath = fileURLToPath(url);
- // Get the root of the path -- should be '/' on MacOS or something like
- // 'C:\' on Windows
- let { root } = path.parse(urlPath);
- // Get the relative path from the root to the full path
- let relPath = path.relative(root, urlPath);
- // Join the relative path with the Ember app directory
- let appPath = path.join(emberAppDir, relPath);
- try {
- await access(appPath);
- return appPath;
- } catch {
- return urlPath;
- }
-}
-
-module.exports = function handleFileURLs(emberAppDir) {
- const { protocol, net } = require('electron');
-
- if (protocol.handle) {
- // Electron >= 25
- protocol.handle('file', async ({ url }) => {
- let path = await getAssetPath(emberAppDir, url);
- return net.fetch(pathToFileURL(path), {
- bypassCustomProtocolHandlers: true,
- });
- });
- } else {
- // Electron < 25
- protocol.interceptFileProtocol('file', async ({ url }, callback) => {
- callback(await getAssetPath(emberAppDir, url));
- });
- }
-};
-
-module.exports.getAssetPath = getAssetPath;
diff --git a/electron-app/src/handle-file-urls.ts b/electron-app/src/handle-file-urls.ts
new file mode 100644
index 000000000..ec2140da4
--- /dev/null
+++ b/electron-app/src/handle-file-urls.ts
@@ -0,0 +1,40 @@
+import { access } from 'node:fs/promises';
+import path from 'node:path';
+import { fileURLToPath, pathToFileURL } from 'node:url';
+import { net, protocol } from 'electron';
+
+//
+// Patch asset loading -- Ember apps use absolute paths to reference their
+// assets, e.g. `
`. When the current URL is a `file:`
+// URL, that ends up resolving to the absolute filesystem path `/images/foo.jpg`
+// rather than being relative to the root of the Ember app. So, we intercept
+// `file:` URL requests and look to see if they point to an asset when
+// interpreted as being relative to the root of the Ember app. If so, we return
+// that path, and if not we leave them as-is, as their absolute path.
+//
+export async function getAssetPath(emberAppDir: string, url: string) {
+ const urlPath = fileURLToPath(url);
+ // Get the root of the path -- should be '/' on MacOS or something like
+ // 'C:\\' on Windows
+ const { root } = path.parse(urlPath);
+ // Get the relative path from the root to the full path
+ const relPath = path.relative(root, urlPath);
+ // Join the relative path with the Ember app directory
+ const appPath = path.join(emberAppDir, relPath);
+
+ try {
+ await access(appPath);
+ return appPath;
+ } catch {
+ return urlPath;
+ }
+}
+
+export default function handleFileURLs(emberAppDir: string) {
+ protocol.handle('file', async ({ url }) => {
+ const assetPath = await getAssetPath(emberAppDir, url);
+ return net.fetch(pathToFileURL(assetPath).href, {
+ bypassCustomProtocolHandlers: true,
+ });
+ });
+}
diff --git a/electron-app/src/index.js b/electron-app/src/index.js
deleted file mode 100644
index 048f4b4ed..000000000
--- a/electron-app/src/index.js
+++ /dev/null
@@ -1,278 +0,0 @@
-const Sentry = require('@sentry/electron');
-const { ipcMain, nativeTheme } = require('electron');
-const isDev = require('electron-is-dev');
-const Store = require('electron-store');
-// eslint-disable-next-line no-redeclare
-const { menubar } = require('menubar');
-const { basename, dirname, join, resolve } = require('path');
-const { pathToFileURL } = require('url');
-
-const { setupUpdateServer } = require('./auto-update');
-const { launchPicker } = require('./color-picker');
-const { noUpdatesAvailableDialog } = require('./dialogs');
-const handleFileUrls = require('./handle-file-urls');
-const { setupEventHandlers } = require('./ipc-events');
-const {
- registerKeyboardShortcuts,
- setupContextMenu,
- setupMenu,
-} = require('./shortcuts');
-
-const emberAppDir = resolve(__dirname, '..', 'ember-dist');
-
-if (isDev) {
- const debug = require('electron-debug');
- debug({ showDevTools: false });
-}
-
-Sentry.init({
- appName: 'swach',
- dsn: 'https://6974b46329f24dc1b9fca4507c65e942@sentry.io/3956140',
- release: `v${require('../package').version}`,
-});
-
-const store = new Store({
- defaults: {
- firstRunV1: true,
- showDockIcon: false,
- },
-});
-
-let emberAppURL = pathToFileURL(join(emberAppDir, 'index.html')).toString();
-
-// On first boot of the application, go through the welcome screen
-if (store.get('firstRunV1')) {
- emberAppURL = `${emberAppURL}#/welcome`;
- store.set('firstRunV1', false);
-}
-
-function openContrastChecker(mb) {
- mb.showWindow();
- mb.window.webContents.send('openContrastChecker');
-}
-
-let menubarIcon = 'resources/menubar-icons/iconTemplate.png';
-
-if (process.platform === 'win32') {
- menubarIcon = 'resources/icon.ico';
-}
-
-if (process.platform === 'linux') {
- menubarIcon = 'resources/png/64x64.png';
-}
-
-const mb = menubar({
- index: false,
- browserWindow: {
- alwaysOnTop: false,
- height: 703,
- resizable: false,
- width: 362,
- webPreferences: {
- contextIsolation: false,
- devTools: isDev,
- preload: join(__dirname, 'preload.js'),
- nodeIntegration: true,
- },
- },
- icon: join(__dirname || resolve(dirname('')), '..', menubarIcon),
- preloadWindow: true,
- showDockIcon: store.get('showDockIcon'),
- showOnAllWorkspaces: false,
-});
-
-mb.app.allowRendererProcessReuse = true;
-
-mb.app.commandLine.appendSwitch('autoplay-policy', 'no-user-gesture-required');
-mb.app.commandLine.appendSwitch(
- 'disable-backgrounding-occluded-windows',
- 'true',
-);
-mb.app.commandLine.appendSwitch('ignore-certificate-errors', true);
-
-let sharedPaletteLink;
-
-async function openSharedPalette() {
- await mb.showWindow();
-
- if (sharedPaletteLink) {
- const query = sharedPaletteLink.split('?data=')[1];
- if (mb?.window && query) {
- mb.window.webContents.send('openSharedPalette', query);
- }
- }
-}
-
-if (isDev && process.platform === 'win32') {
- // Set the path of electron.exe and your app.
- // These two additional parameters are only available on windows.
- // Setting this is required to get this working in dev mode.
- mb.app.setAsDefaultProtocolClient('swach', process.execPath, [
- resolve(process.argv[1]),
- ]);
-} else {
- mb.app.setAsDefaultProtocolClient('swach');
-}
-
-mb.app.on('open-url', function (event, data) {
- event.preventDefault();
- sharedPaletteLink = data;
- openSharedPalette();
-});
-
-// Force single application instance
-const gotTheLock = mb.app.requestSingleInstanceLock();
-
-if (!gotTheLock) {
- mb.app.quit();
-} else {
- mb.app.on('second-instance', (e, argv) => {
- if (mb.window) {
- if (process.platform !== 'darwin') {
- sharedPaletteLink = argv.find((arg) => arg.startsWith('swach://'));
- openSharedPalette();
- }
- }
- });
-}
-
-if (process.platform === 'win32') {
- if (require('electron-squirrel-startup')) mb.app.exit();
-}
-
-// const browsers = require('./browsers')(__dirname);
-// const { settings } = browsers;
-
-// const showPreferences = () => settings.init();
-// ipcMain.on('showPreferences', showPreferences);
-
-setupEventHandlers(mb, store);
-
-// Uncomment the lines below to enable Electron's crash reporter
-// For more information, see http://electron.atom.io/docs/api/crash-reporter/
-// electron.crashReporter.start({
-// productName: 'YourName',
-// companyName: 'YourCompany',
-// submitURL: 'https://your-domain.com/url-to-submit',
-// autoSubmit: true
-// });
-
-mb.app.on('window-all-closed', () => {
- if (process.platform !== 'darwin') {
- mb.app.quit();
- }
-});
-
-mb.on('after-create-window', async () => {
- // If you want to open up dev tools programmatically, call
- // mb.window.openDevTools();
-
- // Load the ember application using our custom protocol/scheme
- await handleFileUrls(emberAppDir);
-
- mb.window.loadURL(emberAppURL);
-
- // If a loading operation goes wrong, we'll send Electron back to
- // Ember App entry point
- mb.window.webContents.on('did-fail-load', () => {
- mb.window.loadURL(emberAppURL);
- });
-
- mb.window.once('ready-to-show', function () {
- setTimeout(() => {
- mb.showWindow();
- }, 750);
- });
-
- mb.window.webContents.on('render-process-gone', () => {
- console.log(
- 'Your Ember app (or other code) in the main window has crashed.',
- );
- console.log(
- 'This is a serious issue that needs to be handled and/or debugged.',
- );
- });
-
- mb.window.on('unresponsive', () => {
- console.log(
- 'Your Ember app (or other code) has made the window unresponsive.',
- );
- });
-
- mb.window.on('responsive', () => {
- console.log('The main window has become responsive again.');
- });
-
- registerKeyboardShortcuts(mb, launchPicker, openContrastChecker);
-
- setupMenu(mb, launchPicker, openContrastChecker);
- setupContextMenu(mb, launchPicker, openContrastChecker);
- const setOSTheme = () => {
- let theme = nativeTheme.shouldUseDarkColors ? 'dark' : 'light';
- mb.window.webContents.send('setTheme', theme);
- };
-
- nativeTheme.on('updated', setOSTheme);
-});
-
-mb.on('ready', async () => {
- ipcMain.on('enableDisableAutoStart', (event, openAtLogin) => {
- // We only want to allow auto-start if in production mode
- if (!isDev) {
- if (process.platform === 'darwin') {
- mb.app.setLoginItemSettings({
- openAtLogin,
- });
- }
-
- if (process.platform === 'win32') {
- const appFolder = dirname(process.execPath);
- const updateExe = resolve(appFolder, '..', 'Update.exe');
- const exeName = basename(process.execPath);
-
- mb.app.setLoginItemSettings({
- openAtLogin,
- path: updateExe,
- args: [
- '--processStart',
- `"${exeName}"`,
- '--process-start-args',
- `"--hidden"`,
- ],
- });
- }
- }
- });
-});
-
-// We only want to auto update if we're on MacOS or Windows. Linux will use Snapcraft.
-if (!isDev && (process.platform === 'darwin' || process.platform === 'win32')) {
- const autoUpdater = setupUpdateServer(mb.app);
- ipcMain.on('checkForUpdates', () => {
- autoUpdater.once('update-not-available', noUpdatesAvailableDialog);
- autoUpdater.checkForUpdates();
- });
-}
-
-// Handle an unhandled error in the main thread
-//
-// Note that 'uncaughtException' is a crude mechanism for exception handling intended to
-// be used only as a last resort. The event should not be used as an equivalent to
-// "On Error Resume Next". Unhandled exceptions inherently mean that an application is in
-// an undefined state. Attempting to resume application code without properly recovering
-// from the exception can cause additional unforeseen and unpredictable issues.
-//
-// Attempting to resume normally after an uncaught exception can be similar to pulling out
-// of the power cord when upgrading a computer -- nine out of ten times nothing happens -
-// but the 10th time, the system becomes corrupted.
-//
-// The correct use of 'uncaughtException' is to perform synchronous cleanup of allocated
-// resources (e.g. file descriptors, handles, etc) before shutting down the process. It is
-// not safe to resume normal operation after 'uncaughtException'.
-process.on('uncaughtException', (err) => {
- console.log('An exception in the main thread was not handled.');
- console.log(
- 'This is a serious issue that needs to be handled and/or debugged.',
- );
- console.log(`Exception: ${err}`);
-});
diff --git a/electron-app/src/ipc-events.js b/electron-app/src/ipc-events.js
deleted file mode 100644
index c79780255..000000000
--- a/electron-app/src/ipc-events.js
+++ /dev/null
@@ -1,87 +0,0 @@
-const {
- app,
- clipboard,
- dialog,
- ipcMain,
- nativeTheme,
- shell,
-} = require('electron');
-const { download } = require('electron-dl');
-const fs = require('fs');
-
-const { launchPicker } = require('./color-picker');
-const { restartDialog } = require('./dialogs');
-
-function setupEventHandlers(mb, store) {
- ipcMain.on('copyColorToClipboard', (channel, color) => {
- clipboard.writeText(color);
- });
-
- ipcMain.on('exitApp', () => mb.app.quit());
-
- ipcMain.on('exportData', async (channel, jsonString) => {
- const downloadPath = `${mb.app.getPath('temp')}/swach-data.json`;
- fs.writeFileSync(downloadPath, jsonString);
- await download(mb.window, `file://${downloadPath}`);
- fs.unlink(downloadPath, (err) => {
- if (err) throw err;
- console.log(`${downloadPath} was deleted`);
- });
- });
-
- ipcMain.handle('getAppVersion', async () => {
- return app.getVersion();
- });
-
- ipcMain.handle('getBackupData', async () => {
- const backupPath = `${mb.app.getPath('temp')}/backup-swach-data.json`;
- return fs.readFileSync(backupPath, { encoding: 'utf8' });
- });
-
- ipcMain.handle('getPlatform', () => {
- return process.platform;
- });
-
- ipcMain.handle('getStoreValue', (event, key) => {
- return store.get(key);
- });
-
- ipcMain.handle('getShouldUseDarkColors', () => {
- return nativeTheme.shouldUseDarkColors ? 'dark' : 'light';
- });
-
- ipcMain.handle('importData', async () => {
- const { canceled, filePaths } = await dialog.showOpenDialog({
- properties: ['openFile'],
- });
-
- if (!canceled && filePaths.length) {
- return fs.readFileSync(filePaths[0], { encoding: 'utf8' });
- }
- });
-
- ipcMain.on('launchContrastBgPicker', async () => {
- await launchPicker(mb, 'contrastBg');
- });
-
- ipcMain.on('launchContrastFgPicker', async () => {
- await launchPicker(mb, 'contrastFg');
- });
-
- ipcMain.on('launchPicker', async () => {
- await launchPicker(mb);
- });
-
- ipcMain.handle('open-external', async (_event, url) => {
- await shell.openExternal(url);
- });
-
- ipcMain.on('setShowDockIcon', async (channel, showDockIcon) => {
- store.set('showDockIcon', showDockIcon);
- await restartDialog();
- });
-}
-
-module.exports = {
- setupEventHandlers,
-};
diff --git a/electron-app/src/ipc-events.ts b/electron-app/src/ipc-events.ts
new file mode 100644
index 000000000..d55cc4e8f
--- /dev/null
+++ b/electron-app/src/ipc-events.ts
@@ -0,0 +1,86 @@
+import fs from 'fs';
+import { app, clipboard, dialog, ipcMain, nativeTheme, shell } from 'electron';
+import { download } from 'electron-dl';
+import type Store from 'electron-store';
+import { type Menubar } from 'menubar';
+import { launchPicker } from './color-picker';
+import { restartDialog } from './dialogs';
+
+function setupEventHandlers(
+ mb: Menubar,
+ store: Store<{ firstRunV1: boolean; showDockIcon: boolean }>
+) {
+ ipcMain.on('copyColorToClipboard', (_channel, color: string) => {
+ clipboard.writeText(color);
+ });
+
+ ipcMain.on('exitApp', () => mb.app.quit());
+
+ // eslint-disable-next-line @typescript-eslint/no-misused-promises
+ ipcMain.on('exportData', async (_channel, jsonString: string) => {
+ const downloadPath = `${mb.app.getPath('temp')}/swach-data.json`;
+ fs.writeFileSync(downloadPath, jsonString);
+ await download(mb.window!, `file://${downloadPath}`);
+ fs.unlink(downloadPath, (err) => {
+ if (err) throw err;
+ console.log(`${downloadPath} was deleted`);
+ });
+ });
+
+ ipcMain.handle('getAppVersion', () => {
+ return app.getVersion();
+ });
+
+ ipcMain.handle('getBackupData', () => {
+ const backupPath = `${mb.app.getPath('temp')}/backup-swach-data.json`;
+ return fs.readFileSync(backupPath, { encoding: 'utf8' });
+ });
+
+ ipcMain.handle('getPlatform', () => {
+ return process.platform;
+ });
+
+ ipcMain.handle('getStoreValue', (_event, key: string) => {
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-return
+ return store.get(key);
+ });
+
+ ipcMain.handle('getShouldUseDarkColors', () => {
+ return nativeTheme.shouldUseDarkColors ? 'dark' : 'light';
+ });
+
+ ipcMain.handle('importData', async () => {
+ const { canceled, filePaths } = await dialog.showOpenDialog({
+ properties: ['openFile'],
+ });
+
+ if (!canceled && filePaths.length) {
+ return fs.readFileSync(filePaths[0]!, { encoding: 'utf8' });
+ }
+ return null;
+ });
+
+ ipcMain.on('launchContrastBgPicker', () => {
+ void launchPicker(mb, 'contrastBg');
+ });
+
+ ipcMain.on('launchContrastFgPicker', () => {
+ void launchPicker(mb, 'contrastFg');
+ });
+
+ ipcMain.on('launchPicker', () => {
+ void launchPicker(mb);
+ });
+
+ ipcMain.handle('open-external', async (_event, url: string) => {
+ await shell.openExternal(url);
+ });
+
+ // eslint-disable-next-line @typescript-eslint/no-misused-promises
+ ipcMain.on('setShowDockIcon', async (_channel, showDockIcon) => {
+ store.set('showDockIcon', showDockIcon);
+ await restartDialog();
+ });
+}
+
+export { setupEventHandlers };
diff --git a/electron-app/src/main.ts b/electron-app/src/main.ts
new file mode 100644
index 000000000..82d5c8d2f
--- /dev/null
+++ b/electron-app/src/main.ts
@@ -0,0 +1,248 @@
+import { basename, dirname, join, resolve } from 'node:path';
+// This should cause a type error
+import { fileURLToPath, pathToFileURL } from 'node:url';
+import { init } from '@sentry/electron';
+import { ipcMain, nativeTheme } from 'electron';
+import isDev from 'electron-is-dev';
+import Store from 'electron-store';
+import { menubar, type Menubar } from 'menubar';
+import pkg from '../../package.json';
+import { setupUpdateServer } from './auto-update.js';
+import { noUpdatesAvailableDialog } from './dialogs.js';
+import handleFileUrls from './handle-file-urls.js';
+import { setupEventHandlers } from './ipc-events.js';
+import {
+ registerKeyboardShortcuts,
+ setupContextMenu,
+ setupMenu,
+} from './shortcuts.js';
+
+// __dirname in ESM
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = dirname(__filename);
+
+init({
+ appName: 'swach',
+ dsn: 'https://6974b46329f24dc1b9fca4507c65e942@sentry.io/3956140',
+ release: `v${pkg.version}`,
+});
+
+const store = new Store({
+ defaults: {
+ firstRunV1: true,
+ showDockIcon: false,
+ },
+});
+
+const emberAppDir = resolve(__dirname, '..', 'dist');
+let emberAppURL = isDev
+ ? 'http://localhost:4200'
+ : pathToFileURL(join(emberAppDir, 'index.html')).toString();
+
+// On first boot of the application, go through the welcome screen
+if (store.get('firstRunV1')) {
+ emberAppURL = `${emberAppURL}#/welcome`;
+ store.set('firstRunV1', false);
+}
+
+function openContrastChecker(mb: Menubar) {
+ void mb.showWindow();
+ mb.window!.webContents.send('openContrastChecker');
+}
+
+let menubarIcon = 'resources/menubar-icons/iconTemplate.png';
+if (process.platform === 'win32') menubarIcon = 'resources/icon.ico';
+if (process.platform === 'linux') menubarIcon = 'resources/png/64x64.png';
+
+const mb = menubar({
+ index: false,
+ browserWindow: {
+ alwaysOnTop: false,
+ height: 703,
+ resizable: false,
+ width: 362,
+ webPreferences: {
+ contextIsolation: true,
+ devTools: isDev,
+ preload: join(__dirname, 'preload.js'),
+ nodeIntegration: false,
+ },
+ },
+ icon: join(__dirname, '../../electron-app', menubarIcon),
+ preloadWindow: true,
+ showDockIcon: store.get('showDockIcon'),
+ showOnAllWorkspaces: false,
+});
+
+// mb.app.allowRendererProcessReuse = true; // Deprecated property
+
+mb.app.commandLine.appendSwitch('autoplay-policy', 'no-user-gesture-required');
+mb.app.commandLine.appendSwitch(
+ 'disable-backgrounding-occluded-windows',
+ 'true'
+);
+mb.app.commandLine.appendSwitch('ignore-certificate-errors', 'true');
+
+let sharedPaletteLink: string | undefined;
+
+async function openSharedPalette() {
+ await mb.showWindow();
+
+ if (sharedPaletteLink) {
+ const query = sharedPaletteLink.split('?data=')[1];
+ if (mb?.window && query) {
+ mb.window.webContents.send('openSharedPalette', query);
+ }
+ }
+}
+
+if (isDev && process.platform === 'win32') {
+ // Windows dev mode protocol registration
+ mb.app.setAsDefaultProtocolClient('swach', process.execPath, [
+ resolve(process.argv[1]!),
+ ]);
+} else {
+ mb.app.setAsDefaultProtocolClient('swach');
+}
+
+mb.app.on('open-url', function (event, data) {
+ event.preventDefault();
+ sharedPaletteLink = data;
+ void openSharedPalette();
+});
+
+// Force single application instance
+const gotTheLock = mb.app.requestSingleInstanceLock();
+
+if (!gotTheLock) {
+ mb.app.quit();
+} else {
+ mb.app.on('second-instance', (e, argv) => {
+ if (mb.window) {
+ if (process.platform !== 'darwin') {
+ sharedPaletteLink = argv.find((arg) => arg.startsWith('swach://'));
+ void openSharedPalette();
+ }
+ }
+ });
+}
+
+if (process.platform === 'win32') {
+ import('electron-squirrel-startup')
+ .then(({ default: handled }) => {
+ if (handled) mb.app.exit();
+ })
+ .catch(() => {});
+}
+
+// const browsers = require('./browsers')(__dirname);
+// const { settings } = browsers;
+// const showPreferences = () => settings.init();
+// ipcMain.on('showPreferences', showPreferences);
+
+setupEventHandlers(mb, store);
+
+// Uncomment to enable Electron's crash reporter
+// electron.crashReporter.start({ ... });
+
+mb.app.on('window-all-closed', () => {
+ if (process.platform !== 'darwin') {
+ mb.app.quit();
+ }
+});
+
+mb.on('after-create-window', () => {
+ // Load the Ember application using our custom protocol/scheme
+ handleFileUrls(emberAppDir);
+
+ void mb.window!.loadURL(emberAppURL);
+
+ // If a loading operation goes wrong, we'll send Electron back to Ember entry
+ mb.window!.webContents.on('did-fail-load', () => {
+ void mb.window!.loadURL(emberAppURL);
+ });
+
+ mb.window!.once('ready-to-show', function () {
+ setTimeout(() => {
+ void mb.showWindow();
+ }, 750);
+ });
+
+ mb.window!.webContents.on('render-process-gone', () => {
+ console.log(
+ 'Your Ember app (or other code) in the main window has crashed.'
+ );
+ console.log(
+ 'This is a serious issue that needs to be handled and/or debugged.'
+ );
+ });
+
+ mb.window!.on('unresponsive', () => {
+ console.log(
+ 'Your Ember app (or other code) has made the window unresponsive.'
+ );
+ });
+
+ mb.window!.on('responsive', () => {
+ console.log('The main window has become responsive again.');
+ });
+
+ registerKeyboardShortcuts(mb, openContrastChecker);
+
+ setupMenu(mb, openContrastChecker);
+ setupContextMenu(mb, openContrastChecker);
+
+ const setOSTheme = () => {
+ const theme = nativeTheme.shouldUseDarkColors ? 'dark' : 'light';
+ mb.window!.webContents.send('setTheme', theme);
+ };
+
+ nativeTheme.on('updated', setOSTheme);
+});
+
+mb.on('ready', () => {
+ ipcMain.on('enableDisableAutoStart', (event, openAtLogin) => {
+ // Only allow auto-start in production
+ if (!isDev) {
+ if (process.platform === 'darwin') {
+ mb.app.setLoginItemSettings({ openAtLogin });
+ }
+
+ if (process.platform === 'win32') {
+ const appFolder = dirname(process.execPath);
+ const updateExe = resolve(appFolder, '..', 'Update.exe');
+ const exeName = basename(process.execPath);
+
+ mb.app.setLoginItemSettings({
+ openAtLogin,
+ path: updateExe,
+ args: [
+ '--processStart',
+ `"${exeName}"`,
+ '--process-start-args',
+ `"--hidden"`,
+ ],
+ });
+ }
+ }
+ });
+});
+
+// Auto update on macOS/Windows (Linux uses Snapcraft)
+if (!isDev && (process.platform === 'darwin' || process.platform === 'win32')) {
+ const autoUpdater = setupUpdateServer(mb.app);
+ ipcMain.on('checkForUpdates', () => {
+ // eslint-disable-next-line @typescript-eslint/no-misused-promises
+ autoUpdater.once('update-not-available', noUpdatesAvailableDialog);
+ autoUpdater.checkForUpdates();
+ });
+}
+
+// Handle an unhandled error in the main thread
+process.on('uncaughtException', (err) => {
+ console.log('An exception in the main thread was not handled.');
+ console.log(
+ 'This is a serious issue that needs to be handled and/or debugged.'
+ );
+ console.log(`Exception: ${err}`);
+});
diff --git a/electron-app/src/preload.js b/electron-app/src/preload.js
deleted file mode 100644
index b2344e9dd..000000000
--- a/electron-app/src/preload.js
+++ /dev/null
@@ -1,7 +0,0 @@
-const Sentry = require('@sentry/electron');
-
-Sentry.init({
- appName: 'swach',
- dsn: 'https://6974b46329f24dc1b9fca4507c65e942@sentry.io/3956140',
- release: `v${require('../package').version}`,
-});
diff --git a/electron-app/src/preload.ts b/electron-app/src/preload.ts
new file mode 100644
index 000000000..14012e693
--- /dev/null
+++ b/electron-app/src/preload.ts
@@ -0,0 +1,35 @@
+import { init } from '@sentry/electron';
+import { contextBridge, ipcRenderer } from 'electron';
+import pkg from '../../package.json';
+
+init({
+ appName: 'swach',
+ dsn: 'https://6974b46329f24dc1b9fca4507c65e942@sentry.io/3956140',
+ release: `v${pkg.version}`,
+});
+
+// Expose protected methods that allow the renderer process to use
+// the ipcRenderer without exposing the entire object
+contextBridge.exposeInMainWorld('electronAPI', {
+ ipcRenderer: {
+ send: (channel: string, ...args: unknown[]) =>
+ ipcRenderer.send(channel, ...args),
+ on: (channel: string, func: (...args: unknown[]) => void) => {
+ const subscription = (_event: unknown, ...args: unknown[]) =>
+ func(...args);
+ ipcRenderer.on(channel, subscription);
+ return subscription;
+ },
+ off: (channel: string, func: (...args: unknown[]) => void) =>
+ ipcRenderer.off(channel, func),
+ once: (channel: string, func: (...args: unknown[]) => void) => {
+ ipcRenderer.once(channel, (_event: unknown, ...args: unknown[]) =>
+ func(...args)
+ );
+ },
+ invoke: (channel: string, ...args: unknown[]) =>
+ ipcRenderer.invoke(channel, ...args),
+ removeAllListeners: (channel: string) =>
+ ipcRenderer.removeAllListeners(channel),
+ },
+});
diff --git a/electron-app/src/shortcuts.js b/electron-app/src/shortcuts.ts
similarity index 74%
rename from electron-app/src/shortcuts.js
rename to electron-app/src/shortcuts.ts
index afe955e36..020ab0f05 100644
--- a/electron-app/src/shortcuts.js
+++ b/electron-app/src/shortcuts.ts
@@ -1,8 +1,20 @@
-const { globalShortcut, shell, Menu } = require('electron');
+import {
+ globalShortcut,
+ Menu,
+ shell,
+ type MenuItemConstructorOptions,
+} from 'electron';
+import { type Menubar } from 'menubar';
+import { launchPicker } from './color-picker.js';
-function registerKeyboardShortcuts(mb, launchPicker, openContrastChecker) {
+type OpenContrastCheckerFn = (mb: Menubar) => void;
+
+export function registerKeyboardShortcuts(
+ mb: Menubar,
+ openContrastChecker: OpenContrastCheckerFn
+) {
globalShortcut.register('Ctrl+Super+Alt+p', () => {
- launchPicker(mb);
+ void launchPicker(mb);
});
globalShortcut.register('Ctrl+Super+Alt+c', () => {
@@ -14,12 +26,15 @@ function registerKeyboardShortcuts(mb, launchPicker, openContrastChecker) {
});
}
-function setupContextMenu(mb, launchPicker, openContrastChecker) {
+export function setupContextMenu(
+ mb: Menubar,
+ openContrastChecker: OpenContrastCheckerFn
+) {
const contextMenu = Menu.buildFromTemplate([
{
label: 'Color Picker',
click() {
- launchPicker(mb);
+ void launchPicker(mb);
},
},
{
@@ -42,7 +57,10 @@ function setupContextMenu(mb, launchPicker, openContrastChecker) {
});
}
-function setupMenu(mb, launchPicker, openContrastChecker) {
+export function setupMenu(
+ mb: Menubar,
+ openContrastChecker: OpenContrastCheckerFn
+) {
const isMac = process.platform === 'darwin';
const template = [
@@ -66,7 +84,7 @@ function setupMenu(mb, launchPicker, openContrastChecker) {
{
label: 'Color Picker',
click() {
- launchPicker(mb);
+ void launchPicker(mb);
},
},
{
@@ -86,15 +104,15 @@ function setupMenu(mb, launchPicker, openContrastChecker) {
{
label: 'Undo',
accelerator: 'CmdOrCtrl+Z',
- async click() {
- await mb.window.webContents.send('undoRedo', 'undo');
+ click() {
+ mb.window!.webContents.send('undoRedo', 'undo');
},
},
{
label: 'Redo',
accelerator: 'Shift+CmdOrCtrl+Z',
- async click() {
- await mb.window.webContents.send('undoRedo', 'redo');
+ click() {
+ mb.window!.webContents.send('undoRedo', 'redo');
},
},
{ type: 'separator' },
@@ -129,12 +147,6 @@ function setupMenu(mb, launchPicker, openContrastChecker) {
},
];
- const menu = Menu.buildFromTemplate(template);
+ const menu = Menu.buildFromTemplate(template as MenuItemConstructorOptions[]);
Menu.setApplicationMenu(menu);
}
-
-module.exports = {
- registerKeyboardShortcuts,
- setupContextMenu,
- setupMenu,
-};
diff --git a/electron-app/tests/index.js b/electron-app/tests/index.js
deleted file mode 100644
index ddad4fd25..000000000
--- a/electron-app/tests/index.js
+++ /dev/null
@@ -1,48 +0,0 @@
-const {
- setupTestem,
- openTestWindow,
-} = require('ember-electron/lib/test-support');
-
-const { app, ipcMain, nativeTheme } = require('electron');
-const Store = require('electron-store');
-const path = require('path');
-
-const store = new Store({
- defaults: {
- firstRunV1: true,
- needsMigration: true,
- showDockIcon: false,
- },
-});
-
-const handleFileUrls = require('../src/handle-file-urls');
-
-const emberAppDir = path.resolve(__dirname, '..', 'ember-test');
-
-ipcMain.handle('getAppVersion', async () => {
- return app.getVersion();
-});
-
-ipcMain.handle('getPlatform', () => {
- return process.platform;
-});
-
-ipcMain.handle('getStoreValue', (event, key) => {
- return store.get(key);
-});
-
-ipcMain.handle('getShouldUseDarkColors', () => {
- return nativeTheme.shouldUseDarkColors ? 'dark' : 'light';
-});
-
-app.on('ready', async function onReady() {
- await handleFileUrls(emberAppDir);
- setupTestem();
- openTestWindow(emberAppDir);
-});
-
-app.on('window-all-closed', function onWindowAllClosed() {
- if (process.platform !== 'darwin') {
- app.quit();
- }
-});
diff --git a/electron-app/tests/index.ts b/electron-app/tests/index.ts
new file mode 100644
index 000000000..200c9bec7
--- /dev/null
+++ b/electron-app/tests/index.ts
@@ -0,0 +1,58 @@
+import { dirname, resolve } from 'node:path';
+import { fileURLToPath } from 'node:url';
+import { app, ipcMain, nativeTheme } from 'electron';
+import Store from 'electron-store';
+import {
+ handleFileUrls,
+ openTestWindow,
+ setupTestem,
+} from 'vite-plugin-testem-electron/electron';
+
+// __dirname in ESM
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = dirname(__filename);
+
+const store = new Store({
+ defaults: {
+ firstRunV1: true,
+ needsMigration: true,
+ showDockIcon: false,
+ },
+});
+
+// IPC handlers needed by the app
+ipcMain.handle('getAppVersion', () => {
+ return app.getVersion();
+});
+
+ipcMain.handle('getPlatform', () => {
+ return process.platform;
+});
+
+ipcMain.handle('getStoreValue', (_event, key: string) => {
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
+ return store.get(key) as unknown;
+});
+
+ipcMain.handle('getShouldUseDarkColors', () => {
+ return nativeTheme.shouldUseDarkColors ? 'dark' : 'light';
+});
+
+const emberAppDir = resolve(__dirname, '..', '..', 'dist');
+
+app.on('ready', function onReady() {
+ // Set a global for the preload script to detect test mode
+ process.env.ELECTRON_IS_TESTING = 'true';
+
+ handleFileUrls(emberAppDir);
+
+ // Set up testem communication
+ setupTestem();
+
+ // Open the test window - testem.js will handle QUnit integration automatically
+ openTestWindow(emberAppDir);
+});
+
+app.on('window-all-closed', function onWindowAllClosed() {
+ app.quit();
+});
diff --git a/electron-app/tsconfig.json b/electron-app/tsconfig.json
new file mode 100644
index 000000000..6ecea40df
--- /dev/null
+++ b/electron-app/tsconfig.json
@@ -0,0 +1,27 @@
+{
+ "compilerOptions": {
+ "target": "ES2022",
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "allowSyntheticDefaultImports": true,
+ "esModuleInterop": true,
+ "skipLibCheck": true,
+ "resolveJsonModule": true,
+ "strict": true,
+ "noImplicitAny": true,
+ "strictNullChecks": true,
+ "strictFunctionTypes": true,
+ "noImplicitReturns": true,
+ "noFallthroughCasesInSwitch": true,
+ "noUncheckedIndexedAccess": true,
+ "allowJs": true,
+ "types": ["node", "electron"],
+ "lib": ["ES2022"],
+ "composite": true,
+ "declaration": true,
+ "declarationMap": true,
+ "outDir": "./dist"
+ },
+ "include": ["src/**/*", "tests/**/*"],
+ "exclude": ["node_modules", "dist", ".vite"]
+}
diff --git a/ember-cli-build.js b/ember-cli-build.js
index d0f0a8785..445ffe775 100644
--- a/ember-cli-build.js
+++ b/ember-cli-build.js
@@ -1,55 +1,22 @@
'use strict';
const EmberApp = require('ember-cli/lib/broccoli/ember-app');
+const { compatBuild } = require('@embroider/compat');
-module.exports = function (defaults) {
- const app = new EmberApp(defaults, {
- autoImport: {
- forbidEval: true,
- },
- babel: {
- plugins: ['@babel/plugin-proposal-object-rest-spread'],
- },
- 'ember-cli-babel': {
- enableTypeScriptTransform: true,
- },
- postcssOptions: {
- compile: {
- enabled: true,
- plugins: [
- require('postcss-import')(),
- require('tailwindcss')('./tailwind.config.js'),
- ],
+module.exports = async function (defaults) {
+ const { buildOnce } = await import('@embroider/vite');
+ let app = new EmberApp(defaults, {
+ emberData: {
+ deprecations: {
+ // New projects can safely leave this deprecation disabled.
+ // If upgrading, to opt-into the deprecated behavior, set this to true and then follow:
+ // https://deprecations.emberjs.com/id/ember-data-deprecate-store-extends-ember-object
+ // before upgrading to Ember Data 6.0
+ DEPRECATE_STORE_EXTENDS_EMBER_OBJECT: false,
},
},
- sourcemaps: {
- enabled: true,
- },
+ // Add options here
});
- if (process.platform !== 'win32') {
- const { Webpack } = require('@embroider/webpack');
-
- //const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
- return require('@embroider/compat').compatBuild(app, Webpack, {
- staticAddonTestSupportTrees: true,
- staticAddonTrees: true,
- staticEmberSource: true,
- staticHelpers: true,
- staticComponents: true,
- packagerOptions: {
- webpackConfig: {
- devtool: false,
- resolve: {
- fallback: {
- crypto: require.resolve('crypto-browserify'),
- stream: require.resolve('stream-browserify'),
- },
- },
- },
- },
- });
- } else {
- return app.toTree();
- }
+ return compatBuild(app, buildOnce);
};
diff --git a/eslint.config.mjs b/eslint.config.mjs
index 2b928b818..769042f86 100644
--- a/eslint.config.mjs
+++ b/eslint.config.mjs
@@ -26,17 +26,11 @@ const parserOptions = {
js: {
ecmaFeatures: { modules: true },
ecmaVersion: 'latest',
- requireConfigFile: false,
- babelOptions: {
- plugins: [
- [
- '@babel/plugin-proposal-decorators',
- { decoratorsBeforeExport: true },
- ],
- ],
- },
},
- ts: { projectService: true, tsconfigRootDir: import.meta.dirname },
+ ts: {
+ projectService: true,
+ tsconfigRootDir: import.meta.dirname,
+ },
},
};
@@ -52,35 +46,46 @@ export default ts.config(
*/
{
ignores: [
- 'declarations/',
+ '.vite/**/*',
+ 'declarations/**/*',
// ember-electron
- 'electron-app/node_modules/',
- 'electron-app/out/',
- 'electron-app/ember-dist/',
- 'electron-app/ember-test/',
- 'electron-out/',
+ 'electron-app/node_modules/**/*',
+ 'electron-app/out/**/*',
+ 'electron-app/dist/**/*',
+ 'electron-out/**/*',
// Sentry
'electron-app/sentry-symbols.js',
- 'dist/',
- 'node_modules/',
- 'coverage/',
- 'types/',
+ 'dist/**/*',
+ 'node_modules/**/*',
+ 'coverage/**/*',
+ 'types/**/*',
'!**/.*',
],
},
/**
* https://eslint.org/docs/latest/use/configure/configuration-files#configuring-linter-options
*/
- { linterOptions: { reportUnusedDisableDirectives: 'error' } },
- { files: ['**/*.js'], languageOptions: { parser: babelParser } },
+ {
+ linterOptions: {
+ reportUnusedDisableDirectives: 'error',
+ },
+ },
+ {
+ files: ['**/*.js'],
+ languageOptions: {
+ parser: babelParser,
+ },
+ },
{
files: ['**/*.{js,gjs}'],
languageOptions: {
parserOptions: parserOptions.esm.js,
- globals: { ...globals.browser },
+ globals: {
+ ...globals.browser,
+ },
},
},
{
@@ -97,7 +102,12 @@ export default ts.config(
'ember/no-at-ember-render-modifiers': 'off',
},
},
- { files: ['tests/**/*-test.{js,gjs,ts,gts}'], plugins: { qunit } },
+ {
+ files: ['tests/**/*-test.{js,gjs,ts,gts}'],
+ plugins: {
+ qunit,
+ },
+ },
/**
* CJS node files
*/
@@ -106,7 +116,6 @@ export default ts.config(
'**/*.cjs',
'config/**/*.js',
'electron-app/**/*.js',
- 'tests/dummy/config/**/*.js',
'testem.js',
'testem*.js',
'index.js',
@@ -116,12 +125,16 @@ export default ts.config(
'ember-cli-build.js',
'tailwind.config.js',
],
- plugins: { n },
+ plugins: {
+ n,
+ },
languageOptions: {
sourceType: 'script',
ecmaVersion: 'latest',
- globals: { ...globals.node },
+ globals: {
+ ...globals.node,
+ },
},
},
/**
@@ -129,13 +142,17 @@ export default ts.config(
*/
{
files: ['**/*.mjs'],
- plugins: { n },
+ plugins: {
+ n,
+ },
languageOptions: {
sourceType: 'module',
ecmaVersion: 'latest',
parserOptions: parserOptions.esm.js,
- globals: { ...globals.node },
+ globals: {
+ ...globals.node,
+ },
},
},
);
diff --git a/electron-app/forge.config.js b/forge.config.ts
similarity index 52%
rename from electron-app/forge.config.js
rename to forge.config.ts
index 80a740d7d..b92e29f55 100644
--- a/electron-app/forge.config.js
+++ b/forge.config.ts
@@ -1,10 +1,18 @@
-module.exports = {
+import { MakerDeb } from '@electron-forge/maker-deb';
+import { MakerDMG } from '@electron-forge/maker-dmg';
+import { MakerSquirrel } from '@electron-forge/maker-squirrel';
+import { MakerZIP } from '@electron-forge/maker-zip';
+import { FusesPlugin } from '@electron-forge/plugin-fuses';
+import { VitePlugin } from '@electron-forge/plugin-vite';
+import type { ForgeConfig } from '@electron-forge/shared-types';
+import { FuseV1Options, FuseVersion } from '@electron/fuses';
+
+const config: ForgeConfig = {
packagerConfig: {
asar: true,
- darwinDarkModeSupport: 'true',
+ darwinDarkModeSupport: true,
icon: 'electron-app/resources/icon',
name: 'Swach',
- packageManager: 'pnpm',
ignore: [
'/.gitignore',
'/electron-forge-config.js',
@@ -23,24 +31,20 @@ module.exports = {
},
},
osxNotarize: {
- tool: 'notarytool',
- appleId: process.env['APPLE_ID'],
- appleIdPassword: process.env['APPLE_ID_PASSWORD'],
+ appleId: process.env.APPLE_ID,
+ appleIdPassword: process.env.APPLE_ID_PASSWORD,
teamId: '779MXKT6B5',
},
protocols: [
{
- protocol: 'swach',
name: 'swach',
- schemes: 'swach',
+ schemes: ['swach'],
},
],
},
makers: [
- {
- name: '@electron-forge/maker-deb',
- platforms: ['linux'],
- config: {
+ new MakerDeb(
+ {
options: {
bin: 'Swach',
name: 'swach',
@@ -52,18 +56,18 @@ module.exports = {
icon: 'electron-app/resources/icon.png',
},
},
- },
- {
- name: '@electron-forge/maker-dmg',
- platforms: ['darwin'],
- config(arch) {
+ ['linux']
+ ),
+ new MakerDMG(
+ (arch) => {
return {
name: arch === 'arm64' ? 'Swach-arm64' : 'Swach',
background: 'electron-app/resources/installBackground.png',
icon: 'electron-app/resources/dmg.icns',
};
},
- },
+ ['darwin']
+ ),
// {
// name: '@electron-forge/maker-snap',
// platforms: ['linux'],
@@ -99,18 +103,48 @@ module.exports = {
// type: 'app',
// },
// },
- {
- name: '@electron-forge/maker-squirrel',
- config: {
- name: 'Swach',
- certificateFile: process.env['WINDOWS_PFX_FILE'],
- certificatePassword: process.env['WINDOWS_PFX_PASSWORD'],
- },
- },
- {
- name: '@electron-forge/maker-zip',
- platforms: ['darwin'],
- },
+ new MakerSquirrel({
+ name: 'Swach',
+ certificateFile: process.env['WINDOWS_PFX_FILE'],
+ certificatePassword: process.env['WINDOWS_PFX_PASSWORD'],
+ }),
+ new MakerZIP({}, ['darwin']),
+ ],
+ plugins: [
+ new VitePlugin({
+ // `build` can specify multiple entry builds, which can be Main process, Preload scripts, Worker process, etc.
+ // If you are familiar with Vite configuration, it will look really familiar.
+ build: [
+ {
+ // `entry` is just an alias for `build.lib.entry` in the corresponding file of `config`.
+ entry: 'electron-app/src/main.ts',
+ config: 'vite.main.config.ts',
+ target: 'main',
+ },
+ {
+ entry: 'electron-app/src/preload.ts',
+ config: 'vite.preload.config.ts',
+ target: 'preload',
+ },
+ ],
+ renderer: [
+ {
+ name: 'main_window',
+ config: 'vite.renderer.config.ts',
+ },
+ ],
+ }),
+ // Fuses are used to enable/disable various Electron functionality
+ // at package time, before code signing the application
+ new FusesPlugin({
+ version: FuseVersion.V1,
+ [FuseV1Options.RunAsNode]: false,
+ [FuseV1Options.EnableCookieEncryption]: true,
+ [FuseV1Options.EnableNodeOptionsEnvironmentVariable]: false,
+ [FuseV1Options.EnableNodeCliInspectArguments]: false,
+ [FuseV1Options.EnableEmbeddedAsarIntegrityValidation]: true,
+ [FuseV1Options.OnlyLoadAppFromAsar]: true,
+ }),
],
// publishers: [
// {
@@ -122,3 +156,5 @@ module.exports = {
// },
// ],
};
+
+export default config;
diff --git a/forge.env.d.ts b/forge.env.d.ts
new file mode 100644
index 000000000..9700e0ae1
--- /dev/null
+++ b/forge.env.d.ts
@@ -0,0 +1 @@
+///