# Package Distributions ## Team Meeting ### Ideas - registry-side dist/target? - Blocked by registry/product investment - [ ] **Action**: "conditonal everything except deps" - [ ] **Action**: explore "pre-fail optional deps" - ie. distributions implemented **not** as an override but rather link post/during reify - [ ] **Action**: @isaac will engage on Yarn's RFC --- ## Goals - [ ] Allow maintainers to define/distribute different packages under a singular name based on conditional logic for the consuming environment's platform/engines - [ ] Limit the amount of net-new concepts & scope (ie. try to use existing paradigms & building blocks as much as possible) - [ ] Make this feature opt-in (at least initially) ### Current Work-Around The current work-around is to define all of your package distributions/variants as `optionalDeps` & rely on the environment failing to install them - then testing for the dep that got properly reified. ```json= { "name": "foo", "optionalDependencies": { "win-foo": "...", "darwin-foo": "..." } } ``` ### Initial Proposal - Introduce a new field called `distributions` which will be utilized by `npm` to determine how/what `overrides` to create in a consuming project's `package.json`. ### Implementation - `distributions` be an `Array` of objects defining conditions in which to **override** the root/default package in the consuming project's `package.json` - Conditions should cascade, so maintaining order is important here (ie. this must be an `Array`) - `engines` & `platform` fields should respect the existing shape/understanding `npm` is already aware of - `override` will define a package spec to be used in the `overrides` definitions of the consuming project when the conditions for `engines` & `platform` are met ```json= { "name": "foo-native", "distributions": [ { "engines": { "node": "^10", }, "platform": "win32", "override": "foo-native-win32-10" }, { "override": "foo-native-default" } ], "engines": { "npm": "^7.12.0" } } ``` #### New flags/config - `--with-distributions` Defaults to `false` - when `true`, will respect `distributions` defined in a package's `package.json` - `--save-overrides` Defaults to `false` - when `true`, will write `overrides` into `package.json` - will also be useful for the [**Audit Overrides** RFC that has already been accepted](https://github.com/npm/rfcs/blob/latest/accepted/0037-audit-overrides.md) #### Example `install` ```bash= npm install foo-native --with-distributions --save-overrides ``` ```json= { "name": "myapp", "dependencies": { "foo": "^1" }, "overrides": { "foo@^1": { "foo-native@^1": "foo-native-darwin-10@^1" } } } ``` ### References & Links - Yarn's **Package Variants** Proposal: https://github.com/yarnpkg/berry/issues/2751 ### Parking Lot #### Best practice for maintainers to ensure they're consumers are using `distributions` properly A best practice we could recommend, to avoid confusion, is to set `engines` value for `npm` & educate maintainers/consumers on `--engines-strict` ```json= { ... "engines": { "npm": "^7.12.0" } ... } ``` ```bash= npm install --engines-strict ``` #### Best practice to support widest audience Utilize the default `optionalDependencies` environment fallback w/ an optional shim while also still defining `distributions`. ```json= { "name": "foo-native", "main": "lib/shim.js", "dependencies": { "foo-native-win32-10": "*", "foo-native-source": "*", "@npmcli/optional-shim": "^1", "debug": "^4" }, "optionalDependencies": { "foo-native-win32-10": "*", "foo-native-source": "*" }, "distributions": [ { "engines": { "node": "^10", }, "platform": "win32", "override": "foo-native-win32-10" }, { "override": "foo-native-source" } ], "engines": { "npm": "^7.12.0" } } ``` #### Concept: use `dependencyMeta` vs. `distributions` Zoltan (pnpm) & Mael (Yarn) have started talking more about `dependencyMeta` to store information about your dependencies; This seems to only serve to further nest information that can live in the top level of a `package.json` since this is "metadata" is directly associated with that package. `dependencyMeta` is better served at the consumer level where we already have `overrides` that can solve this problem. `depdencyMeta` may be useful in the future, but not necessarily for this feature. ## Alternative: "choose and link" This avoids the "different platforms result in different trees" problem. ```json= { "name": "foo", "version": "1.2.3", "main": "index.js", "scripts": { "preinstall": "node-gyp rebuild" }, "distributions": [ { "engines": { "node": "10" }, "platform": "win32", "package": "foo-native-win32-10@1.x" }, { "platform": "linux", "arch": "x64", "package": "foo-native-linux-x64@2.x" }, "..." ] } ``` ### buildIdealTree * Place _all_ distribution targets as peers of the `foo` package * Any that cannot be placed are not placed (eg, package not found, cannot be placed for conflicting peer deps, etc.), similar to `optionalDependencies` * Record status of `idealTree` placed distributions of the main "foo" package (?somehow?), along with their selection criteria. Thus the idealTree is not platform-specific. ### reify * Choose one distribution of the main package, and reify that. * Do not reify the others, or any of their dependencies (unless they are required to meet another dependency in the tree.) * Reify the main package as a `Link` to the chosen distribution. ### publisher * Publish the main package with the source, pointing to distribution package specifiers and selection criteria. * Publish pre-built distributions as needed. This is amenable to publishing distributions post-hoc as a CI build. If any fail to build and publish, no matter, they will simply fail to install, and fall back to the main package. Constraints: distributions cannot conflict with one another or other dependencies within the tree. (So, not possible to have one distribution with a peer on `react@15` and another on `react@16`, for example.) ### consumer * Add `foo` as a dependency. * Pre-built distributions will be added to lockfile. * `require('foo')` will return the appropriate context-specific distribution if one was found successfully, or the original package if it was not. The package-lock.json file will include _all_ the distributions (as it would for `optionalDependencies` today), but only one of these will be reified on disk. No chance of an exponential explosion in lockfile size, as we calculate every possible combination of distribution matrices. ### Polyfill Use Case While this is most useful for slow and costly binary builds, it is also interesting for providing polyfills for node features! ```json= { "name": "fs-readdir", "version": "fs.readdir() guaranteed to have withFileTypes:true support", "distributions": [ { "engines": { "node": "<v10.11.0"}, "package": "fs-readdir-polyfill@1" }, { "package": "fs-readdir-native@1" } ] } ``` ```json= { "name": "fs-readdir-polyfill", "version": "1.2.3", "description": "polyfill the fs.readdir withFileTypes using fs.stat" } ``` ```json= { "name": "fs-readdir-native", "version": "1.2.3", "description": "just export require('fs').readdir" } ``` Of course, we could just as easily have the `fs-readdir-polyfill` define `fs-readdir-native` as a distribution when `"engines": {"node": ">=10.11.0" }`.