Description
One of the discussions at the Node.js Collaborator Summit in Austin last week was about the strategy of vendoring in Node.js ecosystem modules to distribute with the node
binary. The proposal discussed was to introduce a new vendor:
or vendored:
specifier prefix to access such modules included in the distribution. This issue is for discussing that.
This issue was prompted by discussions on the @nodejs/tooling team.
For the sake of this introduction, we'll assume the namespace is vendor:
, but that name still needs to be decided on.
The Proposal ...
The vendor:
namespace provides a clear namespace for exposing vendored-in dependencies distributed with the Node.js binary. For instance, assuming some arbitrary npm module foo
that Node.js decided, for whatever reason, to bundle and distribute with the binary, the module specifier vendor:foo
can be used to load that module from within the node binary as opposed to searching for it in the node_modules
path.
For a more practical example, Node.js currently bundles the undici
HTTP client to provide the implementation of the fetch()
API but does not expose the full undici
module API. If Node.js did decide to expose the full undici
module underlying the fetch implementation, it could decide to do so, using the vendor:undici
specifier.
Modules exported within the vendor:
namespace are not Node.js built-in modules. They are simply ecosystem modules that are distributed with the node binary. These modules are:
- Developed independently of Node.js core.
- May or may not be developed by the Node.js project under the OpenJS Foundation
- May or may not have been developed to the same standards/guidelines as the Node.js project (for instance, they might not make use of the Node.js internal errors or primordials mechanisms).
- May or may not be within the nodejs GitHub Organization
Bugs in the vendored module are not considered to be bugs in Node.js (this is similar to how we treat bugs in npm, for instance, which is also distributed with the Node.js binary).
Why would we do this?
Some modules are used by nearly every one, provide core functionality needed by Node.js itself, and are so important to the ecosystem that bundling them just makes sense. Many of these are already bundled with the node.js distribution via dependencies to the npm client. Whether or not there is sufficient justification to include these in the distribution itself is part of this conversation. Specifically, what is our decision matrix for accepting these?
Small Core!!?
Adding a vendor module does not increase the LTS supported core API surface area of Node.js, and most of the modules that will be considered for bundling are already distributed with Node.js as dependencies of the npm client.
What about versions?
Yes, vendored modules might end up out of date with what is published on the npmjs registry. Node.js will make a best effort to keep the dependencies up to date but it is expected to be common that a vendored version will be older than the version available on npm. If an application wants to make sure that it is using the latest version and not the one bundled in Node.js, then it can continue to list that module as a dependency in its package.json and use the non-prefixed specifier when importing/requiring the module.
What about LTS?
Node.js will support vendored modules only in the sense that Node.js will ensure that non-breaking or security updates to vendored modules will be handled as part of LTS. For instance, if there is a security release for a version of a vendored module, then a new Node.js release with the updated version will be provided. Semver-major and some semver-minor releases to those vendored modules, however, will not be updated in LTS versions, following the spirit of our existing LTS contract. The Node.js release team will have final discretion on details relating to the handling of updates to vendored modules in LTS releases.
What can go into vendor:
?
Adding a new vendor: module is a big decision that can have significant ecosystem impact, it also can have the effect of greatly increasing the size of the node binary distribution. Great care and consideration must be taken in the decision to add a module. A defined set of criteria must be established. These can include the number of dependencies in the ecosystem, considerations about the quality of the code, desire of the maintainers of the code to have the module bundled, generalized utility of the module to the largest number of use cases, and utility of the module to Node.js' own use (e.g. we bundle undici because we directly depend on undici for fetch(), exposing it in vendor: would be optional but also fairly straightforward without introducing a significant additional burden).
Strong consideration should be given to limiting vendored modules to only those that have been contributed to either the Node.js project (the way, for instance, undici has been) or are covered under the OpenJS Foundation open governance model. Especially important is that vendored modules are not maintained by a single source or a single decision maker. In fact, as part of the discussion as to whether a module should be added to vendor:, it is reasonable to request that the module is contributed to Node.js or to the OpenJS Foundation as a first step, and that the project have at least 3+ core contributors with commit/write/publish access..
Why vendor:foo
and not @node/foo
or vendor/foo
or node:foo
?
There is already a vendor
module on npm. Using a specifier such as vendor/foo
would directly conflict with that existing module. Using any other /-prefix could potentially suffer from the same constraint. It's doable, yes, but using the :-prefix avoids this challenge.
Using node:foo
would run the risk of a vendored module being confused with an official Node.js built-in module.
Using something like @node
could be problematic if the module that is being vendored already has an @-prefix. For example, if we have a module like @abc/foo
, then @node/@abc/foo
is rather clumsy and awkward. vendor:@abc/foo
is clearer and less awkward (at least to me)
Vulnerabilities
The risk of shipping additional vulnerabilities in Node.js exists. A few mitigation must be put in place to avoid such risk:
- Processes to update those dependencies in LTS releases if/when a vulnerabilities in them is disclosed
- An API to query which version of said module is being shipped within Node.js (e.g.
process.versions.vendor
or equivalent) - Before vendoring, the frequency of vulnerabilities in a module should be considered and evaluated. For example, something that consistently ships vulnerability releases might not be a good fit for vendoring due to the increased workload on releasers and increased churn for end-users.
/cc @bcoe @darcyclarke @nodejs/tooling