Socket
Socket
Sign inDemoInstall

Engineering

Cleaning up import paths in JS/TS packages

package.json contains a local aliasing mechanism for import paths called "imports" it satisfies many use cases without tooling specific solutions like tsconfig.json

Cleaning up import paths in JS/TS packages

Bradley Meck Farias

August 16, 2023


Applications and libraries grow over time. In JavaScript and TypeScript these lead to large amounts of files over time due to various conventions and linting rules that may be held in a code base like separate files for tests, single component per file, etc. It is very easy to end up with lots of things having large import paths like:

import '../../../../components/customer-table.js'

There are a variety of workaround provided by other tools. TypeScript provides tsconfig.json's (and jsconfig.jsons's) paths field, webpack has resolve plugins and the resolve config, yarn has their link protocol and portal protocol, etc. These generally cause some level of interoperability problems since they are not supported everywhere. All of these have slight differences but the general usage is to alias some path local to the current package scope.

Some bundlers and tools have recently started to add support for tsconfig.json's paths field even if they do not necessarily utilize typescript. However, for most people developing JS/TS they actually have a solution already that they might not even know about.

Almost every tool supports package.json#imports since it is part of the default Node.js resolver standard. This feature is supported all the way back to Node version 12 as well!

If you are familiar with tsconfig.json you might add an alias using paths like the following:

{
  "$schema": "https://json.schemastore.org/tsconfig.json",
  "compilerOptions": {
    "paths": {
      "#components/*": [ "./src/components/*" ]
    }
  }
}

With package.json imports it would look like the following:

{
  "$schema": "https://json.schemastore.org/package.json",
  "imports": {
    "#components/*": "./src/components/*"
  }
}

You might notice I started both aliases with "#" that is because for package.json imports must include that prefix when providing the alias name. Then we could have a much simpler import than above:

import '#components/customer-table.js'

These aliases though are much more dynamic due to the ability to have conditions associated with them. For example you can vary the destination for your alias depending on where it is going to be used:

{
  "$schema": "https://json.schemastore.org/package.json",
  "imports": {
    "#components": {
       "browser": "./src/components/client/*",
       "default": "./src/components/server/*"
    }
  }
}

This allows for things like you to co-locate server & client code in the same repository without needing complex routing just for conditionally choosing which file to load when in a bundler's build for the browser or not.

There is a common gotcha here in which people need to know that imports is a key order dependent JSON field, it is not unordered! In the example above it will iterate and see "browser" and validate that condition before moving onto the next. It might be easier to think of it as a big if/else tree like follows:

// each tool has a default unordered set of conditions
// that set of conditions MUST include "default"
const DEFAULT_CONDITIONS = new Set(['browser', 'default'])

// each import resolves with a set of conditions that is a
// superset of DEFAULT_CONDITIONS that may context some context
// about what is being used for resolving: types, import, require()
function resolveImport(moduleSpecifier, conditions = DEFAULT_CONDITIONS) {}
  if (moduleSpecifier.startsWith('#components/')) {
    // get the value for the * in the template
    const globValue = moduleSpecifier.slice('#components/'.length)
    if (conditions.has('browser')) {
      return './src/components/client/' + globValue
    }
    if (conditions.has('default')) {
      return './src/components/server/' + globValue
    }
  }
  // ... do normal resolution here ...
}

Hope this tiny blog post was helpful; keep on building amazing stuff!

Subscribe to our newsletter

Get notified when we publish new security blog posts!

Related posts

Back to all posts
SocketSocket SOC 2 Logo

Product

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc