diff --git a/examples/realworld/ext/.waspignore b/examples/realworld/ext/.waspignore new file mode 100644 index 000000000..1c432f30d --- /dev/null +++ b/examples/realworld/ext/.waspignore @@ -0,0 +1,3 @@ +# Ignore editor tmp files +**/*~ +**/#*# diff --git a/examples/tutorials/TodoApp/ext/.waspignore b/examples/tutorials/TodoApp/ext/.waspignore new file mode 100644 index 000000000..1c432f30d --- /dev/null +++ b/examples/tutorials/TodoApp/ext/.waspignore @@ -0,0 +1,3 @@ +# Ignore editor tmp files +**/*~ +**/#*# diff --git a/waspc/cli/Command/CreateNewProject.hs b/waspc/cli/Command/CreateNewProject.hs index b9abe5d09..f5f605122 100644 --- a/waspc/cli/Command/CreateNewProject.hs +++ b/waspc/cli/Command/CreateNewProject.hs @@ -41,6 +41,8 @@ createNewProject projectName = do let copyTemplateFile' = copyTemplateFile dataDir extCodeDir + writeFileSP (extCodeDir waspignoreFileInExtCodeDir) waspignoreFileContent + copyTemplateFile' (SP.fromPathRelFile [P.relfile|new/ext/MainPage.js|]) mainPageJsFileInExtCodeDir @@ -95,6 +97,15 @@ createNewProject projectName = do gitignoreFileContent = unlines [ "/.wasp/" ] + + waspignoreFileInExtCodeDir :: Path (Rel SourceExternalCodeDir) File + waspignoreFileInExtCodeDir = SP.fromPathRelFile [P.relfile|.waspignore|] + + waspignoreFileContent = unlines + [ "# Ignore editor tmp files" + , "**/*~" + , "**/#*#" + ] mainPageJsFileInExtCodeDir :: Path (Rel SourceExternalCodeDir) File mainPageJsFileInExtCodeDir = SP.fromPathRelFile [P.relfile|MainPage.js|] diff --git a/waspc/examples/todoApp/ext/.waspignore b/waspc/examples/todoApp/ext/.waspignore new file mode 100644 index 000000000..1c432f30d --- /dev/null +++ b/waspc/examples/todoApp/ext/.waspignore @@ -0,0 +1,3 @@ +# Ignore editor tmp files +**/*~ +**/#*# diff --git a/waspc/package.yaml b/waspc/package.yaml index 5ba95f3b0..86cb376ec 100644 --- a/waspc/package.yaml +++ b/waspc/package.yaml @@ -61,6 +61,7 @@ library: - bytestring - regex-tdfa - utf8-string + - Glob executables: wasp: diff --git a/waspc/src/ExternalCode.hs b/waspc/src/ExternalCode.hs index 2f05e37c0..369d4dc0f 100644 --- a/waspc/src/ExternalCode.hs +++ b/waspc/src/ExternalCode.hs @@ -13,10 +13,12 @@ import qualified Data.Text.Lazy as TextL import qualified Data.Text.Lazy.IO as TextL.IO import Data.Maybe (catMaybes) import Data.Text (Text) +import qualified Path as P import qualified Util.IO import StrongPath (Path, Abs, Rel, Dir, ()) import qualified StrongPath as SP +import WaspignoreFile (readWaspignoreFile, ignores) -- | External code directory in Wasp source, from which external code files are read. data SourceExternalCodeDir @@ -45,10 +47,18 @@ fileText = TextL.toStrict . _text fileAbsPath :: ExternalCode.File -> Path Abs SP.File fileAbsPath file = _extCodeDirPath file _pathInExtCodeDir file --- | Returns all files contained in the specified external code dir, recursively. +waspignorePathInExtCodeDir :: Path (Rel SourceExternalCodeDir) SP.File +waspignorePathInExtCodeDir = SP.fromPathRelFile [P.relfile|.waspignore|] + +-- | Returns all files contained in the specified external code dir, recursively, +-- except files ignores by the specified waspignore file. readFiles :: Path Abs (Dir SourceExternalCodeDir) -> IO [File] readFiles extCodeDirPath = do - relFilePaths <- Util.IO.listDirectoryDeep (SP.toPathAbsDir extCodeDirPath) >>= return . (map SP.fromPathRelFile) + let waspignoreFilePath = extCodeDirPath waspignorePathInExtCodeDir + waspignoreFile <- readWaspignoreFile waspignoreFilePath + relFilePaths <- filter (not . ignores waspignoreFile . SP.toFilePath) . + map SP.fromPathRelFile <$> + Util.IO.listDirectoryDeep (SP.toPathAbsDir extCodeDirPath) let absFilePaths = map (extCodeDirPath ) relFilePaths -- NOTE: We read text from all the files, regardless if they are text files or not, because -- we don't know if they are a text file or not. diff --git a/waspc/src/WaspignoreFile.hs b/waspc/src/WaspignoreFile.hs new file mode 100644 index 000000000..ae94a09f5 --- /dev/null +++ b/waspc/src/WaspignoreFile.hs @@ -0,0 +1,70 @@ +module WaspignoreFile + ( WaspignoreFile + , parseWaspignoreFile + , readWaspignoreFile + , ignores + ) where + +import Control.Exception (catch) +import System.IO.Error (isDoesNotExistError) +import StrongPath (Path, Abs, File, toFilePath) +import System.FilePath.Glob (Pattern, compile, match) + +newtype WaspignoreFile = WaspignoreFile [Pattern] + +-- | These patterns are ignored by every 'WaspignoreFile' +defaultIgnorePatterns :: [Pattern] +defaultIgnorePatterns = map compile [".waspignore"] + +-- | Parses a string to a 'WaspignoreFile'. +-- +-- An ignore file contains lines that are one of: +-- * blank +-- * comments (starting with '#') +-- * a pattern +-- +-- An ignore file always ignores `.waspignore`. +-- +-- Patterns are glob 'Pattern's, for full details "System.FilePath.Glob". A +-- brief description is: +-- +-- [@?@] Matches any single character except slashes. +-- [@*@] Matches a string of at least 1 character, excluding slashes. +-- [@[xyz\]@] Matches a single character in the set `xyz`. +-- [@[^xyz\]@] Matches a single character not in the set `xyz`. +-- [@**/@] Matches a string of at least 1 character, including slashes. +parseWaspignoreFile :: String -> WaspignoreFile +parseWaspignoreFile = WaspignoreFile . + (defaultIgnorePatterns++) . + map compile . + filter isPatternLine . + lines + where + isPatternLine :: String -> Bool + isPatternLine [] = False + isPatternLine ('#':_) = False + isPatternLine _ = True + +-- | Reads and parses the wasp ignore file. See 'parseWaspignoreFile' for details of +-- the file format, but it is very similar to `.gitignore`'s format. +-- +-- If the ignore file does not exist, it is interpreted as a blank file. +readWaspignoreFile :: Path Abs File -> IO WaspignoreFile +readWaspignoreFile fp = do + text <- readFile (toFilePath fp) + `catch` (\e -> if isDoesNotExistError e then return "" + else ioError e) + return $ parseWaspignoreFile text + +-- | Tests whether a file should be ignored according to a 'WaspignoreFile'. +-- +-- Example: +-- +-- @ +-- let ignoreFile = parseWaspignoreFile "**/*.tmp" +-- ignoreFile `ignores` "out.tmp" -- True +-- ignoreFile `ignores` "src/a.tmp" -- True +-- ignoreFile `ignores` "src/a.js" -- False +-- @ +ignores :: WaspignoreFile -> FilePath -> Bool +ignores (WaspignoreFile pats) fp = any (`match` fp) pats diff --git a/waspc/test/WaspignoreFileTest.hs b/waspc/test/WaspignoreFileTest.hs new file mode 100644 index 000000000..915911a5f --- /dev/null +++ b/waspc/test/WaspignoreFileTest.hs @@ -0,0 +1,38 @@ +module WaspignoreFileTest where + +import Test.Tasty.Hspec +import Test.Tasty.QuickCheck (property) + +import WaspignoreFile (parseWaspignoreFile, ignores) + +spec_IgnoreFile :: Spec +spec_IgnoreFile = do + describe "IgnoreFile" $ do + it "When given a single pattern, should match it and '.waspignore'" $ do + let ignoreFile = parseWaspignoreFile "*.tmp" + (ignoreFile `ignores` "a.tmp") `shouldBe` True + (ignoreFile `ignores` "a.src") `shouldBe` False + (ignoreFile `ignores` ".waspignore") `shouldBe` True + + it "When given a blank input, should match only '.waspignore'" $ do + let ignoreFile = parseWaspignoreFile "" + property $ \fp -> if fp == ".waspignore" + then ignoreFile `ignores` fp + else not $ ignoreFile `ignores` fp + + it "When given a comment as the only line, should match only '.waspignore'" $ do + let ignoreFile = parseWaspignoreFile "# test comment" + property $ \fp -> if fp == ".waspignore" + then ignoreFile `ignores` fp + else not $ ignoreFile `ignores` fp + + it "When the only difference between two files is a comment, the files should match the same strings" $ do + let comment = "\n# test comment" + property $ \pat fp -> (parseWaspignoreFile pat `ignores` fp) == + (parseWaspignoreFile (pat ++ comment) `ignores` fp) + + it "When given 2 patterns, should match the path if either of the patterns match" $ do + let pat1 = parseWaspignoreFile "a" + let pat2 = parseWaspignoreFile "b" + let patBoth = parseWaspignoreFile "a\nb" + property $ \fp -> patBoth `ignores` fp == (pat1 `ignores` fp || pat2 `ignores` fp)