├── .circleci └── config.yml ├── .gitignore ├── LICENSE ├── README.md ├── default.nix ├── flake.lock ├── flake.nix ├── napalm-registry ├── Main.hs ├── README.md ├── default.nix └── package.yaml ├── nix ├── default.nix ├── sources.json └── sources.nix ├── script └── test ├── scripts ├── lib.mjs └── lock-patcher.mjs ├── shell.nix ├── template ├── flake.nix └── hello-world │ ├── cli.js │ ├── package-lock.json │ └── package.json └── test ├── deps-alias ├── cli.js ├── package-lock.json └── package.json ├── hello-world-deps-v3 ├── cli.js ├── package-lock.json └── package.json ├── hello-world-deps ├── cli.js ├── package-lock.json └── package.json ├── hello-world-workspace-v3 ├── client │ ├── cli.js │ └── package.json ├── package-lock.json └── package.json └── hello-world ├── cli.js ├── package-lock.json └── package.json /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | jobs: 4 | build-nix: 5 | 6 | machine: 7 | image: ubuntu-2004:current 8 | enabled: true 9 | 10 | steps: 11 | 12 | - run: 13 | name: Prepare nix directories 14 | command: | 15 | sudo mkdir -p /nix 16 | sudo chown circleci /nix 17 | 18 | sudo mkdir -p /etc/nix 19 | 20 | # Enable sandbox 21 | echo "sandbox = true" | sudo tee -a /etc/nix/nix.conf 22 | 23 | # Set a new TMP because /run/user is (1) pretty small and (2) 24 | # mounted with noexec 25 | new_tmp=$HOME/tmp 26 | mkdir -p $new_tmp 27 | echo "export TMPDIR=$new_tmp" >> $BASH_ENV 28 | 29 | - run: 30 | name: Install Nix 31 | command: | 32 | until bash <(curl -L https://nixos.org/nix/install) 33 | do 34 | echo "Nix install failed, retrying" 35 | sudo rm -rf /nix 36 | sudo mkdir -p /nix 37 | sudo chown circleci /nix 38 | done 39 | echo '. /home/circleci/.nix-profile/etc/profile.d/nix.sh' >> $BASH_ENV 40 | 41 | - checkout 42 | 43 | - run: 44 | name: Nix build 45 | command: ./script/test 46 | 47 | workflows: 48 | version: 2 49 | build: 50 | jobs: 51 | - build-nix 52 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | result 2 | result-* 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2023 Nicolas Mattia 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Napalm 2 | 3 | **This project is looking for a new maintainer, [see](https://github.com/nix-community/napalm/issues/67)** 4 | 5 | > When faced with a JavaScript codebase, napalm is just what you need. 6 | > 7 | > -- anonymous 8 | 9 | ## Table of contents 10 | 11 | * [Building npm packages in Nix with Napalm](#building-npm-packages-in-nix-with-napalm) 12 | * [Basic Napalm usage](#basic-napalm-usage) 13 | * [Napalm with Nix flakes](#napalm-with-nix-flakes) 14 | * [More complicated scenarios with Napalm](#handling-complicated-scenarios-with-napalm) 15 | * [Custom node.js version](#custom-nodejs-version) 16 | * [Pre/Post Npm hooks](#prepost-npm-hooks) 17 | * [Multiple package locks](#multiple-package-locks) 18 | * [Patching Npm packages](#patching-npm-packages-before-fetching-them-with-npm) 19 | * [Customizing patching mechanism of npm packages](#customizing-patching-mechanism-of-npm-packages) 20 | * [How does it work ?](#how-does-napalm-work-) 21 | * [Napalm - a lightweight npm registry](#napalm---a-lightweight-npm-registry) 22 | 23 | ## Building npm packages in Nix with Napalm 24 | 25 | ### Basic Napalm usage 26 | 27 | Use the `buildPackage` function provided in the [`default.nix`](./default.nix) 28 | for building npm packages (replace `` with the path to napalm; 29 | with [niv]: `niv add nmattia/napalm`): 30 | 31 | ``` nix 32 | let 33 | napalm = pkgs.callPackage {}; 34 | in napalm.buildPackage ./. {} 35 | ``` 36 | 37 | All executables provided by the npm package will be available in the 38 | derivation's `bin` directory. 39 | 40 | **NOTE**: napalm uses the package's `package-lock.json` (or 41 | `npm-shrinkwrap.json`) for building a package database. Make sure there is 42 | either a `package-lock.json` or `npm-shrinkwrap.json` in the source. 43 | Alternatively provide the path to the package-lock file: 44 | 45 | ``` nix 46 | let 47 | napalm = pkgs.callPackage {}; 48 | in napalm.buildPackage ./. { packageLock = ; } 49 | ``` 50 | 51 | ### Napalm with Nix flakes 52 | 53 | If you want to use Napalm in your flake project, you can do that by adding it to your inputs and either passing `napalm.overlays.default` to your Nixpkgs instance, or by using the `napalm.legacyPackages` `buildPackage` output. To configure the latter's environment, be sure to look at [the complicated scenarios](#handling-complicated-scenarios-with-napalm) and potentially set the `nixpkgs` input of napalm with `follows`. 54 | 55 | #### Example `flake.nix` 56 | 57 | ```nix 58 | { 59 | inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; 60 | inputs.napalm.url = "github:nix-community/napalm"; 61 | 62 | # NOTE: This is optional, but is how to configure napalm's env 63 | inputs.napalm.inputs.nixpkgs.follows = "nixpkgs"; 64 | 65 | outputs = { self, nixpkgs, napalm }: 66 | let 67 | system = "x86_64-linux"; 68 | pkgs = nixpkgs.legacyPackages."${system}"; 69 | in { 70 | # Assuming the flake is in the same directory as package-lock.json 71 | packages."${system}".package-name = napalm.legacyPackages."${system}".buildPackage ./. { }; 72 | 73 | devShells."${system}".shell-name = pkgs.mkShell { 74 | nativeBuildInputs = with pkgs; [ nodejs ]; 75 | }; 76 | }; 77 | } 78 | ``` 79 | 80 | #### Flake Template 81 | 82 | There is also a template that can help you use napalm in your project. You can use it in a new, empty directory by running: 83 | 84 | ```shell 85 | nix flake init -t "github:nix-community/napalm" 86 | ``` 87 | 88 | ## Handling complicated scenarios with Napalm 89 | 90 | Examples below assume that you have imported `napalm` in some way. 91 | 92 | ### Custom node.js version 93 | 94 | Napalm makes it quite simple to use custom node.js (with npm) version. 95 | This is controlled via `nodejs` argument. 96 | 97 | #### Example 1 98 | 99 | Changing node.js version to the one that is supplied in `nixpkgs`: 100 | 101 | ```nix 102 | { napalm, nodejs-16_x, ... }: 103 | napalm.buildPackage ./. { 104 | nodejs = nodejs-16_x; 105 | } 106 | ``` 107 | 108 | #### Example 2 109 | 110 | Changing node.js version to some custom version (just an idea): 111 | 112 | ```nix 113 | { napalm, nodejs-12_x, ... }: 114 | let 115 | nodejs = nodejs-12_x.overrideAttrs (old: rec { 116 | pname = "nodejs"; 117 | version = "12.19.0"; 118 | sha256 = "1qainpkakkl3xip9xz2wbs74g95gvc6125cc05z6vyckqi2iqrrv"; 119 | name = "${pname}-${version}"; 120 | 121 | src = builtins.fetchurl { 122 | url = 123 | "https://nodejs.org/dist/v${version}/node-v${version}.tar.xz"; 124 | inherit sha256; 125 | }; 126 | }); 127 | in 128 | napalm.buildPackage ./. { 129 | inherit nodejs; 130 | } 131 | ``` 132 | 133 | ### Pre/Post Npm hooks 134 | 135 | Napalm allows to specify commands that are run before and after every `npm` call. 136 | These hooks work also for nested `npm` calls thanks to npm override mechanism. 137 | 138 | #### Example 139 | 140 | Patching some folder with executable scripts containing shebangs (that may be generated by npm script): 141 | 142 | ```nix 143 | { napalm, ... }: 144 | napalm.buildPackage ./. { 145 | postNpmHook = '' 146 | patchShebangs tools 147 | ''; 148 | } 149 | ``` 150 | 151 | ### Multiple package locks 152 | 153 | Napalms allows to specify multiple package locks. 154 | This may be useful for some project which consist of some smaller projects. 155 | 156 | #### Example 157 | 158 | ```nix 159 | { napalm, ... }: 160 | napalm.buildPackage ./. { 161 | # package-lock.json that is in the root of the project 162 | # is not required to be specified in `additionalpackagelocks` 163 | # If you want to specify it, you can use `packageLock` argument. 164 | additionalPackageLocks = [ 165 | ./frontend/package-lock.json 166 | ./tests/package-lock.json 167 | ]; 168 | } 169 | ``` 170 | 171 | ### Patching npm packages (before fetching them with npm) 172 | 173 | *This is very useful for errors like: `Invalid interpreter`* 174 | 175 | Napalm has an ability to patch fetched npm packages before serving them to the npm. 176 | By default patching fixes shebangs and binaries that are localized and the tarballs. 177 | Napalm also updates `package-lock.json` with new `integrity` hashes. 178 | 179 | #### Example 180 | 181 | To enable patching, just use: 182 | 183 | ```nix 184 | { napalm, ... }: 185 | napalm.buildPackage ./. { 186 | patchPackages = true; 187 | } 188 | ``` 189 | 190 | This will force repacking of all dependencies, though, so you might want to patch only specific dependencies by passing an empty attribute set to the next method. 191 | 192 | ### Customizing patching mechanism of npm packages 193 | 194 | Sometimes it is required to manually patch some package. 195 | Napalm allows that via `customPatchPackages` attribute. 196 | This attribute is a set of that overrides for packages that will be patched. 197 | 198 | #### Example 199 | 200 | ```nix 201 | { napalm, ... }: 202 | napalm.buildPackage ./. { 203 | # Arguments that are passed to the overrider: 204 | # `pkgs` - Nixpkgs used by Napalm 205 | # `prev` - Current set that will be passed to mkDerivation 206 | customPatchPackages = { 207 | "react-native" = { 208 | "0.65.0" = pkgs: prev: { 209 | EXAMPLE_ENV_VAR = "XYZ"; 210 | dontBuild = false; 211 | buildPhase = '' 212 | # You can copy some stuff here or run some custom stuff 213 | ''; 214 | }; 215 | }; 216 | 217 | # Version is not required. When it is not specified it 218 | # applies override to all packages with that name. 219 | "node-gyp-builder" = pkgs: prev: { }; 220 | }; 221 | } 222 | ``` 223 | 224 | ## How does Napalm work ? 225 | 226 | These are general steps that Napalm makes when building packages (if you want to learn more, see source code of `default.nix`): 227 | 228 | 1. Napalm loads all `package-lock.json` files and parses them. Then it fetches all specified packages into the Nix Store. 229 | 2. (optional) Napalm patches npm packages and stores their output in new location. Then uses this location as default package location in Nix Store. 230 | 3. Napalm creates snapshot that consists of packages names, version and paths to locations in Nix Store that contain them. 231 | 4. (optional) Napalm patches `package-lock.json` integrity if the packages were patched, so that they will work with `npm install`. 232 | 5. Napalm sets up `napalm-registry` which as a main argument accepts snapshot of npm packages and them serves them as if it was npm registry server. 233 | 6. Napalm sets up npm so that it thinks `napalm-registry` server is default npm registry server. 234 | 7. Napalm overrides npm which allows using custom npm hooks (every time it is called) as well as some other default patching activities. 235 | 8. Napalm calls all the npm commands. 236 | 9. Napalm installs everything automatically or based on what was specified in `installPhase`. 237 | 238 | ## Napalm - a lightweight npm registry 239 | 240 | Under the hood napalm uses its own package registry. The registry is available 241 | in [default.nix](./default.nix) as `napalm-registry`. 242 | 243 | ``` 244 | Usage: napalm-registry [-v|--verbose] [--endpoint ARG] [--port ARG] --snapshot ARG 245 | 246 | Available options: 247 | -v,--verbose Print information about requests 248 | --endpoint ARG The endpoint of this server, used in the Tarball URL 249 | --port ARG The to serve on, also used in the Tarball URL 250 | --snapshot ARG Path to the snapshot file. The snapshot is a JSON 251 | file. The top-level keys are the package names. The 252 | top-level values are objects mapping from version to 253 | the path of the package tarball. Example: { "lodash": 254 | { "1.0.0": "/path/to/lodash-1.0.0.tgz" } } 255 | -h,--help Show this help text 256 | ``` 257 | 258 | [niv]: https://github.com/nmattia/niv 259 | 260 | 261 | ## Similar projects 262 | 263 | - [npmlock2nix](https://github.com/tweag/npmlock2nix) 264 | -------------------------------------------------------------------------------- /default.nix: -------------------------------------------------------------------------------- 1 | # The napalm nix support for building npm package. 2 | # See 'buildPackage'. 3 | # This file describes the build logic for buildPackage, as well as the build 4 | # description of the napalm-registry. Some tests are also present at the end of 5 | # the file. 6 | 7 | { pkgs ? import ./nix { }, lib ? pkgs.lib }: 8 | let 9 | fallbackPackageName = "build-npm-package"; 10 | fallbackPackageVersion = "0.0.0"; 11 | 12 | hasFile = dir: filename: 13 | if lib.versionAtLeast builtins.nixVersion "2.3" then 14 | builtins.pathExists (dir + "/${filename}") 15 | else 16 | builtins.hasAttr filename (builtins.readDir dir); 17 | 18 | # Helper functions 19 | ifNotNull = a: b: if a != null then a else b; 20 | ifNotEmpty = a: b: if a != [ ] then a else b; 21 | 22 | concatSnapshots = snapshots: 23 | let 24 | allPkgsNames = 25 | lib.foldl (acc: set: acc ++ (builtins.attrNames set)) [ ] 26 | snapshots; 27 | loadPkgVersions = name: 28 | let 29 | allVersions = 30 | lib.foldl (acc: set: acc // set.${name} or { }) { } snapshots; 31 | in 32 | { 33 | inherit name; 34 | value = allVersions; 35 | }; 36 | in 37 | builtins.listToAttrs (builtins.map loadPkgVersions allPkgsNames); 38 | 39 | # Patches shebangs and elfs in npm package and returns derivation 40 | # which contains package.tgz that is compressed patched package 41 | # 42 | # `customAttrs` argument allows user to override any field that is passed 43 | # into the mkDerivation. It is a function that evaluates to set and overrides 44 | # current mkDerivation arguments. 45 | mkNpmTar = { pname, version, src, buildInputs, customAttrs ? null }: 46 | let 47 | prev = { 48 | pname = "${pname}-patched"; 49 | inherit version src buildInputs; 50 | 51 | dontPatch = true; 52 | dontBuild = true; 53 | 54 | configurePhase = '' 55 | runHook preConfigure 56 | 57 | # Ensures that fixup phase will use these in the path 58 | export PATH=${lib.makeBinPath buildInputs}:$PATH 59 | 60 | runHook postConfigure 61 | ''; 62 | 63 | installPhase = '' 64 | runHook preInstall 65 | 66 | mkdir -p $out/package 67 | cp -rf ./* $out/package 68 | 69 | runHook postInstall 70 | ''; 71 | 72 | preFixup = '' 73 | echo Ensuring that proper files are executable ... 74 | 75 | # Split by newline instead of spaces in case 76 | # some filename contains space 77 | OLD_IFS=$IFS 78 | IFS=$'\n' 79 | 80 | 81 | # This loop looks for files which may contain shebang 82 | # and makes them executable if it is the case. 83 | # This is useful, because patchShbang function patches 84 | # only files that are executable. 85 | # 86 | # See: https://github.com/NixOS/nixpkgs/blob/ba3768aec02b16561ceca1caebdbeb91ae16963d/pkgs/build-support/setup-hooks/patch-shebangs.sh 87 | 88 | for file in $(find $out -type f \( -name "*.js" -or -name "*.sh" \)); do 89 | grep -i '^#! */' "$file" && \ 90 | sed -i 's|^#! */|#!/|' "$file" && \ 91 | chmod +0100 "$file" 92 | done 93 | 94 | IFS=$OLD_IFS 95 | ''; 96 | 97 | postFixup = '' 98 | cd $out 99 | 100 | # Package everything up 101 | echo Packaging ${pname} ... 102 | tar -czf package.tgz package 103 | 104 | # Remove untared package 105 | echo Cleanup of ${pname} 106 | rm -rf ./package 107 | ''; 108 | }; 109 | in 110 | pkgs.stdenv.mkDerivation 111 | (prev // (if customAttrs == null then { } else (customAttrs pkgs prev))); 112 | 113 | # Reads a package-lock.json and assembles a snapshot with all the packages of 114 | # which the URL and sha are known. The resulting snapshot looks like the 115 | # following: 116 | # { "my-package": 117 | # { "1.0.0": { url = "https://npmjs.org/some-tarball", shaX = ...}; 118 | # "1.2.0": { url = "https://npmjs.org/some-tarball2", shaX = ...}; 119 | # }; 120 | # "other-package": { ... }; 121 | # } 122 | snapshotFromPackageLockJson = 123 | { packageLockJson 124 | , pname ? null 125 | , version ? null 126 | , buildInputs ? [ ] 127 | , patchPackages ? false 128 | , customPatchPackages ? { } 129 | }: 130 | let 131 | packageLock = builtins.fromJSON (builtins.readFile packageLockJson); 132 | 133 | lockfileVersion = packageLock.lockfileVersion or 1; 134 | 135 | # Load custom name and version of the program in case it was specified and 136 | # not specified by the package-lock.json 137 | topPackageName = 138 | packageLock.name or (ifNotNull pname fallbackPackageName); 139 | 140 | updateTopPackageVersion = obj: { 141 | version = ifNotNull version fallbackPackageVersion; 142 | } // obj; 143 | 144 | # Version can be a pointer like “npm:vue-loader@15.10.0”. 145 | # In that case we need to replace the name and version with the target one. 146 | parsePointer = { name, version }: let 147 | isPointer = lib.hasPrefix "npm:" version; 148 | fragments = lib.splitString "@" (lib.removePrefix "npm:" version); 149 | name' = if isPointer then builtins.concatStringsSep "@" (lib.init fragments) else name; 150 | version' = if isPointer then lib.last fragments else version; 151 | in 152 | { name = name'; version = version'; }; 153 | 154 | parsePackageNameVersion = name': originalObj: parsePointer { 155 | name = if builtins.hasAttr "name" originalObj then originalObj.name else name'; 156 | version = originalObj.version; 157 | }; 158 | 159 | # XXX: Creates a "node" for genericClosure. We include whether or not 160 | # the packages contains an integrity, and if so the integrity as well, 161 | # in the key. The reason is that the same package and version pair can 162 | # be found several time in a package-lock.json. 163 | mkNode = 164 | originalName: 165 | originalObj: 166 | let 167 | inherit (parsePackageNameVersion originalName originalObj) name version; 168 | obj = originalObj // { 169 | inherit name version; 170 | }; 171 | in 172 | { 173 | inherit name obj version; 174 | key = "${name}-${obj.version}-${obj.integrity or "no-integrity"}"; 175 | next = lib.mapAttrsToList mkNode (obj.dependencies or { }); 176 | }; 177 | 178 | # The list of all packages discovered in the package-lock, excluding 179 | # the top-level package. 180 | flattened = if lockfileVersion < 3 181 | then builtins.genericClosure { 182 | startSet = [ (mkNode topPackageName (updateTopPackageVersion packageLock)) ]; 183 | operator = x: x.next; 184 | } 185 | else let 186 | # Parse a path like "node_modules/underscore" into a package name, like "underscore". 187 | # Also has to support scoped package paths, like "node_modules/@babel/helper-string-parser" and 188 | # nested packages, like "node_modules/@babel/helper-string-parser/node_modules/underscore". 189 | pathToName = name: lib.pipe name [ 190 | (builtins.split "(@[^/]+/)?([^/]+)$") 191 | (builtins.filter (x: builtins.isList x)) 192 | lib.flatten 193 | (builtins.filter (x: x != null)) 194 | lib.concatStrings 195 | ]; 196 | in lib.pipe (packageLock.packages or {}) [ 197 | # filter out the top-level package, which has an empty name 198 | (lib.filterAttrs (name: _: name != "")) 199 | # Filter out linked packages – they lack other attributes and the link target will be present separately. 200 | (lib.filterAttrs (_name: originalObj: !(originalObj.link or false))) 201 | (lib.mapAttrsToList (originalName: originalObj: let 202 | inherit (parsePackageNameVersion (pathToName originalName) originalObj) name version; 203 | obj = originalObj // { 204 | inherit name version; 205 | }; 206 | in { 207 | inherit name obj version; 208 | key = "${name}-${obj.version}-${obj.integrity or "no-integrity"}"; 209 | })) 210 | ]; 211 | 212 | # Create an entry for the snapshot, e.g. 213 | # { some-package = { some-version = { url = ...; shaX = ...} ; }; } 214 | snapshotEntry = x: 215 | let 216 | sha = 217 | if lib.hasPrefix "sha1-" x.obj.integrity then { 218 | sha1 = lib.removePrefix "sha1-" x.obj.integrity; 219 | } else if lib.hasPrefix "sha512-" x.obj.integrity then { 220 | sha512 = lib.removePrefix "sha512-" x.obj.integrity; 221 | } else 222 | abort "Unknown sha for ${x.obj.integrity}"; 223 | in 224 | if builtins.hasAttr "resolved" x.obj then { 225 | ${x.name}.${x.version} = 226 | let 227 | customAttrs = 228 | let 229 | customAttrsOverrider = 230 | customPatchPackages.${x.name}.${x.version} 231 | or (customPatchPackages.${x.name} or null); 232 | in 233 | if builtins.isFunction customAttrsOverrider 234 | then customAttrsOverrider 235 | else null; 236 | src = pkgs.fetchurl ({ url = x.obj.resolved; } // sha); 237 | out = mkNpmTar { 238 | inherit src buildInputs; 239 | pname = lib.strings.sanitizeDerivationName x.name; 240 | version = x.version; 241 | inherit customAttrs; 242 | }; 243 | in 244 | if patchPackages || customAttrs != null then "${out}/package.tgz" else src; 245 | } else { }; 246 | 247 | mergeSnapshotEntries = acc: x: 248 | lib.recursiveUpdate acc (snapshotEntry x); 249 | in 250 | lib.foldl mergeSnapshotEntries { } flattened; 251 | 252 | # Returns either the package-lock or the npm-shrinkwrap. If none is found 253 | # returns null. 254 | findPackageLock = root: 255 | if hasFile root "package-lock.json" then 256 | root + "/package-lock.json" 257 | else if hasFile root "npm-shrinkwrap.json" then 258 | root + "/npm-shrinkwrap.json" 259 | else 260 | null; 261 | 262 | # Returns the package.json as nix values. If not found, returns an empty 263 | # attrset. 264 | readPackageJSON = root: 265 | if hasFile root "package.json" then 266 | lib.importJSON (root + "/package.json") 267 | else 268 | builtins.trace "WARN: package.json not found in ${toString root}" { }; 269 | 270 | # Builds an npm package, placing all the executables the 'bin' directory. 271 | # All attributes are passed to 'runCommand'. 272 | # 273 | # TODO: document environment variables that are set by each phase 274 | buildPackage = src: 275 | attrs@{ name ? null 276 | , pname ? null 277 | , version ? null 278 | # Used by `napalm` to read the `package-lock.json`, `npm-shrinkwrap.json` 279 | # and `npm-shrinkwrap.json` files. May be different from `src`. When `root` 280 | # is not set, it defaults to `src`. 281 | , root ? src 282 | , nodejs ? pkgs.nodejs # Node js and npm version to be used, like pkgs.nodejs-16_x 283 | , packageLock ? null 284 | , additionalPackageLocks ? [ ] # Sometimes node.js may have multiple package locks. 285 | # automatic package-lock.json discovery in the root of the project 286 | # will be used even if this array is specified 287 | , npmCommands ? "npm install --loglevel verbose --nodedir=${nodejs}/include/node" # These are the commands that are supposed to use npm to install the package. 288 | # --nodedir argument helps with building node-gyp based packages. 289 | , buildInputs ? [ ] 290 | , installPhase ? null 291 | # Patches shebangs and ELFs in all npm dependencies, may result in slowing down building process 292 | # if you are having `missing interpreter: /usr/bin/env` issue you should enable this option 293 | , patchPackages ? false 294 | # This argument is a set that has structure like: { "" = ; ... } or 295 | # { ""."" = ; ... }, where is a function that takes two arguments: 296 | # `pkgs` (nixpkgs) and `prev` (default derivation arguments of the package) and returns new arguments that will override 297 | # current mkDerivation arguments. This works similarly to the overrideAttrs method. See README.md 298 | , customPatchPackages ? { } 299 | , preNpmHook ? "" # Bash script to be called before npm call 300 | , postNpmHook ? "" # Bash script to be called after npm call 301 | , ... 302 | }: 303 | assert name != null -> (pname == null && version == null); 304 | let 305 | # Remove all the attributes that are not part of the normal 306 | # stdenv.mkDerivation interface 307 | mkDerivationAttrs = builtins.removeAttrs attrs [ 308 | "packageLock" 309 | "npmCommands" 310 | "nodejs" 311 | "packageLock" 312 | "additionalPackageLocks" 313 | "patchPackages" 314 | "customPatchPackages" 315 | "preNpmHook" 316 | "postNpmHook" 317 | ]; 318 | 319 | # New `npmCommands` should be just multiline string, but 320 | # for backwards compatibility there is a list option 321 | parsedNpmCommands = 322 | let 323 | type = builtins.typeOf attrs.npmCommands; 324 | in 325 | if attrs ? npmCommands then 326 | ( 327 | if type == "list" then 328 | builtins.concatStringsSep "\n" attrs.npmCommands 329 | else 330 | attrs.npmCommands 331 | ) else 332 | npmCommands; 333 | 334 | actualPackageLocks = 335 | let 336 | actualPackageLocks' = additionalPackageLocks ++ [ (ifNotNull packageLock discoveredPackageLock) ]; 337 | in 338 | ifNotEmpty actualPackageLocks' (abort '' 339 | Could not find a suitable package-lock in ${src}. 340 | If you specify a 'packageLock' or 'packageLocks' to 'buildPackage', I will use that. 341 | Otherwise, if there is a file 'package-lock.json' in ${src}, I will use that. 342 | Otherwise, if there is a file 'npm-shrinkwrap.json' in ${src}, I will use that. 343 | Otherwise, you will see this error message. 344 | ''); 345 | 346 | discoveredPackageLock = findPackageLock root; 347 | 348 | snapshot = pkgs.writeText "npm-snapshot" ( 349 | builtins.toJSON ( 350 | concatSnapshots 351 | ( 352 | builtins.map 353 | (lock: snapshotFromPackageLockJson { 354 | inherit patchPackages pname version customPatchPackages; 355 | packageLockJson = lock; 356 | buildInputs = newBuildInputs; 357 | }) 358 | actualPackageLocks 359 | ) 360 | ) 361 | ); 362 | 363 | newBuildInputs = buildInputs ++ [ 364 | haskellPackages.napalm-registry 365 | pkgs.fswatch 366 | pkgs.jq 367 | pkgs.netcat-gnu 368 | nodejs 369 | ]; 370 | 371 | reformatPackageName = pname: 372 | let 373 | # regex adapted from `validate-npm-package-name` 374 | # will produce 3 parts e.g. 375 | # "@someorg/somepackage" -> [ "@someorg/" "someorg" "somepackage" ] 376 | # "somepackage" -> [ null null "somepackage" ] 377 | parts = builtins.tail (builtins.match "^(@([^/]+)/)?([^/]+)$" pname); 378 | # if there is no organisation we need to filter out null values. 379 | non-null = builtins.filter (x: x != null) parts; 380 | in 381 | builtins.concatStringsSep "-" non-null; 382 | 383 | packageJSON = readPackageJSON root; 384 | resolvedPname = attrs.pname or (packageJSON.name or fallbackPackageName); 385 | resolvedVersion = attrs.version or (packageJSON.version or fallbackPackageVersion); 386 | 387 | # If name is not specified, read the package.json to load the 388 | # package name and version from the source package.json 389 | name = attrs.name or "${reformatPackageName resolvedPname}-${resolvedVersion}"; 390 | 391 | # Script that will be executed instead of npm. 392 | # This approach allows adding custom behavior between 393 | # every npm call, even if it is nested. 394 | npmOverrideScript = pkgs.writeShellScriptBin "npm" '' 395 | echo "npm overridden successfully." 396 | 397 | echo "Loading stdenv setup ..." 398 | source "${pkgs.stdenv}/setup" 399 | 400 | set -e 401 | 402 | echo "Running preNpmHook" 403 | ${preNpmHook} 404 | 405 | echo "Running npm $@" 406 | 407 | ${nodejs}/bin/npm "$@" 408 | 409 | echo "Running postNpmHook" 410 | ${postNpmHook} 411 | 412 | echo "Overzealously patching shebangs" 413 | if [[ -d node_modules ]]; then find node_modules -type d -name bin | \ 414 | while read file; do patchShebangs "$file"; done; fi 415 | ''; 416 | in 417 | pkgs.stdenv.mkDerivation ( 418 | mkDerivationAttrs // { 419 | inherit name src; 420 | buildInputs = newBuildInputs; 421 | 422 | configurePhase = attrs.configurePhase or '' 423 | runHook preConfigure 424 | 425 | export HOME=$(mktemp -d) 426 | 427 | runHook postConfigure 428 | ''; 429 | 430 | buildPhase = attrs.buildPhase or '' 431 | runHook preBuild 432 | 433 | # TODO: why does the unpacker not set the sourceRoot? 434 | sourceRoot=$PWD 435 | 436 | ${lib.optionalString (patchPackages || customPatchPackages != { }) '' 437 | echo "Patching npm packages integrity" 438 | ${nodejs}/bin/node ${./scripts}/lock-patcher.mjs ${snapshot} 439 | ''} 440 | 441 | echo "Starting napalm registry" 442 | 443 | napalm_REPORT_PORT_TO=$(mktemp -d)/port 444 | 445 | napalm-registry --snapshot ${snapshot} --report-to "$napalm_REPORT_PORT_TO" & 446 | napalm_REGISTRY_PID=$! 447 | 448 | while [ ! -f "$napalm_REPORT_PORT_TO" ]; do 449 | echo waiting for registry to report port to "$napalm_REPORT_PORT_TO" 450 | sleep 1 451 | done 452 | 453 | napalm_PORT="$(cat "$napalm_REPORT_PORT_TO")" 454 | rm "$napalm_REPORT_PORT_TO" 455 | rmdir "$(dirname "$napalm_REPORT_PORT_TO")" 456 | 457 | echo "Configuring npm to use port $napalm_PORT" 458 | 459 | ${nodejs}/bin/npm config set registry "http://localhost:$napalm_PORT" 460 | 461 | export CPATH="${nodejs}/include/node:$CPATH" 462 | 463 | # Makes custom npm script appear before real npm program 464 | export PATH="${npmOverrideScript}/bin:$PATH" 465 | 466 | echo "Installing npm package" 467 | 468 | ${parsedNpmCommands} 469 | 470 | echo "Shutting down napalm registry" 471 | kill $napalm_REGISTRY_PID 472 | 473 | runHook postBuild 474 | ''; 475 | 476 | installPhase = attrs.installPhase or '' 477 | runHook preInstall 478 | 479 | napalm_INSTALL_DIR=''${napalm_INSTALL_DIR:-$out/_napalm-install} 480 | mkdir -p $napalm_INSTALL_DIR 481 | cp -r $sourceRoot/* $napalm_INSTALL_DIR 482 | 483 | echo "Patching package executables" 484 | package_bins=$(jq -cM '.bin' <"$napalm_INSTALL_DIR/package.json") 485 | echo "bins: $package_bins" 486 | package_bins_type=$(jq -cMr type <<<"$package_bins") 487 | echo "bin type: $package_bins_type" 488 | 489 | case "$package_bins_type" in 490 | object) 491 | mkdir -p $out/bin 492 | 493 | echo "Creating package executable symlinks in bin" 494 | while IFS= read -r key; do 495 | bin=$(jq -cMr --arg key "$key" '.[$key]' <<<"$package_bins") 496 | echo "patching and symlinking binary $key -> $bin" 497 | # https://github.com/NixOS/nixpkgs/pull/60215 498 | chmod +w $(dirname "$napalm_INSTALL_DIR/$bin") 499 | chmod +x $napalm_INSTALL_DIR/$bin 500 | patchShebangs $napalm_INSTALL_DIR/$bin 501 | ln -s $napalm_INSTALL_DIR/$bin $out/bin/$key 502 | done < <(jq -cMr 'keys[]' <<<"$package_bins") 503 | ;; 504 | string) 505 | mkdir -p $out/bin 506 | bin=$(jq -cMr <<<"$package_bins") 507 | chmod +w $(dirname "$napalm_INSTALL_DIR/$bin") 508 | chmod +x $napalm_INSTALL_DIR/$bin 509 | patchShebangs $napalm_INSTALL_DIR/$bin 510 | 511 | ln -s "$napalm_INSTALL_DIR/$bin" "$out/bin/$(basename $bin)" 512 | ;; 513 | null) 514 | echo "No binaries to package" 515 | ;; 516 | *) 517 | echo "unknown type for binaries: $package_bins_type" 518 | echo "please submit an issue: https://github.com/nmattia/napalm/issues/new" 519 | exit 1 520 | ;; 521 | esac 522 | 523 | runHook postInstall 524 | ''; 525 | } 526 | ); 527 | 528 | napalm-registry-source = lib.cleanSource ./napalm-registry; 529 | 530 | haskellPackages = pkgs.haskellPackages.override { 531 | overrides = _: haskellPackages: { 532 | napalm-registry = haskellPackages.callPackage napalm-registry-source { }; 533 | }; 534 | }; 535 | 536 | napalm-registry-devshell = haskellPackages.shellFor { 537 | packages = (ps: [ ps.napalm-registry ]); 538 | shellHook = '' 539 | repl() { 540 | ghci -Wall napalm-registry/Main.hs 541 | } 542 | 543 | echo "To start a REPL session, run:" 544 | echo " > repl" 545 | ''; 546 | }; 547 | in 548 | { 549 | inherit buildPackage napalm-registry-devshell snapshotFromPackageLockJson; 550 | 551 | napalm-registry = haskellPackages.napalm-registry; 552 | 553 | hello-world = pkgs.runCommand "hello-world-test" { } '' 554 | ${buildPackage ./test/hello-world {}}/bin/say-hello 555 | touch $out 556 | ''; 557 | 558 | hello-world-deps = pkgs.runCommand "hello-world-deps-test" { } '' 559 | ${buildPackage ./test/hello-world-deps {}}/bin/say-hello 560 | touch $out 561 | ''; 562 | 563 | hello-world-deps-v3 = pkgs.runCommand "hello-world-deps-v3-test" { } '' 564 | ${buildPackage ./test/hello-world-deps-v3 {}}/bin/say-hello 565 | touch $out 566 | ''; 567 | 568 | hello-world-workspace-v3 = pkgs.runCommand "hello-world-workspace-v3-test" { } '' 569 | ${buildPackage ./test/hello-world-workspace-v3 {}}/_napalm-install/node_modules/.bin/say-hello 570 | touch $out 571 | ''; 572 | 573 | # See https://github.com/nix-community/napalm/pull/58#issuecomment-1701202914 574 | deps-alias = pkgs.runCommand "deps-alias" { } '' 575 | ${buildPackage ./test/deps-alias {}}/bin/say-hello 576 | touch $out 577 | ''; 578 | 579 | netlify-cli = 580 | let 581 | sources = import ./nix/sources.nix; 582 | in 583 | pkgs.runCommand "netlify-cli-test" { } '' 584 | export HOME=$(mktemp -d) 585 | ${buildPackage sources.cli {}}/bin/netlify --help 586 | touch $out 587 | ''; 588 | 589 | deckdeckgo-starter = 590 | let 591 | sources = import ./nix/sources.nix; 592 | in 593 | buildPackage sources.deckdeckgo-starter { 594 | name = "deckdeckgo-starter"; 595 | npmCommands = [ "npm install" "npm run build" ]; 596 | installPhase = '' 597 | mv dist $out 598 | ''; 599 | doInstallCheck = true; 600 | installCheckPhase = '' 601 | if [[ ! -f $out/index.html ]] 602 | then 603 | echo "Dist wasn't generated" 604 | exit 1 605 | else 606 | echo "All good!" 607 | fi 608 | ''; 609 | }; 610 | 611 | bitwarden-cli = 612 | let 613 | sources = import ./nix/sources.nix; 614 | 615 | bw = buildPackage sources.bitwarden-cli { 616 | npmCommands = [ "npm install --ignore-scripts" "npm run build" ]; 617 | 618 | # XXX: niv doesn't support submodules :'( 619 | # we work around that by skipping "npm run sub:init" and installing 620 | # the submodule manually 621 | postUnpack = '' 622 | rmdir $sourceRoot/jslib 623 | cp -r ${sources.bitwarden-jslib} $sourceRoot/jslib 624 | ''; 625 | }; 626 | in 627 | pkgs.runCommand "bitwarden-cli" { buildInputs = [ bw ]; } '' 628 | export HOME=$(mktemp -d) 629 | bw --help 630 | touch $out 631 | ''; 632 | } 633 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-utils": { 4 | "inputs": { 5 | "systems": "systems" 6 | }, 7 | "locked": { 8 | "lastModified": 1701680307, 9 | "narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=", 10 | "owner": "numtide", 11 | "repo": "flake-utils", 12 | "rev": "4022d587cbbfd70fe950c1e2083a02621806a725", 13 | "type": "github" 14 | }, 15 | "original": { 16 | "owner": "numtide", 17 | "repo": "flake-utils", 18 | "type": "github" 19 | } 20 | }, 21 | "nixpkgs": { 22 | "locked": { 23 | "lastModified": 1702312524, 24 | "narHash": "sha256-gkZJRDBUCpTPBvQk25G0B7vfbpEYM5s5OZqghkjZsnE=", 25 | "owner": "NixOS", 26 | "repo": "nixpkgs", 27 | "rev": "a9bf124c46ef298113270b1f84a164865987a91c", 28 | "type": "github" 29 | }, 30 | "original": { 31 | "owner": "NixOS", 32 | "ref": "nixos-unstable", 33 | "repo": "nixpkgs", 34 | "type": "github" 35 | } 36 | }, 37 | "root": { 38 | "inputs": { 39 | "flake-utils": "flake-utils", 40 | "nixpkgs": "nixpkgs" 41 | } 42 | }, 43 | "systems": { 44 | "locked": { 45 | "lastModified": 1681028828, 46 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 47 | "owner": "nix-systems", 48 | "repo": "default", 49 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 50 | "type": "github" 51 | }, 52 | "original": { 53 | "owner": "nix-systems", 54 | "repo": "default", 55 | "type": "github" 56 | } 57 | } 58 | }, 59 | "root": "root", 60 | "version": 7 61 | } 62 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "Build NPM packages in Nix and lightweight NPM registry"; 3 | inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; 4 | inputs.flake-utils.url = "github:numtide/flake-utils"; 5 | 6 | outputs = { self, nixpkgs, flake-utils }: 7 | (flake-utils.lib.eachDefaultSystem 8 | (system: 9 | let 10 | napalm = import ./. { 11 | pkgs = nixpkgs.legacyPackages."${system}"; 12 | }; 13 | in 14 | { 15 | legacyPackages = { 16 | inherit (napalm) 17 | buildPackage 18 | snapshotFromPackageLockJson 19 | ; 20 | }; 21 | 22 | packages = { 23 | inherit (napalm) 24 | hello-world 25 | hello-world-deps 26 | hello-world-deps-v3 27 | hello-world-workspace-v3 28 | deps-alias 29 | netlify-cli 30 | deckdeckgo-starter 31 | bitwarden-cli 32 | napalm-registry 33 | ; 34 | }; 35 | 36 | devShells = { 37 | default = napalm.napalm-registry-devshell; 38 | }; 39 | } 40 | ) 41 | ) // { 42 | overlays = { 43 | default = final: prev: { 44 | napalm = import ./. { 45 | pkgs = final; 46 | }; 47 | }; 48 | }; 49 | 50 | templates = { 51 | default = { 52 | path = ./template; 53 | description = "Template for using Napalm with flakes"; 54 | }; 55 | }; 56 | }; 57 | } 58 | -------------------------------------------------------------------------------- /napalm-registry/Main.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE CPP #-} 2 | {-# LANGUAGE DataKinds #-} 3 | {-# LANGUAGE TupleSections #-} 4 | {-# LANGUAGE ViewPatterns #-} 5 | {-# LANGUAGE LambdaCase #-} 6 | {-# LANGUAGE OverloadedStrings #-} 7 | {-# LANGUAGE DerivingStrategies #-} 8 | {-# LANGUAGE GeneralizedNewtypeDeriving #-} 9 | {-# LANGUAGE TypeOperators #-} 10 | 11 | module Main (main) where 12 | 13 | import Control.Applicative 14 | import Control.Monad 15 | import Control.Monad.IO.Class 16 | import Data.Aeson ((.=)) 17 | import Data.Function 18 | import Data.Hashable (Hashable) 19 | import Data.List 20 | import Data.Proxy 21 | import Data.String (IsString(..)) 22 | import Data.Time (UTCTime(..), Day(ModifiedJulianDay)) 23 | import Servant (err404, err500, errBody, throwError) 24 | import Servant.API 25 | import qualified Options.Applicative as Opts 26 | import qualified Codec.Archive.Tar as Tar 27 | import qualified Codec.Compression.GZip as GZip 28 | import qualified Crypto.Hash.SHA1 as SHA1 29 | import qualified Data.Aeson as Aeson 30 | #if MIN_VERSION_aeson(2, 0, 0) 31 | import qualified Data.Aeson.KeyMap as Aeson.KeyMap 32 | #endif 33 | import qualified Data.ByteString as BS 34 | import qualified Data.ByteString.Lazy as BL 35 | import qualified Data.ByteString.Char8 as BL8 36 | import qualified Data.ByteString.Base16 as Base16 37 | import qualified Data.HashMap.Strict as HMS 38 | import qualified Data.Text as T 39 | import qualified Data.Text.IO as T 40 | import qualified Data.Text.Encoding as T 41 | import qualified Network.URI.Encode as URI 42 | import qualified Network.Wai.Handler.Warp as Warp 43 | import qualified Servant as Servant 44 | 45 | -- | See 'parseConfig' for field descriptions 46 | data Config = Config 47 | { configVerbose :: Bool 48 | , configEndpoint :: T.Text 49 | , configPort :: Port 50 | , configSnapshot :: FilePath 51 | } deriving Show 52 | 53 | data Port = UsePort Int | ReportTo FilePath 54 | deriving Show 55 | 56 | main :: IO () 57 | main = do 58 | config <- Opts.execParser (Opts.info (parseConfig <**> Opts.helper) Opts.fullDesc) 59 | 60 | putStrLn "Running napalm registry with config:" 61 | print config 62 | 63 | snapshot <- Aeson.decodeFileStrict (configSnapshot config) >>= \case 64 | Just snapshot -> pure snapshot 65 | Nothing -> error $ "Could not parse packages" 66 | case configPort config of 67 | UsePort p -> 68 | Warp.run p (Servant.serve api (server config p snapshot)) 69 | ReportTo reportTo -> do 70 | putStrLn "Asking warp for a free port" 71 | (port,sock) <- Warp.openFreePort 72 | putStrLn ("Warp picked port " <> show port <> ", reporting to " <> reportTo) 73 | writeFile reportTo (show port) 74 | let settings = Warp.defaultSettings & Warp.setPort port 75 | Warp.runSettingsSocket settings sock (Servant.serve api (server config port snapshot)) 76 | 77 | parseConfig :: Opts.Parser Config 78 | parseConfig = Config <$> 79 | Opts.switch ( 80 | Opts.long "verbose" <> 81 | Opts.short 'v' <> 82 | Opts.help "Print information about requests" 83 | ) <*> 84 | Opts.strOption ( 85 | Opts.long "endpoint" <> 86 | Opts.value "localhost" <> 87 | Opts.help "The endpoint of this server, used in the Tarball URL" 88 | ) <*> 89 | (UsePort <$> Opts.option Opts.auto ( 90 | Opts.long "port" <> 91 | Opts.value 8081 <> 92 | Opts.help "The to serve on, also used in the Tarball URL" 93 | ) <|> 94 | ReportTo <$> Opts.strOption ( 95 | Opts.long "report-to" <> 96 | Opts.metavar "FILE" <> 97 | Opts.help "Use a random port and report to FILE" 98 | )) <*> 99 | Opts.strOption ( 100 | Opts.long "snapshot" <> 101 | Opts.help (unwords 102 | [ "Path to the snapshot file." 103 | , "The snapshot is a JSON file. The top-level keys are the package" 104 | , "names. The top-level values are objects mapping from version to the" 105 | , "path of the package tarball." 106 | , "Example:" 107 | , "{ \"lodash\": { \"1.0.0\": \"/path/to/lodash-1.0.0.tgz\" } }" 108 | ] 109 | ) 110 | ) 111 | 112 | api :: Proxy API 113 | api = Proxy 114 | 115 | server :: Config -> Warp.Port -> Snapshot -> Servant.Server API 116 | server config port ss = 117 | serveTarballScoped config ss :<|> 118 | serveTarballUnscoped config ss :<|> -- this needs to be before servePackageVersionMetadataScoped to avoid conflicts 119 | servePackageVersionMetadataScoped config port ss :<|> 120 | servePackageVersionMetadataUnscoped config port ss :<|> -- this needs to be before servePackageMetadataScoped to avoid conflicts 121 | servePackageMetadataScoped config port ss :<|> 122 | servePackageMetadataUnscoped config port ss -- this cannot be matched with current servant 123 | 124 | servePackageMetadataScoped :: Config -> Warp.Port -> Snapshot -> ScopeName -> PackageName -> Servant.Handler PackageMetadata 125 | servePackageMetadataScoped config port ss sn pn = servePackageMetadata config port ss (ScopedPackageName (Just sn) pn) 126 | 127 | servePackageMetadataUnscoped :: Config -> Warp.Port -> Snapshot -> PackageName -> Servant.Handler PackageMetadata 128 | servePackageMetadataUnscoped config port ss pn = servePackageMetadata config port ss (ScopedPackageName Nothing pn) 129 | 130 | servePackageMetadata :: Config -> Warp.Port -> Snapshot -> ScopedPackageName -> Servant.Handler PackageMetadata 131 | servePackageMetadata config port (unSnapshot -> ss) pn = do 132 | let flatPn = flattenScopedPackageName pn 133 | when (configVerbose config) $ 134 | liftIO $ T.putStrLn $ "Requesting package info for " <> unScopedPackageNameFlat flatPn 135 | pvs <- maybe 136 | (throwError (err404 {errBody = BL8.fromStrict $ BL8.pack $ "No such package: " <> T.unpack (unScopedPackageNameFlat flatPn)})) 137 | pure 138 | (HMS.lookup flatPn ss) 139 | 140 | pvs' <- forM (HMS.toList pvs) $ \(pv, tarPath) -> 141 | (pv,) <$> mkPackageVersionMetadata config port pn pv tarPath 142 | 143 | pure $ mkPackageMetadata pn (HMS.fromList pvs') 144 | 145 | servePackageVersionMetadataScoped 146 | :: Config 147 | -> Warp.Port 148 | -> Snapshot 149 | -> ScopeName 150 | -> PackageName 151 | -> PackageVersion 152 | -> Servant.Handler PackageVersionMetadata 153 | servePackageVersionMetadataScoped config port ss sn pn = servePackageVersionMetadata config port ss (ScopedPackageName (Just sn) pn) 154 | 155 | servePackageVersionMetadataUnscoped 156 | :: Config 157 | -> Warp.Port 158 | -> Snapshot 159 | -> PackageName 160 | -> PackageVersion 161 | -> Servant.Handler PackageVersionMetadata 162 | servePackageVersionMetadataUnscoped config port ss pn = servePackageVersionMetadata config port ss (ScopedPackageName Nothing pn) 163 | 164 | servePackageVersionMetadata 165 | :: Config 166 | -> Warp.Port 167 | -> Snapshot 168 | -> ScopedPackageName 169 | -> PackageVersion 170 | -> Servant.Handler PackageVersionMetadata 171 | servePackageVersionMetadata config port ss pn pv = do 172 | when (configVerbose config) $ 173 | liftIO $ T.putStrLn $ T.unwords 174 | [ "Requesting package version info for" 175 | , unScopedPackageNameFlat (flattenScopedPackageName pn) <> "#" <> unPackageVersion pv 176 | ] 177 | 178 | tarPath <- maybe 179 | (throwError (err404 {errBody = "No such tarball"})) 180 | pure 181 | (getTarPath ss pn pv) 182 | 183 | mkPackageVersionMetadata config port pn pv tarPath 184 | 185 | getTarPath :: Snapshot -> ScopedPackageName -> PackageVersion -> Maybe FilePath 186 | getTarPath (unSnapshot -> ss) pn pv = do 187 | pvs <- HMS.lookup (flattenScopedPackageName pn) ss 188 | tarPath <- HMS.lookup pv pvs 189 | pure $ tarPath 190 | 191 | serveTarballScoped :: Config -> Snapshot -> ScopeName -> PackageName -> TarballName -> Servant.Handler Tarball 192 | serveTarballScoped config ss sn pn = serveTarball config ss (ScopedPackageName (Just sn) pn) 193 | 194 | serveTarballUnscoped :: Config -> Snapshot -> PackageName -> TarballName -> Servant.Handler Tarball 195 | serveTarballUnscoped config ss pn = serveTarball config ss (ScopedPackageName Nothing pn) 196 | 197 | serveTarball :: Config -> Snapshot -> ScopedPackageName -> TarballName -> Servant.Handler Tarball 198 | serveTarball config ss pn tarName = do 199 | when (configVerbose config) $ 200 | liftIO $ T.putStrLn $ T.unwords 201 | [ "Requesting tarball for" 202 | , unScopedPackageNameFlat (flattenScopedPackageName pn) <> ":" 203 | , unTarballName tarName 204 | ] 205 | 206 | pv <- maybe (throwError $ err500 { errBody = "Could not parse version"}) pure $ do 207 | let pn' = spnName pn -- the tarball filename does not contain scope 208 | let tn' = unTarballName tarName 209 | a <- T.stripPrefix (unPackageName pn' <> "-") tn' 210 | b <- T.stripSuffix ".tgz" a 211 | pure $ PackageVersion b 212 | 213 | tarPath <- maybe 214 | (throwError (err404 {errBody = "No such tarball"})) 215 | pure 216 | (getTarPath ss pn pv) 217 | liftIO $ Tarball <$> BS.readFile tarPath 218 | 219 | toTarballName :: ScopedPackageName -> PackageVersion -> TarballName 220 | toTarballName pn (PackageVersion pv) = 221 | TarballName (unScopedPackageNameFlat (flattenScopedPackageName pn) <> "-" <> pv <> ".tgz") 222 | 223 | flattenScopedPackageName :: ScopedPackageName -> ScopedPackageNameFlat 224 | flattenScopedPackageName (ScopedPackageName Nothing (PackageName pn)) = ScopedPackageNameFlat pn 225 | flattenScopedPackageName (ScopedPackageName (Just (ScopeName sn)) (PackageName pn)) = ScopedPackageNameFlat (sn <> "/" <> pn) 226 | 227 | type API = 228 | Capture "scope_name" ScopeName :> 229 | Capture "package_name" PackageName :> 230 | "-" :> 231 | Capture "tarbal_name" TarballName :> 232 | Get '[OctetStream] Tarball :<|> 233 | Capture "package_name" PackageName :> 234 | "-" :> 235 | Capture "tarbal_name" TarballName :> 236 | Get '[OctetStream] Tarball :<|> 237 | Capture "scope_name" ScopeName :> 238 | Capture "package_name" PackageName :> 239 | Capture "package_version" PackageVersion :> 240 | Get '[JSON] PackageVersionMetadata :<|> 241 | Capture "package_name" PackageName :> 242 | Capture "package_version" PackageVersion :> 243 | Get '[JSON] PackageVersionMetadata :<|> 244 | Capture "scope_name" ScopeName :> Capture "package_name" PackageName :> Get '[JSON] PackageMetadata :<|> 245 | Capture "package_name" PackageName :> Get '[JSON] PackageMetadata 246 | 247 | newtype PackageTag = PackageTag { _unPackageTag :: T.Text } 248 | deriving newtype ( Aeson.ToJSONKey, Eq, IsString, Hashable ) 249 | newtype PackageVersion = PackageVersion { unPackageVersion :: T.Text } 250 | deriving newtype ( Eq, Ord, Hashable, FromHttpApiData, Aeson.ToJSONKey, Aeson.FromJSONKey, Aeson.ToJSON ) 251 | data ScopedPackageName = ScopedPackageName { _spnScope :: Maybe ScopeName, spnName :: PackageName } 252 | deriving ( Eq ) 253 | newtype ScopedPackageNameFlat = ScopedPackageNameFlat { unScopedPackageNameFlat :: T.Text } 254 | deriving newtype ( Eq, Show, Hashable, Aeson.FromJSONKey, Aeson.ToJSON ) 255 | newtype PackageName = PackageName { unPackageName :: T.Text } 256 | deriving newtype ( Eq, Show, Hashable, FromHttpApiData, Aeson.FromJSONKey, Aeson.ToJSON ) 257 | newtype ScopeName = ScopeName { _unScopeName :: T.Text } 258 | deriving newtype ( Eq, Show, Hashable, FromHttpApiData, Aeson.FromJSONKey, Aeson.ToJSON ) 259 | 260 | -- | With .tgz extension 261 | newtype TarballName = TarballName { unTarballName :: T.Text } 262 | deriving newtype FromHttpApiData 263 | 264 | newtype Tarball = Tarball { _unTarball :: BS.ByteString } 265 | deriving newtype (MimeRender OctetStream) 266 | 267 | data PackageMetadata = PackageMetadata 268 | { packageDistTags :: HMS.HashMap PackageTag PackageVersion 269 | , packageModified :: UTCTime 270 | , packageName :: ScopedPackageNameFlat 271 | , packageVersions :: HMS.HashMap PackageVersion PackageVersionMetadata 272 | } 273 | 274 | mkPackageMetadata 275 | :: ScopedPackageName 276 | -> HMS.HashMap PackageVersion PackageVersionMetadata 277 | -> PackageMetadata 278 | mkPackageMetadata pn pvs = PackageMetadata 279 | { packageDistTags = HMS.singleton "latest" latestVersion 280 | -- This is a dummy date 281 | , packageModified = UTCTime (ModifiedJulianDay 0) 0 282 | , packageName = flattenScopedPackageName pn 283 | , packageVersions = pvs 284 | } 285 | where 286 | -- XXX: fails if not versions are specified 287 | latestVersion = maximum (HMS.keys pvs) 288 | 289 | instance Aeson.ToJSON PackageMetadata where 290 | toJSON pm = Aeson.object 291 | [ "versions" .= packageVersions pm 292 | , "name" .= packageName pm 293 | , "dist-tags" .= packageDistTags pm 294 | , "modified" .= packageModified pm 295 | ] 296 | 297 | -- | Basically the package.json 298 | newtype PackageVersionMetadata = PackageVersionMetadata 299 | { _unPackageVersionMetadata :: Aeson.Value } 300 | deriving newtype ( Aeson.ToJSON ) 301 | 302 | sha1sum :: FilePath -> IO T.Text 303 | sha1sum fp = hash <$> BS.readFile fp 304 | where 305 | hash = T.decodeUtf8 . Base16.encode . SHA1.hash 306 | 307 | mkPackageVersionMetadata 308 | :: Config 309 | -> Warp.Port 310 | -> ScopedPackageName 311 | -> PackageVersion 312 | -> FilePath 313 | -> Servant.Handler PackageVersionMetadata 314 | mkPackageVersionMetadata config port pn pv tarPath = do 315 | shasum <- liftIO (sha1sum tarPath) :: Servant.Handler T.Text 316 | 317 | let 318 | tarName = toTarballName pn pv 319 | tarURL = mkTarballURL config port pn tarName 320 | dist = Aeson.object 321 | [ "shasum" .= shasum 322 | , "tarball" .= tarURL 323 | ] 324 | 325 | packageJson <- readPackageJson tarPath 326 | 327 | let 328 | #if MIN_VERSION_aeson(2, 0, 0) 329 | keyMapSingleton = Aeson.KeyMap.singleton 330 | #else 331 | keyMapSingleton = HMS.singleton 332 | #endif 333 | 334 | pure $ PackageVersionMetadata $ 335 | Aeson.Object $ 336 | keyMapSingleton "dist" dist <> packageJson 337 | 338 | mkTarballURL :: Config -> Warp.Port -> ScopedPackageName -> TarballName -> T.Text 339 | mkTarballURL 340 | config 341 | port 342 | (URI.encodeText . unScopedPackageNameFlat . flattenScopedPackageName -> pn) 343 | (URI.encodeText . unTarballName -> tarName) 344 | = "http://" <> 345 | T.intercalate "/" 346 | [ configEndpoint config <> ":" <> tshow port, pn, "-", tarName ] 347 | where 348 | tshow = T.pack . show 349 | 350 | readPackageJson :: FilePath -> Servant.Handler Aeson.Object 351 | readPackageJson fp = do 352 | tar <- GZip.decompress <$> liftIO (BL.readFile fp) 353 | 354 | packageJsonRaw <- maybe 355 | (throwError (err404 {errBody = BL8.fromStrict $ BL8.pack $ "Could not find package JSON for package " <> fp})) 356 | pure 357 | $ Tar.foldEntries 358 | (\e -> case Tar.entryContent e of 359 | Tar.NormalFile bs _size 360 | | "package.json" `isSuffixOf` Tar.entryPath e -> (Just bs <|>) 361 | _ -> (Nothing <|>) 362 | ) Nothing (const Nothing) (Tar.read tar) 363 | 364 | packageJson <- maybe 365 | (throwError $ err500 { errBody = "Could not parse package JSON: " <> packageJsonRaw}) 366 | pure 367 | (Aeson.decode packageJsonRaw) :: Servant.Handler Aeson.Object 368 | 369 | pure $ packageJson 370 | 371 | newtype Snapshot = Snapshot 372 | { unSnapshot :: HMS.HashMap ScopedPackageNameFlat (HMS.HashMap PackageVersion FilePath) 373 | } 374 | deriving newtype ( Aeson.FromJSON ) 375 | -------------------------------------------------------------------------------- /napalm-registry/README.md: -------------------------------------------------------------------------------- 1 | # napalm-registry 2 | 3 | After changing `napalm-registry.cabal`, make sure to execute 4 | `cabal2nix . > default.nix`, so the Nix package definition 5 | is regenerated. 6 | -------------------------------------------------------------------------------- /napalm-registry/default.nix: -------------------------------------------------------------------------------- 1 | { mkDerivation, aeson, base, base16-bytestring, bytestring 2 | , cryptohash, hashable, hpack, lib, optparse-applicative, servant 3 | , servant-server, tar, text, time, unordered-containers, uri-encode 4 | , warp, zlib 5 | }: 6 | mkDerivation { 7 | pname = "napalm-registry"; 8 | version = "0.0.0"; 9 | src = ./.; 10 | isLibrary = false; 11 | isExecutable = true; 12 | libraryToolDepends = [ hpack ]; 13 | executableHaskellDepends = [ 14 | aeson base base16-bytestring bytestring cryptohash hashable 15 | optparse-applicative servant servant-server tar text time 16 | unordered-containers uri-encode warp zlib 17 | ]; 18 | prePatch = "hpack"; 19 | license = lib.licenses.mit; 20 | } 21 | -------------------------------------------------------------------------------- /napalm-registry/package.yaml: -------------------------------------------------------------------------------- 1 | name: napalm-registry 2 | license: MIT 3 | 4 | dependencies: 5 | - aeson 6 | - base 7 | - base16-bytestring 8 | - bytestring 9 | - cryptohash 10 | - hashable 11 | - optparse-applicative 12 | - servant 13 | - servant-server 14 | - tar 15 | - text 16 | - time 17 | - unordered-containers 18 | - uri-encode 19 | - warp 20 | - zlib 21 | 22 | ghc-options: 23 | - -Wall 24 | - -Werror 25 | - -threaded 26 | 27 | executable: 28 | main: Main.hs 29 | -------------------------------------------------------------------------------- /nix/default.nix: -------------------------------------------------------------------------------- 1 | {}: 2 | 3 | let 4 | lock = builtins.fromJSON (builtins.readFile ../flake.lock); 5 | nixpkgs = builtins.fetchTarball { 6 | url = "https://github.com/NixOS/nixpkgs/archive/${lock.nodes.nixpkgs.locked.rev}.tar.gz"; 7 | sha256 = lock.nodes.nixpkgs.locked.narHash; 8 | }; 9 | pkgs = import nixpkgs { }; 10 | in 11 | pkgs 12 | -------------------------------------------------------------------------------- /nix/sources.json: -------------------------------------------------------------------------------- 1 | { 2 | "bitwarden-cli": { 3 | "branch": "master", 4 | "description": "The command line vault (Windows, macOS, & Linux).", 5 | "homepage": "https://bitwarden.com", 6 | "owner": "bitwarden", 7 | "repo": "cli", 8 | "rev": "e7450d27e468f0e68fe5b75f9c52ffa952185484", 9 | "sha256": "0zy5nfphj1jfp9fh7h3jqg9r1zik1lf7by0mg668v4fxp3f9hw5y", 10 | "type": "tarball", 11 | "url": "https://github.com/bitwarden/cli/archive/e7450d27e468f0e68fe5b75f9c52ffa952185484.tar.gz", 12 | "url_template": "https://github.com///archive/.tar.gz" 13 | }, 14 | "bitwarden-jslib": { 15 | "branch": "master", 16 | "description": "Common code referenced across Bitwarden JavaScript projects.", 17 | "homepage": "https://bitwarden.com", 18 | "owner": "bitwarden", 19 | "repo": "jslib", 20 | "rev": "98c7dc162628129d0bddbf20359d389dacb661d3", 21 | "sha256": "10245ypcrqwggjah2qr6zi4xb1wwfaimj566vg760wwk8b1hbh1w", 22 | "type": "tarball", 23 | "url": "https://github.com/bitwarden/jslib/archive/98c7dc162628129d0bddbf20359d389dacb661d3.tar.gz", 24 | "url_template": "https://github.com///archive/.tar.gz" 25 | }, 26 | "cli": { 27 | "branch": "master", 28 | "description": "Netlify command line tool", 29 | "homepage": "https://www.netlify.com/docs/cli", 30 | "owner": "netlify", 31 | "repo": "cli", 32 | "rev": "ba3bd39fa512ecc1d4d08c36225f8f2fbf16bc0e", 33 | "sha256": "0qgs3jzdkaapchiidsyixhankn8l4r3w6xg46aiqlm2a36vysrz5", 34 | "type": "tarball", 35 | "url": "https://github.com/netlify/cli/archive/ba3bd39fa512ecc1d4d08c36225f8f2fbf16bc0e.tar.gz", 36 | "url_template": "https://github.com///archive/.tar.gz" 37 | }, 38 | "deckdeckgo-starter": { 39 | "branch": "master", 40 | "description": "The Progressive Web App alternative for simple presentations", 41 | "homepage": "https://deckdeckgo.com", 42 | "owner": "deckgo", 43 | "repo": "deckdeckgo-starter", 44 | "rev": "f3354abff7654261c66439968fd601a3c0109c03", 45 | "sha256": "1h0viivhwh4ssqkp91xchhcpk8kad1hjfihna0i2nxwzy0sv2m1g", 46 | "type": "tarball", 47 | "url": "https://github.com/deckgo/deckdeckgo-starter/archive/f3354abff7654261c66439968fd601a3c0109c03.tar.gz", 48 | "url_template": "https://github.com///archive/.tar.gz" 49 | }, 50 | "niv": { 51 | "branch": "master", 52 | "description": "Easy dependency management for Nix projects", 53 | "homepage": "https://github.com/nmattia/niv", 54 | "owner": "nmattia", 55 | "repo": "niv", 56 | "rev": "abd0de3269fd712955d27b70e32921841c7b8bb7", 57 | "sha256": "0b38n1ad00s1qqyw3ml3pypf8i1pw4aqw0bpa02qq9iv7sp3x0gz", 58 | "type": "tarball", 59 | "url": "https://github.com/nmattia/niv/archive/abd0de3269fd712955d27b70e32921841c7b8bb7.tar.gz", 60 | "url_template": "https://github.com///archive/.tar.gz" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /nix/sources.nix: -------------------------------------------------------------------------------- 1 | # This file has been generated by Niv. 2 | 3 | let 4 | 5 | # 6 | # The fetchers. fetch_ fetches specs of type . 7 | # 8 | 9 | fetch_file = pkgs: spec: 10 | if spec.builtin or true then 11 | builtins_fetchurl { inherit (spec) url sha256; } 12 | else 13 | pkgs.fetchurl { inherit (spec) url sha256; }; 14 | 15 | fetch_tarball = pkgs: spec: 16 | if spec.builtin or true then 17 | builtins_fetchTarball { inherit (spec) url sha256; } 18 | else 19 | pkgs.fetchzip { inherit (spec) url sha256; }; 20 | 21 | fetch_git = spec: 22 | builtins.fetchGit { url = spec.repo; inherit (spec) rev ref; }; 23 | 24 | fetch_builtin-tarball = spec: 25 | builtins.trace 26 | '' 27 | WARNING: 28 | The niv type "builtin-tarball" will soon be deprecated. You should 29 | instead use `builtin = true`. 30 | 31 | $ niv modify -a type=tarball -a builtin=true 32 | '' 33 | builtins_fetchTarball 34 | { inherit (spec) url sha256; }; 35 | 36 | fetch_builtin-url = spec: 37 | builtins.trace 38 | '' 39 | WARNING: 40 | The niv type "builtin-url" will soon be deprecated. You should 41 | instead use `builtin = true`. 42 | 43 | $ niv modify -a type=file -a builtin=true 44 | '' 45 | (builtins_fetchurl { inherit (spec) url sha256; }); 46 | 47 | # 48 | # Various helpers 49 | # 50 | 51 | # The set of packages used when specs are fetched using non-builtins. 52 | mkPkgs = sources: 53 | if hasNixpkgsPath 54 | then 55 | if hasThisAsNixpkgsPath 56 | then import (builtins_fetchTarball { inherit (mkNixpkgs sources) url sha256; }) { } 57 | else import { } 58 | else 59 | import (builtins_fetchTarball { inherit (mkNixpkgs sources) url sha256; }) { }; 60 | 61 | mkNixpkgs = sources: 62 | if builtins.hasAttr "nixpkgs" sources 63 | then sources.nixpkgs 64 | else 65 | abort 66 | '' 67 | Please specify either (through -I or NIX_PATH=nixpkgs=...) or 68 | add a package called "nixpkgs" to your sources.json. 69 | ''; 70 | 71 | hasNixpkgsPath = (builtins.tryEval ).success; 72 | hasThisAsNixpkgsPath = 73 | (builtins.tryEval ).success && == ./.; 74 | 75 | # The actual fetching function. 76 | fetch = pkgs: name: spec: 77 | 78 | if ! builtins.hasAttr "type" spec then 79 | abort "ERROR: niv spec ${name} does not have a 'type' attribute" 80 | else if spec.type == "file" then fetch_file pkgs spec 81 | else if spec.type == "tarball" then fetch_tarball pkgs spec 82 | else if spec.type == "git" then fetch_git spec 83 | else if spec.type == "builtin-tarball" then fetch_builtin-tarball spec 84 | else if spec.type == "builtin-url" then fetch_builtin-url spec 85 | else 86 | abort "ERROR: niv spec ${name} has unknown type ${builtins.toJSON spec.type}"; 87 | 88 | # Ports of functions for older nix versions 89 | 90 | # a Nix version of mapAttrs if the built-in doesn't exist 91 | mapAttrs = builtins.mapAttrs or ( 92 | f: set: with builtins; 93 | listToAttrs (map (attr: { name = attr; value = f attr set.${attr}; }) (attrNames set)) 94 | ); 95 | 96 | # fetchTarball version that is compatible between all the versions of Nix 97 | builtins_fetchTarball = { url, sha256 }@attrs: 98 | let inherit (builtins) lessThan nixVersion fetchTarball; in 99 | if lessThan nixVersion "1.12" then 100 | fetchTarball { inherit url; } 101 | else 102 | fetchTarball attrs; 103 | 104 | # fetchurl version that is compatible between all the versions of Nix 105 | builtins_fetchurl = { url, sha256 }@attrs: 106 | let 107 | inherit (builtins) lessThan nixVersion fetchurl; 108 | in 109 | if lessThan nixVersion "1.12" then 110 | fetchurl { inherit url; } 111 | else 112 | fetchurl attrs; 113 | 114 | # Create the final "sources" from the config 115 | mkSources = config: 116 | mapAttrs 117 | ( 118 | name: spec: 119 | if builtins.hasAttr "outPath" spec 120 | then 121 | abort 122 | "The values in sources.json should not have an 'outPath' attribute" 123 | else 124 | spec // { outPath = fetch config.pkgs name spec; } 125 | ) 126 | config.sources; 127 | 128 | # The "config" used by the fetchers 129 | mkConfig = 130 | { sourcesFile ? ./sources.json 131 | }: rec { 132 | # The sources, i.e. the attribute set of spec name to spec 133 | sources = builtins.fromJSON (builtins.readFile sourcesFile); 134 | # The "pkgs" (evaluated nixpkgs) to use for e.g. non-builtin fetchers 135 | pkgs = mkPkgs sources; 136 | }; 137 | in 138 | mkSources (mkConfig { }) // 139 | { __functor = _: settings: mkSources (mkConfig settings); } 140 | -------------------------------------------------------------------------------- /script/test: -------------------------------------------------------------------------------- 1 | # vim: set ft=bash 2 | 3 | nix-build --no-link --max-jobs 20 --sandbox 4 | -------------------------------------------------------------------------------- /scripts/lib.mjs: -------------------------------------------------------------------------------- 1 | import fsPromises from "fs/promises"; 2 | import crypto from "crypto"; 3 | 4 | const loadAllPackageLocks = (root) => 5 | fsPromises.readdir(root, { withFileTypes: true }) 6 | .then(files => 7 | files.reduce(async (locksP, file) => { 8 | const fileName = `${root}/${file.name}`; 9 | const locks = await locksP; 10 | 11 | return file.isDirectory() 12 | ? [...locks, ...(await loadAllPackageLocks(fileName))] 13 | : (file.name === "package-lock.json") 14 | ? [...locks, fileName] 15 | : locks; 16 | }, Promise.resolve([]))); 17 | 18 | const loadJSONFile = (file) => fsPromises.readFile(file, { encoding: 'utf8' }).then(JSON.parse); 19 | 20 | const getHashOf = (type, file) => fsPromises.readFile(file).then((contents) => { 21 | const hash = crypto.createHash(type); 22 | hash.setEncoding("hex"); 23 | hash.update(contents); 24 | return `${type}-${hash.digest('base64')}`; 25 | }); 26 | 27 | export { loadAllPackageLocks, loadJSONFile, getHashOf } 28 | -------------------------------------------------------------------------------- /scripts/lock-patcher.mjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /* 4 | This node.js script is required in order to patch package-lock.json with new sha512 hashes. 5 | 6 | It loads all `package-lock.json` files that are in root of the project as well as these 7 | nested inside folders and patches their integrity based on packages snapshot created by Nix. 8 | 9 | Sadly this can't be done with Nix due to restricted evaluation mode. 10 | This script should not have any external npm dependencies 11 | */ 12 | 13 | import fsPromises from "fs/promises" 14 | 15 | import { loadJSONFile, loadAllPackageLocks, getHashOf } from "./lib.mjs" 16 | 17 | // Returns new set, that is modified dependencies argument 18 | // with proper integrity hashes 19 | const updateDependencies = async (snapshot, dependencies) => Object.fromEntries( 20 | await Promise.all(Object.entries(dependencies).map(async ([packageName, pkg]) => { 21 | try { 22 | const hashType = pkg.integrity ? pkg.integrity.split("-")[0] : undefined; 23 | return [ 24 | packageName, 25 | { 26 | ...pkg, 27 | integrity: hashType ? await getHashOf(hashType, snapshot[packageName][pkg.version]) : undefined, 28 | dependencies: pkg.dependencies ? await updateDependencies(snapshot, pkg.dependencies) : undefined 29 | } 30 | ] 31 | } 32 | catch (err) { 33 | console.error(`[lock-patcher] At: ${packageName}-${pkg.version} (${JSON.stringify(snapshot[packageName])})`); 34 | console.error(err); 35 | return [packageName, pkg]; 36 | } 37 | })) 38 | ); 39 | 40 | // Returns new set, that is modified packages argument 41 | // with proper integrity hashes 42 | const packageNameRegex = /node_modules\/(?(?:@[^/]+\/)?[^/]+)$/; 43 | const updatePackages = async (snapshot, packages) => Object.fromEntries( 44 | await Promise.all(Object.entries(packages).map(async ([packagePath, pkg]) => { 45 | const packageName = packagePath.match(packageNameRegex)?.groups.name; 46 | try { 47 | const hashType = pkg.integrity ? pkg.integrity.split("-")[0] : undefined; 48 | return [ 49 | packagePath, 50 | { 51 | ...pkg, 52 | integrity: hashType ? await getHashOf(hashType, snapshot[packageName][pkg.version]) : undefined, 53 | } 54 | ] 55 | } 56 | catch (err) { 57 | console.error(`[lock-patcher] At: ${packageName}-${pkg.version} (${JSON.stringify(snapshot[packageName])})`); 58 | console.error(err); 59 | return [packagePath, pkg]; 60 | } 61 | })) 62 | ); 63 | 64 | (async () => { 65 | if (process.argv.length != 3) { 66 | console.log("Usage:"); 67 | console.log(` ${process.argv[0]} ${process.argv[1]} [snapshot]`); 68 | 69 | process.exit(-1); 70 | }; 71 | 72 | console.log("[lock-patcher] Loading Snapshot ..."); 73 | const snapshot = await loadJSONFile(process.argv[2]); 74 | 75 | console.log(`[lock-patcher] Looking for package locks (in ${process.cwd()}) ...`) 76 | const foundPackageLocks = await loadAllPackageLocks(process.cwd()); 77 | console.log(`[lock-patcher] Found: ${foundPackageLocks}`); 78 | 79 | console.log("[lock-patcher] Loading package-locks ..."); 80 | const packageLocks = await Promise.all( 81 | foundPackageLocks.map((lock) => loadJSONFile(lock) 82 | .then(parsed => ({ parsed: parsed, path: lock })) 83 | .catch((err) => { 84 | console.error(`[lock-patcher] Could not load: ${lock}`); 85 | console.error(err); 86 | return null; 87 | })) 88 | ).then(locks => locks.filter(val => val != null)); 89 | 90 | console.log("[lock-patcher] Patching locks ..."); 91 | 92 | const promises = packageLocks.map(async (lock) => { 93 | const set = { 94 | ...lock.parsed, 95 | // lockfileVersion ≤ 2 96 | dependencies: lock.parsed.dependencies ? await updateDependencies(snapshot, lock.parsed.dependencies) : undefined, 97 | // lockfileVersion ≥ 2 98 | packages: lock.parsed.packages ? await updatePackages(snapshot, lock.parsed.packages) : undefined, 99 | }; 100 | 101 | return await fsPromises.writeFile(lock.path, JSON.stringify(set), { encoding: 'utf8', flag: 'w' }); 102 | }); 103 | 104 | await Promise.all(promises); 105 | })().catch((err) => { 106 | console.error("[lock-patcher] Error:"); 107 | console.error(err); 108 | }); 109 | -------------------------------------------------------------------------------- /shell.nix: -------------------------------------------------------------------------------- 1 | { pkgs ? import ./nix { } }: 2 | (import ./default.nix { inherit pkgs; }).napalm-registry-devshell 3 | -------------------------------------------------------------------------------- /template/flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "An example of Napalm with flakes"; 3 | 4 | # Nixpkgs / NixOS version to use. 5 | inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; 6 | 7 | # Import napalm 8 | inputs.napalm.url = "github:nix-community/napalm"; 9 | 10 | outputs = { self, nixpkgs, napalm }: 11 | let 12 | # Generate a user-friendly version number. 13 | version = builtins.substring 0 8 self.lastModifiedDate; 14 | 15 | # System types to support. 16 | supportedSystems = [ "x86_64-linux" "aarch64-linux" "i686-linux" "x86_64-darwin" "aarch64-darwin" ]; 17 | 18 | # Helper function to generate an attrset '{ x86_64-linux = f "x86_64-linux"; ... }'. 19 | forAllSystems = f: 20 | nixpkgs.lib.genAttrs supportedSystems (system: f system); 21 | 22 | # Nixpkgs instantiated for supported system types. 23 | nixpkgsFor = forAllSystems (system: 24 | import nixpkgs { 25 | inherit system; 26 | # Add napalm to you overlay's list 27 | overlays = [ 28 | self.overlays.default 29 | napalm.overlays.default 30 | ]; 31 | }); 32 | 33 | in 34 | { 35 | # A Nixpkgs overlay. 36 | overlays = { 37 | default = final: prev: { 38 | # Example package 39 | hello-world = final.napalm.buildPackage ./hello-world { }; 40 | }; 41 | }; 42 | 43 | # Provide your packages for selected system types. 44 | packages = forAllSystems (system: { 45 | inherit (nixpkgsFor.${system}) hello-world; 46 | 47 | # The default package for 'nix build'. This makes sense if the 48 | # flake provides only one package or there is a clear "main" 49 | # package. 50 | default = self.packages.${system}.hello-world; 51 | }); 52 | }; 53 | } 54 | -------------------------------------------------------------------------------- /template/hello-world/cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | console.log("Hello, World!"); 4 | -------------------------------------------------------------------------------- /template/hello-world/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hello-world", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1 5 | } 6 | -------------------------------------------------------------------------------- /template/hello-world/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hello-world", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "bin": { 12 | "say-hello": "./cli.js" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /test/deps-alias/cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | console.log("Hello!"); 4 | -------------------------------------------------------------------------------- /test/deps-alias/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "deps-alias", 3 | "version": "1.0.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "deps-alias", 9 | "version": "1.0.0", 10 | "license": "ISC", 11 | "dependencies": { 12 | "@isaacs/cliui": "8.0.2" 13 | }, 14 | "bin": { 15 | "say-hello": "cli.js" 16 | } 17 | }, 18 | "node_modules/@isaacs/cliui": { 19 | "version": "8.0.2", 20 | "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", 21 | "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", 22 | "dependencies": { 23 | "string-width": "^5.1.2", 24 | "string-width-cjs": "npm:string-width@^4.2.0", 25 | "strip-ansi": "^7.0.1", 26 | "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", 27 | "wrap-ansi": "^8.1.0", 28 | "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" 29 | }, 30 | "engines": { 31 | "node": ">=12" 32 | } 33 | }, 34 | "node_modules/ansi-regex": { 35 | "version": "6.0.1", 36 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", 37 | "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", 38 | "engines": { 39 | "node": ">=12" 40 | }, 41 | "funding": { 42 | "url": "https://github.com/chalk/ansi-regex?sponsor=1" 43 | } 44 | }, 45 | "node_modules/ansi-styles": { 46 | "version": "6.2.1", 47 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", 48 | "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", 49 | "engines": { 50 | "node": ">=12" 51 | }, 52 | "funding": { 53 | "url": "https://github.com/chalk/ansi-styles?sponsor=1" 54 | } 55 | }, 56 | "node_modules/color-convert": { 57 | "version": "2.0.1", 58 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", 59 | "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", 60 | "dependencies": { 61 | "color-name": "~1.1.4" 62 | }, 63 | "engines": { 64 | "node": ">=7.0.0" 65 | } 66 | }, 67 | "node_modules/color-name": { 68 | "version": "1.1.4", 69 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", 70 | "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" 71 | }, 72 | "node_modules/eastasianwidth": { 73 | "version": "0.2.0", 74 | "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", 75 | "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" 76 | }, 77 | "node_modules/emoji-regex": { 78 | "version": "9.2.2", 79 | "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", 80 | "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" 81 | }, 82 | "node_modules/is-fullwidth-code-point": { 83 | "version": "3.0.0", 84 | "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", 85 | "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", 86 | "engines": { 87 | "node": ">=8" 88 | } 89 | }, 90 | "node_modules/string-width": { 91 | "version": "5.1.2", 92 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", 93 | "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", 94 | "dependencies": { 95 | "eastasianwidth": "^0.2.0", 96 | "emoji-regex": "^9.2.2", 97 | "strip-ansi": "^7.0.1" 98 | }, 99 | "engines": { 100 | "node": ">=12" 101 | }, 102 | "funding": { 103 | "url": "https://github.com/sponsors/sindresorhus" 104 | } 105 | }, 106 | "node_modules/string-width-cjs": { 107 | "name": "string-width", 108 | "version": "4.2.3", 109 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", 110 | "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", 111 | "dependencies": { 112 | "emoji-regex": "^8.0.0", 113 | "is-fullwidth-code-point": "^3.0.0", 114 | "strip-ansi": "^6.0.1" 115 | }, 116 | "engines": { 117 | "node": ">=8" 118 | } 119 | }, 120 | "node_modules/string-width-cjs/node_modules/ansi-regex": { 121 | "version": "5.0.1", 122 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", 123 | "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", 124 | "engines": { 125 | "node": ">=8" 126 | } 127 | }, 128 | "node_modules/string-width-cjs/node_modules/emoji-regex": { 129 | "version": "8.0.0", 130 | "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", 131 | "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" 132 | }, 133 | "node_modules/string-width-cjs/node_modules/strip-ansi": { 134 | "version": "6.0.1", 135 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", 136 | "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", 137 | "dependencies": { 138 | "ansi-regex": "^5.0.1" 139 | }, 140 | "engines": { 141 | "node": ">=8" 142 | } 143 | }, 144 | "node_modules/strip-ansi": { 145 | "version": "7.1.0", 146 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", 147 | "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", 148 | "dependencies": { 149 | "ansi-regex": "^6.0.1" 150 | }, 151 | "engines": { 152 | "node": ">=12" 153 | }, 154 | "funding": { 155 | "url": "https://github.com/chalk/strip-ansi?sponsor=1" 156 | } 157 | }, 158 | "node_modules/strip-ansi-cjs": { 159 | "name": "strip-ansi", 160 | "version": "6.0.1", 161 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", 162 | "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", 163 | "dependencies": { 164 | "ansi-regex": "^5.0.1" 165 | }, 166 | "engines": { 167 | "node": ">=8" 168 | } 169 | }, 170 | "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { 171 | "version": "5.0.1", 172 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", 173 | "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", 174 | "engines": { 175 | "node": ">=8" 176 | } 177 | }, 178 | "node_modules/wrap-ansi": { 179 | "version": "8.1.0", 180 | "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", 181 | "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", 182 | "dependencies": { 183 | "ansi-styles": "^6.1.0", 184 | "string-width": "^5.0.1", 185 | "strip-ansi": "^7.0.1" 186 | }, 187 | "engines": { 188 | "node": ">=12" 189 | }, 190 | "funding": { 191 | "url": "https://github.com/chalk/wrap-ansi?sponsor=1" 192 | } 193 | }, 194 | "node_modules/wrap-ansi-cjs": { 195 | "name": "wrap-ansi", 196 | "version": "7.0.0", 197 | "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", 198 | "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", 199 | "dependencies": { 200 | "ansi-styles": "^4.0.0", 201 | "string-width": "^4.1.0", 202 | "strip-ansi": "^6.0.0" 203 | }, 204 | "engines": { 205 | "node": ">=10" 206 | }, 207 | "funding": { 208 | "url": "https://github.com/chalk/wrap-ansi?sponsor=1" 209 | } 210 | }, 211 | "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { 212 | "version": "5.0.1", 213 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", 214 | "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", 215 | "engines": { 216 | "node": ">=8" 217 | } 218 | }, 219 | "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { 220 | "version": "4.3.0", 221 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", 222 | "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", 223 | "dependencies": { 224 | "color-convert": "^2.0.1" 225 | }, 226 | "engines": { 227 | "node": ">=8" 228 | }, 229 | "funding": { 230 | "url": "https://github.com/chalk/ansi-styles?sponsor=1" 231 | } 232 | }, 233 | "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { 234 | "version": "8.0.0", 235 | "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", 236 | "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" 237 | }, 238 | "node_modules/wrap-ansi-cjs/node_modules/string-width": { 239 | "version": "4.2.3", 240 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", 241 | "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", 242 | "dependencies": { 243 | "emoji-regex": "^8.0.0", 244 | "is-fullwidth-code-point": "^3.0.0", 245 | "strip-ansi": "^6.0.1" 246 | }, 247 | "engines": { 248 | "node": ">=8" 249 | } 250 | }, 251 | "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { 252 | "version": "6.0.1", 253 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", 254 | "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", 255 | "dependencies": { 256 | "ansi-regex": "^5.0.1" 257 | }, 258 | "engines": { 259 | "node": ">=8" 260 | } 261 | } 262 | } 263 | } 264 | -------------------------------------------------------------------------------- /test/deps-alias/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "deps-alias", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "scripts": { 6 | "test": "echo \"Error: no test specified\" && exit 1" 7 | }, 8 | "author": "", 9 | "license": "ISC", 10 | "bin": { 11 | "say-hello": "./cli.js" 12 | }, 13 | "dependencies": { 14 | "@isaacs/cliui": "8.0.2" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /test/hello-world-deps-v3/cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const _ = require("underscore"); 4 | console.log(_.range(5)); 5 | -------------------------------------------------------------------------------- /test/hello-world-deps-v3/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hello-world-deps-v3", 3 | "version": "1.0.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "hello-world-deps-v3", 9 | "version": "1.0.0", 10 | "license": "ISC", 11 | "dependencies": { 12 | "@babel/helper-string-parser": "^7.22.5", 13 | "underscore": "^1.9.1" 14 | }, 15 | "bin": { 16 | "say-hello": "cli.js" 17 | }, 18 | "devDependencies": {} 19 | }, 20 | "node_modules/@babel/helper-string-parser": { 21 | "version": "7.22.5", 22 | "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", 23 | "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", 24 | "engines": { 25 | "node": ">=6.9.0" 26 | } 27 | }, 28 | "node_modules/underscore": { 29 | "version": "1.9.1", 30 | "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.9.1.tgz", 31 | "integrity": "sha512-5/4etnCkd9c8gwgowi5/om/mYO5ajCaOgdzj/oW+0eQV9WxKBDZw5+ycmKmeaTXjInS/W0BzpGLo2xR2aBwZdg==" 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /test/hello-world-deps-v3/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hello-world-deps-v3", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "scripts": { 6 | "test": "echo \"Error: no test specified\" && exit 1" 7 | }, 8 | "author": "", 9 | "license": "ISC", 10 | "bin": { 11 | "say-hello": "./cli.js" 12 | }, 13 | "dependencies": { 14 | "@babel/helper-string-parser": "^7.22.5", 15 | "underscore": "^1.9.1" 16 | }, 17 | "description": "" 18 | } 19 | -------------------------------------------------------------------------------- /test/hello-world-deps/cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const _ = require("underscore"); 4 | console.log(_.range(5)); 5 | -------------------------------------------------------------------------------- /test/hello-world-deps/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hello-world-deps", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "underscore": { 8 | "version": "1.9.1", 9 | "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.9.1.tgz", 10 | "integrity": "sha512-5/4etnCkd9c8gwgowi5/om/mYO5ajCaOgdzj/oW+0eQV9WxKBDZw5+ycmKmeaTXjInS/W0BzpGLo2xR2aBwZdg==" 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /test/hello-world-deps/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hello-world-deps", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "scripts": { 6 | "test": "echo \"Error: no test specified\" && exit 1" 7 | }, 8 | "author": "", 9 | "license": "ISC", 10 | "bin": { 11 | "say-hello": "./cli.js" 12 | }, 13 | "dependencies": { 14 | "underscore": "^1.9.1" 15 | }, 16 | "devDependencies": {}, 17 | "description": "" 18 | } 19 | -------------------------------------------------------------------------------- /test/hello-world-workspace-v3/client/cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const _ = require("underscore"); 4 | console.log(_.range(5)); 5 | -------------------------------------------------------------------------------- /test/hello-world-workspace-v3/client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hello-world-workspace-v3-client", 3 | "version": "1.0.0", 4 | "license": "ISC", 5 | "dependencies": { 6 | "underscore": "^1.9.1" 7 | }, 8 | "bin": { 9 | "say-hello": "./cli.js" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /test/hello-world-workspace-v3/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hello-world-workspace-v3", 3 | "version": "1.0.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "hello-world-workspace-v3", 9 | "version": "1.0.0", 10 | "license": "ISC", 11 | "workspaces": [ 12 | "client" 13 | ] 14 | }, 15 | "client": { 16 | "name": "hello-world-workspace-v3-client", 17 | "version": "1.0.0", 18 | "license": "ISC", 19 | "dependencies": { 20 | "underscore": "^1.9.1" 21 | }, 22 | "bin": { 23 | "say-hello": "cli.js" 24 | } 25 | }, 26 | "node_modules/hello-world-workspace-v3-client": { 27 | "resolved": "client", 28 | "link": true 29 | }, 30 | "node_modules/underscore": { 31 | "version": "1.13.6", 32 | "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.6.tgz", 33 | "integrity": "sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==" 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /test/hello-world-workspace-v3/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hello-world-workspace-v3", 3 | "version": "1.0.0", 4 | "license": "ISC", 5 | "workspaces": [ 6 | "client" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /test/hello-world/cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | console.log("Hello, World!"); 4 | -------------------------------------------------------------------------------- /test/hello-world/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hello-world", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1 5 | } 6 | -------------------------------------------------------------------------------- /test/hello-world/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hello-world", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "bin": { 12 | "say-hello": "./cli.js" 13 | } 14 | } 15 | --------------------------------------------------------------------------------