diff --git a/NodePackageTool.hs b/NodePackageTool.hs
new file mode 100644
--- /dev/null
+++ b/NodePackageTool.hs
@@ -0,0 +1,170 @@
+{-# LANGUAGE RecordWildCards, NoImplicitPrelude, LambdaCase, FlexibleContexts, NamedFieldPuns, OverloadedStrings #-}
+import Protolude
+import qualified Data.Text.IO as TIO
+import qualified Control.Monad.Except as ExcT
+import qualified Control.Exception as Exc
+import qualified System.IO.Error as IOErr
+import qualified Data.ByteString.Lazy as BL
+import qualified Data.HashMap.Lazy as HML
+import qualified System.FilePath as FP
+import qualified System.Directory as Dir
+import qualified System.Posix.Files as PosixFiles
+import Options.Applicative
+
+import qualified Distribution.Nodejs.Package as NP
+
+data Args
+  = Args
+    { argsPackageDir :: FilePath
+    , argsMode :: Mode }
+
+data Mode
+  = LinkBin
+    { linkBinTargetDir :: FilePath }
+  | SetBinExecFlag
+
+args :: Parser Args
+args = subparser
+    (  command "link-bin"
+       (info (modeCommands linkBinSubcommands)
+         (progDesc "link package dependecies’ bin files"))
+    <> command "set-bin-exec-flag"
+       (info (modeCommands setBinExecFlagSubcommands)
+         (progDesc "make all bin scripts executable")) )
+  where
+    modeCommands modeSubcommands = Args
+      <$> strOption
+        ( long "package"
+        <> metavar "FOLDER"
+        <> help "folder with the node package" )
+      <*> modeSubcommands
+    linkBinSubcommands = LinkBin
+      <$> strOption
+        ( long "to"
+        <> metavar "LINK_TARGET"
+        <> help "folder to link to (absolute or relative from package folder)" )
+    setBinExecFlagSubcommands = pure SetBinExecFlag
+  
+
+type ErrorLogger = ExceptT [Char] IO
+
+-- | Print a warning to stdout.
+warn :: [Text] -> ErrorLogger ()
+warn ws = for_ ws $ \w -> liftIO $ TIO.hPutStrLn stderr ("Warning: " <> w)
+
+-- | On Exception rethrow with annotation.
+tryIOMsg :: ([Char] -> [Char]) -> IO a -> ErrorLogger a
+tryIOMsg errAnn = ExcT.withExceptT (errAnn . Exc.displayException) . tryIO
+
+
+main :: IO ()
+main = execParser (info (args <**> helper)
+                    (progDesc "Tool for various node package maintenance tasks"))
+       >>= realMain
+
+realMain :: Args -> IO ()
+realMain Args{..} = do
+  -- basic sanity checks
+  let packageJsonPath = argsPackageDir FP.</> "package.json"
+  unlessM (Dir.doesFileExist packageJsonPath)
+    $ die $ toS $ packageJsonPath <> " does not exist."
+  case argsMode of
+    LinkBin{..} -> do
+      unlessM (Dir.doesDirectoryExist linkBinTargetDir)
+        $ die $ toS $ linkBinTargetDir <> " is not a directory."
+    _ -> pass
+
+  -- parse & decode & run logic
+  runExceptT
+    (tryRead packageJsonPath >>= tryDecode packageJsonPath >>= go)
+    >>= \case
+      (Left err) -> die $ toS $ "ERROR: " <> err
+      (Right _) -> pass
+
+  where
+    tryRead :: FilePath -> ErrorLogger BL.ByteString
+    tryRead fp = tryIOMsg exc $ BL.readFile fp
+      where exc e = fp <> " cannot be read:\n" <> e
+    tryDecode :: FilePath -> BL.ByteString -> ExceptT [Char] IO NP.Package
+    tryDecode fp fileBs = do
+      (pkg, warnings) <- NP.unLoggingPackage
+                      <$> ExcT.ExceptT (pure $ first exc $ NP.decode fileBs)
+      warn $ fmap ((\w -> toS fp <> ": " <> w) . NP.formatWarning) warnings
+      pure pkg
+      where exc e = fp <> " cannot be decoded\n" <> toS e
+
+    tryAccess :: IO a -> IO (Maybe a)
+    tryAccess io =
+      hush <$> tryJust
+        (\e -> guard (IOErr.isDoesNotExistError e ||
+                      IOErr.isPermissionError e))
+          io
+
+    qte s = "\"" <> s <> "\""
+
+    go :: NP.Package -> ErrorLogger ()
+    go NP.Package{bin} = do
+     binFiles <- readBinFiles bin
+     for_ binFiles $ case argsMode of
+        -- Link all dependency binaries to their target folder
+        LinkBin{..} -> linkBin linkBinTargetDir
+        -- Set the executable flag of all package binaries
+        SetBinExecFlag -> setBinExecFlag . snd
+
+    -- | Read the binary files and return their names & paths.
+    readBinFiles :: NP.Bin -> ErrorLogger [(Text, FilePath)]
+    readBinFiles bin = case bin of
+      -- files with names how they should be linked
+      (NP.BinFiles bs) -> pure $ HML.toList bs
+      -- a whole folder where everything should be linked
+      (NP.BinFolder bf) -> do
+        dirM <- liftIO $ tryAccess (Dir.listDirectory bf)
+        case dirM of
+          Nothing -> do
+            warn ["Binary folder " <> toS (qte bf) <> " could not be accessed."]
+            pure []
+          (Just dir) ->
+            pure $ fmap (\f -> (toS f, bf FP.</> f)) dir
+
+    -- | Canonicalize the path.
+    canon :: FilePath -> ErrorLogger FilePath
+    canon fp = tryIOMsg
+      (\e -> "Couldn’t canonicalize path " <> qte fp <> ": " <> e)
+      (Dir.canonicalizePath fp)
+
+    -- | Canonicalize relative to our package
+    -- and ensure that the relative path is not outside.
+    canonPkg :: FilePath -> ErrorLogger FilePath
+    canonPkg relPath = do
+      pkgDir <- canon argsPackageDir
+      resPath <- canon $ argsPackageDir FP.</> relPath
+      when (not $ pkgDir `isPrefixOf` resPath)
+        $ throwError $ mconcat
+          [ "The link to executable file "
+          , qte relPath
+          , " lies outside of the package folder!\n"
+          , "That’s a security risk, aborting." ]
+      pure resPath
+
+    -- | Link a binary file to @targetDir/name@.
+    -- @relBinPath@ is relative from the package dir.
+    linkBin :: FilePath -> (Text, FilePath) -> ErrorLogger ()
+    linkBin targetDir_ (name, relBinPath) = do
+      binPath <- canonPkg relBinPath
+      targetDir <- canon targetDir_
+      tryIOMsg
+        (\e -> "Symlink could not be created: " <> e)
+        (PosixFiles.createSymbolicLink binPath $ targetDir FP.</> toS name)
+
+    -- | Set executable flag of the file.
+    setBinExecFlag :: FilePath -> ErrorLogger ()
+    setBinExecFlag file_ = do
+      file <- canonPkg file_
+      res <- liftIO $ tryAccess $ do
+        perm <- Dir.getPermissions file
+        Dir.setPermissions file
+          $ Dir.setOwnerExecutable True perm
+      case res of
+        Nothing ->
+          warn ["Cannot set executable bit on " <> toS file]
+        Just () -> pass
diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -19,6 +19,7 @@
 - Extremely fast.
 - Nice code that can be easily extended, new repositories introduced, adapt to new versions of the `yarn.lock` format.
 - Comes with a [nix library][nix-lib] that uses the power of overlays to make overriding dependencies possible.
+- POWERED BY [HNIX](https://github.com/haskell-nix/hnix)™ since before it was cool.
 
 Probably a few more.
 
@@ -55,12 +56,50 @@
 
 [nix-lib]: ./nix-lib/default.nix
 
-## Building
+## Building `yarn2nix`
 
 ```
 $ nix-build
 $ result/bin/yarn2nix
 ```
+
+## Using the generated nix files to build a project
+
+**Note:** This is a temporary interface. Ideally, the library will be in nixpkgs
+and yarn2nix will be callable from inside the build (so the resulting nix files
+don’t have to be checked in).
+
+Once you have the `yarn2nix` binary, use it to generate nix files for the
+`yarn.lock` file and the `package.json`:
+
+```shell
+$ yarn2nix ./jsprotect/yarn.lock > npm-deps.nix
+$ yarn2nix --template ./jsproject/package.json > npm-package.nix
+```
+
+Then use the library to assemble the generated files in a `default.nix`:
+
+```nix
+let
+  nixpkgsPath = <nixpkgs>;
+  pkgs = import nixpkgsPath {};
+  nixLib = pkgs.callPackage /path/to/yarn2nix/nix-lib {
+    # WARNING (TODO): for now you need to use this checked out yarn2nix
+    # because the upstream package (in haskellPackages) might have
+    # broken dependencies (yarn-lock and yarn2nix are not in stackage)
+    yarn2nix = import /path/to/yarn2nix { inherit nixpkgsPath; };
+  };
+
+in
+  nixLib.buildNodePackage
+    ( { src = nixLib.removePrefixes [ "node_modules" ] ./.; } //
+      nixLib.callTemplate ./npm-package.nix
+        (nixLib.buildNodeDeps (pkgs.callPackage ./npm-deps.nix {}))
+```
+
+Finally, run `nix-build`, and voilà, in `./result/` you find the project with
+all its dependencies correctly linked to their corresponding `node_modules`
+folder, recursively.
 
 ## Development
 
diff --git a/SetupNodePackagePaths.hs b/SetupNodePackagePaths.hs
deleted file mode 100644
--- a/SetupNodePackagePaths.hs
+++ /dev/null
@@ -1,116 +0,0 @@
-{-# LANGUAGE RecordWildCards, NoImplicitPrelude, LambdaCase, FlexibleContexts, NamedFieldPuns, OverloadedStrings #-}
-import Protolude
-import qualified Data.Text.IO as TIO
-import qualified Control.Monad.Except as ExcT
-import qualified Control.Exception as Exc
-import qualified System.IO.Error as IOErr
-import qualified Data.ByteString.Lazy as BL
-import qualified Data.HashMap.Lazy as HML
-import qualified System.FilePath as FP
-import qualified System.Directory as Dir
-import qualified System.Posix.Files as PosixFiles
-import Options.Applicative
-
-import qualified Distribution.Nodejs.Package as NP
-
-data Args
-  = Args { argMode :: Mode
-         , argTargetDir :: FilePath
-         , argPackageDir :: FilePath }
-
-data Mode = BinMode
-
-args :: Parser Args
-args = subparser
-    ( command "bin" (info (modeCommands BinMode)
-                    (progDesc "link package bin files")) )
-  where
-    modeCommands mode = Args
-      <$> pure mode
-      <*> strOption
-            ( long "to"
-           <> metavar "LINK_TARGET"
-           <> help "folder to link to (absolute or relative from package folder)" )
-      <*> strOption
-            ( long "package"
-           <> metavar "FOLDER"
-           <> help "folder with the node package" )
-  
-
-main :: IO ()
-main = execParser (info (args <**> helper)
-                    (progDesc "Link various files from npm packages to folders"))
-       >>= realMain
-
-type ErrorLogger = ExceptT [Char] IO
-
-realMain :: Args -> IO ()
-realMain Args{..} = do
-  let packageJsonPath = argPackageDir FP.</> "package.json"
-  unlessM (Dir.doesDirectoryExist argTargetDir)
-    $ die $ argTargetDir <> " is not a directory."
-  unlessM (Dir.doesFileExist packageJsonPath)
-    $ die $ packageJsonPath <> " does not exist."
-
-  runExceptT
-    (tryRead packageJsonPath >>= tryDecode packageJsonPath >>= go)
-    >>= \case
-      (Left err) -> die $ "ERROR: " <> err
-      (Right _) -> pass
-
-  where
-    tryIOMsg :: ([Char] -> [Char]) -> IO a -> ErrorLogger a
-    tryIOMsg errAnn = ExcT.withExceptT (errAnn . Exc.displayException) . tryIO
-
-    tryRead :: FilePath -> ErrorLogger BL.ByteString
-    tryRead fp = tryIOMsg exc $ BL.readFile fp
-      where exc e = fp <> " cannot be read:\n" <> e
-    tryDecode :: FilePath -> BL.ByteString -> ExceptT [Char] IO NP.Package
-    tryDecode fp fileBs = do
-      (pkg, warnings) <- NP.unLoggingPackage
-                      <$> ExcT.ExceptT (pure $ first exc $ NP.decode fileBs)
-      warn $ fmap NP.formatWarning warnings
-      pure pkg
-      where exc e = fp <> " cannot be decoded\n" <> toS e
-
-    qte s = "\"" <> s <> "\""
-    warn :: [Text] -> ErrorLogger ()
-    warn ws = for_ ws $ liftIO . TIO.hPutStrLn stderr
-
-    go :: NP.Package -> ErrorLogger ()
-    go NP.Package{bin} = case argMode of
-      BinMode -> traverse_ linkBin =<< case bin of
-        -- files with names how they should be linked
-        (NP.BinFiles bs) -> pure $ HML.toList bs
-        -- a whole folder where everything should be linked
-        (NP.BinFolder bf) -> do
-          dirE <- liftIO $ tryJust
-            (\e -> guard (IOErr.isDoesNotExistError e ||
-                          IOErr.isPermissionError e))
-            (Dir.listDirectory bf)
-          case dirE of
-            (Left _) -> do
-              warn ["Binary folder " <> toS (qte bf) <> " could not be accessed."]
-              pure []
-            (Right dir) ->
-              pure $ fmap (\f -> (toS f, bf FP.</> f)) dir
-
-    -- | Link a binary file to @targetDir/name@.
-    -- @relBinPath@ is relative from the package dir.
-    linkBin :: (Text, FilePath) -> ErrorLogger ()
-    linkBin (name, relBinPath) = do
-      let canon fp = tryIOMsg
-            (\e -> "Couldn’t canonicalize path " <> qte fp <> ": " <> e)
-            (Dir.canonicalizePath fp)
-      pkgDir <- canon argPackageDir
-      binPath <- canon $ argPackageDir FP.</> relBinPath
-      targetDir <- canon argTargetDir
-      when (not $ pkgDir `isPrefixOf` binPath)
-        $ throwError $ mconcat
-          [ "The link to executable file "
-          , qte relBinPath
-          , " lies outside of the package folder!\n"
-          , "That’s a security risk, aborting." ]
-      tryIOMsg
-        (\e -> "Symlink could not be created: " <> e)
-        (PosixFiles.createSymbolicLink binPath $ targetDir FP.</> toS name)
diff --git a/default.nix b/default.nix
--- a/default.nix
+++ b/default.nix
@@ -1,16 +1,31 @@
-(import <nixpkgs> {
+{ nixpkgsPath ? ./nixpkgs-pinned.nix }:
+(import nixpkgsPath {
   overlays = [(pkgs: oldpkgs: {
     haskellPackages =
       let nix-lib = pkgs.callPackage ./nix-lib {};
       in oldpkgs.haskellPackages.override {
-        overrides = self: super: {
-          inherit (pkgs.callPackage ./nix-lib/yarn-lock.nix {} self super) yarn-lock;
-          yarn2nix =
-            let pkg = self.callPackage ./yarn2nix.nix {};
-            in oldpkgs.haskell.lib.overrideCabal pkg (old: {
-              src = nix-lib.removePrefixes [ "nix-lib" "dist" "result" ] ./.;
-            });
-        };
+        overrides = oldpkgs.lib.composeExtensions
+          (pkgs.callPackage ./nix-lib/old-version-dependencies.nix {})
+          (self: super: {
+            yarn2nix =
+              let
+                pkg = oldpkgs.haskell.lib.overrideCabal
+                  (self.callPackage ./yarn2nix.nix {})
+                  (old: {
+                    prePatch = old.prePatch or "" + ''
+                      ${oldpkgs.lib.getBin self.hpack}/bin/hpack
+                      # we depend on the git prefetcher
+                      substituteInPlace \
+                        src/Distribution/Nixpkgs/Nodejs/ResolveLockfile.hs \
+                        --replace '"nix-prefetch-git"' \
+                          '"${pkgs.nix-prefetch-scripts}/bin/nix-prefetch-git"'
+                    '';
+                  });
+              in oldpkgs.haskell.lib.overrideCabal pkg (old: {
+                src = nix-lib.removePrefixes
+                  [ "nix-lib" "dist" "result" "tests/nix-tests" ] ./.;
+              });
+          });
       };
   })];
 
diff --git a/nix-lib/buildNodePackage.nix b/nix-lib/buildNodePackage.nix
--- a/nix-lib/buildNodePackage.nix
+++ b/nix-lib/buildNodePackage.nix
@@ -1,8 +1,8 @@
-{ stdenv, linkNodeDeps, nodejs }:
-{ name # String
+{ stdenv, linkNodeDeps, nodejs, yarn2nix }:
+{ key # { scope: String, name: String }
 , version # String
 , src # Drv
-, nodeBuildInputs # Listof { name : String, drv : Drv }
+, nodeBuildInputs # Listof { key: { scope: String, name: String }, drv : Drv }
 , ... }@args:
 
 # since we skip the build phase, pre and post will not work
@@ -13,8 +13,15 @@
 
 with stdenv.lib;
 
-stdenv.mkDerivation ((removeAttrs args [ "nodeBuildInputs" ]) // {
-  name = "${name}-${version}";
+let
+  # TODO: scope should be more structured somehow. :(
+  packageName =
+    if key.scope == ""
+    then "${key.name}-${version}"
+    else "${key.scope}-${key.name}-${version}";
+
+in stdenv.mkDerivation ((removeAttrs args [ "key" "nodeBuildInputs" ]) // {
+  name = packageName;
   inherit version src;
 
   buildInputs = [ nodejs ];
@@ -33,11 +40,19 @@
     # a npm package is just the tarball extracted to $out
     cp -r . $out
 
+    # the binaries should be executable (TODO: always on?)
+    ${yarn2nix}/bin/node-package-tool \
+      set-bin-exec-flag \
+      --package $out
+
     # then a node_modules folder is created for all its dependencies
     ${if nodeBuildInputs != []
       then ''
         rm -rf $out/node_modules
-        ln -sT "${linkNodeDeps name nodeBuildInputs}" $out/node_modules
+        ln -sT "${linkNodeDeps {
+            name = packageName;
+            dependencies = nodeBuildInputs;
+          }}" $out/node_modules
       '' else ""}
 
     runHook postInstall
diff --git a/nix-lib/default.nix b/nix-lib/default.nix
--- a/nix-lib/default.nix
+++ b/nix-lib/default.nix
@@ -1,21 +1,49 @@
-{ lib, pkgs }:
+{ lib, pkgs
+# TODO: temporary, to make overwriting yarn2nix easy
+# TODO: remove static building once RPATHs are fixed
+, yarn2nix ? pkgs.haskell.lib.justStaticExecutables
+               pkgs.haskellPackages.yarn2nix
+}:
 
 let
   # Build an attrset of node dependencies suitable for the `nodeBuildInputs`
-  # argument of `buildNodePackage`. The input is the path of a dependency
-  # file generated by the `yarn2nix` utility from a `yarn.lock` file,
-  # in turn the output of the `yarn` tool for an npm package.
-  buildNodeDeps = lockDotNix: lib.fix
+  # argument of `buildNodePackage`. The input is an overlay
+  # of node packages that call `_buildNodePackage`, like in the
+  # files generated by `yarn2nix`.
+  # It is possible to just call it with a generated file, like so:
+  # `buildNodeDeps (pkgs.callPackage ./npm-deps.nix {})`
+  # You can also use `lib.composeExtensions` to override packages
+  # in the set:
+  # ```
+  # buildNodeDeps (lib.composeExtensions
+  #   (pkgs.callPackage ./npm-deps.nix {})
+  #   (self: super: { pkg = super.pkg.override {…}; }))
+  # ```
+  # TODO: should _buildNodePackage be fixed in here?
+  buildNodeDeps = nodeDeps: lib.fix
     (lib.extends
-      (import lockDotNix { inherit (pkgs) fetchurl fetchgit; })
+      nodeDeps
       (self: {
-        # wrap the invocation in the fix point, to construct the
-        # list of { name, drv } needed by buildNodePackage
+        # The actual function building our packages.
+        # type: { key: String | { scope: String, name: String }
+        #       , <other arguments of ./buildNodePackage.nix> }
+        # Wraps the invocation in the fix point, to construct the
+        # list of { key, drv } needed by buildNodePackage
         # from the templates.
         # It is basically a manual paramorphism, carrying parts of the
-        # information of the previous layer (the original package name).
-        _buildNodePackage = { name, ... }@args:
-          { inherit name; drv = buildNodePackage args; };
+        # information of the previous layer (the original package key).
+        # TODO: move that function out of the package set
+        #       and get nice self/super scoping right
+        _buildNodePackage = { key, ... }@args:
+          # To keep the generated files shorter, we allow keys to
+          # be represented as strings if they have no scopes.
+          # This is the only place where this is accepted,
+          # but hacky nonetheless. Probably fix with above TODO.
+          let key' = if builtins.isString key
+                     then { scope = ""; name = key; }
+                     else key;
+          in { key = key';
+               drv = buildNodePackage (args // { key = key'; }); };
       }));
 
   # Build a package template generated by the `yarn2nix --template`
@@ -23,13 +51,11 @@
   # template nix file, the second input is all node dependencies
   # needed by the template, in the form generated by `buildNodeDeps`.
   callTemplate = yarn2nixTemplate: allDeps:
-    pkgs.callPackage yarn2nixTemplate {
-      inherit buildNodePackage removePrefixes;
-    } allDeps;
+    pkgs.callPackage yarn2nixTemplate {} allDeps;
 
 
   buildNodePackage = import ./buildNodePackage.nix {
-    inherit linkNodeDeps;
+    inherit linkNodeDeps yarn2nix;
     inherit (pkgs) stdenv nodejs;
   };
 
@@ -37,22 +63,40 @@
   # by npm’s module system to call dependencies.
   # Also link executables of all dependencies into `.bin`.
   # TODO: copy manpages & docs as well
-  # type: String -> ListOf { name: String, drv : Drv } -> Drv
-  linkNodeDeps = name: packageDeps:
-    pkgs.runCommand (name + "-node_modules") {} ''
+  # type: { name: String
+  #       , dependencies: ListOf { key: { scope: String, name: String }
+  #                              , drv : Drv } }
+  #       -> Drv
+  linkNodeDeps = {name, dependencies}:
+    pkgs.runCommand ("${name}-node_modules") {
+      # This just creates a simple link farm, which should be pretty fast,
+      # saving us from additional hydra requests for potentially hundreds
+      # of packages.
+      allowSubstitutes = false;
+      # Also tell Hydra it’s not worth copying to a builder.
+      preferLocalBuild = true;
+    } ''
       mkdir -p $out/.bin
       ${lib.concatMapStringsSep "\n"
-        (dep: ''
-          echo "linking node dependency ${dep.name}"
-          ln -sT ${dep.drv} "$out/${dep.name}"
-          ${ # TODO: remove static building once RPATHs are fixed
-             pkgs.haskell.lib.justStaticExecutables
-               pkgs.haskellPackages.yarn2nix}/bin/setup-node-package-paths \
-            bin \
-            --to=$out/.bin \
-            --package=$out/${dep.name}
-        '')
-        packageDeps}
+        (dep:
+          let
+            hasScope = dep.key.scope != "";
+            # scoped packages get another subdirectory for their scope (`@scope/`)
+            parentfolder = if hasScope
+              then "$out/@${dep.key.scope}"
+              else "$out";
+            subfolder = "${parentfolder}/${dep.key.name}";
+          in ''
+            echo "linking node dependency ${formatKey dep.key}"
+            ${ # we need to create the scope folder, otherwise ln fails
+               lib.optionalString hasScope ''mkdir "${parentfolder}"'' }
+            ln -sT ${dep.drv} "${subfolder}"
+            ${yarn2nix}/bin/node-package-tool \
+              link-bin \
+              --to=$out/.bin \
+              --package=${subfolder}
+          '')
+        dependencies}
     '';
 
   # Filter out files/directories with one of the given prefix names
@@ -65,6 +109,13 @@
     in
       builtins.filterSource (file: _: ! (hasAnyPrefix file)) path;
 
+  # format a package key of { scope: String, name: String }
+  formatKey = { scope, name }:
+    if scope == ""
+    then name
+    else "@${scope}/${name}";
+
 in {
-  inherit buildNodeDeps callTemplate removePrefixes;
+  inherit buildNodeDeps linkNodeDeps buildNodePackage
+          callTemplate removePrefixes;
 }
diff --git a/nix-lib/old-version-dependencies.nix b/nix-lib/old-version-dependencies.nix
new file mode 100644
--- /dev/null
+++ b/nix-lib/old-version-dependencies.nix
@@ -0,0 +1,20 @@
+{ lib, pkgs }:
+self: super: {
+  yarn-lock =
+    super.mkDerivation rec {
+      pname = "yarn-lock";
+      version = "0.5.0";
+      # src = ../../../haskell/yarn-lock;
+      src = pkgs.fetchFromGitHub {
+        owner = "Profpatsch";
+        repo = "yarn-lock";
+        rev = version;
+        # rev = "61cc65a858db92e7c0bea861bf279f286c34bb81";
+        sha256 = "007a7n1qqa7cp85bbw00yvkg2ikg1yzj1k8i9bav1bnbk6n7xp7s";
+      };
+      license = lib.licenses.mit;
+      buildDepends = with self; [ megaparsec protolude tasty-hunit tasty-th either neat-interpolation tasty-quickcheck quickcheck-instances ];
+      buildTools = [ self.hpack ];
+      prePatch = ''hpack'';
+    };
+}
diff --git a/nix-lib/yarn-lock.nix b/nix-lib/yarn-lock.nix
deleted file mode 100644
--- a/nix-lib/yarn-lock.nix
+++ /dev/null
@@ -1,19 +0,0 @@
-{ lib, pkgs }:
-self: super: {
-  yarn-lock =
-    super.mkDerivation rec {
-      pname = "yarn-lock";
-      version = "0.4.0";
-      # src = ../../haskell/yarn-lock;
-      src = pkgs.fetchFromGitHub {
-        owner = "Profpatsch";
-        repo = "yarn-lock";
-        rev = version;
-        sha256 = "0r5ipyfpdmg9n9zv7q1knrlypv9q2l1x24j1sgibxy411b7r62wv";
-      };
-      license = lib.licenses.mit;
-      buildDepends = with self; [ megaparsec protolude tasty-hunit tasty-th either neat-interpolation tasty-quickcheck ];
-      buildTools = [ self.hpack ];
-      preConfigure = ''hpack'';
-    };
-}
diff --git a/shell.nix b/shell.nix
--- a/shell.nix
+++ b/shell.nix
@@ -1,40 +1,39 @@
 with import <nixpkgs> {};
 (haskellPackages.override {
-  overrides = (self: super: {
-    inherit (pkgs.callPackage ./nix-lib/yarn-lock.nix {} self super)
-      yarn-lock;
-
-    my-pkg = let
-      buildDepends = with self; [
-        protolude
-        yarn-lock
-        hnix
-        hpack
-        aeson
-        async-pool
-        ansi-wl-pprint
-        regex-tdfa
-        regex-tdfa-text
-        neat-interpolation
-        tasty-th
-        tasty-quickcheck
-        tasty-hunit
-        http-client-tls
-        monad-par
-      ];
-      in super.mkDerivation {
-        pname = "pkg-env";
-        src = "/dev/null";
-        version = "none";
-        license = "none";
-        inherit buildDepends;
-        buildTools = with self; [
-          ghcid
-          cabal-install
-          (hoogleLocal {
-            packages = buildDepends;
-          })
+  overrides = lib.composeExtensions
+    (pkgs.callPackage ./nix-lib/old-version-dependencies.nix {})
+    (self: super: {
+      my-pkg = let
+        buildDepends = with self; [
+          protolude
+          yarn-lock
+          hnix
+          hpack
+          aeson
+          async-pool
+          ansi-wl-pprint
+          regex-tdfa
+          regex-tdfa-text
+          neat-interpolation
+          tasty-th
+          tasty-quickcheck
+          tasty-hunit
+          http-client-tls
+          monad-par
         ];
-      };
-    });
+        in super.mkDerivation {
+          pname = "pkg-env";
+          src = "/dev/null";
+          version = "none";
+          license = "none";
+          inherit buildDepends;
+          buildTools = with self; [
+            ghcid
+            cabal-install
+            (hoogleLocal {
+              packages = buildDepends;
+            })
+          ];
+        };
+      });
 }).my-pkg.env
diff --git a/src/Distribution/Nixpkgs/Nodejs/Cli.hs b/src/Distribution/Nixpkgs/Nodejs/Cli.hs
--- a/src/Distribution/Nixpkgs/Nodejs/Cli.hs
+++ b/src/Distribution/Nixpkgs/Nodejs/Cli.hs
@@ -77,7 +77,7 @@
         Left err -> die' ("Could not parse " <> toS path <> ":\n" <> show err)
 
 die' :: Text -> IO a
-die' err = putText err *> exitFailure
+die' err = putErrText err *> exitFailure
 dieWithUsage :: Text -> IO ()
 dieWithUsage err = die' (err <> "\n" <> usage)
 
diff --git a/src/Distribution/Nixpkgs/Nodejs/FromPackage.hs b/src/Distribution/Nixpkgs/Nodejs/FromPackage.hs
--- a/src/Distribution/Nixpkgs/Nodejs/FromPackage.hs
+++ b/src/Distribution/Nixpkgs/Nodejs/FromPackage.hs
@@ -18,21 +18,29 @@
 
 
 depsToPkgKeys :: NP.Dependencies -> [YLT.PackageKey]
-depsToPkgKeys = map (\(k, v) -> YLT.PackageKey k v) . HML.toList
+depsToPkgKeys = map toPkgKey . HML.toList
+  where
+    toPkgKey (k, v) =
+      YLT.PackageKey (parsePackageKeyName k) v
 
+parsePackageKeyName :: Text -> YLT.PackageKeyName
+parsePackageKeyName k =
+  -- we don’t crash on a “wrong” package key to keep this
+  -- code pure, but assume it’s a simple key instead.
+  maybe (YLT.SimplePackageKey k) identity
+    $ YLT.parsePackageKeyName k
+
 -- | generate a nix expression that translates your package.nix
 --
 -- and can serve as template for manual adjustments
 genTemplate :: NP.Package -> NExpr
 genTemplate NP.Package{..} =
-  simpleParamSet ["stdenv", "buildNodePackage", "filterSourcePrefixes"]
+  -- reserved for possible future arguments (to prevent breakage)
+  simpleParamSet []
   ==> Param nodeDepsSym
-  -- TODO: devDeps
-  ==> ("buildNodePackage" @@ mkRecSet
+  ==> (mkNonRecSet
         [ "name" $= nameStr
         , "version" $= mkStr version
-        , "src" $= ("filterSourcePrefixes"
-                     @@ mkList [ mkStr "node_modules" ] @@ "./.")
         , "nodeBuildInputs"  $= (letE "a" (mkSym nodeDepsSym)
                                   $ mkList (map (pkgDep "a") depPkgKeys))
         , "meta"      $= (mkNonRecSet
@@ -41,8 +49,16 @@
           <> may "homepage" homepage)
         ])
   where
-    depPkgKeys = depsToPkgKeys dependencies
+    -- TODO: The devDependencies are only needed for the build
+    -- and probably also only from packages not stemming from
+    -- a npm registry (e.g. a git package). It would be cool
+    -- if these dependencies were gone in the final output.
+    -- See https://github.com/Profpatsch/yarn2nix/issues/5
+    depPkgKeys = depsToPkgKeys (dependencies <> devDependencies)
     pkgDep depsSym pk = mkSym depsSym !!. packageKeyToSymbol pk
     nodeDepsSym = "allDeps"
-    nameStr = mkStrQ [StrQ name, "-", AntiQ "version"]
+    nameStr = mkStrQ [StrQ
+      $ pkgKeyToName $ parsePackageKeyName name]
+    pkgKeyToName (YLT.SimplePackageKey n) = n
+    pkgKeyToName (YLT.ScopedPackageKey s n) = s <> "-" <> n
     may k v = [k $= mkStr (fromMaybe mempty v)]
diff --git a/src/Distribution/Nixpkgs/Nodejs/OptimizedNixOutput.hs b/src/Distribution/Nixpkgs/Nodejs/OptimizedNixOutput.hs
--- a/src/Distribution/Nixpkgs/Nodejs/OptimizedNixOutput.hs
+++ b/src/Distribution/Nixpkgs/Nodejs/OptimizedNixOutput.hs
@@ -31,8 +31,8 @@
 import qualified Data.List as List
 import qualified Data.List.NonEmpty as NE
 
-import Nix.Expr (NExpr, ($=), (==>), (!.), (@@))
-import Nix.Expr.Additions (($$=), (!!.))
+import Nix.Expr (NExpr, ($=), (==>), (@@))
+import Nix.Expr.Additions (($$=), (!!.), inheritStatic)
 import qualified Nix.Expr as N
 import qualified Nix.Expr.Additions as NA
 
@@ -87,38 +87,53 @@
 
 -- | Package definition needed for calling the build function.
 data PkgData a = PkgData
-  { pkgDataName :: Text           -- ^ package name
-  , pkgDataVersion :: Text        -- ^ package version
-  , pkgDataUpstream :: a          -- ^ points to upstream
-  , pkgDataHashSum :: Text        -- ^ the hash sum of the package
-  , pkgDataDependencies :: [Text] -- ^ list of dependencies (as resolved nix symbols)
+  { pkgDataName :: YLT.PackageKeyName -- ^ package name
+  , pkgDataVersion :: Text            -- ^ package version
+  , pkgDataUpstream :: a              -- ^ points to upstream
+  , pkgDataHashSum :: Text            -- ^ the hash sum of the package
+  , pkgDataDependencies :: [Text]     -- ^ list of dependencies (as resolved nix symbols)
   }
 
-
+-- | Tuples of prefix string to registry
 registries :: [(Text, Registry)]
 registries =
   [ ( yarnP
     , Registry "yarn"
         $ \n v -> [T yarnP, V n, T "/-/", V n, T "-", V v, T ".tgz"] )
+  , ( npmjsP
+    , Registry "npm"
+        $ \n v -> [T npmjsP, V n, T "/-/", V n, T "-", V v, T ".tgz"] )
+
   ]
   where
     yarnP = "https://registry.yarnpkg.com/"
+    npmjsP = "https://registry.npmjs.org/"
 
 shortcuts :: M.Map [NSym] NSym
 shortcuts = M.fromList
   [ (["self"], "s")
   , (["registries", "yarn"], "y")
+  , (["registries", "npm"], "n")
   , (["nodeFilePackage"], "f")
   , (["nodeGitPackage"], "g")
   , (["identityRegistry"], "ir")
+  , (["scopedName"], "sc")
   ]
 
 -- | Find out which registry the given 'YLT.Remote' shortens to.
-recognizeRegistry :: Text -> Maybe Registry
-recognizeRegistry fileUrl = snd <$> filterRegistry fileUrl
+recognizeRegistry :: YLT.PackageKeyName -- ^ package name
+                  -> Text -- ^ url to file
+                  -> Maybe Registry
+-- We don’t shorten scoped key names, because
+-- they are handled specially by npm registries and
+-- the URLs differ from other packages
+recognizeRegistry (YLT.ScopedPackageKey{}) _ = Nothing
+recognizeRegistry _ fileUrl = snd <$> foundRegistry
   where
     -- | Get registry by the prefix of the registry’s URL.
-    filterRegistry url = find (\reg -> fst reg `T.isPrefixOf` url) registries
+    foundRegistry = find predicate registries
+    predicate :: (Text, Registry) -> Bool
+    predicate reg = fst reg `T.isPrefixOf` fileUrl
 
 
 -- | Convert a 'Res.ResolvedLockfile' to its final, nix-ready form.
@@ -147,7 +162,8 @@
         }
       def = case YLT.remote pkg of
         YLT.FileRemote{fileUrl} ->
-          PkgDefFile $ pkgDataGeneric $ note fileUrl $ recognizeRegistry fileUrl
+          PkgDefFile $ pkgDataGeneric $ note fileUrl
+            $ recognizeRegistry defName fileUrl
         YLT.GitRemote{gitRepoUrl, gitRev} ->
           PkgDefGit $ pkgDataGeneric $ Git gitRepoUrl gitRev
                  -- we don’t need another ref indirection
@@ -169,8 +185,6 @@
     yarn = n: v: "https://registry.yarnpkg.com/${n}/-/${n}-${v}.tgz";
   };
 
-  sanitizePackageName = builtins.replaceStrings ["@" "/"] ["-" "-"];
-
   # We want each package definition to be one line, by putting
   # the boilerplate into these functions for different remotes.
   nodeFilePackage = …
@@ -179,18 +193,23 @@
   # an identity function for e.g. git repos or unknown registries
   identityRegistry = url: _: _: url;
 
+  # a way to pass through scoped package names
+  scopedName = scope: name: { inherit scope name; }
+
   # shortcut section
   s = self;
   ir = identityRegistry;
   f = nodeFilePackage;
   g = nodeGitPackage;
   y = registries.yarnpkg;
+  sc = scopedName;
   …
 
 # the actual package definitions
 in {
   "accepts@~1.3.3" = s."accepts@1.3.3";
   "accepts@1.3.3" = f "accepts" "1.3.3" y "sha" [];
+  "@types/accepts@1.3.3" = f (sc "types" "accepts") "1.3.3" y "sha" [];
   "babel-core@^6.14.0" = s."babel-core@6.24.1";
   "babel-core@6.24.1" = f "babel-core" "6.24.1" y "a0e457c58ebdbae575c9f8cd75127e93756435d8" [
     s."accepts@~1.3.3"
@@ -210,10 +229,13 @@
     ==> N.Param "self" ==> N.Param "super"
     ==> N.mkLets
         (  [ "registries" $= N.mkNonRecSet (fmap (mkRegistry . snd) registries)
-           , "sanitizePackageName" $= sanitizePackageName
            , "nodeFilePackage" $= buildPkgFn
            , "nodeGitPackage" $= buildPkgGitFn
-           , "identityRegistry" $= NA.multiParam ["url", "_", "_"] "url" ]
+           , "identityRegistry" $= NA.multiParam ["url", "_", "_"] "url"
+           , "scopedName" $=
+               (NA.multiParam ["scope", "name"]
+                 $ N.mkNonRecSet [ inheritStatic ["scope", "name"] ])
+           ]
         <> fmap mkShortcut (M.toList shortcuts) )
         (N.mkNonRecSet (map mkPkg $ M.toAscList packages))
   where
@@ -222,26 +244,18 @@
 
     concatNSyms :: [NSym] -> NExpr
     concatNSyms [] = panic "non-empty shortcut syms!"
-    concatNSyms (l:ls) = foldl (!.) (N.mkSym $ unNSym l) (fmap unNSym ls)
+    concatNSyms (l:ls) = foldl (!!.) (N.mkSym $ unNSym l) (fmap unNSym ls)
     mkShortcut :: ([NSym], NSym) -> N.Binding NExpr
     mkShortcut (nSyms, short) = unNSym short $= concatNSyms nSyms
     -- | Try to shorten sym, otherwise use input.
     shorten :: [NSym] -> NExpr
     shorten s = maybe (concatNSyms s) (N.mkSym . unNSym) $ M.lookup s shortcuts
-
-    -- | remove symbols not allowed in nix derivation names
-    sanitizePackageName :: NExpr
-    sanitizePackageName = "builtins" !. "replaceStrings"
-                            @@ N.mkList [N.mkStr "@", N.mkStr "/"]
-                            @@ N.mkList [N.mkStr "-", N.mkStr "-"]
-
     -- | Build function boilerplate the build functions share in common.
     buildPkgFnGeneric :: [Text] -> NExpr -> NExpr
     buildPkgFnGeneric additionalArguments srcNExpr =
-      NA.multiParam (["name", "version"] <> additionalArguments <> ["deps"])
+      NA.multiParam (["key", "version"] <> additionalArguments <> ["deps"])
         $ ("super" !!. "_buildNodePackage") @@ N.mkNonRecSet
-          [ "name" $= ("sanitizePackageName" @@ "name")
-          , N.inherit $ map N.StaticKey ["version"]
+          [ inheritStatic ["key", "version"]
           , "src" $= srcNExpr
           , "nodeBuildInputs" $= "deps" ]
     -- | Building a 'YLT.FileRemote' package.
@@ -249,20 +263,22 @@
     buildPkgFn =
       buildPkgFnGeneric ["registry", "sha1"]
         ("fetchurl" @@ N.mkNonRecSet
-          [ "url" $= ("registry" @@ "name" @@ "version")
-          , N.inherit $ [N.StaticKey "sha1"] ])
+          [ "url" $= ("registry" @@ "key" @@ "version")
+          , inheritStatic ["sha1"] ])
     -- | Building a 'YLT.GitRemote' package.
     buildPkgGitFn :: NExpr
     buildPkgGitFn =
       buildPkgFnGeneric ["url", "rev", "sha256"]
         ("fetchgit" @@ N.mkNonRecSet
-          [ N.inherit $ map N.StaticKey ["url", "rev", "sha256"] ])
+          [ inheritStatic ["url", "rev", "sha256"] ])
 
     mkDefGeneric :: PkgData a -> NSym -> [NExpr] -> NExpr
     mkDefGeneric PkgData{..} buildFnSym additionalArguments =
       foldl' (@@) (shorten [buildFnSym])
-        $ [  N.mkStr pkgDataName
-          ,  N.mkStr pkgDataVersion ]
+        $ [ case pkgDataName of
+              YLT.SimplePackageKey n -> N.mkStr n
+              YLT.ScopedPackageKey s n -> "sc" @@ N.mkStr s @@ N.mkStr n
+          , N.mkStr pkgDataVersion ]
           <> additionalArguments <>
           [ N.mkList $ map (N.mkSym selfSym !!.) pkgDataDependencies ]
 
diff --git a/src/Distribution/Nixpkgs/Nodejs/ResolveLockfile.hs b/src/Distribution/Nixpkgs/Nodejs/ResolveLockfile.hs
--- a/src/Distribution/Nixpkgs/Nodejs/ResolveLockfile.hs
+++ b/src/Distribution/Nixpkgs/Nodejs/ResolveLockfile.hs
@@ -11,7 +11,7 @@
 ) where
 
 import Protolude
-import qualified Control.Monad.Trans.Either as E
+import qualified Control.Monad.Trans.Except as E
 import qualified Data.List.NonEmpty as NE
 import qualified Data.MultiKeyedMap as MKM
 import qualified Data.Aeson as Aeson
@@ -23,6 +23,9 @@
 
 import qualified Yarn.Lock.Types as YLT
 
+nixPrefetchGitPath :: FilePath
+nixPrefetchGitPath = "nix-prefetch-git"
+
 maxFetchers :: Int
 maxFetchers = 5
 
@@ -42,7 +45,7 @@
                       -> IO (Either (NE.NonEmpty Text) ResolvedLockfile)
 resolveLockfileStatus msgChan lf = Async.withTaskGroup maxFetchers $ \taskGroup -> do
   job <- STM.atomically $ Async.mapReduce taskGroup
-           $ fmap (\(ks, pkg) -> (:[]) <$> (E.runEitherT $ do
+           $ fmap (\(ks, pkg) -> (:[]) <$> (E.runExceptT $ do
                         liftIO $ writeChan msgChan (YLT.remote pkg)
                         res <- resolve pkg
                         pure (ks, res)))
@@ -53,20 +56,20 @@
     (_   , ys) -> pure $ Right $ MKM.fromList YLT.lockfileIkProxy ys
 
   where
-    resolve :: YLT.Package -> E.EitherT Text IO (Resolved YLT.Package)
+    resolve :: YLT.Package -> E.ExceptT Text IO (Resolved YLT.Package)
     resolve pkg = case YLT.remote pkg of
       YLT.FileRemote{..} -> pure $ r fileSha1
       YLT.GitRemote{..}  -> r <$> fetchFromGit gitRepoUrl gitRev
       where
         r sha = Resolved { hashSum = sha, resolved = pkg }
 
-    fetchFromGit :: Text -> Text -> E.EitherT Text IO Text
+    fetchFromGit :: Text -> Text -> E.ExceptT Text IO Text
     fetchFromGit repo rev = do
-      res <- liftIO $ Process.readProcessWithExitCode "nix-prefetch-git"
+      res <- liftIO $ Process.readProcessWithExitCode nixPrefetchGitPath
                ["--url", toS repo, "--rev", toS rev, "--hash", "sha256"] ""
       case res of
-        ((ExitFailure _), _, err) -> E.left $ toS err
-        (ExitSuccess, out, _) -> E.hoistEither
+        ((ExitFailure _), _, err) -> E.throwE $ toS err
+        (ExitSuccess, out, _) -> E.ExceptT . pure
           $ first (\decErr -> "parsing json output failed:\n"
                     <> toS decErr <> "\nThe output was:\n" <> toS out)
             $ do val <- Aeson.eitherDecode' (toS out)
diff --git a/src/Distribution/Nixpkgs/Nodejs/Utils.hs b/src/Distribution/Nixpkgs/Nodejs/Utils.hs
--- a/src/Distribution/Nixpkgs/Nodejs/Utils.hs
+++ b/src/Distribution/Nixpkgs/Nodejs/Utils.hs
@@ -1,4 +1,4 @@
-{-# LANGUAGE NoImplicitPrelude, OverloadedStrings, RecordWildCards #-}
+{-# LANGUAGE NoImplicitPrelude, OverloadedStrings, RecordWildCards, LambdaCase #-}
 {-|
 Description: Misc utils
 -}
@@ -8,4 +8,14 @@
 
 -- | Representation of a PackageKey as nix attribute name.
 packageKeyToSymbol :: YLT.PackageKey -> Text
-packageKeyToSymbol (YLT.PackageKey{..}) = name <> "@" <> npmVersionSpec
+packageKeyToSymbol (YLT.PackageKey{..}) =
+  packageKeyNameToSymbol name <> "@" <> npmVersionSpec
+{-# INLINABLE packageKeyToSymbol #-}
+
+-- | Representation of a PackageKeyName as nix attribute name.
+packageKeyNameToSymbol :: YLT.PackageKeyName -> Text
+packageKeyNameToSymbol = \case
+  YLT.SimplePackageKey n -> n
+  YLT.ScopedPackageKey scope n -> "@" <> scope <> "/" <> n
+{-# INLINABLE packageKeyNameToSymbol #-}
+
diff --git a/src/Distribution/Nodejs/Package.hs b/src/Distribution/Nodejs/Package.hs
--- a/src/Distribution/Nodejs/Package.hs
+++ b/src/Distribution/Nodejs/Package.hs
@@ -13,7 +13,7 @@
 , Bin(..), Man(..), Dependencies
 ) where
 
-import Protolude
+import Protolude hiding (packageName)
 import Control.Monad (fail)
 import qualified Control.Monad.Writer.Lazy as WL
 import qualified Data.ByteString.Lazy as BL
@@ -50,8 +50,9 @@
 -- | Possible warnings from parsing.
 data Warning
   = WrongType
-  { wrongTypeField :: Text
-  , wrongTypeDefault :: Text }
+  { wrongTypeField :: Text -- ^ the field which has a wrong type
+  , wrongTypeDefault :: Maybe Text -- ^ the default value, if used
+  }
   | PlainWarning Text
 
 -- | The package’s executable files.
@@ -71,23 +72,29 @@
 -- | Dependencies of a package.
 type Dependencies = HML.HashMap Text Text
 
+type Warn = WL.WriterT [Warning] AT.Parser
+putWarning :: a -> Warning -> Warn a
+putWarning a w = WL.writer (a, [w])
+
 -- | See https://github.com/npm/normalize-package-data for
 -- normalization steps used by npm itself.
 instance A.FromJSON LoggingPackage where
   parseJSON = A.withObject "Package" $ \v -> fmap LoggingPackage . WL.runWriterT $ do
     let
-      l :: AT.Parser a -> WL.WriterT [Warning] AT.Parser a
+      l :: AT.Parser a -> Warn a
       l = WL.WriterT . fmap (\a -> (a, []))
       tryWarn :: (AT.FromJSON a, Show a)
-              => Text -> a -> WL.WriterT [Warning] AT.Parser a
-      tryWarn field def = lift (v .:? field .!= def)
-                          <|> WL.writer (def, [WrongType field (show def)])
+              => Text -> a -> Warn a
+      tryWarn field def =
+        lift (v .:? field .!= def)
+        <|> putWarning def (WrongType { wrongTypeField = field
+                                       , wrongTypeDefault = Just (show def) })
     name            <- l $ v .:  "name"
     version         <- l $ v .:  "version"
     description     <- tryWarn "description" Nothing
     homepage        <- tryWarn "homepage" Nothing
     private         <- tryWarn "private" False
-    scripts         <- l $ v .:? "scripts" .!= mempty
+    scripts         <- (parseMapText "scripts" =<< (tryWarn "scripts" mempty))
     bin             <- parseBin name v
     man             <- l $ parseMan name v
     license         <- tryWarn "license" Nothing
@@ -96,7 +103,17 @@
     pure Package{..}
     where
 
-      parseBin :: Text -> AT.Object -> WL.WriterT [Warning] AT.Parser Bin
+      parseMapText :: Text -> HML.HashMap Text AT.Value
+                   -> Warn (HML.HashMap Text Text)
+      parseMapText fieldPath val =
+        HML.mapMaybe identity <$> HML.traverseWithKey tryParse val
+        where
+          tryParse :: Text -> A.Value -> Warn (Maybe Text)
+          tryParse key el = lift (Just <$> AT.parseJSON el)
+            <|> putWarning Nothing
+                  (WrongType { wrongTypeField = fieldPath <> "." <> key
+                             , wrongTypeDefault = Nothing })
+      parseBin :: Text -> AT.Object -> Warn Bin
       parseBin packageName v = do
         -- check for existence of these fields
         binVal <- lift $ optional $ v .: "bin"
@@ -105,8 +122,8 @@
         -- see npm documentation for more
         case (binVal, dirBinVal) of
           (Just _              , Just _) ->
-            WL.writer (BinFiles mempty, [PlainWarning
-              "`bin` and `directories.bin` must not exist at the same time."])
+            putWarning (BinFiles mempty) $ PlainWarning
+              "`bin` and `directories.bin` must not exist at the same time, skipping."
           -- either "bin" is a direct path, then it’s linked to the package name
           (Just (A.String path),      _) -> pure $ BinFiles
             $ HML.singleton packageName (toS path)
@@ -141,7 +158,13 @@
 
 -- | Convert a @package.json@ parsing warning to plain text.
 formatWarning :: Warning -> Text
-formatWarning = ("Warning: " <>) . \case
-  (WrongType field def) ->
-    "Field \"" <> field <> "\" has the wrong type. Defaulting to " <> def <> "."
+formatWarning = \case
+  WrongType{..} ->
+       "Field \""
+    <> wrongTypeField
+    <> "\" has the wrong type. "
+    <> (case wrongTypeDefault of
+         Just def -> "Defaulting to " <> def
+         Nothing  -> "Leaving it out")
+    <> "."
   (PlainWarning t) -> t
diff --git a/src/Nix/Expr/Additions.hs b/src/Nix/Expr/Additions.hs
--- a/src/Nix/Expr/Additions.hs
+++ b/src/Nix/Expr/Additions.hs
@@ -5,7 +5,7 @@
 Nix generation helpers. No guarantee of stability (internal!).
 -}
 module Nix.Expr.Additions
-( stringKey, ($$=), dynamicKey
+( stringKey, ($$=), dynamicKey, inheritStatic
 , simpleParamSet, multiParam
 , (!!.)
 , StrQ(..), mkStrQ, mkStrQI
@@ -24,7 +24,7 @@
 
 -- | Make a binding, but have the key be a string, not symbol.
 stringKey :: Text -> NExpr -> Binding NExpr
-stringKey k v = NamedVar [dynamicKey k] v
+stringKey k v = NamedVar (pure $ dynamicKey k) v nullPos
 -- | Infix version of 'stringKey'.
 ($$=) :: Text -> NExpr -> Binding NExpr
 ($$=) = stringKey
@@ -34,9 +34,13 @@
 dynamicKey :: Text -> NKeyName NExpr
 dynamicKey k = DynamicKey $ Plain $ DoubleQuoted [Plain k]
 
+-- | Inherit the given list of symbols.
+inheritStatic :: [Text] -> Binding e
+inheritStatic names = inherit (map StaticKey names) nullPos
+
 -- | shortcut to create a list of closed params, like @{ foo, bar, baz }:@
 simpleParamSet :: [Text] -> Params NExpr
-simpleParamSet = mkParamset . fmap (, Nothing)
+simpleParamSet prms = mkParamset (fmap (, Nothing) prms) False
 
 -- | shortcut to create a list of multiple params, like @a: b: c:@
 multiParam :: [Text] -> NExpr -> NExpr
@@ -49,7 +53,7 @@
 (!!.) :: NExpr -> Text -> NExpr
 aset !!. k = Fix
   $ NSelect aset
-      [(if isPlainSymbol k then StaticKey else dynamicKey) k] Nothing
+      (pure $ (if isPlainSymbol k then StaticKey else dynamicKey) k) Nothing
   where
     -- the nix lexer regex for IDs (symbols) is 
     -- [a-zA-Z\_][a-zA-Z0-9\_\'\-]*
@@ -73,4 +77,4 @@
 -- | Create a double-quoted string from a list of antiquotes/plain strings.
 mkStrQ = mkStrQtmpl DoubleQuoted
 -- | Create a single-quoted string from a list of antiquotes/plain strings.
-mkStrQI = mkStrQtmpl Indented
+mkStrQI = mkStrQtmpl (Indented 2)
diff --git a/tests/TestNpmjsPackage.hs b/tests/TestNpmjsPackage.hs
--- a/tests/TestNpmjsPackage.hs
+++ b/tests/TestNpmjsPackage.hs
@@ -1,4 +1,4 @@
-{-# LANGUAGE OverloadedStrings, TemplateHaskell, QuasiQuotes, NoImplicitPrelude #-}
+{-# LANGUAGE OverloadedStrings, TemplateHaskell, QuasiQuotes, NoImplicitPrelude, LambdaCase #-}
 module TestNpmjsPackage (tests) where
 
 import Protolude
@@ -21,34 +21,77 @@
 
 case_binPaths :: Assertion
 case_binPaths = do
-  parseSuccess bin (baseAnd [ ("bin", "./abc") ])
-    >>= assertEqual "bin path" (NP.BinFiles $ HML.fromList
-                           [ ("foopkg", "./abc") ])
-  parseSuccess bin (baseAnd [ ("directories", A.object [("bin", "./abc")]) ])
-    >>= assertEqual "bin path" (NP.BinFolder "./abc")
-  parseSuccess bin (baseAnd [ ("bin", A.object
-                           [ ("one", "./bin/one")
-                           , ("two", "imhere") ]) ])
-    >>= assertEqual "bin multiple" (NP.BinFiles $ HML.fromList
-                           [ ("one", "./bin/one")
-                           , ("two", "imhere") ])
-  -- TODO: succeeds with warning; test whether the correct warnings are generated
-  -- parseFailure bin (baseAnd [ ("bin", "foo")
-  --                           , ("directories", A.object [("bin", "foo")]) ])
-  parseSuccess bin (baseAnd [])
-    >>= assertEqual "no bins" (NP.BinFiles mempty)
-  where
-    bin = fmap (NP.bin . fst . NP.unLoggingPackage) . A.parseJSON
+  let
+    parseWithWarningsZoom :: (Eq a, Show a)
+                          => Text -> A.Value -> (NP.Package -> a) -> a
+                          -> ([NP.Warning] -> Assertion)
+                          -> Assertion
+    parseWithWarningsZoom name got zoom want warningPred =
+      NP.unLoggingPackage <$> parseSuccess got
+      >>= \(val, warnings) -> do
+              assertEqual (toS name) want (zoom val)
+              warningPred warnings
+    parseZoom name got zoom want =
+      parseWithWarningsZoom name got zoom want (const $ pure ())
 
-parseSuccess :: (A.Value -> AT.Parser a) -> A.Value -> IO a
-parseSuccess p v = case AT.parse p v of
+    hasWarning :: (NP.Warning -> Bool) -> [NP.Warning] -> Assertion
+    hasWarning warningPred = assertBool "no such warning!" . any warningPred
+    wrongType field def = \case
+      (NP.WrongType f d) -> field == f && def == d
+      _ -> False
+    plainWarning = \case
+      (NP.PlainWarning _) -> True
+      _ -> False
+
+  parseZoom ".bin exists with files"
+            (baseAnd [ ("bin", "./abc") ])
+            NP.bin
+            (NP.BinFiles $ HML.fromList [ ("foopkg", "./abc") ])
+
+  parseZoom ".directories.bin exists with path"
+            (baseAnd [ ("directories", A.object [("bin", "./abc")]) ])
+            NP.bin
+            (NP.BinFolder "./abc")
+
+  parseZoom "multiple .bin files are parsed"
+            (baseAnd [ ("bin", A.object
+                       [ ("one", "./bin/one")
+                       , ("two", "imhere") ]) ])
+            NP.bin
+            (NP.BinFiles $ HML.fromList
+              [ ("one", "./bin/one")
+              , ("two", "imhere") ])
+
+  parseWithWarningsZoom "bin and directories.bin both exist"
+                        (baseAnd [ ("bin", "foo")
+                                 , ("directories", A.object
+                                     [ ("bin", "foo") ]) ])
+                        NP.bin
+                        (NP.BinFiles mempty)
+                        (hasWarning plainWarning)
+
+  parseZoom "neither .bin nor .directories.bin exis"
+            (baseAnd [])
+            NP.bin
+            (NP.BinFiles mempty)
+
+  parseWithWarningsZoom ".scripts field has a wrong type"
+                        (baseAnd [ ("scripts", A.object
+                                   [ ("foo", A.object [])
+                                   , ("bar", "imascript") ]) ])
+                        NP.scripts
+                        (HML.fromList [ ("bar", "imascript") ])
+                        (hasWarning (wrongType "scripts.foo" Nothing))
+
+parseSuccess :: (A.FromJSON a) => A.Value -> IO a
+parseSuccess v = case A.fromJSON v of
   (AT.Error err) -> assertFailure err >> panic "not reached"
   (AT.Success a) -> pure a
   
-parseFailure :: Show a => (A.Value -> AT.Parser a) -> A.Value -> IO ()
-parseFailure p v = case AT.parse p v of
-  (AT.Error _) -> pass
-  (AT.Success a) -> assertFailure $ "no parse" <> show a
+-- parseFailure :: Show a => (A.Value -> AT.Parser a) -> A.Value -> IO ()
+-- parseFailure p v = case AT.parse p v of
+--   (AT.Error _) -> pass
+--   (AT.Success a) -> assertFailure $ "no parse" <> show a
 
 tests :: TestTree
 tests = $(testGroupGenerator)
diff --git a/yarn2nix.cabal b/yarn2nix.cabal
--- a/yarn2nix.cabal
+++ b/yarn2nix.cabal
@@ -1,9 +1,11 @@
--- This file has been generated from package.yaml by hpack version 0.18.1.
+-- This file has been generated from package.yaml by hpack version 0.27.0.
 --
 -- see: https://github.com/sol/hpack
+--
+-- hash: 9f5e62ecebb812a5457199df157d56a8529dd64b80a8ea3c7fbbbf22d86627f3
 
 name:           yarn2nix
-version:        0.5.0
+version:        0.7.0
 synopsis:       Convert yarn.lock files to nix expressions
 description:    Convert @yarn.lock@ files to nix expressions. See @yarn2nix@ executable. Contains a nix library to call the generated nix files in @nix-lib/@. Library functions and module names might be restructured in the future.
 category:       Distribution, Nix
@@ -21,7 +23,7 @@
     LICENSE
     nix-lib/buildNodePackage.nix
     nix-lib/default.nix
-    nix-lib/yarn-lock.nix
+    nix-lib/old-version-dependencies.nix
     README.md
     shell.nix
     yarn2nix.nix
@@ -31,30 +33,6 @@
   location: https://github.com/Profpatsch/yarn2nix
 
 library
-  hs-source-dirs:
-      src
-  ghc-options: -Wall
-  build-depends:
-      aeson >= 1.0 && < 1.3
-    , ansi-wl-pprint == 0.6.*
-    , async-pool == 0.9.*
-    , base == 4.*
-    , bytestring == 0.10.*
-    , containers >= 0.5 && < 0.7
-    , data-fix >= 0.0.7 && < 0.1
-    , directory == 1.3.*
-    , either == 4.4.*
-    , filepath == 1.4.*
-    , hnix == 0.3.*
-    , mtl == 2.2.*
-    , process == 1.4.*
-    , protolude == 0.1.*
-    , regex-tdfa == 1.2.*
-    , regex-tdfa-text == 1.0.0.*
-    , stm == 2.4.*
-    , text == 1.2.*
-    , unordered-containers == 0.2.*
-    , yarn-lock == 0.4.*
   exposed-modules:
       Distribution.Nixpkgs.Nodejs.Cli
       Distribution.Nixpkgs.Nodejs.FromPackage
@@ -65,98 +43,126 @@
       Nix.Expr.Additions
   other-modules:
       Paths_yarn2nix
+  hs-source-dirs:
+      src
+  ghc-options: -Wall
+  build-depends:
+      aeson >=1.0 && <1.3
+    , ansi-wl-pprint ==0.6.*
+    , async-pool ==0.9.*
+    , base ==4.*
+    , bytestring ==0.10.*
+    , containers >=0.5 && <0.7
+    , data-fix ==0.0.7 || ==0.2.0
+    , directory ==1.3.*
+    , filepath ==1.4.*
+    , hnix ==0.5.*
+    , mtl ==2.2.*
+    , process >=1.4
+    , protolude ==0.2.*
+    , regex-tdfa ==1.2.*
+    , regex-tdfa-text ==1.0.0.*
+    , stm ==2.4.*
+    , text ==1.2.*
+    , transformers ==0.5.*
+    , unordered-containers ==0.2.*
+    , yarn-lock ==0.5.*
   default-language: Haskell2010
 
-executable setup-node-package-paths
-  main-is: SetupNodePackagePaths.hs
+executable node-package-tool
+  main-is: NodePackageTool.hs
+  other-modules:
+      Paths_yarn2nix
   ghc-options: -Wall
   build-depends:
-      aeson >= 1.0 && < 1.3
-    , ansi-wl-pprint == 0.6.*
-    , async-pool == 0.9.*
-    , base == 4.*
-    , bytestring == 0.10.*
-    , containers >= 0.5 && < 0.7
-    , data-fix >= 0.0.7 && < 0.1
-    , directory == 1.3.*
-    , either == 4.4.*
-    , filepath == 1.4.*
-    , hnix == 0.3.*
-    , mtl == 2.2.*
-    , process == 1.4.*
-    , protolude == 0.1.*
-    , regex-tdfa == 1.2.*
-    , regex-tdfa-text == 1.0.0.*
-    , stm == 2.4.*
-    , text == 1.2.*
-    , unordered-containers == 0.2.*
-    , yarn-lock == 0.4.*
-    , optparse-applicative == 0.13.*
-    , unix == 2.7.*
+      aeson >=1.0 && <1.3
+    , ansi-wl-pprint ==0.6.*
+    , async-pool ==0.9.*
+    , base ==4.*
+    , bytestring ==0.10.*
+    , containers >=0.5 && <0.7
+    , data-fix ==0.0.7 || ==0.2.0
+    , directory ==1.3.*
+    , filepath ==1.4.*
+    , hnix ==0.5.*
+    , mtl ==2.2.*
+    , optparse-applicative >=0.13 && <0.15
+    , process >=1.4
+    , protolude ==0.2.*
+    , regex-tdfa ==1.2.*
+    , regex-tdfa-text ==1.0.0.*
+    , stm ==2.4.*
+    , text ==1.2.*
+    , transformers ==0.5.*
+    , unix ==2.7.*
+    , unordered-containers ==0.2.*
+    , yarn-lock ==0.5.*
     , yarn2nix
   default-language: Haskell2010
 
 executable yarn2nix
   main-is: Main.hs
+  other-modules:
+      Paths_yarn2nix
   ghc-options: -Wall
   build-depends:
-      aeson >= 1.0 && < 1.3
-    , ansi-wl-pprint == 0.6.*
-    , async-pool == 0.9.*
-    , base == 4.*
-    , bytestring == 0.10.*
-    , containers >= 0.5 && < 0.7
-    , data-fix >= 0.0.7 && < 0.1
-    , directory == 1.3.*
-    , either == 4.4.*
-    , filepath == 1.4.*
-    , hnix == 0.3.*
-    , mtl == 2.2.*
-    , process == 1.4.*
-    , protolude == 0.1.*
-    , regex-tdfa == 1.2.*
-    , regex-tdfa-text == 1.0.0.*
-    , stm == 2.4.*
-    , text == 1.2.*
-    , unordered-containers == 0.2.*
-    , yarn-lock == 0.4.*
+      aeson >=1.0 && <1.3
+    , ansi-wl-pprint ==0.6.*
+    , async-pool ==0.9.*
+    , base ==4.*
+    , bytestring ==0.10.*
+    , containers >=0.5 && <0.7
+    , data-fix ==0.0.7 || ==0.2.0
+    , directory ==1.3.*
+    , filepath ==1.4.*
+    , hnix ==0.5.*
+    , mtl ==2.2.*
+    , process >=1.4
+    , protolude ==0.2.*
+    , regex-tdfa ==1.2.*
+    , regex-tdfa-text ==1.0.0.*
+    , stm ==2.4.*
+    , text ==1.2.*
+    , transformers ==0.5.*
+    , unordered-containers ==0.2.*
+    , yarn-lock ==0.5.*
     , yarn2nix
   default-language: Haskell2010
 
 test-suite yarn2nix-tests
   type: exitcode-stdio-1.0
   main-is: Test.hs
+  other-modules:
+      TestNpmjsPackage
+      Paths_yarn2nix
   hs-source-dirs:
       tests
   ghc-options: -Wall
   build-depends:
-      aeson >= 1.0 && < 1.3
-    , ansi-wl-pprint == 0.6.*
-    , async-pool == 0.9.*
-    , base == 4.*
-    , bytestring == 0.10.*
-    , containers >= 0.5 && < 0.7
-    , data-fix >= 0.0.7 && < 0.1
-    , directory == 1.3.*
-    , either == 4.4.*
-    , filepath == 1.4.*
-    , hnix == 0.3.*
-    , mtl == 2.2.*
-    , process == 1.4.*
-    , protolude == 0.1.*
-    , regex-tdfa == 1.2.*
-    , regex-tdfa-text == 1.0.0.*
-    , stm == 2.4.*
-    , text == 1.2.*
-    , unordered-containers == 0.2.*
-    , yarn-lock == 0.4.*
-    , neat-interpolation == 0.3.*
-    , protolude == 0.1.*
-    , tasty == 0.11.*
-    , tasty-hunit == 0.9.*
-    , tasty-quickcheck == 0.8.*
-    , tasty-th == 0.1.7.*
+      aeson >=1.0 && <1.3
+    , ansi-wl-pprint ==0.6.*
+    , async-pool ==0.9.*
+    , base ==4.*
+    , bytestring ==0.10.*
+    , containers >=0.5 && <0.7
+    , data-fix ==0.0.7 || ==0.2.0
+    , directory ==1.3.*
+    , filepath ==1.4.*
+    , hnix ==0.5.*
+    , mtl ==2.2.*
+    , neat-interpolation ==0.3.*
+    , process >=1.4
+    , protolude ==0.2.*
+    , regex-tdfa ==1.2.*
+    , regex-tdfa-text ==1.0.0.*
+    , stm ==2.4.*
+    , tasty >=0.11 && <1.2
+    , tasty-hunit >=0.9 && <0.11
+    , tasty-quickcheck ==0.8.* || ==0.9.*
+    , tasty-th ==0.1.7.*
+    , text ==1.2.*
+    , transformers ==0.5.*
+    , unordered-containers ==0.2.*
+    , yarn-lock ==0.5.*
     , yarn2nix
-  other-modules:
-      TestNpmjsPackage
   default-language: Haskell2010
diff --git a/yarn2nix.nix b/yarn2nix.nix
--- a/yarn2nix.nix
+++ b/yarn2nix.nix
@@ -8,7 +8,7 @@
 }:
 mkDerivation {
   pname = "yarn2nix";
-  version = "0.1.0";
+  version = "0.6.0";
   src = ./.;
   isLibrary = true;
   isExecutable = true;
