<?xml version="1.0" encoding="utf-8"?>
    <feed xmlns="http://www.w3.org/2005/Atom">
     <title>BigBinary Blog</title>
     <link href="https://www.bigbinary.com/feed.xml" rel="self"/>
     <link href="https://www.bigbinary.com/"/>
     <updated>2026-05-19T03:11:18+00:00</updated>
     <id>https://www.bigbinary.com/</id>
     <entry>
       <title><![CDATA[How to remotely EV code-sign a Windows application using ssl.com]]></title>
       <author><name>Farhan CK</name></author>
      <link href="https://www.bigbinary.com/blog/ev-code-sign-windows-application-ssl-com"/>
      <updated>2024-12-17T12:00:00+00:00</updated>
      <id>https://www.bigbinary.com/blog/ev-code-sign-windows-application-ssl-com</id>
      <content type="html"><![CDATA[<p><em>Recently, we built <a href="https://neetorecord.com/neetorecord/">NeetoRecord</a>, a loomalternative. The desktop application was built using Electron. In a series ofblogs, we capture how we built the desktop application and the challenges we raninto. This blog is part 9 of the blog series. You can also read about<a href="https://www.bigbinary.com/blog/sync-store-main-renderer-electron">part 1</a>,<a href="https://www.bigbinary.com/blog/publish-electron-application">part 2</a>,<a href="https://www.bigbinary.com/blog/video-background-removal">part 3</a>,<a href="https://www.bigbinary.com/blog/electron-multiple-browser-windows">part 4</a>,<a href="https://www.bigbinary.com/blog/code-sign-notorize-mac-desktop-app">part 5</a>,<a href="https://www.bigbinary.com/blog/deep-link-electron-app">part 6</a>,<a href="https://www.bigbinary.com/blog/request-camera-micophone-permission-electron">part 7</a>and <a href="https://www.bigbinary.com/blog/native-modules-electron">part 8</a>.</em></p><p>Code-signing allows Windows to verify the identity of an application'spublisher, making it authentic and trustworthy. Additionally, code-signing helpsapplications comply with Windows security policies, avoiding warnings andinstallation blocks, ultimately building user confidence and providing asmoother, safer experience.</p><h3>What is code-signing?</h3><p>At its core, code-signing involves using a cryptographic hash function to createa unique digital fingerprint of the code. This fingerprint and a certificatefrom a trusted Certificate Authority (CA) form the digital signature. When usersdownload and run the software, their operating system or browser checks thedigital signature to verify its authenticity.</p><p>There are two types of certificates we can use to sign a Windows application:</p><ul><li>Code-signing Certificate</li><li>EV Code-signing Certificate</li></ul><p>A <strong>Standard Code-signing Certificate</strong> provides our application a baselinelevel of security and trust. While it verifies the software publisher's identityand ensures that the code has not been tampered with since it was signed, thevalidation process is less rigorous than EV certificates. It shows a warningduring installation that goes away once enough users have installed ourapplication and we've built up trust.</p><p><strong>Extended Validation (EV) Code-signing Certificates</strong>, on the other hand, offerthe highest level of security and trust. These certificates require a morerigorous vetting process by the Certificate Authority (CA) before they areissued, ensuring that the entity requesting the certificate is thoroughlyverified. This process involves verifying the legal, physical, and operationalexistence of the entity, as well as confirming the identity of the individualrequesting the certificate.</p><p>Unlike Apple, Microsoft allows developers to purchase these certificates on theopen market. They are typically sold by the same companies that offer HTTPScertificates. Prices can vary, so it's worth shopping around. Some popularresellers include:</p><ul><li><a href="https://www.digicert.com/signing/code-signing-certificates">DigiCert code-signing certificate</a></li><li><a href="https://shop.certum.eu/data-security/code-signing-certificates/certum-ev-code-sigining.html">Certum EV code-signing certificate</a></li><li><a href="https://www.entrust.com/products/digital-signing/code-signing-certificates">Entrust code-signing certificate</a></li><li><a href="https://www.ssl.com/certificates/ev-code-signing/">SSL.com EV code-signing certificate</a></li></ul><h3>Why choose EV code-signing?</h3><p>Even though EV code-signing certificates are comparatively pricier than standardcode-signing certificates and have a rigorous vetting process. It is importantto note that, effective June 2023, the<a href="https://cabforum.org/wp-content/uploads/Baseline-Requirements-for-the-Issuance-and-Management-of-Code-Signing.v3.2.pdf">new CA/Browser Forum code-signing requirements</a>is in effect. As a result, publicly trusted Certificate Authorities (CA) willrequire that certificate requestors use an appropriately certified (FIPS 140-2level 2 or Common Criteria EAL 4+) hardware security module (HSM) to protecttheir code-signing private keys. This requirement applies to the issuance ofboth extended validation (EV) certificates and non-EV certificates.</p><p>In simple words, what this means is that standard code-signing certificates nolonger provides benefits they provided in the past. Windows will treat ourapplication as completely unsigned and display the equivalent warning dialogs.Another thing to note here is that since certificates are required to be storedon an HSM, the certificate cannot be simply downloaded onto a CI infrastructure.</p><h3>Cloud-based EV code-signing</h3><p>In the past, EV code-signing could only be performed using a physical USBdongle. Upon purchasing an EV certificate, the provider would send a physicalUSB device containing the certificate. This method required code-signing to beexecuted from a local machine, which posed flexibility challenges, especiallyfor team environments. Additionally, physical USB dongles are prone to loss ordamage.</p><p>As a solution to this, many certificate providers now offer &quot;cloud-based EVsigning,&quot; where the signing hardware is housed in their data centers, allowingus to remotely sign code.</p><p>In this blog, we will look into how we can remotely EV code-sign a Windowsapplication built using Electron, using SSL.com's<a href="https://www.ssl.com/guide/esigner-codesigntool-command-guide/">eSigner CodeSignTool</a></p><h3>Purchase and validate EV certificate from SSL.com</h3><p>For the NeetoRecord desktop application, we used EV certificate from ssl.com andhence we will use ssl.com as an example in this blog.</p><p>Once we sign into <a href="http://ssl.com/">SSL.com</a> and purchase the<a href="https://www.ssl.com/certificates/ev-code-signing/buy/">EV code-signing certificate</a>,We need to validate the certificate. For this, we need to fill out the <strong>EVsubscriber agreement</strong> and <strong>EV authorization form</strong>. Both of them can bedownloaded from<a href="https://www.ssl.com/faqs/what-are-the-requirements-for-ssl-com-ev-certificates/">here</a>.Also, make sure we have <a href="http://www.dnb.com/get-a-duns-number.html">D-U-N-S</a>number or equivalent ready before starting the validation process. To know aboutall the requirements in detail, follow<a href="https://www.ssl.com/faqs/what-are-the-requirements-for-ssl-com-ev-certificates/">this article</a>or watch this <a href="https://www.youtube.com/watch?v=y9rhsL7jZnc">Youtube video</a>.</p><h3>eSigner and CodeSignTool</h3><p><a href="https://www.ssl.com/esigner/">eSigner</a> is SSL.com's cloud signing service thatallows remote access to its HSM hardware from anywhere. We can use our SSL.comsigning credentials to access it.</p><p><a href="https://www.ssl.com/guide/esigner-codesigntool-command-guide/">CodeSignTool</a> isa command-line tool for remotely signing various types of scripts like MSIinstallers, Microsoft Authenticode etc., with eSigner EV code-signingcertificates. Hashes of the files are sent to SSL.com for signing so that thecode itself is not sent. This is ideal where sensitive files need to be signedbut should not be sent over the wire for signing. CodeSignTool is also ideal forautomated batch processes for high-volume signings or integration into existingCI/CD pipeline workflows.</p><h3>Enroll in eSigner</h3><p>To be able to code-sign the application using eSigner, we need to first enrollin eSigner. To do so,</p><ul><li>Navigate to an issued Code-signing order in our SSL.com account. Note that theorder is labeled eSigner Ready.<img src="/blog_images/2024/ev-code-sign-windows-application-ssl-com/esigner-code-signing-01.png" alt="esigner ready"></li><li>Click one of the download links.<img src="/blog_images/2024/ev-code-sign-windows-application-ssl-com/esigner-code-signing-02.png" alt="download link"></li><li>Create and confirm a 4-digit PIN and click the create PIN button.<img src="/blog_images/2024/ev-code-sign-windows-application-ssl-com/esigner-code-signing-03.png" alt="set pin"></li><li>Our certificate will be generated, and after a few moments, a QR code willappear above the certificate downloads table.<img src="/blog_images/2024/ev-code-sign-windows-application-ssl-com/esigner-totp-secret.png" alt="QR code"></li></ul><p>When the eSigner QR code is displayed for our certificate, copy and save thesecret code value shown in a safe location. This is the TOTP (time-basedone-time password) secret associated with our eSigner certificate. Just as 2FAauthentication software like Authy can scan this value from the QR code togenerate valid OTPs for code-signing, CodeSignTool can also use it to generateOTPs are automatically generated during the signing process. In the nextsection, we will use this secret code as <code>totp_secret</code> while integrating withCodeSignTool.</p><p>For more information, check out these articles:<a href="https://www.ssl.com/guide/remote-ev-code-signing-with-esigner/">How to Enroll in eSigner</a>and<a href="https://www.ssl.com/how-to/automate-esigner-ev-code-signing/">Automate eSigner EV Code-signing</a>.</p><h3>Obtain Required Credentials</h3><p>We need to provide the following credentials to the CodeSignTool for it tosuccessfully connect with eSigner and code-sign the application.</p><p>If we haven't purchased a certificate yet and want to try out how it works,SSL.com offers<a href="https://www.ssl.com/guide/esigner-demo-credentials-and-certificates/">eSigner Demo Credentials and Certificates</a>.</p><ul><li>username</li><li>password</li><li>totp_secret</li><li>credential_id</li></ul><p>For <code>username</code> and <code>password</code>, use the same credentials we used to sign in toSSL.com. If needed, we can also add additional users to the same account. Forthe <code>totp_secret</code>, use the secret code we saved during the eSigner enrollmentprocess.</p><p><img src="/blog_images/2024/ev-code-sign-windows-application-ssl-com/esigner-totp-secret.png" alt="QR code"></p><p>To obtain the <code>credential_id</code>, navigate to the certificate details section onthe SSL.com dashboard. The <code>credential_id</code> will be listed under<code>SIGNING CREDENTIALS</code>.</p><p><img src="/blog_images/2024/ev-code-sign-windows-application-ssl-com/ssl-com-credential-id.png" alt="credential id"></p><p>After acquiring all the credentials, add them to GitHub Secrets with thefollowing names:</p><ul><li><code>username</code> -&gt; <code>WINDOWS_SIGN_USER_NAME</code></li><li><code>password</code> -&gt; <code>WINDOWS_SIGN_USER_PASSWORD</code></li><li><code>totp_secret</code> -&gt; <code>WINDOWS_SIGN_USER_TOTP</code></li><li><code>credential_id</code> -&gt; <code>WINDOWS_SIGN_CREDENTIAL_ID</code></li></ul><p>Load up these secrets as environment variables in our Github Action workflow.</p><pre><code class="language-yml">- name: Publish releases    env:      AWS_ACCESS_KEY_ID: ${{secrets.AWS_ACCESS_KEY}}      AWS_SECRET_ACCESS_KEY: ${{secrets.AWS_SECRET}}      WINDOWS_SIGN_USER_NAME: ${{ secrets.WINDOWS_SIGN_USER_NAME }}      WINDOWS_SIGN_USER_PASSWORD: ${{ secrets.WINDOWS_SIGN_USER_PASSWORD }}      WINDOWS_SIGN_USER_TOTP: ${{ secrets.WINDOWS_SIGN_USER_TOTP }}      WINDOWS_SIGN_CREDENTIAL_ID: ${{ secrets.WINDOWS_SIGN_CREDENTIAL_ID }}    run: npm exec electron-builder -- --publish always -mwl</code></pre><h3>Integrate CodeSignTool</h3><p>The Windows version of CodeSignTool is provided as a batch file(<code>CodeSignTool.bat</code>), while the Linux/macOS version is available as a shellscript (<code>CodeSignTool.sh</code>). We can find the download links and more detailsabout CodeSignTool in<a href="https://www.ssl.com/guide/esigner-codesigntool-command-guide/">this article</a></p><p>For our purposes, we'll be using the shell script. Download the Linux/macOSversion, unzip it, and place it in the root directory of our Electron project.</p><p>To integrate the <code>CodeSignTool</code> into the Electron build process, create a script(<code>./scripts/windows-sign.mjs</code>) that runs the <code>CodeSignTool</code> shell script.</p><pre><code class="language-js">import path from &quot;path&quot;;import fs from &quot;fs&quot;;import childProcess from &quot;child_process&quot;;const TEMP_DIR = path.join(__dirname, &quot;../release&quot;, &quot;temp&quot;);if (!fs.existsSync(TEMP_DIR)) {  fs.mkdirSync(TEMP_DIR);}const sign = file =&gt; {  const USER_NAME = process.env.WINDOWS_SIGN_USER_NAME;  const USER_PASSWORD = process.env.WINDOWS_SIGN_USER_PASSWORD;  const CREDENTIAL_ID = process.env.WINDOWS_SIGN_CREDENTIAL_ID;  const USER_TOTP = process.env.WINDOWS_SIGN_USER_TOTP;  if (USER_NAME &amp;&amp; USER_PASSWORD &amp;&amp; USER_TOTP &amp;&amp; CREDENTIAL_ID) {    console.log(`Windows code-signing ${file.path}`);    const { name, dir } = path.parse(file.path);    const tempFile = path.join(TEMP_DIR, name);    const setDir = `cd ./CodeSignTool-v1.3.0`;    const signFile = `./CodeSignTool.sh sign -input_file_path=&quot;${file.path}&quot; -output_dir_path=&quot;${TEMP_DIR}&quot; -credential_id=${CREDENTIAL_ID}   -username=&quot;${USER_NAME}&quot; -password=&quot;${USER_PASSWORD}&quot; -totp_secret=&quot;${USER_TOTP}&quot;`;    const moveFile = `mv &quot;${tempFile}.exe&quot; &quot;${dir}&quot;`;    childProcess.execSync(`${setDir} &amp;&amp; ${signFile} &amp;&amp; ${moveFile}`, {      stdio: &quot;inherit&quot;,    });  } else {    console.warn(`windows-sign.js - Can't sign file, credentials are missing`);    process.exit(1);  }};export default sign;</code></pre><p>The script exports a <code>sign</code> function that accepts an object containing the pathto the file that needs to be signed. It reads all the necessary credentials fromenvironment variables and then passes these credentials along with the filepath.</p><p>To finish up, pass this script to the <code>electron-builder</code> configuration.</p><pre><code class="language-json"> &quot;build&quot;: {    &quot;productName&quot;: &quot;MyProduct&quot;,     &quot;win&quot;: {      &quot;target&quot;: {        &quot;target&quot;: &quot;nsis&quot;,        &quot;arch&quot;: [          &quot;x64&quot;        ]      },      &quot;signingHashAlgorithms&quot;: [        &quot;sha256&quot;      ],      &quot;sign&quot;: &quot;./scripts/windows-sign.mjs&quot;,      &quot;publisherName&quot;: &quot;Neeto LLC&quot;,      &quot;artifactName&quot;: &quot;${productName}-Setup-${version}.${ext}&quot;    }, }</code></pre><p>Here, we pass the script path to the <code>win.sign</code> field in the <code>electron-builder</code>configuration. Once added, <code>electron-builder</code> will call this script for eachfile that needs to be signed.</p><p>Great! With this, we've completed the EV code-signing process for our Windowsapplication. Now, when we run the GitHub Action workflow, it will successfullycode-sign our Windows application.</p><h3>SSL.com pricing</h3><p>As mentioned earlier, EV code signing certificates are expensive. SSL.comcharges $349 per year for an EV code signing certificate, with discountsavailable for multi-year purchases. For more information, visit:https://www.ssl.com/certificates/ev-code-signing/buy/.</p><p>In addition to the certificate cost, we must also subscribe to the eSigner cloudsigning service, which is a subscription-based service. The Tier 1 plan costs$100 per month and allows a maximum of 10 files to be signed, equating to aminimum of $10 per signing. An Electron application requires four files to besigned per build. This means we can only build an Electron application twice permonth under the Tier 1 plan.</p><p>If we need to build the application more than twice per month, we recommendupgrading to Tier 2. This plan costs $300 per month and allows up to 100 filesignings, reducing the cost to $3 per signing. For more information, visit:https://www.ssl.com/guide/esigner-pricing-for-code-signing/</p><p><img src="/blog_images/2024/ev-code-sign-windows-application-ssl-com/esigner_pricing.png" alt="esigner pricing"></p>]]></content>
    </entry><entry>
       <title><![CDATA[Using native modules in Electron]]></title>
       <author><name>Farhan CK</name></author>
      <link href="https://www.bigbinary.com/blog/native-modules-electron"/>
      <updated>2024-12-11T12:00:00+00:00</updated>
      <id>https://www.bigbinary.com/blog/native-modules-electron</id>
      <content type="html"><![CDATA[<p><em>Recently, we built <a href="https://neetorecord.com/neetorecord/">NeetoRecord</a>, a loomalternative. The desktop application was built using Electron. In a series ofblogs, we capture how we built the desktop application and the challenges we raninto. This blog is part 8 of the blog series. You can also read about<a href="https://www.bigbinary.com/blog/sync-store-main-renderer-electron">part 1</a>,<a href="https://www.bigbinary.com/blog/publish-electron-application">part 2</a>,<a href="https://www.bigbinary.com/blog/video-background-removal">part 3</a>,<a href="https://www.bigbinary.com/blog/electron-multiple-browser-windows">part 4</a>,<a href="https://www.bigbinary.com/blog/code-sign-notorize-mac-desktop-app">part 5</a>,<a href="https://www.bigbinary.com/blog/deep-link-electron-app">part 6</a>,<a href="https://www.bigbinary.com/blog/request-camera-micophone-permission-electron">part 7</a>and<a href="https://www.bigbinary.com/blog/ev-code-sign-windows-application-ssl-com">part 9</a>.</em></p><p>Native modules allow developers to access low-level APIs like hardwareinteraction, native GUI components, or other system-specific features. BecauseElectron applications run across different platforms (Windows, macOS, Linux),native modules must be compiled for each target platform. This can introducechallenges in cross-platform development.</p><p>To simplify this process, Electron provides tools like<a href="https://github.com/electron/rebuild">electron-rebuild</a> to automate therecompilation of native modules against Electron's custom Node.js environment,ensuring compatibility and stability in the Electron application.</p><h3>How to use electron-rebuild</h3><p><code>electron-rebuild</code> can automatically determine the version of Electron andhandle the manual steps of downloading headers and rebuilding native modules forour app.</p><p>To do a manual rebuild, run the below command.</p><pre><code class="language-bash">./node_modules/.bin/electron-rebuild// If we are on windows.\node_modules\.bin\electron-rebuild.cmd</code></pre><p>This process should be run after each native package is installed. We can addthis command to the <code>postinstall</code> script to automate it.</p><pre><code class="language-json">&quot;scripts&quot;: {    &quot;postinstall&quot;: &quot;./node_modules/.bin/electron-rebuild&quot;    //...others}</code></pre><p>Since Electron uses Chromium browser windows as the user interface, we need toexclude any native modules from being bundled in the renderer process, whichruns inside the browser window. To achieve this, we need to separate frontendmodules from native modules and install them in separate <code>node_modules</code> folders.</p><h3>Two package.json structure</h3><p>To tackle this problem, Electron developers started using two <code>package.json</code>structure, where the first one, which sits at the root of the project, includesthe <code>dependencies</code> that are needed for the user interface and all the<code>devDependencies</code> that are needed to develop, build, and package theapplication.</p><pre><code class="language-json">// root package.json{  &quot;name&quot;: &quot;my-app&quot;,  &quot;version&quot;: &quot;1.0.0&quot;,  &quot;description&quot;: &quot;A sample application&quot;,  &quot;license&quot;: &quot;Apache-2.0&quot;,  &quot;main&quot;: &quot;./src/main/main.mjs&quot;,  &quot;dependencies&quot;: {    &quot;react&quot;: &quot;^18.2.0&quot;,    &quot;react-router-dom&quot;: &quot;5.3.3&quot;  },  &quot;devDependencies&quot;: {    &quot;electron&quot;: &quot;^31.2.1&quot;,    &quot;electron-builder&quot;: &quot;^25.0.1&quot;  }}</code></pre><p>And a second <code>package.json</code> file, located at <code>./app/package.json</code>, includes allthe native dependencies that should only run in a Node.js environment. The<code>electron-rebuild postinstall</code> script we discussed should be added to the<code>./app/package.json</code>.</p><pre><code class="language-json">// ./app/package.json{  &quot;name&quot;: &quot;my-app&quot;,  &quot;version&quot;: &quot;1.0.0&quot;,  &quot;description&quot;: &quot;A sample application&quot;,  &quot;license&quot;: &quot;Apache-2.0&quot;,  &quot;main&quot;: &quot;./dist/main/main.js&quot;,  &quot;scripts&quot;: {    &quot;postinstall&quot;: &quot;./node_modules/.bin/electron-rebuild&quot;  },  &quot;dependencies&quot;: {    &quot;sqlite3&quot;: &quot;^5.1.7&quot;,    &quot;sharp&quot;: &quot;^0.33.5&quot;  }}</code></pre><p>We can add a <code>postinstall</code> script in the root to automatically install the<code>dependencies</code> listed in <code>./app/package.json</code> when installing the root<code>package.json</code>.</p><pre><code class="language-json">// root package.json{  &quot;name&quot;: &quot;my-app&quot;,  &quot;version&quot;: &quot;1.0.0&quot;,  &quot;description&quot;: &quot;A sample application&quot;,  &quot;license&quot;: &quot;Apache-2.0&quot;,  &quot;main&quot;: &quot;./src/main/main.mjs&quot;,  &quot;dependencies&quot;: {    &quot;react&quot;: &quot;^18.2.0&quot;,    &quot;react-router-dom&quot;: &quot;5.3.3&quot;,  }  &quot;devDependencies&quot;: {    &quot;electron&quot;: &quot;^31.2.1&quot;,    &quot;electron-builder&quot;: &quot;^25.0.1&quot;,  },  &quot;scripts&quot;: {    &quot;postinstall&quot;: &quot;yarn --cwd ./app install&quot;  }}</code></pre><p>Now, if we run the <code>yarn install</code> from the root, after installing rootdependencies, it will install native dependencies in the <code>app/</code> folder and thenrebuild the native packages. All in one command.</p><p>When building, we should output the frontend bundles to the <code>app/dist</code> folder.This way, when packaging, both our native packages and other dependencies willbe contained within the app folder. Read<a href="https://www.bigbinary.com/blog/publish-electron-application">this blog</a> tolearn more about how to build and publish an Electron application.</p><pre><code class="language-javascript">electron-app assets app    node_modules    dist    package.json src node_modules package.json</code></pre><p>This approach works well while packaging the app, but during development, howcan we access the native modules? To solve this, we need to create a symlinkfrom <code>app/node_modules</code> to <code>src/main/node_modules</code>. We can create a script tohandle this.</p><pre><code class="language-js">// ./scripts/link-modules.mjsimport fs from &quot;fs&quot;;fs.symlinkSync(&quot;./app/node_modules&quot;, &quot;./src/main/node_modules&quot;, &quot;junction&quot;);</code></pre><p>We can run this script in <code>postinstall</code> as well:</p><pre><code class="language-json">// ./app/package.json{  &quot;name&quot;: &quot;my-app&quot;,  &quot;version&quot;: &quot;1.0.0&quot;,  &quot;description&quot;: &quot;A sample application&quot;,  &quot;license&quot;: &quot;Apache-2.0&quot;,  &quot;main&quot;: &quot;./dist/main/main.js&quot;,  &quot;scripts&quot;: {    &quot;link-modules&quot;: &quot;node ../scripts/link-modules.mjs&quot;,    &quot;postinstall&quot;: &quot;./node_modules/.bin/electron-rebuild &amp;&amp; yarn link-modules&quot;  },  &quot;dependencies&quot;: {    &quot;sqlite3&quot;: &quot;^5.1.7&quot;,    &quot;sharp&quot;: &quot;^0.33.5&quot;  }}</code></pre><h3>Problem with two package.json structure</h3><p>In most JavaScript projects, metadata such as <code>version</code>, <code>name</code>, and<code>description</code> is typically stored in the root <code>package.json</code>. However, in thiscase, when packaging the app, we'll be including <code>./app/package.json</code> instead ofthe root <code>package.json</code>. This means all metadata should be in<code>./app/package.json</code> rather than in the root file.</p><p>This approach poses a challenge, particularly with tasks like automatic versionbumping. When updating the version or other metadata, changes need to be made intwo places, which can lead to inconsistencies.</p><p>To simplify this process and avoid maintaining two separate <code>package.json</code>files, we can dynamically create <code>./app/package.json</code>, allowing us to manageeverything in one place. Since we cannot add native dependencies directly to theroot <code>dependencies</code>, we can introduce a new field called <code>nativeDependencies</code>.When dynamically generating <code>./app/package.json</code>, we can copy the<code>nativeDependencies</code> into the <code>dependencies</code> field of <code>./app/package.json</code>.</p><p>This can be accomplished by creating a script to automate the process:</p><pre><code class="language-js">// ./script/create-native-package-json.jsimport fse from &quot;fs-extra&quot;;const packageJson = JSON.parse(fse.readFileSync(&quot;./package.json&quot;, &quot;utf8&quot;));const APP_DIR = &quot;./app/&quot;;const releaseJson = {  name: packageJson.name,  version: packageJson.version,  description: packageJson.description,  license: packageJson.license,  author: packageJson.author,  main: &quot;./dist/main/main.js&quot;,  scripts: {    postinstall: &quot;/node_modules/.bin/electron-rebuild &amp;&amp; yarn link-modules&quot;,    &quot;link-modules&quot;: &quot;node ../scripts/link-modules.mjs&quot;,  },  dependencies: packageJson.nativeDependencies,};fse.mkdirSync(APP_DIR, { recursive: true });fse.writeFileSync(  APP_DIR + &quot;package.json&quot;,  JSON.stringify(releaseJson, null, 2));</code></pre><p>In the script, we first fetch the root <code>package.json</code>, create a JSON file, andcopy all the metadata. We then update the <code>main</code> field to point to the compiledversion of <code>main.js</code>, copy <code>nativeDependencies</code> into the <code>dependencies</code>, and addthe <code>postinstall</code> script for electron-rebuild and node_modules linking wediscussed earlier.</p><p>We can run this script in the root <code>postinstall</code>, so that if we make any changesto <code>package.json</code>, it will be updated in <code>./app/package.json</code> during<code>yarn install</code>.</p><pre><code class="language-json">// root package.json&quot;scripts&quot;: {    &quot;nativeInstall&quot;: &quot;yarn --cwd ./app install&quot;,    &quot;postinstall&quot;: &quot;node ./script/create-native-package-json.js &amp;&amp; yarn nativeInstall&quot;,    //...others}</code></pre>]]></content>
    </entry><entry>
       <title><![CDATA[Building deep-links in Electron application]]></title>
       <author><name>Farhan CK</name></author>
      <link href="https://www.bigbinary.com/blog/deep-link-electron-app"/>
      <updated>2024-11-26T12:00:00+00:00</updated>
      <id>https://www.bigbinary.com/blog/deep-link-electron-app</id>
      <content type="html"><![CDATA[<p><em>Recently, we built <a href="https://neetorecord.com/neetorecord/">NeetoRecord</a>, a loomalternative. The desktop application was built using Electron. In a series ofblogs, we capture how we built the desktop application and the challenges we raninto. This blog is part 6 of the blog series. You can also read about<a href="https://www.bigbinary.com/blog/sync-store-main-renderer-electron">part 1</a>,<a href="https://www.bigbinary.com/blog/publish-electron-application">part 2</a>,<a href="https://www.bigbinary.com/blog/video-background-removal">part 3</a>,<a href="https://www.bigbinary.com/blog/electron-multiple-browser-windows">part 4</a>,<a href="https://www.bigbinary.com/blog/code-sign-notorize-mac-desktop-app">part 5</a>,<a href="https://www.bigbinary.com/blog/request-camera-micophone-permission-electron">part 7</a><a href="https://www.bigbinary.com/blog/native-modules-electron">part 8</a> and<a href="https://www.bigbinary.com/blog/ev-code-sign-windows-application-ssl-com">part 9</a>.</em></p><p>When developing a desktop application, including every feature directly withinthe app is often unnecessary. Instead, we can offload some tasks such aslogin/signup to a web application. And from the web app, create deep-links tothe desktop app. We can also create shareable links that opens specific contenton the app.</p><p>In this blog, we are going to discuss how to create deep-links in our<a href="https://electronjs.org/">Electron</a> application.</p><h2>Register a custom protocol</h2><p>A protocol is a custom URL scheme that an application can handle, similar to howbrowsers handle protocols like <code>http</code>, <code>https</code>, <code>mailto</code>, or <code>ftp</code>. Everyoperating system supports the handling of custom protocols. We can register anapplication as a default handler for a custom protocol with the operatingsystem. Electron provides a simple API to register a default protocol client.</p><pre><code class="language-js">if (process.defaultApp) {  if (process.argv.length &gt;= 2) {    app.setAsDefaultProtocolClient(&quot;my-app&quot;, process.execPath, [      path.resolve(process.argv[1]),    ]);  }} else {  app.setAsDefaultProtocolClient(&quot;my-app&quot;);}</code></pre><p>The code snippet above is a simple example of registering a default protocolclient. In the example, we registered a custom protocol named <code>my-app</code> with theoperating system. It's important to note that the application must be properlypackaged and installed so that the operating system can correctly handle andlaunch the registered app.</p><p>We used <code>process.defaultApp</code> instead of <code>app.isPackaged</code> because Electron allowsus to run a packaged app using the <code>electron .</code> command. In such cases, we needto provide the execution path for the system to recognize the app. However, ifthe app is running in normal mode, simply calling<code>app.setAsDefaultProtocolClient</code> is sufficient.</p><h2>Handling custom protocol</h2><p>Though registering the protocol is simple and the same for every platform, thereare some differences when it comes to handling it.</p><p><strong>For macOS</strong>, we need to listen to the <code>open-url</code> event.</p><pre><code class="language-js">const handleCustomUrl = url =&gt; {  // Handle url};app.on(&quot;open-url&quot;, (event, url) =&gt; {  handleCustomUrl(url);});</code></pre><p><strong>On both Windows and Linux</strong>, if the application is up and running, a<code>second-instance</code> event is emitted instead of <code>open-url</code> when a protocol requestis received. This means a new instance of our app will be created. If we want toavoid this behavior and notify the existing instance instead, we'll need toensure the app runs as a single instance. We can achieve this by using<code>requestSingleInstanceLock</code>.</p><pre><code class="language-js">const gotTheLock = app.requestSingleInstanceLock();if (!gotTheLock) {  app.quit();}</code></pre><p>The above code ensures that our Windows or Linux app runs as a single instance.If a user attempts to open a new instance, that instance will try to acquire thesingle instance lock. If it fails, the newly created instance will simply quit.</p><pre><code class="language-js">const gotTheLock = app.requestSingleInstanceLock();if (!gotTheLock) {  app.quit();} else {  app.on(&quot;second-instance&quot;, (event, commands, workingDir) =&gt; {    handleCustomUrl(commands.pop());  });}</code></pre><p>If we successfully acquire the single instance lock, it means we're running thefirst instance of the app. When a user attempts to open another instance,Electron emits a <code>second-instance</code> event to the existing instance. The secondargument of this event contains an array of command-line arguments, and we canretrieve the custom URL from the last item in this array.</p><p>We've addressed the case where the app is already running, but what happens ifthe app is completely closed and a protocol request is made? On <strong>macOS</strong>,there's nothing extra to do; the app will launch, and the <code>open-url</code> event willbe triggered automatically.</p><p>However, for <strong>Windows and Linux</strong>, the behavior is different. The<code>second-instance</code> event won't be triggered since the app is starting for thefirst time. Instead, we can retrieve the custom URL from <code>process.argv</code>, as theapp will start with the protocol URL passed as one of its parameters. To handlethis case, we need to check <code>process.argv</code> when the app is started.</p><pre><code class="language-js">const handleCustomUrl = url =&gt; {  // Handle url};app.whenReady().then(() =&gt; {  const customUrl = process.argv.find(item =&gt; item.startsWith(&quot;my-app://&quot;));  if (customUrl) {    handleCustomUrl(customUrl);  }});</code></pre><p>Here, when the app is ready, we look for an item in <code>process.argv</code> that startswith our URL scheme (<code>my-app://</code>), if yes we can confirm that this instance isstarted when a protocol request is received.</p><p>Great! Here's the complete solution that works across all platforms, whether theapp is already running or not.</p><pre><code class="language-js">if (process.defaultApp) {  if (process.argv.length &gt;= 2) {    app.setAsDefaultProtocolClient(&quot;my-app&quot;, process.execPath, [      path.resolve(process.argv[1]),    ]);  }} else {  app.setAsDefaultProtocolClient(&quot;my-app&quot;);}const handleCustomUrl = url =&gt; {  // Handle url};app.on(&quot;open-url&quot;, (event, url) =&gt; {  handleCustomUrl(url);});const gotTheLock = app.requestSingleInstanceLock();if (!gotTheLock) {  app.quit();} else {  app.on(&quot;second-instance&quot;, (event, commands, workingDir) =&gt; {    handleCustomUrl(commands.pop());  });}app.whenReady().then(() =&gt; {  const customUrl = process.argv.find(item =&gt; item.startsWith(&quot;my-app://&quot;));  if (customUrl) {    handleCustomUrl(customUrl);  }});</code></pre><h2>Packaging</h2><p>On macOS and Linux, this feature is only functional when our app is packaged; itwon't work during development when launching from the command line. To ensureproper functionality, we must update the macOS <code>Info.plist</code> file and the Linux<code>.desktop</code> file to include the new protocol handler when packaging our app. Thisallows the operating system to recognize and handle the custom URLs correctly.</p><p><code>electron-builder</code> handles this internally when packaging the app; We just needto configure the <code>electron-builder</code> accordingly. To learn more about how topackage our app using <code>electron-builder</code>, check out<a href="https://www.bigbinary.com/blog/publish-electron-application">this blog</a>.</p><pre><code class="language-json">&quot;build&quot;: {    &quot;productName&quot;: &quot;NeetoRecord&quot;,    &quot;appId&quot;: &quot;com.neeto.neetoRecord&quot;,    &quot;protocols&quot;: {      &quot;name&quot;: &quot;my-app-protocol&quot;,      &quot;schemes&quot;: [        &quot;my-app&quot;      ]    },    &quot;win&quot;: {...},    &quot;linux&quot;: {...},    &quot;mac&quot;: {...}}</code></pre><p>We can use the <code>protocols</code> field for that, give a name, then pass an array ofURL schemes the app supports, and the rest <code>electron-builder</code> will handle it.</p>]]></content>
    </entry><entry>
       <title><![CDATA[How to code-sign and notarize an Electron application for macOS]]></title>
       <author><name>Farhan CK</name></author>
      <link href="https://www.bigbinary.com/blog/code-sign-notorize-mac-desktop-app"/>
      <updated>2024-11-19T12:00:00+00:00</updated>
      <id>https://www.bigbinary.com/blog/code-sign-notorize-mac-desktop-app</id>
      <content type="html"><![CDATA[<p><em>Recently, we built <a href="https://neetorecord.com/neetorecord/">NeetoRecord</a>, a loomalternative. The desktop application was built using Electron. In a series ofblogs, we capture how we built the desktop application and the challenges we raninto. This blog is part 5 of the blog series. You can also read about<a href="https://www.bigbinary.com/blog/sync-store-main-renderer-electron">part 1</a>,<a href="https://www.bigbinary.com/blog/publish-electron-application">part 2</a>,<a href="https://www.bigbinary.com/blog/video-background-removal">part 3</a>,<a href="https://www.bigbinary.com/blog/electron-multiple-browser-windows">part 4</a>,<a href="https://www.bigbinary.com/blog/deep-link-electron-app">part 6</a>,<a href="https://www.bigbinary.com/blog/request-camera-micophone-permission-electron">part 7</a><a href="https://www.bigbinary.com/blog/native-modules-electron">part 8</a> and<a href="https://www.bigbinary.com/blog/ev-code-sign-windows-application-ssl-com">part 9</a>.</em></p><p>macOS identifies applications that are not code-signed and notarized as beingfrom unknown publishers and blocks their installation. Code-signing allows macOSto recognize the application's creator. Notarization, an additional step,provides extra credibility and security, ensuring a safer experience for users.</p><h3>What is code-signing?</h3><p>Code-signing is the process of generating a unique digital fingerprint of thecode using a cryptographic hash function. This fingerprint is combined with acertificate from a trusted Certificate Authority (CA) to create the digitalsignature. When users download or execute the software, the operating systemverifies this signature to confirm its authenticity.</p><p>Apple prefers developers to use certificates issued through the Apple DeveloperProgram to sign macOS applications. This is because macOS verifies signaturesagainst Apple's own Certificate Authority. If a third-party certificate is used,macOS might not recognize it as trusted, leading to warnings or blocking theapplication from running due to<a href="https://support.apple.com/en-in/guide/security/sec5599b66df/web">Gatekeeper</a>,Apple's security feature.</p><h3>Enroll in the Apple developer program</h3><p>We should enroll in the Apple developer program (which costs $99 per year) tocreate a certificate that we can use to code-sign our application. We can follow<a href="https://developer.apple.com/programs/enroll/">this link</a> to know what we needto enroll in the Apple developer program.</p><h3>Apple certificates</h3><p>Apple provides two main types of code-signing certificates:</p><ul><li><strong>Developer ID Certificate:</strong> Used to sign apps distributed outside the MacApp Store. Apps signed with this can be gatekeeper-approved.</li><li><strong>Mac App Distribution Certificate:</strong> Required for submitting apps to the MacApp Store. Apple will re-sign the application after review and approval fordistribution on the Mac App Store.</li></ul><p>In this blog, we will look into how to code-sign an<a href="https://electronjs.org/">Electron</a> application using <strong>Developer IDCertificate</strong>.</p><h3>Create a Developer ID certificate</h3><p>To create a Developer ID certificate, we can follow Apple's detailed guide on<a href="https://developer.apple.com/help/account/create-certificates/create-developer-id-certificates/">how to create a Developer ID certificate</a>.</p><p>Once we've successfully created the certificate and downloaded the <code>.cer</code> file,the next step is to convert this file into a <code>.p12</code> format.</p><p>First, we'll need to convert the <code>.cer</code> file into a <code>.pem</code> format. We can dothis using <code>openssl</code>.</p><pre><code class="language-bash">openssl x509 -in certificate.cer -inform DER -out certificate.pem -outform PEM</code></pre><p>Then, use the <code>.pem</code> file and our private <code>.key</code> to generate the <code>.p12</code> file.</p><pre><code class="language-bash">openssl pkcs12 -export -out certificate.p12 -inkey certificate-private.key -in certificate.pem</code></pre><p>When generating the <code>.p12</code> file, we'll be prompted to set a password. Save thispassword in a secure location, as we'll need it later when code-signing theapplication.</p><p>To use this certificate with our existing GitHub Action workflow to automate thedeployment process, we need to convert this <code>.p12</code> file into a base64 string.This is necessary because GitHub doesn't allow uploading files as secrets, butwe can store the base64 string instead.</p><pre><code class="language-bash">openssl base64 -in certificate.p12 -out certificate.txt</code></pre><p>The command will output the base64 version of the <code>.p12</code> file into a<code>certificate.txt</code> file. We can then add the text contents of this file as asecret in GitHub. Save the base64 string in GitHub secrets with the name<code>CSC_CONTENT</code> and password as <code>CSC_KEY_PASSWORD</code></p><h3>Update Electron build process</h3><p>To code-sign a macOS app, we just need to pass the certificate and password tothe <a href="https://www.electron.build/">electron-builder</a> <code>publish</code> command. However,since we saved our certificate as a base64 string, we need to convert it back toa <code>.p12</code> file before publishing.</p><pre><code class="language-yml"> - name: Publish releases    env:      GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}      AWS_ACCESS_KEY_ID: ${{secrets.AWS_ACCESS_KEY}}      AWS_SECRET_ACCESS_KEY: ${{secrets.AWS_SECRET}}      CSC_CONTENT: ${{ secrets.CSC_CONTENT }}      CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}    run: |      echo &quot;$CSC_CONTENT&quot; | base64 --decode &gt; certificate.p12      export CSC_LINK=&quot;./certificate.p12&quot;      npm exec electron-builder -- --publish always -mwl</code></pre><p>As mentioned above, we first decoded the base64 string back to a<code>certificate.p12</code> file. We then set the path to this file as <code>CSC_LINK,</code> which<code>electron-builder</code> expects.</p><p>Great! With everything in place, running this workflow should successfullycode-sign our application.</p><h2>Notarize</h2><p>Code-signing allows macOS to recognize the application's creator, but this aloneis insufficient. Users will still see a warning stating, <strong>&quot;macOS cannot verifyif the app is free from malware.&quot;</strong></p><p>To eliminate this warning, we need to notarize our application.<a href="https://developer.apple.com/documentation/security/notarizing-macos-software-before-distribution">Notarization</a>is a security feature introduced by Apple to ensure that macOS applications aresafe and free of malicious content. It's an additional layer of security thatbuilds on code-signing. The notarization process involves submitting our app toApple for automated security checks. Once notarized, macOS will recognize theapp as trustworthy, ensuring smooth installation and execution on users'systems, even when downloaded from outside the Mac App Store.</p><p>To notarize, we need to create an &quot;App-specific password&quot;. To create anApp-specific password:</p><ul><li>Sign in to <a href="https://appleid.apple.com/account/home">appleid.apple.com</a></li><li>In the Sign-In and Security section, select App-Specific Passwords.</li><li>Select Generate an app-specific password or select the Add button(+).<img src="/blog_images/2024/code-sign-notorize-mac-desktop-app/app-specific-1.png" alt="app specific password 1"></li><li>Then give a name for the password and click <code>Create</code>.<img src="/blog_images/2024/code-sign-notorize-mac-desktop-app/app-specific-2.png" alt="app specific password 2"></li></ul><p>A new App-Specific Password will be generated. Save it in a safe place. We willuse this password to notarize our macOS application.</p><h3>Update Electron build process</h3><p>Add <code>APPLE_APP_SPECIFIC_PASSWORD</code>, <code>TEAM_ID</code>, and <code>APPLE_ID</code> to GitHub secrets.Then load up these secrets as environment variables along with others in ourGithub Action workflow.</p><pre><code class="language-yml"> - name: Publish releases    env:      GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}      AWS_ACCESS_KEY_ID: ${{secrets.AWS_ACCESS_KEY}}      AWS_SECRET_ACCESS_KEY: ${{secrets.AWS_SECRET}}      CSC_CONTENT: ${{ secrets.CSC_CONTENT }}      CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}      TEAM_ID: ${{ secrets.TEAM_ID }}      APPLE_ID: ${{ secrets.APPLE_ID }}      APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}    run: |      echo &quot;$CSC_CONTENT&quot; | base64 --decode &gt; certificate.p12      export CSC_LINK=&quot;./certificate.p12&quot;      npm exec electron-builder -- --publish always -mwl</code></pre><p>At the time of writing this, we encountered issues with the built-in notarizefeature of <code>electron-builder</code>, so we created a custom script to handle thenotarization process.</p><pre><code class="language-js">const { notarize } = require(&quot;@electron/notarize&quot;);const { build } = require(&quot;../package.json&quot;);const notarizeMacos = async context =&gt; {  const { electronPlatformName, appOutDir } = context;  if (electronPlatformName !== &quot;darwin&quot;) return;  if (process.env.CI !== &quot;true&quot;) {    console.warn(&quot;Skipping notarizing step. Packaging is not running in CI&quot;);    return;  }  const appName = context.packager.appInfo.productFilename;  await notarize({    tool: &quot;notarytool&quot;,    appBundleId: build.appId,    appPath: `${appOutDir}/${appName}.app`,    teamId: process.env.TEAM_ID,    appleId: process.env.APPLE_ID,    appleIdPassword: process.env.APPLE_APP_SPECIFIC_PASSWORD,    verbose: true,  });  console.log(&quot;--- notarization completed ---&quot;);};exports.default = notarizeMacos;</code></pre><p>The script uses the <code>notarize</code> function from the <code>@electron/notarize</code> package.It passes the path to the generated <code>.app</code> file during the build process, alongwith the required <code>TEAM_ID</code>, <code>APPLE_ID</code>, and <code>APPLE_APP_SPECIFIC_PASSWORD</code>,which were obtained earlier.</p><p>To run the custom notarization script, disable the built-in notarization featurein the <code>electron-builder</code> configuration. Then, call this script from the<code>afterSign</code> callback to ensure it runs after the signing process is complete.</p><pre><code class="language-json">&quot;build&quot;: {    &quot;mac&quot;: {      &quot;notarize&quot;: false,      &quot;target&quot;: {        &quot;target&quot;: &quot;default&quot;,        &quot;arch&quot;: [          &quot;arm64&quot;,          &quot;x64&quot;        ]      },    },    &quot;afterSign&quot;: &quot;./scripts/notarize.js&quot;,}</code></pre><p>Great! We have successfully code-signed and notarized our macOS application.Now, macOS will trust our application, and an added benefit of this process isthat it allows us to auto-update our application seamlessly, ensuring that usersalways have the latest version.</p>]]></content>
    </entry><entry>
       <title><![CDATA[Configuring webpack to handle multiple browser windows in Electron]]></title>
       <author><name>Farhan CK</name></author>
      <link href="https://www.bigbinary.com/blog/electron-multiple-browser-windows"/>
      <updated>2024-11-12T12:00:00+00:00</updated>
      <id>https://www.bigbinary.com/blog/electron-multiple-browser-windows</id>
      <content type="html"><![CDATA[<p><em>Recently, we built <a href="https://neetorecord.com/neetorecord/">NeetoRecord</a>, a loomalternative. The desktop application was built using Electron. In a series ofblogs, we capture how we built the desktop application and the challenges we raninto. This blog is part 4 of the blog series. You can also read about<a href="https://www.bigbinary.com/blog/sync-store-main-renderer-electron">part 1</a>,<a href="https://www.bigbinary.com/blog/publish-electron-application">part 2</a>,<a href="https://www.bigbinary.com/blog/video-background-removal">part 3</a>,<a href="https://www.bigbinary.com/blog/code-sign-notorize-mac-desktop-app">part 5</a>,<a href="https://www.bigbinary.com/blog/deep-link-electron-app">part 6</a>,<a href="https://www.bigbinary.com/blog/request-camera-micophone-permission-electron">part 7</a><a href="https://www.bigbinary.com/blog/native-modules-electron">part 8</a> and<a href="https://www.bigbinary.com/blog/ev-code-sign-windows-application-ssl-com">part 9</a>.</em></p><p>When developing desktop applications with Electron, managing multiple browserwindows within a single app is often necessary. Whether we need to displaydifferent types of content or create a more complex user interface, handlingmultiple windows efficiently can be challenging.</p><p>In this blog, we'll explore how to configure Webpack to manage multiple browserwindows in our Electron application, ensuring that each window operates smoothlyin our project.</p><h3>Configuring Webpack for a Single Browser Window</h3><p>Before diving into the setup for multiple windows, let's first review how toconfigure Webpack for a single browser window. This example focuses on therenderer process, which is responsible for rendering the UI and handlinginteractions. If interested in learning how to configure Webpack for the entireElectron app, including the main process, check out<a href="https://www.bigbinary.com/blog/publish-electron-application">this blog</a>.</p><p>Consider the following typical folder structure for an Electron project:</p><pre><code class="language-js">electron-app assets app src    main      main.js    renderer      App.jsx      index.ejs node_modules package.json</code></pre><p>This structure separates the code for the <code>main</code> and <code>renderer</code> processes, whichis a standard practice in Electron projects.</p><p>Here's how we can configure Webpack for a single browser window:</p><pre><code class="language-js">// ./config/webpack/renderer.mjsimport webpack from &quot;webpack&quot;;import HtmlWebpackPlugin from &quot;html-webpack-plugin&quot;;const configuration = {  target: [&quot;web&quot;, &quot;electron-renderer&quot;],  entry: &quot;src/renderer/App.jsx&quot;,  output: {    path: &quot;app/dist/renderer&quot;,    publicPath: &quot;./&quot;,    filename: &quot;renderer.js&quot;,    library: {      type: &quot;umd&quot;,    },  },  module: {...},  // Module configuration (loaders, etc.)  optimization: {...},  // Optimization settings (minification, etc.)  plugins: [    // Other plugins...    new HtmlWebpackPlugin({      filename: &quot;app.html&quot;,      template: &quot;src/renderer/index.ejs&quot;,    }),  ],};export default configuration;</code></pre><p>In this configuration:</p><ul><li>The <code>target</code> is set to <code>[&quot;web&quot;, &quot;electron-renderer&quot;]</code>, enabling both standardweb and Electron renderer environments.</li><li>The <code>entry</code> specifies the entry point for the renderer process, which is<code>src/renderer/App.jsx</code>.</li><li>The output is bundled into a single file named <code>renderer.js</code>, which is storedin the <code>app/dist/renderer</code> directory. In an Electron app, it's oftenpreferable to bundle everything into a single file since the files are loadedlocally.</li><li>The <code>HtmlWebpackPlugin</code> generates an <code>app.html</code> file from a template(<code>index.ejs</code>), embedding the necessary script to load <code>renderer.js</code>.</li></ul><p>We can compile and bundle our frontend code using the following command:</p><pre><code class="language-bash">webpack --config ./config/webpack/renderer.mjs</code></pre><p>This will produce an <code>app.html</code> file in the <code>app/dist/renderer</code> directory, alongwith <code>renderer.js</code>.</p><pre><code class="language-html">&lt;!DOCTYPE html&gt;&lt;html&gt;   &lt;head&gt;      &lt;meta charset=utf-8&gt;      &lt;meta http-equiv=Content-Security-Policy content=&quot;script-src 'self' 'unsafe-inline'&quot;&gt;      &lt;title&gt;MyApp&lt;/title&gt;      &lt;script defer=defer src=./renderer.js&gt;&lt;/script&gt;   &lt;/head&gt;   &lt;body&gt;      &lt;div id=root&gt;&lt;/div&gt;   &lt;/body&gt;&lt;/html&gt;</code></pre><p>The <code>HtmlWebpackPlugin</code> correctly injects the <code>&lt;script/&gt;</code> tag to load<code>renderer.js</code>. This <code>app.html</code> can now be loaded into a browser window from the<code>main</code> process.</p><pre><code class="language-js">const appWindow = new BrowserWindow({  show: false,  width: 1408,  height: 896,});appWindow.loadURL(&quot;app/dist/renderer/app.html&quot;);appWindow.on(&quot;ready-to-show&quot;, () =&gt; {  appWindow.show();});</code></pre><h3>Configuring Webpack for Multiple Browser Windows</h3><p>With the single window setup complete, let's add another browser window to theapp. For example, let's say we want to create a <code>Settings.jsx</code> component withinthe renderer folder:</p><pre><code class="language-js">electron-app assets app src    main      main.js    renderer      App.jsx      Settings.jsx      index.ejs node_modules package.json</code></pre><p>Previously, we bundled all JavaScript code into a single <code>renderer.js</code> file.However, since we're now working with multiple windows, it makes sense to createseparate bundles for each windowone for the <code>App</code> window and another for the<code>Settings</code> window. To achieve this, we can specify multiple entry points inWebpack:</p><pre><code class="language-js">// ./config/webpack/renderer.mjsimport webpack from &quot;webpack&quot;;import HtmlWebpackPlugin from &quot;html-webpack-plugin&quot;;const configuration = {  target: [&quot;web&quot;, &quot;electron-renderer&quot;],  entry: {    app: &quot;src/renderer/App.jsx&quot;,    settings: &quot;src/renderer/Settings.jsx&quot;,  },  output: {    path: &quot;app/dist/renderer&quot;,    publicPath: &quot;./&quot;,    filename: &quot;[name].js&quot;, // Use placeholders to generate separate bundles    library: {      type: &quot;umd&quot;,    },  },  // Other configuration options...};export default configuration;</code></pre><p>In this configuration:</p><ul><li>The <code>entry</code> property now contains two entry points: <code>app</code> and <code>settings</code>.Webpack will generate separate bundles for each, named <code>app.js</code> and<code>settings.js</code> respectively.</li><li>The <code>filename</code> in the <code>output</code> section uses the <code>[name]</code> placeholder todynamically generate filenames based on the entry point names.</li></ul><p>Next, we need to generate two HTML filesone for each window. We can achievethis by adding another instance of <code>HtmlWebpackPlugin</code> to the <code>plugins</code> array:</p><pre><code class="language-js">// ./config/webpack/renderer.mjsimport webpack from &quot;webpack&quot;;import HtmlWebpackPlugin from &quot;html-webpack-plugin&quot;;const configuration = {  target: [&quot;web&quot;, &quot;electron-renderer&quot;],  entry: {    app: &quot;src/renderer/App.jsx&quot;,    settings:&quot;src/renderer/Settings.jsx&quot;  },  output: {    path: &quot;app/dist/renderer&quot;,    publicPath: &quot;./&quot;,    filename: '[name].js',    library: {      type: &quot;umd&quot;,    },  },  module: {...},  optimization: {...},  plugins: [    // Other plugins...    new HtmlWebpackPlugin({      filename: &quot;app.html&quot;,      template: &quot;src/renderer/index.ejs&quot;,      chunks: ['app'],  // Load only the 'app' bundle    }),    new HtmlWebpackPlugin({      filename: &quot;settings.html&quot;,      template: &quot;src/renderer/index.ejs&quot;,      chunks: ['settings'],  // Load only the 'settings' bundle    }),  ],};export default configuration;</code></pre><p>By specifying the <code>chunks</code> property for each <code>HtmlWebpackPlugin</code> instance, weensure that each HTML file only includes the appropriate JavaScript bundle. Thefinal output will include two HTML files:</p><pre><code class="language-html">&lt;!-- app.html --&gt;&lt;!DOCTYPE html&gt;&lt;html&gt;   &lt;head&gt;      &lt;meta charset=utf-8&gt;      &lt;meta http-equiv=Content-Security-Policy content=&quot;script-src 'self' 'unsafe-inline'&quot;&gt;      &lt;title&gt;MyApp&lt;/title&gt;      &lt;script defer=defer src=./app.js&gt;&lt;/script&gt;   &lt;/head&gt;   &lt;body&gt;      &lt;div id=root&gt;&lt;/div&gt;   &lt;/body&gt;&lt;/html&gt;&lt;!-- settings.html --&gt;&lt;!DOCTYPE html&gt;&lt;html&gt;   &lt;head&gt;      &lt;meta charset=utf-8&gt;      &lt;meta http-equiv=Content-Security-Policy content=&quot;script-src 'self' 'unsafe-inline'&quot;&gt;      &lt;title&gt;MyApp&lt;/title&gt;      &lt;script defer=defer src=./settings.js&gt;&lt;/script&gt;   &lt;/head&gt;   &lt;body&gt;      &lt;div id=root&gt;&lt;/div&gt;   &lt;/body&gt;&lt;/html&gt;</code></pre><p>Finally, from the <code>main</code> process, we can easily create two browser windows, eachwith its own renderer code:</p><pre><code class="language-js">const appWindow = new BrowserWindow({  show: false,  width: 1408,  height: 896,});appWindow.loadURL(&quot;app/dist/renderer/app.html&quot;);appWindow.on(&quot;ready-to-show&quot;, () =&gt; {  appWindow.show();});const settingsWindow = new BrowserWindow({  show: false,  width: 1408,  height: 896,});settingsWindow.loadURL(&quot;app/dist/renderer/settings.html&quot;);settingsWindow.on(&quot;ready-to-show&quot;, () =&gt; {  settingsWindow.show();});</code></pre><p>With this setup, each browser window will load its own dedicated JavaScriptbundle, ensuring that our Electron application is both efficient and modular.This approach not only makes our code easier to manage but also enhances theperformance of our application by reducing unnecessary code loading.</p>]]></content>
    </entry><entry>
       <title><![CDATA[Building and publishing an Electron application using electron-builder]]></title>
       <author><name>Farhan CK</name></author>
      <link href="https://www.bigbinary.com/blog/publish-electron-application"/>
      <updated>2024-10-22T12:00:00+00:00</updated>
      <id>https://www.bigbinary.com/blog/publish-electron-application</id>
      <content type="html"><![CDATA[<p><em>Recently, we built <a href="https://neetorecord.com/neetorecord/">NeetoRecord</a>, a loomalternative. The desktop application was built using Electron. In a series ofblogs, we capture how we built the desktop application and the challenges we raninto. This blog is part 2 of the blog series. You can also read<a href="https://www.bigbinary.com/blog/sync-store-main-renderer-electron">part 1</a>,<a href="https://www.bigbinary.com/blog/video-background-removal">part 3</a>,<a href="https://www.bigbinary.com/blog/electron-multiple-browser-windows">part 4</a>,<a href="https://www.bigbinary.com/blog/code-sign-notorize-mac-desktop-app">part 5</a>,<a href="https://www.bigbinary.com/blog/deep-link-electron-app">part 6</a>,<a href="https://www.bigbinary.com/blog/request-camera-micophone-permission-electron">part 7</a><a href="https://www.bigbinary.com/blog/native-modules-electron">part 8</a> and<a href="https://www.bigbinary.com/blog/ev-code-sign-windows-application-ssl-com">part 9</a>.</em></p><p>Building, packaging and publishing an app with the default Electron npm packagescan be quite challenging. It involves multiple packages and offers limitedcustomization. Additionally, setting up auto-updates requires significantadditional effort, often involving separate tools or services.</p><p><a href="https://www.electron.build/">electron-builder</a> is a complete solution forbuilding, packaging, and distributing <a href="https://electronjs.org/">Electron</a>applications for macOS, Windows, and Linux. It is a highly configurablealternative to the default Electron packaging process that supports auto-updateout of the box.</p><p>In this blog, we look into how we can build, package and distribute Electronapplications using <code>electron-builder</code>.</p><h3>Electron processes</h3><p>Electron has two types of processes: the <code>main</code> process and the <code>renderer</code>process. The main process acts as the entry point to the application, where wecan create a browser window and load a webpage. This webpage runs in the<code>renderer</code> process. The <code>main</code> process is written in <code>Node.js</code>, while therenderer process can be developed using JavaScript or any JS framework like<code>React</code>, <code>Vue</code>, or <code>Angular</code>.</p><h3>Project structure</h3><p>When building an Electron application, it's best to keep the <code>main</code> and<code>renderer</code> processes in separate folders since they are built separately.</p><pre><code class="language-javascript">electron-app assets release src    main      main.js    renderer      App.jsx      index.ejs node_modules package.json</code></pre><h3>Browser window preload script</h3><p>Since the <code>main</code> process is written in <code>Node.js</code>, it has access to <code>Node.js</code> andElectron APIs, but the <code>renderer</code> process does not. To bridge this gap, Electronsupports a special script called a <code>preload</code> script, which we can specify whencreating a <code>BrowserWindow</code>. This script runs in a context that has access toboth the HTML DOM and a limited subset of Node.js and Electron APIs. An example<code>preload</code> script looks like this:</p><pre><code class="language-js">import { contextBridge, ipcRenderer } from &quot;electron&quot;;const electronHandler = {  ipcRenderer: {    sendMessage(channel, ...args) {      ipcRenderer.send(channel, ...args);    },    on(channel, func) {      const subscription = (_event, ...args) =&gt; func(...args);      ipcRenderer.on(channel, subscription);      return () =&gt; {        ipcRenderer.removeListener(channel, subscription);      };    },    once(channel, func) {      ipcRenderer.once(channel, (_event, ...args) =&gt; func(...args));    },  },};contextBridge.exposeInMainWorld(&quot;electron&quot;, electronHandler);</code></pre><p>This preload script exposes <code>send</code>, <code>on</code> and <code>once</code> methods of <code>ipcRenderer</code> tothe renderer process via the <code>contextBridge</code>. It allows the renderer to sendmessages, listen for events, and handle one-time events from the main processwhile maintaining security by controlling which APIs are accessible.</p><h3>Build</h3><p>Since an Electron application has two processes, we need two separate Webpackconfigurationsone for the <code>main</code> process and another for the <code>renderer</code>process.</p><pre><code class="language-js">// ./config/webpack/main.mjsimport path from &quot;path&quot;;import webpack from &quot;webpack&quot;;import { merge } from &quot;webpack-merge&quot;;import TerserPlugin from &quot;terser-webpack-plugin&quot;;const configuration = {  target: &quot;electron-main&quot;,  entry: {    main: &quot;src/main/main.mjs&quot;,    preload: &quot;src/main/preload.mjs&quot;,  },  output: {    path: &quot;release/app/dist/main&quot;,    filename: &quot;[name].js&quot;,    library: {      type: &quot;umd&quot;,    },  },  optimization: {    minimizer: [      new TerserPlugin({        parallel: true,      }),    ],  },  node: {    __dirname: false,    __filename: false,  },};export default configuration;</code></pre><p>The above is a basic Webpack configuration for the <code>main</code> process. Webpacksupports Electron out of the box, so by setting <code>target: &quot;electron-main&quot;</code>,Webpack includes all the necessary Electron variables. Since we also have a<code>preload</code> script, we added <code>preload.mjs</code> as an entry point as well. We will beminifying the code using <code>TerserPlugin</code>.</p><p>Another important detail is that we've disabled <code>__dirname</code> and <code>__filename</code>.This prevents Webpack's handling of these variables from interfering withNode.js's native <code>__dirname</code> and <code>__filename</code>, ensuring they behave as expectedin our Electron app.</p><pre><code class="language-js">// ./config/webpack/renderer.mjsimport path from &quot;path&quot;;import webpack from &quot;webpack&quot;;import HtmlWebpackPlugin from &quot;html-webpack-plugin&quot;;import MiniCssExtractPlugin from &quot;mini-css-extract-plugin&quot;;import { BundleAnalyzerPlugin } from &quot;webpack-bundle-analyzer&quot;;import CssMinimizerPlugin from &quot;css-minimizer-webpack-plugin&quot;;import { merge } from &quot;webpack-merge&quot;;import TerserPlugin from &quot;terser-webpack-plugin&quot;;const configuration = {  target: [&quot;web&quot;, &quot;electron-renderer&quot;],  entry: &quot;src/renderer/App.jsx&quot;,  output: {    path: &quot;release/app/dist/renderer&quot;,    publicPath: &quot;./&quot;,    filename: &quot;renderer.js&quot;,    library: {      type: &quot;umd&quot;,    },  },  module: {    rules: [      {        test: /\.s?(a|c)ss$/,        use: [          MiniCssExtractPlugin.loader,          {            loader: &quot;css-loader&quot;,            options: {              modules: true,              sourceMap: true,              importLoaders: 1,            },          },          &quot;sass-loader&quot;,        ],        include: /\.module\.s?(c|a)ss$/,      },      {        test: /\.s?(a|c)ss$/,        use: [          MiniCssExtractPlugin.loader,          &quot;css-loader&quot;,          &quot;sass-loader&quot;,          &quot;postcss-loader&quot;,        ],        exclude: /\.module\.s?(c|a)ss$/,      },      // Fonts      {        test: /\.(woff|woff2|eot|ttf|otf)$/i,        type: &quot;asset/resource&quot;,      },      // Images      {        test: /\.(png|jpg|jpeg|gif)$/i,        type: &quot;asset/resource&quot;,      },      // SVG      {        test: /\.svg$/,        use: [          {            loader: &quot;@svgr/webpack&quot;,            options: {              prettier: false,              svgo: false,              svgoConfig: {                plugins: [{ removeViewBox: false }],              },              titleProp: true,              ref: true,            },          },          &quot;file-loader&quot;,        ],      },    ],  },  optimization: {    minimize: true,    minimizer: [new TerserPlugin(), new CssMinimizerPlugin()],  },  plugins: [    new MiniCssExtractPlugin({      filename: &quot;style.css&quot;,    }),    new HtmlWebpackPlugin({      filename: &quot;index.html&quot;,      template: &quot;src/renderer/index.ejs&quot;,      minify: {        collapseWhitespace: true,        removeAttributeQuotes: true,        removeComments: true,      },      isBrowser: false,      isDevelopment: false,    }),  ],};export default configuration;</code></pre><p>In the <code>renderer</code> configuration, we set the target to<code>target: [&quot;web&quot;, &quot;electron-renderer&quot;]</code>, which provides both standard web andElectron's renderer variables. Similar to a typical web application setup, weload various plugins to handle fonts, images, and SVGs and to minify CSS andJavaScript. Since JavaScript files are loaded locally in an Electronapplication, we can bundle everything into a single file called <code>renderer.js</code>instead of splitting it into multiple chunks as we would for a standard webapplication.</p><p>Our build configuration is ready; add it to <code>scripts</code> in <code>package.json</code> for easyexecution.</p><pre><code class="language-json">&quot;scripts&quot;: {    &quot;build:main&quot;: &quot;cross-env NODE_ENV=production webpack --config ./config/webpack/main.mjs&quot;,    &quot;build:renderer&quot;: &quot;cross-env NODE_ENV=production webpack --config ./config/webpack/renderer.mjs&quot;,    &quot;build&quot;: &quot;yarn build:main &amp;&amp; yarn build:renderer&quot;,}</code></pre><p>Great! Now, we can build the entire Electron application using a single<code>yarn build</code> command.</p><h3>Package</h3><p>We can configure the <code>electron-builder</code> in <code>package.json</code> using the <code>build</code>field. Below is an example configuration for an app named <code>MyApp</code>.</p><pre><code class="language-json"> &quot;build&quot;: {    &quot;productName&quot;: &quot;MyApp&quot;,    &quot;appId&quot;: &quot;com.neeto.MyApp&quot;,    &quot;directories&quot;: {      &quot;app&quot;: &quot;release/app&quot;,      &quot;buildResources&quot;: &quot;assets&quot;,      &quot;output&quot;: &quot;release/build&quot;    },     &quot;mac&quot;: {      &quot;target&quot;: {        &quot;target&quot;: &quot;default&quot;,        &quot;arch&quot;: [          &quot;arm64&quot;,          &quot;x64&quot;        ]      }    },    &quot;win&quot;: {      &quot;target&quot;: {        &quot;target&quot;: &quot;nsis&quot;,        &quot;arch&quot;: [          &quot;x64&quot;        ]      },      &quot;artifactName&quot;: &quot;${productName}-Setup-${version}.${ext}&quot;    },    &quot;linux&quot;: {      &quot;category&quot;: &quot;Utility&quot;,      &quot;target&quot;: [        {          &quot;target&quot;: &quot;rpm&quot;,          &quot;arch&quot;: [            &quot;x64&quot;          ]        },        {          &quot;target&quot;: &quot;deb&quot;,          &quot;arch&quot;: [            &quot;x64&quot;          ]        }      ],    }, }</code></pre><p>In the Webpack configuration, we specified <code>release/app</code> as the output directoryfor the compiled JS code. This directory needs to be specified in<code>electron-builder</code> so it knows where to find the compiled code during packaging.Use the <code>directories.app</code> field to specify this path, and we can also definewhere the packaged builds should be output using the <code>directories.output</code> field.</p><p>In addition to <code>directories</code>, the configuration includes settings for <code>appId</code>,<code>productName</code> and platform-specific configurations for <code>mac</code>, <code>win</code>(Windows),and <code>linux</code>. For each platform, we specify the installer target andarchitecture. This configuration will produce builds for both Intel (<code>x64</code>) andApple Silicon (<code>arm64</code>) on <strong>macOS</strong>. For <strong>Windows</strong>, it generates an <code>NSIS</code>installer targeting 64-bit architecture (<code>x64</code>). On <strong>Linux</strong>, it produces both<code>RPM</code> and <code>DEB</code> installers for 64-bit architecture (<code>x64</code>).</p><p>With the <code>electron-builder</code> configuration set, we can proceed to package ourapplication using the <code>electron-builder build</code> command.</p><pre><code class="language-json">&quot;scripts&quot;: {    &quot;build:main&quot;: &quot;cross-env NODE_ENV=production webpack --config ./webpack/main.prod.mjs&quot;,    &quot;build:renderer&quot;: &quot;cross-env NODE_ENV=production webpack --config ./webpack/renderer.prod.mjs&quot;,    &quot;build&quot;: &quot;yarn build:main &amp;&amp; yarn build:renderer&quot;,    &quot;package&quot;: &quot;yarn build &amp;&amp; electron-builder build&quot;,}</code></pre><p>We run <code>yarn build</code> before <code>electron-builder build</code> to ensure that theJavaScript code is compiled before packaging. This enables us to handle both thebuild and packaging processes in a single command.</p><h3>Publish</h3><p>To publish the app to a server where users can download and use it, we can passthe <code>--publish</code> flag to <code>electron-builder build</code> command. Before we can do that,we need to update our <code>electron-builder</code> configuration with <code>publish</code> serverinformation.</p><p>Here is an example configuration to publish the builds to an S3 bucket named<code>my-app-downloads</code>:</p><pre><code class="language-json"> &quot;build&quot;: {  // other configs  &quot;publish&quot;: [      {        &quot;provider&quot;: &quot;s3&quot;,        &quot;bucket&quot;: &quot;my-app-downloads&quot;,        &quot;path&quot;: &quot;/electron/my-app/&quot;,        &quot;region&quot;: &quot;us-east-1&quot;,        &quot;acl&quot;: null      },    ] }</code></pre><p>The <code>publish</code> field accepts an array, allowing us to publish to multiplelocations. We need to specify a provider, such as <code>s3</code>, but <code>electron-builder</code>also supports other providers like <code>github</code> and more by default. For a completelist of all publishing options, check out the<a href="https://www.electron.build/configuration/publish">publish documentation</a>.</p><p>To wrap things up, let's automate the deployment process by creating a GitHubActions workflow. Since building macOS apps is only supported on macOS, we'llneed to run the workflow from a macOS environment.</p><pre><code class="language-yml">name: Publishjobs:  publish:    runs-on: ${{ matrix.os }}    strategy:      matrix:        os: [macos-latest]    steps:      - name: Setup Java        uses: actions/setup-java@v3        with:          distribution: &quot;adopt&quot;          java-version: &quot;11&quot;      - name: Checkout git repo        uses: actions/checkout@v3      - name: Install Node and NPM        uses: actions/setup-node@v3        with:          node-version: 20          cache: npm      - name: Install rpm using Homebrew        run: brew install rpm      - name: Install and build        run: |          yarn install          yarn build      - name: Publish releases        env:          AWS_ACCESS_KEY_ID: ${{secrets.AWS_ACCESS_KEY}}          AWS_SECRET_ACCESS_KEY: ${{secrets.AWS_SECRET}}        run: npm exec electron-builder -- --publish always -mwl</code></pre><p>To successfully build for Windows and Linux from macOS, we'll need to set up aJava version and install the <code>rpm</code> package. Additionally, configure our<code>AWS_ACCESS_KEY_ID</code> and <code>AWS_SECRET_ACCESS_KEY</code> for the S3 bucket where we planto upload the builds. Once these steps are complete, we should be able to buildand publish our Electron app using the <code>electron-builder -- --publish</code> command.The <code>-mwl</code> flag indicates that the build should target macOS, Windows, andLinux.</p><h3>Auto-update</h3><p>To enable auto-updating for our application, we first need to code-sign andnotarize it. We'll cover the code-signing and notarization details in upcomingblog posts. Stay tuned!</p>]]></content>
    </entry><entry>
       <title><![CDATA[Creating a synchronized store between main and renderer process in Electron]]></title>
       <author><name>Farhan CK</name></author>
      <link href="https://www.bigbinary.com/blog/sync-store-main-renderer-electron"/>
      <updated>2024-10-01T12:00:00+00:00</updated>
      <id>https://www.bigbinary.com/blog/sync-store-main-renderer-electron</id>
      <content type="html"><![CDATA[<p><em>Recently, we built <a href="https://neetorecord.com/neetorecord/">NeetoRecord</a>, a loomalternative. The desktop application was built using Electron. In a series ofblogs, we capture how we built the desktop application and the challenges we raninto. This blog is part 1 of the blog series. You can also read<a href="https://www.bigbinary.com/blog/publish-electron-application">part 2</a>,<a href="https://www.bigbinary.com/blog/video-background-removal">part 3</a>,<a href="https://www.bigbinary.com/blog/electron-multiple-browser-windows">part 4</a>,<a href="https://www.bigbinary.com/blog/code-sign-notorize-mac-desktop-app">part 5</a>,<a href="https://www.bigbinary.com/blog/deep-link-electron-app">part 6</a>,<a href="https://www.bigbinary.com/blog/request-camera-micophone-permission-electron">part 7</a><a href="https://www.bigbinary.com/blog/native-modules-electron">part 8</a> and<a href="https://www.bigbinary.com/blog/ev-code-sign-windows-application-ssl-com">part 9</a>.</em></p><p>When building desktop applications with <a href="https://electronjs.org/">Electron</a>, oneof the key challenges developers often face is managing the shared state betweenthe <code>main</code> process and multiple <code>renderer</code> processes. While the <code>main</code> processhandles the core application logic, <code>renderer</code> processes are responsible for theuser interface. However, they often need access to the same data, like userpreferences, application state, or session information.</p><p>Electron does not natively provide a way to persist data, let alone give asynchronized state across these processes.</p><h3>electron-store to store data persistently</h3><p>Since Electron doesn't have a built-in way to persist data, We can use<a href="https://github.com/sindresorhus/electron-store">electron-store</a>, an npm packageto store data persistently. <code>electron-store</code> stores the data in a JSON filenamed <code>config.json</code> in <code>app.getPath('userData')</code>.</p><p>Even though we can configure <code>electron-store</code> to be made directly available inthe <code>renderer</code> process, it is recommended not to do so. The best way is toexpose it via<a href="https://www.electronjs.org/docs/latest/tutorial/tutorial-preload">Electron's preload script</a>.</p><p>Let's look at how we can expose the <code>electron store</code> to the renderer via apreload script.</p><pre><code class="language-js">// preload.jsimport { contextBridge, ipcRenderer } from &quot;electron&quot;;const electronHandler = {  store: {    get(key) {      return ipcRenderer.sendSync(&quot;get-store&quot;, key);    },    set(property, val) {      ipcRenderer.send(&quot;set-store&quot;, property, val);    },  },  // ...others code};contextBridge.exposeInMainWorld(&quot;electron&quot;, electronHandler);</code></pre><p>Here, we exposed a <code>set</code> function that calls the <code>ipcRenderer.send</code> method,which just sends a message to the <code>main</code> process. The <code>get</code> function calls the<code>ipcRenderer.sendSync</code> method, which will send a message to the <code>main</code> processwhile expecting a return value.</p><p>Now, let's add <code>ipcMain</code> events to handle these requests in the <code>main</code> process.</p><pre><code class="language-js">import Store from &quot;electron-store&quot;;const store = new Store();ipcMain.on(&quot;get-store&quot;, async (event, val) =&gt; {  event.returnValue = store.get(val);});ipcMain.on(&quot;set-store&quot;, async (_, key, val) =&gt; {  store.set(key, val);});</code></pre><p>In the <code>main</code> process, we created an <code>electron-store</code> instance and added<code>get-store</code> and <code>set-store</code> event handlers to retrieve and set data from thestore.</p><p>Now, we can read and write data from any <code>renderer</code> process without exposing thewhole <code>electron-store</code> class to it.</p><pre><code class="language-js">window.electron.store.set(&quot;key&quot;, &quot;value&quot;);window.electron.store.get(&quot;key&quot;);</code></pre><h2>Synchronization</h2><p>Since we sorted out the storage issue, let's look into how we can synchronizedata between the <code>main</code> process and all its <code>renderer</code> processes.</p><p>Before we start synchronization, let's create a simple utility function that cansend a message to all active <code>renderer</code> processes or, in other words, browserwindows (we will use the terms <code>renderer</code> process and browser windowsinterchangeably).</p><pre><code class="language-js">export const sendToAll = (channel, msg) =&gt; {  BrowserWindow.getAllWindows().forEach(browseWindow =&gt; {    browseWindow.webContents.send(channel, msg);  });};</code></pre><p><code>BrowserWindow.getAllWindows()</code> returns all active browser windows, and<code>browseWindow.webContents.send</code> is the standard way of sending a message from<code>main</code> to a <code>renderer</code> process.</p><h3>electron-store onDidChange</h3><p><code>electron-store</code> provides an option to add an event listener when there is achange in the store called <code>onDidChange</code>. This is the key feature we are goingto use to create synchronization.</p><pre><code class="language-js">store.onDidChange(&quot;key&quot;, newValue =&gt; {  // TODO});</code></pre><p>Not all data needs to be synchronized. So, instead of adding <code>onDidChange</code> toevery field, let's expose an API for the <code>renderer</code> process so that it candecide which data it needs and subscribe to it.</p><pre><code class="language-js">import Store from &quot;electron-store&quot;;const store = new Store();const subscriptions = new Map();ipcMain.on(&quot;get-store&quot;, async (event, val) =&gt; {  event.returnValue = store.get(val);});ipcMain.on(&quot;set-store&quot;, async (_, key, val) =&gt; {  store.set(key, val);});ipcMain.on(&quot;subscribe-store&quot;, async (event, key) =&gt; {  const unsubscribeFn = store.onDidChange(key, newValue =&gt; {    sendToAll(`onChange:${key}`, newValue);  });  subscriptions.set(key, unsubscribeFn);});</code></pre><p>Here, we exposed another API called <code>subscribe-store</code>. When calling that APIwith a key, we listen to that field's <code>onDidChange</code> event. Then, when the<code>onDidChange</code> triggers, we call the <code>sendToAll</code> function we created earlier, andall the <code>renderer</code> processes listening to these changes will be notified withthe latest data. For example, if a field called <code>user</code> is subscribed to changes,we send a message to all <code>renderer</code> processes with the new value on a channelcalled <code>onChange:user.</code> We will soon add code in the <code>renderer</code> process tohandle this.</p><p><code>store.onDidChange</code> returns the <code>unsubscribe</code> function for that particular key.Since we won't be unsubscribing straight away, we need to store this functionfor later use. Here, we are storing it in a hash map against the same key.</p><p>Let's add an option to unsubscribe as well.</p><pre><code class="language-js">//... other codesipcMain.on(&quot;unsubscribe-store&quot;, async (event, key) =&gt; {  subscriptions.get(key)();});</code></pre><h3>Update preload script</h3><p>Let's update the preload script to support the store'ssubscription/unsubscribing.</p><pre><code class="language-js">// preload.jsimport { contextBridge, ipcRenderer } from &quot;electron&quot;;const electronHandler = {  store: {    get(key) {      return ipcRenderer.sendSync(&quot;get-store&quot;, key);    },    set(property, val) {      ipcRenderer.send(&quot;set-store&quot;, property, val);    },    subscribe(key, func) {      ipcRenderer.send(&quot;subscribe-store&quot;, key);      const subscription = (_event, ...args) =&gt; func(...args);      const channelName = `onChange:${key}`;      ipcRenderer.on(channelName, subscription);      return () =&gt; {        ipcRenderer.removeListener(channelName, subscription);      };    },    unsubscribe(key) {      ipcRenderer.send(&quot;unsubscribe-store&quot;, key);    },  },  // ...others code};contextBridge.exposeInMainWorld(&quot;electron&quot;, electronHandler);</code></pre><p>We add two APIs here, <code>subscribe</code> and <code>unsubscribe</code>. While <code>unsubscribe</code> isstraightforward, <code>subscribe</code> might need some explanation. It exposes twoarguments, a store key and a callback function, to be called when there is achange to that field.</p><p>First, we call <code>subscribe-store</code> to subscribe to change to that data field;then, we listen to <code>ipcRenderer.on</code> for any changes. For example, when there isa change to the <code>user</code> field, <code>sendToAll</code> will propagate the change, and here weare listening to it on <code>onChange:user</code>.</p><p>Now, from a <code>renderer</code> process, if it needs to be notified of changes to the<code>user</code> field, we can subscribe to it like below.</p><pre><code class="language-js">window.electron.store.subscribe(&quot;user&quot;, newUser =&gt; {  // TODO});</code></pre><h3>useSyncExternalStore</h3><p>React provides a hook to connect to an external store called<code>useSyncExternalStore</code>. It expects two functions as arguments.</p><ul><li>The <code>subscribe</code> function should subscribe to the store and return anunsubscribe function.</li><li>The <code>getSnapshot</code> function should read a snapshot of the data from the store.</li></ul><p>In the <code>renderer</code> process, create a <code>SyncedStore</code> class with <code>subscribe</code> and<code>getSnapshot</code> functions that <code>useSyncExternalStore</code> expects.</p><pre><code class="language-js">class SyncedStore {  snapshot;  defaultValue;  storageKey;  constructor(defaultValue = &quot;&quot;, storageKey) {    this.defaultValue = defaultValue;    this.snapshot = window.electron.store.get(storageKey) ?? defaultValue;    this.storageKey = storageKey;  }  getSnapshot = () =&gt; this.snapshot;  subscribe = callback =&gt; {    // TODO  };}</code></pre><p>Here, we created a generic class that takes a <code>defaultValue</code> and <code>storageKey</code>.While creating the object, we loaded the existing data for that field from the<code>main</code> store.</p><p>When React tries to subscribe to this using <code>useSyncExternalStore</code>, we need tocall our <code>main</code> store's subscribe.</p><pre><code class="language-js">class SyncedStore {  snapshot;  defaultValue;  storageKey;  constructor(defaultValue = &quot;&quot;, storageKey) {    this.defaultValue = defaultValue;    this.snapshot = window.electron.store.get(storageKey) ?? defaultValue;    this.storageKey = storageKey;  }  getSnapshot = () =&gt; this.snapshot;  subscribe = callback =&gt; {    window.electron.store.subscribe(this.storageKey, callback);    return () =&gt; {      window.electron.store.unsubscribe(this.storageKey);    };  };}</code></pre><p>We have our <code>SyncedStore</code> ready, but it's a bit inefficient; for example, if weare subscribed to the same <code>storageKey</code> in multiple places, it will create asubscription for each instance in the main store. That is needless IPCcommunications for the same data.</p><p>Let's improve this a bit so that only one subscription is registered per browserwindow(<code>renderer</code> process), and if there are multiple use cases of the same,let's handle it internally.</p><pre><code class="language-js">class SyncedStore {  snapshot;  defaultValue;  storageKey;  listeners = new Set();  constructor(defaultValue = &quot;&quot;, storageKey) {    this.defaultValue = defaultValue;    this.snapshot = window.electron.store.get(storageKey) ?? defaultValue;    this.storageKey = storageKey;  }  getSnapshot = () =&gt; this.snapshot;  onChange = newValue =&gt; {    if (JSON.stringify(newValue) === JSON.stringify(this.snapshot)) return;    this.snapshot = newValue ?? this.defaultValue;    this.listeners.forEach(listener =&gt; listener());  };  subscribe = callback =&gt; {    this.listeners.add(callback);    if (this.listeners.size === 1) {      window.electron.store.subscribe(this.storageKey, this.onChange);    }    return () =&gt; {      this.listeners.delete(callback);      if (this.listeners.size !== 0) return;      window.electron.store.unsubscribe(this.storageKey);    };  };}</code></pre><p>We made the change so that only one request is sent to <code>main</code>; the rest of thesubscriptions are stored internally and respond to it when the first one isnotified.</p><p>We also added additional checks to ensure that rerender is not triggered ifthere are no changes to the data.</p><h3>Usage</h3><p>Now, whenever a synchronized store for a field is needed, we just need to createan instance of this class and pass it to <code>useSyncExternalStore</code>.</p><pre><code class="language-js">import { useSyncExternalStore } from &quot;react&quot;;const createSyncedStore = ({ defaultValue, storageKey }) =&gt; {  const store = new SyncedStore(defaultValue, storageKey);  return () =&gt; useSyncExternalStore(store.subscribe, store.getSnapshot);};const useUser = createSyncedStore({  storageKey: &quot;user&quot;,  defaultValue: { firstName: &quot;Oliver&quot;, lastName: &quot;Smith&quot; },});const App = () =&gt; {  const user = useUser();  return &lt;div&gt;Name: {`${user.firstName} ${user.lastName}`}&lt;/div&gt;;};</code></pre><p>Now, if we update the <code>user</code> field from anywhere, let it be from any <code>renderer</code>process or <code>main</code>.</p><pre><code class="language-js">window.electron.store.set(&quot;user&quot;, { firstName: &quot;John&quot;, lastName: &quot;Smith&quot; });</code></pre><p>The above <code>App</code> component will be rerendered with the latest user data.</p>]]></content>
    </entry>
     </feed>