diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8b822b5c69..acbb606c1e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,7 +29,7 @@ jobs: - uses: "actions/setup-node@v1" with: - node-version: "10" + node-version: "12" - id: "haskell" uses: "haskell/actions/setup@v1" diff --git a/app/Command/REPL.hs b/app/Command/REPL.hs index 01429b093e..27be4bc9e9 100644 --- a/app/Command/REPL.hs +++ b/app/Command/REPL.hs @@ -44,8 +44,7 @@ import System.IO.UTF8 (readUTF8File) import System.Exit import System.Directory (doesFileExist, getCurrentDirectory) import System.FilePath (()) -import System.FilePath.Glob (glob) -import System.Process (readProcessWithExitCode) +import qualified System.FilePath.Glob as Glob import qualified Data.ByteString.Lazy.UTF8 as U -- | Command line options @@ -108,7 +107,7 @@ pasteMode = -- | Make a JavaScript bundle for the browser. bundle :: IO (Either Bundle.ErrorMessage String) bundle = runExceptT $ do - inputFiles <- liftIO (glob (".psci_modules" "node_modules" "*" "*.js")) + inputFiles <- liftIO $ concat <$> Glob.globDir [Glob.compile "*/*.js", Glob.compile "*/foreign.cjs"] modulesDir input <- for inputFiles $ \filename -> do js <- liftIO (readUTF8File filename) mid <- Bundle.guessModuleIdentifier filename @@ -280,13 +279,12 @@ nodeBackend nodePath nodeArgs = Backend setup eval reload shutdown eval :: () -> String -> IO () eval _ _ = do - writeFile indexFile "require('$PSCI')['$main']();" - process <- maybe findNodeProcess (pure . pure) nodePath - result <- traverse (\node -> readProcessWithExitCode node (nodeArgs ++ [indexFile]) "") process + writeFile indexFile "import('./$PSCI/index.js').then(({ $main }) => $main());" + result <- readNodeProcessWithExitCode nodePath (nodeArgs ++ [indexFile]) "" case result of - Just (ExitSuccess, out, _) -> putStrLn out - Just (ExitFailure _, _, err) -> putStrLn err - Nothing -> putStrLn "Could not find node.js. Do you have node.js installed and available in your PATH?" + Right (ExitSuccess, out, _) -> putStrLn out + Right (ExitFailure _, _, err) -> putStrLn err + Left err -> putStrLn err reload :: () -> IO () reload _ = return () @@ -303,7 +301,7 @@ command = loop <$> options where loop :: PSCiOptions -> IO () loop PSCiOptions{..} = do - inputFiles <- concat <$> traverse glob psciInputGlob + inputFiles <- concat <$> traverse Glob.glob psciInputGlob e <- runExceptT $ do modules <- ExceptT (loadAllModules inputFiles) when (null modules) . liftIO $ do diff --git a/app/static/index.js b/app/static/index.js index 1d0714fd71..f496540c4c 100644 --- a/app/static/index.js +++ b/app/static/index.js @@ -16,13 +16,24 @@ var evaluate = function evaluate(js) { // which will be returned to PSCi. buffer.push(s); }; - // Replace any require(...) statements with lookups on the PSCI object. + // Replace any require and import statements with lookups on the PSCI object + // and export statements with assignments to module.exports. var replaced = js.replace(/require\("[^"]*"\)/g, function(s) { return "PSCI['" + s.split('/')[1] + "']"; + }).replace(/import \* as ([^\s]+) from "([^"]*)"/g, function (_, as, from) { + return "var " + as + " = PSCI['" + from.split('/')[1] + "']"; + }).replace(/export \{([^}]+)\} from "\.\/foreign\.js";?/g, function (_, exports) { + return exports.replace(/^\s*([^,\s]+),?\s*$/gm, function (_, exported) { + return "module.exports." + exported + " = $foreign." + exported + ";"; + }); + }).replace(/export \{([^}]+)\};?/g, function (_, exports) { + return exports.replace(/^\s*([^,\s]+)(?: as ([^\s]+))?,?\s*$/gm, function (_, exported, as) { + return "module.exports." + (as || exported) + " = " + exported + ";"; + }); }); // Wrap the module and evaluate it. var wrapped = - [ 'var module = {};' + [ 'var module = { exports: {} };' , '(function(module) {' , replaced , '})(module);' diff --git a/src/Language/PureScript/Bundle.hs b/src/Language/PureScript/Bundle.hs index 734c61da8c..951f06859a 100644 --- a/src/Language/PureScript/Bundle.hs +++ b/src/Language/PureScript/Bundle.hs @@ -13,7 +13,10 @@ module Language.PureScript.Bundle , ModuleType(..) , ErrorMessage(..) , printErrorMessage + , ForeignModuleExports(..) , getExportedIdentifiers + , ForeignModuleImports(..) + , getImportedModules , Module ) where @@ -31,16 +34,19 @@ import Data.Foldable (fold) import Data.Generics (GenericM, everything, everythingWithContext, everywhere, gmapMo, mkMp, mkQ, mkT) import Data.Graph import Data.List (stripPrefix, (\\)) -import Data.Maybe (catMaybes, fromMaybe, mapMaybe) +import Data.Maybe (catMaybes, fromMaybe, mapMaybe, maybeToList) import Data.Version (showVersion) import qualified Data.Aeson as A import qualified Data.Map as M import qualified Data.Set as S -import qualified Data.Text.Lazy as T +import qualified Data.Text as T +import qualified Data.Text.Lazy as LT import Language.JavaScript.Parser import Language.JavaScript.Parser.AST import Language.JavaScript.Process.Minify +import Language.PureScript.Names (ModuleName(..)) +import Language.PureScript.CodeGen.JS.Common (moduleNameToJs) import qualified Paths_purescript as Paths @@ -54,6 +60,7 @@ data ErrorMessage = UnsupportedModulePath String | InvalidTopLevel | UnableToParseModule String + | UnsupportedImport | UnsupportedExport | ErrorInModule ModuleIdentifier ErrorMessage | MissingEntryPoint String @@ -89,6 +96,7 @@ guessModuleIdentifier filename = ModuleIdentifier (takeFileName (takeDirectory f where guessModuleType "index.js" = pure Regular guessModuleType "foreign.js" = pure Foreign + guessModuleType "foreign.cjs" = pure Foreign guessModuleType name = throwError $ UnsupportedModulePath name data Visibility @@ -111,7 +119,7 @@ data ExportType -- | There are four types of module element we are interested in: -- --- 1) Require statements +-- 1) Import declarations and require statements -- 2) Member declarations -- 3) Export lists -- 4) Everything else @@ -119,22 +127,22 @@ data ExportType -- Each is labelled with the original AST node which generated it, so that we can dump it back -- into the output during codegen. data ModuleElement - = Require JSStatement String (Either String ModuleIdentifier) + = Import JSModuleItem String (Either String ModuleIdentifier) | Member JSStatement Visibility String JSExpression [Key] | ExportsList [(ExportType, String, JSExpression, [Key])] | Other JSStatement - | Skip JSStatement + | Skip JSModuleItem deriving (Show) instance A.ToJSON ModuleElement where toJSON = \case - (Require _ name (Right target)) -> - A.object [ "type" .= A.String "Require" + (Import _ name (Right target)) -> + A.object [ "type" .= A.String "Import" , "name" .= name , "target" .= target ] - (Require _ name (Left targetPath)) -> - A.object [ "type" .= A.String "Require" + (Import _ name (Left targetPath)) -> + A.object [ "type" .= A.String "Import" , "name" .= name , "targetPath" .= targetPath ] @@ -150,11 +158,11 @@ instance A.ToJSON ModuleElement where ] (Other stmt) -> A.object [ "type" .= A.String "Other" - , "js" .= getFragment stmt + , "js" .= getFragment (JSAstStatement stmt JSNoAnnot) ] - (Skip stmt) -> + (Skip item) -> A.object [ "type" .= A.String "Skip" - , "js" .= getFragment stmt + , "js" .= getFragment (JSAstModule [item] JSNoAnnot) ] where @@ -177,9 +185,9 @@ instance A.ToJSON ModuleElement where , "dependsOn" .= map keyToJSON dependsOn ] - getFragment = ellipsize . renderToText . minifyJS . flip JSAstStatement JSNoAnnot + getFragment = ellipsize . renderToText . minifyJS where - ellipsize text = if T.compareLength text 20 == GT then T.take 19 text `T.snoc` ellipsis else text + ellipsize text = if LT.compareLength text 20 == GT then LT.take 19 text `LT.snoc` ellipsis else text ellipsis = '\x2026' -- | A module is just a list of elements of the types listed above. @@ -195,10 +203,11 @@ instance A.ToJSON Module where -- | Prepare an error message for consumption by humans. printErrorMessage :: ErrorMessage -> [String] printErrorMessage (UnsupportedModulePath s) = - [ "A CommonJS module has an unsupported name (" ++ show s ++ ")." + [ "An ES or CommonJS module has an unsupported name (" ++ show s ++ ")." , "The following file names are supported:" , " 1) index.js (PureScript native modules)" - , " 2) foreign.js (PureScript foreign modules)" + , " 2) foreign.js (PureScript ES foreign modules)" + , " 3) foreign.cjs (PureScript CommonJS foreign modules)" ] printErrorMessage InvalidTopLevel = [ "Expected a list of source elements at the top level." ] @@ -206,10 +215,24 @@ printErrorMessage (UnableToParseModule err) = [ "The module could not be parsed:" , err ] +printErrorMessage UnsupportedImport = + [ "An import was unsupported." + , "Modules can be imported with ES namespace imports declarations:" + , " import * as module from \"Module.Name\"" + , "Alternatively, they can be also be imported with the CommonJS require function:" + , " var module = require(\"Module.Name\")" + ] printErrorMessage UnsupportedExport = - [ "An export was unsupported. Exports can be defined in one of two ways: " - , " 1) exports.name = ..." - , " 2) exports = { ... }" + [ "An export was unsupported." + , "Declarations can be exported as ES named exports:" + , " export var decl" + , "Existing identifiers can be exported as well:" + , " export { name }" + , "They can also be renamed on export:" + , " export { name as alias }" + , "Alternatively, CommonJS exports can be defined in one of two ways:" + , " 1) exports.name = value" + , " 2) exports = { name: value }" ] printErrorMessage (ErrorInModule mid e) = ("Error in module " ++ displayIdentifier mid ++ ":") @@ -219,13 +242,13 @@ printErrorMessage (ErrorInModule mid e) = displayIdentifier (ModuleIdentifier name ty) = name ++ " (" ++ showModuleType ty ++ ")" printErrorMessage (MissingEntryPoint mName) = - [ "Couldn't find a CommonJS module for the specified entry point: " ++ mName + [ "Could not find an ES module or CommonJS module for the specified entry point: " ++ mName ] printErrorMessage (MissingMainModule mName) = - [ "Couldn't find a CommonJS module for the specified main module: " ++ mName + [ "Could not find an ES module or CommonJS module for the specified main module: " ++ mName ] --- | Calculate the ModuleIdentifier which a require(...) statement imports. +-- | Calculate the ModuleIdentifier imported by an import declaration or a require(...) statement. checkImportPath :: String -> ModuleIdentifier -> S.Set String -> Either String ModuleIdentifier checkImportPath "./foreign.js" m _ = Right (ModuleIdentifier (moduleName m) Foreign) @@ -247,10 +270,14 @@ stripSuffix suffix xs = -- -- 1) module.name or member["name"] -- --- where module was imported using +-- where module was imported using require -- -- var module = require("Module.Name"); -- +-- or an import declaration +-- +-- import * as module from "Module.Name"; +-- -- 2) name -- -- where name is the name of a member defined in the current module. @@ -262,7 +289,7 @@ withDeps (Module modulePath fn es) = Module modulePath fn (map expandDeps es) imports = mapMaybe toImport es where toImport :: ModuleElement -> Maybe (String, ModuleIdentifier) - toImport (Require _ nm (Right mid)) = Just (nm, mid) + toImport (Import _ nm (Right mid)) = Just (nm, mid) toImport _ = Nothing -- | Collects all member names in scope, so that we can identify dependencies of the second type. @@ -369,52 +396,159 @@ trailingCommaList :: JSCommaTrailingList a -> [a] trailingCommaList (JSCTLComma l _) = commaList l trailingCommaList (JSCTLNone l) = commaList l +identName :: JSIdent -> Maybe String +identName (JSIdentName _ ident) = Just ident +identName _ = Nothing + +exportStatementIdentifiers :: JSStatement -> [String] +exportStatementIdentifiers (JSVariable _ jsExpressions _) = + varNames jsExpressions +exportStatementIdentifiers (JSConstant _ jsExpressions _) = + varNames jsExpressions +exportStatementIdentifiers (JSLet _ jsExpressions _) = + varNames jsExpressions +exportStatementIdentifiers (JSClass _ jsIdent _ _ _ _ _) = + maybeToList . identName $ jsIdent +exportStatementIdentifiers (JSFunction _ jsIdent _ _ _ _ _) = + maybeToList . identName $ jsIdent +exportStatementIdentifiers (JSGenerator _ _ jsIdent _ _ _ _ _) = + maybeToList . identName $ jsIdent +exportStatementIdentifiers _ = [] + +varNames :: JSCommaList JSExpression -> [String] +varNames = mapMaybe varName . commaList + where + varName (JSVarInitExpression (JSIdentifier _ ident) _) = Just ident + varName _ = Nothing + +sp :: JSAnnot +sp = JSAnnot tokenPosnEmpty [ WhiteSpace tokenPosnEmpty " " ] + +stringLiteral :: String -> JSExpression +stringLiteral s = JSStringLiteral JSNoAnnot $ "\"" ++ s ++ "\"" + -- | Attempt to create a Module from a JavaScript AST. -- -- Each type of module element is matched using pattern guards, and everything else is bundled into the -- Other constructor. toModule :: forall m. (MonadError ErrorMessage m) => S.Set String -> ModuleIdentifier -> Maybe FilePath -> JSAST -> m Module toModule mids mid filename top - | JSAstProgram smts _ <- top = Module mid filename <$> traverse toModuleElement smts + | JSAstModule jsModuleItems _ <- top + , JSModuleImportDeclaration _ jsImportDeclaration : _ <- jsModuleItems + , JSImportDeclaration JSImportClauseDefault{} jsFromClause _ <- jsImportDeclaration + , JSFromClause _ _ importPath <- jsFromClause + , "./foreign.cjs" <- strValue importPath + = pure $ Module mid filename [] + | JSAstModule jsModuleItems _ <- top = Module mid filename . mconcat <$> traverse toModuleElements jsModuleItems | otherwise = err InvalidTopLevel where err :: ErrorMessage -> m a err = throwError . ErrorInModule mid - toModuleElement :: JSStatement -> m ModuleElement - toModuleElement stmt - | Just (importName, importPath) <- matchRequire mids mid stmt - = pure (Require stmt importName importPath) - toModuleElement stmt - | Just (visibility, name, decl) <- matchMember stmt - = pure (Member stmt visibility name decl []) - toModuleElement stmt - | Just props <- matchExportsAssignment stmt - = ExportsList <$> traverse toExport (trailingCommaList props) + toModuleElements :: JSModuleItem -> m [ModuleElement] + toModuleElements item@(JSModuleImportDeclaration _ jsImportDeclaration) + | JSImportDeclaration jsImportClause jsFromClause _ <- jsImportDeclaration + , JSImportClauseNameSpace jsImportNameSpace <- jsImportClause + , JSImportNameSpace _ _ jsIdent <- jsImportNameSpace + , JSFromClause _ _ importPath <- jsFromClause + , importPath' <- checkImportPath (strValue importPath) mid mids + = maybe (err UnsupportedImport) pure (identName jsIdent) >>= \name -> + pure [Import item name importPath'] + toModuleElements (JSModuleImportDeclaration _ _) + = err UnsupportedImport + + toModuleElements (JSModuleExportDeclaration _ jsExportDeclaration) + | JSExportFrom jsExportClause jsFromClause _ <- jsExportDeclaration + , JSFromClause _ _ from <- jsFromClause + , JSExportClause _ jsExportSpecifiers _ <- jsExportClause + = pure . ExportsList <$> exportSpecifiersList (Just (strValue from)) jsExportSpecifiers + toModuleElements (JSModuleExportDeclaration _ jsExportDeclaration) + | JSExportLocals jsExportClause _ <- jsExportDeclaration + , JSExportClause _ jsExportSpecifiers _ <- jsExportClause + = pure . ExportsList <$> exportSpecifiersList Nothing jsExportSpecifiers + toModuleElements (JSModuleExportDeclaration _ jsExportDeclaration) + | JSExport jsStatement _ <- jsExportDeclaration + , Just (name, decl) <- matchInternalMember jsStatement + = pure [ Member jsStatement Internal name decl [] + , ExportsList [toRegularExport' name] + ] + toModuleElements (JSModuleExportDeclaration _ JSExport{}) + = err UnsupportedExport + + toModuleElements item@(JSModuleStatementListItem jsStatement) + | Just (importName, importPath) <- matchRequire jsStatement + = pure [Import item importName $ checkImportPath importPath mid mids] + toModuleElements (JSModuleStatementListItem jsStatement) + | Just (visibility, name, decl) <- matchMember jsStatement + = pure [Member jsStatement visibility name decl []] + toModuleElements (JSModuleStatementListItem jsStatement) + | Just props <- matchExportsAssignment jsStatement + = pure . ExportsList <$> traverse objectPropertyToExport (trailingCommaList props) where - toExport :: JSObjectProperty -> m (ExportType, String, JSExpression, [Key]) - toExport (JSPropertyNameandValue name _ [val]) = - (,,val,[]) <$> exportType val + objectPropertyToExport :: JSObjectProperty -> m (ExportType, String, JSExpression, [Key]) + objectPropertyToExport (JSPropertyNameandValue name _ [val]) = + (,,val,[]) <$> expressionExportType val <*> extractLabel' name - toExport _ = err UnsupportedExport + objectPropertyToExport _ = err UnsupportedExport - exportType :: JSExpression -> m ExportType - exportType (JSMemberDot f _ _) + expressionExportType :: JSExpression -> m ExportType + expressionExportType (JSMemberDot f _ _) | JSIdentifier _ "$foreign" <- f = pure ForeignReexport | JSIdentifier _ ident <- f = pure (RegularExport ident) - exportType (JSMemberSquare f _ _ _) + expressionExportType (JSMemberSquare f _ _ _) | JSIdentifier _ "$foreign" <- f = pure ForeignReexport | JSIdentifier _ ident <- f = pure (RegularExport ident) - exportType (JSIdentifier _ s) = pure (RegularExport s) - exportType _ = err UnsupportedExport + expressionExportType (JSIdentifier _ s) = pure (RegularExport s) + expressionExportType _ = err UnsupportedExport extractLabel' = maybe (err UnsupportedExport) pure . extractLabel - toModuleElement other = pure (Other other) + toModuleElements (JSModuleStatementListItem other) = pure [Other other] + + exportSpecifiersList from = + fmap catMaybes . traverse (exportSpecifier from) . commaList + + exportSpecifier from (JSExportSpecifier jsIdent) + = traverse (toExport' from) $ identName jsIdent + exportSpecifier from (JSExportSpecifierAs jsIdent _ jsIdentAs) + = sequence $ toExport from <$> identName jsIdent <*> identName jsIdentAs + + toExport :: Maybe String -> String -> String -> m (ExportType, String, JSExpression, [Key]) + toExport (Just from) name as + | from == "./foreign.js" = + pure . (ForeignReexport, as,, []) $ + JSMemberSquare (JSIdentifier sp "$foreign") JSNoAnnot + (stringLiteral name) JSNoAnnot + | Just from' <- stripSuffix "/index.js" =<< stripPrefix "../" from = + pure . (RegularExport name, as,, []) $ + JSMemberSquare (JSIdentifier sp (T.unpack . moduleNameToJs . ModuleName $ T.pack from')) JSNoAnnot + (stringLiteral name) JSNoAnnot + | otherwise = err UnsupportedExport + toExport Nothing name as = + pure $ toRegularExport name as + + toExport' from name = toExport from name name + + toRegularExport name as = + (RegularExport name, as, JSIdentifier sp name, []) + + toRegularExport' name = toRegularExport name name + +data ForeignModuleExports = + ForeignModuleExports + { cjsExports :: [String] + , esExports :: [String] + } deriving (Show) + +instance Semigroup ForeignModuleExports where + (ForeignModuleExports cjsExports esExports) <> (ForeignModuleExports cjsExports' esExports') = + ForeignModuleExports (cjsExports <> cjsExports') (esExports <> esExports') +instance Monoid ForeignModuleExports where + mempty = ForeignModuleExports [] [] -- Get a list of all the exported identifiers from a foreign module. -- @@ -423,21 +557,25 @@ toModule mids mid filename top getExportedIdentifiers :: forall m. (MonadError ErrorMessage m) => String -> JSAST - -> m [String] + -> m ForeignModuleExports getExportedIdentifiers mname top - | JSAstProgram stmts _ <- top = concat <$> traverse go stmts + | JSAstModule jsModuleItems _ <- top = fold <$> traverse go jsModuleItems | otherwise = err InvalidTopLevel where err :: ErrorMessage -> m a err = throwError . ErrorInModule (ModuleIdentifier mname Foreign) - go stmt - | Just props <- matchExportsAssignment stmt - = traverse toIdent (trailingCommaList props) - | Just (Public, name, _) <- matchMember stmt - = pure [name] + go (JSModuleStatementListItem jsStatement) + | Just props <- matchExportsAssignment jsStatement + = do cjsExports <- traverse toIdent (trailingCommaList props) + pure ForeignModuleExports{ cjsExports, esExports = [] } + | Just (Public, name, _) <- matchMember jsStatement + = pure ForeignModuleExports{ cjsExports = [name], esExports = [] } | otherwise - = pure [] + = pure mempty + go (JSModuleExportDeclaration _ jsExportDeclaration) = + pure ForeignModuleExports{ cjsExports = [], esExports = exportDeclarationIdentifiers jsExportDeclaration } + go _ = pure mempty toIdent (JSPropertyNameandValue name _ [_]) = extractLabel' name @@ -446,13 +584,57 @@ getExportedIdentifiers mname top extractLabel' = maybe (err UnsupportedExport) pure . extractLabel + exportDeclarationIdentifiers (JSExportFrom jsExportClause _ _) = + exportClauseIdentifiers jsExportClause + exportDeclarationIdentifiers (JSExportLocals jsExportClause _) = + exportClauseIdentifiers jsExportClause + exportDeclarationIdentifiers (JSExport jsStatement _) = + exportStatementIdentifiers jsStatement + + exportClauseIdentifiers (JSExportClause _ jsExportsSpecifiers _) = + mapMaybe exportSpecifierName $ commaList jsExportsSpecifiers + + exportSpecifierName (JSExportSpecifier jsIdent) = identName jsIdent + exportSpecifierName (JSExportSpecifierAs _ _ jsIdentAs) = identName jsIdentAs + +data ForeignModuleImports = + ForeignModuleImports + { cjsImports :: [String] + , esImports :: [String] + } deriving (Show) + +instance Semigroup ForeignModuleImports where + (ForeignModuleImports cjsImports esImports) <> (ForeignModuleImports cjsImports' esImports') = + ForeignModuleImports (cjsImports <> cjsImports') (esImports <> esImports') +instance Monoid ForeignModuleImports where + mempty = ForeignModuleImports [] [] + +-- Get a list of all the imported module identifiers from a foreign module. +getImportedModules :: forall m. (MonadError ErrorMessage m) + => String + -> JSAST + -> m ForeignModuleImports +getImportedModules mname top + | JSAstModule jsModuleItems _ <- top = pure $ foldMap go jsModuleItems + | otherwise = err InvalidTopLevel + where + err :: ErrorMessage -> m a + err = throwError . ErrorInModule (ModuleIdentifier mname Foreign) + + go (JSModuleStatementListItem jsStatement) + | Just (_, mid) <- matchRequire jsStatement + = ForeignModuleImports{ cjsImports = [mid], esImports = [] } + go (JSModuleImportDeclaration _ jsImportDeclaration) = + ForeignModuleImports{ cjsImports = [], esImports = [importDeclarationModuleId jsImportDeclaration] } + go _ = mempty + + importDeclarationModuleId (JSImportDeclaration _ (JSFromClause _ _ mid) _) = mid + importDeclarationModuleId (JSImportDeclarationBare _ mid _) = mid + -- Matches JS statements like this: -- var ModuleName = require("file"); -matchRequire :: S.Set String - -> ModuleIdentifier - -> JSStatement - -> Maybe (String, Either String ModuleIdentifier) -matchRequire mids mid stmt +matchRequire :: JSStatement -> Maybe (String, String) +matchRequire stmt | JSVariable _ jsInit _ <- stmt , [JSVarInitExpression var varInit] <- commaList jsInit , JSIdentifier _ importName <- var @@ -460,24 +642,30 @@ matchRequire mids mid stmt , JSMemberExpression req _ argsE _ <- jsInitEx , JSIdentifier _ "require" <- req , [ Just importPath ] <- map fromStringLiteral (commaList argsE) - , importPath' <- checkImportPath importPath mid mids - = Just (importName, importPath') + = Just (importName, importPath) | otherwise = Nothing -- Matches JS member declarations. matchMember :: JSStatement -> Maybe (Visibility, String, JSExpression) matchMember stmt + | Just (name, decl) <- matchInternalMember stmt + = pure (Internal, name, decl) + -- exports.foo = expr; exports["foo"] = expr; + | JSAssignStatement e (JSAssign _) decl _ <- stmt + , Just name <- exportsAccessor e + = Just (Public, name, decl) + | otherwise + = Nothing + +matchInternalMember :: JSStatement -> Maybe (String, JSExpression) +matchInternalMember stmt -- var foo = expr; | JSVariable _ jsInit _ <- stmt , [JSVarInitExpression var varInit] <- commaList jsInit , JSIdentifier _ name <- var , JSVarInit _ decl <- varInit - = Just (Internal, name, decl) - -- exports.foo = expr; exports["foo"] = expr; - | JSAssignStatement e (JSAssign _) decl _ <- stmt - , Just name <- exportsAccessor e - = Just (Public, name, decl) + = pure (name, decl) | otherwise = Nothing @@ -526,8 +714,8 @@ compile modules entryPoints = filteredModules where -- | Create a set of vertices for a module element. -- - -- Require statements don't contribute towards dependencies, since they effectively get - -- inlined wherever they are used inside other module elements. + -- Imports declarations and require statements don't contribute towards dependencies, + -- since they effectively get inlined wherever they are used inside other module elements. toVertices :: ModuleIdentifier -> ModuleElement -> [(ModuleElement, Key, [Key])] toVertices p m@(Member _ visibility nm _ deps) = [(m, (p, nm, visibility), deps)] toVertices p m@(ExportsList exps) = map toVertex exps @@ -567,11 +755,11 @@ compile modules entryPoints = filteredModules | otherwise = d : go rest skipDecl :: ModuleElement -> ModuleElement - skipDecl (Require s _ _) = Skip s - skipDecl (Member s _ _ _ _) = Skip s - skipDecl (ExportsList _) = Skip (JSEmptyStatement JSNoAnnot) - skipDecl (Other s) = Skip s - skipDecl (Skip s) = Skip s + skipDecl (Import item _ _) = Skip item + skipDecl (Member stmt _ _ _ _) = Skip $ JSModuleStatementListItem stmt + skipDecl (ExportsList _) = Skip . JSModuleStatementListItem $ JSEmptyStatement JSNoAnnot + skipDecl (Other stmt) = Skip $ JSModuleStatementListItem stmt + skipDecl (Skip item) = Skip item -- | Filter out the exports for members which aren't used. filterExports :: ModuleElement -> ModuleElement @@ -580,7 +768,7 @@ compile modules entryPoints = filteredModules isDeclUsed :: ModuleElement -> Bool isDeclUsed (Member _ visibility nm _ _) = isKeyUsed (mid, nm, visibility) - isDeclUsed (Require _ _ (Right midRef)) = midRef `S.member` modulesReferenced + isDeclUsed (Import _ _ (Right midRef)) = midRef `S.member` modulesReferenced isDeclUsed _ = True isKeyUsed :: Key -> Bool @@ -601,7 +789,7 @@ sortModules modules = map (\v -> case nodeFor v of (n, _, _) -> n) (reverse (top return (m, mid, mapMaybe getKey els) getKey :: ModuleElement -> Maybe ModuleIdentifier - getKey (Require _ _ (Right mi)) = Just mi + getKey (Import _ _ (Right mi)) = Just mi getKey _ = Nothing -- | A module is empty if it contains no exported members (in other words, @@ -614,7 +802,7 @@ isModuleEmpty (Module _ _ els) = all isElementEmpty els where isElementEmpty :: ModuleElement -> Bool isElementEmpty (ExportsList exps) = null exps - isElementEmpty Require{} = True + isElementEmpty Import{} = True isElementEmpty (Other _) = True isElementEmpty (Skip _) = True isElementEmpty _ = False @@ -655,7 +843,7 @@ codeGen optionsMainModule optionsNamespace ms outFileOpt = (fmap sourceMapping o }) (offsets (0,0) (Right 1 : positions))) moduleFns - (scanl (+) (3 + moduleLength [prelude]) (map (3+) moduleLengths)) -- 3 lines between each module & at top + (scanl (+) (3 + moduleLength [JSModuleStatementListItem prelude]) (map (3+) moduleLengths)) -- 3 lines between each module & at top (map snd modulesJS) } where @@ -665,7 +853,7 @@ codeGen optionsMainModule optionsNamespace ms outFileOpt = (fmap sourceMapping o offsets (m, n) (Right d:rest) = map ((m+) &&& (n+)) [0 .. d - 1] ++ offsets (m+d, n+d) rest offsets _ _ = [] - moduleLength :: [JSStatement] -> Int + moduleLength :: [JSModuleItem] -> Int moduleLength = everything (+) (mkQ 0 countw) where countw :: CommentAnnotation -> Int @@ -684,13 +872,13 @@ codeGen optionsMainModule optionsNamespace ms outFileOpt = (fmap sourceMapping o (jsDecls, lengths) = unzip $ map declToJS ds withLength :: [JSStatement] -> ([JSStatement], Either Int Int) - withLength n = (n, Right $ moduleLength n) + withLength n = (n, Right . moduleLength $ JSModuleStatementListItem <$> n) declToJS :: ModuleElement -> ([JSStatement], Either Int Int) declToJS (Member n _ _ _ _) = withLength [n] declToJS (Other n) = withLength [n] declToJS (Skip n) = ([], Left $ moduleLength [n]) - declToJS (Require _ nm req) = withLength + declToJS (Import _ nm req) = withLength [ JSVariable lfsp (cList [ @@ -698,15 +886,15 @@ codeGen optionsMainModule optionsNamespace ms outFileOpt = (fmap sourceMapping o (JSVarInit sp $ either require (innerModuleReference sp . moduleName) req ) ]) (JSSemi JSNoAnnot) ] - declToJS (ExportsList exps) = withLength $ map toExport exps + declToJS (ExportsList exps) = withLength $ map toCommonJSExport exps where - toExport :: (ExportType, String, JSExpression, [Key]) -> JSStatement - toExport (_, nm, val, _) = + toCommonJSExport :: (ExportType, String, JSExpression, [Key]) -> JSStatement + toCommonJSExport (_, nm, val, _) = JSAssignStatement (JSMemberSquare (JSIdentifier lfsp "exports") JSNoAnnot - (str nm) JSNoAnnot) + (stringLiteral nm) JSNoAnnot) (JSAssign sp) val (JSSemi JSNoAnnot) @@ -744,22 +932,18 @@ codeGen optionsMainModule optionsNamespace ms outFileOpt = (fmap sourceMapping o require :: String -> JSExpression require mn = - JSMemberExpression (JSIdentifier JSNoAnnot "require") JSNoAnnot (cList [ str mn ]) JSNoAnnot + JSMemberExpression (JSIdentifier JSNoAnnot "require") JSNoAnnot + (cList [ stringLiteral mn ]) JSNoAnnot moduleReference :: JSAnnot -> String -> JSExpression moduleReference a mn = JSMemberSquare (JSIdentifier a optionsNamespace) JSNoAnnot - (str mn) JSNoAnnot + (stringLiteral mn) JSNoAnnot innerModuleReference :: JSAnnot -> String -> JSExpression innerModuleReference a mn = JSMemberSquare (JSIdentifier a "$PS") JSNoAnnot - (str mn) JSNoAnnot - - - str :: String -> JSExpression - str s = JSStringLiteral JSNoAnnot $ "\"" ++ s ++ "\"" - + (stringLiteral mn) JSNoAnnot emptyObj :: JSAnnot -> JSExpression emptyObj a = JSObjectLiteral a (JSCTLNone JSLNil) JSNoAnnot @@ -827,9 +1011,6 @@ codeGen optionsMainModule optionsNamespace ms outFileOpt = (fmap sourceMapping o lfsp :: JSAnnot lfsp = JSAnnot tokenPosnEmpty [ WhiteSpace tokenPosnEmpty "\n " ] - sp :: JSAnnot - sp = JSAnnot tokenPosnEmpty [ WhiteSpace tokenPosnEmpty " " ] - -- | The bundling function. -- This function performs dead code elimination, filters empty modules -- and generates and prints the final JavaScript bundle. @@ -848,7 +1029,7 @@ bundleSM inputStrs entryPoints mainModule namespace outFilename reportRawModules forM_ entryPoints $ \mIdent -> when (mIdent `notElem` map mid inputStrs) (throwError (MissingEntryPoint (moduleName mIdent))) input <- forM inputStrs $ \(ident, filename, js) -> do - ast <- either (throwError . ErrorInModule ident . UnableToParseModule) pure $ parse js (moduleName ident) + ast <- either (throwError . ErrorInModule ident . UnableToParseModule) pure $ parseModule js (moduleName ident) return (ident, filename, ast) let mids = S.fromList (map (moduleName . mid) input) diff --git a/src/Language/PureScript/CodeGen/JS.hs b/src/Language/PureScript/CodeGen/JS.hs index f3d0253dd6..d75ed5778c 100644 --- a/src/Language/PureScript/CodeGen/JS.hs +++ b/src/Language/PureScript/CodeGen/JS.hs @@ -9,17 +9,18 @@ module Language.PureScript.CodeGen.JS import Prelude.Compat import Protolude (ordNub) -import Control.Arrow ((&&&)) import Control.Monad (forM, replicateM, void) import Control.Monad.Except (MonadError, throwError) import Control.Monad.Reader (MonadReader, asks) import Control.Monad.Supply.Class -import Data.List ((\\), intersect) +import Data.Bifunctor (first) +import Data.List ((\\), intersect, uncons) +import qualified Data.List.NonEmpty as NEL (nonEmpty) import qualified Data.Foldable as F import qualified Data.Map as M import qualified Data.Set as S -import Data.Maybe (fromMaybe, isNothing) +import Data.Maybe (fromMaybe, mapMaybe, maybeToList) import Data.String (fromString) import Data.Text (Text) import qualified Data.Text as T @@ -48,9 +49,9 @@ moduleToJs :: forall m . (Monad m, MonadReader Options m, MonadSupply m, MonadError MultipleErrors m) => Module Ann - -> Maybe AST + -> Maybe PSString -> m [AST] -moduleToJs (Module _ coms mn _ imps exps reExps foreigns decls) foreign_ = +moduleToJs (Module _ coms mn _ imps exps reExps foreigns decls) foreignInclude = rethrow (addHint (ErrorInModule mn)) $ do let usedNames = concatMap getNames decls let mnLookup = renameImports usedNames imps @@ -65,17 +66,16 @@ moduleToJs (Module _ coms mn _ imps exps reExps foreigns decls) foreign_ = . (\\ (mn : C.primModules)) $ ordNub $ map snd imps F.traverse_ (F.traverse_ checkIntegers) optimized comments <- not <$> asks optionsNoComments - let strict = AST.StringLiteral Nothing "use strict" - let header = if comments && not (null coms) then AST.Comment Nothing coms strict else strict - let foreign' = [AST.VariableIntroduction Nothing "$foreign" foreign_ | not $ null foreigns || isNothing foreign_] - let moduleBody = header : foreign' ++ jsImports ++ concat optimized + let header = if comments && not (null coms) then AST.Comment Nothing coms else id + let foreign' = maybe [] (pure . AST.Import Nothing "$foreign") $ if null foreigns then Nothing else foreignInclude + let moduleBody = (maybe [] (uncurry (:) . first header) . uncons) $ foreign' ++ jsImports ++ concat optimized let foreignExps = exps `intersect` foreigns let standardExps = exps \\ foreignExps let reExps' = M.toList (M.withoutKeys reExps (S.fromList C.primModules)) - let exps' = AST.ObjectLiteral Nothing $ map (mkString . runIdent &&& AST.Var Nothing . identToJs) standardExps - ++ map (mkString . runIdent &&& foreignIdent) foreignExps - ++ concatMap (reExportPairs mnLookup) reExps' - return $ moduleBody ++ [AST.Assignment Nothing (accessorString "exports" (AST.Var Nothing "module")) exps'] + return $ moduleBody + ++ (maybeToList . exportsToJs foreignInclude $ foreignExps) + ++ (maybeToList . exportsToJs Nothing $ standardExps) + ++ mapMaybe reExportsToJs reExps' where @@ -84,21 +84,6 @@ moduleToJs (Module _ coms mn _ imps exps reExps foreigns decls) foreign_ = getNames (NonRec _ ident _) = [ident] getNames (Rec vals) = map (snd . fst) vals - -- | Generate code in the JavaScript IR for re-exported declarations, prepending - -- the module name from whence it was imported. - reExportPairs :: M.Map ModuleName (Ann, ModuleName) -> (ModuleName, [Ident]) -> [(PSString, AST)] - reExportPairs mnLookup (mn', idents) = - let toExportedMember :: Ident -> AST - toExportedMember = - maybe - (AST.Var Nothing . identToJs) - (flip accessor . AST.Var Nothing . moduleNameToJs . snd) - (M.lookup mn' mnLookup) - in - map - (mkString . runIdent &&& toExportedMember) - idents - -- | Creates alternative names for each module to ensure they don't collide -- with declaration names. renameImports :: [Ident] -> [(Ann, ModuleName)] -> M.Map ModuleName (Ann, ModuleName) @@ -125,9 +110,20 @@ moduleToJs (Module _ coms mn _ imps exps reExps foreigns decls) foreign_ = importToJs :: M.Map ModuleName (Ann, ModuleName) -> ModuleName -> m AST importToJs mnLookup mn' = do let ((ss, _, _, _), mnSafe) = fromMaybe (internalError "Missing value in mnLookup") $ M.lookup mn' mnLookup - let moduleBody = AST.App Nothing (AST.Var Nothing "require") - [AST.StringLiteral Nothing (fromString (".." T.unpack (runModuleName mn') "index.js"))] - withPos ss $ AST.VariableIntroduction Nothing (moduleNameToJs mnSafe) (Just moduleBody) + withPos ss $ AST.Import Nothing (moduleNameToJs mnSafe) (moduleImportPath mn') + + -- | Generates JavaScript code for exporting at least one identifier, + -- eventually from another module. + exportsToJs :: Maybe PSString -> [Ident] -> Maybe AST + exportsToJs from = fmap (flip (AST.Export Nothing) from) . NEL.nonEmpty . fmap runIdent + + -- | Generates JavaScript code for re-exporting at least one identifier from + -- from another module. + reExportsToJs :: (ModuleName, [Ident]) -> Maybe AST + reExportsToJs = uncurry exportsToJs . first (Just . moduleImportPath) + + moduleImportPath :: ModuleName -> PSString + moduleImportPath mn' = fromString (".." T.unpack (runModuleName mn') "index.js") -- | Replaces the `ModuleName`s in the AST so that the generated code refers to -- the collision-avoiding renamed module imports. @@ -193,10 +189,13 @@ moduleToJs (Module _ coms mn _ imps exps reExps foreigns decls) foreign_ = -- | Generate code in the simplified JavaScript intermediate representation for an accessor based on -- a PureScript identifier. If the name is not valid in JavaScript (symbol based, reserved name) an -- indexer is returned. - accessor :: Ident -> AST -> AST - accessor (Ident prop) = accessorString $ mkString prop - accessor (GenIdent _ _) = internalError "GenIdent in accessor" - accessor UnusedIdent = internalError "UnusedIdent in accessor" + moduleAccessor :: Ident -> AST -> AST + moduleAccessor (Ident prop) = moduleAccessorString prop + moduleAccessor (GenIdent _ _) = internalError "GenIdent in moduleAccessor" + moduleAccessor UnusedIdent = internalError "UnusedIdent in moduleAccessor" + + moduleAccessorString :: Text -> AST -> AST + moduleAccessorString = accessorString . mkString . T.replace "'" "$prime" accessorString :: PSString -> AST -> AST accessorString prop = AST.Indexer Nothing (AST.StringLiteral Nothing prop) @@ -326,7 +325,7 @@ moduleToJs (Module _ coms mn _ imps exps reExps foreigns decls) foreign_ = -- variable that may have a qualified name. qualifiedToJS :: (a -> Ident) -> Qualified a -> AST qualifiedToJS f (Qualified (Just C.Prim) a) = AST.Var Nothing . runIdent $ f a - qualifiedToJS f (Qualified (Just mn') a) | mn /= mn' = accessor (f a) (AST.Var Nothing (moduleNameToJs mn')) + qualifiedToJS f (Qualified (Just mn') a) | mn /= mn' = moduleAccessor (f a) (AST.Var Nothing (moduleNameToJs mn')) qualifiedToJS f (Qualified _ a) = AST.Var Nothing $ identToJs (f a) foreignIdent :: Ident -> AST diff --git a/src/Language/PureScript/CodeGen/JS/Printer.hs b/src/Language/PureScript/CodeGen/JS/Printer.hs index 39fe77c897..505b4d1150 100644 --- a/src/Language/PureScript/CodeGen/JS/Printer.hs +++ b/src/Language/PureScript/CodeGen/JS/Printer.hs @@ -15,6 +15,7 @@ import qualified Control.Arrow as A import Data.Maybe (fromMaybe) import Data.Text (Text) import qualified Data.Text as T +import qualified Data.List.NonEmpty as NEL (toList) import Language.PureScript.AST (SourceSpan(..)) import Language.PureScript.CodeGen.JS.Common @@ -119,6 +120,26 @@ literals = mkPattern' match' , mconcat <$> forM com comment , prettyPrintJS' js ] + match (Import _ ident from) = return . emit $ + "import * as " <> ident <> " from " <> prettyPrintStringJS from + match (Export _ idents from) = mconcat <$> sequence + [ return $ emit "export {\n" + , withIndent $ do + let exportsStrings = emit . exportedIdentToString from <$> idents + indentString <- currentIndent + return . intercalate (emit ",\n") . NEL.toList $ (indentString <>) <$> exportsStrings + , return $ emit "\n" + , currentIndent + , return . emit $ "}" <> maybe "" ((" from " <>) . prettyPrintStringJS) from + ] + where + exportedIdentToString Nothing ident + | nameIsJsReserved ident || nameIsJsBuiltIn ident + = "$$" <> ident <> " as " <> ident + exportedIdentToString _ "$main" + = T.concatMap identCharToText "$main" <> " as $main" + exportedIdentToString _ ident + = T.concatMap identCharToText ident match _ = mzero comment :: (Emit gen) => Comment -> StateT PrinterState Maybe gen diff --git a/src/Language/PureScript/CoreImp/AST.hs b/src/Language/PureScript/CoreImp/AST.hs index b6dcad1446..4753daeee1 100644 --- a/src/Language/PureScript/CoreImp/AST.hs +++ b/src/Language/PureScript/CoreImp/AST.hs @@ -6,6 +6,7 @@ import Prelude.Compat import Control.Monad ((>=>)) import Control.Monad.Identity (Identity(..), runIdentity) import Data.Text (Text) +import qualified Data.List.NonEmpty as NEL (NonEmpty) import Language.PureScript.AST (SourceSpan(..)) import Language.PureScript.Comments @@ -92,6 +93,10 @@ data AST -- ^ instanceof check | Comment (Maybe SourceSpan) [Comment] AST -- ^ Commented JavaScript + | Import (Maybe SourceSpan) Text PSString + -- ^ Imported identifier and path to its module + | Export (Maybe SourceSpan) (NEL.NonEmpty Text) (Maybe PSString) + -- ^ Exported identifiers and optional path to their module (for re-exports) deriving (Show, Eq) withSourceSpan :: SourceSpan -> AST -> AST @@ -123,6 +128,8 @@ withSourceSpan withSpan = go where go (Throw _ js) = Throw ss js go (InstanceOf _ j1 j2) = InstanceOf ss j1 j2 go (Comment _ com j) = Comment ss com j + go (Import _ ident from) = Import ss ident from + go (Export _ idents from) = Export ss idents from getSourceSpan :: AST -> Maybe SourceSpan getSourceSpan = go where @@ -150,6 +157,8 @@ getSourceSpan = go where go (Throw ss _) = ss go (InstanceOf ss _ _) = ss go (Comment ss _ _) = ss + go (Import ss _ _) = ss + go (Export ss _ _) = ss everywhere :: (AST -> AST) -> AST -> AST everywhere f = go where diff --git a/src/Language/PureScript/Errors.hs b/src/Language/PureScript/Errors.hs index 9029417740..a0e55bfd4e 100644 --- a/src/Language/PureScript/Errors.hs +++ b/src/Language/PureScript/Errors.hs @@ -63,6 +63,9 @@ data SimpleErrorMessage | UnusedFFIImplementations ModuleName [Ident] | InvalidFFIIdentifier ModuleName Text | DeprecatedFFIPrime ModuleName Text + | DeprecatedFFICommonJSModule ModuleName FilePath + | UnsupportedFFICommonJSExports ModuleName [Text] + | UnsupportedFFICommonJSImports ModuleName [Text] | FileIOError Text IOError -- ^ A description of what we were trying to do, and the error which occurred | InfiniteType SourceType | InfiniteKind SourceType @@ -237,6 +240,9 @@ errorCode em = case unwrapErrorMessage em of UnusedFFIImplementations{} -> "UnusedFFIImplementations" InvalidFFIIdentifier{} -> "InvalidFFIIdentifier" DeprecatedFFIPrime{} -> "DeprecatedFFIPrime" + DeprecatedFFICommonJSModule {} -> "DeprecatedFFICommonJSModule" + UnsupportedFFICommonJSExports {} -> "UnsupportedFFICommonJSExports" + UnsupportedFFICommonJSImports {} -> "UnsupportedFFICommonJSImports" FileIOError{} -> "FileIOError" InfiniteType{} -> "InfiniteType" InfiniteKind{} -> "InfiniteKind" @@ -699,9 +705,22 @@ prettyPrintSingleError (PPEOptions codeColor full level showDocs relPath) e = fl paras [ line $ "In the FFI module for " <> markCode (runModuleName mn) <> ":" , indent . paras $ [ line $ "The identifier " <> markCode ident <> " contains a prime (" <> markCode "'" <> ")." - , line "Primes in identifiers exported from FFI modules are deprecated and won’t be supported in the future." + , line "Primes are not allowed in identifiers exported from FFI modules." ] ] + renderSimpleErrorMessage (DeprecatedFFICommonJSModule mn path) = + paras [ line $ "A CommonJS foreign module implementation was provided for module " <> markCode (runModuleName mn) <> ": " + , indent . lineS $ path + , line "CommonJS foreign modules are deprecated and won't be supported in the future." + ] + renderSimpleErrorMessage (UnsupportedFFICommonJSExports mn idents) = + paras [ line $ "The following CommonJS exports are not supported in the ES foreign module for module " <> markCode (runModuleName mn) <> ": " + , indent . paras $ map line idents + ] + renderSimpleErrorMessage (UnsupportedFFICommonJSImports mn mids) = + paras [ line $ "The following CommonJS imports are no supported in the ES foreign module for module " <> markCode (runModuleName mn) <> ": " + , indent . paras $ map line mids + ] renderSimpleErrorMessage InvalidDoBind = line "The last statement in a 'do' block must be an expression, but this block ends with a binder." renderSimpleErrorMessage InvalidDoLet = diff --git a/src/Language/PureScript/Interactive/IO.hs b/src/Language/PureScript/Interactive/IO.hs index 92a2e8dc64..f1d61cb47a 100644 --- a/src/Language/PureScript/Interactive/IO.hs +++ b/src/Language/PureScript/Interactive/IO.hs @@ -1,13 +1,24 @@ -module Language.PureScript.Interactive.IO (findNodeProcess, getHistoryFilename) where +{-# LANGUAGE TypeApplications #-} + +module Language.PureScript.Interactive.IO (findNodeProcess, readNodeProcessWithExitCode, getHistoryFilename) where import Prelude.Compat -import Control.Monad (msum) +import Control.Monad (msum, void) +import Control.Monad.Error.Class (throwError) +import Control.Monad.Trans.Class (lift) +import Control.Monad.Trans.Except (ExceptT(..), runExceptT) import Control.Monad.Trans.Maybe (MaybeT(..), runMaybeT) +import Data.Functor ((<&>)) +import Data.List (isInfixOf) import System.Directory (XdgDirectory (..), createDirectoryIfMissing, getAppUserDataDirectory, getXdgDirectory, findExecutable, doesFileExist) +import System.Exit (ExitCode(ExitFailure, ExitSuccess)) import System.FilePath (takeDirectory, ()) +import System.Process (readProcessWithExitCode) +import Text.Parsec ((), many1, parse, sepBy) +import Text.Parsec.Char (char, digit) mkdirp :: FilePath -> IO () mkdirp = createDirectoryIfMissing True . takeDirectory @@ -21,9 +32,42 @@ onFirstFileMatching f pathVariants = runMaybeT . msum $ map (MaybeT . f) pathVar -- Locates the node executable. -- Checks for either @nodejs@ or @node@. -- -findNodeProcess :: IO (Maybe String) -findNodeProcess = onFirstFileMatching findExecutable names - where names = ["nodejs", "node"] +findNodeProcess :: IO (Either String String) +findNodeProcess = onFirstFileMatching findExecutable ["nodejs", "node"] <&> + maybe (throwError "Could not find node.js. Do you have node.js installed and available in your PATH?") pure + +findNodeVersion :: String -> IO (Maybe String) +findNodeVersion node = do + result <- readProcessWithExitCode node ["--version"] "" + return $ case result of + (ExitSuccess, version, _) -> Just version + (ExitFailure _, _, _) -> Nothing + +readNodeProcessWithExitCode :: Maybe FilePath -> [String] -> String -> IO (Either String (ExitCode, String, String)) +readNodeProcessWithExitCode nodePath nodeArgs stdin = runExceptT $ do + process <- maybe (ExceptT findNodeProcess) pure nodePath + (_, minor, _) <- lift (findNodeVersion process) >>= \case + Nothing -> throwError "Could not find node.js version." + Just version -> do + let semver = do + void $ char 'v' + major : minor : patch : _ <- fmap (read @Int) (many1 digit) `sepBy` void (char '.') + pure (major, minor, patch) + case parse (semver "Could not parse node.js version.") "" version of + Left err -> throwError $ show err + Right (major, minor, patch) + | major < 12 -> throwError "Unsupported node.js version." + | otherwise -> pure (major, minor, patch) + let nodeArgs' = if minor < 7 then "--experimental-modules" : nodeArgs else nodeArgs + lift (readProcessWithExitCode process nodeArgs' stdin) <&> \case + (ExitSuccess, out, err) -> + (ExitSuccess, out, censorExperimentalWarnings err) + (ExitFailure code, out, err) -> + (ExitFailure code, out, err) + +censorExperimentalWarnings :: String -> String +censorExperimentalWarnings = + unlines . filter (not . ("ExperimentalWarning" `isInfixOf`)) . lines -- | -- Grabs the filename where the history is stored. diff --git a/src/Language/PureScript/Interactive/Module.hs b/src/Language/PureScript/Interactive/Module.hs index 66d930b0f2..b79d5fb188 100644 --- a/src/Language/PureScript/Interactive/Module.hs +++ b/src/Language/PureScript/Interactive/Module.hs @@ -98,7 +98,7 @@ indexFile :: FilePath indexFile = ".psci_modules" ++ pathSeparator : "index.js" modulesDir :: FilePath -modulesDir = ".psci_modules" ++ pathSeparator : "node_modules" +modulesDir = ".psci_modules" internalSpan :: P.SourceSpan internalSpan = P.internalModuleSourceSpan "" diff --git a/src/Language/PureScript/Make.hs b/src/Language/PureScript/Make.hs index 2f4065d717..5c3e2f90df 100644 --- a/src/Language/PureScript/Make.hs +++ b/src/Language/PureScript/Make.hs @@ -162,6 +162,8 @@ make ma@MakeActions{..} ms = do -- Write the updated build cache database to disk writeCacheDb $ Cache.removeModules (M.keysSet failures) newCacheDb + writePackageJson + -- If generating docs, also generate them for the Prim modules outputPrimDocs diff --git a/src/Language/PureScript/Make/Actions.hs b/src/Language/PureScript/Make/Actions.hs index 2931ae2191..8bc41c40fa 100644 --- a/src/Language/PureScript/Make/Actions.hs +++ b/src/Language/PureScript/Make/Actions.hs @@ -11,6 +11,7 @@ module Language.PureScript.Make.Actions import Prelude +import Control.Arrow ((&&&)) import Control.Monad hiding (sequence) import Control.Monad.Error.Class (MonadError(..)) import Control.Monad.IO.Class @@ -18,6 +19,7 @@ import Control.Monad.Reader (asks) import Control.Monad.Supply import Control.Monad.Trans.Class (MonadTrans(..)) import Control.Monad.Writer.Class (MonadWriter(..)) +import Data.Aeson (Value(String), (.=), object) import Data.Bifunctor (bimap) import Data.Either (partitionEithers) import Data.Foldable (for_) @@ -37,7 +39,6 @@ import qualified Language.PureScript.CodeGen.JS as J import Language.PureScript.CodeGen.JS.Printer import qualified Language.PureScript.CoreFn as CF import qualified Language.PureScript.CoreFn.ToJSON as CFJ -import qualified Language.PureScript.CoreImp.AST as Imp import Language.PureScript.Crash import qualified Language.PureScript.CST as CST import qualified Language.PureScript.Docs.Prim as Docs.Prim @@ -109,6 +110,9 @@ data MakeActions m = MakeActions , writeCacheDb :: CacheDb -> m () -- ^ Write the given cache database to some external source (e.g. a file on -- disk). + , writePackageJson :: m () + -- ^ Write to the output directory the package.json file allowing Node.js to + -- load .js files as ES modules. , outputPrimDocs :: m () -- ^ If generating docs, output the documentation for the Prim modules } @@ -135,6 +139,15 @@ writeCacheDb' -> m () writeCacheDb' = writeJSONFile . cacheDbFile +writePackageJson' + :: (MonadIO m, MonadError MultipleErrors m) + => FilePath + -- ^ The path to the output directory + -> m () +writePackageJson' outputDir = writeJSONFile (outputDir "package.json") $ object + [ "type" .= String "module" + ] + -- | A set of make actions that read and write modules from the given directory. buildMakeActions :: FilePath @@ -147,7 +160,7 @@ buildMakeActions -- ^ Generate a prefix comment? -> MakeActions Make buildMakeActions outputDir filePathMap foreigns usePrefix = - MakeActions getInputTimestampsAndHashes getOutputTimestamp readExterns codegen ffiCodegen progress readCacheDb writeCacheDb outputPrimDocs + MakeActions getInputTimestampsAndHashes getOutputTimestamp readExterns codegen ffiCodegen progress readCacheDb writeCacheDb writePackageJson outputPrimDocs where getInputTimestampsAndHashes @@ -233,7 +246,7 @@ buildMakeActions outputDir filePathMap foreigns usePrefix = | not $ requiresForeign m -> do return Nothing | otherwise -> do - return $ Just $ Imp.App Nothing (Imp.Var Nothing "require") [Imp.StringLiteral Nothing "./foreign.js"] + return $ Just "./foreign.js" Nothing | requiresForeign m -> throwError . errorMessage' (CF.moduleSourceSpan m) $ MissingFFIModule mn | otherwise -> return Nothing rawJs <- J.moduleToJs m foreignInclude @@ -260,12 +273,34 @@ buildMakeActions outputDir filePathMap foreigns usePrefix = Just path | not $ requiresForeign m -> tell $ errorMessage' (CF.moduleSourceSpan m) $ UnnecessaryFFIModule mn path - | otherwise -> - checkForeignDecls m path + | otherwise -> do + (foreignModuleType, foreignIdents) <- checkForeignDecls m path + case foreignModuleType of + ESModule -> copyFile path (outputFilename mn "foreign.js") + CJSModule -> do + tell $ errorMessage' (CF.moduleSourceSpan m) $ DeprecatedFFICommonJSModule mn path + copyFile path (outputFilename mn "foreign.cjs") + writeESForeignModuleWrapper mn foreignIdents + Nothing | requiresForeign m -> throwError . errorMessage' (CF.moduleSourceSpan m) $ MissingFFIModule mn | otherwise -> return () - for_ (mn `M.lookup` foreigns) $ \path -> - copyFile path (outputFilename mn "foreign.js") + + writeESForeignModuleWrapper :: ModuleName -> S.Set Ident -> Make () + writeESForeignModuleWrapper mn idents = + writeTextFile (outputFilename mn "foreign.js") wrapper + where + xs = (J.identToJs &&& runIdent) <$> S.toList idents + wrapper = TE.encodeUtf8 . T.intercalate "\n" $ + "import $foreign from \"./foreign.cjs\";" : + fmap (uncurry toLocalDeclaration) xs ++ + [ "export { " <> T.intercalate ", " (uncurry toNamedExport <$> xs) <> " };" + , "" + ] + toLocalDeclaration local exported = + "var " <> local <> " = $foreign." <> exported <> ";" + toNamedExport local exported + | local == exported = local + | otherwise = local <> " as " <> exported genSourceMap :: String -> String -> Int -> [SMap] -> Make () genSourceMap dir mapFile extraLines mappings = do @@ -303,18 +338,38 @@ buildMakeActions outputDir filePathMap foreigns usePrefix = writeCacheDb :: CacheDb -> Make () writeCacheDb = writeCacheDb' outputDir + writePackageJson :: Make () + writePackageJson = writePackageJson' outputDir + +data ForeignModuleType = ESModule | CJSModule deriving (Show) + -- | Check that the declarations in a given PureScript module match with those -- in its corresponding foreign module. -checkForeignDecls :: CF.Module ann -> FilePath -> Make () +checkForeignDecls :: CF.Module ann -> FilePath -> Make (ForeignModuleType, S.Set Ident) checkForeignDecls m path = do jsStr <- T.unpack <$> readTextFile path - js <- either (errorParsingModule . Bundle.UnableToParseModule) pure $ JS.parse jsStr path + js <- either (errorParsingModule . Bundle.UnableToParseModule) pure $ JS.parseModule jsStr path + + (foreignModuleType, foreignIdentsStrs) <- + case (,) <$> getForeignModuleExports js <*> getForeignModuleImports js of + Left reason -> errorParsingModule reason + Right (Bundle.ForeignModuleExports{..}, Bundle.ForeignModuleImports{..}) + | not (null cjsExports && null cjsImports) + , null esExports + , null esImports -> do + let deprecatedFFI = filter (elem '\'') cjsExports + unless (null deprecatedFFI) $ + errorDeprecatedForeignPrimes deprecatedFFI - foreignIdentsStrs <- either errorParsingModule pure $ getExps js + pure (CJSModule, cjsExports) + | otherwise -> do + unless (null cjsImports) $ + errorUnsupportedFFICommonJSImports cjsImports - let deprecatedFFI = filter (elem '\'') foreignIdentsStrs - unless (null deprecatedFFI) $ - warningDeprecatedForeignPrimes deprecatedFFI + unless (null cjsExports) $ + errorUnsupportedFFICommonJSExports cjsExports + + pure (ESModule, esExports) foreignIdents <- either errorInvalidForeignIdentifiers @@ -332,6 +387,7 @@ checkForeignDecls m path = do throwError . errorMessage' modSS . MissingFFIImplementations mname $ S.toList missingFFI + pure (foreignModuleType, foreignIdents) where mname = CF.moduleName m modSS = CF.moduleSourceSpan m @@ -339,16 +395,27 @@ checkForeignDecls m path = do errorParsingModule :: Bundle.ErrorMessage -> Make a errorParsingModule = throwError . errorMessage' modSS . ErrorParsingFFIModule path . Just - getExps :: JS.JSAST -> Either Bundle.ErrorMessage [String] - getExps = Bundle.getExportedIdentifiers (T.unpack (runModuleName mname)) + getForeignModuleExports :: JS.JSAST -> Either Bundle.ErrorMessage Bundle.ForeignModuleExports + getForeignModuleExports = Bundle.getExportedIdentifiers (T.unpack (runModuleName mname)) + + getForeignModuleImports :: JS.JSAST -> Either Bundle.ErrorMessage Bundle.ForeignModuleImports + getForeignModuleImports = Bundle.getImportedModules (T.unpack (runModuleName mname)) errorInvalidForeignIdentifiers :: [String] -> Make a errorInvalidForeignIdentifiers = throwError . mconcat . map (errorMessage . InvalidFFIIdentifier mname . T.pack) - warningDeprecatedForeignPrimes :: [String] -> Make () - warningDeprecatedForeignPrimes = - tell . mconcat . map (errorMessage' modSS . DeprecatedFFIPrime mname . T.pack) + errorDeprecatedForeignPrimes :: [String] -> Make a + errorDeprecatedForeignPrimes = + throwError . mconcat . map (errorMessage' modSS . DeprecatedFFIPrime mname . T.pack) + + errorUnsupportedFFICommonJSExports :: [String] -> Make a + errorUnsupportedFFICommonJSExports = + throwError . errorMessage' modSS . UnsupportedFFICommonJSExports mname . map T.pack + + errorUnsupportedFFICommonJSImports :: [String] -> Make a + errorUnsupportedFFICommonJSImports = + throwError . errorMessage' modSS . UnsupportedFFICommonJSImports mname . map T.pack parseIdents :: [String] -> Either [String] [Ident] parseIdents strs = diff --git a/tests/TestBundle.hs b/tests/TestBundle.hs index 42712c6100..bff8f30d5a 100644 --- a/tests/TestBundle.hs +++ b/tests/TestBundle.hs @@ -7,6 +7,7 @@ import Prelude.Compat import qualified Language.PureScript as P import Language.PureScript.Bundle +import Language.PureScript.Interactive.IO (readNodeProcessWithExitCode) import Data.Function (on) import Data.List (minimumBy) @@ -16,7 +17,6 @@ import Control.Monad.IO.Class (liftIO) import Control.Monad.Trans.Except import System.Exit -import System.Process import System.FilePath import System.IO import System.IO.UTF8 @@ -51,9 +51,8 @@ assertBundles support inputFiles outputFile = do case result of Left errs -> expectationFailure . P.prettyPrintMultipleErrors P.defaultPPEOptions $ errs Right _ -> do - process <- findNodeProcess - jsFiles <- Glob.globDir1 (Glob.compile "**/*.js") modulesDir - let entryPoint = modulesDir "index.js" + jsFiles <- concat <$> Glob.globDir [Glob.compile "*/*.js", Glob.compile "*/foreign.cjs"] modulesDir + let entryPoint = modulesDir "index.cjs" let entryModule = map (`ModuleIdentifier` Regular) ["Main"] bundled <- runExceptT $ do input <- forM jsFiles $ \filename -> do @@ -64,15 +63,15 @@ assertBundles support inputFiles outputFile = do case bundled of Right (_, js) -> do writeUTF8File entryPoint js - nodeResult <- traverse (\node -> readProcessWithExitCode node [entryPoint] "") process + nodeResult <- readNodeProcessWithExitCode Nothing [entryPoint] "" hPutStrLn outputFile $ "\n" <> takeFileName (last inputFiles) <> ":" case nodeResult of - Just (ExitSuccess, out, err) + Right (ExitSuccess, out, err) | not (null err) -> expectationFailure $ "Test wrote to stderr:\n\n" <> err | not (null out) && trim (last (lines out)) == "Done" -> hPutStr outputFile out | otherwise -> expectationFailure $ "Test did not finish with 'Done':\n\n" <> out - Just (ExitFailure _, _, err) -> expectationFailure err - Nothing -> expectationFailure "Couldn't find node.js executable" + Right (ExitFailure _, _, err) -> expectationFailure err + Left err -> expectationFailure err Left err -> expectationFailure $ "Could not bundle: " ++ show err logfile :: FilePath diff --git a/tests/TestCompiler.hs b/tests/TestCompiler.hs index 06f5a3cf92..72de299196 100644 --- a/tests/TestCompiler.hs +++ b/tests/TestCompiler.hs @@ -26,6 +26,7 @@ import Prelude () import Prelude.Compat import qualified Language.PureScript as P +import Language.PureScript.Interactive.IO (readNodeProcessWithExitCode) import Control.Arrow ((>>>)) import Data.Function (on) @@ -38,7 +39,6 @@ import qualified Data.Text.Encoding as T import Control.Monad import System.Exit -import System.Process import System.FilePath import System.IO import System.IO.UTF8 (readUTF8File) @@ -128,18 +128,17 @@ assertCompiles support inputFiles outputFile = do case result of Left errs -> expectationFailure . P.prettyPrintMultipleErrors P.defaultPPEOptions $ errs Right _ -> do - process <- findNodeProcess let entryPoint = modulesDir "index.js" - writeFile entryPoint "require('Main').main()" - nodeResult <- traverse (\node -> readProcessWithExitCode node [entryPoint] "") process + writeFile entryPoint "import('./Main/index.js').then(({ main }) => main());" + nodeResult <- readNodeProcessWithExitCode Nothing [entryPoint] "" hPutStrLn outputFile $ "\n" <> takeFileName (last inputFiles) <> ":" case nodeResult of - Just (ExitSuccess, out, err) + Right (ExitSuccess, out, err) | not (null err) -> expectationFailure $ "Test wrote to stderr:\n\n" <> err | not (null out) && trim (last (lines out)) == "Done" -> hPutStr outputFile out | otherwise -> expectationFailure $ "Test did not finish with 'Done':\n\n" <> out - Just (ExitFailure _, _, err) -> expectationFailure err - Nothing -> expectationFailure "Couldn't find node.js executable" + Right (ExitFailure _, _, err) -> expectationFailure err + Left err -> expectationFailure err assertCompilesWithWarnings :: SupportModules diff --git a/tests/TestMake.hs b/tests/TestMake.hs index c4d811e1ac..d7cfc4296e 100644 --- a/tests/TestMake.hs +++ b/tests/TestMake.hs @@ -88,7 +88,7 @@ spec = do writeFileWithTimestamp modulePath timestampA moduleContent compile [modulePath] `shouldReturn` moduleNames ["Module"] - writeFileWithTimestamp moduleFFIPath timestampB "exports.bar = 1;\n" + writeFileWithTimestamp moduleFFIPath timestampB "export var bar = 1;\n" compile [modulePath] `shouldReturn` moduleNames ["Module"] it "recompiles if an FFI file was removed" $ do @@ -98,7 +98,7 @@ spec = do moduleContent = "module Module where\nfoo = 0\n" writeFileWithTimestamp modulePath timestampA moduleContent - writeFileWithTimestamp moduleFFIPath timestampB "exports.bar = 1;\n" + writeFileWithTimestamp moduleFFIPath timestampB "export var bar = 1;\n" compile [modulePath] `shouldReturn` moduleNames ["Module"] removeFile moduleFFIPath diff --git a/tests/TestPsci/TestEnv.hs b/tests/TestPsci/TestEnv.hs index 5ac693aa1b..8b8d5d7eb7 100644 --- a/tests/TestPsci/TestEnv.hs +++ b/tests/TestPsci/TestEnv.hs @@ -17,7 +17,6 @@ import System.Directory (getCurrentDirectory, doesPathExist, removeFil import System.Exit import System.FilePath ((), pathSeparator) import qualified System.FilePath.Glob as Glob -import System.Process (readProcessWithExitCode) import Test.Hspec (shouldBe, Expectation) -- | A monad transformer for handle PSCi actions in tests @@ -55,13 +54,12 @@ execTestPSCi i = do -- command evaluation. jsEval :: TestPSCi String jsEval = liftIO $ do - writeFile indexFile "require('$PSCI')['$main']();" - process <- findNodeProcess - result <- traverse (\node -> readProcessWithExitCode node [indexFile] "") process + writeFile indexFile "import('./$PSCI/index.js').then(({ $main }) => $main());" + result <- readNodeProcessWithExitCode Nothing [indexFile] "" case result of - Just (ExitSuccess, out, _) -> return out - Just (ExitFailure _, _, err) -> putStrLn err >> exitFailure - Nothing -> putStrLn "Couldn't find node.js" >> exitFailure + Right (ExitSuccess, out, _) -> return out + Right (ExitFailure _, _, err) -> putStrLn err >> exitFailure + Left err -> putStrLn err >> exitFailure -- | Run a PSCi command and evaluate its outputs: -- * jsOutputEval is used to evaluate compiled JS output by PSCi diff --git a/tests/TestUtils.hs b/tests/TestUtils.hs index d14cd8d2a9..4db550f8d7 100644 --- a/tests/TestUtils.hs +++ b/tests/TestUtils.hs @@ -5,6 +5,7 @@ import Prelude.Compat import qualified Language.PureScript as P import qualified Language.PureScript.CST as CST +import Language.PureScript.Interactive.IO (findNodeProcess) import Control.Arrow ((***), (>>>)) import Control.Monad @@ -35,11 +36,6 @@ import qualified System.FilePath.Glob as Glob import System.IO import Test.Hspec -findNodeProcess :: IO (Maybe String) -findNodeProcess = runMaybeT . msum $ map (MaybeT . findExecutable) names - where - names = ["nodejs", "node"] - -- | -- Fetches code necessary to run the tests with. The resulting support code -- should then be checked in, so that npm/bower etc is not required to run the @@ -75,15 +71,15 @@ updateSupportCode = withCurrentDirectory "tests/support" $ do heading "Updating support code" callCommand "npm install" -- bower uses shebang "/usr/bin/env node", but we might have nodejs - node <- maybe cannotFindNode pure =<< findNodeProcess + node <- either cannotFindNode pure =<< findNodeProcess -- Sometimes we run as a root (e.g. in simple docker containers) -- And we are non-interactive: https://github.com/bower/bower/issues/1162 callProcess node ["node_modules/bower/bin/bower", "--allow-root", "install", "--config.interactive=false"] writeFile lastUpdatedFile "" where - cannotFindNode :: IO a - cannotFindNode = do - hPutStrLn stderr "Cannot find node (or nodejs) executable" + cannotFindNode :: String -> IO a + cannotFindNode message = do + hPutStrLn stderr message exitFailure getModificationTimeMaybe :: FilePath -> IO (Maybe UTCTime) @@ -251,7 +247,7 @@ trim :: String -> String trim = dropWhile isSpace >>> reverse >>> dropWhile isSpace >>> reverse modulesDir :: FilePath -modulesDir = ".test_modules" "node_modules" +modulesDir = ".test_modules" logpath :: FilePath logpath = "purescript-output" diff --git a/tests/purs/bundle/3551/ModuleWithDeadCode.js b/tests/purs/bundle/3551/ModuleWithDeadCode.js index ab7965286f..faa66d6178 100644 --- a/tests/purs/bundle/3551/ModuleWithDeadCode.js +++ b/tests/purs/bundle/3551/ModuleWithDeadCode.js @@ -1,10 +1,8 @@ -"use strict"; - -var fs = require('fs'); +import * as fs from 'fs'; var source = fs.readFileSync(__filename, 'utf-8'); -exports.results = { +export var results = { fooIsNotEliminated: /^ *var foo =/m.test(source), barIsExported: /^ *exports\["bar"\] =/m.test(source), barIsNotEliminated: /^ *var bar =/m.test(source), diff --git a/tests/purs/bundle/3727.js b/tests/purs/bundle/3727.js index 02e18d2982..d2148a0750 100644 --- a/tests/purs/bundle/3727.js +++ b/tests/purs/bundle/3727.js @@ -1,4 +1,2 @@ -'use strict'; - -exports.foo = 1; -exports.bar = exports.foo; +export var foo = 1; +export { foo as bar }; diff --git a/tests/purs/bundle/ObjectShorthand.js b/tests/purs/bundle/ObjectShorthand.js index 156ff0c9da..225e8bf063 100644 --- a/tests/purs/bundle/ObjectShorthand.js +++ b/tests/purs/bundle/ObjectShorthand.js @@ -1,15 +1,13 @@ -"use strict"; - var foo = 1; -exports.bar = { foo }; +export var bar = { foo }; var baz = 2; -exports.quux = function(baz) { +export var quux = function(baz) { return { baz }; }; -var fs = require('fs'); +import * as fs from 'fs'; var source = fs.readFileSync(__filename, 'utf-8'); -exports.bazIsEliminated = !/^ *var baz =/m.test(source); +export var bazIsEliminated = !/^ *var baz =/m.test(source); diff --git a/tests/purs/warning/DeprecatedFFIPrime.js b/tests/purs/failing/DeprecatedFFIPrime.js similarity index 100% rename from tests/purs/warning/DeprecatedFFIPrime.js rename to tests/purs/failing/DeprecatedFFIPrime.js diff --git a/tests/purs/warning/DeprecatedFFIPrime.out b/tests/purs/failing/DeprecatedFFIPrime.out similarity index 51% rename from tests/purs/warning/DeprecatedFFIPrime.out rename to tests/purs/failing/DeprecatedFFIPrime.out index 94e1912e92..fd22d4708b 100644 --- a/tests/purs/warning/DeprecatedFFIPrime.out +++ b/tests/purs/failing/DeprecatedFFIPrime.out @@ -1,56 +1,56 @@ -Warning 1 of 4: +Error 1 of 4: - at tests/purs/warning/DeprecatedFFIPrime.purs:5:1 - 10:28 (line 5, column 1 - line 10, column 28) + at tests/purs/failing/DeprecatedFFIPrime.purs:5:1 - 10:28 (line 5, column 1 - line 10, column 28) In the FFI module for Main: The identifier a' contains a prime ('). - Primes in identifiers exported from FFI modules are deprecated and won’t be supported in the future. + Primes are not allowed in identifiers exported from FFI modules. See https://github.com/purescript/documentation/blob/master/errors/DeprecatedFFIPrime.md for more information, - or to contribute content related to this warning. + or to contribute content related to this error. -Warning 2 of 4: +Error 2 of 4: - at tests/purs/warning/DeprecatedFFIPrime.purs:5:1 - 10:28 (line 5, column 1 - line 10, column 28) + at tests/purs/failing/DeprecatedFFIPrime.purs:5:1 - 10:28 (line 5, column 1 - line 10, column 28) In the FFI module for Main: The identifier b' contains a prime ('). - Primes in identifiers exported from FFI modules are deprecated and won’t be supported in the future. + Primes are not allowed in identifiers exported from FFI modules. See https://github.com/purescript/documentation/blob/master/errors/DeprecatedFFIPrime.md for more information, - or to contribute content related to this warning. + or to contribute content related to this error. -Warning 3 of 4: +Error 3 of 4: - at tests/purs/warning/DeprecatedFFIPrime.purs:5:1 - 10:28 (line 5, column 1 - line 10, column 28) + at tests/purs/failing/DeprecatedFFIPrime.purs:5:1 - 10:28 (line 5, column 1 - line 10, column 28) In the FFI module for Main: The identifier c' contains a prime ('). - Primes in identifiers exported from FFI modules are deprecated and won’t be supported in the future. + Primes are not allowed in identifiers exported from FFI modules. See https://github.com/purescript/documentation/blob/master/errors/DeprecatedFFIPrime.md for more information, - or to contribute content related to this warning. + or to contribute content related to this error. -Warning 4 of 4: +Error 4 of 4: - at tests/purs/warning/DeprecatedFFIPrime.purs:5:1 - 10:28 (line 5, column 1 - line 10, column 28) + at tests/purs/failing/DeprecatedFFIPrime.purs:5:1 - 10:28 (line 5, column 1 - line 10, column 28) In the FFI module for Main: The identifier d' contains a prime ('). - Primes in identifiers exported from FFI modules are deprecated and won’t be supported in the future. + Primes are not allowed in identifiers exported from FFI modules. See https://github.com/purescript/documentation/blob/master/errors/DeprecatedFFIPrime.md for more information, - or to contribute content related to this warning. + or to contribute content related to this error. diff --git a/tests/purs/failing/DeprecatedFFIPrime.purs b/tests/purs/failing/DeprecatedFFIPrime.purs new file mode 100644 index 0000000000..0100e1fad8 --- /dev/null +++ b/tests/purs/failing/DeprecatedFFIPrime.purs @@ -0,0 +1,10 @@ +-- @shouldFailWith DeprecatedFFIPrime +-- @shouldFailWith DeprecatedFFIPrime +-- @shouldFailWith DeprecatedFFIPrime +-- @shouldFailWith DeprecatedFFIPrime +module Main where + +foreign import a' :: Number +foreign import b' :: Number +foreign import c' :: Number +foreign import d' :: Number diff --git a/tests/purs/failing/MissingFFIImplementations.js b/tests/purs/failing/MissingFFIImplementations.js index d29ee4cff9..ccb7243f7e 100644 --- a/tests/purs/failing/MissingFFIImplementations.js +++ b/tests/purs/failing/MissingFFIImplementations.js @@ -1 +1 @@ -exports.yes = true; +export var yes = true; diff --git a/tests/purs/failing/UnsupportedFFICommonJSExports1.js b/tests/purs/failing/UnsupportedFFICommonJSExports1.js new file mode 100644 index 0000000000..a74e1904db --- /dev/null +++ b/tests/purs/failing/UnsupportedFFICommonJSExports1.js @@ -0,0 +1,2 @@ +export var yes = true; +exports.no = false; diff --git a/tests/purs/failing/UnsupportedFFICommonJSExports1.out b/tests/purs/failing/UnsupportedFFICommonJSExports1.out new file mode 100644 index 0000000000..d39cd8ad0b --- /dev/null +++ b/tests/purs/failing/UnsupportedFFICommonJSExports1.out @@ -0,0 +1,12 @@ +Error found: +at tests/purs/failing/UnsupportedFFICommonJSExports1.purs:2:1 - 5:29 (line 2, column 1 - line 5, column 29) + + The following CommonJS exports are not supported in the ES foreign module for module Main: + + no + + + +See https://github.com/purescript/documentation/blob/master/errors/UnsupportedFFICommonJSExports.md for more information, +or to contribute content related to this error. + diff --git a/tests/purs/failing/UnsupportedFFICommonJSExports1.purs b/tests/purs/failing/UnsupportedFFICommonJSExports1.purs new file mode 100644 index 0000000000..fc64c41988 --- /dev/null +++ b/tests/purs/failing/UnsupportedFFICommonJSExports1.purs @@ -0,0 +1,5 @@ +-- @shouldFailWith UnsupportedFFICommonJSExports +module Main where + +foreign import yes :: Boolean +foreign import no :: Boolean diff --git a/tests/purs/failing/UnsupportedFFICommonJSExports2.js b/tests/purs/failing/UnsupportedFFICommonJSExports2.js new file mode 100644 index 0000000000..10854c8a3b --- /dev/null +++ b/tests/purs/failing/UnsupportedFFICommonJSExports2.js @@ -0,0 +1,4 @@ +import { yes, no } from "some ES module"; + +exports.yes = yes; +exports.no = no; diff --git a/tests/purs/failing/UnsupportedFFICommonJSExports2.out b/tests/purs/failing/UnsupportedFFICommonJSExports2.out new file mode 100644 index 0000000000..d06dad5f4d --- /dev/null +++ b/tests/purs/failing/UnsupportedFFICommonJSExports2.out @@ -0,0 +1,13 @@ +Error found: +at tests/purs/failing/UnsupportedFFICommonJSExports2.purs:2:1 - 5:29 (line 2, column 1 - line 5, column 29) + + The following CommonJS exports are not supported in the ES foreign module for module Main: + + yes + no + + + +See https://github.com/purescript/documentation/blob/master/errors/UnsupportedFFICommonJSExports.md for more information, +or to contribute content related to this error. + diff --git a/tests/purs/failing/UnsupportedFFICommonJSExports2.purs b/tests/purs/failing/UnsupportedFFICommonJSExports2.purs new file mode 100644 index 0000000000..fc64c41988 --- /dev/null +++ b/tests/purs/failing/UnsupportedFFICommonJSExports2.purs @@ -0,0 +1,5 @@ +-- @shouldFailWith UnsupportedFFICommonJSExports +module Main where + +foreign import yes :: Boolean +foreign import no :: Boolean diff --git a/tests/purs/failing/UnsupportedFFICommonJSImports1.js b/tests/purs/failing/UnsupportedFFICommonJSImports1.js new file mode 100644 index 0000000000..c34d89c38c --- /dev/null +++ b/tests/purs/failing/UnsupportedFFICommonJSImports1.js @@ -0,0 +1,4 @@ +var cjsImports = require("some CJS module"); + +export var yes = cjsImports.yes; +export var no = cjsImports.no; diff --git a/tests/purs/failing/UnsupportedFFICommonJSImports1.out b/tests/purs/failing/UnsupportedFFICommonJSImports1.out new file mode 100644 index 0000000000..8cc5f980a4 --- /dev/null +++ b/tests/purs/failing/UnsupportedFFICommonJSImports1.out @@ -0,0 +1,12 @@ +Error found: +at tests/purs/failing/UnsupportedFFICommonJSImports1.purs:2:1 - 5:29 (line 2, column 1 - line 5, column 29) + + The following CommonJS imports are no supported in the ES foreign module for module Main: + + some CJS module + + + +See https://github.com/purescript/documentation/blob/master/errors/UnsupportedFFICommonJSImports.md for more information, +or to contribute content related to this error. + diff --git a/tests/purs/failing/UnsupportedFFICommonJSImports1.purs b/tests/purs/failing/UnsupportedFFICommonJSImports1.purs new file mode 100644 index 0000000000..85e64dc9f3 --- /dev/null +++ b/tests/purs/failing/UnsupportedFFICommonJSImports1.purs @@ -0,0 +1,5 @@ +-- @shouldFailWith UnsupportedFFICommonJSImports +module Main where + +foreign import yes :: Boolean +foreign import no :: Boolean diff --git a/tests/purs/failing/UnsupportedFFICommonJSImports2.js b/tests/purs/failing/UnsupportedFFICommonJSImports2.js new file mode 100644 index 0000000000..7d4b8973b5 --- /dev/null +++ b/tests/purs/failing/UnsupportedFFICommonJSImports2.js @@ -0,0 +1,5 @@ +import { yes } from "some ES module"; +var cjsImports = require("some CJS module"); + +exports.yes = yes; +exports.no = cjsImports.no; diff --git a/tests/purs/failing/UnsupportedFFICommonJSImports2.out b/tests/purs/failing/UnsupportedFFICommonJSImports2.out new file mode 100644 index 0000000000..9be6007f69 --- /dev/null +++ b/tests/purs/failing/UnsupportedFFICommonJSImports2.out @@ -0,0 +1,12 @@ +Error found: +at tests/purs/failing/UnsupportedFFICommonJSImports2.purs:2:1 - 5:29 (line 2, column 1 - line 5, column 29) + + The following CommonJS imports are no supported in the ES foreign module for module Main: + + some CJS module + + + +See https://github.com/purescript/documentation/blob/master/errors/UnsupportedFFICommonJSImports.md for more information, +or to contribute content related to this error. + diff --git a/tests/purs/failing/UnsupportedFFICommonJSImports2.purs b/tests/purs/failing/UnsupportedFFICommonJSImports2.purs new file mode 100644 index 0000000000..85e64dc9f3 --- /dev/null +++ b/tests/purs/failing/UnsupportedFFICommonJSImports2.purs @@ -0,0 +1,5 @@ +-- @shouldFailWith UnsupportedFFICommonJSImports +module Main where + +foreign import yes :: Boolean +foreign import no :: Boolean diff --git a/tests/purs/passing/EffFn.js b/tests/purs/passing/EffFn.js index b645b0527e..8360cbe7cd 100644 --- a/tests/purs/passing/EffFn.js +++ b/tests/purs/passing/EffFn.js @@ -1 +1 @@ -exports.add3 = function (a,b,c) { return a + b + c; }; \ No newline at end of file +export var add3 = function (a,b,c) { return a + b + c; }; diff --git a/tests/purs/passing/FFIDefaultCJSExport.js b/tests/purs/passing/FFIDefaultCJSExport.js new file mode 100644 index 0000000000..873a59a12b --- /dev/null +++ b/tests/purs/passing/FFIDefaultCJSExport.js @@ -0,0 +1 @@ +exports.default = "Done"; diff --git a/tests/purs/passing/FFIDefaultCJSExport.purs b/tests/purs/passing/FFIDefaultCJSExport.purs new file mode 100644 index 0000000000..1d084b6d8d --- /dev/null +++ b/tests/purs/passing/FFIDefaultCJSExport.purs @@ -0,0 +1,7 @@ +module Main where + +import Effect.Console (log) + +foreign import default :: String + +main = log default diff --git a/tests/purs/passing/FFIDefaultESExport.js b/tests/purs/passing/FFIDefaultESExport.js new file mode 100644 index 0000000000..ab294f31ea --- /dev/null +++ b/tests/purs/passing/FFIDefaultESExport.js @@ -0,0 +1,3 @@ +var message = "Done"; + +export { message as default }; diff --git a/tests/purs/passing/FFIDefaultESExport.purs b/tests/purs/passing/FFIDefaultESExport.purs new file mode 100644 index 0000000000..1d084b6d8d --- /dev/null +++ b/tests/purs/passing/FFIDefaultESExport.purs @@ -0,0 +1,7 @@ +module Main where + +import Effect.Console (log) + +foreign import default :: String + +main = log default diff --git a/tests/purs/passing/FunWithFunDeps.js b/tests/purs/passing/FunWithFunDeps.js index dea73d18fe..171f389176 100644 --- a/tests/purs/passing/FunWithFunDeps.js +++ b/tests/purs/passing/FunWithFunDeps.js @@ -1,15 +1,15 @@ //: forall e. FVect Z e -exports.fnil = []; +export var fnil = []; //: forall n e. e -> FVect n e -> FVect (S n) e -exports.fcons = function (hd) { +export var fcons = function (hd) { return function (tl) { return [hd].concat(tl); }; }; -exports.fappend = function (dict) { +export var fappend = function (dict) { return function (left) { return function (right) { return left.concat(right); @@ -17,7 +17,7 @@ exports.fappend = function (dict) { }; }; -exports.fflatten = function (dict) { +export var fflatten = function (dict) { return function (v) { var accRef = []; for (var indexRef = 0; indexRef < v.length; indexRef += 1) { @@ -27,6 +27,6 @@ exports.fflatten = function (dict) { }; }; -exports.ftoArray = function (vect) { +export var ftoArray = function (vect) { return vect; }; diff --git a/tests/purs/passing/PolyLabels.js b/tests/purs/passing/PolyLabels.js index b9900e4d3b..115375cd48 100644 --- a/tests/purs/passing/PolyLabels.js +++ b/tests/purs/passing/PolyLabels.js @@ -1,12 +1,10 @@ -"use strict"; - -exports.unsafeGet = function (s) { +export var unsafeGet = function (s) { return function (o) { return o[s]; }; }; -exports.unsafeSet = function(s) { +export var unsafeSet = function (s) { return function(a) { return function (o) { var o1 = {}; diff --git a/tests/purs/passing/ReExportsExported.js b/tests/purs/passing/ReExportsExported.js index b73154be1e..5ca086e78a 100644 --- a/tests/purs/passing/ReExportsExported.js +++ b/tests/purs/passing/ReExportsExported.js @@ -1,4 +1,2 @@ -"use strict"; - // Import `A.a` which was re-exported from `B` and then again from `C` -exports.a = require('../C').a; +export { a } from '../C/index.js'; diff --git a/tests/purs/passing/RowUnion.js b/tests/purs/passing/RowUnion.js index c002b18f57..4f037587a2 100644 --- a/tests/purs/passing/RowUnion.js +++ b/tests/purs/passing/RowUnion.js @@ -1,6 +1,4 @@ -"use strict"; - -exports.merge = function (dict) { +export var merge = function (dict) { return function (l) { return function (r) { var o = {}; diff --git a/tests/purs/warning/DeprecatedConstraintInForeignImport.js b/tests/purs/warning/DeprecatedConstraintInForeignImport.js index 3be8843e1f..8e629a2a03 100644 --- a/tests/purs/warning/DeprecatedConstraintInForeignImport.js +++ b/tests/purs/warning/DeprecatedConstraintInForeignImport.js @@ -1,4 +1,4 @@ -exports.show = function (showDict) { +export var show = function (showDict) { return function (a) { return showDict.show(a); }; diff --git a/tests/purs/warning/DeprecatedFFICommonJSModule.js b/tests/purs/warning/DeprecatedFFICommonJSModule.js new file mode 100644 index 0000000000..45e5121ffc --- /dev/null +++ b/tests/purs/warning/DeprecatedFFICommonJSModule.js @@ -0,0 +1,4 @@ +"use strict"; + +exports.yes = true; +exports.no = true; diff --git a/tests/purs/warning/DeprecatedFFICommonJSModule.out b/tests/purs/warning/DeprecatedFFICommonJSModule.out new file mode 100644 index 0000000000..38fb74714a --- /dev/null +++ b/tests/purs/warning/DeprecatedFFICommonJSModule.out @@ -0,0 +1,13 @@ +Warning found: +at tests/purs/warning/DeprecatedFFICommonJSModule.purs:2:1 - 5:29 (line 2, column 1 - line 5, column 29) + + A CommonJS foreign module implementation was provided for module Main: + + tests/purs/warning/DeprecatedFFICommonJSModule.js + + CommonJS foreign modules are deprecated and won't be supported in the future. + + +See https://github.com/purescript/documentation/blob/master/errors/DeprecatedFFICommonJSModule.md for more information, +or to contribute content related to this warning. + diff --git a/tests/purs/warning/DeprecatedFFICommonJSModule.purs b/tests/purs/warning/DeprecatedFFICommonJSModule.purs new file mode 100644 index 0000000000..b91bed426b --- /dev/null +++ b/tests/purs/warning/DeprecatedFFICommonJSModule.purs @@ -0,0 +1,5 @@ +-- @shouldWarnWith DeprecatedFFICommonJSModule +module Main where + +foreign import yes :: Boolean +foreign import no :: Boolean diff --git a/tests/purs/warning/DeprecatedFFIPrime.purs b/tests/purs/warning/DeprecatedFFIPrime.purs deleted file mode 100644 index 3c57a19d92..0000000000 --- a/tests/purs/warning/DeprecatedFFIPrime.purs +++ /dev/null @@ -1,10 +0,0 @@ --- @shouldWarnWith DeprecatedFFIPrime --- @shouldWarnWith DeprecatedFFIPrime --- @shouldWarnWith DeprecatedFFIPrime --- @shouldWarnWith DeprecatedFFIPrime -module Main where - -foreign import a' :: Number -foreign import b' :: Number -foreign import c' :: Number -foreign import d' :: Number diff --git a/tests/purs/warning/UnnecessaryFFIModule.js b/tests/purs/warning/UnnecessaryFFIModule.js index 346c8e9012..bd1835d69d 100644 --- a/tests/purs/warning/UnnecessaryFFIModule.js +++ b/tests/purs/warning/UnnecessaryFFIModule.js @@ -1 +1 @@ -exports.out = null; +export var out = null; diff --git a/tests/purs/warning/UnusedFFIImplementations.js b/tests/purs/warning/UnusedFFIImplementations.js index d50f2e60a8..78ab638547 100644 --- a/tests/purs/warning/UnusedFFIImplementations.js +++ b/tests/purs/warning/UnusedFFIImplementations.js @@ -1,2 +1,2 @@ -exports.yes = true; -exports.no = false; +export var yes = true; +export var no = false; diff --git a/tests/support/pscide/src/RebuildSpecWithForeign.js b/tests/support/pscide/src/RebuildSpecWithForeign.js index 8ea453ff71..577e8a5d5d 100644 --- a/tests/support/pscide/src/RebuildSpecWithForeign.js +++ b/tests/support/pscide/src/RebuildSpecWithForeign.js @@ -1 +1 @@ -exports.f = 5; +export var f = 5;