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