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 +2/−5
- NodePackageTool.hs +22/−8
- README.md +43/−28
- default.nix +0/−32
- nix-lib/buildNodePackage.nix +2/−2
- nix-lib/default.nix +43/−2
- nix-lib/old-version-dependencies.nix +0/−19
- shell.nix +0/−39
- src/Distribution/Nixpkgs/Nodejs/Cli.hs +135/−45
- src/Distribution/Nixpkgs/Nodejs/FromPackage.hs +23/−24
- src/Distribution/Nixpkgs/Nodejs/License.hs +128/−0
- src/Distribution/Nixpkgs/Nodejs/OptimizedNixOutput.hs +5/−3
- src/Distribution/Nixpkgs/Nodejs/ResolveLockfile.hs +22/−4
- src/Distribution/Nixpkgs/Nodejs/Utils.hs +11/−0
- src/Distribution/Nodejs/Package.hs +63/−22
- tests/TestNpmjsPackage.hs +122/−38
- yarn2nix.cabal +41/−39
- yarn2nix.nix +0/−37
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;-}