# npm packages
avril loads ESM and CommonJS packages directly from npm:
```ts
import { z } from 'npm:zod@^4';
console.log(z.number().parse(42));
```
Declare bare-name bindings in `avril.conf`:
```text
deps: [
'npm:zod@^4',
{from: 'npm:preact@^10', name: 'ui'},
],
```
Requests accept scoped and unscoped names, npm version ranges, dist-tags, and
export subpaths (`npm:preact@^10/hooks`). Published packages use their own
manifest dependencies, never the application's import map. avril does not
read `node_modules` or `.npmrc`, and never runs npm
installation scripts. Local directory `require` reads a local `package.json`
`main` only for directory lookup (no versioning or `node_modules`).
## Type declarations
`avril.conf` can redirect package imports for checking without changing runtime
imports, and can add explicit global declaration modules:
```text
types: {
imports: {
'npm:legacy-sdk': 'npm:@types/legacy-sdk@^3',
},
globals: ['npm:@types/node@^22'],
},
```
`types.imports` uses the same exact-key, trailing-slash prefix, and `null`
blocking semantics as the top-level `imports` map. Relative import
specifiers are resolved from the importing module; relative mapping keys,
targets, and `types.globals` entries are resolved from `avril.conf`. Globals must
be URLs or relative file paths, not bare package names. A versionless `npm:` key
can match the resolved versioned package import. These mappings affect type resolution only: they do not redirect
execution or enable Node runtime APIs.
Explicit overrides take precedence over bundled declarations. Otherwise the
resolver uses `types` export conditions, `types`/`typings`, declaration siblings,
and legacy declaration subpaths. Import and require conditions stay distinct.
`typesVersions` uses the pinned TypeScript backend version, the first matching
version range, and exact or wildcard path mappings; an exports map takes
precedence.
During `avril add`, `avril cache`, `avril update`, and `avril vendor`, Avril
discovers `@types` packages for npm imports without bundled declarations and
records the discovery result, including negative results, in the lock.
Automatic selection stays within the library's major version, including major
zero; it never falls back to an unrelated `latest`. Scoped libraries use the
DefinitelyTyped `@types/scope__name` convention. `avril check` and the language
server replay those locked results. Type dependencies are traversed only when needed
to resolve types; a discovered package is not automatically included as a
module or as a global declaration. `types.globals` explicitly adds declaration
roots to every compiler-options group. TypeScript `/// `
and `/// ` directives are resolved through the checked
module graph. A `types="name"` reference must resolve to an explicitly
configured global type package or a declared npm dependency/peer; Avril does not
fetch an arbitrary `@types/name` package or guess its version from a directive.
Reference edges remain scoped to their referring program rather than becoming
project-wide roots.
Run `avril cache main.ts` before offline/frozen checking to prepare the type graph.
`--no-types-packages` disables automatic DefinitelyTyped discovery and replay,
while retaining bundled declarations, explicit overrides, and configured globals.
## Resolution
`exports` and `imports` support exact keys, wildcard patterns, arrays, and
nested conditions. For `import`, active conditions are `avril`, `import`, `module`, and
`default`, evaluated in the manifest's order; `node`, `require`, and `browser`
are not active. For synchronous `require`, conditions are `avril`, `require`, and
`default` (never `import`/`module`/`node`), so conditional exports choose the
`require` branch independently from `import`. An exports map blocks access to
unexported subpaths and stays exact in both modes (no extension inference
through export targets). Without one, `import` uses `module`, `main`, then
`index.js`; `require` uses `main` before `index` (never `module`), with
`index.json`/`index.cjs` also accepted. Exact file subpaths are available;
relative `import`s must name files explicitly, while relative `require`s use
Node-style lookup (exact, then `.js`/`.json`/`.node` rejecting `.node`, then
directory `package.json` `main` and `index` files) against verified manifests.
Missing required peers are installed automatically. Available providers are
shared with consumers; different resolved dependency/peer environments get
separate package instances. Absent optional peers are not installed solely to
satisfy a peer declaration.
Recorded roots replay their locked dependency and peer bindings directly.
Those bindings are authoritative: startup does not rederive them from package
metadata or check that an edited edge still matches a declared version range
or peer context. Structural checks still reject invalid identities, unsafe
paths, and dangling references. New root contexts and `--update` use the
resolver to construct bindings. Recorded optional-dependency choices are also
replayed; use `--update` to resolve again for a different platform. Packages
that do not support the current platform cannot be loaded.
Node builtins, native addons, and runtime JSON `import`s are unsupported.
See `commonjs.md` for CommonJS/JSON `require` support: `.cjs`/`.cts` and
`.js` outside a `"type": "module"` scope execute as CommonJS (with ESM
wrappers for `import`), except for the exact entry file advertised by `module`,
which is ESM regardless of how it is imported. This hint does not change sibling
files or override explicit `.cjs`/`.cts` extensions. `main` selects an entry, not
its format. `.json` loads via `require` (parsing, not ESM), and `.mjs` stays ESM
regardless of package type. TypeScript uses avril's existing
transpiler (explicit `.cts` preserves CJS); type-only imports are erased
before runtime acquisition. A package being ESM does not guarantee that it
avoids Node APIs.
## Project-local storage and Nix
Packages are materialized beneath the discovered project root's `.avril/npm/`.
avril creates `.avril/.gitignore` containing `*` if it does not already exist;
it never overwrites an existing ignore file. There is no required global npm
cache or `node_modules` tree. `--lock=PATH` changes the lockfile location, not
the materialization root.
`import.meta.url` is the package file's **`file:` URL**. Relative asset URLs
work with ordinary `new URL('./asset', import.meta.url)`; reading those assets
still requires the relevant guest permissions. Identical resolved instances
share a module regardless of the spelling used to import them.
Commit `avril.lock`. It pins selections, instance edges, archive integrity,
and one manifest digest per package without embedding absolute project
paths. The full file-hash list lives in a content-addressed sidecar at
`.avril/npm/.manifests/.json`, so locks stay small no matter how
many files a package holds, and no retained tarball is needed for
read-only or frozen runs. Warm execution loads and authenticates file manifests
and root package metadata only for instances it uses, and verifies each module's
bytes when it is loaded. Preparation restores missing backing and instance trees
from their pins before synchronous `require` can run; `require` itself never
acquires packages. Nested `package.json` files are authenticated and parsed only
when module-type or directory-entry resolution needs them; explicit extensions
such as `.mjs` and `.cjs` do not need type metadata. Consumed metadata is shared
within a prepared instance, so later resolution uses the same verified snapshot.
Corruption in an unused file does not block execution. `avril cache `
is the explicit full-tree audit of reachable packages and reports corruption
rather than repairing it.
Peer-specific instances retain distinct module identities and real package
paths, so `import.meta.url` and relative asset reads work normally. New
instances are published atomically from verified package content using anchored
hard links where supported, with a copy fallback. Existing trees receive a
no-follow readiness check; their metadata and loaded files are verified at use.
`avril cache` audits both logical instance trees and their canonical backing trees.
Locks written by older avril versions carry the inline file list instead: normal
runs migrate visited entries to the compact form (publishing any missing sidecars
first), while unvisited
entries keep their old form until used. Frozen runs never rewrite the
lock, so an old lock stays byte-identical.
```sh
avril cache main.ts
avril run --offline --frozen main.ts
```
For Nix, acquire dependencies during a network-enabled preparation step, then
include `.avril/npm` and `avril.lock` in the build input. A complete tree works
read-only with `--offline --frozen`, including after relocation. `--frozen`
alone permits downloading missing pinned artifacts; `--offline` prohibits
network acquisition. Previously unknown computed imports need preparation
with `--include`/`--include-from`.
## Commands and permissions
`add`, `bundle`, `cache`, `info`, `update`, and `vendor` support npm packages
alongside JSR and HTTP modules. `bundle` selects packages' browser builds (see
[bundle.md](bundle.md)); running never consults the `browser` field. `avril add npm:zod` writes a caret range to `avril.conf`.
`--npm-registry=URL` overrides `https://registry.npmjs.org/` independently of
JSR's `--jsr-registry`. The lockfile records these bases as `npmRegistry` and
`jsrRegistry`, respectively. Registry downloads are restricted to that origin;
private registry authentication is not supported yet.
Vendoring mirrors verified npm data and manifest sidecars into `vendor/npm/`.
An offline frozen run with `--vendor=vendor` can reconstruct an absent
`.avril/npm` tree without retained tarballs. That reconstruction needs a
writable project directory.
Acquisition is host tooling authority, not guest network/filesystem authority.
Execution grants use virtual provenance URLs, not the local cache paths:
```sh
avril run -0 --allow-js-exec=https://registry.npmjs.org/zod/ main.ts
```
A version/file grant can be narrower, such as
`https://registry.npmjs.org/zod/4.1.5/index.js`. Transitive imports need their
own execution authority. Local project execution grants do not implicitly
authorize materialized npm code, and acquiring packages grants no guest
network or cache-filesystem access.