diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2021 Dan Fithian
+
+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.
diff --git a/app/main.hs b/app/main.hs
new file mode 100644
--- /dev/null
+++ b/app/main.hs
@@ -0,0 +1,61 @@
+import Prelude
+
+import Control.Applicative (many)
+import Control.Monad (unless)
+import Control.Monad.IO.Class (liftIO)
+import Control.Monad.State (execStateT, put)
+import Data.Foldable (for_, traverse_)
+import Data.Text (Text, pack, unpack)
+import Data.Traversable (for)
+import System.Exit (ExitCode(ExitFailure, ExitSuccess), exitWith)
+import qualified Data.Set as Set
+import qualified Options.Applicative as Opt
+
+import Data.Prune.Dependency (getDependencyByModule)
+import Data.Prune.ImportParser (getCompilableUsedDependencies)
+import Data.Prune.Package (parseStackYaml)
+import qualified Data.Prune.Types as T
+
+data Opts = Opts
+  { optsStackYamlFile :: FilePath
+  , optsPackages :: [Text]
+  }
+
+parseArgs :: IO Opts
+parseArgs = Opt.execParser (Opt.info (Opt.helper <*> parser) $ Opt.progDesc "Prune a Stack project's dependencies")
+  where
+    parser = Opts
+      <$> Opt.strOption (
+        Opt.long "stack-yaml-file"
+          <> Opt.metavar "STACK_YAML_FILE"
+          <> Opt.help "Location of stack.yaml"
+          <> Opt.value "stack.yaml"
+          <> Opt.showDefault )
+      <*> many ( pack <$> Opt.strOption (
+        Opt.long "package"
+          <> Opt.metavar "PACKAGE"
+          <> Opt.help "Package name(s)" ) )
+
+
+main :: IO ()
+main = do
+  Opts {..} <- parseArgs
+  packages <- parseStackYaml optsStackYamlFile optsPackages
+
+  dependencyByModule <- liftIO $ getDependencyByModule optsStackYamlFile packages
+  code <- flip execStateT ExitSuccess $ for_ packages $ \T.Package {..} -> do
+    baseUsedDependencies <- fmap mconcat . for packageCompilables $ \compilable@T.Compilable {..} -> do
+      usedDependencies <- liftIO $ getCompilableUsedDependencies dependencyByModule compilable
+      let (baseUsedDependencies, otherUsedDependencies) = Set.partition (flip Set.member packageBaseDependencies) usedDependencies
+          otherUnusedDependencies = Set.difference compilableDependencies otherUsedDependencies
+      unless (Set.null otherUnusedDependencies) $ do
+        liftIO . putStrLn . unpack $ "Some unused dependencies for " <> pack (show compilableType) <> " " <> T.unCompilableName compilableName <> " in package " <> packageName
+        traverse_ (liftIO . putStrLn . unpack . ("  " <>) . T.unDependencyName) $ Set.toList otherUnusedDependencies
+        put $ ExitFailure 1
+      pure baseUsedDependencies
+    let baseUnusedDependencies = Set.difference packageBaseDependencies baseUsedDependencies
+    unless (Set.null baseUnusedDependencies) $ do
+      liftIO . putStrLn . unpack $ "Some unused base dependencies for package " <> packageName
+      liftIO . traverse_ (putStrLn . unpack . ("  " <>) . T.unDependencyName) $ Set.toList baseUnusedDependencies
+      put $ ExitFailure 1
+  exitWith code
diff --git a/prune-juice.cabal b/prune-juice.cabal
new file mode 100644
--- /dev/null
+++ b/prune-juice.cabal
@@ -0,0 +1,99 @@
+cabal-version: 1.12
+
+-- This file has been generated from package.yaml by hpack version 0.33.0.
+--
+-- see: https://github.com/sol/hpack
+--
+-- hash: c82d607d1298918a330352ff35cee89ea7edd8f5b4321866f33d55b7558eb1c7
+
+name:           prune-juice
+version:        0.1
+synopsis:       Prune unused Haskell dependencies
+description:    Prune unused Haskell dependencies from a Stack/Hpack project
+category:       Development
+maintainer:     Dan Fithian
+copyright:      2020 Dan Fithian
+license:        MIT
+license-file:   LICENSE
+build-type:     Simple
+
+library
+  exposed-modules:
+      Data.Prune.Dependency
+      Data.Prune.ImportParser
+      Data.Prune.Package
+      Data.Prune.Types
+  other-modules:
+      Paths_prune_juice
+  hs-source-dirs:
+      src
+  default-extensions: ConstraintKinds DataKinds DefaultSignatures DeriveDataTypeable DeriveGeneric DerivingStrategies DerivingVia EmptyDataDecls FlexibleContexts FlexibleInstances GADTs GeneralizedNewtypeDeriving LambdaCase MultiParamTypeClasses MultiWayIf NamedFieldPuns NoImplicitPrelude NoMonomorphismRestriction OverloadedStrings QuasiQuotes RankNTypes RecordWildCards ScopedTypeVariables StandaloneDeriving TemplateHaskell TupleSections TypeApplications TypeFamilies TypeOperators ViewPatterns
+  ghc-options: -Wall -fwarn-tabs -fwarn-redundant-constraints -Wincomplete-uni-patterns -eventlog
+  build-depends:
+      aeson
+    , base <5.0
+    , bytestring
+    , containers
+    , directory
+    , filepath
+    , hpack
+    , megaparsec
+    , mtl
+    , text
+    , turtle
+    , yaml
+  default-language: Haskell2010
+
+executable prune-juice
+  main-is: main.hs
+  other-modules:
+      Paths_prune_juice
+  hs-source-dirs:
+      app
+  default-extensions: ConstraintKinds DataKinds DefaultSignatures DeriveDataTypeable DeriveGeneric DerivingStrategies DerivingVia EmptyDataDecls FlexibleContexts FlexibleInstances GADTs GeneralizedNewtypeDeriving LambdaCase MultiParamTypeClasses MultiWayIf NamedFieldPuns NoImplicitPrelude NoMonomorphismRestriction OverloadedStrings QuasiQuotes RankNTypes RecordWildCards ScopedTypeVariables StandaloneDeriving TemplateHaskell TupleSections TypeApplications TypeFamilies TypeOperators ViewPatterns
+  ghc-options: -Wall -fwarn-tabs -fwarn-redundant-constraints -Wincomplete-uni-patterns -eventlog
+  build-depends:
+      aeson
+    , base <5.0
+    , bytestring
+    , containers
+    , directory
+    , filepath
+    , hpack
+    , megaparsec
+    , mtl
+    , optparse-applicative
+    , prune-juice
+    , text
+    , turtle
+    , yaml
+  default-language: Haskell2010
+
+test-suite test
+  type: exitcode-stdio-1.0
+  main-is: main.hs
+  other-modules:
+      Data.Prune.ImportParserSpec
+      Data.Prune.PackageSpec
+      Paths_prune_juice
+  hs-source-dirs:
+      test
+  default-extensions: ConstraintKinds DataKinds DefaultSignatures DeriveDataTypeable DeriveGeneric DerivingStrategies DerivingVia EmptyDataDecls FlexibleContexts FlexibleInstances GADTs GeneralizedNewtypeDeriving LambdaCase MultiParamTypeClasses MultiWayIf NamedFieldPuns NoImplicitPrelude NoMonomorphismRestriction OverloadedStrings QuasiQuotes RankNTypes RecordWildCards ScopedTypeVariables StandaloneDeriving TemplateHaskell TupleSections TypeApplications TypeFamilies TypeOperators ViewPatterns
+  ghc-options: -Wall -fwarn-tabs -fwarn-redundant-constraints -Wincomplete-uni-patterns -eventlog
+  build-depends:
+      aeson
+    , base <5.0
+    , bytestring
+    , containers
+    , directory
+    , file-path-th
+    , filepath
+    , hpack
+    , hspec
+    , megaparsec
+    , mtl
+    , prune-juice
+    , text
+    , turtle
+    , yaml
+  default-language: Haskell2010
diff --git a/src/Data/Prune/Dependency.hs b/src/Data/Prune/Dependency.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/Prune/Dependency.hs
@@ -0,0 +1,49 @@
+module Data.Prune.Dependency where
+
+import Prelude hiding (words)
+
+import Data.Foldable (find)
+import Data.Map (Map)
+import Data.Maybe (mapMaybe)
+import Data.Text (Text, isPrefixOf, pack, strip, unpack, words)
+import Data.Traversable (for)
+import System.Exit (ExitCode(ExitFailure, ExitSuccess))
+import Turtle (shellStrict)
+import qualified Data.Map as Map
+import qualified Data.Set as Set
+
+import Data.Prune.ImportParser (parseExposedModules)
+import qualified Data.Prune.Types as T
+
+runOrFail :: Text -> IO Text
+runOrFail cmd = shellStrict cmd mempty >>= \case
+  (ExitSuccess, out) -> pure out
+  (ExitFailure _, out) -> fail . unpack $ "Failed to \"" <> cmd <> "\" due to " <> out
+
+getDependencyByModule :: FilePath -> [T.Package] -> IO (Map T.ModuleName T.DependencyName)
+getDependencyByModule stackYamlFile packages = do
+  let allDependencies = foldMap T.packageBaseDependencies packages <> foldMap T.compilableDependencies (foldMap T.packageCompilables packages)
+      tupleDependency x = (, x) <$> find (\d -> isPrefixOf (T.unDependencyName d) x) allDependencies
+  compilerBin <- strip <$> runOrFail ("stack --stack-yaml " <> pack stackYamlFile <> " path --compiler-bin")
+  snapshotPkgDb <- strip <$> runOrFail ("stack --stack-yaml " <> pack stackYamlFile <> " path --snapshot-pkg-db")
+  globalPkgDb <- strip <$> runOrFail ("stack --stack-yaml " <> pack stackYamlFile <> " path --global-pkg-db")
+  localPkgDb <- strip <$> runOrFail ("stack --stack-yaml " <> pack stackYamlFile <> " path --local-pkg-db")
+  let snapshotGhcPkg = compilerBin <> "/ghc-pkg --package-db " <> snapshotPkgDb
+      globalGhcPkg = compilerBin <> "/ghc-pkg --package-db " <> globalPkgDb
+      localGhcPkg = compilerBin <> "/ghc-pkg --package-db " <> localPkgDb
+  snapshotPkgs <- mapMaybe tupleDependency . words . strip
+    <$> runOrFail (snapshotGhcPkg <> " list --simple-output")
+  globalPkgs <- mapMaybe tupleDependency . words . strip
+    <$> runOrFail (globalGhcPkg <> " list --simple-output")
+  localPkgs <- mapMaybe tupleDependency . words . strip
+    <$> runOrFail (localGhcPkg <> " list --simple-output")
+  snapshotDependencyByModule <- fmap mconcat . for snapshotPkgs $ \(dependencyName, pkg) -> do
+    moduleNames <- parseExposedModules . unpack . strip =<< runOrFail (snapshotGhcPkg <> " field " <> pkg <> " exposed-modules")
+    pure . Map.fromList . map (, dependencyName) . Set.toList $ moduleNames
+  globalDependencyByModule <- fmap mconcat . for globalPkgs $ \(dependencyName, pkg) -> do
+    moduleNames <- parseExposedModules . unpack . strip =<< runOrFail (globalGhcPkg <> " field " <> pkg <> " exposed-modules")
+    pure . Map.fromList . map (, dependencyName) . Set.toList $ moduleNames
+  localDependencyByModule <- fmap mconcat . for localPkgs $ \(dependencyName, pkg) -> do
+    moduleNames <- parseExposedModules . unpack . strip =<< runOrFail (localGhcPkg <> " field " <> pkg <> " exposed-modules")
+    pure . Map.fromList . map (, dependencyName) . Set.toList $ moduleNames
+  pure $ snapshotDependencyByModule <> globalDependencyByModule <> localDependencyByModule
diff --git a/src/Data/Prune/ImportParser.hs b/src/Data/Prune/ImportParser.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/Prune/ImportParser.hs
@@ -0,0 +1,72 @@
+module Data.Prune.ImportParser where
+
+import Prelude
+
+import Control.Applicative ((<|>), optional, some)
+import Control.Monad (void)
+import Data.List (isPrefixOf)
+import Data.Map (Map)
+import Data.Set (Set)
+import Data.Text (pack)
+import Data.Traversable (for)
+import Data.Void (Void)
+import Text.Megaparsec (Parsec, between, oneOf, parse)
+import Text.Megaparsec.Char (alphaNumChar, char, space, string, symbolChar)
+import qualified Data.Map as Map
+import qualified Data.Set as Set
+
+import qualified Data.Prune.Types as T
+
+type Parser = Parsec Void String
+
+padded :: Parser a -> Parser a
+padded = between space space
+
+quoted :: Parser a -> Parser a
+quoted = between (ptoken "\"") (ptoken "\"")
+
+ptoken :: String -> Parser String
+ptoken = padded . string
+
+operator :: Parser String
+operator = concat <$> sequence [ptoken "(", symbolChars, ptoken ")"]
+
+symbolChars :: Parser String
+symbolChars = some (oneOf ("!#$%&*+./<=>?@^|-~:\\" :: String)) <|> some symbolChar
+
+symbol :: Parser String
+symbol = padded $ operator <|> some (alphaNumChar <|> oneOf ("._'" :: String))
+
+pkgName :: Parser String
+pkgName = some (alphaNumChar <|> char '-')
+
+oneImport :: Parser T.ModuleName
+oneImport = void (string "import") *> space
+  *> optional (void (string "qualified") *> space)
+  *> optional (void (padded (quoted pkgName)) *> space)
+  *> (T.ModuleName . pack <$> (symbol <* space))
+
+exposedModules :: Parser (Set T.ModuleName)
+exposedModules = void (string "exposed-modules:") *> space
+  *> (Set.fromList <$> some (T.ModuleName . pack <$> symbol))
+
+parseFileImports :: FilePath -> IO (Set T.ModuleName)
+parseFileImports fp = do
+  either (fail . ("Failed to parse imports due to " <>) . show) (pure . Set.fromList) . traverse (parse oneImport fp) . filter (isPrefixOf "import ") . lines
+    =<< readFile fp
+
+parseExposedModules :: String -> IO (Set T.ModuleName)
+parseExposedModules input =
+  if null input
+    then pure mempty
+    else either (\e -> fail $ "Failed to parse exposed modules due to " <> show e <> " original input " <> input) pure $ parse exposedModules "" input
+
+getUsedDependencies :: Map T.ModuleName T.DependencyName -> Set T.ModuleName -> Set T.DependencyName
+getUsedDependencies dependencyByModule = foldr go mempty . Set.toList
+  where
+    go next acc = acc <> maybe mempty Set.singleton (Map.lookup next dependencyByModule)
+
+getCompilableUsedDependencies :: Map T.ModuleName T.DependencyName -> T.Compilable -> IO (Set T.DependencyName)
+getCompilableUsedDependencies dependencyByModule T.Compilable {..} = fmap mconcat . for (Set.toList compilableFiles) $ \fp -> do
+  moduleNames <- parseFileImports fp
+  pure $ getUsedDependencies dependencyByModule moduleNames
diff --git a/src/Data/Prune/Package.hs b/src/Data/Prune/Package.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/Prune/Package.hs
@@ -0,0 +1,73 @@
+module Data.Prune.Package where
+
+import Prelude
+
+import Data.Map (Map)
+import Data.Set (Set)
+import Data.Text (Text, pack)
+import Data.Traversable (for)
+import Hpack.Config
+  ( Section, decodeOptionsTarget, decodeResultPackage, defaultDecodeOptions, packageBenchmarks
+  , packageConfig, packageExecutables, packageInternalLibraries, packageLibrary, packageName
+  , packageTests, readPackageConfig, sectionDependencies, sectionSourceDirs, unDependencies
+  )
+import System.Directory (doesDirectoryExist, listDirectory, pathIsSymbolicLink)
+import System.FilePath.Posix ((</>), isExtensionOf)
+import qualified Data.ByteString as BS
+import qualified Data.Map as Map
+import qualified Data.Set as Set
+import qualified Data.Yaml as Yaml
+
+import qualified Data.Prune.Types as T
+
+listFilesRecursive :: FilePath -> IO (Set FilePath)
+listFilesRecursive dir = do
+  dirs <- listDirectory dir
+  fmap mconcat . for dirs $ \case
+    -- don't include "hidden" directories, i.e. those that start with a '.'
+    '.' : _ -> pure mempty
+    fn -> do
+      let
+        path = if dir == "." then fn else dir </> fn
+      isDir <- doesDirectoryExist path
+      isSymlink <- pathIsSymbolicLink path
+      case (isSymlink, isDir) of
+        (True, _) -> pure mempty
+        (_, True) -> listFilesRecursive path
+        _ -> pure $ Set.singleton path
+
+getSectionDependencyNames :: Section a -> Set T.DependencyName
+getSectionDependencyNames = Set.fromList . map (T.DependencyName . pack) . Map.keys . unDependencies . sectionDependencies
+
+getSectionFiles :: FilePath -> Section a -> IO (Set FilePath)
+getSectionFiles fp section = fmap mconcat . for (sectionSourceDirs section) $ \dir -> do
+  allFiles <- listFilesRecursive $ fp </> dir
+  pure $ Set.filter (isExtensionOf "hs") allFiles
+
+getSectionCompilables :: FilePath -> T.CompilableType -> Set T.DependencyName -> Map String (Section a) -> IO [T.Compilable]
+getSectionCompilables fp typ baseDependencies sections = for (Map.toList sections) $ \(name, section) -> do
+  sourceFiles <- getSectionFiles fp section
+  pure $ T.Compilable (T.CompilableName (pack name)) typ (Set.difference (getSectionDependencyNames section) baseDependencies) sourceFiles
+
+parsePackageYaml :: FilePath -> IO T.Package
+parsePackageYaml fp = do
+  package <- either fail (pure . decodeResultPackage) =<< readPackageConfig (defaultDecodeOptions { decodeOptionsTarget = fp </> packageConfig })
+  let baseDependencies = maybe mempty getSectionDependencyNames $ packageLibrary package
+  libraries         <- getSectionCompilables fp T.CompilableTypeLibrary    baseDependencies $ maybe mempty (Map.singleton (packageName package)) $ packageLibrary package
+  internalLibraries <- getSectionCompilables fp T.CompilableTypeLibrary    baseDependencies $ packageInternalLibraries package
+  executables       <- getSectionCompilables fp T.CompilableTypeExecutable baseDependencies $ packageExecutables package
+  tests             <- getSectionCompilables fp T.CompilableTypeTest       baseDependencies $ packageTests package
+  benchmarks        <- getSectionCompilables fp T.CompilableTypeBenchmark  baseDependencies $ packageBenchmarks package
+  pure T.Package
+    { packageName = pack $ packageName package
+    , packageBaseDependencies = baseDependencies
+    , packageCompilables = libraries <> internalLibraries <> executables <> tests <> benchmarks
+    }
+
+parseStackYaml :: FilePath -> [Text] -> IO [T.Package]
+parseStackYaml stackYamlFile packages = do
+  T.StackYaml {..} <- either (fail . ("Couldn't parse stack.yaml due to " <>) . show) pure . Yaml.decodeEither' =<< BS.readFile stackYamlFile
+  rawPackages <- traverse parsePackageYaml stackYamlPackages
+  if null packages
+    then pure rawPackages
+    else pure $ filter (flip elem packages . T.packageName) rawPackages
diff --git a/src/Data/Prune/Types.hs b/src/Data/Prune/Types.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/Prune/Types.hs
@@ -0,0 +1,56 @@
+module Data.Prune.Types where
+
+import Prelude
+
+import Data.Aeson ((.:), FromJSON, parseJSON, withObject)
+import Data.Set (Set)
+import Data.Text (Text)
+
+data CompilableType
+  = CompilableTypeLibrary
+  | CompilableTypeExecutable
+  | CompilableTypeTest
+  | CompilableTypeBenchmark
+  deriving (Eq, Ord)
+
+instance Show CompilableType where
+  show = \case
+    CompilableTypeLibrary -> "library"
+    CompilableTypeExecutable -> "executable"
+    CompilableTypeTest -> "test"
+    CompilableTypeBenchmark -> "benchmark"
+
+newtype CompilableName = CompilableName { unCompilableName :: Text }
+  deriving (Eq, Ord, Show)
+
+data DependencyName = DependencyName { unDependencyName :: Text }
+  deriving (Eq, Ord, Show)
+
+data ModuleName = ModuleName { unModuleName :: Text }
+  deriving (Eq, Ord, Show)
+
+data Compilable = Compilable
+  { compilableName :: CompilableName
+  , compilableType :: CompilableType
+  , compilableDependencies :: Set DependencyName
+  , compilableFiles :: Set FilePath
+  }
+  deriving (Eq, Ord, Show)
+
+data Package = Package
+  { packageName :: Text
+  , packageBaseDependencies :: Set DependencyName
+  , packageCompilables :: [Compilable]
+  }
+  deriving (Eq, Ord, Show)
+
+data StackYaml = StackYaml
+  { stackYamlPackages :: [FilePath]
+  -- ^ FIXME not every package is this way.
+  }
+  deriving (Eq, Ord, Show)
+
+instance FromJSON StackYaml where
+  parseJSON = withObject "StackYaml" $ \obj ->
+    StackYaml
+      <$> obj .: "packages"
diff --git a/test/Data/Prune/ImportParserSpec.hs b/test/Data/Prune/ImportParserSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/Data/Prune/ImportParserSpec.hs
@@ -0,0 +1,46 @@
+module Data.Prune.ImportParserSpec where
+
+import Prelude
+
+import Test.Hspec (Spec, describe, it, shouldBe)
+import Text.Megaparsec (parse)
+import qualified Data.Set as Set
+
+import qualified Data.Prune.Types as T
+
+-- the module being tested
+import Data.Prune.ImportParser
+
+spec :: Spec
+spec = describe "Data.Prune.ImportParser" $ do
+  it "should parse wildcards" $ do
+    parse oneImport "" "import Foo.Bar" `shouldBe` Right (T.ModuleName "Foo.Bar")
+
+  it "should parse imports" $ do
+    parse oneImport "" "import Foo.Bar (foo, bar)" `shouldBe` Right (T.ModuleName "Foo.Bar")
+
+  it "should parse qualified" $ do
+    parse oneImport "" "import qualified Foo.Bar as Baz" `shouldBe` Right (T.ModuleName "Foo.Bar")
+
+  it "should parse package imports" $ do
+    parse oneImport "" "import \"foobar\" Foo.Bar ()" `shouldBe` Right (T.ModuleName "Foo.Bar")
+
+  it "should parse something complicated" $ do
+    parse oneImport "" "import   qualified   \"foobar\"   Foo.Bar   as   Baz (foo, bar) " `shouldBe` Right (T.ModuleName "Foo.Bar")
+
+  it "should parse exposed modules - single line" $ do
+    parse exposedModules "" "exposed-modules: Hpack Hpack.Config Hpack.Render Hpack.Yaml"
+      `shouldBe` Right (Set.fromList [T.ModuleName "Hpack", T.ModuleName "Hpack.Config", T.ModuleName "Hpack.Render", T.ModuleName "Hpack.Yaml"])
+
+  it "should parse exposed modules - multiline" $ do
+    parse exposedModules "" "exposed-modules: Text.Megaparsec Text.Megaparsec.Byte\n\
+                                              \Text.Megaparsec.Byte.Lexer Text.Megaparsec.Char\n\
+                                              \Text.Megaparsec.Char.Lexer Text.Megaparsec.Debug\n\
+                                              \Text.Megaparsec.Error Text.Megaparsec.Error.Builder\n\
+                                              \Text.Megaparsec.Internal Text.Megaparsec.Pos Text.Megaparsec.Stream"
+      `shouldBe` Right ( Set.fromList
+        [ T.ModuleName "Text.Megaparsec", T.ModuleName "Text.Megaparsec.Byte"
+        , T.ModuleName "Text.Megaparsec.Byte.Lexer", T.ModuleName "Text.Megaparsec.Char"
+        , T.ModuleName "Text.Megaparsec.Char.Lexer", T.ModuleName "Text.Megaparsec.Debug"
+        , T.ModuleName "Text.Megaparsec.Error", T.ModuleName "Text.Megaparsec.Error.Builder"
+        , T.ModuleName "Text.Megaparsec.Internal", T.ModuleName "Text.Megaparsec.Pos", T.ModuleName "Text.Megaparsec.Stream" ] )
diff --git a/test/Data/Prune/PackageSpec.hs b/test/Data/Prune/PackageSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/Data/Prune/PackageSpec.hs
@@ -0,0 +1,26 @@
+module Data.Prune.PackageSpec where
+
+import Prelude
+
+import Data.Foldable (find)
+import Data.List (isSuffixOf)
+import System.FilePath.TH (fileRelativeToAbsolute)
+import Test.Hspec (Spec, describe, it, shouldBe, shouldSatisfy)
+import qualified Data.Set as Set
+
+import qualified Data.Prune.Types as T
+
+-- the module being tested
+import Data.Prune.Package
+
+spec :: Spec
+spec = describe "Data.Prune.Package" $ do
+  let stackYamlFile = $(fileRelativeToAbsolute "../../../stack.yaml")
+  it "parses stack.yaml" $ do
+    [T.Package {..}] <- parseStackYaml stackYamlFile []
+    packageName `shouldBe` "prune-juice"
+    packageBaseDependencies `shouldSatisfy` Set.member (T.DependencyName "base")
+    T.Compilable {..} <- maybe (fail "No compilable with the name \"test\"") pure $ find ((==) (T.CompilableName "test") . T.compilableName) packageCompilables
+    compilableType `shouldBe` T.CompilableTypeTest
+    compilableDependencies `shouldSatisfy` not . Set.null
+    compilableFiles `shouldSatisfy` not . Set.null . Set.filter (isSuffixOf "PackageSpec.hs")
diff --git a/test/main.hs b/test/main.hs
new file mode 100644
--- /dev/null
+++ b/test/main.hs
@@ -0,0 +1,11 @@
+import Prelude
+
+import Test.Hspec (hspec)
+
+import qualified Data.Prune.ImportParserSpec
+import qualified Data.Prune.PackageSpec
+
+main :: IO ()
+main = hspec $ do
+  Data.Prune.ImportParserSpec.spec
+  Data.Prune.PackageSpec.spec
