purescript-0.15.5: src/Language/PureScript/Bundle.hs
-- |
-- Bundles compiled PureScript modules for the browser.
--
-- This module takes as input the individual generated modules from 'Language.PureScript.Make' and
-- performs dead code elimination, filters empty modules,
-- and generates the final JavaScript bundle.
module Language.PureScript.Bundle
( ModuleIdentifier(..)
, ModuleType(..)
, ErrorMessage(..)
, printErrorMessage
, ForeignModuleExports(..)
, getExportedIdentifiers
, ForeignModuleImports(..)
, getImportedModules
, Module
) where
import Prelude
import Control.Monad.Error.Class
import Data.Aeson ((.=))
import Data.Char (chr, digitToInt)
import Data.Foldable (fold)
import Data.Maybe (mapMaybe, maybeToList)
import qualified Data.Aeson as A
import qualified Data.Text.Lazy as LT
import Language.JavaScript.Parser
import Language.JavaScript.Parser.AST
import Language.JavaScript.Process.Minify
-- | The type of error messages. We separate generation and rendering of errors using a data
-- type, in case we need to match on error types later.
data ErrorMessage
= UnsupportedModulePath String
| InvalidTopLevel
| UnableToParseModule String
| UnsupportedImport
| UnsupportedExport
| ErrorInModule ModuleIdentifier ErrorMessage
| MissingEntryPoint String
| MissingMainModule String
deriving (Show)
-- | Modules are either "regular modules" (i.e. those generated by the PureScript compiler) or
-- foreign modules.
data ModuleType
= Regular
| Foreign
deriving (Show, Eq, Ord)
showModuleType :: ModuleType -> String
showModuleType Regular = "Regular"
showModuleType Foreign = "Foreign"
-- | A module is identified by its module name and its type.
data ModuleIdentifier = ModuleIdentifier String ModuleType deriving (Show, Eq, Ord)
instance A.ToJSON ModuleIdentifier where
toJSON (ModuleIdentifier name mt) =
A.object [ "name" .= name
, "type" .= show mt
]
data Visibility
= Public
| Internal
deriving (Show, Eq, Ord)
-- | A piece of code is identified by its module, its name, and whether it is an internal variable
-- or a public member. These keys are used to label vertices in the dependency graph.
type Key = (ModuleIdentifier, String, Visibility)
-- | An export is either a "regular export", which exports a name from the regular module we are in,
-- or a reexport of a declaration in the corresponding foreign module.
--
-- Regular exports are labelled, since they might re-export an operator with another name.
data ExportType
= RegularExport String
| ForeignReexport
deriving (Show, Eq, Ord)
-- | There are four types of module element we are interested in:
--
-- 1) Import declarations and require statements
-- 2) Member declarations
-- 3) Export lists
-- 4) Everything else
--
-- 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
= Import JSModuleItem String (Either String ModuleIdentifier)
| Member JSStatement Visibility String JSExpression [Key]
| ExportsList [(ExportType, String, JSExpression, [Key])]
| Other JSStatement
| Skip JSModuleItem
deriving (Show)
instance A.ToJSON ModuleElement where
toJSON = \case
(Import _ name (Right target)) ->
A.object [ "type" .= A.String "Import"
, "name" .= name
, "target" .= target
]
(Import _ name (Left targetPath)) ->
A.object [ "type" .= A.String "Import"
, "name" .= name
, "targetPath" .= targetPath
]
(Member _ visibility name _ dependsOn) ->
A.object [ "type" .= A.String "Member"
, "name" .= name
, "visibility" .= show visibility
, "dependsOn" .= map keyToJSON dependsOn
]
(ExportsList exports) ->
A.object [ "type" .= A.String "ExportsList"
, "exports" .= map exportToJSON exports
]
(Other stmt) ->
A.object [ "type" .= A.String "Other"
, "js" .= getFragment (JSAstStatement stmt JSNoAnnot)
]
(Skip item) ->
A.object [ "type" .= A.String "Skip"
, "js" .= getFragment (JSAstModule [item] JSNoAnnot)
]
where
keyToJSON (mid, member, visibility) =
A.object [ "module" .= mid
, "member" .= member
, "visibility" .= show visibility
]
exportToJSON (RegularExport sourceName, name, _, dependsOn) =
A.object [ "type" .= A.String "RegularExport"
, "name" .= name
, "sourceName" .= sourceName
, "dependsOn" .= map keyToJSON dependsOn
]
exportToJSON (ForeignReexport, name, _, dependsOn) =
A.object [ "type" .= A.String "ForeignReexport"
, "name" .= name
, "dependsOn" .= map keyToJSON dependsOn
]
getFragment = ellipsize . renderToText . minifyJS
where
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.
data Module = Module ModuleIdentifier (Maybe FilePath) [ModuleElement] deriving (Show)
instance A.ToJSON Module where
toJSON (Module moduleId filePath elements) =
A.object [ "moduleId" .= moduleId
, "filePath" .= filePath
, "elements" .= elements
]
-- | Prepare an error message for consumption by humans.
printErrorMessage :: ErrorMessage -> [String]
printErrorMessage (UnsupportedModulePath 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 ES foreign modules)"
, " 3) foreign.cjs (PureScript CommonJS foreign modules)"
]
printErrorMessage InvalidTopLevel =
[ "Expected a list of source elements at the top level." ]
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."
, "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 ++ ":")
: ""
: map (" " ++) (printErrorMessage e)
where
displayIdentifier (ModuleIdentifier name ty) =
name ++ " (" ++ showModuleType ty ++ ")"
printErrorMessage (MissingEntryPoint mName) =
[ "Could not find an ES module or CommonJS module for the specified entry point: " ++ mName
]
printErrorMessage (MissingMainModule mName) =
[ "Could not find an ES module or CommonJS module for the specified main module: " ++ mName
]
-- String literals include the quote chars
fromStringLiteral :: JSExpression -> Maybe String
fromStringLiteral (JSStringLiteral _ str) = Just $ strValue str
fromStringLiteral _ = Nothing
strValue :: String -> String
strValue str = go $ drop 1 str
where
go ('\\' : 'b' : xs) = '\b' : go xs
go ('\\' : 'f' : xs) = '\f' : go xs
go ('\\' : 'n' : xs) = '\n' : go xs
go ('\\' : 'r' : xs) = '\r' : go xs
go ('\\' : 't' : xs) = '\t' : go xs
go ('\\' : 'v' : xs) = '\v' : go xs
go ('\\' : '0' : xs) = '\0' : go xs
go ('\\' : 'x' : a : b : xs) = chr (a' + b') : go xs
where
a' = 16 * digitToInt a
b' = digitToInt b
go ('\\' : 'u' : a : b : c : d : xs) = chr (a' + b' + c' + d') : go xs
where
a' = 16 * 16 * 16 * digitToInt a
b' = 16 * 16 * digitToInt b
c' = 16 * digitToInt c
d' = digitToInt d
go ('\\' : x : xs) = x : go xs
go "\"" = ""
go "'" = ""
go (x : xs) = x : go xs
go "" = ""
commaList :: JSCommaList a -> [a]
commaList JSLNil = []
commaList (JSLOne x) = [x]
commaList (JSLCons l _ x) = commaList l ++ [x]
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
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.
--
-- TODO: what if we assign to exports.foo and then later assign to
-- module.exports (presumably overwriting exports.foo)?
getExportedIdentifiers :: forall m. (MonadError ErrorMessage m)
=> String
-> JSAST
-> m ForeignModuleExports
getExportedIdentifiers mname top
| JSAstModule jsModuleItems _ <- top = fold <$> traverse go jsModuleItems
| otherwise = err InvalidTopLevel
where
err :: ErrorMessage -> m a
err = throwError . ErrorInModule (ModuleIdentifier mname Foreign)
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 mempty
go (JSModuleExportDeclaration _ jsExportDeclaration) =
pure ForeignModuleExports{ cjsExports = [], esExports = exportDeclarationIdentifiers jsExportDeclaration }
go _ = pure mempty
toIdent (JSPropertyNameandValue name _ [_]) =
extractLabel' name
toIdent _ =
err UnsupportedExport
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 :: JSStatement -> Maybe (String, String)
matchRequire stmt
| JSVariable _ jsInit _ <- stmt
, [JSVarInitExpression var varInit] <- commaList jsInit
, JSIdentifier _ importName <- var
, JSVarInit _ jsInitEx <- varInit
, JSMemberExpression req _ argsE _ <- jsInitEx
, JSIdentifier _ "require" <- req
, [ Just importPath ] <- map fromStringLiteral (commaList argsE)
= 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
= pure (name, decl)
-- function foo(...args) { body }
| JSFunction a0 jsIdent a1 args a2 body _ <- stmt
, JSIdentName _ name <- jsIdent
= pure (name, JSFunctionExpression a0 jsIdent a1 args a2 body)
| otherwise
= Nothing
-- Matches exports.* or exports["*"] expressions and returns the property name.
exportsAccessor :: JSExpression -> Maybe String
exportsAccessor (JSMemberDot exports _ nm)
| JSIdentifier _ "exports" <- exports
, JSIdentifier _ name <- nm
= Just name
exportsAccessor (JSMemberSquare exports _ nm _)
| JSIdentifier _ "exports" <- exports
, Just name <- fromStringLiteral nm
= Just name
exportsAccessor _ = Nothing
-- Matches assignments to module.exports, like this:
-- module.exports = { ... }
matchExportsAssignment :: JSStatement -> Maybe JSObjectPropertyList
matchExportsAssignment stmt
| JSAssignStatement e (JSAssign _) decl _ <- stmt
, JSMemberDot module' _ exports <- e
, JSIdentifier _ "module" <- module'
, JSIdentifier _ "exports" <- exports
, JSObjectLiteral _ props _ <- decl
= Just props
| otherwise
= Nothing
extractLabel :: JSPropertyName -> Maybe String
extractLabel (JSPropertyString _ nm) = Just $ strValue nm
extractLabel (JSPropertyIdent _ nm) = Just nm
extractLabel _ = Nothing