Javascript Questions for the Dazed and Confused
Published: 2023-01-14 14:52:15

---
title: Javascript Questions for the Dazed and Confused
published: true
description: 
tags: 
# cover_image: https://direct_url_to_image.jpg
# Use a ratio of 100:42 for best results.
# published_at: 2023-01-14 14:51 +0000
---

This post is a little bit of a ratatouille on things that I often see people be confused about in the javascript ecosystem, and will hopefully serve as a reference to point to whenever I see people ask questions about some of the following topics: `process.env`, bare module specifiers, import maps, package exports, and non-standard imports.

## `process.env`
The bane of my existence, and the curse that will haunt frontend for the next few years or so. Unfortunately, many frontend libraries (even modern ESM libraries, looking at you `floating-ui`) use `process.env` to distinguish between development-time and build-time, for example to enable development-time only logging. You'll often see code that looks something like this:

```js
if (process.env.NODE_ENV === 'development') {
  console.log('Some dev logging!');
}
```

The problem with code like this is that the `process` global doesn't actually exist in the browser; its a Node.js global. So whenever you import a library that uses `process.env` in the browser, it'll cause a pesky `Uncaught ReferenceError: process is not defined` error. The browser is not Node.js.

When library authors include these kind of `process.env` checks, they make the assumption that their user uses some kind of tooling (like a _bundler_) to take care of handling the `process` global. However, this is assumption is often wrong, which leads to many people running into runtime errors caused by `process.env`. 

So how can we deal with `process.env` in frontend code? Well, there's two things you can do, and they're equally bad. Your first option is to simply define the `process` global on the `window` object in your `index.html`:

`index.html`:
```html
<script>
  window.process = {
    env: {
      NODE_ENV: 'development' // or 'production'
    }
  }
</script>
```

The second option is to use a buildtool to take care of this for you. Some buildtools will take care of `process.env` by default, but some buildtools do not. For example, if you're using Rollup as your buildtool of choice, you'll have to use something like `@rollup/plugin-replace` to replace any instance of `process.env`, or something like `rollup-plugin-dotenv`.

The reality is, that in the year 2023, there is **no good reason** for a _browser-only_ library to contain `process.env`, period. So whenever you encounter the `Uncaught ReferenceError: process is not defined` error caused by a library using `process.env`, I urge you create a github issue on their repository.

