DEV Community

Mykhailo Toporkov
Mykhailo Toporkov

Posted on

Chrome extension using Vite & React, Build and Deployment

Hi, It may seem that the internet is full of guides, however, I haven't found any comprehensive, one that could cover all the steps of building extension from a project setup to deployment. So in this post, I will try to fix this this enormous problem and cover all these steps. Feel free to navigate to the needed one if you already pass through some, using the navigation links below:


Environment setup

First of all, let's setup the project using Vite with React and install some dependencies.

npm create vite@latest chrome-extension -- --template react-swc-ts
Enter fullscreen mode Exit fullscreen mode
npm i
Enter fullscreen mode Exit fullscreen mode
npm run dev
Enter fullscreen mode Exit fullscreen mode

As a result, the app is running on some port, in my case it is 5173. That is not the common 3000 port, it may seem as nothing but for me I caused lots of troubles on the deployment stage, so lets change this to common 3000 port. It can be done inside vite.config.ts file by defining server properly and assigning port to 3000:

import { defineConfig } from "vite";
import react from "@vitejs/plugin-react-swc";

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [react()],
  server: {
    port: 3000,
  },
});
Enter fullscreen mode Exit fullscreen mode

Now restart the app, it will be running on the 3000 port.

For styling, I prefer using tailwindcss so let's install this:

npm install -D tailwindcss postcss autoprefixer
Enter fullscreen mode Exit fullscreen mode
npx tailwindcss init -p
Enter fullscreen mode Exit fullscreen mode

And add the Tailwind directives to index.css:

@tailwind base;
@tailwind components;
@tailwind utilities;
Enter fullscreen mode Exit fullscreen mode

Alos for merging classes I will install two more libs:

npm install -D tailwind-merge clsx
Enter fullscreen mode Exit fullscreen mode

And create utils.ts with cn function that will help merging classes:

import { type ClassValue, clsx } from "clsx";;
import { twMerge } from "tailwind-merge";

export const cn = (...inputs: ClassValue[]) => twMerge(clsx(inputs));
Enter fullscreen mode Exit fullscreen mode

Manifest
The base setup is over. Now for working inside the browser as an extension app needs the manifest file, which is the important part as the manifest defines the scripts, pages, and permission that the app requires for working.
For these, we need to install CRXJS lib and create manifest.json file in the root of directory:

npm i @crxjs/vite-plugin@beta -D
Enter fullscreen mode Exit fullscreen mode
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react-swc";
import { crx } from "@crxjs/vite-plugin";
import manifest from "./manifest.json";

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [react(), crx({ manifest })],
  server: {
    port: 3000,
  },
});
Enter fullscreen mode Exit fullscreen mode
{
  "manifest_version": 3,
  "name": "Chrome extension",
  "version": "1.0.0",
  "action": { "default_popup": "index.html" },
  "icons": {
    "16": "icon_logo_16px.png",
    "32": "icon_logo_32px.png",
    "48": "icon_logo_48px.png",
    "128": "icon_logo_128px.png"
  }
}
Enter fullscreen mode Exit fullscreen mode

The icons are not required they just improve experience and should stored in public folder.

Restart the app and open the chrome -> chrome://extensions/. Enable the developer mode, click load unpacked and select the dist folder inside your repository. In a result your extension will be available for testing and development and if you active it you will get something like this:
Image description

Implementing feature

Now that the setup is over we need to build the app, however, make sure that if you build some custom extension your manifest fulfils all needs requirement (for cases if the extension directly interacts with a current tab, collect some data, etc.). I will build just a simple calculator so my manifest does not require any addition. I will not explain the logic behind the app and jus share the code for it:

import { Calculator } from "./components/calculator";

