<?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-10-05T12:08:36+00:00</updated>
     <id>https://www.bigbinary.com/</id>
     <entry>
       <title><![CDATA[How we build, release and maintain frontend packages]]></title>
       <author><name>Farhan CK</name></author>
      <link href="https://www.bigbinary.com/blog/build-release-frontend-packages"/>
      <updated>2024-06-11T12:00:00+00:00</updated>
      <id>https://www.bigbinary.com/blog/build-release-frontend-packages</id>
      <content type="html"><![CDATA[<p>Here at <a href="https://www.neeto.com/">neeto</a>, we build and manage a<a href="https://blog.neeto.com/p/neeto-products-and-people">lot of products</a>. Eachproduct has its team. However, ensuring consistent design and functionalitiesacross these products poses a significant challenge. To aid our product teams infocusing on their core business logic, we've organized common functionalitiesinto separate packages.</p><p>In this blog, we'll look into how we build, release, and maintain these packagesto support our product development cycles. Let's take a look at some of thesekey packages.</p><h3>neeto-cist</h3><p><a href="https://github.com/bigbinary/neeto-cist">neeto-cist</a> contains essential pureutility functions and forms the backbone of our development framework.</p><h3>neeto-ui</h3><p><a href="https://github.com/bigbinary/neeto-ui">neeto-ui</a> is the foundational designstructure for our Neeto products. It contains basic-level components likeButtons, Input fields, etc.</p><h3>neeto-molecules</h3><p>Built on top of <code>neeto-ui</code>, the <code>neeto-molecules</code> package houses reusable UIelements like a login page, settings page, sidebars etc. It simplifies thecreation of consistent user experiences across Neeto products.</p><h3>neeto-commons</h3><p><code>neeto-commons</code> store crucial elements such as initializers, constants, hooksand configurations shared across our entire product range. We wrote in greatdetail about the<a href="https://www.bigbinary.com/blog/neeto-commons-frontend">challenges we faced while building neeto-commons</a>.</p><p>Let's look at how we build, release, and maintain these packages.</p><h2>Build</h2><p>In general we use <a href="https://babeljs.io/">Babel</a> to transpile and<a href="https://rollupjs.org">Rollup</a> to bundle with some exceptions.</p><p>Let's look at our standard build configuration.</p><pre><code class="language-js">import peerDepsExternal from &quot;rollup-plugin-peer-deps-external&quot;;import svgr from &quot;@svgr/rollup&quot;;import babel from &quot;@rollup/plugin-babel&quot;;import resolve from &quot;@rollup/plugin-node-resolve&quot;;import alias from &quot;@rollup/plugin-alias&quot;;import json from &quot;@rollup/plugin-json&quot;;import commonjs from &quot;@rollup/plugin-commonjs&quot;;import styles from &quot;rollup-plugin-styles&quot;;import aliases from &quot;./aliases&quot;;export default {  input: {    Component1: &quot;src/components/Component1&quot;,    Component2: &quot;src/components/Component2&quot;,    //... rest of the entry points  },  output: [&quot;esm&quot;, &quot;cjs&quot;].map(format =&gt; ({    format,    sourcemap: true,    //... other options  })),  plugins: [    // To automatically externalize peerDependencies in a Rollup bundle.    peerDepsExternal(),    // Inline any svg files    svgr(),    // To integrate Rollup and Babel.    babel({      exclude: &quot;node_modules/**&quot;,      babelHelpers: &quot;runtime&quot;,    }),    // To use third party modules from node_modules    resolve({ extensions: [&quot;.js&quot;, &quot;.jsx&quot;, &quot;.svg&quot;] }),    // To define aliases while bundling package.    alias({ entries: aliases }),    // To convert .json files to ES6 modules.    json(),    // To convert CommonJS modules to ES6.    commonjs(),    // Handle styles    styles({      minimize: true,      extensions: [&quot;.css&quot;, &quot;.scss&quot;, &quot;.min.css&quot;],    }),  ],};</code></pre><p>When dealing with multiple entry points, passing an array of <code>input</code> is a commonmistake people make. The problem with an <code>input</code> array is that it will beduplicated if there is shared code. So, the best approach here is to pass akey-value object. This way, Rollup will create separate files for shared codeand reuse them everywhere.</p><p>Looking at the <code>output</code>, notice that we built it for CommonJS and ECMAScript.Even though we don't have any Node.js projects (we are mainly a Rails company),we still need the CommonJS format to run Jest tests and some scripts for buildand automation purposes.</p><p>When using Babel to transpile and Rollup to bundle, we ran into someconfiguration pain points.<a href="https://www.npmjs.com/package/@rollup/plugin-babel"><code>@rollup/plugin-babel</code></a>plugin made the configuration far easier.<a href="https://www.npmjs.com/package/@svgr/rollup"><code>@svgr/rollup</code></a> converts normal svgfiles to react components.<a href="https://www.npmjs.com/package/rollup-plugin-peer-deps-external"><code>rollup-plugin-peer-deps-external</code></a>automatically externalizes peer dependencies, keeping the bundle lean. Eventhough we don't have any CommonJS modules, it's better to add<a href="https://www.npmjs.com/package/@rollup/plugin-commonjs"><code>@rollup/plugin-commonjs</code></a>so that if there is any deep in dependency rabbit hole.</p><p>Above is a simplified version of the Rollup configuration we use on<code>neeto-cist</code>, <code>neeto-ui</code> and <code>neeto-molecules</code>. We took a much simpler approachwhen building <code>neeto-commons</code>. The reason for abandoning bundling for<code>neeto-commons</code> is that within this package, we have a variety of commonly usedelements, such as components, hooks, initializers, constants, utils, etc., eachwith varying sizes and dependency requirements. If the host project only wantsto use a few simple util functions, we don't want them to be served with theentire bundle and need to install lots of dependencies.</p><p>Instead of serving everything from the root <code>index.js</code>, we use<a href="https://webpack.js.org/guides/package-exports/"><code>exports</code></a> field in the<code>package.json</code> to specify which files can be imported by the host project. Belowis a sample of our exports.</p><pre><code class="language-json">&quot;exports&quot;: {  &quot;./react-utils&quot;: {    &quot;import&quot;: &quot;./react-utils/index.js&quot;,    &quot;require&quot;: &quot;./cjs/react-utils/index.js&quot;,    &quot;types&quot;: &quot;./react-utils.d.ts&quot;  },  &quot;./react-utils/*&quot;: {    &quot;import&quot;: &quot;./react-utils/*&quot;,    &quot;require&quot;: &quot;./cjs/react-utils/*&quot;,    &quot;types&quot;: &quot;./react-utils.d.ts&quot;  },  &quot;./utils&quot;: {    &quot;import&quot;: &quot;./utils/index.js&quot;,    &quot;require&quot;: &quot;./cjs/utils/index.js&quot;,    &quot;types&quot;: &quot;./utils.d.ts&quot;  },  &quot;./utils/*&quot;: {    &quot;import&quot;: &quot;./utils/*&quot;,    &quot;require&quot;: &quot;./cjs/utils/*&quot;,    &quot;types&quot;: &quot;./utils.d.ts&quot;  },  &quot;./initializers&quot;: {    &quot;import&quot;: &quot;./initializers/index.js&quot;,    &quot;require&quot;: &quot;./cjs/initializers/index.js&quot;,    &quot;types&quot;: &quot;./initializers.d.ts&quot;  },  &quot;./constants&quot;: {    &quot;import&quot;: &quot;./constants/index.js&quot;,    &quot;require&quot;: &quot;./cjs/constants/index.js&quot;,    &quot;types&quot;: &quot;./constants.d.ts&quot;  }}</code></pre><p>We export, for example, <code>./react-utils</code> and <code>./react-utils/*</code> because we want tosupport both import styles below.</p><pre><code class="language-js">import { useLocalStorage } from &quot;neetocommons/react-utils&quot;;</code></pre><pre><code class="language-js">import useLocalStorage from &quot;neetocommons/react-utils/useLocalStorage&quot;;</code></pre><p>We initially employed the first import style, but as we expanded, we recognizedthe necessity of supporting a more concise approach in smaller projects. Thissecond method involves importing solely the target file without additionaldependencies, significantly improving tree-shaking capabilities.</p><p>We also ensure we build for <code>esm</code> and <code>cjs</code>. Below is our simplified Babelconfig.</p><pre><code class="language-js">const defaultConfigurations = require(&quot;./defaultConfigurations&quot;);const alias = {  assets: &quot;./src/assets&quot;,  neetocist: &quot;@bigbinary/neeto-cist&quot;,  // others};module.exports = function (api) {  const config = defaultConfigurations(api);  config.sourceMaps = true;  config.plugins.push(    [&quot;module-resolver&quot;, { root: [&quot;./src&quot;], alias }],    &quot;inline-react-svg&quot;  );  if (process.env.BABEL_MODE === &quot;commonjs&quot;) {    config.overrides = [      {        presets: [[&quot;@babel/preset-env&quot;, { modules: &quot;commonjs&quot; }]],      },    ];  }  return config;};</code></pre><p>When transpiling, we ensure aliases are correctly resolved, and if there are anySVG files, we inline them using the<a href="https://www.npmjs.com/package/babel-plugin-inline-react-svg"><code>inline-react-svg</code></a>plugin to make life easy for the host application. Below is our build script.</p><pre><code class="language-json">&quot;scripts&quot;: {  &quot;build:pre&quot;: &quot;del-cli dist&quot;,  &quot;build:es&quot;: &quot;babel  --extensions \&quot;.js,.jsx\&quot; src --out-dir=dist&quot;,  &quot;build:cjs&quot;: &quot;BABEL_MODE=commonjs babel  --extensions \&quot;.js,.jsx\&quot; src --out-dir=dist/cjs&quot;,  &quot;build:post&quot;: &quot;node ./.scripts/post-build.mjs&quot;,  &quot;build&quot;: &quot;yarn build:pre &amp;&amp; yarn build:es &amp;&amp; yarn build:cjs &amp;&amp; yarn build:post&quot;,}</code></pre><h2>Release</h2><p>We did not want to create a release every time we merged a PR, and we alsodidn't want to manually release packages every time. Instead, we rely on GitHubLabels while running Github Actions to do the release.</p><p>We created three labels specifically for this purpose: <code>patch</code>, <code>minor</code> and<code>major</code>. As the names suggest, these labels help us create <code>patch</code>, <code>minor</code> and<code>major</code> versions. When we create a PR and want to do a release when merging,attach any of these labels, and GitHub Action will create releases accordingly.</p><pre><code class="language-yaml">name: &quot;Create and publish releases&quot;on:  pull_request:    branches:      - main    types: [closed]jobs:  release:    name: &quot;Create Release&quot;    runs-on: ubuntu-latest    if: &gt;-      ${{ github.event.pull_request.merged == true &amp;&amp; (      contains(github.event.pull_request.labels.*.name, 'patch') ||      contains(github.event.pull_request.labels.*.name, 'minor') ||      contains(github.event.pull_request.labels.*.name, 'major') ) }}</code></pre><p>As evident from the code above, we trigger the <code>Create and publish releases</code>GitHub Action only if any of the three aforementioned labels are present. Oncethat condition is met, we proceed to use<a href="https://classic.yarnpkg.com/lang/en/docs/cli/version/"><code>yarn version</code></a> toupdate the version.</p><pre><code class="language-yaml">- name: Bump the patch version and create a git tag on release  if: ${{ contains(github.event.pull_request.labels.*.name, 'patch') }}  run: yarn version --patch --no-git-tag-version- name: Bump the minor version and create a git tag on release  if: ${{ contains(github.event.pull_request.labels.*.name, 'minor') }}  run: yarn version --minor --no-git-tag-version- name: Bump the major version and create a git tag on release  if: ${{ contains(github.event.pull_request.labels.*.name, 'major') }}  run: yarn version --major --no-git-tag-version</code></pre><p>Then, we extract changelogs from PR's title and description.</p><pre><code class="language-yaml">- name: Extract changelog  id: CHANGELOG  run: |    content=$(echo '${{ steps.PR.outputs.pr_body }}' | python3 -c 'import json; import sys; print(json.dumps(sys.stdin.read().partition(&quot;**Description**&quot;)[2].partition(&quot;**Checklist**&quot;)[0].strip()))')    echo &quot;CHANGELOG=${content}&quot; &gt;&gt; $GITHUB_ENV  shell: bash- name: Update Changelog  continue-on-error: true  uses: stefanzweifel/changelog-updater-action@v1  with:    latest-version: ${{ steps.package-version.outputs.version }}    release-notes: ${{ fromJson(env.CHANGELOG) }}</code></pre><p>Finally, we publish the package to NPM.</p><pre><code class="language-yaml">- name: Publish the package on NPM  uses: JS-DevTools/npm-publish  with:    access: &quot;public&quot;    token: ${{ secrets.NPM_TOKEN }}</code></pre><h2>Maintenance</h2><p>Now that the release is done, we need to propagate changes to our products. Forexample, we may have extracted an existing functionality from one of thepackages. Here is an example of<a href="https://www.bigbinary.com/blog/how-we-standardized-keyboard-shortcuts">how we standardized keyboard shortcuts in neeto</a>and extracted out into a package. Now, the product team needs to remove thatfunctionality and use it from the package. Reaching out to each team and askingthem to replace it can be tedious, especially considering their priorities. Sowe came up with a plan to use custom<a href="https://eslint.org/docs/latest/extend/custom-rules">ESLint rules</a> to enforcethem, and we built <code>eslint-plugin-neeto</code>.</p><p>Let's look at a few examples of how we use <code>eslint-plugin-neeto</code> to enforcechanges on the product team.</p><p>There are some third-party packages we decided not to use when we found betteralternatives, and they even included our own. For example, <code>bootstrap</code>,<code>moment.js</code>, <code>@bigbinary/neeto-utils</code>, etc. To enforce this, we create ESLintrules called <code>no-blacklisted-imports</code>. This rule throws a lint error ifdevelopers attempt to commit changes that include these blacklisted imports.</p><p>Another example is that we moved higher-level constants, commonly utilizedacross various products, to <code>neeto-commons</code>. To enforce this, we created anESLint rule called <code>use-common-constants</code>. This rule detects any local importsand recommends importing from <code>neeto-commons</code> instead.</p><p>Here is another great blog post in which we explained the challenges we facedwhile<a href="https://www.bigbinary.com/blog/react-localization">adding translations and enforcing them using ESLint and Babel plugins</a>.</p><p>These are just a few of the many ESLint and Babel plugins we created.</p>]]></content>
    </entry><entry>
       <title><![CDATA[dependencies vs devDependencies vs peerDependencies]]></title>
       <author><name>Farhan CK</name></author>
      <link href="https://www.bigbinary.com/blog/different-dependencies"/>
      <updated>2024-05-14T12:00:00+00:00</updated>
      <id>https://www.bigbinary.com/blog/different-dependencies</id>
      <content type="html"><![CDATA[<p>In a JavaScript project, understanding the distinctions between <code>dependencies</code>,<code>devDependencies</code>, and <code>peerDependencies</code> is crucial for effective packagemanagement. Each plays a distinct role in shaping how a project is built anddistributed. In this blog, we'll explore these terms and their differences.</p><h2>dependencies</h2><p>The packages that are really needed for a project to function should be listedunder <code>dependencies</code>. These packages are always installed within that project.If the project is also a package, then these dependencies will also getinstalled in the host project that uses this package. Below are some commonexamples of what might go under <code>dependencies</code>.</p><pre><code class="language-json"> &quot;dependencies&quot;: {   &quot;dayjs&quot;: &quot;1.11.1&quot;,   &quot;immer&quot;: &quot;^10.0.2&quot;,   &quot;ramda&quot;: &quot;^0.29.0&quot;,   &quot;react&quot;: &quot;^18.2.0&quot; }</code></pre><p>It's important to understand that the above packages do not always have to beunder <code>dependencies</code>. For example, if <code>dayjs</code> is needed only for development,deployment or testing purposes, It should not be listed under <code>dependencies</code> butrather in the <code>devDependencies</code> section, because <code>dependencies</code> will be bundledwith the main code, whereas <code>devDependencies</code> will not. So adding dependenciesthat are only used in development purposes under <code>dependencies</code> is unnecessaryand will increase the bundle size, affecting the performance of the application.</p><p>To add a package to <code>dependencies</code> section, simply run:</p><pre><code class="language-bash">yarn add package-name</code></pre><p>If we are shipping a package, all our dependencies are installed in the root ofhost projects <code>node_modules</code>. However, an exception occurs if the host projectalready has the same dependency but with a different version. In this scenario,that specific conflicting dependency will be installed within the <code>node_modules</code>of our package to avoid version conflicts.</p><p>To explain it bit more lets say we have <code>dayjs@1.0.0</code> as one of our dependencybut the host project uses <code>2.0.0</code>. Then both versions will be installed, <code>2.0.0</code>would be the in root <code>node_modules</code> and <code>1.0.0</code> in our package's <code>node_modules</code>.This way, our package can continue to use <code>1.0.0</code> while the host uses <code>2.0.0</code>. Thefolder structure of that host will look something like below.</p><pre><code class="language-javascript">host-project-app src    index.js node_modules    react    dayjs    my-awesome-lib      node_modules        dayjs package.json README.md</code></pre><h2>devDependencies</h2><p>Packages listed in <code>devDependencies</code> are used specifically for developmentpurposes. Any dependencies that do not go into our actual code are listed hereand not under <code>dependencies</code>. These dependencies won't be installed in aproduction environment or in a host project if our project acts as a package.Here are a few examples of what might belong in <code>devDependencies</code>.</p><pre><code class="language-json"> &quot;devDependencies&quot;: {    &quot;@babel/core&quot;: &quot;^7.16.5&quot;,    &quot;eslint&quot;: &quot;^8.41.0&quot;,    &quot;husky&quot;: &quot;^7.0.4&quot;,    &quot;jest&quot;: &quot;27.5.1&quot;,    &quot;prettier&quot;: &quot;^2.8.8&quot; }</code></pre><p>Just like in <code>dependencies</code>, these packages may not always be in<code>devDependencies</code>. For example, if we are building a package that enhances Jesttests capabilities, then we should place <code>jest</code> under <code>dependencies</code>. Otherwiseour host project will break, because <code>jest</code> will not be installed in the hostproject.</p><p>To add a package to <code>devDependencies</code> section, simply run:</p><pre><code class="language-bash">yarn add -D package-name</code></pre><h2>peerDependencies</h2><p><code>peerDependencies</code> are needed only if we are building a package. It allows hostto install any desired version unless we specify otherwise.</p><p>If we are using <code>yarn</code> as the package manager then <code>peerDependencies</code> are notinstalled automatically. We have to install manually, even in our own package.But there is a slight difference if you are using <code>npm</code>. Up to version 6 <code>npm</code>does not install <code>peerDependencies</code> automatically. However, this changes fromversion 7 onwards, as it will install <code>peerDependencies</code> automatically. So if weare using <code>yarn</code> or <code>npm&lt;=v6</code> and we have for example Storybook or Jest tests inour package project, then we have to install <code>peerDependencies</code> as<code>devDependencies</code>(not as <code>dependencies</code> which defeats the purpose) as well.</p><pre><code class="language-json"> &quot;devDependencies&quot;: {    &quot;@babel/core&quot;: &quot;^7.16.5&quot;,    &quot;eslint&quot;: &quot;^8.41.0&quot;,    &quot;husky&quot;: &quot;^7.0.4&quot;,    &quot;jest&quot;: &quot;27.5.1&quot;,    &quot;prettier&quot;: &quot;^2.8.8&quot;,    &quot;dayjs&quot;: &quot;1.11.1&quot;, } &quot;peerDependencies&quot;: {    &quot;dayjs&quot;: &quot;latest&quot; }</code></pre><p>You might wonder about its purpose if manual installation is required. It servesas a means for our package project to specify essential dependencies requiredfor the package to function properly. Simultaneously, it grants control to thehost project to choose which versions to install.</p><p>To add a package to <code>peerDependencies</code> section, simply run:</p><pre><code class="language-bash">yarn add --peer package-name</code></pre><p>Now the tricky part is determining whether a particular dependency should gounder <code>dependencies</code> or <code>peerDependencies</code>. Unfortunately, there is no clearanswer here. But there are some questions we can ask ourselves to narrow it down.</p><ol><li><p>If the specific version of the dependency is important for our package, thatis if using a different version breaks our package, then definitely we wantto place that package under <code>dependencies</code>.</p><p>It is possible to place such dependencies under <code>peerDependencies</code> andspecify supported versions like below, but it is not a good practice.</p></li></ol><pre><code class="language-json"> &quot;peerDependencies&quot;: {    &quot;dayjs&quot;: &quot;1.2.0 || 1.5.1&quot;, }</code></pre><ol start="2"><li>If the dependency is a widely used package like <code>react</code>, better place itunder <code>peerDependency</code>and ensure that our package works with differentversion of that dependency. This way, the dependency installed in the hostproject can be reused by our package without installing a separate version.</li><li>If we want to make changes to a dependency in a manner that impacts its usagewithin the host project, then place it under <code>peerDependencies</code>. Good exampleof this is, at <a href="https://www.neeto.com/">neeto</a> we have a package called<code>neeto-commons-frontend</code> which extracts numerous common functionalitiesutilized across various products. One such functionality is our errorhandling system, for which we use an<a href="https://axios-http.com/docs/interceptors">Axios interceptor</a>. To ensure thefunctionality of this interceptor, it's crucial to apply this interceptor tothe same instance of Axios. To elaborate further, if we add Axios under<code>dependencies</code> in <code>neeto-commons-frontend</code>, but the host project uses adifferent Axios version, we will be making changes to the Axios instancethat's in <code>node_modules</code> of <code>neeto-commons-frontend</code> and not of the hostproject, means the functionality will not work on the host project.</li></ol><p>Another rationale behind utilizing <code>peerDependencies</code> is its substantial impacton reducing the bundle size, particularly when bundling our package. Now if weare using Rollup, placing certain packages under <code>peerDependencies</code> doesn'tautomatically exclude them from the bundle. We need to explicitly specify thisin the Rollup configuration. This is achieved through the Rollup <code>external</code>configuration option, where we provide a list of <code>peerDependencies</code> to beexcluded from the bundle. To simplify this process,<code>rollup-plugin-peer-deps-external</code> automates the inclusion of <code>peerDependencies</code>within the <code>external</code> configuration.</p><pre><code class="language-js">import peerDepsExternal from &quot;rollup-plugin-peer-deps-external&quot;;export default {  plugins: [    // Preferably set as first plugin.    peerDepsExternal(),  ],};</code></pre>]]></content>
    </entry>
     </feed>