yarn2nix (empty) → 0.5.0
raw patch · 21 files changed
+1438/−0 lines, 21 filesdep +aesondep +ansi-wl-pprintdep +async-poolsetup-changed
Dependencies added: aeson, ansi-wl-pprint, async-pool, base, bytestring, containers, data-fix, directory, either, filepath, hnix, mtl, neat-interpolation, optparse-applicative, process, protolude, regex-tdfa, regex-tdfa-text, stm, tasty, tasty-hunit, tasty-quickcheck, tasty-th, text, unix, unordered-containers, yarn-lock, yarn2nix
Files
- LICENSE +20/−0
- Main.hs +10/−0
- README.md +71/−0
- Setup.hs +2/−0
- SetupNodePackagePaths.hs +116/−0
- default.nix +17/−0
- nix-lib/buildNodePackage.nix +48/−0
- nix-lib/default.nix +70/−0
- nix-lib/yarn-lock.nix +19/−0
- shell.nix +40/−0
- src/Distribution/Nixpkgs/Nodejs/Cli.hs +97/−0
- src/Distribution/Nixpkgs/Nodejs/FromPackage.hs +48/−0
- src/Distribution/Nixpkgs/Nodejs/OptimizedNixOutput.hs +310/−0
- src/Distribution/Nixpkgs/Nodejs/ResolveLockfile.hs +74/−0
- src/Distribution/Nixpkgs/Nodejs/Utils.hs +11/−0
- src/Distribution/Nodejs/Package.hs +147/−0
- src/Nix/Expr/Additions.hs +76/−0
- tests/Test.hs +9/−0
- tests/TestNpmjsPackage.hs +54/−0
- yarn2nix.cabal +162/−0
- yarn2nix.nix +37/−0
+ LICENSE view
@@ -0,0 +1,20 @@+Copyright (c) 2017 Profpatsch <mail@profpatsch.de>+MIT LICENSE++Permission is hereby granted, free of charge, to any person obtaining a copy+of this software and associated documentation files (the "Software"), to deal+in the Software without restriction, including without limitation the rights+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell+copies of the Software, and to permit persons to whom the Software is+furnished to do so, subject to the following conditions:++The above copyright notice and this permission notice shall be included in+all copies or substantial portions of the Software.++THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE+SOFTWARE.
+ Main.hs view
@@ -0,0 +1,10 @@+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++
+ README.md view
@@ -0,0 +1,71 @@+# yarn2nix++```+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`.+```++## Features++- Purely transform `yarn.lock` files into very minimal, line-diffable nix expressions.+- Nix is used to its fullest. Every package is a derivation, whole dependency+ subtrees are shared in an optimal way, even between projects.+- The ability to resolve git dependencies by prefetching their repos and including the hashes.+- Completely local transformation if there are no git dependencies (can be used inside nix-build, no large file check-in).+- Extremely fast.+- Nice code that can be easily extended, new repositories introduced, adapt to new versions of the `yarn.lock` format.+- Comes with a [nix library][nix-lib] that uses the power of overlays to make overriding dependencies possible.++Probably a few more.++## 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`+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+```++The output of this conversion [can be seen+here](https://gist.github.com/Profpatsch/9e50d25faf5a5c4269566e9b7d89199b). Also+note that [git dependencies are resolved+correctly](https://gist.github.com/Profpatsch/9e50d25faf5a5c4269566e9b7d89199b#file-hackmd-dependencies-nix-L1291).++Pushing it through the provided [library of nix+functions][nix-lib], we get a complete build of HackMD+dependencies, using the project template (generated with `--template`), we also+build HackMD. 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-lib]: ./nix-lib/default.nix++## Building++```+$ nix-build+$ result/bin/yarn2nix+```++## Development++```+$ nix-shell+nix-shell> hpack+nix-shell> cabal build+```
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ SetupNodePackagePaths.hs view
@@ -0,0 +1,116 @@+{-# LANGUAGE RecordWildCards, NoImplicitPrelude, LambdaCase, FlexibleContexts, NamedFieldPuns, OverloadedStrings #-}+import Protolude+import qualified Data.Text.IO as TIO+import qualified Control.Monad.Except as ExcT+import qualified Control.Exception as Exc+import qualified System.IO.Error as IOErr+import qualified Data.ByteString.Lazy as BL+import qualified Data.HashMap.Lazy as HML+import qualified System.FilePath as FP+import qualified System.Directory as Dir+import qualified System.Posix.Files as PosixFiles+import Options.Applicative++import qualified Distribution.Nodejs.Package as NP++data Args+ = Args { argMode :: Mode+ , argTargetDir :: FilePath+ , argPackageDir :: FilePath }++data Mode = BinMode++args :: Parser Args+args = subparser+ ( command "bin" (info (modeCommands BinMode)+ (progDesc "link package bin files")) )+ where+ modeCommands mode = Args+ <$> pure mode+ <*> strOption+ ( long "to"+ <> metavar "LINK_TARGET"+ <> help "folder to link to (absolute or relative from package folder)" )+ <*> strOption+ ( long "package"+ <> metavar "FOLDER"+ <> help "folder with the node package" )+ ++main :: IO ()+main = execParser (info (args <**> helper)+ (progDesc "Link various files from npm packages to folders"))+ >>= realMain++type ErrorLogger = ExceptT [Char] IO++realMain :: Args -> IO ()+realMain Args{..} = do+ let packageJsonPath = argPackageDir FP.</> "package.json"+ unlessM (Dir.doesDirectoryExist argTargetDir)+ $ die $ argTargetDir <> " is not a directory."+ unlessM (Dir.doesFileExist packageJsonPath)+ $ die $ packageJsonPath <> " does not exist."++ runExceptT+ (tryRead packageJsonPath >>= tryDecode packageJsonPath >>= go)+ >>= \case+ (Left err) -> die $ "ERROR: " <> err+ (Right _) -> pass++ where+ tryIOMsg :: ([Char] -> [Char]) -> IO a -> ErrorLogger a+ tryIOMsg errAnn = ExcT.withExceptT (errAnn . Exc.displayException) . tryIO++ tryRead :: FilePath -> ErrorLogger BL.ByteString+ tryRead fp = tryIOMsg exc $ BL.readFile fp+ where exc e = fp <> " cannot be read:\n" <> e+ tryDecode :: FilePath -> BL.ByteString -> ExceptT [Char] IO NP.Package+ tryDecode fp fileBs = do+ (pkg, warnings) <- NP.unLoggingPackage+ <$> ExcT.ExceptT (pure $ first exc $ NP.decode fileBs)+ warn $ fmap NP.formatWarning warnings+ pure pkg+ where exc e = fp <> " cannot be decoded\n" <> toS e++ qte s = "\"" <> s <> "\""+ warn :: [Text] -> ErrorLogger ()+ warn ws = for_ ws $ liftIO . TIO.hPutStrLn stderr++ go :: NP.Package -> ErrorLogger ()+ go NP.Package{bin} = case argMode of+ BinMode -> traverse_ linkBin =<< case bin of+ -- files with names how they should be linked+ (NP.BinFiles bs) -> pure $ HML.toList bs+ -- a whole folder where everything should be linked+ (NP.BinFolder bf) -> do+ dirE <- liftIO $ tryJust+ (\e -> guard (IOErr.isDoesNotExistError e ||+ IOErr.isPermissionError e))+ (Dir.listDirectory bf)+ case dirE of+ (Left _) -> do+ warn ["Binary folder " <> toS (qte bf) <> " could not be accessed."]+ pure []+ (Right dir) ->+ pure $ fmap (\f -> (toS f, bf FP.</> f)) dir++ -- | Link a binary file to @targetDir/name@.+ -- @relBinPath@ is relative from the package dir.+ linkBin :: (Text, FilePath) -> ErrorLogger ()+ linkBin (name, relBinPath) = do+ let canon fp = tryIOMsg+ (\e -> "Couldn’t canonicalize path " <> qte fp <> ": " <> e)+ (Dir.canonicalizePath fp)+ pkgDir <- canon argPackageDir+ binPath <- canon $ argPackageDir FP.</> relBinPath+ targetDir <- canon argTargetDir+ when (not $ pkgDir `isPrefixOf` binPath)+ $ throwError $ mconcat+ [ "The link to executable file "+ , qte relBinPath+ , " lies outside of the package folder!\n"+ , "That’s a security risk, aborting." ]+ tryIOMsg+ (\e -> "Symlink could not be created: " <> e)+ (PosixFiles.createSymbolicLink binPath $ targetDir FP.</> toS name)
+ default.nix view
@@ -0,0 +1,17 @@+(import <nixpkgs> {+ overlays = [(pkgs: oldpkgs: {+ haskellPackages =+ let nix-lib = pkgs.callPackage ./nix-lib {};+ in oldpkgs.haskellPackages.override {+ overrides = self: super: {+ inherit (pkgs.callPackage ./nix-lib/yarn-lock.nix {} self super) yarn-lock;+ yarn2nix =+ let pkg = self.callPackage ./yarn2nix.nix {};+ in oldpkgs.haskell.lib.overrideCabal pkg (old: {+ src = nix-lib.removePrefixes [ "nix-lib" "dist" "result" ] ./.;+ });+ };+ };+ })];++}).haskellPackages.yarn2nix
+ nix-lib/buildNodePackage.nix view
@@ -0,0 +1,48 @@+{ stdenv, linkNodeDeps, nodejs }:+{ name # String+, version # String+, src # Drv+, nodeBuildInputs # Listof { name : String, drv : Drv }+, ... }@args:++# since we skip the build phase, pre and post will not work+# the caller gives them with no buildPhase+assert (args ? preBuild || args ? postBuild) -> args ? buildPhase;+# same for configurePhase+assert (args ? preConfigure || args ? postConfigure) -> args ? configurePhase;++with stdenv.lib;++stdenv.mkDerivation ((removeAttrs args [ "nodeBuildInputs" ]) // {+ name = "${name}-${version}";+ inherit version src;++ buildInputs = [ nodejs ];++ configurePhase = args.configurePhase or "true";+ # skip the build phase except when given as attribute+ dontBuild = !(args ? buildPhase);++ # TODO: maybe we can enable tests?+ doCheck = false;++ installPhase = ''+ runHook preInstall+ mkdir $out++ # a npm package is just the tarball extracted to $out+ cp -r . $out++ # then a node_modules folder is created for all its dependencies+ ${if nodeBuildInputs != []+ then ''+ rm -rf $out/node_modules+ ln -sT "${linkNodeDeps name nodeBuildInputs}" $out/node_modules+ '' else ""}++ runHook postInstall+ '';++ dontStrip = true; # stolen from npm2nix++})
+ nix-lib/default.nix view
@@ -0,0 +1,70 @@+{ lib, pkgs }:++let+ # Build an attrset of node dependencies suitable for the `nodeBuildInputs`+ # argument of `buildNodePackage`. The input is the path of a dependency+ # file generated by the `yarn2nix` utility from a `yarn.lock` file,+ # in turn the output of the `yarn` tool for an npm package.+ buildNodeDeps = lockDotNix: lib.fix+ (lib.extends+ (import lockDotNix { inherit (pkgs) fetchurl fetchgit; })+ (self: {+ # wrap the invocation in the fix point, to construct the+ # list of { name, drv } needed by buildNodePackage+ # from the templates.+ # It is basically a manual paramorphism, carrying parts of the+ # information of the previous layer (the original package name).+ _buildNodePackage = { name, ... }@args:+ { inherit name; drv = buildNodePackage args; };+ }));++ # Build a package template generated by the `yarn2nix --template`+ # utility from a yarn package. The first input is the path to the+ # template nix file, the second input is all node dependencies+ # needed by the template, in the form generated by `buildNodeDeps`.+ callTemplate = yarn2nixTemplate: allDeps:+ pkgs.callPackage yarn2nixTemplate {+ inherit buildNodePackage removePrefixes;+ } allDeps;+++ buildNodePackage = import ./buildNodePackage.nix {+ inherit linkNodeDeps;+ inherit (pkgs) stdenv nodejs;+ };++ # Link together a `node_modules` folder that can be used+ # by npm’s module system to call dependencies.+ # Also link executables of all dependencies into `.bin`.+ # TODO: copy manpages & docs as well+ # type: String -> ListOf { name: String, drv : Drv } -> Drv+ linkNodeDeps = name: packageDeps:+ pkgs.runCommand (name + "-node_modules") {} ''+ mkdir -p $out/.bin+ ${lib.concatMapStringsSep "\n"+ (dep: ''+ echo "linking node dependency ${dep.name}"+ ln -sT ${dep.drv} "$out/${dep.name}"+ ${ # TODO: remove static building once RPATHs are fixed+ pkgs.haskell.lib.justStaticExecutables+ pkgs.haskellPackages.yarn2nix}/bin/setup-node-package-paths \+ bin \+ --to=$out/.bin \+ --package=$out/${dep.name}+ '')+ packageDeps}+ '';++ # Filter out files/directories with one of the given prefix names+ # from the given path.+ # type: ListOf File -> Path -> Drv+ removePrefixes = prfxs: path:+ let+ hasPrefix = file: prfx: lib.hasPrefix ((builtins.toPath path) + "/" + prfx) file;+ hasAnyPrefix = file: lib.any (hasPrefix file) prfxs;+ in+ builtins.filterSource (file: _: ! (hasAnyPrefix file)) path;++in {+ inherit buildNodeDeps callTemplate removePrefixes;+}
+ nix-lib/yarn-lock.nix view
@@ -0,0 +1,19 @@+{ lib, pkgs }:+self: super: {+ yarn-lock =+ super.mkDerivation rec {+ pname = "yarn-lock";+ version = "0.4.0";+ # src = ../../haskell/yarn-lock;+ src = pkgs.fetchFromGitHub {+ owner = "Profpatsch";+ repo = "yarn-lock";+ rev = version;+ sha256 = "0r5ipyfpdmg9n9zv7q1knrlypv9q2l1x24j1sgibxy411b7r62wv";+ };+ license = lib.licenses.mit;+ buildDepends = with self; [ megaparsec protolude tasty-hunit tasty-th either neat-interpolation tasty-quickcheck ];+ buildTools = [ self.hpack ];+ preConfigure = ''hpack'';+ };+}
+ shell.nix view
@@ -0,0 +1,40 @@+with import <nixpkgs> {};+(haskellPackages.override {+ overrides = (self: super: {+ inherit (pkgs.callPackage ./nix-lib/yarn-lock.nix {} self super)+ yarn-lock;++ my-pkg = let+ buildDepends = with self; [+ protolude+ yarn-lock+ hnix+ hpack+ aeson+ async-pool+ ansi-wl-pprint+ regex-tdfa+ regex-tdfa-text+ neat-interpolation+ tasty-th+ tasty-quickcheck+ tasty-hunit+ http-client-tls+ monad-par+ ];+ in super.mkDerivation {+ pname = "pkg-env";+ src = "/dev/null";+ version = "none";+ license = "none";+ inherit buildDepends;+ buildTools = with self; [+ ghcid+ cabal-install+ (hoogleLocal {+ packages = buildDepends;+ })+ ];+ };+ });+}).my-pkg.env
+ src/Distribution/Nixpkgs/Nodejs/Cli.hs view
@@ -0,0 +1,97 @@+{-# LANGUAGE OverloadedStrings, LambdaCase, NoImplicitPrelude #-}+{-|+Description: command line interface+-}+module Distribution.Nixpkgs.Nodejs.Cli+( cli+)+where++import Protolude+import qualified Data.ByteString.Lazy as BL+import qualified Data.Text as T+import qualified Data.Text.IO as TIO+import qualified System.Directory as Dir++import qualified Nix.Pretty as NixP+import Text.PrettyPrint.ANSI.Leijen (putDoc)+import qualified Yarn.Lock as YL+import qualified Yarn.Lock.Types as YLT+import qualified Yarn.Lock.Helpers as YLH++import qualified Distribution.Nixpkgs.Nodejs.OptimizedNixOutput as NixOut+import qualified Distribution.Nixpkgs.Nodejs.FromPackage as NodeFP+import qualified Distribution.Nixpkgs.Nodejs.ResolveLockfile as Res+import qualified Distribution.Nodejs.Package as NP++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"++-- | Main entry point for @yarn2nix@.+cli :: [Text] -> IO ()+cli = \case+ ["--help"] -> putText usage+ ("--template":xs) -> fileLogic Node xs+ xs -> fileLogic Yarn xs+ 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+ 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 ""+ case YL.parse path fc of+ Right yarnfile -> toStdout yarnfile+ Left err -> die' ("Could not parse " <> pathT <> ":\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+ Left err -> die' ("Could not parse " <> toS path <> ":\n" <> show err)++die' :: Text -> IO a+die' err = putText err *> exitFailure+dieWithUsage :: Text -> IO ()+dieWithUsage err = die' (err <> "\n" <> usage)+++-- TODO refactor+toStdout :: YLT.Lockfile -> IO ()+toStdout 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+ Left err -> die' (T.intercalate "\n" $ toList err)+ Right res -> pure res+ -- killThread thrd+ putDoc $ NixP.prettyNix $ NixOut.mkPackageSet $ NixOut.convertLockfile lf'
+ src/Distribution/Nixpkgs/Nodejs/FromPackage.hs view
@@ -0,0 +1,48 @@+{-# LANGUAGE NoImplicitPrelude, DeriveGeneric, OverloadedStrings, RecordWildCards #-}+{-|+Description: Generate nix expression for 'NP.Package'+-}+module Distribution.Nixpkgs.Nodejs.FromPackage+( genTemplate+) where++import Protolude+import qualified Data.HashMap.Lazy as HML++import Nix.Expr+import Nix.Expr.Additions++import Distribution.Nixpkgs.Nodejs.Utils (packageKeyToSymbol)+import qualified Distribution.Nodejs.Package as NP+import qualified Yarn.Lock.Types as YLT+++depsToPkgKeys :: NP.Dependencies -> [YLT.PackageKey]+depsToPkgKeys = map (\(k, v) -> YLT.PackageKey k v) . HML.toList++-- | generate a nix expression that translates your package.nix+--+-- and can serve as template for manual adjustments+genTemplate :: NP.Package -> NExpr+genTemplate NP.Package{..} =+ simpleParamSet ["stdenv", "buildNodePackage", "filterSourcePrefixes"]+ ==> Param nodeDepsSym+ -- TODO: devDeps+ ==> ("buildNodePackage" @@ mkRecSet+ [ "name" $= nameStr+ , "version" $= mkStr version+ , "src" $= ("filterSourcePrefixes"+ @@ mkList [ mkStr "node_modules" ] @@ "./.")+ , "nodeBuildInputs" $= (letE "a" (mkSym nodeDepsSym)+ $ mkList (map (pkgDep "a") depPkgKeys))+ , "meta" $= (mkNonRecSet+ $ may "description" description+ <> may "license" license+ <> may "homepage" homepage)+ ])+ where+ depPkgKeys = depsToPkgKeys dependencies+ pkgDep depsSym pk = mkSym depsSym !!. packageKeyToSymbol pk+ nodeDepsSym = "allDeps"+ nameStr = mkStrQ [StrQ name, "-", AntiQ "version"]+ may k v = [k $= mkStr (fromMaybe mempty v)]
+ src/Distribution/Nixpkgs/Nodejs/OptimizedNixOutput.hs view
@@ -0,0 +1,310 @@+{-# LANGUAGE OverloadedStrings, NoImplicitPrelude, GeneralizedNewtypeDeriving, ViewPatterns, RecordWildCards, LambdaCase, NamedFieldPuns #-}+{-|+Description: Generate an optimized nix file from a resolved @YLT.Lockfile@++We want to generate a nix file with the following attributes:++1. easy to parse by humans+2. as short as possible+3. updating the yarn.lock generates diffs that are as short as possible++Readability means a clear structure, with definitions at the top.++Reducing the filesize means we can’t duplicate any information and keep identifiers very short. This interferes with readability, but can be amended by giving the full names in the static section and then giving them short identifiers in a second section.++Nice diffing includes having line-based output (if possible one line per package/dependency), as well as keeping the order of items stable (alphabetically sorting package names and dependencies).+-}+module Distribution.Nixpkgs.Nodejs.OptimizedNixOutput+( convertLockfile+-- * File Structure+-- $fileStructure+, mkPackageSet+-- * NOTE: fix+-- $noteFix+) where++import Protolude+import qualified Data.Map as M+import qualified Data.Text as T+import Data.Fix (Fix(Fix))+import qualified Data.MultiKeyedMap as MKM+import qualified Data.List as List+import qualified Data.List.NonEmpty as NE++import Nix.Expr (NExpr, ($=), (==>), (!.), (@@))+import Nix.Expr.Additions (($$=), (!!.))+import qualified Nix.Expr as N+import qualified Nix.Expr.Additions as NA++import qualified Yarn.Lock.Types as YLT++import qualified Distribution.Nixpkgs.Nodejs.ResolveLockfile as Res+import Distribution.Nixpkgs.Nodejs.Utils (packageKeyToSymbol)++-- | Nix symbol.+newtype NSym = NSym { unNSym :: Text }+ deriving (IsString, Ord, Eq)++-- | Nix input variable.+newtype NVar = NVar NSym+ deriving (IsString)++-- | Builder type for simple antiquoted nix strings.+data AStrVal = V NVar+ -- ^ nix antiquoted variable+ | T Text+ -- ^ normal nix string++-- | Build a nix string from multiple @AStrVal@s.+antiquote :: [AStrVal] -> NExpr+antiquote vals = Fix . N.NStr . N.DoubleQuoted+ $ flip map vals $ \case+ T t -> N.Plain t+ V (NVar (NSym t)) -> N.Antiquoted $ N.mkSym t++-- | A registry that we know of and can therefore shorten+-- into a nix function call.+data Registry = Registry+ { registrySym :: NSym+ -- ^ nix symbol used in the output file+ , registryBuilder :: NVar -> NVar -> [AStrVal]+ -- ^ constructs a nix function that in turn constructs a repository string;+ -- the function takes a package name and package version+ }++data Git = Git+ { gitUrl :: Text+ , gitRev :: Text }++-- | Final package reference used in the generated package list.+data PkgRef+ = PkgRef Text+ -- ^ reference to another package definition (e.g. @^1.2@ points to @1.2@)+ | PkgDefFile (PkgData (Either Text Registry))+ -- ^ actual definiton of a file package+ | PkgDefGit (PkgData Git)+ -- ^ actual definiton of a git package++-- | Package definition needed for calling the build function.+data PkgData a = PkgData+ { pkgDataName :: Text -- ^ package name+ , pkgDataVersion :: Text -- ^ package version+ , pkgDataUpstream :: a -- ^ points to upstream+ , pkgDataHashSum :: Text -- ^ the hash sum of the package+ , pkgDataDependencies :: [Text] -- ^ list of dependencies (as resolved nix symbols)+ }+++registries :: [(Text, Registry)]+registries =+ [ ( yarnP+ , Registry "yarn"+ $ \n v -> [T yarnP, V n, T "/-/", V n, T "-", V v, T ".tgz"] )+ ]+ where+ yarnP = "https://registry.yarnpkg.com/"++shortcuts :: M.Map [NSym] NSym+shortcuts = M.fromList+ [ (["self"], "s")+ , (["registries", "yarn"], "y")+ , (["nodeFilePackage"], "f")+ , (["nodeGitPackage"], "g")+ , (["identityRegistry"], "ir")+ ]++-- | Find out which registry the given 'YLT.Remote' shortens to.+recognizeRegistry :: Text -> Maybe Registry+recognizeRegistry fileUrl = snd <$> filterRegistry fileUrl+ where+ -- | Get registry by the prefix of the registry’s URL.+ filterRegistry url = find (\reg -> fst reg `T.isPrefixOf` url) registries+++-- | Convert a 'Res.ResolvedLockfile' to its final, nix-ready form.+convertLockfile :: Res.ResolvedLockfile -> M.Map Text PkgRef+convertLockfile = M.fromList . foldMap convert . MKM.toList+ where+ -- | For the list of package keys we generate a @PkgRef@ each+ -- and then one actual @PkgDef@.+ convert :: (NE.NonEmpty YLT.PackageKey, (Res.Resolved YLT.Package))+ -> [(Text, PkgRef)]+ convert (keys, Res.Resolved{ hashSum, resolved=pkg }) = let+ -- | Since there might be more than one key name, we choose+ -- the one with most entries.+ defName = NE.head $ maximumBy (comparing length) $ NE.group $ NE.sort $ fmap YLT.name keys+ defSym = packageKeyToSymbol $ YLT.PackageKey+ { YLT.name = defName+ , YLT.npmVersionSpec = YLT.version pkg }+ pkgDataGeneric upstream = PkgData+ { pkgDataName = defName+ , pkgDataVersion = YLT.version pkg+ , pkgDataUpstream = upstream+ , pkgDataHashSum = hashSum+ , pkgDataDependencies = map packageKeyToSymbol+ -- TODO: handle optional dependencies better+ $ YLT.dependencies pkg <> YLT.optionalDependencies pkg+ }+ def = case YLT.remote pkg of+ YLT.FileRemote{fileUrl} ->+ PkgDefFile $ pkgDataGeneric $ note fileUrl $ recognizeRegistry fileUrl+ YLT.GitRemote{gitRepoUrl, gitRev} ->+ PkgDefGit $ pkgDataGeneric $ Git gitRepoUrl gitRev+ -- we don’t need another ref indirection+ -- if that’s already the name of our def+ refNames = List.delete defSym $ toList $ NE.nub+ $ fmap packageKeyToSymbol keys+ in (defSym, def) : map (\rn -> (rn, PkgRef defSym)) refNames+++{- $fileStructure++@+{ fetchgit, fetchurl }:+# self & super: see notes on fix+self: super:+let+ # shorten the name of known package registries+ registries = {+ yarn = n: v: "https://registry.yarnpkg.com/${n}/-/${n}-${v}.tgz";+ };++ sanitizePackageName = builtins.replaceStrings ["@" "/"] ["-" "-"];++ # We want each package definition to be one line, by putting+ # the boilerplate into these functions for different remotes.+ nodeFilePackage = …+ nodeGitPackage = …++ # an identity function for e.g. git repos or unknown registries+ identityRegistry = url: _: _: url;++ # shortcut section+ s = self;+ ir = identityRegistry;+ f = nodeFilePackage;+ g = nodeGitPackage;+ y = registries.yarnpkg;+ …++# the actual package definitions+in {+ "accepts@~1.3.3" = s."accepts@1.3.3";+ "accepts@1.3.3" = f "accepts" "1.3.3" y "sha" [];+ "babel-core@^6.14.0" = s."babel-core@6.24.1";+ "babel-core@6.24.1" = f "babel-core" "6.24.1" y "a0e457c58ebdbae575c9f8cd75127e93756435d8" [+ s."accepts@~1.3.3"+ ];+}+@+-}++-- | Convert a list of packages prepared with 'convertLockfile'+-- to a nix expression.+mkPackageSet :: M.Map Text PkgRef -> NExpr+mkPackageSet packages =+ NA.simpleParamSet ["fetchurl", "fetchgit"]+ -- enable self-referencing of packages+ -- with string names with a self/super fix+ -- see note FIX+ ==> N.Param "self" ==> N.Param "super"+ ==> N.mkLets+ ( [ "registries" $= N.mkNonRecSet (fmap (mkRegistry . snd) registries)+ , "sanitizePackageName" $= sanitizePackageName+ , "nodeFilePackage" $= buildPkgFn+ , "nodeGitPackage" $= buildPkgGitFn+ , "identityRegistry" $= NA.multiParam ["url", "_", "_"] "url" ]+ <> fmap mkShortcut (M.toList shortcuts) )+ (N.mkNonRecSet (map mkPkg $ M.toAscList packages))+ where+ mkRegistry (Registry{..}) = unNSym registrySym $=+ (N.Param "n" ==> N.Param "v" ==> antiquote (registryBuilder "n" "v"))++ concatNSyms :: [NSym] -> NExpr+ concatNSyms [] = panic "non-empty shortcut syms!"+ concatNSyms (l:ls) = foldl (!.) (N.mkSym $ unNSym l) (fmap unNSym ls)+ mkShortcut :: ([NSym], NSym) -> N.Binding NExpr+ mkShortcut (nSyms, short) = unNSym short $= concatNSyms nSyms+ -- | Try to shorten sym, otherwise use input.+ shorten :: [NSym] -> NExpr+ shorten s = maybe (concatNSyms s) (N.mkSym . unNSym) $ M.lookup s shortcuts++ -- | remove symbols not allowed in nix derivation names+ sanitizePackageName :: NExpr+ sanitizePackageName = "builtins" !. "replaceStrings"+ @@ N.mkList [N.mkStr "@", N.mkStr "/"]+ @@ N.mkList [N.mkStr "-", N.mkStr "-"]++ -- | Build function boilerplate the build functions share in common.+ buildPkgFnGeneric :: [Text] -> NExpr -> NExpr+ buildPkgFnGeneric additionalArguments srcNExpr =+ NA.multiParam (["name", "version"] <> additionalArguments <> ["deps"])+ $ ("super" !!. "_buildNodePackage") @@ N.mkNonRecSet+ [ "name" $= ("sanitizePackageName" @@ "name")+ , N.inherit $ map N.StaticKey ["version"]+ , "src" $= srcNExpr+ , "nodeBuildInputs" $= "deps" ]+ -- | Building a 'YLT.FileRemote' package.+ buildPkgFn :: NExpr+ buildPkgFn =+ buildPkgFnGeneric ["registry", "sha1"]+ ("fetchurl" @@ N.mkNonRecSet+ [ "url" $= ("registry" @@ "name" @@ "version")+ , N.inherit $ [N.StaticKey "sha1"] ])+ -- | Building a 'YLT.GitRemote' package.+ buildPkgGitFn :: NExpr+ buildPkgGitFn =+ buildPkgFnGeneric ["url", "rev", "sha256"]+ ("fetchgit" @@ N.mkNonRecSet+ [ N.inherit $ map N.StaticKey ["url", "rev", "sha256"] ])++ mkDefGeneric :: PkgData a -> NSym -> [NExpr] -> NExpr+ mkDefGeneric PkgData{..} buildFnSym additionalArguments =+ foldl' (@@) (shorten [buildFnSym])+ $ [ N.mkStr pkgDataName+ , N.mkStr pkgDataVersion ]+ <> additionalArguments <>+ [ N.mkList $ map (N.mkSym selfSym !!.) pkgDataDependencies ]++ mkPkg :: (Text, PkgRef) -> N.Binding NExpr+ mkPkg (key, pkgRef) = key $$= case pkgRef of+ PkgRef t -> N.mkSym selfSym !!. t+ PkgDefFile pd@PkgData{pkgDataUpstream, pkgDataHashSum} ->+ mkDefGeneric pd "nodeFilePackage"+ [ either (\url -> shorten ["identityRegistry"] @@ N.mkStr url )+ (\reg -> shorten ["registries", registrySym reg])+ pkgDataUpstream+ , N.mkStr pkgDataHashSum ]+ PkgDefGit pd@PkgData{pkgDataUpstream = Git{..}, pkgDataHashSum} ->+ mkDefGeneric pd "nodeGitPackage"+ [ N.mkStr gitUrl, N.mkStr gitRev, N.mkStr pkgDataHashSum ]++ selfSym :: Text+ selfSym = "s"++{- $noteFix++@+self: super:+@++follows the fixpoint scheme first introduced+by the @haskellPackage@ set in @nixpkgs@.+See the @Overlays@ documentation in the @nixpkgs@+manual for explanations of how this works.++Note: originally, this was a shallow fix like++@+let attrs = self: {+ "foo bar" = 1;+ bar = self."foo bar" + 2;+ };+in fix attrs+@++which was just in place to work around referencing+attrset attributes through string names.+The new method is a lot more general and allows deep+overrides of arbitrary packages in the dependency set.+-}
+ src/Distribution/Nixpkgs/Nodejs/ResolveLockfile.hs view
@@ -0,0 +1,74 @@+{-# LANGUAGE OverloadedStrings, TupleSections, ScopedTypeVariables, ViewPatterns, RecordWildCards, NoImplicitPrelude, LambdaCase, NamedFieldPuns, GeneralizedNewtypeDeriving, DeriveFunctor #-}+-- TODO: remove exts+{-|+Description: IO-based resolving of missing hashes++Resolving a 'YLT.Lockfile' and generating all necessary data (e.g. hashes), so that it can be converted to a nix expression. Might need IO & network access to succeed.+-}+module Distribution.Nixpkgs.Nodejs.ResolveLockfile+( resolveLockfileStatus+, Resolved(..), ResolvedLockfile+) where++import Protolude+import qualified Control.Monad.Trans.Either as E+import qualified Data.List.NonEmpty as NE+import qualified Data.MultiKeyedMap as MKM+import qualified Data.Aeson as Aeson+import qualified Data.Aeson.Types as AesonT+import qualified System.Process as Process++import qualified Control.Concurrent.Async.Pool as Async+import qualified Control.Monad.STM as STM++import qualified Yarn.Lock.Types as YLT++maxFetchers :: Int+maxFetchers = 5++-- | A thing whose hash is already known (“resolved”).+--+-- Only packages with known hashes are truly “locked”.+data Resolved a = Resolved+ { hashSum :: Text+ , resolved :: a+ } deriving (Show, Eq, Functor)++-- | In order to write a nix file, all packages need to know their shasums first.+type ResolvedLockfile = MKM.MKMap YLT.PackageKey (Resolved YLT.Package)++-- | Resolve all packages by downloading their sources if necessary.+resolveLockfileStatus :: (Chan YLT.Remote) -> YLT.Lockfile+ -> IO (Either (NE.NonEmpty Text) ResolvedLockfile)+resolveLockfileStatus msgChan lf = Async.withTaskGroup maxFetchers $ \taskGroup -> do+ job <- STM.atomically $ Async.mapReduce taskGroup+ $ fmap (\(ks, pkg) -> (:[]) <$> (E.runEitherT $ do+ liftIO $ writeChan msgChan (YLT.remote pkg)+ res <- resolve pkg+ pure (ks, res)))+ $ MKM.toList lf+ resolved <- Async.wait job+ case partitionEithers resolved of+ (x:xs, _ ) -> pure $ Left $ x NE.:| xs+ (_ , ys) -> pure $ Right $ MKM.fromList YLT.lockfileIkProxy ys++ where+ resolve :: YLT.Package -> E.EitherT Text IO (Resolved YLT.Package)+ resolve pkg = case YLT.remote pkg of+ YLT.FileRemote{..} -> pure $ r fileSha1+ YLT.GitRemote{..} -> r <$> fetchFromGit gitRepoUrl gitRev+ where+ r sha = Resolved { hashSum = sha, resolved = pkg }++ fetchFromGit :: Text -> Text -> E.EitherT Text IO Text+ fetchFromGit repo rev = do+ res <- liftIO $ Process.readProcessWithExitCode "nix-prefetch-git"+ ["--url", toS repo, "--rev", toS rev, "--hash", "sha256"] ""+ case res of+ ((ExitFailure _), _, err) -> E.left $ toS err+ (ExitSuccess, out, _) -> E.hoistEither+ $ first (\decErr -> "parsing json output failed:\n"+ <> toS decErr <> "\nThe output was:\n" <> toS out)+ $ do val <- Aeson.eitherDecode' (toS out)+ AesonT.parseEither+ (Aeson.withObject "PrefetchOutput" (Aeson..: "sha256")) val
+ src/Distribution/Nixpkgs/Nodejs/Utils.hs view
@@ -0,0 +1,11 @@+{-# LANGUAGE NoImplicitPrelude, OverloadedStrings, RecordWildCards #-}+{-|+Description: Misc utils+-}+module Distribution.Nixpkgs.Nodejs.Utils where+import Protolude+import qualified Yarn.Lock.Types as YLT++-- | Representation of a PackageKey as nix attribute name.+packageKeyToSymbol :: YLT.PackageKey -> Text+packageKeyToSymbol (YLT.PackageKey{..}) = name <> "@" <> npmVersionSpec
+ src/Distribution/Nodejs/Package.hs view
@@ -0,0 +1,147 @@+{-# LANGUAGE NoImplicitPrelude, DeriveGeneric, OverloadedStrings, RecordWildCards, LambdaCase #-}+{-|+Description: Parse and make sense of npm’s @package.json@ project files++They are documented on https://docs.npmjs.com/files/package.json and have a few gotchas. Luckily plain JSON, but the interpretation of certain fields is non-trivial (since they contain a lot of “sugar”).+-}+module Distribution.Nodejs.Package+( -- * Parsing @package.json@+ LoggingPackage(..), decode+, Warning(..), formatWarning+ -- * @package.json@ data+, Package(..)+, Bin(..), Man(..), Dependencies+) where++import Protolude+import Control.Monad (fail)+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 qualified Data.Aeson as A+import qualified Data.Aeson.Types as AT++-- | npm `package.json`. Not complete.+--+-- See https://docs.npmjs.com/files/package.json+data Package = Package+ { name :: Text+ , version :: Text+ , description :: Maybe Text+ , homepage :: Maybe Text+ , private :: Bool+ , scripts :: HML.HashMap Text Text+ , bin :: Bin+ , man :: Man+ , license :: Maybe Text+ , dependencies :: Dependencies+ , devDependencies :: Dependencies+ } deriving (Show, Eq)++-- | 'Package' with a potential bunch of parsing warnings.+-- Note the 'A.FromJson' instance.+newtype LoggingPackage = LoggingPackage+ { unLoggingPackage :: (Package, [Warning]) }++-- | Possible warnings from parsing.+data Warning+ = WrongType+ { wrongTypeField :: Text+ , wrongTypeDefault :: Text }+ | PlainWarning Text++-- | The package’s executable files.+data Bin+ = BinFiles (HML.HashMap Text 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)+ deriving (Show, Eq)++-- | The package’s manual files.+data Man+ = ManFiles (HML.HashMap Text 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++-- | See https://github.com/npm/normalize-package-data for+-- normalization steps used by npm itself.+instance A.FromJSON LoggingPackage where+ parseJSON = A.withObject "Package" $ \v -> fmap LoggingPackage . WL.runWriterT $ do+ let+ l :: AT.Parser a -> WL.WriterT [Warning] AT.Parser a+ l = WL.WriterT . fmap (\a -> (a, []))+ tryWarn :: (AT.FromJSON a, Show a)+ => Text -> a -> WL.WriterT [Warning] AT.Parser a+ tryWarn field def = lift (v .:? field .!= def)+ <|> WL.writer (def, [WrongType field (show def)])+ name <- l $ v .: "name"+ version <- l $ v .: "version"+ description <- tryWarn "description" Nothing+ homepage <- tryWarn "homepage" Nothing+ private <- tryWarn "private" False+ scripts <- l $ v .:? "scripts" .!= mempty+ bin <- parseBin name v+ man <- l $ parseMan name v+ license <- tryWarn "license" Nothing+ dependencies <- l $ v .:? "dependencies" .!= mempty+ devDependencies <- l $ v .:? "devDependencies" .!= mempty+ pure Package{..}+ where++ parseBin :: Text -> AT.Object -> WL.WriterT [Warning] AT.Parser Bin+ parseBin packageName v = do+ -- check for existence of these fields+ binVal <- lift $ optional $ v .: "bin"+ dirBinVal <- lift $ optional $ v .: "directories" >>= (.: "bin")+ -- now check for all possible cases of the fields+ -- see npm documentation for more+ case (binVal, dirBinVal) of+ (Just _ , Just _) ->+ WL.writer (BinFiles mempty, [PlainWarning+ "`bin` and `directories.bin` must not exist at the same time."])+ -- 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)+ -- or it’s a map from names to paths+ (Just (A.Object bins), _) -> lift $ BinFiles+ <$> traverse (A.withText "BinPath" (pure.toS)) bins+ (Just _ , _) -> fail+ $ "`bin` must be a path or a map of names to paths."+ (_ , Just (A.String path)) -> pure $ BinFolder $ toS path+ (_ , Just _) -> fail+ $ "`directories.bin` must be a path."+ -- if no executables are given, return an empty set+ (Nothing , Nothing) -> pure . BinFiles $ mempty++ -- 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 file =+ let f = T.pack $ FP.takeFileName file+ in if name `T.isPrefixOf` f+ then (name, file)+ else (name <> "-" <> f, file)+ -- TODO: handle directories.man+ (getMan (HML.fromList . map extractName)+ <|> getMan (HML.fromList . (:[]) . extractName)+ <|> pure (ManFiles mempty))++-- | Convenience decoding function.+decode :: BL.ByteString -> Either Text LoggingPackage+decode = first toS . A.eitherDecode++-- | Convert a @package.json@ parsing warning to plain text.+formatWarning :: Warning -> Text+formatWarning = ("Warning: " <>) . \case+ (WrongType field def) ->+ "Field \"" <> field <> "\" has the wrong type. Defaulting to " <> def <> "."+ (PlainWarning t) -> t
+ src/Nix/Expr/Additions.hs view
@@ -0,0 +1,76 @@+{-# LANGUAGE TupleSections, OverloadedStrings #-}+{-|+Description: Additional functions that should probably be in @hnix@++Nix generation helpers. No guarantee of stability (internal!).+-}+module Nix.Expr.Additions+( stringKey, ($$=), dynamicKey+, simpleParamSet, multiParam+, (!!.)+, StrQ(..), mkStrQ, mkStrQI+) where++import Data.Fix (Fix(..))+import Data.Text (Text)+import Data.String (IsString(..))++import Nix.Expr+import Text.Regex.TDFA.Text ()+import Text.Regex.TDFA ((=~))++-- hnix helpers+-- TODO submit upstream++-- | Make a binding, but have the key be a string, not symbol.+stringKey :: Text -> NExpr -> Binding NExpr+stringKey k v = NamedVar [dynamicKey k] v+-- | Infix version of 'stringKey'.+($$=) :: Text -> NExpr -> Binding NExpr+($$=) = stringKey+infixr 2 $$=++-- | Make a dynamic key name that is only enclosed in double quotes (no antiquotes).+dynamicKey :: Text -> NKeyName NExpr+dynamicKey k = DynamicKey $ Plain $ DoubleQuoted [Plain k]++-- | shortcut to create a list of closed params, like @{ foo, bar, baz }:@+simpleParamSet :: [Text] -> Params NExpr+simpleParamSet = mkParamset . fmap (, Nothing)++-- | shortcut to create a list of multiple params, like @a: b: c:@+multiParam :: [Text] -> NExpr -> NExpr+multiParam ps expr = foldr mkFunction expr $ map Param ps++-- TODO: switch over to !. when+-- https://github.com/jwiegley/hnix/commit/8b4c137a3b125f52bb78039a9d201492032b38e8+-- goes upstream+-- | Like '!.', but automatically convert plain strings to static keys.+(!!.) :: NExpr -> Text -> NExpr+aset !!. k = Fix+ $ NSelect aset+ [(if isPlainSymbol k then StaticKey else dynamicKey) k] Nothing+ where+ -- the nix lexer regex for IDs (symbols) is + -- [a-zA-Z\_][a-zA-Z0-9\_\'\-]*+ isPlainSymbol :: Text -> Bool+ isPlainSymbol s = s =~ ("^[a-zA-Z_][a-zA-Z0-9_'-]*$" :: Text)+infixl 8 !!.+++-- | String quotation, either a plain string (S) or antiquoted (A)+data StrQ = StrQ !Text | AntiQ !NExpr+instance IsString StrQ where+ fromString = StrQ . fromString++-- +mkStrQtmpl :: ([Antiquoted Text NExpr] -> NString NExpr) -> [StrQ] -> NExpr+mkStrQtmpl strtr = Fix . NStr . strtr . map trans+ where trans (StrQ t) = Plain t+ trans (AntiQ r) = Antiquoted r++mkStrQ, mkStrQI :: [StrQ] -> NExpr+-- | Create a double-quoted string from a list of antiquotes/plain strings.+mkStrQ = mkStrQtmpl DoubleQuoted+-- | Create a single-quoted string from a list of antiquotes/plain strings.+mkStrQI = mkStrQtmpl Indented
+ tests/Test.hs view
@@ -0,0 +1,9 @@+module Main where++import Test.Tasty+import qualified TestNpmjsPackage as Npmjs++main :: IO ()+main = defaultMain $ testGroup "tests"+ [ Npmjs.tests+ ]
+ tests/TestNpmjsPackage.hs view
@@ -0,0 +1,54 @@+{-# LANGUAGE OverloadedStrings, TemplateHaskell, QuasiQuotes, NoImplicitPrelude #-}+module TestNpmjsPackage (tests) where++import Protolude+import Test.Tasty (TestTree)+import Test.Tasty.TH+import Test.Tasty.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 Distribution.Nodejs.Package as NP++baseAnd :: [(Text, A.Value)] -> A.Value+baseAnd fields = A.Object $ HML.fromList $+ [ ("name", "foopkg")+ , ("version", "1.0.2")+ ] <> fields++case_binPaths :: Assertion+case_binPaths = do+ parseSuccess bin (baseAnd [ ("bin", "./abc") ])+ >>= assertEqual "bin path" (NP.BinFiles $ HML.fromList+ [ ("foopkg", "./abc") ])+ parseSuccess bin (baseAnd [ ("directories", A.object [("bin", "./abc")]) ])+ >>= assertEqual "bin path" (NP.BinFolder "./abc")+ parseSuccess bin (baseAnd [ ("bin", A.object+ [ ("one", "./bin/one")+ , ("two", "imhere") ]) ])+ >>= assertEqual "bin multiple" (NP.BinFiles $ HML.fromList+ [ ("one", "./bin/one")+ , ("two", "imhere") ])+ -- TODO: succeeds with warning; test whether the correct warnings are generated+ -- parseFailure bin (baseAnd [ ("bin", "foo")+ -- , ("directories", A.object [("bin", "foo")]) ])+ parseSuccess bin (baseAnd [])+ >>= assertEqual "no bins" (NP.BinFiles mempty)+ where+ bin = fmap (NP.bin . fst . NP.unLoggingPackage) . A.parseJSON++parseSuccess :: (A.Value -> AT.Parser a) -> A.Value -> IO a+parseSuccess p v = case AT.parse p v of+ (AT.Error err) -> assertFailure err >> panic "not reached"+ (AT.Success a) -> pure a+ +parseFailure :: Show a => (A.Value -> AT.Parser a) -> A.Value -> IO ()+parseFailure p v = case AT.parse p v of+ (AT.Error _) -> pass+ (AT.Success a) -> assertFailure $ "no parse" <> show a++tests :: TestTree+tests = $(testGroupGenerator)
+ yarn2nix.cabal view
@@ -0,0 +1,162 @@+-- This file has been generated from package.yaml by hpack version 0.18.1.+--+-- see: https://github.com/sol/hpack++name: yarn2nix+version: 0.5.0+synopsis: Convert yarn.lock files to nix expressions+description: Convert @yarn.lock@ files to nix expressions. See @yarn2nix@ executable. Contains a nix library to call the generated nix files in @nix-lib/@. Library functions and module names might be restructured in the future.+category: Distribution, Nix+homepage: https://github.com/Profpatsch/yarn2nix#readme+bug-reports: https://github.com/Profpatsch/yarn2nix/issues+author: Profpatsch+maintainer: mail@profpatsch.de+license: MIT+license-file: LICENSE+build-type: Simple+cabal-version: >= 1.10++extra-source-files:+ default.nix+ LICENSE+ nix-lib/buildNodePackage.nix+ nix-lib/default.nix+ nix-lib/yarn-lock.nix+ README.md+ shell.nix+ yarn2nix.nix++source-repository head+ type: git+ location: https://github.com/Profpatsch/yarn2nix++library+ hs-source-dirs:+ src+ ghc-options: -Wall+ build-depends:+ aeson >= 1.0 && < 1.3+ , ansi-wl-pprint == 0.6.*+ , async-pool == 0.9.*+ , base == 4.*+ , bytestring == 0.10.*+ , containers >= 0.5 && < 0.7+ , data-fix >= 0.0.7 && < 0.1+ , directory == 1.3.*+ , either == 4.4.*+ , filepath == 1.4.*+ , hnix == 0.3.*+ , mtl == 2.2.*+ , process == 1.4.*+ , protolude == 0.1.*+ , regex-tdfa == 1.2.*+ , regex-tdfa-text == 1.0.0.*+ , stm == 2.4.*+ , text == 1.2.*+ , unordered-containers == 0.2.*+ , yarn-lock == 0.4.*+ exposed-modules:+ Distribution.Nixpkgs.Nodejs.Cli+ Distribution.Nixpkgs.Nodejs.FromPackage+ Distribution.Nixpkgs.Nodejs.OptimizedNixOutput+ Distribution.Nixpkgs.Nodejs.ResolveLockfile+ Distribution.Nixpkgs.Nodejs.Utils+ Distribution.Nodejs.Package+ Nix.Expr.Additions+ other-modules:+ Paths_yarn2nix+ default-language: Haskell2010++executable setup-node-package-paths+ main-is: SetupNodePackagePaths.hs+ ghc-options: -Wall+ build-depends:+ aeson >= 1.0 && < 1.3+ , ansi-wl-pprint == 0.6.*+ , async-pool == 0.9.*+ , base == 4.*+ , bytestring == 0.10.*+ , containers >= 0.5 && < 0.7+ , data-fix >= 0.0.7 && < 0.1+ , directory == 1.3.*+ , either == 4.4.*+ , filepath == 1.4.*+ , hnix == 0.3.*+ , mtl == 2.2.*+ , process == 1.4.*+ , protolude == 0.1.*+ , regex-tdfa == 1.2.*+ , regex-tdfa-text == 1.0.0.*+ , stm == 2.4.*+ , text == 1.2.*+ , unordered-containers == 0.2.*+ , yarn-lock == 0.4.*+ , optparse-applicative == 0.13.*+ , unix == 2.7.*+ , yarn2nix+ default-language: Haskell2010++executable yarn2nix+ main-is: Main.hs+ ghc-options: -Wall+ build-depends:+ aeson >= 1.0 && < 1.3+ , ansi-wl-pprint == 0.6.*+ , async-pool == 0.9.*+ , base == 4.*+ , bytestring == 0.10.*+ , containers >= 0.5 && < 0.7+ , data-fix >= 0.0.7 && < 0.1+ , directory == 1.3.*+ , either == 4.4.*+ , filepath == 1.4.*+ , hnix == 0.3.*+ , mtl == 2.2.*+ , process == 1.4.*+ , protolude == 0.1.*+ , regex-tdfa == 1.2.*+ , regex-tdfa-text == 1.0.0.*+ , stm == 2.4.*+ , text == 1.2.*+ , unordered-containers == 0.2.*+ , yarn-lock == 0.4.*+ , yarn2nix+ default-language: Haskell2010++test-suite yarn2nix-tests+ type: exitcode-stdio-1.0+ main-is: Test.hs+ hs-source-dirs:+ tests+ ghc-options: -Wall+ build-depends:+ aeson >= 1.0 && < 1.3+ , ansi-wl-pprint == 0.6.*+ , async-pool == 0.9.*+ , base == 4.*+ , bytestring == 0.10.*+ , containers >= 0.5 && < 0.7+ , data-fix >= 0.0.7 && < 0.1+ , directory == 1.3.*+ , either == 4.4.*+ , filepath == 1.4.*+ , hnix == 0.3.*+ , mtl == 2.2.*+ , process == 1.4.*+ , protolude == 0.1.*+ , regex-tdfa == 1.2.*+ , regex-tdfa-text == 1.0.0.*+ , stm == 2.4.*+ , text == 1.2.*+ , unordered-containers == 0.2.*+ , yarn-lock == 0.4.*+ , neat-interpolation == 0.3.*+ , protolude == 0.1.*+ , tasty == 0.11.*+ , tasty-hunit == 0.9.*+ , tasty-quickcheck == 0.8.*+ , tasty-th == 0.1.7.*+ , yarn2nix+ other-modules:+ TestNpmjsPackage+ default-language: Haskell2010
+ yarn2nix.nix view
@@ -0,0 +1,37 @@+{ 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.1.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;+}