How to Use Winston in Vite
Tags: vite, webdev, typescript, react
Published: 2023-01-27 23:20:07

## [;TLDR](#the-working-solution)
**I understand the need to just grab code and go, but I would strongly suggest that you read the whole post to understand the details of the solution and to give credit to those who made this solution possible.**

## Background
I am trying to move my company away from using create-react-app and steering them towards a faster react development environment.  Vite's goal is to maintain a small, fast, and modern web bundling framework and I have loved working with it so far.  Despite taking 3 days to solve this problem, the reason why I found it important to continue with Vite is because it stay true to its principles by cutting legacy support.

The issue I ran into while using Vite was because my company uses an internal library which wraps around the open source logger [Winston](https://github.com/winstonjs/winston).  

Winston is a great tool that makes logging really easy for both developers and logging parsers, such as Splunk.  Winston was originally built for NodeJs, but in version 3, they enabled support for logging to web applications, which means that it should not be a problem to use in a React application, and it isn't.  When using create-react-app, the logger works perfectly fine for reasons that will be outlined in this article, but when using Vite, you might find yourself venturing down some rabbit holes, many of which you might not wish to learn about.

The motivation for this post is:
1. To document the findings for myself, because I am sure I will have to do this again for some other project.
2. I hope to help others who run into the same issue, saving their time and energy.

### Disclaimer
It is important to understand that I am not an expert when it comes to Vite or Esbuild so I will try to credit all the posts and people whose ideas I used to piece together a solution that works for me.

## The Problem with Vite (but Actually Winston)

Upon importing Winston, more specifically my-custom-logger, Vite will compile and run the development server with `npm run dev`.  However, I hit my first error message right away in the console.

`Uncaught ReferenceError: process is not defined`

![Fatal Winston error in the browser console](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yh6wuop7uj7fzbg4sy66.png)

Not only did my logger not work, but my application would not even load.  The library which supposed to operate in the background was now breaking the whole website.

### What is `process` and How do I Define It?

While you may think that `process` is a pretty generic error message, but it is actually referencing the `process` object that the NodeJs framework uses.  _Remember, Winston was originally built for NodeJs logging, not the browser._

After checking the internet for this error, you will probably come across this [closed Vite issue on Github](https://github.com/vitejs/vite/issues/2618).  This led me to understand that I have to polyfill the NodeJs functions Winston is looking for, but the Vite team isn't going to polyfill anything for me.  I wish they would, but again, it seems that the JS ecosystem is moving away from this approach, because we really shouldn't be enabling the use of old or unsupported API's.  On the other hand, you can't just tell these packages that have millions of dependents to just refactor and catch up overnight.  Everyone knows the technical debt in those libraries must be a nightmare.

In the issue mentioned above, you should also know that the solutions in the thread don't solve this problem and shouldn't be used.  We will use a variation of the solutions posted in our actual solution.

## Working Towards a Solution

While looking for polyfills in Vite, I found this [Medium Article](https://medium.com/@ftaioli/using-node-js-builtin-modules-with-vite-6194737c2cd2) written by Fabiano Taioli.  He also has a [Git Gist](https://gist.github.com/FbN/0e651105937c8000f10fefdf9ec9af3d) with many comments from people dealing with the same issue.  One user seemed to be very active in the discussion since nothing was working in their Vite v4 project, [Terkea](https://gist.github.com/Terkea).  I decided to try [the solution they posted] (https://gist.github.com/FbN/0e651105937c8000f10fefdf9ec9af3d?permalink_comment_id=4450749#gistcomment-4450749).

```typescript
// yarn add --dev @esbuild-plugins/node-globals-polyfill
import { NodeGlobalsPolyfillPlugin } from "@esbuild-plugins/node-globals-polyfill";
// yarn add --dev @esbuild-plugins/node-modules-polyfill
import { NodeModulesPolyfillPlugin } from "@esbuild-plugins/node-modules-polyfill";
// You don't need to add this to deps, it's included by @esbuild-plugins/node-modules-polyfill
import rollupNodePolyFill from "rollup-plugin-node-polyfills";
import react from "@vitejs/plugin-react";
import tsconfigPaths from "vite-tsconfig-paths";

export default {
    plugins: [
        react(),
        tsconfigPaths(),
        {
            name: "fix-node-globals-polyfill",
            setup(build) {
                build.onResolve({ filter: /util\.js/ }, ({ path }) => ({ path }));
            },
        },
    ],
    resolve: {
        alias: {
            // This Rollup aliases are extracted from @esbuild-plugins/node-modules-polyfill,
            // see https://github.com/remorses/esbuild-plugins/blob/master/node-modules-polyfill/src/polyfills.ts
            // process and buffer are excluded because already managed
            // by node-globals-polyfill
            util: "util",
            sys: "util",
            events: "rollup-plugin-node-polyfills/polyfills/events",
            stream: "rollup-plugin-node-polyfills/polyfills/stream",
            path: "rollup-plugin-node-polyfills/polyfills/path",
            querystring: "rollup-plugin-node-polyfills/polyfills/qs",
            punycode: "rollup-plugin-node-polyfills/polyfills/punycode",
            url: "rollup-plugin-node-polyfills/polyfills/url",
            string_decoder: "rollup-plugin-node-polyfills/polyfills/string-decoder",
            http: "rollup-plugin-node-polyfills/polyfills/http",
            https: "rollup-plugin-node-polyfills/polyfills/http",
            os: "rollup-plugin-node-polyfills/polyfills/os",
            assert: "rollup-plugin-node-polyfills/polyfills/assert",
            constants: "rollup-plugin-node-polyfills/polyfills/constants",
            _stream_duplex: "rollup-plugin-node-polyfills/polyfills/readable-stream/duplex",
            _stream_passthrough: "rollup-plugin-node-polyfills/polyfills/readable-stream/passthrough",
            _stream_readable: "rollup-plugin-node-polyfills/polyfills/readable-stream/readable",
            _stream_writable: "rollup-plugin-node-polyfills/polyfills/readable-stream/writable",
            _stream_transform: "rollup-plugin-node-polyfills/polyfills/readable-stream/transform",
            timers: "rollup-plugin-node-polyfills/polyfills/timers",
            console: "rollup-plugin-node-polyfills/polyfills/console",
            vm: "rollup-plugin-node-polyfills/polyfills/vm",
            zlib: "rollup-plugin-node-polyfills/polyfills/zlib",
            tty: "rollup-plugin-node-polyfills/polyfills/tty",
            domain: "rollup-plugin-node-polyfills/polyfills/domain",
        },
    },
    optimizeDeps: {
        esbuildOptions: {
            // Node.js global to browser globalThis
            define: {
                global: "globalThis",
            },
            // Enable esbuild polyfill plugins
            plugins: [
                NodeGlobalsPolyfillPlugin({
                    process: true,
                    buffer: true,
                }),
                NodeModulesPolyfillPlugin(),
            ],
        },
    },
    build: {
        rollupOptions: {
            plugins: [
                // Enable rollup polyfills plugin
                // used during production bundling
                rollupNodePolyFill(),
            ],
        },
    },
};
```

### What Does This Solution Do?

I'll break down this information by the properties in the Vite configuration object with the knowledge that I have:

- Plugins

    These are the plugins used by Vite in both the `build` and `dev` processes. The `react` plugin is used to bundle react, but I am really unclear what the other two lines do, other than solve my errors.

- Resolve.alias

    These are the global objects that polyfill the libraries that the solution had us install.

- OptimizeDeps.esbuildOptions.define

    This replaces the word `global` with `globalThis` wherever the reference `global` appears in code.  This helps our polyfills get assigned to the correct scope in the browser.

- OptimizeDeps.esbuildOptions.plugins

    These are the plugins ES Build will use when it builds our app.  This also requires that the `NodeGlobalsPolyfillPlugin` includes the `process` and `buffer` polyfills since they tend to be larger and aren't included by default.

- Build.rollupOptions.plugins

    These are the plugins that rollup uses only when it `build`s our application.

### The Results From the Solution    

When running my server, I received the following error in my console:

`✘ [ERROR] Could not read from file: (my-project-path)/rollup-plugin-node-polyfills/polyfills/string-decoder`

### The Solution to This Error

_Some people might not like this solution, but it worked for me._

The error was being caused by the `string-decoder` polyfill, which Winston doesn't actually need, so I just deleted that line and my app has complied now!

The `vite.config.ts` file now looks like this:

```typescript
// yarn add --dev @esbuild-plugins/node-globals-polyfill
import { NodeGlobalsPolyfillPlugin } from "@esbuild-plugins/node-globals-polyfill";
// yarn add --dev @esbuild-plugins/node-modules-polyfill
import { NodeModulesPolyfillPlugin } from "@esbuild-plugins/node-modules-polyfill";
// You don't need to add this to deps, it's included by @esbuild-plugins/node-modules-polyfill
import rollupNodePolyFill from "rollup-plugin-node-polyfills";
import react from "@vitejs/plugin-react";
import tsconfigPaths from "vite-tsconfig-paths";

export default {
    plugins: [
        react(),
        tsconfigPaths(),
        {
            name: "fix-node-globals-polyfill",
            setup(build) {
                build.onResolve({ filter: /util\.js/ }, ({ path }) => ({ path }));
            },
        },
    ],
    resolve: {
        alias: {
            // This Rollup aliases are extracted from @esbuild-plugins/node-modules-polyfill,
            // see https://github.com/remorses/esbuild-plugins/blob/master/node-modules-polyfill/src/polyfills.ts
            // process and buffer are excluded because already managed
            // by node-globals-polyfill
            util: "util",
            sys: "util",
            events: "rollup-plugin-node-polyfills/polyfills/events",
            stream: "rollup-plugin-node-polyfills/polyfills/stream",
            path: "rollup-plugin-node-polyfills/polyfills/path",
            querystring: "rollup-plugin-node-polyfills/polyfills/qs",
            punycode: "rollup-plugin-node-polyfills/polyfills/punycode",
            url: "rollup-plugin-node-polyfills/polyfills/url",
/******** Remove the string decoder below *********/
            // string_decoder: "rollup-plugin-node-polyfills/polyfills/string-decoder",
            http: "rollup-plugin-node-polyfills/polyfills/http",
            https: "rollup-plugin-node-polyfills/polyfills/http",
            os: "rollup-plugin-node-polyfills/polyfills/os",
            assert: "rollup-plugin-node-polyfills/polyfills/assert",
            constants: "rollup-plugin-node-polyfills/polyfills/constants",
            _stream_duplex: "rollup-plugin-node-polyfills/polyfills/readable-stream/duplex",
            _stream_passthrough: "rollup-plugin-node-polyfills/polyfills/readable-stream/passthrough",
            _stream_readable: "rollup-plugin-node-polyfills/polyfills/readable-stream/readable",
            _stream_writable: "rollup-plugin-node-polyfills/polyfills/readable-stream/writable",
            _stream_transform: "rollup-plugin-node-polyfills/polyfills/readable-stream/transform",
            timers: "rollup-plugin-node-polyfills/polyfills/timers",
            console: "rollup-plugin-node-polyfills/polyfills/console",
            vm: "rollup-plugin-node-polyfills/polyfills/vm",
            zlib: "rollup-plugin-node-polyfills/polyfills/zlib",
            tty: "rollup-plugin-node-polyfills/polyfills/tty",
            domain: "rollup-plugin-node-polyfills/polyfills/domain",
        },
    },
    optimizeDeps: {
        esbuildOptions: {
            // Node.js global to browser globalThis
            define: {
                global: "globalThis",
            },
            // Enable esbuild polyfill plugins
            plugins: [
                NodeGlobalsPolyfillPlugin({
                    process: true,
                    buffer: true,
                }),
                NodeModulesPolyfillPlugin(),
            ],
        },
    },
    build: {
        rollupOptions: {
            plugins: [
                // Enable rollup polyfills plugin
                // used during production bundling
                rollupNodePolyFill(),
            ],
        },
    },
};
```
### A False Hope

Now while the app compiles and loads (and I thought it was working), you might notice that Winston actually breaks internally, and won't log anything because of the following error:

`Uncaught ReferenceError: setImmediate is not defined`

![Non-fatal Winston error in browser console](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lcf7ewccxb95893in8px.png)

### Rock Bottom

At this point, I was very confused because I had all the polyfills known to man (or at least NodeJs) so what was the issue this time?

Well... it turns out `setImmediate` is actually attached directly to the `window` object and our giant polyfilled statement is attached to the `window`.  In fact, the `OptimizeDeps.esbuildOptions.define` ensures that they aren't attached to the `window`, but to the `global` object.

## The Working Solution

I finally decided to stop letting other developers try to solve my problem for me (kind of) and do it myself, so I went to the [ES Build] Docs to see if there was something I could do to attach a polyfill for `setImmediate`.  This is assuming I could find a polyfill, which I did find at [YuzuJS's](https://github.com/YuzuJS) polyfill for [setImmediate](https://github.com/YuzuJS/setImmediate/blob/master/setImmediate.js).

The [ES Build Docs for Inject](https://esbuild.github.io/api/#inject) do a pretty poor job explaining what this configuration property does for someone who doesn't know the in's and out's of ES Build.  I was desperate, so I just threw it in my code to see if it would work.  Turns out, `inject` was exactly what I needed; it imports the shims from the polyfill into the app, as long as it is in another file.  I decided to create another folder called `vite-polyfills`, just in case I ever need more in the future, and then copied and pasted the code from the setImmediate polyfill into a file.

The final vite.config.js looks like this:

```typescript
// yarn add --dev @esbuild-plugins/node-globals-polyfill
import { NodeGlobalsPolyfillPlugin } from "@esbuild-plugins/node-globals-polyfill";
// yarn add --dev @esbuild-plugins/node-modules-polyfill
import { NodeModulesPolyfillPlugin } from "@esbuild-plugins/node-modules-polyfill";
// You don't need to add this to deps, it's included by @esbuild-plugins/node-modules-polyfill
import rollupNodePolyFill from "rollup-plugin-node-polyfills";
import react from "@vitejs/plugin-react";
import tsconfigPaths from "vite-tsconfig-paths";

export default {
    plugins: [
        react(),
        tsconfigPaths(),
        {
            name: "fix-node-globals-polyfill",
            setup(build) {
                build.onResolve({ filter: /util\.js/ }, ({ path }) => ({ path }));
            },
        },
    ],
    resolve: {
        alias: {
            // This Rollup aliases are extracted from @esbuild-plugins/node-modules-polyfill,
            // see https://github.com/remorses/esbuild-plugins/blob/master/node-modules-polyfill/src/polyfills.ts
            // process and buffer are excluded because already managed
            // by node-globals-polyfill
            util: "util",
            sys: "util",
            events: "rollup-plugin-node-polyfills/polyfills/events",
            stream: "rollup-plugin-node-polyfills/polyfills/stream",
            path: "rollup-plugin-node-polyfills/polyfills/path",
            querystring: "rollup-plugin-node-polyfills/polyfills/qs",
            punycode: "rollup-plugin-node-polyfills/polyfills/punycode",
            url: "rollup-plugin-node-polyfills/polyfills/url",
/******** Remove the string decoder below *********/
            // string_decoder: "rollup-plugin-node-polyfills/polyfills/string-decoder",
            http: "rollup-plugin-node-polyfills/polyfills/http",
            https: "rollup-plugin-node-polyfills/polyfills/http",
            os: "rollup-plugin-node-polyfills/polyfills/os",
            assert: "rollup-plugin-node-polyfills/polyfills/assert",
            constants: "rollup-plugin-node-polyfills/polyfills/constants",
            _stream_duplex: "rollup-plugin-node-polyfills/polyfills/readable-stream/duplex",
            _stream_passthrough: "rollup-plugin-node-polyfills/polyfills/readable-stream/passthrough",
            _stream_readable: "rollup-plugin-node-polyfills/polyfills/readable-stream/readable",
            _stream_writable: "rollup-plugin-node-polyfills/polyfills/readable-stream/writable",
            _stream_transform: "rollup-plugin-node-polyfills/polyfills/readable-stream/transform",
            timers: "rollup-plugin-node-polyfills/polyfills/timers",
            console: "rollup-plugin-node-polyfills/polyfills/console",
            vm: "rollup-plugin-node-polyfills/polyfills/vm",
            zlib: "rollup-plugin-node-polyfills/polyfills/zlib",
            tty: "rollup-plugin-node-polyfills/polyfills/tty",
            domain: "rollup-plugin-node-polyfills/polyfills/domain",
        },
    },
    optimizeDeps: {
        esbuildOptions: {
/********* New line inserted ***********/
            inject: ['./vite-polyfills/setImmediate.js'],
            // Node.js global to browser globalThis
            define: {
                global: "globalThis",
            },
            // Enable esbuild polyfill plugins
            plugins: [
                NodeGlobalsPolyfillPlugin({
                    process: true,
                    buffer: true,
                }),
                NodeModulesPolyfillPlugin(),
            ],
        },
    },
    build: {
        rollupOptions: {
            plugins: [
                // Enable rollup polyfills plugin
                // used during production bundling
                rollupNodePolyFill(),
            ],
        },
    },
};
```

## Conclusion and Retrospective

As much of a pain as this process was, I would encourage people to read this whole posting because this journey led me to learn more about the magical `vite` and `esbuild` tools which I take for granted (and I'm sure I'm not the only one).

The next steps I would probably take are removing the polyfills I don't need from the `global` and figuring out a way not to add another random file to my app, since this really feels dirty to me.  While I am happy for the polyfill for `setImmediate`, it does seem best to use this in a node module, which is an option.  However, I am unsure how to reference this file in an ES Build configuration.