diff --git a/Main.hs b/Main.hs
--- a/Main.hs
+++ b/Main.hs
@@ -1,10 +1,7 @@
+{-# LANGUAGE OverloadedStrings #-}
 module Main where
 
 import Distribution.Nixpkgs.Nodejs.Cli (cli)
-import qualified Data.Text as T
-import System.Environment (getArgs)
 
 main :: IO ()
-main = getArgs >>= cli . map T.pack
-
-
+main = cli
diff --git a/NodePackageTool.hs b/NodePackageTool.hs
--- a/NodePackageTool.hs
+++ b/NodePackageTool.hs
@@ -1,17 +1,19 @@
 {-# LANGUAGE RecordWildCards, NoImplicitPrelude, LambdaCase, FlexibleContexts, NamedFieldPuns, OverloadedStrings #-}
 import Protolude
+import qualified Data.Text as T
 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
+import qualified Data.Aeson.KeyMap as KeyMap
+import qualified Data.Aeson.Key as Key
 
 data Args
   = Args
@@ -44,8 +46,8 @@
         <> 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.
@@ -115,7 +117,7 @@
     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
+      (NP.BinFiles bs) -> pure $ (bs & KeyMap.toList <&> first Key.toText)
       -- a whole folder where everything should be linked
       (NP.BinFolder bf) -> do
         dirM <- liftIO $ tryAccess (Dir.listDirectory bf)
@@ -149,12 +151,24 @@
     -- | Link a binary file to @targetDir/name@.
     -- @relBinPath@ is relative from the package dir.
     linkBin :: FilePath -> (Text, FilePath) -> ErrorLogger ()
-    linkBin targetDir_ (name, relBinPath) = do
+    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)
+      (name, targetDir) <- traverse canon $
+        symlinkTarget name_ targetDir_
+      tryIOMsg (\e -> "Directory could not be created: " <> e) $
+        Dir.createDirectoryIfMissing False targetDir
+      tryIOMsg (\e -> "Symlink could not be created: " <> e) $
+        PosixFiles.createSymbolicLink binPath $ targetDir FP.</> toS name
+
+    -- | Given a name and a target directory, return
+    --   the basename and the target (sub) directory
+    --   of the target file
+    symlinkTarget :: Text -> FilePath -> (FilePath, FilePath)
+    symlinkTarget name targetDir =
+      if "/" `T.isInfixOf` name
+        then (FP.takeFileName name', targetDir FP.</> FP.takeDirectory name')
+        else (name', targetDir)
+      where name' = T.unpack name
 
     -- | Set executable flag of the file.
     setBinExecFlag :: FilePath -> ErrorLogger ()
diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -1,12 +1,22 @@
+
+ATTENTION: You are not looking at the `yarn2nix` as packaged in `nixpkgs`. This is an alternative implementation, with different tradeoffs.
+For an overview of the history of these tools and current options, see [this nixpkgs issue](https://github.com/NixOS/nixpkgs/issues/20637).
+
+I currently don’t have much time to maintain this project, it should work for some cases, but has not been “battle-tested” very much.
+
 # yarn2nix
 
 ```
-yarn2nix [path/to/yarn.lock]
+yarn2nix [--offline] [path/to/yarn.lock]
+
+  Convert a `yarn.lock` into a synonymous nix expression.
+  If no path is given, search for `./yarn.lock`.
+  If --offline is given, abort if figuring out a hash
+  requires network access.
+
 yarn2nix --template [path/to/package.json]
 
-Convert a `yarn.lock` into a synonymous nix expression.
-If no path is given, search for `./yarn.lock`.
-In the second invocation generate a template for your `package.json`.
+  Generate a package template nix-expression for your `package.json`.
 ```
 
 ## Features
@@ -25,33 +35,34 @@
 
 ## Example Output
 
-The [HackMD](https://github.com/hackmdio/hackmd) project is an elaborate npm package with hundreds of
-dependencies. `yarn2nix` flawlessly parses the current (2017-12) `yarn.lock`
+The [CodiMD server](https://github.com/codimd/server) is an elaborate npm package with hundreds of
+dependencies. `yarn2nix` flawlessly parses the current (2020-07) `yarn.lock`
 file distributed with the project, including resolving their manual git forks of
 multiple npm packages:
 
 ```
-dist/build/yarn2nix/yarn2nix ~/tmp/hackmd/yarn.lock | wc
-   5306   17068  280246
-cat ~/tmp/hackmd/yarn.lock | wc
-   7667   11307  266652
+$ yarn2nix ~/tmp/server/yarn.lock | wc
+   7320   22701  399111
+$ wc ~/tmp/server/yarn.lock
+ 11938  18615 500078 /home/lukas/tmp/server/yarn.lock
 ```
 
 The output of this conversion [can be seen
-here](https://gist.github.com/Profpatsch/9e50d25faf5a5c4269566e9b7d89199b). Also
+here](https://gist.github.com/sternenseemann/0c253305350b2406e38c700b840869f2). Also
 note that [git dependencies are resolved
-correctly](https://gist.github.com/Profpatsch/9e50d25faf5a5c4269566e9b7d89199b#file-hackmd-dependencies-nix-L1291).
+correctly](https://gist.github.com/sternenseemann/0c253305350b2406e38c700b840869f2#file-codimd-dependencies-nix-L2086-L2087).
 
 Pushing it through the provided [library of nix
-functions][nix-lib], we get a complete build of HackMD
+functions][nix-lib], we get a complete build of CodiMD's
 dependencies, using the project template (generated with `--template`), we also
-build HackMD. Included executables will be in `node_modules/.bin` as expected and
+build the CodiMD server. Included executables will be in `node_modules/.bin` as expected and
 correctly link to their respective library paths in the nix store, for example:
 
 ```
- ls /nix/store/2jc8b4q9i2cvx7pamv5r8md45prrvx4f-hackmd-0.5.1-0.5.1/node_modules/.bin/markdown-it --help
-Usage: ls [OPTION]... [FILE]...
-List information about the FILEs (the current directory by default).
+$ /nix/store/zs9jk7yhdxsasn26m0903fq89cmyllzv-CodiMD-1.6.0/node_modules/.bin/markdown-it -v
+10.0.0
+$ readlink /nix/store/zs9jk7yhdxsasn26m0903fq89cmyllzv-CodiMD-1.6.0/node_modules/.bin/markdown-it
+/nix/store/bgas2l5izznq1b61a3jyf3gpb73x8chn-markdown-it-10.0.0/bin/markdown-it.js
 ```
 
 [nix-lib]: ./nix-lib/default.nix
@@ -81,30 +92,34 @@
 
 ```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; };
-  };
+  pkgs = import <nixpkgs> {};
+  yarn2nix = import /path/to/yarn2nix {};
+  nixLib = yarn2nix.nixLib;
 
 in
   nixLib.buildNodePackage
     ( { src = nixLib.removePrefixes [ "node_modules" ] ./.; } //
       nixLib.callTemplate ./npm-package.nix
-        (nixLib.buildNodeDeps (pkgs.callPackage ./npm-deps.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.
 
+## Using private package repository
+
+Since `yarn2nix` uses standard `fetchurl` to download packages,
+it is possible to authenticate by overriding `fetchurl`
+to use the access credentials in `/etc/nix/netrc`.
+
+Refer to the [Enterprise NixOS Wiki article](https://nixos.wiki/wiki/Enterprise)
+for instructions.
+
 ## Development
 
 ```
 $ nix-shell
-nix-shell> hpack
-nix-shell> cabal build
+nix-shell> ninja
+nix-shell> cabal build yarn2nix
 ```
diff --git a/default.nix b/default.nix
deleted file mode 100644
--- a/default.nix
+++ /dev/null
@@ -1,32 +0,0 @@
-{ nixpkgsPath ? ./nixpkgs-pinned.nix }:
-(import nixpkgsPath {
-  overlays = [(pkgs: oldpkgs: {
-    haskellPackages =
-      let nix-lib = pkgs.callPackage ./nix-lib {};
-      in oldpkgs.haskellPackages.override {
-        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" ] ./.;
-              });
-          });
-      };
-  })];
-
-}).haskellPackages.yarn2nix
diff --git a/nix-lib/buildNodePackage.nix b/nix-lib/buildNodePackage.nix
--- a/nix-lib/buildNodePackage.nix
+++ b/nix-lib/buildNodePackage.nix
@@ -1,4 +1,4 @@
-{ stdenv, linkNodeDeps, nodejs, yarn2nix }:
+{ lib, stdenv, linkNodeDeps, nodejs, yarn2nix }:
 { key # { scope: String, name: String }
 , version # String
 , src # Drv
@@ -11,7 +11,7 @@
 # same for configurePhase
 assert (args ? preConfigure || args ? postConfigure) -> args ? configurePhase;
 
-with stdenv.lib;
+with lib;
 
 let
   # TODO: scope should be more structured somehow. :(
diff --git a/nix-lib/default.nix b/nix-lib/default.nix
--- a/nix-lib/default.nix
+++ b/nix-lib/default.nix
@@ -56,7 +56,7 @@
 
   buildNodePackage = import ./buildNodePackage.nix {
     inherit linkNodeDeps yarn2nix;
-    inherit (pkgs) stdenv nodejs;
+    inherit (pkgs) stdenv lib nodejs;
   };
 
   # Link together a `node_modules` folder that can be used
@@ -109,6 +109,46 @@
     in
       builtins.filterSource (file: _: ! (hasAnyPrefix file)) path;
 
+  # `callYarnLock` calls `yarn2nix` to generate a nix representation of
+  # a `yarn.lock` file and directly imports it. It uses `yarn2nix`'s
+  # offline mode, so its resolving capabilities are limited, i. e. git
+  # dependencies are not possible.
+  #
+  # Example usage:
+  #
+  # ```
+  # buildNodeDeps (callYarnLock ./yarn.lock {})
+  # ```
+  callYarnLock = yarnLock: { name ? "yarn.lock.nix" }:
+    pkgs.callPackage (pkgs.runCommand name {
+      # faster to build locally, see also note at linkNodeDeps
+      allowSubstitutes = false;
+      preferLocalBuild = true;
+    } ''
+      ${yarn2nix}/bin/yarn2nix --offline ${yarnLock} > $out
+    '') { };
+
+  # `callPackageJson` calls `yarn2nix --template` to generate a
+  # nix representation of a `package.json` and directly imports
+  # it. It returns a function which expects a dependency attrset
+  # like `callYarnLock` generates.
+  #
+  # Example usage:
+  #
+  # ```
+  # let template = callPackageJson ./package.json {};
+  # in buildNodePackage ({ src = ./.; } //
+  #   template (callYarnLock ./yarn.lock {}))
+  # ```
+  callPackageJson = packageJson: { name ? "package.json.nix" }:
+    pkgs.callPackage (pkgs.runCommand name {
+      # faster to build locally, see also note at linkNodeDeps
+      allowSubstitutes = false;
+      preferLocalBuild = true;
+    } ''
+      ${yarn2nix}/bin/yarn2nix --template ${packageJson} > $out
+    '') {};
+
   # format a package key of { scope: String, name: String }
   formatKey = { scope, name }:
     if scope == ""
@@ -117,5 +157,6 @@
 
 in {
   inherit buildNodeDeps linkNodeDeps buildNodePackage
-          callTemplate removePrefixes;
+          callTemplate removePrefixes callYarnLock
+          callPackageJson;
 }
diff --git a/nix-lib/old-version-dependencies.nix b/nix-lib/old-version-dependencies.nix
deleted file mode 100644
--- a/nix-lib/old-version-dependencies.nix
+++ /dev/null
@@ -1,19 +0,0 @@
-{ lib, pkgs }:
-self: super: {
-  yarn-lock =
-    super.mkDerivation rec {
-      pname = "yarn-lock";
-      version = "0.6.2";
-      # src = ../../../haskell/yarn-lock;
-      src = pkgs.fetchFromGitHub {
-        owner = "Profpatsch";
-        repo = "yarn-lock";
-        rev = version;
-        sha256 = "06171ya075yx88gfx39z6mh1k1al0qaqrarbas5mv6lrky19bdxs";
-      };
-      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/shell.nix b/shell.nix
deleted file mode 100644
--- a/shell.nix
+++ /dev/null
@@ -1,39 +0,0 @@
-with import <nixpkgs> {};
-(haskellPackages.override {
-  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
@@ -1,4 +1,5 @@
 {-# LANGUAGE OverloadedStrings, LambdaCase, NoImplicitPrelude #-}
+{-# LANGUAGE TypeApplications #-}
 {-|
 Description: command line interface
 -}
@@ -11,10 +12,13 @@
 import qualified Data.ByteString.Lazy as BL
 import qualified Data.Text as T
 import qualified Data.Text.IO as TIO
+import qualified Options.Applicative as O
+import qualified Options.Applicative.Help.Pretty as O (linebreak)
 import qualified System.Directory as Dir
+import System.Environment (getProgName)
 
 import qualified Nix.Pretty as NixP
-import Data.Text.Prettyprint.Doc.Render.Text (putDoc)
+import qualified Prettyprinter.Render.Text as RT
 import qualified Yarn.Lock as YL
 import qualified Yarn.Lock.Types as YLT
 import qualified Yarn.Lock.Helpers as YLH
@@ -23,75 +27,161 @@
 import qualified Distribution.Nixpkgs.Nodejs.FromPackage as NodeFP
 import qualified Distribution.Nixpkgs.Nodejs.ResolveLockfile as Res
 import qualified Distribution.Nodejs.Package as NP
+import Distribution.Nixpkgs.Nodejs.ResolveLockfile (ResolverConfig(ResolverConfig, resolveOffline))
+import Distribution.Nixpkgs.Nodejs.License (LicensesBySpdxId)
+import qualified Data.Aeson as Json
 
-usage :: Text
-usage = mconcat $ intersperse "\n"
-  [ "yarn2nix [path/to/yarn.lock]"
-  , "yarn2nix --template [path/to/package.json]"
-  , ""
-  , "Convert a `yarn.lock` into a synonymous nix expression."
-  , "If no path is given, search for `./yarn.lock`."
-  , "In the second invocation generate a template for your `package.json`."
-  ]
 
-data Mode = Yarn | Node
-fileFor :: Mode -> Text
-fileFor Yarn = "yarn.lock"
-fileFor Node = "package.json"
+description :: O.InfoMod a
+description = O.fullDesc
+  <> O.progDescDoc (Just $ mconcat $ intersperse O.linebreak
+   [ "yarn2nix has two modes:"
+   <> O.linebreak
+   , "In its default mode (started without --template) it parses a given yarn.lock file"
+   , "and prints a nix expressions representing it to stdout."
+   <> O.linebreak
+   , "If --template is given, it processes a given package.json"
+   , "and prints a template nix expression for an equivalent nix package."
+   <> O.linebreak
+   , "In both modes yarn2nix will take the file as an argument"
+   , "or read it from stdin if it is missing."
+   ])
 
 -- | Main entry point for @yarn2nix@.
-cli :: [Text] -> IO ()
-cli = \case
-  ["--help"] -> putText usage
-  ("--template":xs) -> fileLogic Node xs
-  xs -> fileLogic Yarn xs
+cli :: IO ()
+cli = parseOpts >>= runAction
+
+-- | Type of action @yarn2nix@ is performing.
+data RunMode
+  = YarnLock     -- ^ Output a nix expression for a @yarn.lock@
+  | NodeTemplate -- ^ Output a nix template corresponding to a @package.json@
+  deriving (Show, Eq)
+
+-- | Runtime configuration of @yarn2nix@. Typically this is determined from
+--   its command line arguments and valid for the current invocation only.
+data RunConfig
+  = RunConfig
+  { runMode         :: RunMode
+  , runOffline      :: Bool            -- ^ If @True@, @yarn2nix@ will fail if it
+                                       --   requires network access. Currently this means
+                                       --   'Distribution.Nixpkgs.Nodejs.ResolveLockfile.resolveLockfileStatus'
+                                       --   will throw an error in case resolving a hash
+                                       --   requires network access.
+  , runLicensesJson :: Maybe FilePath  -- ^ Optional Path to a licenses.json file
+                                       --   equivalent to the lib.licenses set from
+                                       --   @nixpkgs@.
+  , runInputFile    :: Maybe FilePath  -- ^ File to process. If missing the appropriate
+                                       --   file for the current mode from the current
+                                       --   working directory is used.
+  } deriving (Show, Eq)
+
+
+fileFor :: RunConfig -> Text
+fileFor cfg =
+  case runMode cfg of
+    YarnLock -> "yarn.lock"
+    NodeTemplate -> "package.json"
+
+parseOpts :: IO RunConfig
+parseOpts = O.customExecParser optparsePrefs runConfigParserWithHelp
+
+runAction :: RunConfig -> IO ()
+runAction cfg = do
+  file <- fileForConfig
+  case runMode cfg of
+    YarnLock -> parseYarn file
+    NodeTemplate -> parseNode file
   where
-    fileLogic :: Mode -> [Text] -> IO ()
-    fileLogic mode = \case
-      [] -> Dir.getCurrentDirectory >>= \d ->
-          Dir.findFile [d] (toS $ fileFor mode) >>= \case
-            Nothing -> do
-              dieWithUsage $ "No " <> fileFor mode <> " found in current directory"
-            Just path  -> parseFile mode path
-      [path] -> parseFile mode (toS path)
-      _ -> dieWithUsage ""
-    parseFile :: Mode -> FilePath -> IO ()
-    parseFile Yarn = parseYarn
-    parseFile Node = parseNode
+    fileForConfig :: IO FilePath
+    fileForConfig =
+      case runInputFile cfg of
+        Just f -> pure f
+        Nothing -> Dir.getCurrentDirectory >>= \d ->
+          Dir.findFile [d] (toS $ fileFor cfg) >>= \case
+            Nothing -> dieWithUsage
+              $ "No " <> fileFor cfg <> " found in current directory"
+            Just path -> pure path
     parseYarn :: FilePath -> IO ()
     parseYarn path = do
-      let pathT = toS path
-      fc <- readFile path
-        `catch` \e
-          -> do dieWithUsage ("Unable to open " <> pathT <> ":\n" <> show (e :: IOException))
-                pure ""
+      fc <- catchCouldNotOpen path $ readFile path
       case YL.parse path fc of
-        Right yarnfile  -> toStdout yarnfile
-        Left err -> die' ("Could not parse " <> pathT <> ":\n" <> show err)
+        Right yarnfile  -> toStdout cfg yarnfile
+        Left err -> die' ("Could not parse " <> toS path <> ":\n" <> show err)
+
     parseNode :: FilePath -> IO ()
     parseNode path = do
       NP.decode <$> BL.readFile path >>= \case
         Right (NP.LoggingPackage (nodeModule, warnings)) -> do
           for_ warnings $ TIO.hPutStrLn stderr . NP.formatWarning
-          print $ NixP.prettyNix $ NodeFP.genTemplate nodeModule
+
+          licenseSet <- case cfg & runLicensesJson of
+            Nothing -> pure Nothing
+            Just licensesJson -> do
+              catchCouldNotOpen licensesJson
+                (BL.readFile licensesJson)
+                <&> Json.decode @LicensesBySpdxId
+
+          print $ NixP.prettyNix $ NodeFP.genTemplate licenseSet nodeModule
         Left err -> die' ("Could not parse " <> toS path <> ":\n" <> show err)
+    catchCouldNotOpen :: FilePath -> IO a -> IO a
+    catchCouldNotOpen path action = action `catch` \e ->
+      dieWithUsage $ "Could not open " <> toS path <> ":\n" <> show (e :: IOException)
 
+-- get rid of odd linebreaks by increasing width enough
+optparsePrefs :: O.ParserPrefs
+optparsePrefs = O.defaultPrefs { O.prefColumns = 100 }
+
+-- If --template is given, run in NodeTemplate mode,
+-- otherwise the default mode YarnLock is used.
+runModeParser :: O.Parser RunMode
+runModeParser = O.flag YarnLock NodeTemplate $
+     O.long "template"
+  <> O.help "Output a nix package template for a given package.json"
+
+runConfigParser :: O.Parser RunConfig
+runConfigParser = RunConfig
+  <$> runModeParser
+  <*> O.switch
+      (O.long "offline"
+    <> O.help "Makes yarn2nix fail if network access is required")
+  <*> O.optional (O.option O.str
+     (O.long "license-data"
+   <> O.metavar "FILE"
+   <> O.help "Path to a license.json equivalent to nixpkgs.lib.licenses"
+   -- only really interesting for wrapping at build
+   <> O.internal))
+  <*> O.optional (O.argument O.str (O.metavar "FILE"))
+
+runConfigParserWithHelp :: O.ParserInfo RunConfig
+runConfigParserWithHelp =
+  O.info (runConfigParser <**> O.helper) description
+
 die' :: Text -> IO a
 die' err = putErrText err *> exitFailure
-dieWithUsage :: Text -> IO ()
-dieWithUsage err = die' (err <> "\n" <> usage)
 
+dieWithUsage :: Text -> IO a
+dieWithUsage err = do
+  putErrText (err <> "\n")
+  progn <- getProgName
+  hPutStr stderr
+    . fst . flip O.renderFailure progn
+    $ O.parserFailure optparsePrefs
+        runConfigParserWithHelp (O.ShowHelpText Nothing) mempty
+  exitFailure
 
 -- TODO refactor
-toStdout :: YLT.Lockfile -> IO ()
-toStdout lf = do
+toStdout :: RunConfig -> YLT.Lockfile -> IO ()
+toStdout cfg lf = do
   ch <- newChan
   -- thrd <- forkIO $ forever $ do
   --   readChan ch >>= \case
   --     FileRemote{..} -> pass
   --     GitRemote{..} -> print $ "Downloading " <> gitRepoUrl
-  lf' <- Res.resolveLockfileStatus ch (YLH.decycle lf) >>= \case
+  let resolverConfig = ResolverConfig {
+    resolveOffline = cfg & runOffline
+   }
+  lf' <- Res.resolveLockfileStatus resolverConfig ch (YLH.decycle lf) >>= \case
     Left err -> die' (T.intercalate "\n" $ toList err)
     Right res -> pure res
   -- killThread thrd
-  putDoc $ NixP.prettyNix $ NixOut.mkPackageSet $ NixOut.convertLockfile lf'
+  RT.putDoc $ NixP.prettyNix $ NixOut.mkPackageSet $ NixOut.convertLockfile lf'
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
@@ -1,4 +1,4 @@
-{-# LANGUAGE NoImplicitPrelude, DeriveGeneric, OverloadedStrings, RecordWildCards #-}
+{-# LANGUAGE NoImplicitPrelude, OverloadedStrings, RecordWildCards #-}
 {-|
 Description: Generate nix expression for 'NP.Package'
 -}
@@ -7,46 +7,44 @@
 ) where
 
 import Protolude
-import qualified Data.HashMap.Lazy as HML
 
 import Nix.Expr
 import Nix.Expr.Additions
 
-import Distribution.Nixpkgs.Nodejs.Utils (packageKeyToSymbol)
+import Distribution.Nixpkgs.Nodejs.Utils (packageKeyToSymbol, attrSetMayStr, attrSetMay)
 import qualified Distribution.Nodejs.Package as NP
+import qualified Distribution.Nixpkgs.Nodejs.License as NL
 import qualified Yarn.Lock.Types as YLT
-
+import qualified Data.Aeson.KeyMap as KeyMap
+import qualified Data.Aeson.Key as Key
 
 depsToPkgKeys :: NP.Dependencies -> [YLT.PackageKey]
-depsToPkgKeys = map toPkgKey . HML.toList
+depsToPkgKeys deps =
+  deps
+  & KeyMap.toList
+  <&> first Key.toText
+  <&> toPkgKey
   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
+      YLT.PackageKey (NP.parsePackageKeyName k) v
 
 -- | generate a nix expression that translates your package.nix
 --
 -- and can serve as template for manual adjustments
-genTemplate :: NP.Package -> NExpr
-genTemplate NP.Package{..} =
+genTemplate :: Maybe NL.LicensesBySpdxId -> NP.Package -> NExpr
+genTemplate licSet NP.Package{..} =
   -- reserved for possible future arguments (to prevent breakage)
   simpleParamSet []
   ==> Param nodeDepsSym
   ==> (mkNonRecSet
-        [ "name" $= nameStr
+        [ "key" $= packageKeyToSet (NP.parsePackageKeyName name)
         , "version" $= mkStr version
         , "nodeBuildInputs"  $= (letE "a" (mkSym nodeDepsSym)
                                   $ mkList (map (pkgDep "a") depPkgKeys))
         , "meta"      $= (mkNonRecSet
-           $ may "description" description
-          <> may "license" license
-          <> may "homepage" homepage)
+           $ attrSetMayStr "description" description
+          <> attrSetMay    "license" (NL.nodeLicenseToNixpkgs <$> license <*> licSet)
+          <> attrSetMayStr "homepage" homepage)
         ])
   where
     -- TODO: The devDependencies are only needed for the build
@@ -57,8 +55,9 @@
     depPkgKeys = depsToPkgKeys (dependencies <> devDependencies)
     pkgDep depsSym pk = mkSym depsSym !!. packageKeyToSymbol pk
     nodeDepsSym = "allDeps"
-    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)]
+    packageKeyToSet (YLT.SimplePackageKey n) =
+      packageKeyToSet $ YLT.ScopedPackageKey "" n
+    packageKeyToSet (YLT.ScopedPackageKey s n) = mkNonRecSet $
+      [ bindTo "name"  $ mkStrQ [ StrQ n ]
+      , bindTo "scope" $ mkStrQ [ StrQ s ]
+      ]
diff --git a/src/Distribution/Nixpkgs/Nodejs/License.hs b/src/Distribution/Nixpkgs/Nodejs/License.hs
new file mode 100644
--- /dev/null
+++ b/src/Distribution/Nixpkgs/Nodejs/License.hs
@@ -0,0 +1,128 @@
+{-# LANGUAGE NoImplicitPrelude, OverloadedStrings, RecordWildCards, FlexibleInstances, GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE LambdaCase #-}
+{-# LANGUAGE MultiWayIf #-}
+{-# LANGUAGE DerivingStrategies #-}
+{-# LANGUAGE TypeApplications #-}
+  {-|
+Description: Convert @package.json@ license fields to nixpkgs license attribute sets
+-}
+module Distribution.Nixpkgs.Nodejs.License
+  ( -- * Conversion Logic
+    nodeLicenseToNixpkgs
+    -- * License Lookup Table
+  , LicensesBySpdxId
+  ) where
+
+import Protolude
+
+import qualified Data.Aeson as A
+import qualified Nix.Expr as Nix
+import qualified Data.Map.Strict as Map
+import qualified Data.Aeson.BetterErrors as Json
+import qualified Data.Scientific as Scientific
+
+-- newtype to circumvent the default instance: we don't want
+-- the key of the JSON object to be the key of the HashMap,
+-- but one of its values (spdxId).
+-- | Lookup table from SPDX identifier (as 'Text') to 'NixpkgsLicense'.
+newtype LicensesBySpdxId
+  = LicensesBySpdxId (Map Text NixpkgsLicense)
+  deriving stock (Show, Eq)
+  deriving newtype (Semigroup, Monoid)
+
+-- | Representation of a nixpkgs license set as found in
+--   @lib.licenses@. There doesn't seem to be a strict
+--   definition of what is required and what is optional,
+--   the distribution of 'Maybe' and non-'Maybe' values
+--   is based on the current situation in @lib/licenses.nix@.
+data NixpkgsLicense
+  = NixpkgsLicense ([(Text, LicenseValue)])
+  deriving stock (Show, Eq)
+data LicenseValue
+  = LText Text
+  | LBool Bool
+  | LInt Int
+  deriving stock (Show, Eq)
+
+-- | Static version of @lib.licenses.unfree@,
+--   so @UNLICENSED@ can be handled correctly
+--   even if no lookup table is provided.
+--
+-- TODO: this will go out of sync with the nixpkgs definitions every once in a while, how to fix?
+unfreeLicense :: NixpkgsLicense
+unfreeLicense = NixpkgsLicense $ [
+    ("shortName", LText "unfree")
+  , ("deprecated", LBool False)
+  , ("fullName", LText "Unfree")
+  , ("redistributable", LBool False)
+  , ("free", LBool False)
+ ]
+
+instance A.FromJSON LicensesBySpdxId where
+  parseJSON = Json.toAesonParser identity ((Json.forEachInObject $ \_key -> do
+    Json.keyMay "spdxId" Json.asText
+    >>= \case
+      Nothing -> pure Nothing
+      Just spdxId -> do
+        spdxLicense <- (Json.eachInObject $ Json.withValue $ \case
+          A.String t -> Right $ LText t
+          A.Bool b -> Right $ LBool b
+          A.Number s -> case Scientific.toBoundedInteger @Int s of
+            Just i -> Right $ LInt i
+            Nothing -> Left $ "Not an integer: " <> (s & show)
+          A.Null -> Left "Cannot parse Null as license value for now"
+          A.Object _ -> Left "Cannot parse Object as license value for now"
+          A.Array _ -> Left "Cannot parse Array as license value for now")
+          <&> NixpkgsLicense
+        pure $ Just (spdxId, spdxLicense)
+   )
+      <&> catMaybes
+      <&> Map.fromList
+      <&> LicensesBySpdxId
+   )
+
+
+-- | Build nix attribute set for given 'NixpkgsLicense'.
+--
+--   The resulting nix value of @nixpkgsLicenseExpression x@
+--   should be equal to @lib.licenses.<attrName x>@ for the
+--   same version of nixpkgs used.
+nixpkgsLicenseExpression :: NixpkgsLicense -> Nix.NExpr
+nixpkgsLicenseExpression (NixpkgsLicense m) =
+  m
+  <&> second licenseValueToNExpr
+  & Nix.attrsE
+
+licenseValueToNExpr :: LicenseValue -> Nix.NExpr
+licenseValueToNExpr = \case
+  LText t -> Nix.mkStr t
+  LInt i -> Nix.mkInt (i & fromIntegral @Int @Integer)
+  LBool b -> Nix.mkBool b
+
+-- | Implements the logic for converting from an (optional)
+--   @package.json@ @license@ field to a nixpkgs @meta.license@
+--   set. Since support for multiple licenses is poor in nixpkgs
+--   at the moment, we don't attempt to convert SPDX expressions
+--   like @(ISC OR GPL-3.0-only)@.
+--
+--   See <https://docs.npmjs.com/files/package.json#license> for
+--   details on npm's @license@ field.
+nodeLicenseToNixpkgs :: Text -> LicensesBySpdxId -> Nix.NExpr
+nodeLicenseToNixpkgs nodeLicense licSet = do
+  if nodeLicense == "UNLICENSED"
+    then nixpkgsLicenseExpression unfreeLicense
+    else case lookupSpdxId nodeLicense licSet of
+      Nothing -> Nix.mkStr nodeLicense
+      Just license -> license
+
+-- | Lookup function for 'LicensesBySpdxId' which directly returns a 'NExpr'.
+--   This function only looks up by SPDX identifier and does not take
+--   npm-specific quirks into account.
+--
+--   Use 'nodeLicenseToNixpkgs' when dealing with the @license@ field
+--   of a npm-ish javascript package.
+lookupSpdxId :: Text -> LicensesBySpdxId -> Maybe Nix.NExpr
+lookupSpdxId lic (LicensesBySpdxId licSet) =
+  licSet
+  & Map.lookup lic
+  <&> nixpkgsLicenseExpression
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
@@ -171,9 +171,9 @@
           PkgDefFileLocal $ pkgDataGeneric $ fileLocalPath
         YLT.GitRemote{gitRepoUrl, gitRev} ->
           PkgDefGit $ pkgDataGeneric $ Git gitRepoUrl gitRev
-        YLT.FileRemoteNoIntegrity{..} ->
+        YLT.FileRemoteNoIntegrity {} ->
           panic "programming error, should have thrown an error in ResolveLockfile"
-        YLT.FileLocalNoIntegrity{..} ->
+        YLT.FileLocalNoIntegrity {} ->
           panic "programming error, should have thrown an error in ResolveLockfile"
                  -- we don’t need another ref indirection
                  -- if that’s already the name of our def
@@ -262,7 +262,9 @@
     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
+    shorten s = case M.lookup s shortcuts of
+      Nothing -> concatNSyms s
+      Just sc -> N.mkSym (unNSym sc)
     -- | Build function boilerplate the build functions share in common.
     buildPkgFnGeneric :: [Text] -> NExpr -> NExpr
     buildPkgFnGeneric additionalArguments srcNExpr =
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
@@ -7,11 +7,14 @@
 -}
 module Distribution.Nixpkgs.Nodejs.ResolveLockfile
 ( resolveLockfileStatus
+, ResolverConfig(..)
 , Resolved(..), ResolvedLockfile
 ) where
 
-import Protolude
+import Protolude hiding (toS)
+import Protolude.Conv (toS)
 import qualified Control.Monad.Trans.Except as E
+import Data.ByteString.Lazy ()
 import qualified Data.List.NonEmpty as NE
 import qualified Data.MultiKeyedMap as MKM
 import qualified Data.Aeson as Aeson
@@ -29,6 +32,13 @@
 maxFetchers :: Int
 maxFetchers = 5
 
+data ResolverConfig
+  = ResolverConfig
+  { resolveOffline :: Bool -- ^ If @True@, 'resolveLockfileStatus' will throw an
+                           --   error in case resolving a hash requires network
+                           --   access (for when it started in a nix build)
+  }
+
 -- | A thing whose hash is already known (“resolved”).
 --
 -- Only packages with known hashes are truly “locked”.
@@ -41,9 +51,13 @@
 type ResolvedLockfile = MKM.MKMap YLT.PackageKey (Resolved YLT.Package)
 
 -- | Resolve all packages by downloading their sources if necessary.
-resolveLockfileStatus :: (Chan YLT.Remote) -> YLT.Lockfile
+--
+--   Respects 'runOffline' from 'RunConfig': If it is 'True', it throws
+--   an error as soon as it would need to download something which is the
+--   case for 'YLT.GitRemote'.
+resolveLockfileStatus :: ResolverConfig -> (Chan YLT.Remote) -> YLT.Lockfile
                       -> IO (Either (NE.NonEmpty Text) ResolvedLockfile)
-resolveLockfileStatus msgChan lf = Async.withTaskGroup maxFetchers $ \taskGroup -> do
+resolveLockfileStatus cfg msgChan lf = Async.withTaskGroup maxFetchers $ \taskGroup -> do
   job <- STM.atomically $ Async.mapReduce taskGroup
            $ fmap (\(ks, pkg) -> (:[]) <$> (E.runExceptT $ do
                         liftIO $ writeChan msgChan (YLT.remote pkg)
@@ -60,7 +74,11 @@
     resolve pkg = case YLT.remote pkg of
       YLT.FileRemote{..} -> pure $ r fileSha1
       YLT.FileLocal{..}  -> pure $ r fileLocalSha1
-      YLT.GitRemote{..}  -> r <$> fetchFromGit gitRepoUrl gitRev
+      YLT.GitRemote{..}  -> if cfg & resolveOffline
+                              then E.throwE $ "Refusing to resolve \"git+"
+                              <> gitRepoUrl <> "#" <> gitRev
+                              <> "\" because --offline is set"
+                              else r <$> fetchFromGit gitRepoUrl gitRev
       YLT.FileRemoteNoIntegrity{..} -> E.throwE
         $ "The remote "
         <> fileNoIntegrityUrl
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
@@ -4,6 +4,7 @@
 -}
 module Distribution.Nixpkgs.Nodejs.Utils where
 import Protolude
+import Nix.Expr
 import qualified Yarn.Lock.Types as YLT
 
 -- | Representation of a PackageKey as nix attribute name.
@@ -19,3 +20,13 @@
   YLT.ScopedPackageKey scope n -> "@" <> scope <> "/" <> n
 {-# INLINABLE packageKeyNameToSymbol #-}
 
+-- | Return a 'Binding' if 'Just' (or none if 'Nothing')
+--   for 'mkRecSet' and 'mkNonRecSet'.
+attrSetMay :: Text -> Maybe NExpr -> [Binding NExpr]
+attrSetMay k v = maybeToList $ (k $=) <$> v
+{-# INLINABLE attrSetMay #-}
+
+-- | Convenience shortcut for @'attrSetMay' x (mkStr \<$\> y)@.
+attrSetMayStr :: Text -> Maybe Text -> [Binding NExpr]
+attrSetMayStr k = attrSetMay k . fmap mkStr
+{-# INLINABLE attrSetMayStr #-}
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
@@ -1,4 +1,4 @@
-{-# LANGUAGE NoImplicitPrelude, DeriveGeneric, OverloadedStrings, RecordWildCards, LambdaCase #-}
+{-# LANGUAGE NoImplicitPrelude, OverloadedStrings, RecordWildCards, LambdaCase, TypeApplications #-}
 {-|
 Description: Parse and make sense of npm’s @package.json@ project files
 
@@ -11,6 +11,7 @@
   -- * @package.json@ data
 , Package(..)
 , Bin(..), Man(..), Dependencies
+, parsePackageKeyName
 ) where
 
 import Protolude hiding (packageName)
@@ -18,12 +19,15 @@
 import qualified Control.Monad.Writer.Lazy as WL
 import qualified Data.ByteString.Lazy as BL
 import qualified Data.Text as T
-import qualified Data.HashMap.Lazy as HML
 import qualified System.FilePath as FP
 
-import Data.Aeson ((.:), (.:?), (.!=))
+import Data.Aeson ((.:), (.:?), (.!=), Key)
 import qualified Data.Aeson as A
 import qualified Data.Aeson.Types as AT
+import qualified Yarn.Lock.Types as YLT
+import qualified Data.Aeson.Key as Key
+import Data.Aeson.KeyMap (KeyMap)
+import qualified Data.Aeson.KeyMap as KeyMap
 
 -- | npm `package.json`. Not complete.
 --
@@ -34,7 +38,7 @@
   , description :: Maybe Text
   , homepage :: Maybe Text
   , private :: Bool
-  , scripts :: HML.HashMap Text Text
+  , scripts :: KeyMap Text
   , bin :: Bin
   , man :: Man
   , license :: Maybe Text
@@ -57,7 +61,7 @@
 
 -- | The package’s executable files.
 data Bin
-  = BinFiles (HML.HashMap Text FilePath)
+  = BinFiles (KeyMap FilePath)
   -- ^ map of files from name to their file path (relative to package path)
   | BinFolder FilePath
   -- ^ a folder containing all executable files of the project (also relative)
@@ -65,12 +69,12 @@
 
 -- | The package’s manual files.
 data Man
-  = ManFiles (HML.HashMap Text FilePath)
+  = ManFiles (KeyMap FilePath)
   -- ^ map of files from name to their file path (relative to package path)
   deriving (Show, Eq)
 
 -- | Dependencies of a package.
-type Dependencies = HML.HashMap Text Text
+type Dependencies = KeyMap Text
 
 type Warn = WL.WriterT [Warning] AT.Parser
 putWarning :: a -> Warning -> Warn a
@@ -84,10 +88,10 @@
       l :: AT.Parser a -> Warn a
       l = WL.WriterT . fmap (\a -> (a, []))
       tryWarn :: (AT.FromJSON a, Show a)
-              => Text -> a -> Warn a
+              => AT.Key -> a -> Warn a
       tryWarn field def =
         lift (v .:? field .!= def)
-        <|> putWarning def (WrongType { wrongTypeField = field
+        <|> putWarning def (WrongType { wrongTypeField = field & Key.toText
                                        , wrongTypeDefault = Just (show def) })
     name            <- l $ v .:  "name"
     version         <- l $ v .:  "version"
@@ -98,20 +102,42 @@
     bin             <- parseBin name v
     man             <- l $ parseMan name v
     license         <- tryWarn "license" Nothing
-    dependencies    <- l $ v .:? "dependencies" .!= mempty
-    devDependencies <- l $ v .:? "devDependencies" .!= mempty
+    dependencies    <- tryWarn "dependencies" (AT.Object mempty)
+                         >>= parseDependencies "dependencies"
+    devDependencies <- tryWarn "devDependencies" (AT.Object mempty)
+                         >>= parseDependencies "devDependencies"
     pure Package{..}
     where
 
-      parseMapText :: Text -> HML.HashMap Text AT.Value
-                   -> Warn (HML.HashMap Text Text)
+      parseDependencies ::  Text -> AT.Value -> Warn Dependencies
+      parseDependencies field v =
+        let
+          warn = putWarning mempty
+              $ WrongType
+              { wrongTypeField   = field
+              , wrongTypeDefault = Just (show (mempty :: Dependencies)) }
+        in case v of
+          AT.Array a ->
+            -- we interpret empty arrays as just confused users
+            if null a then warn
+            -- however if the user uses a non-empty array,
+            -- they probably mean something which we don’t know how to deal with.
+            else fail
+              $ "\"" ++ T.unpack field ++ "\" is a non empty array instead of a JSON object"
+          -- if we get an object here, it's malformed
+          AT.Object deps -> lift $ traverse (A.parseJSON @Text) deps
+          -- everything else defaults to mempty and generates a warning
+          _ -> warn
+
+      parseMapText :: Text -> KeyMap AT.Value
+                   -> Warn (KeyMap Text)
       parseMapText fieldPath val =
-        HML.mapMaybe identity <$> HML.traverseWithKey tryParse val
+        KeyMap.mapMaybe identity <$> KeyMap.traverseWithKey tryParse val
         where
-          tryParse :: Text -> A.Value -> Warn (Maybe Text)
+          tryParse :: A.Key -> A.Value -> Warn (Maybe Text)
           tryParse key el = lift (Just <$> AT.parseJSON el)
             <|> putWarning Nothing
-                  (WrongType { wrongTypeField = fieldPath <> "." <> key
+                  (WrongType { wrongTypeField = fieldPath <> "." <> (key & Key.toText)
                              , wrongTypeDefault = Nothing })
       parseBin :: Text -> AT.Object -> Warn Bin
       parseBin packageName v = do
@@ -126,7 +152,7 @@
               "`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)
+            $ KeyMap.singleton (parsePackageName packageName & Key.fromText) (toS path)
           -- or it’s a map from names to paths
           (Just (A.Object bins),      _) -> lift $ BinFiles
             <$> traverse (A.withText "BinPath" (pure.toS)) bins
@@ -141,15 +167,15 @@
       -- TODO: parsing should be as thorough as with "bin"
       parseMan name v = do
         let getMan f = ManFiles . f <$> v .: "man"
-            extractName :: FilePath -> (Text, FilePath)
+            extractName :: FilePath -> (Key, FilePath)
             extractName file =
               let f = T.pack $ FP.takeFileName file
               in if name `T.isPrefixOf` f
-                  then (name, file)
-                  else (name <> "-" <> f, file)
+                  then (Key.fromText name, file)
+                  else (Key.fromText $ name <> "-" <> f, file)
         -- TODO: handle directories.man
-        (getMan (HML.fromList . map extractName)
-            <|> getMan (HML.fromList . (:[]) . extractName)
+        (getMan (KeyMap.fromList . map extractName)
+            <|> getMan (KeyMap.fromList . (:[]) . extractName)
             <|> pure (ManFiles mempty))
 
 -- | Convenience decoding function.
@@ -168,3 +194,18 @@
          Nothing  -> "Leaving it out")
     <> "."
   (PlainWarning t) -> t
+
+-- | Parse a package name string into a 'YLT.PackageKeyName'.
+parsePackageKeyName :: Text -> YLT.PackageKeyName
+parsePackageKeyName k =
+  case YLT.parsePackageKeyName k of
+    -- we don’t crash on a “wrong” package key to keep this
+    -- code pure, but assume it’s a simple key instead.
+    Nothing -> (YLT.SimplePackageKey k)
+    Just pkn -> pkn
+
+parsePackageName :: Text -> Text
+parsePackageName k =
+  case parsePackageKeyName k of
+     YLT.SimplePackageKey n -> n
+     YLT.ScopedPackageKey _ n -> n
diff --git a/tests/TestNpmjsPackage.hs b/tests/TestNpmjsPackage.hs
--- a/tests/TestNpmjsPackage.hs
+++ b/tests/TestNpmjsPackage.hs
@@ -1,53 +1,134 @@
-{-# LANGUAGE OverloadedStrings, TemplateHaskell, QuasiQuotes, NoImplicitPrelude, LambdaCase #-}
+{-# LANGUAGE OverloadedStrings, TemplateHaskell, QuasiQuotes, NoImplicitPrelude, LambdaCase, TypeApplications, RecordWildCards, ScopedTypeVariables #-}
 module TestNpmjsPackage (tests) where
 
 import Protolude
 import Test.Tasty (TestTree)
 import Test.Tasty.TH
-import Test.Tasty.HUnit
+import Test.Tasty.HUnit (Assertion, testCase)
+import qualified Test.Tasty.HUnit as HUnit
 -- import Test.Tasty.QuickCheck
 
 import qualified Data.Aeson as A
 import qualified Data.Aeson.Types as AT
-import qualified Data.HashMap.Lazy as HML
+import qualified Data.Text as Text
 
 import qualified Distribution.Nodejs.Package as NP
+import qualified Data.Aeson.KeyMap as KeyMap
+import Data.Aeson (Key)
 
-baseAnd :: [(Text, A.Value)] -> A.Value
-baseAnd fields = A.Object $ HML.fromList $
+assertEqual :: (HasCallStack, Eq a, Show a) => Text -> a -> a -> Assertion
+assertEqual t a b = HUnit.assertEqual (toS t) a b
+
+assertBool :: (HasCallStack) => Text -> Bool -> Assertion
+assertBool t b = HUnit.assertBool (toS t) b
+
+baseAnd :: [(Key, A.Value)] -> A.Value
+baseAnd fields = A.Object $ KeyMap.fromList $
   [ ("name", "foopkg")
   , ("version", "1.0.2")
   ] <> fields
 
-case_binPaths :: Assertion
-case_binPaths = do
-  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 ())
+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
 
-    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
+formatWarnings :: [NP.Warning] -> Text
+formatWarnings ws = Text.intercalate ", " (map f ws)
+  where
+    f w@(NP.PlainWarning _) = "PlainWarning `" <> NP.formatWarning w <> "`"
+    f w@(NP.WrongType {}) = "WrongType `" <> NP.formatWarning w <> "`"
 
+parseZoom :: (Eq a, Show a)
+          => Text -> A.Value -> (NP.Package -> a) -> a
+          -> Assertion
+parseZoom name got zoom want =
+  parseWithWarningsZoom name got zoom want
+    (\ws -> assertBool ("unexpected warnings: " <> formatWarnings ws ) $ null ws)
+
+data WarningType
+  = SomePlainWarning
+  | WrongTypeField
+    { wrongTypeField :: Text
+    , wrongTypeDefault :: Maybe ()
+    }
+  deriving (Show)
+
+-- TODO: the warning list should be an exact list/set!
+hasWarning :: WarningType -> [NP.Warning] -> Assertion
+hasWarning t = assertBool ("no such warning: " <> show t)
+               . any (checkWarningType t)
+
+checkWarningType :: WarningType -> NP.Warning -> Bool
+checkWarningType tp w = case (tp, w) of
+  (SomePlainWarning, NP.PlainWarning _) -> True
+  ( WrongTypeField { wrongTypeField = ft
+                   , wrongTypeDefault = deft },
+    NP.WrongType { NP.wrongTypeField = f
+                 , NP.wrongTypeDefault = def })
+    -> ft == f && case (deft, def) of
+        (Nothing, Nothing) -> True
+        (Just (), Just _) -> True
+        _ -> False
+  (_, _) -> False
+
+case_dependencies :: Assertion
+case_dependencies = do
+  parseZoom "dependencies are missing"
+            (baseAnd [ ])
+            NP.dependencies
+            mempty
+
+  parseZoom "dependencies are empty"
+            (baseAnd [ ("dependencies", A.object []) ])
+            NP.dependencies
+            mempty
+
+  parseZoom "some dependencies"
+            (baseAnd [ ("dependencies", A.object
+                       [ ("foo", "1.2.3")
+                       , ("bar", "3.4.0") ]) ])
+            NP.dependencies
+            (KeyMap.fromList
+              [ ("foo", "1.2.3")
+              , ("bar", "3.4.0") ])
+
+  parseWithWarningsZoom "dependencies are an empty list"
+            (baseAnd [ ("dependencies", A.Array mempty) ])
+            NP.dependencies
+            mempty
+            (hasWarning $ WrongTypeField
+              { wrongTypeField = "dependencies"
+              , wrongTypeDefault = Just () })
+
+  parseWithWarningsZoom "dependencies is a random scalar"
+            (baseAnd [ ("dependencies", A.String "hiho") ])
+            NP.dependencies
+            mempty
+            (hasWarning $ WrongTypeField
+              { wrongTypeField = "dependencies"
+              , wrongTypeDefault = Just () })
+
+  parseFailure (Proxy @NP.LoggingPackage) "dependencies are a non-empty list"
+            (baseAnd [ ("dependencies", A.Array (pure "foo")) ])
+
+case_binPaths :: Assertion
+case_binPaths = do
   parseZoom ".bin exists with files"
             (baseAnd [ ("bin", "./abc") ])
             NP.bin
-            (NP.BinFiles $ HML.fromList [ ("foopkg", "./abc") ])
+            (NP.BinFiles $ KeyMap.fromList [ ("foopkg", "./abc") ])
 
+  parseZoom "scoped package"
+            (baseAnd [ ("name", "@foo/bar"), ("bin", "./abc") ])
+            NP.bin
+            (NP.BinFiles $ KeyMap.fromList [ ("bar", "./abc") ])
+
   parseZoom ".directories.bin exists with path"
             (baseAnd [ ("directories", A.object [("bin", "./abc")]) ])
             NP.bin
@@ -58,7 +139,7 @@
                        [ ("one", "./bin/one")
                        , ("two", "imhere") ]) ])
             NP.bin
-            (NP.BinFiles $ HML.fromList
+            (NP.BinFiles $ KeyMap.fromList
               [ ("one", "./bin/one")
               , ("two", "imhere") ])
 
@@ -68,7 +149,7 @@
                                      [ ("bin", "foo") ]) ])
                         NP.bin
                         (NP.BinFiles mempty)
-                        (hasWarning plainWarning)
+                        (hasWarning SomePlainWarning)
 
   parseZoom "neither .bin nor .directories.bin exis"
             (baseAnd [])
@@ -80,18 +161,21 @@
                                    [ ("foo", A.object [])
                                    , ("bar", "imascript") ]) ])
                         NP.scripts
-                        (HML.fromList [ ("bar", "imascript") ])
-                        (hasWarning (wrongType "scripts.foo" Nothing))
+                        (KeyMap.fromList [ ("bar", "imascript") ])
+                        (hasWarning (WrongTypeField
+                           { wrongTypeField = "scripts.foo"
+                           , wrongTypeDefault = 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.Error err) -> HUnit.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 :: forall a. (A.FromJSON a) => Proxy a -> Text -> A.Value -> IO ()
+parseFailure Proxy msg v = case AT.fromJSON @a v of
+  -- TODO: check the error?
+  (AT.Error _) -> pass
+  (AT.Success _) -> HUnit.assertFailure $ (toS msg) <> ", parse should have failed."
 
 tests :: TestTree
 tests = $(testGroupGenerator)
diff --git a/yarn2nix.cabal b/yarn2nix.cabal
--- a/yarn2nix.cabal
+++ b/yarn2nix.cabal
@@ -1,13 +1,11 @@
 cabal-version: 1.12
 
--- This file has been generated from package.yaml by hpack version 0.33.0.
+-- This file has been generated from package.yaml by hpack version 0.34.6.
 --
 -- see: https://github.com/sol/hpack
---
--- hash: 5daa56420b918ef3cbd08426830e2e6fd2caf9897d7b21ec8141f33a81148fd3
 
 name:           yarn2nix
-version:        0.8.0
+version:        0.10.1
 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,12 +19,8 @@
 extra-source-files:
     LICENSE
     README.md
-    yarn2nix.nix
-    default.nix
-    shell.nix
     nix-lib/buildNodePackage.nix
     nix-lib/default.nix
-    nix-lib/old-version-dependencies.nix
 
 source-repository head
   type: git
@@ -36,6 +30,7 @@
   exposed-modules:
       Distribution.Nixpkgs.Nodejs.Cli
       Distribution.Nixpkgs.Nodejs.FromPackage
+      Distribution.Nixpkgs.Nodejs.License
       Distribution.Nixpkgs.Nodejs.OptimizedNixOutput
       Distribution.Nixpkgs.Nodejs.ResolveLockfile
       Distribution.Nixpkgs.Nodejs.Utils
@@ -47,21 +42,23 @@
       src
   ghc-options: -Wall
   build-depends:
-      aeson >=1.0 && <1.5
+      aeson >=2.0
+    , aeson-better-errors >=0.9.1.1
     , async-pool ==0.9.*
     , base ==4.*
     , bytestring ==0.10.*
     , containers >=0.5 && <0.7
-    , data-fix ==0.0.7 || ==0.2.0
+    , data-fix >=0.0.7 && <0.4
     , directory ==1.3.*
     , filepath ==1.4.*
-    , hnix ==0.6.*
+    , hnix >=0.6 && <0.15
     , mtl ==2.2.*
-    , prettyprinter ==1.2.*
+    , optparse-applicative ==0.16.*
+    , prettyprinter >=1.2 && <1.8
     , process >=1.4
-    , protolude ==0.2.*
-    , regex-tdfa ==1.2.*
-    , regex-tdfa-text ==1.0.0.*
+    , protolude ==0.3.*
+    , regex-tdfa ==1.3.*
+    , scientific >0.3.3.0 && <0.4
     , stm >2.4.0 && <2.6.0.0
     , text ==1.2.*
     , transformers ==0.5.*
@@ -75,22 +72,23 @@
       Paths_yarn2nix
   ghc-options: -Wall
   build-depends:
-      aeson >=1.0 && <1.5
+      aeson >=2.0
+    , aeson-better-errors >=0.9.1.1
     , async-pool ==0.9.*
     , base ==4.*
     , bytestring ==0.10.*
     , containers >=0.5 && <0.7
-    , data-fix ==0.0.7 || ==0.2.0
+    , data-fix >=0.0.7 && <0.4
     , directory ==1.3.*
     , filepath ==1.4.*
-    , hnix ==0.6.*
+    , hnix >=0.6 && <0.15
     , mtl ==2.2.*
-    , optparse-applicative >=0.13 && <0.15
-    , prettyprinter ==1.2.*
+    , optparse-applicative >=0.13
+    , prettyprinter >=1.2 && <1.8
     , process >=1.4
-    , protolude ==0.2.*
-    , regex-tdfa ==1.2.*
-    , regex-tdfa-text ==1.0.0.*
+    , protolude ==0.3.*
+    , regex-tdfa ==1.3.*
+    , scientific >0.3.3.0 && <0.4
     , stm >2.4.0 && <2.6.0.0
     , text ==1.2.*
     , transformers ==0.5.*
@@ -106,21 +104,23 @@
       Paths_yarn2nix
   ghc-options: -Wall
   build-depends:
-      aeson >=1.0 && <1.5
+      aeson >=2.0
+    , aeson-better-errors >=0.9.1.1
     , async-pool ==0.9.*
     , base ==4.*
     , bytestring ==0.10.*
     , containers >=0.5 && <0.7
-    , data-fix ==0.0.7 || ==0.2.0
+    , data-fix >=0.0.7 && <0.4
     , directory ==1.3.*
     , filepath ==1.4.*
-    , hnix ==0.6.*
+    , hnix >=0.6 && <0.15
     , mtl ==2.2.*
-    , prettyprinter ==1.2.*
+    , optparse-applicative ==0.16.*
+    , prettyprinter >=1.2 && <1.8
     , process >=1.4
-    , protolude ==0.2.*
-    , regex-tdfa ==1.2.*
-    , regex-tdfa-text ==1.0.0.*
+    , protolude ==0.3.*
+    , regex-tdfa ==1.3.*
+    , scientific >0.3.3.0 && <0.4
     , stm >2.4.0 && <2.6.0.0
     , text ==1.2.*
     , transformers ==0.5.*
@@ -139,24 +139,26 @@
       tests
   ghc-options: -Wall
   build-depends:
-      aeson >=1.0 && <1.5
+      aeson >=2.0
+    , aeson-better-errors >=0.9.1.1
     , async-pool ==0.9.*
     , base ==4.*
     , bytestring ==0.10.*
     , containers >=0.5 && <0.7
-    , data-fix ==0.0.7 || ==0.2.0
+    , data-fix >=0.0.7 && <0.4
     , directory ==1.3.*
     , filepath ==1.4.*
-    , hnix ==0.6.*
+    , hnix >=0.6 && <0.15
     , mtl ==2.2.*
-    , neat-interpolation ==0.3.*
-    , prettyprinter ==1.2.*
+    , neat-interpolation >=0.3 && <0.6
+    , optparse-applicative ==0.16.*
+    , prettyprinter >=1.2 && <1.8
     , process >=1.4
-    , protolude ==0.2.*
-    , regex-tdfa ==1.2.*
-    , regex-tdfa-text ==1.0.0.*
+    , protolude ==0.3.*
+    , regex-tdfa ==1.3.*
+    , scientific >0.3.3.0 && <0.4
     , stm >2.4.0 && <2.6.0.0
-    , tasty >=0.11 && <1.2
+    , tasty >=0.11 && <1.5
     , tasty-hunit >=0.9 && <0.11
     , tasty-quickcheck >=0.8 && <0.11
     , tasty-th ==0.1.7.*
diff --git a/yarn2nix.nix b/yarn2nix.nix
deleted file mode 100644
--- a/yarn2nix.nix
+++ /dev/null
@@ -1,37 +0,0 @@
-{ mkDerivation, aeson, ansi-wl-pprint, async-pool, base, bytestring
-, containers, data-fix, directory, either, filepath, hnix
-, http-client, http-client-tls, monad-par, mtl, neat-interpolation
-, optparse-applicative, process, protolude, regex-tdfa
-, regex-tdfa-text, stdenv, stm, tasty, tasty-hunit
-, tasty-quickcheck, tasty-th, text, unix, unordered-containers
-, yarn-lock
-}:
-mkDerivation {
-  pname = "yarn2nix";
-  version = "0.6.0";
-  src = ./.;
-  isLibrary = true;
-  isExecutable = true;
-  libraryHaskellDepends = [
-    aeson ansi-wl-pprint async-pool base bytestring containers data-fix
-    directory either filepath hnix http-client http-client-tls
-    monad-par mtl process protolude regex-tdfa regex-tdfa-text stm text
-    unordered-containers yarn-lock
-  ];
-  executableHaskellDepends = [
-    aeson ansi-wl-pprint async-pool base bytestring containers data-fix
-    directory either filepath hnix http-client http-client-tls
-    monad-par mtl optparse-applicative process protolude regex-tdfa
-    regex-tdfa-text stm text unix unordered-containers yarn-lock
-  ];
-  testHaskellDepends = [
-    aeson ansi-wl-pprint async-pool base bytestring containers data-fix
-    directory either filepath hnix http-client http-client-tls
-    monad-par mtl neat-interpolation process protolude regex-tdfa
-    regex-tdfa-text stm tasty tasty-hunit tasty-quickcheck tasty-th
-    text unordered-containers yarn-lock
-  ];
-  homepage = "https://github.com/Profpatsch/yarn2nix#readme";
-  description = "Convert yarn.lock files to nix expressions";
-  license = stdenv.lib.licenses.mit;
-}
