NPM
npm package management for Elixir — resolve, fetch, cache, and link npm packages from Mix without requiring Node.js for installation.
mix npm.install lodash
mix npm.exec eslint .
npm_ex reads package.json, resolves npm semver with PubGrub, writes package-lock.json, and links packages into node_modules/.
Why npm_ex
Elixir projects increasingly need JavaScript packages for assets, formatters, linters, browser libraries, and runtime integrations. npm_ex keeps that workflow inside Mix:
- no
npm installstep required for dependency resolution or linking - reproducible installs through
package-lock.json - global package cache in
~/.npm_ex/cache/ - npm registry auth, mirrors, scoped registries, peer/deprecation warnings
- CI-friendly Mix tasks for install, verify, audit, outdated, tree, and exec
Installation
def deps do
[{:duskmoon_npm, "~> 9.5.1"}]
end
mix npm.init
mix npm.install lodash
Elixir umbrella projects
For umbrella projects, prefer npm workspaces for frontend assets:
- keep one root
package.jsonwith aworkspaceslist such as["apps/*"] - keep a
package.jsonin each Phoenix web app that owns assets - run
mix npm.installfrom the umbrella root or from any configured workspace package; npm_ex resolves the workspace root and writes one rootpackage-lock.jsonandnode_modules/
Example root manifest:
{
"name": "my_umbrella",
"private": true,
"workspaces": ["apps/*"],
"dependencies": {
"tailwindcss": "4.3.0"
}
}
Example Phoenix web app manifest:
{
"name": "my_app_web",
"private": true,
"dependencies": {
"phoenix_html": "file:../../deps/phoenix_html"
},
"devDependencies": {
"postcss": "8.5.15"
}
}
npm_ex links workspace packages and local directory file: packages into the root node_modules/ and resolves registry dependencies into package-lock.json. When mix npm.install <package> is run from a workspace package, the dependency is saved to that package's package.json while installation still happens at the workspace root.
Ordinary install checks dependency ranges from each package's location, including
nested versions, and repairs incomplete transitive locks. Install and update lock
both required and optional dependency trees, so the resulting lock can be reused
with mix npm.install --frozen under the same manifests and security policy.
Optional dependency ranges replace duplicate required ranges, and the root
manifest's supported flat overrides apply throughout transitive resolution and
validation. Frozen install validates the graph without rewriting the lockfile.
Common workflows
# Install and maintain dependencies
mix npm.install
mix npm.install lodash@^4.0
mix npm.install eslint --save-dev
mix npm.update
mix npm.remove lodash
# CI / reproducibility
mix npm.install --frozen
mix npm.ci
mix npm.verify
# Inspect dependency state
mix npm.list
mix npm.tree
mix npm.why accepts
mix npm.outdated
# Run scripts and binaries
mix npm.run build
mix npm.exec eslint .
# Registry, cache, and config
mix npm.info express
mix npm.search react
mix npm.cache status
mix npm.config
How installs work
- Read root
package.jsondependencies, workspace package dependencies, dev dependencies, optional dependencies, and root overrides. - Resolve the full dependency tree using duskmoon_hex_solver and the bundled npm semver parser.
- Fetch registry packuments and tarballs with integrity verification.
- Store package contents in the global cache.
- Link registry packages plus local workspace packages into
node_modules/and writepackage-lock.json.
npm_ex writes npm package-lock.json v3 and stores npm_ex-specific security policy metadata under x-npm-ex. Existing legacy npm.lock files are migrated automatically when package-lock.json is missing.
Supply-chain safety
npm_ex is intentionally conservative around install-time code execution:
- package lifecycle hooks are not executed automatically
- packages declaring
preinstall,install,postinstall, orprepareare installed but reported as warnings - tarball paths are validated before extraction to prevent cache escapes
- transitive git, URL, GitHub shorthand, and
file:dependencies are blocked by default - direct exotic dependencies require an explicit
exotic_depsallowlist entry - registry origins and redirects are policy checked
- newly created packages and freshly published versions can warn during install
This blocks common install-time credential stealers that rely on postinstall hooks reading files like .env and exfiltrating secrets during dependency installation.
Auditing malicious packages
mix npm.audit supports npm vulnerability checks and OSV/OpenSSF malicious-package intelligence:
# npm registry vulnerability audit
mix npm.audit
# Strict online OSV malicious-package gate
mix npm.audit --osv
# Refresh the shared local malicious-package cache for the current lockfile
mix npm.audit --osv --write-cache --policy warn
# Deterministic offline gate using the shared cache or configured DB
mix npm.audit --compromised
--write-cache merges matching OSV advisories into ~/.npm_ex/security/compromised_packages.json by default. mix npm.audit --osv fails closed when OSV cannot be queried; mix npm.audit --compromised is offline and deterministic.
OpenSSF/OSV is the default-compatible open data source. Socket, Snyk, and Phylum provide valuable proprietary intelligence or install-time firewall workflows; they fit best as external scanners/proxies or future optional integrations rather than default npm_ex install dependencies.
Configuration
Most projects only need the defaults. Use mix npm.config to inspect effective settings.
Common environment variables:
NPM_REGISTRY,NPM_TOKEN,NPM_MIRRORNPM_EX_CACHE_DIR,NPM_INSTALL_DIRNPM_EX_BLOCK_EXOTIC_SUBDEPS,NPM_EX_EXOTIC_DEPSNPM_EX_ALLOWED_REGISTRIES,NPM_EX_ALLOW_REGISTRY_REDIRECTSNPM_EX_PACKAGE_AGE_WARNING_DAYS,NPM_EX_VERSION_AGE_WARNING_DAYSNPM_EX_COMPROMISED_DB_PATH,NPM_EX_COMPROMISED_POLICY
Elixir application config is also supported:
config :duskmoon_npm,
registry: "https://registry.npmjs.org",
token: System.get_env("NPM_TOKEN"),
cache_dir: Path.expand("~/.npm_ex"),
block_exotic_subdeps: true,
exotic_deps: [],
allowed_registries: ["https://registry.npmjs.org"],
allow_registry_redirects: false,
package_age_warning_days: 7,
version_age_warning_days: 3,
compromised_db_path: Path.expand("~/.npm_ex/security/compromised_packages.json"),
compromised_policy: :error
API organization
The main public API is NPM. Supporting modules are grouped by domain: NPM.Package.*, NPM.Dependency.*, NPM.Lockfile.*, NPM.Security.*, NPM.Registry.*, NPM.Config.*, NPM.Install.*, NPM.Node.*, NPM.NodeModules.*, and NPM.Diagnostics.*.
See CHANGELOG.md for the 0.7 migration map from older pre-namespace module names.
Documentation
Full guides and API documentation are available on HexDocs:
- Getting Started
- Dependency Workflows
- CI and Reproducibility
- Supply-Chain Safety
- Malicious Package Audits
- Configuration
- CLI and configuration cheatsheets
License
MIT © 2026 Danila Poyarkov