Thankfully, there are other, more browser-friendly ways for library authors to distinguish between development and build-time, like for example the [`esm-env`](https://github.com/benmccann/esm-env) package by Ben McCann, which makes clever use of package exports, which I'll talk more about later in this blog post.


## Bare module specifiers

Another thing that often throws developers off are _bare module specifiers_. In Node.js, you can import a library like so:

```
import { foo } from 'foo';
                     ^^^
```

And Node's _resolution logic_ will try to _resolve_ the `'foo'` package. If you're using Node.js, its likely that the `'foo'` package is a third-party package installed via NPM; because if it was a local file, the import specifier would be relative, and start with a `'/'`, `'./'` or `'../'`. So Node's resolution logic will try to locate the file on the filesystem, and resolve it to wherever it's installed.

At some point in time, bare module specifiers started making their way into frontend code, because NPM turned out to be a convenient way of publishing and installing libraries, and modularizing code. However, bare module specifiers by themself won't work in the browser; browsers dont have the same resolution logic thats built-in to Node.js, and they sure as hell don't have access to your filesystem by default. This means that if you use a bare module specifier in the browser, you'll get the following error:

```
Uncaught TypeError: Failed to resolve module specifier "foo". Relative references must start with either "/", "./", or "../".
```

This means that whenever you're using bare module specifiers, you'll have to somehow resolve those specifiers. This is usually done by applying Node.js's resolution logic to the bare module specifiers via tooling. For example, if you're using a development server, the development server may take a look at the imports in your code, and resolve them following Node.js's resolution logic. If you're using a bundler, it may take care of this behavior for you out of the box, or you may have to enable it specifically, like for example using `@rollup/plugin-node-resolve`.

If you've installed the `foo` package via NPM's `npm install foo` command, the `foo` package will be on your disk at `my-project/node_modules/foo`. So whenever you import bare module specifier `'foo'`, tools can resolve that bare module specifier to point to `my-project/node_modules/foo`. But by default, bare module specifiers will not work in the browser; they need to be handled somehow. The browser is not Node.js.

### Import maps

Another way to handle bare module specifiers is by using a relatively new standard known as Import Maps. In your `index.html` you can define an import map via a `script` with `type="importmap"`, and tell the browser how to resolve specific imports. Consider the following example:

```html
<script type="importmap">
  { 
    "imports": {
      "foo": "./node_modules/foo/index.js",
      // or
      "foo": "https://some-cdn.com/foo/index.js",
    }                   ^
  }                     |
</script>               |
                        |
<script type="module">  |
  import { foo } from 'foo';
</script>
```

The browser will now resolve any import on the page being made to `'foo'` to whatever we assigned to it in the import map; `'https://some-cdn.com/foo/index.js'`. Now we _can_ use bare module specifiers in the browser 🙂

## Package exports

Another good thing to be aware of are a relatively new concept called _package exports_. Package exports modify the way Node's resolution logic resolves imports for your package. You can define package exports in your `package.json`. Consider the following project structure:

```
my-package/
├─ src/
│  ├─ bar.js
├─ index.js
├─ foo.js
├─ package.json
├─ README.md
```

And the following `package.json`:

```json
{
  "exports": {
    ".": "./index.js",
    "./foo.js": "./foo.js"
  }
}
```

This will cause any import for `'my-package'` to resolve to `my-package/index.js`, and any import to `'my-package/foo.js'` to `'my-package/foo.js'`. **However**, this will also PREVENT any import for `'my-package/src/bar.js'`; it's not specified in the package exports, so there's no way for us to import that file. This can be nice for package authors, because it means they can control which code is public facing, and which code is intended for internal use only. 

However, package exports can also be painful; sometimes packages add package exports to their project on minor or patch semver versions, not fully realizing how it will affect their users use of their code, and lead to unexpected breaking changes. As a rule of thumb, adding package exports to your project is _always_ a breaking change!

### On extensionless imports

I generally recommend package export keys to contain file extensions, e.g. prefer:

```json
{
  "exports": {
    "./foo.js": "./foo.js"
  }
}
```

over:

```json
{
  "exports": {
    "./foo": "./foo.js"
  }
}
```

The reason for this is how this translates to import maps. With import maps, we can support both extensionful and extensionless specifiers, but it'll lead to bloating your import map. Consider the following library:

```
my-library/
├─ bar.js
├─ foo.js
├─ index.js
```

```json
 {
   "imports": {
     "my-library": "/node_modules/my-library/index.js",
     "my-library/": "/node_modules/my-library/",
   }
 }
```

This import map would allow the following imports:
```js
import 'my-library';
import 'my-library/foo.js'; // ✅
import 'my-library/bar.js'; // ✅
```

But _not_:
```js
import 'my-library/foo'; // ❌
import 'my-library/bar'; // ❌
```

While technically we _can_ support the extensionless imports, it would mean adding lots of extra entries for every file that we want to support having extensionless imports for to our import map:

```json
 {
   "imports": {
     "my-library": "/node_modules/my-library/index.js",
     "my-library/": "/node_modules/my-library/",
     "my-library/foo": "/node_modules/my-library/foo.js",
     "my-library/bar": "/node_modules/my-library/bar.js",
   }
 }
```

This results in the import map becoming more complicated and convoluted than it needs to be. As a rule of thumb; just always use file extensions in your package exports keys, and you'll keep your users import maps smaller and simpler.

### Export conditions

You can also add conditions to your exports. Here's an example of what export conditions can look like:

```json
{
  "exports": {
    ".": {
      "import": "./index.js", // when your package is loaded via `import` or `import()`
      "require": "./index.cjs", // when your package is loaded via `require`
      "default": "./index.js" // should always be last
    }
  }
}
```

By default, Node.js supports the following export conditions: `"node-addons"`, `"node"`, `"import"`, `"require"` and `"default"`. When resolving imports based on package exports, Node will look for keys in the package export to figure out which file to use. 

Tools however can use custom keys here as well, like `"types"`, `"browser"`, `"development"`, or `"production"`. This is nice, because it means tools can easily distinguish between environments without having to rely on `process.env`; this is how [`esm-env`](https://github.com/benmccann/esm-env) cleverly utilizes package exports to distinguish between development and production environments;

```json
{
  "exports": {
    ".": {
      "development": "./dev.js",
      "default": "./prod.js"
    },
  },
}
```

Where `dev.js` looks like:
```js
export const DEV = true;
export const PROD = false;
```

and `prod.js` looks like:
```js
export const DEV = false;
export const PROD = true;
```

There are many quirks related to this however, for example if you use `"types"` it should always be the _first_ entry in your exports, and if you use `"default"` it should always be the _last_ entry in your exports. Package exports are easy to mess up, and get wrong. Thankfully, there's a really nice project called [`publint`](https://github.com/bluwy/publint) that helps you with things like these.

### Package.json

Another quirk of package exports is that, if not specified, it also prevents tooling from `import` or `require`ing your `package.json` 🙃 This means that it can sometimes be useful to add your `package.json` to your package exports as well.

```json
{
  "exports": {
    "./package.json": "./package.json",
  }
}
```

### Fallback

Technically, we can make package export keys pretty much anything we want. Consider the following example:

```
my-library/
├─ bar.js
├─ index.js
```

And the following package exports:

```json
{
  "exports": {
    "./whatever-name-we-want": "./bar.js",
  }
}
```

This package exports map will allow the following import:

```js
import 'my-library/whatever-name-we-want';
```

However, tools that don't support package exports yet, or CDN's will not be able to resolve that import. This is why generally it's good practice to keep a 1-on-1 mapping between your package export keys and the project structure on the filesystem. In this case, the following package exports would have been better:

```json
{
  "exports": {
    "./bar.js": "./bar.js",
  }
}
```

## Non standard imports

Another nice example of non-standard behavior I see often is non standard imports. Here's a common example:

```js
import icon from './my-icon.svg';
```

This, also, will not run in the browser. You need a buildtool to enable this behavior. Some buildtools, like Vite (which under the hood uses an opinionated Rollup build), enable this behavior by default. Philosophies and opinions on this differ, but I would consider this a bad default. The problem with enabling imports like these in tools by default, while convenient, is that it's non-standard behavior, and developers learn the wrong basics.

Alternatively, you can use `import.meta` to reference non-javascript assets. `import.meta` is a special object provided by the runtime that provides some metadata about the current module.

For example, given the following project:

```
my-project/
├─ index.html
├─ src/
│  ├─ bar.js
│  ├─ icon.svg
```

Where `bar.js` looks like:

```js
const iconUrl = new URL('./icon.svg', import.meta.url);
const image = document.createElement('img');
image.src = iconUrl.href;
document.body.appendChild(image);
```

When opening the `index.html` in the browser, `import.meta.url` will point to `http://localhost:8000/src/bar.js`; the full URL to the module. This means that we can reference assets _relative_ to our current module by creating a new URL that combines`'./icon.svg'` and `import.meta.url`, the `iconUrl.href` will correctly point to `http://localhost:8000/src/icon.svg`.

During local development, this should all work nicely and without any build magic. However, imagine we want to bundle our project for production. We give our bundler an entrypoint javascript file, and from there it bundles any other javascript thats used in the project. After running our build, our project directory may look something like this:

```
my-project/
├─ dist/
│  ├─ as7d547asd45.js
├─ index.html
├─ src/
│  ├─ bar.js
│  ├─ icon.svg
```

In our build output, which is now a bundled (and probably minified) javascript file (`as7d547asd45.js`), `import.meta.url` does not point to the correct location of `'./icon.svg'` anymore! There's several things we can do about this; we can simply copy `icon.svg` to our `dist/` folder, or we can use a plugin in our buildtool to automatically take care of this for us. If you're using rollup, [`@web/rollup-plugin-import-meta-assets`](https://modern-web.dev/docs/building/rollup-plugin-import-meta-assets/) takes care of this for you.