1{-# LANGUAGE OverloadedStrings #-}
2{-# LANGUAGE RecordWildCards #-}
3{-# LANGUAGE TupleSections #-}
4{-# LANGUAGE GeneralizedNewtypeDeriving #-}
6module Language.PureScript.Publish
12 , defaultPublishOptions
13 , getGitWorkingTreeStatus
14 , requireCleanWorkingTree
15 , getVersionFromGitTag
17 , getModulesAndBookmarks
18 , getResolvedDependencies
21import Prelude hiding (userError)
24import Data.Char (isSpace)
25import Data.String (fromString)
26import Data.List (stripPrefix, isSuffixOf, (\\), nubBy)
27import Data.List.Split (splitOn)
28import Data.List.NonEmpty (NonEmpty(..))
30import Data.Function (on)
32import Data.Aeson.BetterErrors
33import qualified Data.Text as T
35import Control.Applicative
36import Control.Category ((>>>))
37import Control.Arrow ((***))
38import Control.Exception (catch, try)
39import Control.Monad.Trans.Except
40import Control.Monad.Error.Class (MonadError(..))
41import Control.Monad.Writer
43import System.Directory (doesFileExist)
44import System.Process (readProcess)
45import System.Exit (exitFailure)
46import System.FilePath (pathSeparator)
47import qualified System.FilePath.Glob as Glob
49import Web.Bower.PackageMeta (PackageMeta(..), BowerError(..), PackageName,
50 runPackageName, parsePackageName, Repository(..))
51import qualified Web.Bower.PackageMeta as Bower
53import qualified Language.PureScript as P (version)
54import qualified Language.PureScript.Docs as D
55import Language.PureScript.Publish.Utils
56import Language.PureScript.Publish.ErrorsWarnings
58data PublishOptions = PublishOptions
59 { -- | How to obtain the version tag and version that the data being
60 -- generated will refer to.
61 publishGetVersion :: PrepareM (String, Version)
64defaultPublishOptions :: PublishOptions
65defaultPublishOptions = PublishOptions
66 { publishGetVersion = getVersionFromGitTag
69-- | Attempt to retrieve package metadata from the current directory.
70-- Calls exitFailure if no package metadata could be retrieved.
71preparePackage :: PublishOptions -> IO D.UploadedPackage
73 runPrepareM (preparePackage' opts)
74 >>= either (\e -> printError e >> exitFailure)
77 handleWarnings (result, warns) = do
82 PrepareM { unPrepareM :: WriterT [PackageWarning] (ExceptT PackageError IO) a }
83 deriving (Functor, Applicative, Monad,
84 MonadWriter [PackageWarning],
85 MonadError PackageError)
87-- This MonadIO instance ensures that IO errors don't crash the program.
88instance MonadIO PrepareM where
90 lift' (try act) >>= either (otherError . IOExceptionThrown) return
92 lift' :: IO a -> PrepareM a
93 lift' = PrepareM . lift . lift
95runPrepareM :: PrepareM a -> IO (Either PackageError (a, [PackageWarning]))
96runPrepareM = runExceptT . runWriterT . unPrepareM
98warn :: PackageWarning -> PrepareM ()
101userError :: UserError -> PrepareM a
102userError = throwError . UserError
104internalError :: InternalError -> PrepareM a
105internalError = throwError . InternalError
107otherError :: OtherError -> PrepareM a
108otherError = throwError . OtherError
110catchLeft :: Applicative f => Either a b -> (a -> f b) -> f b
111catchLeft a f = either f pure a
113preparePackage' :: PublishOptions -> PrepareM D.UploadedPackage
114preparePackage' opts = do
115 exists <- liftIO (doesFileExist "bower.json")
116 unless exists (userError BowerJSONNotFound)
118 requireCleanWorkingTree
120 pkgMeta <- liftIO (Bower.decodeFile "bower.json")
121 >>= flip catchLeft (userError . CouldntParseBowerJSON)
122 (pkgVersionTag, pkgVersion) <- publishGetVersion opts
123 pkgGithub <- getBowerInfo pkgMeta
124 (pkgBookmarks, pkgModules) <- getModulesAndBookmarks
126 let declaredDeps = map fst (bowerDependencies pkgMeta ++
127 bowerDevDependencies pkgMeta)
128 pkgResolvedDependencies <- getResolvedDependencies declaredDeps
130 let pkgUploader = D.NotYetKnown
131 let pkgCompilerVersion = P.version
135getModulesAndBookmarks :: PrepareM ([D.Bookmark], [D.Module])
136getModulesAndBookmarks = do
137 (inputFiles, depsFiles) <- liftIO getInputAndDepsFiles
138 liftIO (D.parseAndDesugar inputFiles depsFiles renderModules)
139 >>= either (userError . ParseAndDesugarError) return
141 renderModules bookmarks modules =
142 return (bookmarks, map D.convertModule modules)
144data TreeStatus = Clean | Dirty deriving (Show, Eq, Ord, Enum)
146getGitWorkingTreeStatus :: PrepareM TreeStatus
147getGitWorkingTreeStatus = do
148 out <- readProcess' "git" ["status", "--porcelain"] ""
150 if null . filter (not . null) . lines $ out
154requireCleanWorkingTree :: PrepareM ()
155requireCleanWorkingTree = do
156 status <- getGitWorkingTreeStatus
157 unless (status == Clean) $
158 userError DirtyWorkingTree
160getVersionFromGitTag :: PrepareM (String, Version)
161getVersionFromGitTag = do
162 out <- readProcess' "git" ["tag", "--list", "--points-at", "HEAD"] ""
163 let vs = map trimWhitespace (lines out)
164 case mapMaybe parseMay vs of
165 [] -> userError TagMustBeCheckedOut
167 xs -> userError (AmbiguousVersions (map snd xs))
170 dropWhile isSpace >>> reverse >>> dropWhile isSpace >>> reverse
172 (str,) <$> D.parseVersion' (dropPrefix "v" str)
173 dropPrefix prefix str =
174 fromMaybe str (stripPrefix prefix str)
176getBowerInfo :: PackageMeta -> PrepareM (D.GithubUser, D.GithubRepo)
177getBowerInfo = either (userError . BadRepositoryField) return . tryExtract
180 case bowerRepository pkgMeta of
181 Nothing -> Left RepositoryFieldMissing
182 Just Repository{..} -> do
183 unless (repositoryType == "git")
184 (Left (BadRepositoryType repositoryType))
185 maybe (Left NotOnGithub) Right (extractGithub repositoryUrl)
187extractGithub :: String -> Maybe (D.GithubUser, D.GithubRepo)
189 stripPrefix "git://github.com/"
190 >>> fmap (splitOn "/")
192 >>> fmap (D.GithubUser *** (D.GithubRepo . dropDotGit))
195 takeTwo :: [a] -> Maybe (a, a)
196 takeTwo [x, y] = Just (x, y)
199 dropDotGit :: String -> String
201 | ".git" `isSuffixOf` str = take (length str - 4) str
204readProcess' :: String -> [String] -> String -> PrepareM String
205readProcess' prog args stdin = do
206 out <- liftIO (catch (Right <$> readProcess prog args stdin)
208 either (otherError . ProcessFailed prog args) return out
212 -- ^ Listed in bower.json, but not installed.
214 -- ^ In the output of `bower list --json --offline`, there was no
215 -- _resolution key. This can be caused by adding the dependency using
216 -- `bower link`, or simply copying it into bower_components instead of
217 -- installing it normally.
218 | ResolvedOther String
219 -- ^ Resolved, but to something other than a version. The String argument
220 -- is the resolution type. The values it can take that I'm aware of are
221 -- "commit" and "branch".
222 | ResolvedVersion String
223 -- ^ Resolved to a version. The String argument is the resolution tag (eg,
227-- Go through all bower dependencies which contain purescript code, and
228-- extract their versions.
230-- In the case where a bower dependency is taken from a particular version,
231-- that's easy; take that version. In any other case (eg, a branch, or a commit
232-- sha) we print a warning that documentation links will not work, and avoid
233-- linking to documentation for any types from that package.
235-- The rationale for this is: people will prefer to use a released version
236-- where possible. If they are not using a released version, then this is
237-- probably for a reason. However, docs are only ever available for released
238-- versions. Therefore there will probably be no version of the docs which is
239-- appropriate to link to, and we should omit links.
240getResolvedDependencies :: [PackageName] -> PrepareM [(PackageName, Version)]
241getResolvedDependencies declaredDeps = do
242 depsBS <- fromString <$> readProcess' "bower" ["list", "--json", "--offline"] ""
244 -- Check for undeclared dependencies
245 toplevels <- catchJSON (parse asToplevelDependencies depsBS)
246 warnUndeclared declaredDeps toplevels
248 deps <- catchJSON (parse asResolvedDependencies depsBS)
252 catchJSON = flip catchLeft (internalError . JSONError FromBowerList)
254-- | Extracts all dependencies and their versions from
255-- `bower list --json --offline`
256asResolvedDependencies :: Parse BowerError [(PackageName, DependencyStatus)]
257asResolvedDependencies = nubBy ((==) `on` fst) <$> go
260 fmap (fromMaybe []) $
261 keyMay "dependencies" $
262 (++) <$> eachInObjectWithKey (parsePackageName . T.unpack)
264 <*> (concatMap snd <$> eachInObject asResolvedDependencies)
266-- | Extracts only the top level dependency names from the output of
267-- `bower list --json --offline`
268asToplevelDependencies :: Parse BowerError [PackageName]
269asToplevelDependencies =
272 eachInObjectWithKey (parsePackageName . T.unpack) (return ())
274asDependencyStatus :: Parse e DependencyStatus
275asDependencyStatus = do
276 isMissing <- keyOrDefault "missing" False asBool
282 keyOrDefault "_resolution" NoResolution $ do
283 type_ <- key "type" asString
285 "version" -> ResolvedVersion <$> key "tag" asString
286 other -> return (ResolvedOther other)
288warnUndeclared :: [PackageName] -> [PackageName] -> PrepareM ()
289warnUndeclared declared actual =
290 mapM_ (warn . UndeclaredDependency) (actual \\ declared)
293 [(PackageName, DependencyStatus)] -> PrepareM [(PackageName, Version)]
295 let (missing, noVersion, installed) = partitionDeps deps
298 userError (MissingDependencies (x :| xs))
300 mapM_ (warn . NoResolvedVersion) noVersion
301 withVersions <- catMaybes <$> mapM tryExtractVersion' installed
302 filterM (liftIO . isPureScript . bowerDir . fst) withVersions
305 partitionDeps = foldr go ([], [], [])
306 go (pkgName, d) (ms, os, is) =
308 Missing -> (pkgName : ms, os, is)
309 NoResolution -> (ms, pkgName : os, is)
310 ResolvedOther _ -> (ms, pkgName : os, is)
311 ResolvedVersion v -> (ms, os, (pkgName, v) : is)
313 bowerDir pkgName = "bower_components/" ++ runPackageName pkgName
315 -- Try to extract a version, and warn if unsuccessful.
316 tryExtractVersion' pair =
317 maybe (warn (UnacceptableVersion pair) >> return Nothing)
319 (tryExtractVersion pair)
321tryExtractVersion :: (PackageName, String) -> Maybe (PackageName, Version)
322tryExtractVersion (pkgName, tag) =
323 let tag' = fromMaybe tag (stripPrefix "v" tag)
324 in (pkgName,) <$> D.parseVersion' tag'
326-- | Returns whether it looks like there is a purescript package checked out
327-- in the given directory.
328isPureScript :: FilePath -> IO Bool
330 files <- Glob.globDir1 purescriptSourceFiles dir
331 return (not (null files))
333getInputAndDepsFiles :: IO ([FilePath], [(PackageName, FilePath)])
334getInputAndDepsFiles = do
335 inputFiles <- globRelative purescriptSourceFiles
336 depsFiles' <- globRelative purescriptDepsFiles
337 return (inputFiles, mapMaybe withPackageName depsFiles')
339withPackageName :: FilePath -> Maybe (PackageName, FilePath)
340withPackageName fp = (,fp) <$> getPackageName fp
342getPackageName :: FilePath -> Maybe PackageName
343getPackageName fp = do
344 let xs = splitOn [pathSeparator] fp
345 ys <- stripPrefix ["bower_components"] xs
347 case Bower.mkPackageName y of
348 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