DEV Community

Kyle Kravette
Kyle Kravette

Posted on • Updated on

How to Use Winston in Vite

;TLDR

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.

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

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. 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 written by Fabiano Taioli. He also has a Git Gist 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. I decided to try the solution they posted.

// 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(),
            ],
        },
    },
};
Enter fullscreen mode Exit fullscreen mode

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 builds 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:

// 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(),
            ],
        },
    },
};
Enter fullscreen mode Exit fullscreen mode

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

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 polyfill for setImmediate.

The ES Build Docs for 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:

// 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(),
            ],
        },
    },
};
Enter fullscreen mode Exit fullscreen mode

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.

Latest comments (3)

Collapse
 
edertxodw profile image
Edertxo

It helps me!! Thanks a lot!

Collapse
 
numel2020 profile image
Numel • Edited

Hi kyle,
Your solution is much appreciated. Read the article and followed to the 'T'.
In regards to building the app I still get an error:

vite v4.1.4 building for production...
✓ 403 modules transformed.

"debuglog" is not exported by "__vite-browser-external", imported by "node_modules/rollup-plugin-node-polyfills/polyfills/readable-stream/readable.js".
4: Readable.ReadableState = ReadableState;
5: import EventEmitter from 'events';
6: import {inherits, debuglog} from 'util';
                     ^
7: import BufferList from './buffer-list';
8: import {StringDecoder} from 'string_decoder';
error during build:
RollupError: "debuglog" is not exported by "__vite-browser-external", imported by "node_modules/rollup-plugin-node-polyfills/polyfills/readable-stream/readable.js".
Enter fullscreen mode Exit fullscreen mode

I just wondered if you encounter the same effect.

Collapse
 
antoinewg profile image
Antoine Garcia

I've used plugins: [nodePolyfills()] from import { nodePolyfills } from 'vite-plugin-node-polyfills' instead of rollupNodePolyFill() and it seems to fix the build