function App() {
  return (
    <div className="container w-[25rem] p-1 bg-zinc-800">
      <Calculator />
    </div>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode
import { cn } from "../utils";

interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {}

export const Button: React.FC<ButtonProps> = ({ className, ...props }) => {
  return (
    <button
      className={cn(
        "text-white px-2 py-4 text-2xl rounded-md bg-zinc-300/10 hover:bg-zinc-300/40 transition-colors duration-300",
        className
      )}
      {...props}
    />
  );
};
Enter fullscreen mode Exit fullscreen mode
interface DisplayProps {
  value: string;
}

export const Display: React.FC<DisplayProps> = ({ value }) => {
  return (
    <div className="text-right text-4xl p-2 pr-3 text-white w-full bg-transparant rounded-md">
      {value}
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode
import { useReducer } from "react";
import { Display } from "./display";
import { Button } from "./buttons";

interface CalculatorState {
  value: number | null;
  displayValue: string;
  operator: string | null;
  waitingForOperand: boolean;
}

type CalculatorAction = {
  type:
    | "inputDigit"
    | "inputDot"
    | "toggleSign"
    | "inputPercent"
    | "performOperation"
    | "clearAll"
    | "clearDisplay"
    | "clearLastChar";
  payload?: any;
};

const CalculatorOperations: {
  [key: string]: (prevValue: number, nextValue: number) => number;
} = {
  "/": (prevValue, nextValue) => prevValue / nextValue,
  "*": (prevValue, nextValue) => prevValue * nextValue,
  "+": (prevValue, nextValue) => prevValue + nextValue,
  "-": (prevValue, nextValue) => prevValue - nextValue,
  "=": (nextValue) => nextValue,
};

const initState: CalculatorState = {
  value: null,
  displayValue: "0",
  operator: null,
  waitingForOperand: false,
};

const reducer = (state: CalculatorState, action: CalculatorAction) => {
  switch (action.type) {
    case "inputDigit": {
      console.log(action);

      if (state.waitingForOperand) {
        return {
          ...state,
          displayValue: String(action.payload),
          waitingForOperand: false,
        };
      }
      console.log(state);

      return {
        ...state,
        displayValue:
          state.displayValue === "0"
            ? String(action.payload)
            : state.displayValue + action.payload,
      };
    }
    case "inputDot": {
      const { displayValue } = state;

      if (!/\./.test(displayValue)) {
        return {
          ...state,
          displayValue: displayValue + ".",
          waitingForOperand: false,
        };
      }

      return state;
    }
    case "toggleSign": {
      const { displayValue } = state;
      const newValue = parseFloat(displayValue) * -1;

      return { ...state, displayValue: String(newValue) };
    }
    case "inputPercent": {
      const { displayValue } = state;
      const currentValue = parseFloat(displayValue);

      if (currentValue === 0) return state;

      const fixedDigits = displayValue.replace(/^-?\d*\.?/, "");
      const newValue = parseFloat(displayValue) / 100;

      return {
        ...state,
        displayValue: String(newValue.toFixed(fixedDigits.length + 2)),
      };
    }
    case "performOperation": {
      const { value, displayValue, operator } = state;
      const inputValue = parseFloat(displayValue);

      const newState = { ...state };

      if (value == null) {
        newState.value = inputValue;
      }

      if (operator) {
        const currentValue = value || 0;
        const newValue = CalculatorOperations[operator](
          currentValue,
          inputValue
        );

        newState.value = newValue;
        newState.displayValue = String(newValue);
      }

      newState.waitingForOperand = true;
      newState.operator = action.payload;

      return newState;
    }
    case "clearAll": {
      return {
        value: null,
        displayValue: "0",
        operator: null,
        waitingForOperand: false,
      };
    }
    case "clearDisplay": {
      return {
        ...state,
        displayValue: "0",
      };
    }
    case "clearLastChar": {
      const { displayValue } = state;

      return {
        ...state,
        displayValue: displayValue.substring(0, displayValue.length - 1) || "0",
      };
    }
    default:
      return state;
  }
};

export const Calculator: React.FC = () => {
  const [state, dispatch] = useReducer(reducer, initState);

  const { displayValue } = state;
  const clearDisplay = displayValue !== "0";
  const clearText = clearDisplay ? "C" : "AC";

  return (
    <>
      <div className="mb-1">
      <Display value={displayValue} />
      </div>
      <div className="grid grid-cols-4 gap-[0.2rem]">
        <Button
          onClick={() =>
            clearDisplay
              ? dispatch({ type: "clearDisplay" })
              : dispatch({ type: "clearAll" })
          }
        >
          {clearText}
        </Button>
        <Button onClick={() => dispatch({ type: "toggleSign" })}>±</Button>
        <Button onClick={() => dispatch({ type: "inputPercent" })}>%</Button>
        <Button
          onClick={() => dispatch({ type: "performOperation", payload: "/" })}
        >
          ÷
        </Button>
        <Button onClick={() => dispatch({ type: "inputDigit", payload: 7 })}>
          7
        </Button>
        <Button onClick={() => dispatch({ type: "inputDigit", payload: 8 })}>
          8
        </Button>
        <Button onClick={() => dispatch({ type: "inputDigit", payload: 9 })}>
          9
        </Button>
        <Button
          onClick={() => dispatch({ type: "performOperation", payload: "*" })}
        >
          ×
        </Button>
        <Button onClick={() => dispatch({ type: "inputDigit", payload: 4 })}>
          4
        </Button>
        <Button onClick={() => dispatch({ type: "inputDigit", payload: 5 })}>
          5
        </Button>
        <Button onClick={() => dispatch({ type: "inputDigit", payload: 6 })}>
          6
        </Button>
        <Button
          onClick={() => dispatch({ type: "performOperation", payload: "-" })}
        ></Button>
        <Button onClick={() => dispatch({ type: "inputDigit", payload: 1 })}>
          1
        </Button>
        <Button onClick={() => dispatch({ type: "inputDigit", payload: 2 })}>
          2
        </Button>
        <Button onClick={() => dispatch({ type: "inputDigit", payload: 3 })}>
          3
        </Button>
        <Button
          onClick={() => dispatch({ type: "performOperation", payload: "+" })}
        >
          +
        </Button>
        <Button
          onClick={() => dispatch({ type: "inputDigit", payload: 0 })}
          className="col-span-2"
        >
          0
        </Button>
        <Button
          onClick={() =>
            dispatch({
              type: "inputDot",
            })
          }
        ></Button>
        <Button
          onClick={() => dispatch({ type: "performOperation", payload: "=" })}
        >
          =
        </Button>
      </div>
    </>
  );
};
Enter fullscreen mode Exit fullscreen mode

Deployment

To be able to deploy extension you need to register in Chrome web store as a developer, which requires payment, however, then you will be able to deploy as many extensions as you want. Now need to build an app and compress the dist folder to ZIP:

npm run build
Enter fullscreen mode Exit fullscreen mode

Click new item in the dashboard and upload the compressed build, after that, you need to fill in all necessary info regarding your extension:
Image description
The Save draft button will help you find missing fields and other errors. When all are filed and complete you will be able to submit an extension for a review, it may take from a few minutes to a few days depending on what you app supposed to do (for me it took several hours).

After the extension is reviewed, it will be published automatically, you can find it in store using its id:

Image description

Now the extension is publicly available for everyone, so enjoy it.

Conclusion

I do not know what needs to be sad in the end... I hope I covered all the steps to build and deploy a simple extension using React and Vite. Below this, I post links to the repo and extension in the store maybe they will be useful for someone))

GitHub repository link

Extension link

Top comments (1)

Collapse
 
groophylifefor profile image
Murat Kirazkaya

After setup the project, don't forget to fill in content in tailwind.config.js, it's not written here.