Your dependencies, naked.
This package introduces lightweight tooling as an alternative to bundlers.
It lets you use npm install as you normally would, and import dependencies via specifiers in the browser without a bundler or build step.
Yes, you read that right.
- ✅ No watchers! Nothing to remember to run before working on code
- ✅ No transpilation or bundling needed for either your code or your dependencies
- ✅ Granular cache busting, only for modules that change version
- ✅ No additional client-side code you need to run 1
- ✅ Even CJS packages work! (experimental)
Try it out in the demos repository.
For background, see Web dependencies are broken. Can we fix them?.
- How does it work?
- Nudeps vs JSPM
- Current limitations
- Installation
- Config options
- Commands
- Local dependencies
- FAQ
- Which browsers are supported?
- Does this support pnpm/bun/yarn/etc.?
- Why does it add the version number to the directory name?
- Do I need to add
.nudeps,client_modulesandimportmap.jsto my.gitignore? - Why doesn't Nudeps have an option to add integrity hashes to the import map?
- How are CJS (CommonJS) packages handled?
- Troubleshooting
You run nudeps install once to initialize the project.
That’s it.
You can then forget about it, it will run automatically whenever you install or uninstall packages.
Unless you need to transpile your JS for other reasons, you can write JS that just runs, no transpilation needed.
Instead of forcing you to use a CDN, Nudeps copies your dependencies to a local directory you specify (./client_modules by default), adds versions to directory names for cache busting, and generates an import map that maps specifiers to these local paths.
For example, lit may be mapped to "./client_modules/[email protected]/index.js".
All it takes to use these definitions is to include the importmap.js file in your HTML before any modules are loaded:
<script src="importmap.js"></script>You can include that one line of HTML either manually or via your templating system of choice. You can see an example of what such a file looks like at https://github.com/nudeps/nudeps-demos/blob/main/floating-ui/importmap.js
You then install and uninstall dependencies as needed and use them straight away, and both the import map and copied dependencies will be automatically updated. No, without you having to remember to run anything before or after.
If you want, you can periodically run nudeps --prune to subset the copied dependencies and import map to only those used by your own package entry points.
Nudeps is actually implemented as an opinionated wrapper over the excellent JSPM Generator, which handles a lot of the heavy lifting around import map generation. Unlike JSPM, Nudeps does not aim to cover all possible use cases. Instead, it aims to cover a subset of use cases with the best DX possible.
JSPM has paved the way in managing import maps that let you use specifiers in the browser and its Generator module doing a lot of the heavy lifting here.
However:
- It basically forces you into using a CDN. While there is a
nodemodulesprovider, it is only meant for local development and does not do any cache busting. However, CDNs are generally considered insecure, and introduce an additional point of failure in terms of reliability. Nudeps copies only the dependencies you use in a directory you specify and adds version numbers to directory names so that you get the same cache busting behavior as you would with a CDN, but in your own domain. - While subsetting modules to only those you actually use is a good idea, it forces you to run a build process before working on code, which is a hassle and error-prone, especially when working with beginners. Nudeps separates this subsetting into a separate step ("pruning") that you can run separately, as an optimization. This allows it to only run when dependencies change, rather than continuously during development.
With nudeps, your workflow is basically:
npm installornpm uninstallas needed- Just use specifiers in your code
- Nothing more to do, no build process to run, everything just works.
Here is a handy table to compare the two:
| Feature | nudeps | JSPM |
|---|---|---|
| Use specifiers both in your own code, and in code you distribute. | ✅ | ✅ |
Manages copying the right dependencies out of node_modules |
✅ | ❌ |
| Use dependencies without having to transpile your own code. | ✅ | ✅ |
| No "browser bundle" nonsense: common transitive dependencies are shared | ✅ | ✅ |
| Separate files are kept separate and cached separately. | ✅ | ✅ |
npm link still works |
✅ | ✅ |
| No build process to remember to run before working on code | ✅ | ❌ |
| Granular cache busting, only for modules that change version | ✅ | CDNs only |
| Import map automatically updated as you install packages | ✅ | ❌ |
| Supports CDNs like unpkg, jsdelivr, etc. | ❌ | ✅ |
| Self-host dependencies | ✅ | ❌ |
You can install nudeps as a devDependency, locally in each project or globally to have it available on every project.
This can be useful for signaling to collaborators that nudeps is required to work on the project.
npm install nudeps -DYou can also install Nudeps globally:
npm install nudeps -gDThen, whenever you want to initialize for a given project, just run nudeps in its root directory.
It will automatically detect that it has not run before and initialize.
This is essential, otherwise you’d need to manually run nudeps whenever you install or uninstall a dependency.
Either run nudeps install which will do this automatically, or add a dependencies script manually to your package.json:
{
"name": "my-project",
"scripts": {
"dependencies": "npx nudeps"
}
}Tip
If you have another dependencies script, you can use predependencies to run nudeps before it or postdependencies to run nudeps after it.
Then, to use the import map in your app, include this script in your HTML before any modules are loaded, either manually or via your templating system of choice:
<script src="/importmap.js"></script>Important
This script needs to be included before any module scripts are loaded or it won't work!
Each of the following options is available either as a config file key, or a command line option (e.g. foo would be --foo).
Some command line options allow for a shorthand one letter syntax, which is listed after a slash.
| Option | Config file key | CLI option | CLI short flag | Default | Description |
|---|---|---|---|---|---|
| Directory | dir |
--dir |
-d |
./client_modules |
Directory to copy deployed dependencies to, relative to project root. It will be created if it does not exist. It is assumed that Nudeps owns this directory, do not use a directory path that you use for other things. |
| Import map | map |
--map |
-m |
importmap.js |
File path for import map injection script, relative to project root. Nudeps needs to be able to own this file, do not input a file you use for other things too. |
| Prune | prune |
--prune |
false |
Whether to subset only to specifiers used by the package entry points (true), or include all direct dependencies anyway. |
|
| Ignore files | ignore |
- | - | See below | Any files to exclude from being copied to the target directory. See below for more details. |
| Exclude | exclude |
--exclude |
-e |
[] |
Any packages to exclude from import map even though they appear in dependencies. Useful for server-side dependencies. When providing via the command line option, comma-separate and do not include any spaces. They will still be included if actively used in your code. |
| External config file | - | --config |
-c |
nudeps.js |
File path for nudeps configuration, relative to project root. It should export an object literal with the configuration options as keys. |
| Overrides | overrides |
- | - | {} |
Overrides for the import map, using ./node_modules/ paths. Set a key to undefined to remove it from the map. |
| CommonJS | cjs |
--cjs |
- | true |
Whether to add a CommonJS shim to the import if any CJS packages are detected. Setting to false will omit both the shim and these packages from the import map. |
By default, Nudeps will copy everything in each package except for the following:
readmeorREADMEfiles with any extension- Files and directories starting with a dot
package.json,package-lock.json,pnpm-lock.jsonfiles at the top level of any package
Why not just restrict to copying *.js files by default?
Because this allows dependencies to fetch other files dynamically, e.g. stylesheets, images, data files, etc.
This is particularly important for UI libraries, component libraries, etc.
Since files are only fetched when used, this does not impact actual bandwidth usage.
And if you’re trusting a package to run JS in your domain anyway, the additional risk from copying its entire package directory is tiny.
That said, there are cases where you know you won’t need certain files.
You can add additional globs (per Node’s native glob syntax) to be included or excluded by providing globs to the ignore option.
Its value can be either an array or a singular value.
Each glob can be provided as a raw string (glob to exclude) or an object with an include or exclude property.
The values of these properties can also be arrays of strings or objects.
Globs are relative to the package root.
The semantics are similar to a .gitignore file, meaning that negative globs can only undo globs that precede them.
For example:
- To include
package.jsonfiles you'd useignore: { include: "package.json" }. - To only copy
*.jsfiles and nothing else you'd useignore: [{ exclude: "**/*" }, { include: "**/*.js" } ]. (but see above why this is not recommended)
To restrict rules to specific packages, you can provide the rule as an object and add one or more (as an array) package names via the packageName property.
Initialize or update as needed. Takes care of
- Copying dependencies to the target directory
- Generating a new import map
Subset copied dependencies and import map to only those used by your own package entry points.
Subsequent runs of nudeps will respect previously pruned dependencies (unless you use --init).
This allows you to use dependencies immediately as they are added, without having to continuously watch all your JS files, and periodically run nudeps --prune to subset.
You can set prune: true in your config file to always prune dependencies but then you will need to re-run it when your code changes.
Force initialization, even if nudeps has already run. Note that this also clears the list of local dependents (see below). They will re-register the next time they run nudeps.
When you have local dependencies (installed via npm install ../other-repo), nudeps automatically handles propagation between them, but there are a few things you need to know about it.
- You need Nudeps on both sides of the dependency for things to work
- Instead of copying
other-repotoclient_modules/other-repo@<version>by default it creates a symlink. You can tweak thesymlinkoption to change this. - Since the npm
dependencieshook does not fire when the dependencies ofother-repochange (see npm bug #8984), Nudeps onother-repowill runnpm run dependencies --if-presentin its own dependencies to trigger nudeps in them.
Each time nudeps runs, it registers itself as a dependent of each of its local dependencies by writing its relative path to the dep's .nudeps/local-dependents.json.
If a local dependency doesn't have nudeps installed, a warning is printed suggesting you run npx nudeps install there.
When nudeps detects that the generated import map has actually changed (content differs from the file on disk), it reads .nudeps/local-dependents.json and runs npx nudeps in each listed dependent.
This ensures that when package B's dependencies change, any repo A that depends on B locally gets its import map updated automatically.
Circular local dependencies (A depends on B and B depends on A) are handled naturally: propagation only triggers when the map content changes, so cycles terminate once the maps converge.
Nudeps works in pretty much every browser that supports import maps, which is all of them at this point, including:
- Chrome 89+
- Safari 16.4+
- Firefox 108+
At the moment, we’re focusing on nailing the best DX possible, and to let us focus on that, we're cutting scope by only supporting npm for now. However, Nudeps should however work with any other package managers that follow similar conventions in terms of:
node_modulesdirectory structurepackage-lock.jsonfile formatpackage.jsonfile format
You're welcome to contribute support for other package managers, but please let me know first so we can discuss the best approach.
Because this allows you to get the same cache busting behavior as you would with a CDN, but in your own domain. It also allows us to flatten dependencies to get better caching behavior: when you upgrade a dependency, its own dependencies remain cached by the browser unless they also change version.
This is up to you.
.nudepsandclient_modulesinclude local.gitignorefiles that prevent you from accidentally committing paths from them, but you may want to gitignore them at the top level so that you don't see them in your IDE.- Whether you gitignore
importmap.jsis up to you. On one hand it's a generated file, and these generally should not be committed, on the other hand it can help track changes to dependencies in a compact way.
The purpose of integrity hashes is to guard against compromise in resources you don't control, such as public CDNs. When using Nudeps you host your own dependencies, so that is not necessary, and would unnecessarily double the size of your import map. However, if we later decide there is a need for this,the PR is already written.
When CJS packages are detected, cjs-browser-shim is automatically included.
This is a tiny shim that makes require() work in the browser, both for relative paths and specifiers, allowing such dependencies to work out of the box.
Note that you would need to import such dependencies using require() in your code, like so:
import { require } from "cjs-browser-shim";
const { createElement } = require("react");You can see a demo of this in nudeps-demos/react.
To disable this, set the cjs option to false and both these packages and the CJS shim will be omitted from the import map.
While most packages should work fine, some packages make certain over-reaching assumptions about the environment they are running in.
There are a few cases where not all specifiers supported by a package can be detected upfront, and are only added when actually used in your code. This is not frequent enough to warrant continuously running a watcher for every edit, but it can happen occassionally (e.g. see #25).
Before investigating further:
- Make sure your entry points are declared correctly in your
package.json - Run
npx nudeps
Some packages don't just use specifiers — they actively assume that if they can use specifiers, it must mean that a bundler is being used and that the environment is NodeJS or similar.
For example, as of this writing, using vue out of the box will fail with an error about process not being available.
There are two ways to fix this:
- Use the package's browser bundle through the
overridesoption. This is usually not advisable because it inlines dependencies that other packages may be using too, but sometimes it's the best way forwards. You can see an example of this in the Vue demo. - Stub NodeJS objects like
process. This can work if the surface area is limited, but it can quickly turn into a game of whack-a-mole. Additionally, it can cause bugs in other packages that depend on the presence of these objects to detect NodeJS.
Some packages use extension-less paths even for their own imports, e.g. ./foo/bar instead of ./foo/bar.js.
While this doesn't usually make it to the files they distribute, there are a few exceptions.
Because these are not actual specifiers, import maps will not help here.
However, since the browser will see these as URLs, you can take advantage of whatever URL rewriting capabilities your server has and simply rewrite not-found URLs in that directory to their corresponding .js paths.
For example, using a Netlify _redirects file this may look like this:
/client_modules/* /client_modules/:splat.js 301
Footnotes
-
Except for CJS packages, which require a shim that is automatically included. ↩