Packages
elements man packages Read as markdownThe 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.
Terminalelements 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.jsocpackage: { 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:
Terminalelements 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.jsocand env files.deploy: how packages, local ones included, reach a machine.