packages feed

yarn2nix 0.8.0 → 0.10.1

raw patch · 18 files changed

+662/−347 lines, 18 filesdep +aeson-better-errorsdep +scientificdep −regex-tdfa-textdep ~aesondep ~data-fixdep ~hnixPVP ok

version bump matches the API change (PVP)

Dependencies added: aeson-better-errors, scientific

Dependencies removed: regex-tdfa-text

Dependency ranges changed: aeson, data-fix, hnix, neat-interpolation, optparse-applicative, prettyprinter, protolude, regex-tdfa, tasty

API changes (from Hackage documentation)

+ Distribution.Nixpkgs.Nodejs.Cli: instance GHC.Classes.Eq Distribution.Nixpkgs.Nodejs.Cli.RunConfig
+ Distribution.Nixpkgs.Nodejs.Cli: instance GHC.Classes.Eq Distribution.Nixpkgs.Nodejs.Cli.RunMode
+ Distribution.Nixpkgs.Nodejs.Cli: instance GHC.Show.Show Distribution.Nixpkgs.Nodejs.Cli.RunConfig
+ Distribution.Nixpkgs.Nodejs.Cli: instance GHC.Show.Show Distribution.Nixpkgs.Nodejs.Cli.RunMode
+ Distribution.Nixpkgs.Nodejs.License: data LicensesBySpdxId
+ Distribution.Nixpkgs.Nodejs.License: instance Data.Aeson.Types.FromJSON.FromJSON Distribution.Nixpkgs.Nodejs.License.LicensesBySpdxId
+ Distribution.Nixpkgs.Nodejs.License: instance GHC.Base.Monoid Distribution.Nixpkgs.Nodejs.License.LicensesBySpdxId
+ Distribution.Nixpkgs.Nodejs.License: instance GHC.Base.Semigroup Distribution.Nixpkgs.Nodejs.License.LicensesBySpdxId
+ Distribution.Nixpkgs.Nodejs.License: instance GHC.Classes.Eq Distribution.Nixpkgs.Nodejs.License.LicenseValue
+ Distribution.Nixpkgs.Nodejs.License: instance GHC.Classes.Eq Distribution.Nixpkgs.Nodejs.License.LicensesBySpdxId
+ Distribution.Nixpkgs.Nodejs.License: instance GHC.Classes.Eq Distribution.Nixpkgs.Nodejs.License.NixpkgsLicense
+ Distribution.Nixpkgs.Nodejs.License: instance GHC.Show.Show Distribution.Nixpkgs.Nodejs.License.LicenseValue
+ Distribution.Nixpkgs.Nodejs.License: instance GHC.Show.Show Distribution.Nixpkgs.Nodejs.License.LicensesBySpdxId
+ Distribution.Nixpkgs.Nodejs.License: instance GHC.Show.Show Distribution.Nixpkgs.Nodejs.License.NixpkgsLicense
+ Distribution.Nixpkgs.Nodejs.License: nodeLicenseToNixpkgs :: Text -> LicensesBySpdxId -> NExpr
+ Distribution.Nixpkgs.Nodejs.ResolveLockfile: ResolverConfig :: Bool -> ResolverConfig
+ Distribution.Nixpkgs.Nodejs.ResolveLockfile: [resolveOffline] :: ResolverConfig -> Bool
+ Distribution.Nixpkgs.Nodejs.ResolveLockfile: data ResolverConfig
+ Distribution.Nixpkgs.Nodejs.Utils: attrSetMay :: Text -> Maybe NExpr -> [Binding NExpr]
+ Distribution.Nixpkgs.Nodejs.Utils: attrSetMayStr :: Text -> Maybe Text -> [Binding NExpr]
+ Distribution.Nodejs.Package: parsePackageKeyName :: Text -> PackageKeyName
- Distribution.Nixpkgs.Nodejs.Cli: cli :: [Text] -> IO ()
+ Distribution.Nixpkgs.Nodejs.Cli: cli :: IO ()
- Distribution.Nixpkgs.Nodejs.FromPackage: genTemplate :: Package -> NExpr
+ Distribution.Nixpkgs.Nodejs.FromPackage: genTemplate :: Maybe LicensesBySpdxId -> Package -> NExpr
- Distribution.Nixpkgs.Nodejs.ResolveLockfile: resolveLockfileStatus :: Chan Remote -> Lockfile -> IO (Either (NonEmpty Text) ResolvedLockfile)
+ Distribution.Nixpkgs.Nodejs.ResolveLockfile: resolveLockfileStatus :: ResolverConfig -> Chan Remote -> Lockfile -> IO (Either (NonEmpty Text) ResolvedLockfile)
- Distribution.Nodejs.Package: BinFiles :: HashMap Text FilePath -> Bin
+ Distribution.Nodejs.Package: BinFiles :: KeyMap FilePath -> Bin
- Distribution.Nodejs.Package: ManFiles :: HashMap Text FilePath -> Man
+ Distribution.Nodejs.Package: ManFiles :: KeyMap FilePath -> Man
- Distribution.Nodejs.Package: Package :: Text -> Text -> Maybe Text -> Maybe Text -> Bool -> HashMap Text Text -> Bin -> Man -> Maybe Text -> Dependencies -> Dependencies -> Package
+ Distribution.Nodejs.Package: Package :: Text -> Text -> Maybe Text -> Maybe Text -> Bool -> KeyMap Text -> Bin -> Man -> Maybe Text -> Dependencies -> Dependencies -> Package
- Distribution.Nodejs.Package: [scripts] :: Package -> HashMap Text Text
+ Distribution.Nodejs.Package: [scripts] :: Package -> KeyMap Text
- Distribution.Nodejs.Package: type Dependencies = HashMap Text Text
+ Distribution.Nodejs.Package: type Dependencies = KeyMap Text

Files

Main.hs view
@@ -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
NodePackageTool.hs view
@@ -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 ()
README.md view
@@ -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 ```
− default.nix
@@ -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
nix-lib/buildNodePackage.nix view
@@ -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. :(
nix-lib/default.nix view
@@ -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; }
− nix-lib/old-version-dependencies.nix
@@ -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'';-    };-}
− shell.nix
@@ -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
src/Distribution/Nixpkgs/Nodejs/Cli.hs view
@@ -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'
src/Distribution/Nixpkgs/Nodejs/FromPackage.hs view
@@ -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 ]+      ]
+ src/Distribution/Nixpkgs/Nodejs/License.hs view
@@ -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
src/Distribution/Nixpkgs/Nodejs/OptimizedNixOutput.hs view
@@ -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 =
src/Distribution/Nixpkgs/Nodejs/ResolveLockfile.hs view
@@ -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
src/Distribution/Nixpkgs/Nodejs/Utils.hs view
@@ -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 #-}
src/Distribution/Nodejs/Package.hs view
@@ -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
tests/TestNpmjsPackage.hs view
@@ -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)
yarn2nix.cabal view
@@ -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.*
− yarn2nix.nix
@@ -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;-}