@@ -6,6 +6,7 @@ module Spago.Command.Init
66 , InitOptions
77 , defaultConfig
88 , defaultConfig'
9+ , folderToPackageName
910 , pursReplFile
1011 , run
1112 , srcMainTemplate
@@ -14,14 +15,15 @@ module Spago.Command.Init
1415
1516import Spago.Prelude
1617
18+ import Data.Array (mapMaybe )
1719import Data.Map as Map
1820import Data.String as String
21+ import Data.String.Utils as StringUtils
1922import Registry.PackageName as PackageName
2023import Registry.Version as Version
2124import Spago.Config (Dependencies (..), SetAddress (..), Config )
2225import Spago.Config as Config
2326import Spago.FS as FS
24- import Spago.Log as Log
2527import Spago.Path as Path
2628import Spago.Registry (RegistryEnv )
2729import Spago.Registry as Registry
@@ -111,14 +113,13 @@ run opts = do
111113 InitWorkspace { packageName: Nothing } -> String .take 150 $ Path .basename rootPath
112114 InitWorkspace { packageName: Just n } -> n
113115 InitSubpackage { packageName: n } -> n
114- logDebug [ Path .quote rootPath, " \" " <> candidateName <> " \" " ]
115- pname <- case PackageName .parse (PackageName .stripPureScriptPrefix candidateName) of
116- Left err -> die
117- [ toDoc " Could not figure out a name for the new package. Error:"
118- , Log .break
119- , Log .indent2 $ toDoc err
116+ pname <- case folderToPackageName candidateName of
117+ Nothing -> die
118+ [ " Could not derive a valid package name from directory " <> Path .quote rootPath <> " ."
119+ , " Please use --name to specify a package name."
120120 ]
121- Right p -> pure p
121+ Just p -> pure p
122+ logDebug [ Path .quote rootPath, " \" " <> candidateName <> " \" -> \" " <> PackageName .print pname <> " \" " ]
122123 logDebug [ " Got packageName and setVersion:" , PackageName .print pname, unsafeStringify opts.setVersion ]
123124 pure pname
124125
@@ -299,3 +300,59 @@ foundExistingDirectory dir = "Found existing directory " <> Path.quote dir <> ",
299300
300301foundExistingFile :: LocalPath -> String
301302foundExistingFile file = " Found existing file " <> Path .quote file <> " , not overwriting it"
303+
304+ -- SANITIZATION -----------------------------------------------------------------
305+
306+ -- | Convert a folder name to a valid package name.
307+ -- | We try to convert as much Unicode as possible to ASCII (through NFD normalisation),
308+ -- | and otherwise strip out and/or replace non-alpanumeric chars with dashes.
309+ -- | After all this work that is still not enough to guarantee a successful PackageName
310+ -- | parse, so this is still a Maybe.
311+ folderToPackageName :: String -> Maybe PackageName
312+ folderToPackageName input =
313+ input
314+ # String .toLower
315+ -- NFD normalization decomposes accented chars (é → e + combining accent)
316+ -- so the base ASCII letter is preserved when we filter non-ASCII later
317+ # StringUtils .normalize' StringUtils.NFD
318+ # String .toCodePointArray
319+ # mapMaybe sanitizeCodePoint
320+ # String .fromCodePointArray
321+ # collapseConsecutiveDashes
322+ # stripLeadingTrailingDashes
323+ # PackageName .stripPureScriptPrefix
324+ # PackageName .parse
325+ # hush
326+ where
327+ dash = String .codePointFromChar ' -'
328+
329+ -- Transform each codepoint:
330+ -- - ASCII lowercase (a-z) and digits (0-9): keep as-is
331+ -- - Apostrophes and quotes: remove (shouldn't create word boundaries)
332+ -- - Other ASCII: convert to dash (word boundaries)
333+ -- - Non-ASCII (combining marks from NFD, etc.): remove
334+ sanitizeCodePoint cp
335+ | isAsciiLower cp || isAsciiDigit cp = Just cp
336+ | isRemovable cp = Nothing
337+ | isAscii cp = Just dash
338+ | otherwise = Nothing
339+
340+ isAsciiLower cp = cp >= String .codePointFromChar ' a' && cp <= String .codePointFromChar ' z'
341+ isAsciiDigit cp = cp >= String .codePointFromChar ' 0' && cp <= String .codePointFromChar ' 9'
342+ isAscii cp = cp <= String .codePointFromChar ' \x7F '
343+ -- ASCII apostrophe and quote shouldn't create word boundaries (Tim's → tims, not tim-s)
344+ isRemovable cp = cp == String .codePointFromChar ' \' ' || cp == String .codePointFromChar ' "'
345+
346+ -- Collapse consecutive dashes into one
347+ collapseConsecutiveDashes str =
348+ case String .indexOf (String.Pattern " --" ) str of
349+ Nothing -> str
350+ Just _ -> collapseConsecutiveDashes $ String .replaceAll (String.Pattern " --" ) (String.Replacement " -" ) str
351+
352+ -- Remove all leading and trailing dashes
353+ stripLeadingTrailingDashes str =
354+ case String .stripPrefix (String.Pattern " -" ) str of
355+ Just stripped -> stripLeadingTrailingDashes stripped
356+ Nothing -> case String .stripSuffix (String.Pattern " -" ) str of
357+ Just stripped -> stripLeadingTrailingDashes stripped
358+ Nothing -> str
0 commit comments