#+TITLE: floco
=floco= is a JavaScript package management and build tool powered by
[[https://nixos.org][Nix]].
=floco= is a bold departure from conventional JavaScript package management
tooling focusing on reproducibility, distributed caching, and sandboxing.
Every package is built in a strictly declared sandbox isolated from the runtime
system, much like an /OCI/ container.
This approach allows packages to be built once and reused on any system that
shares the same architecture and platform.
Artifacts are cached locally and may be distributed among a cluster of systems
allowing development environments to be created at more than twice the speed of
=npm= or =yarn=.
Underlying installers are implemented in =bash= and limit themselves to using
=coreutils=, =findutils=, and =jq= improving reproducibility and readability
compared to tooling implemented using a sprawling maze of JavaScript.
Despite this repository's seemingly small form, it is the result of nearly
a year of exploration, trial, and refinement.
A great deal of effort was expended to make this piece of software
/suck less/ than the competition, but rest assured that these routines were
not implemented naively.
Writing a JavaScript package management framework in =bash= and =nix= was
done /not because it is easy/ - on the contrary it was fucking hard.
* Getting Started
There's a dedicated
[[https://github.com/aakropotkin/floco/blob/main/doc/guides/basics.org][Getting Started]]
guide that is the best place to dive in.
For projects that depend only on registry packages without =install= scripts that require
native dependencies or cycle breaking,
the following process will have you up and running:
#+BEGIN_SRC shell
set -euo pipefail;
cd ./my-project;
nix flake init -t github:aakropotkin/floco;
nix run github:aakropotkin/floco -- translate;
[[ -r ./package-lock.json~ ]] && mv ./package-lock.json{~,};
nix build -f ./. global;
ls -R ./result;
#+END_SRC
** Documentation
A collection of documentation is hosted on GitHub on the project's
[[https://github.com/aakropotkin/floco/wiki][wiki]] tab, and is also
available under
[[https://github.com/aakropotkin/floco/blob/main/doc][<floco>/doc]]
alongside many of the workspace directories used in examples.
Additionally all CLI tooling and scripts support the =--help= option.
The =floco help CMD= sub-command may also be used to view "help" messages
for a given =CMD=.
** Templates
A simple template with the boilerplate needed for use with our /updaters/
is available through ~nix flake init -t github:aakropotkin/floco~.
More templates are on the way.
* CLI
The =floco= CLI interface is currently under active development and is
expected to change rapidly in the near future.
** Common Behaviors
Across the =floco= CLI a few behaviors are consistent.
The following config files are included/applied if they exist:
- ~/etc/floco/floco-cfg.{nix,json}~
- =${XDG_CONFIG_HOME:-$HOME/.config}/floco/floco-config.{nix,json}=
- "Local" =floco-config.{nix,json}= searched for between =PWD= and git
project root, or =/= if =PWD= is not a =git= repository checkout.
References to =floco= will be pulled from the nearest =flake.lock=, or
=nix registry list=, using =github:aakropotkin/floco/main= as a fallback.
** =floco translate=
Generate a =pdefs.nix= file from a =package[-lock].json= or
registry package.
This routine utilized =npm= internally to resolve packages and form
=node_modules= trees.
*** Local Project Example
#+BEGIN_SRC shell
mkdir -p /tmp/foo;
pushd /tmp/foo;
echo '{
"name": "@floco/phony",
"version": "4.2.0",
"dependencies": {
"lodash": "^4.17.21"
},
"scripts": {
"build": "touch ./built"
}
}' > ./package.json;
nix shell github:aakropotkin/floco;
floco -- translate -pt;
nix flake init github:aakropotkin/floco -t;
floco build; # or `nix build -f ./. global;
ls ./result/lib/node_modules/@floco/phony/;
#+END_SRC
*** Remote Project Example
#+BEGIN_SRC shell
mkdir -p /tmp/foo;
pushd /tmp/foo;
nix shell github:aakropotkin/floco;
floco -- translate -pt lodash@4.17.21;
nix flake init github:aakropotkin/floco -t registry;
echo '{ ident = "lodash"; version = "4.17.21"; }' > ./info.nix;
floco build; # or `nix build -f ./.;'
ls ./result/lib/node_modules/lodash/;
#+END_SRC
** =floco list=
List all declared projects by "key", being =<IDENT>/<VERSION>=, such as
=@foo/bar/4.2.0= or =baz/4.2.0=.
#+BEGIN_SRC shell :exports both :results output
nix run github:aakropotkin/floco -- list;
#+END_SRC
#+RESULTS:
#+begin_example
@webassemblyjs/wast-printer/1.9.0
@xtuc/ieee754/1.2.0
@xtuc/long/4.2.2
abab/2.0.6
abbrev/1.1.1
abbrev/2.0.0
abort-controller/3.0.0
accepts/1.3.8
acorn/6.4.2
acorn/7.4.1
acorn/8.8.2
acorn-globals/4.3.4
acorn-jsx/5.3.2
#+end_example
** =floco show=
Print the =pdef= record for a given package.
#+BEGIN_SRC shell :exports both :results output
nix run github:aakropotkin/floco -- show lodash@4.17.21 --json;
#+END_SRC
#+RESULTS:
#+begin_example json
{
"ident": "lodash",
"version": "4.17.21",
"ltype": "file",
"fetchInfo": {
"narHash": "sha256-amyN064Yh6psvOfLgcpktd5dRNQStUYHHoIqiI6DMek=",
"type": "tarball",
"url": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz"
},
"treeInfo": {}
}
#+end_example
* Core Scripts
=floco= uses a small collection of =bash= scripts to perform install tasks
and drive builds.
These scripts do not depend on Nix, and are suitable for standalone use
as replacements for ~pacote extract~ ( =install-module.sh= ) and
~(npm|yarn) run~ ( =run-script.sh= ).
You can install these as standalone executables using the =floco-utils=
installable in the top-level flake.
For example usage and more details please see
[[https://github.com/aakropotkin/floco/blob/main/doc/scripts/README.org#core-scripts][Core Scripts]].
* Updaters/Generators
The top level flake provides an installable =floco-updaters= as well as
=app= targets ( =fromPlock= and =fromRegistry= ) that can be used to generate
=pdefs.nix= and =pdefs.json= files to be loaded by as configs.
These scripts will allow you to convert existing JavaScript projects to be
used with =floco=, and update/regenerate configs as projects'
dependencies and build requirements change.
** =fromPlock=
This generator is intended for use with local projects.
It is essentially a wrapper around =npm i --package-lock-only=.
For example usage please see the
[[https://github.com/aakropotkin/floco/blob/main/doc/guides/basics.org][Getting Started]]
guide.
** =fromRegistry=
This generator is intended for use with published registry packages that
you'd like to make accessible to =floco= and =nix=.
This script behaves almost identically to =fromPlock=, except that it
ignores =devDependencies= entirely, and accepts package descriptors as an
argument ( as =npm= or =yarn= would ).
For example usage please see the
[[https://github.com/aakropotkin/floco/blob/main/doc/guides/native-deps.org#preparing-a-workspace][Native Dependencies]]
guide.
This script most useful for packaging executables and generating =treeInfo=
information for packages that have =install= scripts ( such as =node-gyp=
compilation ).
* Modules
Package metadata collection, also called /translation/, and project
composition is managed using
[[https://github.com/NixOS/nixpkgs/blob/master/lib/modules.nix][Nixpkgs Modules]]
similar to those used by
[[https://nixos.org/manual/nixos/stable/#sec-writing-modules][NixOS]],
[[https://github.com/nix-community/dream2nix][dream2nix]], or
[[https://github.com/nix-community/home-manager][home-manager]].
These modules are organized as sets of =interface.nix= and
=implementation.nix= files and are designed to be extensible.
** Organization
The core of the module system revolves around a record called =pdef=, short
for "package definition", which organizes translated metadata, and
=package= records which organize the build pipelines.
This separation simplifies the organization of the /translation/ and
/builder/ APIs, but the rationale runs further.
The split allows us to flatly state: build routines must never perform
impure operations, and translation routines must only produce fields that
can be serialized to JSON.
Serialization of translated metadata allows Nix's =flake= features to
drastically improve performance by leveraging
[[https://www.tweag.io/blog/2020-06-25-eval-cache/][eval caching]] to avoid
re-evaluation of recipe generation on successive runs.
** =pdef= Package Definitions
The =pdef= record closely mirrors the pseudo-standard schema used by most
=package.json= files; but is much stricter about how declarations
are written.
If desired, users could ditch =package.json= files altogether and simply
write =pdef= records for their projects.
** Translators
At time of writing only a few translators have been migrated from the alpha
iteration, [[https://github.com/aameen-tulip/at-node-nix][at-node-nix]], but
in the near future these will be finalized for production use.
*** =package.json=
This is our bread and butter, and serves as the default implementation for
creating a =pdef= record.
On its own this translator would require users to explicitly declare the
structure of their =node_modules/= tree using the =treeInfo= submodule.
For this reason we strongly recommend using the =package-lock.json=
translator for projects with large dependency graphs.
**** Progress on /Ideal Tree/
The term /ideal tree/ refers to the mapping of a =node_modules/= tree
from a dependency graph.
This process is by far the most complex and challenging aspect of
Node.js package management.
While =floco= currently relies on =npm= to generate /ideal trees/, this
is expected to end soon.
The alpha repository
[[https://github.com/aameen-tulip/at-node-nix][at-node-nix]] contains a
large body of routines to perform /best effort/ =treeInfo=
mapping, specifically handling projects which only require a single
version of any package ( this property is called /The Golden Rule/ in
package management contexts ).
Additionally the
[[https://github.com/aameen-tulip/at-node-nix/blob/main/lib/sat.nix#L372][semver resolution]]
routines used to fetch closures of /packument/ records effectively solve
half of the /ideal tree/ process, leaving only scope and /follows/
management to be completed.
*** =package-lock.json= v2/v3
This is by far the most developed translator, and is the recommended
option for large projects.
This translator will automatically fill =treeInfo= submodules, and
triggers minimal network fetching.
*** =yarn.lock= v5
A rudimentary translator exists to collect information from =yarn.lock=
v5 ( produced by =yarn= v3 ), but because these lockfiles lack
/ideal tree/ information users will need to provide =treeInfo= themselves.
In the future we intend to produce =treeInfo= from these locks using
the pinned version information they contain; but this routine still needs
to be authored.
* Experimental Features
** =treeFor=
A CLI frontend for the =npm= /ideal tree/ routine,
[[https://github.com/npm/cli/blob/main/workspaces/arborist/README.md][arborist]],
modified such that =package-lock.json= files can be emitted to =STDOUT=
without modifying the project.
This is expected to be used in later iterations of the /updaters/ allowing
them to be run on ~/nix/store/~ paths.
The =builtins.npmLock= example in the section takes advantage of this.
This executable is exposed as an installable and =app= in the
top-level flake.
** Nix Plugin
A =nix= plugin for use with ~nix --plugin-files ...~ is available in the
top level flake, along with a wrapper executable, =floco-nix=, which
automatically loads this plugin.
In the future this plugin is expected to grow into a full executable that
provides a suite of CLI commands; but for now it accepts =nix= arguments
and sub-commands.
This plugin was developed for Nix v2.12.0, but is likely compatible with
a wider range of versions.
*** New Builtins
Our plugin adds a few new =builtins= to the =nix= evaluator which are
useful for dynamically generating package definitions.
**** =builtins.npmShow=
Wraps ~npm show~ allowing Nix to query package registries using a users
existing =npm= config and any environment =NPM_CONFIG_*= variables.
While =floco= is already able to fetch package registry information
without any external tools; this builtin is useful for accessing private
package registries and inheriting authorization settings with
minimal setup.
#+BEGIN_SRC shell :results output :exports both
nix run github:aakropotkin/floco#floco-nix -- eval --json --expr '
builtins.attrNames ( builtins.npmShow "lodash" )
'|jq;
#+END_SRC
#+RESULTS:
#+begin_example json
[
"_cached",
"_contentLength",
"_hasShrinkwrap",
"_id",
"_nodeVersion",
"_npmOperationalInternal",
"_npmUser",
"_npmVersion",
"_rev",
"author",
"bugs",
"contributors",
"description",
"directories",
"dist",
"dist-tags",
"gitHead",
"homepage",
"icon",
"keywords",
"license",
"main",
"maintainers",
"name",
"readmeFilename",
"repository",
"scripts",
"time",
"users",
"version",
"versions"
]
#+end_example
**** =builtins.npmResolve=
Resolves package descriptors such as =foo@^1.0.0= or =bar@latest=
using =npm=, returning a resolved URI.
This has the same environment and configuration properties as =npmShow=.
NOTE: if you use ranges such as =lodash@2.x= you will want to use
=builtins.split= to parse the output.
#+BEGIN_SRC shell :results output :exports both
nix run github:aakropotkin/floco#floco-nix -- eval --expr '
builtins.npmResolve "lodash@latest"
';
#+END_SRC
#+RESULTS:
: "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz"
**** =builtins.npmLock=
Produces a virtual =package-lock.json= for a given project path
without modifying the project or making any writes to the filesystem.
This is an ideal alternative to the =fromRegistry= /updater/ when
used in combination with =builtins.fetchTree= and =builtins.npmResolve=.
In practice you can dynamically generate full dependency closures'
=treeInfo= records using this routine.
I currently use it for this purpose out in the field; but have avoided
using it in the default modules so that they are usable without plugins.
#+BEGIN_SRC shell :results output :exports both
nix run github:aakropotkin/floco#floco-nix -- eval --impure \
--expr 'let
url = builtins.npmResolve "pacote@latest";
src = builtins.fetchTree { type = "tarball"; inherit url; };
plock = builtins.npmLock src;
in builtins.attrNames plock
';
#+END_SRC
#+RESULTS:
: [ "lockfileVersion" "name" "packages" "requires" "version" ]
**** =builtins.semverSat=
Runs =node-semver= to test whether a semantic version satisfies
a constraint.
In the future =node-semver= will be replaced using a native C++ port
[[https://github.com/aakropotkin/semi.git][semi]].
This largely exists as a stop-gap until the pure =nix= implementation
from the alpha repository is polished and/or =semi= is completed.
#+BEGIN_SRC shell :results output :exports both
nix run github:aakropotkin/floco#floco-nix -- eval --expr '[
( builtins.semverSat "^4.2.0" "4.0.0" )
( builtins.semverSat "^4.2.0" "4.2.0" )
( builtins.semverSat "^4.2.0" "4.2.1" )
( builtins.semverSat "^4.2.0" "4.3.0" )
]
';
#+END_SRC
#+RESULTS:
: [ false true true true ]
* Future Extensions
Many of the following extensions have function drafts or well tested
prototypes in the alpha release of =floco=; but are not developed enough for
use in production code-bases as pieces of reliable infrastructure.
- Improved support for package.json workspaces.
+ Currently reliance on =npm= and special configuration based on in depth
knowledge of =floco= is necessary to accomplish workspace support.
+ Practically a template or example using workspaces is likely sufficient
for the immediate future; but the NixOS Module system is expected to
resolve issues that previously made workspaces complex to manage.
- Expanded CLI tooling.
+ Currently users are asked to interact with nix to drive builds, tests,
update metadata, etc.
Ideally a simple bash script would provide familiar commands such as
~floco add <PKG>~, ~floco publish~, ~floco update~, ~floco build~,
etc that =npm= and =yarn= users are already familiar with.
- Nix plugin to read/write caches globally and into =flake.lock=.
+ This is the real end goal for =floco=.
It should be possible to read/write =floco= metadata to =flake.lock= and
existing =nix= caches.
+ There is currently a draft plugin which allows nix to adopt =npm= URIs to
refer to packages as =lodash@4.17.21= which could be expanded upon.
+ Project templates and propagation of build recipes could allow =nix= to
abstract away the generation of =flake.nix= for the vast majority of
projects which would be a significant UX breakthrough.
- Semantic version parsing, and /ideal tree/ formation.
+ Currently =floco= really relies on =npm= and its =package-lock.json= to
construct non-trivial node_module/ metadata declarations.
This reliance is a major pain point for handling projects which currently
use yarn since interoperability between =yarn= and =npm= across their
associated lockfiles is implemented incredibly poorly, to such a degree
that you cannot trust them to behave predictably in the same source tree.
+ Semver parsing and solving SAT is implemented in the alpha repository, and
has been testing on large non-trivial inputs quite successfully.
Still this effort requires a few weeks of polishing to really approve for
use in production.
- For now we have provided
[[https://github.com/npm/node-semver.git][node-semver]] as an
installable in the top-level flake for use in scripts and our
[[https://github.com/aakropotkin/floco/blob/main/pkgs/nix-plugin][floco-nix]]
through =builtins.semverSat=.
+ Construction of ideal tree from semver SAT is a project in and of itself
in order to support things like =optionDependencies=, =peerDependencies=,
=bundledDependencies=, and other oddballs which are a prerequisite for use
in the general case.
* Community
** Matrix
Sadly IRC is dead. IRC remains dead.
So like most folks these days we use Matrix Chat.
Space: [[https://matrix.to/#/#floco:matrix.org][#floco:matrix.org]]
General Room:
[[https://matrix.to/#/!wMSeevIIjIbAOVbqHh:matrix.org?via=matrix.org]]
( Recommended )
Support Room:
[[https://matrix.to/#/!tBPFHeGmZfhbuYgvcw:matrix.org?via=matrix.org]]
Development Room:
[[https://matrix.to/#/!qDFpEnHkbpkhLSenko:matrix.org?via=matrix.org]]
* Supporters
** [[https://tulip.co/][Tulip Interfaces]]
=floco= was originally developed for use by Tulip Interfaces.
Without their support this project never would have been possible.
Not written in Markdown, so it's shown here as plain text — view it formatted on GitHub.
Nix
80.7%
Shell
16.4%
C++
2.6%
#+TITLE: floco
=floco= is a JavaScript package management and build tool powered by
[[https://nixos.org][Nix]].
=floco= is a bold departure from conventional JavaScript package management
tooling focusing on reproducibility, distributed caching, and sandboxing.
Every package is built in a strictly declared sandbox isolated from the runtime
system, much like an /OCI/ container.
This approach allows packages to be built once and reused on any system that
shares the same architecture and platform.
Artifacts are cached locally and may be distributed among a cluster of systems
allowing development environments to be created at more than twice the speed of
=npm= or =yarn=.
Underlying installers are implemented in =bash= and limit themselves to using
=coreutils=, =findutils=, and =jq= improving reproducibility and readability
compared to tooling implemented using a sprawling maze of JavaScript.
Despite this repository's seemingly small form, it is the result of nearly
a year of exploration, trial, and refinement.
A great deal of effort was expended to make this piece of software
/suck less/ than the competition, but rest assured that these routines were
not implemented naively.
Writing a JavaScript package management framework in =bash= and =nix= was
done /not because it is easy/ - on the contrary it was fucking hard.
* Getting Started
There's a dedicated
[[https://github.com/aakropotkin/floco/blob/main/doc/guides/basics.org][Getting Started]]
guide that is the best place to dive in.
For projects that depend only on registry packages without =install= scripts that require
native dependencies or cycle breaking,
the following process will have you up and running:
#+BEGIN_SRC shell
set -euo pipefail;
cd ./my-project;
nix flake init -t github:aakropotkin/floco;
nix run github:aakropotkin/floco -- translate;
[[ -r ./package-lock.json~ ]] && mv ./package-lock.json{~,};
nix build -f ./. global;
ls -R ./result;
#+END_SRC
** Documentation
A collection of documentation is hosted on GitHub on the project's
[[https://github.com/aakropotkin/floco/wiki][wiki]] tab, and is also
available under
[[https://github.com/aakropotkin/floco/blob/main/doc][<floco>/doc]]
alongside many of the workspace directories used in examples.
Additionally all CLI tooling and scripts support the =--help= option.
The =floco help CMD= sub-command may also be used to view "help" messages
for a given =CMD=.
** Templates
A simple template with the boilerplate needed for use with our /updaters/
is available through ~nix flake init -t github:aakropotkin/floco~.
More templates are on the way.
* CLI
The =floco= CLI interface is currently under active development and is
expected to change rapidly in the near future.
** Common Behaviors
Across the =floco= CLI a few behaviors are consistent.
The following config files are included/applied if they exist:
- ~/etc/floco/floco-cfg.{nix,json}~
- =${XDG_CONFIG_HOME:-$HOME/.config}/floco/floco-config.{nix,json}=
- "Local" =floco-config.{nix,json}= searched for between =PWD= and git
project root, or =/= if =PWD= is not a =git= repository checkout.
References to =floco= will be pulled from the nearest =flake.lock=, or
=nix registry list=, using =github:aakropotkin/floco/main= as a fallback.
** =floco translate=
Generate a =pdefs.nix= file from a =package[-lock].json= or
registry package.
This routine utilized =npm= internally to resolve packages and form
=node_modules= trees.
*** Local Project Example
#+BEGIN_SRC shell
mkdir -p /tmp/foo;
pushd /tmp/foo;
echo '{
"name": "@floco/phony",
"version": "4.2.0",
"dependencies": {
"lodash": "^4.17.21"
},
"scripts": {
"build": "touch ./built"
}
}' > ./package.json;
nix shell github:aakropotkin/floco;
floco -- translate -pt;
nix flake init github:aakropotkin/floco -t;
floco build; # or `nix build -f ./. global;
ls ./result/lib/node_modules/@floco/phony/;
#+END_SRC
*** Remote Project Example
#+BEGIN_SRC shell
mkdir -p /tmp/foo;
pushd /tmp/foo;
nix shell github:aakropotkin/floco;
floco -- translate -pt lodash@4.17.21;
nix flake init github:aakropotkin/floco -t registry;
echo '{ ident = "lodash"; version = "4.17.21"; }' > ./info.nix;
floco build; # or `nix build -f ./.;'
ls ./result/lib/node_modules/lodash/;
#+END_SRC
** =floco list=
List all declared projects by "key", being =<IDENT>/<VERSION>=, such as
=@foo/bar/4.2.0= or =baz/4.2.0=.
#+BEGIN_SRC shell :exports both :results output
nix run github:aakropotkin/floco -- list;
#+END_SRC
#+RESULTS:
#+begin_example
@webassemblyjs/wast-printer/1.9.0
@xtuc/ieee754/1.2.0
@xtuc/long/4.2.2
abab/2.0.6
abbrev/1.1.1
abbrev/2.0.0
abort-controller/3.0.0
accepts/1.3.8
acorn/6.4.2
acorn/7.4.1
acorn/8.8.2
acorn-globals/4.3.4
acorn-jsx/5.3.2
#+end_example
** =floco show=
Print the =pdef= record for a given package.
#+BEGIN_SRC shell :exports both :results output
nix run github:aakropotkin/floco -- show lodash@4.17.21 --json;
#+END_SRC
#+RESULTS:
#+begin_example json
{
"ident": "lodash",
"version": "4.17.21",
"ltype": "file",
"fetchInfo": {
"narHash": "sha256-amyN064Yh6psvOfLgcpktd5dRNQStUYHHoIqiI6DMek=",
"type": "tarball",
"url": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz"
},
"treeInfo": {}
}
#+end_example
* Core Scripts
=floco= uses a small collection of =bash= scripts to perform install tasks
and drive builds.
These scripts do not depend on Nix, and are suitable for standalone use
as replacements for ~pacote extract~ ( =install-module.sh= ) and
~(npm|yarn) run~ ( =run-script.sh= ).
You can install these as standalone executables using the =floco-utils=
installable in the top-level flake.
For example usage and more details please see
[[https://github.com/aakropotkin/floco/blob/main/doc/scripts/README.org#core-scripts][Core Scripts]].
* Updaters/Generators
The top level flake provides an installable =floco-updaters= as well as
=app= targets ( =fromPlock= and =fromRegistry= ) that can be used to generate
=pdefs.nix= and =pdefs.json= files to be loaded by as configs.
These scripts will allow you to convert existing JavaScript projects to be
used with =floco=, and update/regenerate configs as projects'
dependencies and build requirements change.
** =fromPlock=
This generator is intended for use with local projects.
It is essentially a wrapper around =npm i --package-lock-only=.
For example usage please see the
[[https://github.com/aakropotkin/floco/blob/main/doc/guides/basics.org][Getting Started]]
guide.
** =fromRegistry=
This generator is intended for use with published registry packages that
you'd like to make accessible to =floco= and =nix=.
This script behaves almost identically to =fromPlock=, except that it
ignores =devDependencies= entirely, and accepts package descriptors as an
argument ( as =npm= or =yarn= would ).
For example usage please see the
[[https://github.com/aakropotkin/floco/blob/main/doc/guides/native-deps.org#preparing-a-workspace][Native Dependencies]]
guide.
This script most useful for packaging executables and generating =treeInfo=
information for packages that have =install= scripts ( such as =node-gyp=
compilation ).
* Modules
Package metadata collection, also called /translation/, and project
composition is managed using
[[https://github.com/NixOS/nixpkgs/blob/master/lib/modules.nix][Nixpkgs Modules]]
similar to those used by
[[https://nixos.org/manual/nixos/stable/#sec-writing-modules][NixOS]],
[[https://github.com/nix-community/dream2nix][dream2nix]], or
[[https://github.com/nix-community/home-manager][home-manager]].
These modules are organized as sets of =interface.nix= and
=implementation.nix= files and are designed to be extensible.
** Organization
The core of the module system revolves around a record called =pdef=, short
for "package definition", which organizes translated metadata, and
=package= records which organize the build pipelines.
This separation simplifies the organization of the /translation/ and
/builder/ APIs, but the rationale runs further.
The split allows us to flatly state: build routines must never perform
impure operations, and translation routines must only produce fields that
can be serialized to JSON.
Serialization of translated metadata allows Nix's =flake= features to
drastically improve performance by leveraging
[[https://www.tweag.io/blog/2020-06-25-eval-cache/][eval caching]] to avoid
re-evaluation of recipe generation on successive runs.
** =pdef= Package Definitions
The =pdef= record closely mirrors the pseudo-standard schema used by most
=package.json= files; but is much stricter about how declarations
are written.
If desired, users could ditch =package.json= files altogether and simply
write =pdef= records for their projects.
** Translators
At time of writing only a few translators have been migrated from the alpha
iteration, [[https://github.com/aameen-tulip/at-node-nix][at-node-nix]], but
in the near future these will be finalized for production use.
*** =package.json=
This is our bread and butter, and serves as the default implementation for
creating a =pdef= record.
On its own this translator would require users to explicitly declare the
structure of their =node_modules/= tree using the =treeInfo= submodule.
For this reason we strongly recommend using the =package-lock.json=
translator for projects with large dependency graphs.
**** Progress on /Ideal Tree/
The term /ideal tree/ refers to the mapping of a =node_modules/= tree
from a dependency graph.
This process is by far the most complex and challenging aspect of
Node.js package management.
While =floco= currently relies on =npm= to generate /ideal trees/, this
is expected to end soon.
The alpha repository
[[https://github.com/aameen-tulip/at-node-nix][at-node-nix]] contains a
large body of routines to perform /best effort/ =treeInfo=
mapping, specifically handling projects which only require a single
version of any package ( this property is called /The Golden Rule/ in
package management contexts ).
Additionally the
[[https://github.com/aameen-tulip/at-node-nix/blob/main/lib/sat.nix#L372][semver resolution]]
routines used to fetch closures of /packument/ records effectively solve
half of the /ideal tree/ process, leaving only scope and /follows/
management to be completed.
*** =package-lock.json= v2/v3
This is by far the most developed translator, and is the recommended
option for large projects.
This translator will automatically fill =treeInfo= submodules, and
triggers minimal network fetching.
*** =yarn.lock= v5
A rudimentary translator exists to collect information from =yarn.lock=
v5 ( produced by =yarn= v3 ), but because these lockfiles lack
/ideal tree/ information users will need to provide =treeInfo= themselves.
In the future we intend to produce =treeInfo= from these locks using
the pinned version information they contain; but this routine still needs
to be authored.
* Experimental Features
** =treeFor=
A CLI frontend for the =npm= /ideal tree/ routine,
[[https://github.com/npm/cli/blob/main/workspaces/arborist/README.md][arborist]],
modified such that =package-lock.json= files can be emitted to =STDOUT=
without modifying the project.
This is expected to be used in later iterations of the /updaters/ allowing
them to be run on ~/nix/store/~ paths.
The =builtins.npmLock= example in the section takes advantage of this.
This executable is exposed as an installable and =app= in the
top-level flake.
** Nix Plugin
A =nix= plugin for use with ~nix --plugin-files ...~ is available in the
top level flake, along with a wrapper executable, =floco-nix=, which
automatically loads this plugin.
In the future this plugin is expected to grow into a full executable that
provides a suite of CLI commands; but for now it accepts =nix= arguments
and sub-commands.
This plugin was developed for Nix v2.12.0, but is likely compatible with
a wider range of versions.
*** New Builtins
Our plugin adds a few new =builtins= to the =nix= evaluator which are
useful for dynamically generating package definitions.
**** =builtins.npmShow=
Wraps ~npm show~ allowing Nix to query package registries using a users
existing =npm= config and any environment =NPM_CONFIG_*= variables.
While =floco= is already able to fetch package registry information
without any external tools; this builtin is useful for accessing private
package registries and inheriting authorization settings with
minimal setup.
#+BEGIN_SRC shell :results output :exports both
nix run github:aakropotkin/floco#floco-nix -- eval --json --expr '
builtins.attrNames ( builtins.npmShow "lodash" )
'|jq;
#+END_SRC
#+RESULTS:
#+begin_example json
[
"_cached",
"_contentLength",
"_hasShrinkwrap",
"_id",
"_nodeVersion",
"_npmOperationalInternal",
"_npmUser",
"_npmVersion",
"_rev",
"author",
"bugs",
"contributors",
"description",
"directories",
"dist",
"dist-tags",
"gitHead",
"homepage",
"icon",
"keywords",
"license",
"main",
"maintainers",
"name",
"readmeFilename",
"repository",
"scripts",
"time",
"users",
"version",
"versions"
]
#+end_example
**** =builtins.npmResolve=
Resolves package descriptors such as =foo@^1.0.0= or =bar@latest=
using =npm=, returning a resolved URI.
This has the same environment and configuration properties as =npmShow=.
NOTE: if you use ranges such as =lodash@2.x= you will want to use
=builtins.split= to parse the output.
#+BEGIN_SRC shell :results output :exports both
nix run github:aakropotkin/floco#floco-nix -- eval --expr '
builtins.npmResolve "lodash@latest"
';
#+END_SRC
#+RESULTS:
: "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz"
**** =builtins.npmLock=
Produces a virtual =package-lock.json= for a given project path
without modifying the project or making any writes to the filesystem.
This is an ideal alternative to the =fromRegistry= /updater/ when
used in combination with =builtins.fetchTree= and =builtins.npmResolve=.
In practice you can dynamically generate full dependency closures'
=treeInfo= records using this routine.
I currently use it for this purpose out in the field; but have avoided
using it in the default modules so that they are usable without plugins.
#+BEGIN_SRC shell :results output :exports both
nix run github:aakropotkin/floco#floco-nix -- eval --impure \
--expr 'let
url = builtins.npmResolve "pacote@latest";
src = builtins.fetchTree { type = "tarball"; inherit url; };
plock = builtins.npmLock src;
in builtins.attrNames plock
';
#+END_SRC
#+RESULTS:
: [ "lockfileVersion" "name" "packages" "requires" "version" ]
**** =builtins.semverSat=
Runs =node-semver= to test whether a semantic version satisfies
a constraint.
In the future =node-semver= will be replaced using a native C++ port
[[https://github.com/aakropotkin/semi.git][semi]].
This largely exists as a stop-gap until the pure =nix= implementation
from the alpha repository is polished and/or =semi= is completed.
#+BEGIN_SRC shell :results output :exports both
nix run github:aakropotkin/floco#floco-nix -- eval --expr '[
( builtins.semverSat "^4.2.0" "4.0.0" )
( builtins.semverSat "^4.2.0" "4.2.0" )
( builtins.semverSat "^4.2.0" "4.2.1" )
( builtins.semverSat "^4.2.0" "4.3.0" )
]
';
#+END_SRC
#+RESULTS:
: [ false true true true ]
* Future Extensions
Many of the following extensions have function drafts or well tested
prototypes in the alpha release of =floco=; but are not developed enough for
use in production code-bases as pieces of reliable infrastructure.
- Improved support for package.json workspaces.
+ Currently reliance on =npm= and special configuration based on in depth
knowledge of =floco= is necessary to accomplish workspace support.
+ Practically a template or example using workspaces is likely sufficient
for the immediate future; but the NixOS Module system is expected to
resolve issues that previously made workspaces complex to manage.
- Expanded CLI tooling.
+ Currently users are asked to interact with nix to drive builds, tests,
update metadata, etc.
Ideally a simple bash script would provide familiar commands such as
~floco add <PKG>~, ~floco publish~, ~floco update~, ~floco build~,
etc that =npm= and =yarn= users are already familiar with.
- Nix plugin to read/write caches globally and into =flake.lock=.
+ This is the real end goal for =floco=.
It should be possible to read/write =floco= metadata to =flake.lock= and
existing =nix= caches.
+ There is currently a draft plugin which allows nix to adopt =npm= URIs to
refer to packages as =lodash@4.17.21= which could be expanded upon.
+ Project templates and propagation of build recipes could allow =nix= to
abstract away the generation of =flake.nix= for the vast majority of
projects which would be a significant UX breakthrough.
- Semantic version parsing, and /ideal tree/ formation.
+ Currently =floco= really relies on =npm= and its =package-lock.json= to
construct non-trivial node_module/ metadata declarations.
This reliance is a major pain point for handling projects which currently
use yarn since interoperability between =yarn= and =npm= across their
associated lockfiles is implemented incredibly poorly, to such a degree
that you cannot trust them to behave predictably in the same source tree.
+ Semver parsing and solving SAT is implemented in the alpha repository, and
has been testing on large non-trivial inputs quite successfully.
Still this effort requires a few weeks of polishing to really approve for
use in production.
- For now we have provided
[[https://github.com/npm/node-semver.git][node-semver]] as an
installable in the top-level flake for use in scripts and our
[[https://github.com/aakropotkin/floco/blob/main/pkgs/nix-plugin][floco-nix]]
through =builtins.semverSat=.
+ Construction of ideal tree from semver SAT is a project in and of itself
in order to support things like =optionDependencies=, =peerDependencies=,
=bundledDependencies=, and other oddballs which are a prerequisite for use
in the general case.
* Community
** Matrix
Sadly IRC is dead. IRC remains dead.
So like most folks these days we use Matrix Chat.
Space: [[https://matrix.to/#/#floco:matrix.org][#floco:matrix.org]]
General Room:
[[https://matrix.to/#/!wMSeevIIjIbAOVbqHh:matrix.org?via=matrix.org]]
( Recommended )
Support Room:
[[https://matrix.to/#/!tBPFHeGmZfhbuYgvcw:matrix.org?via=matrix.org]]
Development Room:
[[https://matrix.to/#/!qDFpEnHkbpkhLSenko:matrix.org?via=matrix.org]]
* Supporters
** [[https://tulip.co/][Tulip Interfaces]]
=floco= was originally developed for use by Tulip Interfaces.
Without their support this project never would have been possible.
Not written in Markdown, so it's shown here as plain text — view it formatted on GitHub.
Nix
80.7%
Shell
16.4%
C++
2.6%