1{-# LANGUAGE OverloadedStrings #-}
2{-# LANGUAGE RecordWildCards #-}
3{-# LANGUAGE TupleSections #-}
4{-# LANGUAGE GeneralizedNewtypeDeriving #-}
6module Language.PureScript.Publish
10 , getVersionFromGitTag
12 , getModulesAndBookmarks
13 , getResolvedDependencies
16import Prelude hiding (userError)
19import Data.Char (isSpace)
20import Data.String (fromString)
21import Data.List (stripPrefix, isSuffixOf, (\\), nubBy)
22import Data.List.Split (splitOn)
23import Data.List.NonEmpty (NonEmpty(..))
25import Data.Function (on)
27import Data.Aeson.BetterErrors
28import qualified Data.Text as T
30import Control.Applicative
31import Control.Category ((>>>))
32import Control.Arrow ((***))
33import Control.Exception (catch, try)
34import Control.Monad.Trans.Except
35import Control.Monad.Error.Class (MonadError(..))
36import Control.Monad.Writer
38import System.Directory (doesFileExist)
39import System.Process (readProcess)
40import System.Exit (exitFailure)
41import qualified System.FilePath.Glob as Glob
43import Web.Bower.PackageMeta (PackageMeta(..), BowerError(..), PackageName,
44 runPackageName, parsePackageName, Repository(..))
45import qualified Web.Bower.PackageMeta as Bower
47import qualified Language.PureScript as P (version)
48import qualified Language.PureScript.Docs as D
49import Language.PureScript.Publish.Utils
50import Language.PureScript.Publish.ErrorsWarnings
52-- | Attempt to retrieve package metadata from the current directory.
53-- Calls exitFailure if no package metadata could be retrieved.
54preparePackage :: IO D.UploadedPackage
56 runPrepareM preparePackage'
57 >>= either (\e -> printError e >> exitFailure)
60 handleWarnings (result, warns) = do
65 PrepareM { unPrepareM :: WriterT [PackageWarning] (ExceptT PackageError IO) a }
66 deriving (Functor, Applicative, Monad,
67 MonadWriter [PackageWarning],
68 MonadError PackageError)
70-- This MonadIO instance ensures that IO errors don't crash the program.
71instance MonadIO PrepareM where
73 lift' (try act) >>= either (otherError . IOExceptionThrown) return
75 lift' :: IO a -> PrepareM a
76 lift' = PrepareM . lift . lift
78runPrepareM :: PrepareM a -> IO (Either PackageError (a, [PackageWarning]))
79runPrepareM = runExceptT . runWriterT . unPrepareM
81warn :: PackageWarning -> PrepareM ()
84userError :: UserError -> PrepareM a
85userError = throwError . UserError
87internalError :: InternalError -> PrepareM a
88internalError = throwError . InternalError
90otherError :: OtherError -> PrepareM a
91otherError = throwError . OtherError
93catchLeft :: Applicative f => Either a b -> (a -> f b) -> f b
94catchLeft a f = either f pure a
96preparePackage' :: PrepareM D.UploadedPackage
98 exists <- liftIO (doesFileExist "bower.json")
99 unless exists (userError BowerJSONNotFound)
101 pkgMeta <- liftIO (Bower.decodeFile "bower.json")
102 >>= flip catchLeft (userError . CouldntParseBowerJSON)
103 (pkgVersionTag, pkgVersion) <- getVersionFromGitTag
104 pkgGithub <- getBowerInfo pkgMeta
105 (pkgBookmarks, pkgModules) <- getModulesAndBookmarks
107 let declaredDeps = map fst (bowerDependencies pkgMeta ++
108 bowerDevDependencies pkgMeta)
109 pkgResolvedDependencies <- getResolvedDependencies declaredDeps
111 let pkgUploader = D.NotYetKnown
112 let pkgCompilerVersion = P.version
116getModulesAndBookmarks :: PrepareM ([D.Bookmark], [D.Module])
117getModulesAndBookmarks = do
118 (inputFiles, depsFiles) <- liftIO getInputAndDepsFiles
119 liftIO (D.parseAndDesugar inputFiles depsFiles renderModules)
120 >>= either (userError . ParseAndDesugarError) return
122 renderModules bookmarks modules =
123 return (bookmarks, map D.convertModule modules)
125getVersionFromGitTag :: PrepareM (String, Version)
126getVersionFromGitTag = do
127 out <- readProcess' "git" ["tag", "--list", "--points-at", "HEAD"] ""
128 let vs = map trimWhitespace (lines out)
129 case mapMaybe parseMay vs of
130 [] -> userError TagMustBeCheckedOut
132 xs -> userError (AmbiguousVersions (map snd xs))
135 dropWhile isSpace >>> reverse >>> dropWhile isSpace >>> reverse
137 (str,) <$> D.parseVersion' (dropPrefix "v" str)
138 dropPrefix prefix str =
139 fromMaybe str (stripPrefix prefix str)
141getBowerInfo :: PackageMeta -> PrepareM (D.GithubUser, D.GithubRepo)
142getBowerInfo = either (userError . BadRepositoryField) return . tryExtract
145 case bowerRepository pkgMeta of
146 Nothing -> Left RepositoryFieldMissing
147 Just Repository{..} -> do
148 unless (repositoryType == "git")
149 (Left (BadRepositoryType repositoryType))
150 maybe (Left NotOnGithub) Right (extractGithub repositoryUrl)
152extractGithub :: String -> Maybe (D.GithubUser, D.GithubRepo)
154 stripPrefix "git://github.com/"
155 >>> fmap (splitOn "/")
157 >>> fmap (D.GithubUser *** (D.GithubRepo . dropDotGit))
160 takeTwo :: [a] -> Maybe (a, a)
161 takeTwo [x, y] = Just (x, y)
164 dropDotGit :: String -> String
166 | ".git" `isSuffixOf` str = take (length str - 4) str
169readProcess' :: String -> [String] -> String -> PrepareM String
170readProcess' prog args stdin = do
171 out <- liftIO (catch (Right <$> readProcess prog args stdin)
173 either (otherError . ProcessFailed prog args) return out
177 -- ^ Listed in bower.json, but not installed.
179 -- ^ In the output of `bower list --json --offline`, there was no
180 -- _resolution key. This can be caused by adding the dependency using
181 -- `bower link`, or simply copying it into bower_components instead of
182 -- installing it normally.
183 | ResolvedOther String
184 -- ^ Resolved, but to something other than a version. The String argument
185 -- is the resolution type. The values it can take that I'm aware of are
186 -- "commit" and "branch".
187 | ResolvedVersion String
188 -- ^ Resolved to a version. The String argument is the resolution tag (eg,
192-- Go through all bower dependencies which contain purescript code, and
193-- extract their versions.
195-- In the case where a bower dependency is taken from a particular version,
196-- that's easy; take that version. In any other case (eg, a branch, or a commit
197-- sha) we print a warning that documentation links will not work, and avoid
198-- linking to documentation for any types from that package.
200-- The rationale for this is: people will prefer to use a released version
201-- where possible. If they are not using a released version, then this is
202-- probably for a reason. However, docs are only ever available for released
203-- versions. Therefore there will probably be no version of the docs which is
204-- appropriate to link to, and we should omit links.
205getResolvedDependencies :: [PackageName] -> PrepareM [(PackageName, Version)]
206getResolvedDependencies declaredDeps = do
207 depsBS <- fromString <$> readProcess' "bower" ["list", "--json", "--offline"] ""
209 -- Check for undeclared dependencies
210 toplevels <- catchJSON (parse asToplevelDependencies depsBS)
211 warnUndeclared declaredDeps toplevels
213 deps <- catchJSON (parse asResolvedDependencies depsBS)
217 catchJSON = flip catchLeft (internalError . JSONError FromBowerList)
219-- | Extracts all dependencies and their versions from
220-- `bower list --json --offline`
221asResolvedDependencies :: Parse BowerError [(PackageName, DependencyStatus)]
222asResolvedDependencies = nubBy ((==) `on` fst) <$> go
225 fmap (fromMaybe []) $
226 keyMay "dependencies" $
227 (++) <$> eachInObjectWithKey (parsePackageName . T.unpack)
229 <*> (concatMap snd <$> eachInObject asResolvedDependencies)
231-- | Extracts only the top level dependency names from the output of
232-- `bower list --json --offline`
233asToplevelDependencies :: Parse BowerError [PackageName]
234asToplevelDependencies =
237 eachInObjectWithKey (parsePackageName . T.unpack) (return ())
239asDependencyStatus :: Parse e DependencyStatus
240asDependencyStatus = do
241 isMissing <- keyOrDefault "missing" False asBool
247 keyOrDefault "_resolution" NoResolution $ do
248 type_ <- key "type" asString
250 "version" -> ResolvedVersion <$> key "tag" asString
251 other -> return (ResolvedOther other)
253warnUndeclared :: [PackageName] -> [PackageName] -> PrepareM ()
254warnUndeclared declared actual =
255 mapM_ (warn . UndeclaredDependency) (actual \\ declared)
258 [(PackageName, DependencyStatus)] -> PrepareM [(PackageName, Version)]
260 let (missing, noVersion, installed) = partitionDeps deps
263 userError (MissingDependencies (x :| xs))
265 mapM_ (warn . NoResolvedVersion) noVersion
266 withVersions <- catMaybes <$> mapM tryExtractVersion' installed
267 filterM (liftIO . isPureScript . bowerDir . fst) withVersions
270 partitionDeps = foldr go ([], [], [])
271 go (pkgName, d) (ms, os, is) =
273 Missing -> (pkgName : ms, os, is)
274 NoResolution -> (ms, pkgName : os, is)
275 ResolvedOther _ -> (ms, pkgName : os, is)
276 ResolvedVersion v -> (ms, os, (pkgName, v) : is)
278 bowerDir pkgName = "bower_components/" ++ runPackageName pkgName
280 -- Try to extract a version, and warn if unsuccessful.
281 tryExtractVersion' pair =
282 maybe (warn (UnacceptableVersion pair) >> return Nothing)
284 (tryExtractVersion pair)
286tryExtractVersion :: (PackageName, String) -> Maybe (PackageName, Version)
287tryExtractVersion (pkgName, tag) =
288 let tag' = fromMaybe tag (stripPrefix "v" tag)
289 in (pkgName,) <$> D.parseVersion' tag'
291-- | Returns whether it looks like there is a purescript package checked out
292-- in the given directory.
293isPureScript :: FilePath -> IO Bool
295 files <- Glob.globDir1 purescriptSourceFiles dir
296 return (not (null files))
298getInputAndDepsFiles :: IO ([FilePath], [(PackageName, FilePath)])
299getInputAndDepsFiles = do
300 inputFiles <- globRelative purescriptSourceFiles
301 depsFiles' <- globRelative purescriptDepsFiles
302 return (inputFiles, mapMaybe withPackageName depsFiles')
304withPackageName :: FilePath -> Maybe (PackageName, FilePath)
305withPackageName fp = (,fp) <$> getPackageName fp
307getPackageName :: FilePath -> Maybe PackageName
308getPackageName fp = do
309 let xs = splitOn "/" fp
310 ys <- stripPrefix ["bower_components"] xs
312 case Bower.mkPackageName y of
313 Right name -> Just name
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list