packages feed

yarn2nix-0.10.1: src/Distribution/Nixpkgs/Nodejs/Cli.hs

{-# LANGUAGE OverloadedStrings, LambdaCase, NoImplicitPrelude #-}
{-# LANGUAGE TypeApplications #-}
{-|
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 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 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

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
import Distribution.Nixpkgs.Nodejs.ResolveLockfile (ResolverConfig(ResolverConfig, resolveOffline))
import Distribution.Nixpkgs.Nodejs.License (LicensesBySpdxId)
import qualified Data.Aeson as 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 :: 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
    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
      fc <- catchCouldNotOpen path $ readFile path
      case YL.parse path fc of
        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

          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 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 :: RunConfig -> YLT.Lockfile -> IO ()
toStdout cfg lf = do
  ch <- newChan
  -- thrd <- forkIO $ forever $ do
  --   readChan ch >>= \case
  --     FileRemote{..} -> pass
  --     GitRemote{..} -> print $ "Downloading " <> gitRepoUrl
  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
  RT.putDoc $ NixP.prettyNix $ NixOut.mkPackageSet $ NixOut.convertLockfile lf'