diff --git a/LICENSE.markdown b/LICENSE.markdown
--- a/LICENSE.markdown
+++ b/LICENSE.markdown
@@ -1,6 +1,6 @@
 MIT License
 
-Copyright (c) 2021 Taylor Fausak
+Copyright (c) 2022 Taylor Fausak
 
 Permission is hereby granted, free of charge, to any person obtaining a copy
 of this software and associated documentation files (the "Software"), to deal
diff --git a/autoexporter.cabal b/autoexporter.cabal
--- a/autoexporter.cabal
+++ b/autoexporter.cabal
@@ -1,7 +1,7 @@
-cabal-version: >= 1.10
+cabal-version: 2.2
 
 name: autoexporter
-version: 1.1.20
+version: 2.0.0.1
 
 synopsis: Automatically re-export modules.
 description: Autoexporter automatically re-exports modules.
@@ -17,10 +17,15 @@
   location: https://github.com/tfausak/autoexporter
   type: git
 
-library
+flag pedantic
+  default: False
+  description: Enables @-Werror@, which turns warnings into errors.
+  manual: True
+
+common library
   build-depends:
-    base >= 4.13.0 && < 4.16
-    , Cabal >= 3.0.1 && < 3.5
+    , base >= 4.13.0 && < 4.17
+    , Cabal >= 3.0.1 && < 3.7
     , directory >= 1.3.6 && < 1.4
     , filepath >= 1.4.2 && < 1.5
   default-language: Haskell2010
@@ -28,18 +33,40 @@
     -Weverything
     -Wno-implicit-prelude
     -Wno-missing-deriving-strategies
+    -Wno-missing-exported-signatures
     -Wno-unsafe
-  exposed-modules: Autoexporter
-  hs-source-dirs: src/lib
+    -Wno-all-missed-specialisations
+    -Wno-missed-specialisations
 
+  if flag(pedantic)
+    ghc-options: -Werror
+
   if impl(ghc >= 8.10)
     ghc-options:
       -Wno-missing-safe-haskell-mode
       -Wno-prepositive-qualified-module
 
+  if impl(ghc >= 9.2)
+    ghc-options:
+      -Wno-missing-kind-signatures
+
+common executable
+  import: library
+
+  build-depends: autoexporter
+  ghc-options:
+    -rtsopts
+    -threaded
+    -Wno-unused-packages
+
+library
+  import: library
+
+  exposed-modules: Autoexporter
+  hs-source-dirs: source/library
+
 executable autoexporter
-  build-depends: base, autoexporter
-  default-language: Haskell2010
-  ghc-options: -rtsopts -threaded
-  hs-source-dirs: src/exe
+  import: executable
+
+  hs-source-dirs: source/executable
   main-is: Main.hs
