Manual Packages

Packages

elements man packages Read as markdown

The package installer is built into Elements and runs on the project server, alongside the build. elements install adds a package and the app rebuilds against it, and every build makes sure the packages your app declares are installed. There is no separate installer to run and nothing to keep in sync by hand. A package you are writing yourself installs from its folder and is watched like your own code, so you can work on a package and the app that uses it at the same time.

Terminal
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.

config.jsoc
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

The installer runs on the project server alongside builds and test runs, never at the same time as one, so two installs never conflict and an install never lands in the middle of a build. When an install finishes, the app rebuilds against it. Package code is built like your own: checked, tree-shaken, and hot reloaded, and every import path is resolved at build time to the exact file it will load at runtime.

Local packages

Build a package and the app that uses it at the same time, with one editor and no linking step. A package you are developing alongside your app installs from a directory instead of a registry. Give it the version local:

Terminal
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.

Put a local package in a directory outside the project, on the package search path. Everything inside the project directory is the project's own source, so the package lives beside it and is watched as a dependency.

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.

Fast

Installs are fast because they do as little as possible. 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.

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.