Manual Packages

Packages

elements man packages Read as markdown

Installing packages is part of the build. You state a dependency, the project server resolves it, writes node_modules, and rebuilds against it, on the same loop that compiles your files and runs your tests. There is no separate installer to run and nothing to keep in sync by hand.

elements install lodash              # add a package at its latest version
elements install stripe@^14          # a range
elements install dayjs@1.11.10       # a pin
elements install @elements/app@local # a package you are building yourself
elements install                     # install everything config.jsoc declares
elements install -u                  # move each package to the newest its spec allows
elements uninstall lodash

The model

Dependencies are declared under package.dependencies in config.jsoc, and the versions they resolve to are written to package.lock. The CLI owns both: elements install <pkg> adds the spec to the config and the resolution to the lock in one step. You can also edit the dependency map by hand and save; the next build installs what changed. package.lock is generated. Do not edit it.

package: {
  dependencies: {
    "@elements/app":   "latest",
    "@elements/style": "latest",
    "stripe":          "^14",
  },
},

An existing resolution is reused from the lock, so a build stays on the versions it was tested against until you ask. -u upgrades within each spec. -f ignores the lock and resolves everything again, which can move packages you did not ask about, so reach for a version spec first. -d and -p install as dev and peer dependencies. An install that cannot resolve prints the reason and exits non-zero, and neither file is touched.

Part of the build loop

The installer runs through the same sequential build loop as everything else, so it is sequenced with file changes, type checks, and test runs. Two installs never conflict, and an install never interleaves with a half-finished build. Package sources enter the same source graph as your code: they are versioned, cached, tree-shaken, and hot reloaded like anything else, and every import path is resolved at build time to the exact file it will load at runtime.

Fast

The node_modules tree is computed in memory and diffed against what is on disk, so an install writes only the packages that actually changed. An install that changes nothing touches nothing. A package that is written is cloned into place, copy-on-write where the filesystem supports it, rather than extracted file by file. Registry manifests and tarballs are cached per machine under ~/elements/packages, so a package you have installed once anywhere lands in the next project without a download. Resolution reads the manifest, not the tarball, so the only thing a fresh install waits on is the bytes it has not seen before.

Node module compatible

node_modules is a real tree laid out the way Node resolves modules, with nested copies where two packages need different versions of the same dependency. Anything that reads node_modules works, and every npm package installs as is. Packages come from the npm registry and from packages.elements.dev, which is where the Elements packages live.

Elements emits CJS on both the server and the browser, so ESM-only packages and CJS-only packages both work everywhere; see build for why.

Local packages

A package you are developing alongside your app installs from a directory instead of a registry. Give it the version local:

elements install @elements/app@local

or write "@elements/app": "local" in config.jsoc. The package is found on the package search path: the directories in ELEMENTS_PACKAGE_PATH (a path list, ~ allowed), or ~/src when that is unset, so @elements/app resolves to ~/src/@elements/app by default. Set ELEMENTS_PACKAGE_PATH in config/env/development.env to point somewhere else. If the directory carries a build under .elements/package/all or .elements/package/<os>-<arch>, that build is what installs; otherwise the directory itself does.

A local package must live outside the project. Inside it, the project would be watching its own dependency as source, and an install could not tell the two apart.

Local packages are watched. Save a file in the package and the project server copies the change into node_modules and rebuilds the app against it, the same as saving a file in the app. That is how you work on a package and the app that uses it at the same time, with one editor and no linking step.

Flip the spec back to a registry version and the local copy is evicted on the next install; flip to local again and it is re-cloned cleanly. A deploy ships local packages to the machine and installs them there, so an app built on a package you have not published deploys the same as any other.

Where things live

  • config.jsoc: the specs you declared.
  • package.lock: the resolutions, generated.
  • node_modules/: the installed tree, generated. Delete it and the next install rebuilds it in full.
  • ~/elements/packages/: the per-machine cache of manifests and package sources, shared by every project.

Related

  • build: the build loop that installs, compiles, migrates, and tests on save.
  • cli: every install flag.
  • config: config.jsoc and env files.
  • deploy: how packages, local ones included, reach a machine.