diff --git a/source/executable/Main.hs b/source/executable/Main.hs
new file mode 100644
--- /dev/null
+++ b/source/executable/Main.hs
@@ -0,0 +1,8 @@
+module Main
+  ( main
+  ) where
+
+import qualified Autoexporter
+
+main :: IO ()
+main = Autoexporter.autoexporter
diff --git a/source/library/Autoexporter.hs b/source/library/Autoexporter.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Autoexporter.hs
@@ -0,0 +1,160 @@
+-- | This package isn't really meant to be used as a library. It's typically
+-- used as a GHC preprocessor, like so:
+--
+-- > {-# OPTIONS_GHC -F -pgmF autoexporter #-}
+--
+-- For more information, please see the README on GitHub:
+-- <https://github.com/tfausak/autoexporter#readme>.
+module Autoexporter
+  ( autoexporter
+  ) where
+
+import qualified Control.Exception as Exception
+import qualified Data.List as List
+import qualified Data.Maybe as Maybe
+import qualified Distribution.ModuleName as Cabal
+import qualified Distribution.Text as Cabal
+import qualified System.Directory as Directory
+import qualified System.Environment as Environment
+import qualified System.FilePath as FilePath
+
+
+autoexporter :: IO ()
+autoexporter = do
+  -- Start by getting the command line arguments. We expect three positional
+  -- arguments from GHC: the path to the original source file, the path to the
+  -- actual input file, and the path to the output file. The source and input
+  -- files could be different if another preprocessor is involved. Since we
+  -- don't consider the file's contents, we can ignore the input file.
+  --
+  -- After GHC's arguments, we have to check for anything passed in by the user
+  -- with @-optF@.
+  arguments <- Environment.getArgs
+  (input, output, depth) <- case arguments of
+    [input, _, output] -> pure (input, output, DepthShallow)
+    [input, _, output, "--deep"] -> pure (input, output, DepthDeep)
+    _ -> Exception.throwIO (InvalidArguments arguments)
+
+  -- Next we convert the original source file path into a module name. If we
+  -- aren't able to do this then something weird is going on and we should
+  -- crash.
+  moduleName <- case toModuleName input of
+    Just moduleName -> pure moduleName
+    Nothing -> Exception.throwIO (InvalidModuleName input)
+
+  -- Then we want to find all of the relevant modules to re-export. Note that
+  -- we simply ignore non-Haskell files and files that don't form valid module
+  -- names. Also we sort the module names so that the output is deterministic.
+  entries <- listDirectory depth (FilePath.dropExtension input)
+  let moduleNames = getModuleNames entries
+
+  -- Finally we render the module and write it to the output file.
+  let content = renderModule moduleName moduleNames
+  writeFile output content
+
+
+-- | This type describes how to search for modules to export. A shallow search
+-- only considers files in one directory. A deep search considers all files in
+-- the directory tree.
+data Depth
+  = DepthShallow
+  | DepthDeep
+  deriving (Eq, Show)
+
+
+-- | This exception type is thrown when we don't know how to interpret the
+-- arguments passed to the program.
+newtype InvalidArguments
+  = InvalidArguments [String]
+  deriving (Eq, Show)
+
+instance Exception.Exception InvalidArguments
+
+
+-- | This function attempts to convert an arbitrary file path into a valid
+-- Haskell module name. Any extensions are ignored.
+--
+-- >>> toModuleName "invalid/module.name"
+-- Nothing
+-- >>> toModuleName "valid/Module.name"
+-- Just (ModuleName ["Module"])
+-- >>> toModuleName "Qualified/Module.name"
+-- Just (ModuleName ["Qualified","Module"])
+toModuleName :: FilePath -> Maybe Cabal.ModuleName
+toModuleName =
+  Maybe.listToMaybe
+    . Maybe.mapMaybe (Cabal.simpleParse . List.intercalate ".")
+    . List.tails
+    . FilePath.splitDirectories
+    . FilePath.dropExtensions
+
+
+-- | This exception type is thrown when we can't create a valid module name
+-- from the source file path.
+newtype InvalidModuleName
+  = InvalidModuleName FilePath
+  deriving (Eq, Show)
+
+instance Exception.Exception InvalidModuleName
+
+
+-- | Lists all of the entries in the given directory. Note that unlike
+-- 'Directory.listDirectory' the results of calling this function will include
+-- the original directory name.
+listDirectory :: Depth -> FilePath -> IO [FilePath]
+listDirectory depth = case depth of
+  DepthShallow -> listDirectoryShallow
+  DepthDeep -> listDirectoryDeep
+
+
+listDirectoryShallow :: FilePath -> IO [FilePath]
+listDirectoryShallow directory = do
+  entries <- Directory.listDirectory directory
+  pure (fmap (FilePath.combine directory) entries)
+
+
+listDirectoryDeep :: FilePath -> IO [FilePath]
+listDirectoryDeep directory = do
+  entries <- listDirectoryShallow directory
+  let
+    listEntry entry = do
+      isDirectory <- Directory.doesDirectoryExist entry
+      if isDirectory then listDirectoryDeep entry else pure [entry]
+  fmap concat (mapM listEntry entries)
+
+
+-- | Given a list of file paths, returns a sorted list of module names from the
+-- entries that were Haskell files.
+getModuleNames :: [FilePath] -> [Cabal.ModuleName]
+getModuleNames =
+  List.sort . Maybe.mapMaybe toModuleName . filter isHaskellFile
+
+
+-- | This predicate tells you if the given file path is a Haskell source file.
+isHaskellFile :: FilePath -> Bool
+isHaskellFile = flip elem haskellExtensions . FilePath.takeExtensions
+
+
+-- | These are the extensions that we consider to be Haskell source files.
+haskellExtensions :: [String]
+haskellExtensions = [".hs", ".lhs"]
+
+
+-- | Given a module name and a list of module names to re-export, renders a
+-- module with all the appropriate imports and exports.
+renderModule :: Cabal.ModuleName -> [Cabal.ModuleName] -> String
+renderModule moduleName moduleNames = unlines
+  [ "{-# OPTIONS_GHC -fno-warn-dodgy-exports -fno-warn-unused-imports #-}"
+  , "module " <> Cabal.display moduleName <> " ("
+  , List.intercalate "\n" (fmap renderExport moduleNames)
+  , ") where"
+  , List.intercalate "\n" (fmap renderImport moduleNames)
+  ]
+
+
+renderExport :: Cabal.ModuleName -> String
+renderExport moduleName = "module " <> Cabal.display moduleName <> ","
+
+
+renderImport :: Cabal.ModuleName -> String
+renderImport moduleName = "import " <> Cabal.display moduleName
diff --git a/src/exe/Main.hs b/src/exe/Main.hs
deleted file mode 100644
--- a/src/exe/Main.hs
+++ /dev/null
@@ -1,6 +0,0 @@
-module Main ( main ) where
-
-import qualified Autoexporter
-
-main :: IO ()
-main = Autoexporter.autoexporter
diff --git a/src/lib/Autoexporter.hs b/src/lib/Autoexporter.hs
deleted file mode 100644
--- a/src/lib/Autoexporter.hs
+++ /dev/null
@@ -1,160 +0,0 @@
--- | This package isn't really meant to be used as a library. It's typically
--- used as a GHC preprocessor, like so:
---
--- > {-# OPTIONS_GHC -F -pgmF autoexporter #-}
---
--- For more information, please see the README on GitHub:
--- <https://github.com/tfausak/autoexporter#readme>.
-module Autoexporter ( autoexporter ) where
-
-import qualified Control.Exception as Exception
-import qualified Data.List as List
-import qualified Data.Maybe as Maybe
-import qualified Distribution.ModuleName as Cabal
-import qualified Distribution.Text as Cabal
-import qualified System.Directory as Directory
-import qualified System.Environment as Environment
-import qualified System.FilePath as FilePath
-
-
-autoexporter :: IO ()
-autoexporter = do
-  -- Start by getting the command line arguments. We expect three positional
-  -- arguments from GHC: the path to the original source file, the path to the
-  -- actual input file, and the path to the output file. The source and input
-  -- files could be different if another preprocessor is involved. Since we
-  -- don't consider the file's contents, we can ignore the input file.
-  --
-  -- After GHC's arguments, we have to check for anything passed in by the user
-  -- with @-optF@.
-  arguments <- Environment.getArgs
-  (input, output, depth) <- case arguments of
-    [input, _, output] -> pure (input, output, DepthShallow)
-    [input, _, output, "--deep"] -> pure (input, output, DepthDeep)
-    _ -> Exception.throwIO (InvalidArguments arguments)
-
-  -- Next we convert the original source file path into a module name. If we
-  -- aren't able to do this then something weird is going on and we should
-  -- crash.
-  moduleName <- case toModuleName input of
-    Just moduleName -> pure moduleName
-    Nothing -> Exception.throwIO (InvalidModuleName input)
-
-  -- Then we want to find all of the relevant modules to re-export. Note that
-  -- we simply ignore non-Haskell files and files that don't form valid module
-  -- names. Also we sort the module names so that the output is deterministic.
-  entries <- listDirectory depth (FilePath.dropExtension input)
-  let moduleNames = getModuleNames entries
-
-  -- Finally we render the module and write it to the output file.
-  let content = renderModule moduleName moduleNames
-  writeFile output content
-
-
--- | This type describes how to search for modules to export. A shallow search
--- only considers files in one directory. A deep search considers all files in
--- the directory tree.
-data Depth
-  = DepthShallow
-  | DepthDeep
-  deriving (Eq, Show)
-
-
--- | This exception type is thrown when we don't know how to interpret the
--- arguments passed to the program.
-newtype InvalidArguments
-  = InvalidArguments [String]
-  deriving (Eq, Show)
-
-instance Exception.Exception InvalidArguments
-
-
--- | This function attempts to convert an arbitrary file path into a valid
--- Haskell module name. Any extensions are ignored.
---
--- >>> toModuleName "invalid/module.name"
--- Nothing
--- >>> toModuleName "valid/Module.name"
--- Just (ModuleName ["Module"])
--- >>> toModuleName "Qualified/Module.name"
--- Just (ModuleName ["Qualified","Module"])
-toModuleName :: FilePath -> Maybe Cabal.ModuleName
-toModuleName
-  = Maybe.listToMaybe
-  . Maybe.mapMaybe Cabal.simpleParse
-  . fmap (List.intercalate ".")
-  . List.tails
-  . FilePath.splitDirectories
-  . FilePath.dropExtensions
-
-
--- | This exception type is thrown when we can't create a valid module name
--- from the source file path.
-newtype InvalidModuleName
-  = InvalidModuleName FilePath
-  deriving (Eq, Show)
-
-instance Exception.Exception InvalidModuleName
-
-
--- | Lists all of the entries in the given directory. Note that unlike
--- 'Directory.listDirectory' the results of calling this function will include
--- the original directory name.
-listDirectory :: Depth -> FilePath -> IO [FilePath]
-listDirectory depth = case depth of
-  DepthShallow -> listDirectoryShallow
-  DepthDeep -> listDirectoryDeep
-
-
-listDirectoryShallow :: FilePath -> IO [FilePath]
-listDirectoryShallow directory = do
-  entries <- Directory.listDirectory directory
-  pure (fmap (FilePath.combine directory) entries)
-
-
-listDirectoryDeep :: FilePath -> IO [FilePath]
-listDirectoryDeep directory = do
-  entries <- listDirectoryShallow directory
-  let
-    listEntry entry = do
-      isDirectory <- Directory.doesDirectoryExist entry
-      if isDirectory
-        then listDirectoryDeep entry
-        else pure [entry]
-  fmap concat (mapM listEntry entries)
-
-
--- | Given a list of file paths, returns a sorted list of module names from the
--- entries that were Haskell files.
-getModuleNames :: [FilePath] -> [Cabal.ModuleName]
-getModuleNames = List.sort . Maybe.mapMaybe toModuleName . filter isHaskellFile
-
-
--- | This predicate tells you if the given file path is a Haskell source file.
-isHaskellFile :: FilePath -> Bool
-isHaskellFile = flip elem haskellExtensions . FilePath.takeExtensions
-
-
--- | These are the extensions that we consider to be Haskell source files.
-haskellExtensions :: [String]
-haskellExtensions = [".hs", ".lhs"]
-
-
--- | Given a module name and a list of module names to re-export, renders a
--- module with all the appropriate imports and exports.
-renderModule :: Cabal.ModuleName -> [Cabal.ModuleName] -> String
-renderModule moduleName moduleNames = unlines
-  [ "{-# OPTIONS_GHC -fno-warn-dodgy-exports -fno-warn-unused-imports #-}"
-  , "module " <> Cabal.display moduleName <> " ("
-  , List.intercalate "\n" (fmap renderExport moduleNames)
-  , ") where"
-  , List.intercalate "\n" (fmap renderImport moduleNames)
-  ]
-
-
-renderExport :: Cabal.ModuleName -> String
-renderExport moduleName = "module " <> Cabal.display moduleName <> ","
-
-
-renderImport :: Cabal.ModuleName -> String
-renderImport moduleName = "import " <> Cabal.display moduleName
