2-- Bundles compiled PureScript modules for the browser.
4-- This module takes as input the individual generated modules from 'Language.PureScript.Make' and
5-- performs dead code elimination, filters empty modules,
6-- and generates the final Javascript bundle.
7module Language.PureScript.Bundle
14 , getExportedIdentifiers
20import Control.Monad.Error.Class
22import Data.Generics (everything, everywhere, mkQ, mkT)
24import Data.List (nub, stripPrefix)
25import Data.Maybe (mapMaybe, catMaybes)
26import Data.Version (showVersion)
27import qualified Data.Set as S
29import Language.JavaScript.Parser
30import Language.JavaScript.Parser.AST
32import qualified Paths_purescript as Paths
34-- | The type of error messages. We separate generation and rendering of errors using a data
35-- type, in case we need to match on error types later.
37 = UnsupportedModulePath String
39 | UnableToParseModule String
41 | ErrorInModule ModuleIdentifier ErrorMessage
44-- | Modules are either "regular modules" (i.e. those generated by psc) or foreign modules.
48 deriving (Show, Read, Eq, Ord)
50showModuleType :: ModuleType -> String
51showModuleType Regular = "Regular"
52showModuleType Foreign = "Foreign"
54-- | A module is identified by its module name and its type.
55data ModuleIdentifier = ModuleIdentifier String ModuleType deriving (Show, Read, Eq, Ord)
57moduleName :: ModuleIdentifier -> String
58moduleName (ModuleIdentifier name _) = name
60-- | A piece of code is identified by its module and its name. These keys are used to label vertices
61-- in the dependency graph.
62type Key = (ModuleIdentifier, String)
64-- | An export is either a "regular export", which exports a name from the regular module we are in,
65-- or a reexport of a declaration in the corresponding foreign module.
67-- Regular exports are labelled, since they might re-export an operator with another name.
69 = RegularExport String
71 deriving (Show, Eq, Ord)
73-- | There are four types of module element we are interested in:
75-- 1) Require statements
76-- 2) Member declarations
80-- Each is labelled with the original AST node which generated it, so that we can dump it back
81-- into the output during codegen.
83 = Require JSStatement String (Either String ModuleIdentifier)
84 | Member JSStatement Bool String JSExpression [Key]
85 | ExportsList [(ExportType, String, JSExpression, [Key])]
89-- | A module is just a list of elements of the types listed above.
90data Module = Module ModuleIdentifier [ModuleElement] deriving (Show)
92-- | Prepare an error message for consumption by humans.
93printErrorMessage :: ErrorMessage -> [String]
94printErrorMessage (UnsupportedModulePath s) =
95 [ "A CommonJS module has an unsupported name (" ++ show s ++ ")."
96 , "The following file names are supported:"
97 , " 1) index.js (psc native modules)"
98 , " 2) foreign.js (psc foreign modules)"
100printErrorMessage InvalidTopLevel =
101 [ "Expected a list of source elements at the top level." ]
102printErrorMessage (UnableToParseModule err) =
103 [ "The module could not be parsed:"
106printErrorMessage UnsupportedExport =
107 [ "An export was unsupported. Exports can be defined in one of two ways: "
108 , " 1) exports.name = ..."
109 , " 2) exports = { ... }"
111printErrorMessage (ErrorInModule mid e) =
112 ("Error in module " ++ displayIdentifier mid ++ ":")
114 : map (" " ++) (printErrorMessage e)
116 displayIdentifier (ModuleIdentifier name ty) =
117 name ++ " (" ++ showModuleType ty ++ ")"
119-- | Calculate the ModuleIdentifier which a require(...) statement imports.
120checkImportPath :: String -> ModuleIdentifier -> S.Set String -> Either String ModuleIdentifier
121checkImportPath "./foreign" m _ =
122 Right (ModuleIdentifier (moduleName m) Foreign)
123checkImportPath name _ names
124 | Just name' <- stripPrefix "../" name
125 , name' `S.member` names = Right (ModuleIdentifier name' Regular)
126checkImportPath name _ _ = Left name
128-- | Compute the dependencies of all elements in a module, and add them to the tree.
130-- Members and exports can have dependencies. A dependency is of one of the following forms:
132-- 1) module.name or member["name"]
134-- where module was imported using
136-- var module = require("Module.Name");
140-- where name is the name of a member defined in the current module.
141withDeps :: Module -> Module
142withDeps (Module modulePath es) = Module modulePath (map expandDeps es)
144 -- | Collects all modules which are imported, so that we can identify dependencies of the first type.
145 imports :: [(String, ModuleIdentifier)]
146 imports = mapMaybe toImport es
148 toImport :: ModuleElement -> Maybe (String, ModuleIdentifier)
149 toImport (Require _ nm (Right mid)) = Just (nm, mid)
152 -- | Collects all member names in scope, so that we can identify dependencies of the second type.
153 boundNames :: [String]
154 boundNames = mapMaybe toBoundName es
156 toBoundName :: ModuleElement -> Maybe String
157 toBoundName (Member _ _ nm _ _) = Just nm
158 toBoundName _ = Nothing
160 -- | Calculate dependencies and add them to the current element.
161 expandDeps :: ModuleElement -> ModuleElement
162 expandDeps (Member n f nm decl _) = Member n f nm decl (nub $ dependencies modulePath decl)
163 expandDeps (ExportsList exps) = ExportsList (map expand exps)
165 expand (ty, nm, n1, _) = (ty, nm, n1, nub (dependencies modulePath n1))
166 expandDeps other = other
168 dependencies :: ModuleIdentifier -> JSExpression -> [(ModuleIdentifier, String)]
169 dependencies m = everything (++) (mkQ [] toReference)
171 toReference :: JSExpression -> [(ModuleIdentifier, String)]
172 toReference (JSMemberDot mn _ nm)
173 | JSIdentifier _ mn' <- mn
174 , JSIdentifier _ nm' <- nm
175 , Just mid <- lookup mn' imports
177 toReference (JSMemberSquare mn _ nm _)
178 | JSIdentifier _ mn' <- mn
179 , Just nm' <- fromStringLiteral nm
180 , Just mid <- lookup mn' imports
182 toReference (JSIdentifier _ nm)
183 | nm `elem` boundNames
187-- String literals include the quote chars
188fromStringLiteral :: JSExpression -> Maybe String
189fromStringLiteral (JSStringLiteral _ str) = Just $ trimStringQuotes str
190fromStringLiteral _ = Nothing
192trimStringQuotes :: String -> String
193trimStringQuotes str = reverse $ drop 1 $ reverse $ drop 1 $ str
195commaList :: JSCommaList a -> [a]
197commaList (JSLOne x) = [x]
198commaList (JSLCons l _ x) = commaList l ++ [x]
200trailingCommaList :: JSCommaTrailingList a -> [a]
201trailingCommaList (JSCTLComma l _) = commaList l
202trailingCommaList (JSCTLNone l) = commaList l
204-- | Attempt to create a Module from a Javascript AST.
206-- Each type of module element is matched using pattern guards, and everything else is bundled into the
208toModule :: forall m. (MonadError ErrorMessage m) => S.Set String -> ModuleIdentifier -> JSAST -> m Module
210 | JSAstProgram smts _ <- top = Module mid <$> traverse toModuleElement smts
211 | otherwise = err InvalidTopLevel
213 err = throwError . ErrorInModule mid
215 toModuleElement :: JSStatement -> m ModuleElement
217 | Just (importName, importPath) <- matchRequire mids mid stmt
218 = pure (Require stmt importName importPath)
220 | Just (exported, name, decl) <- matchMember stmt
221 = pure (Member stmt exported name decl [])
223 | Just props <- matchExportsAssignment stmt
224 = (ExportsList <$> traverse toExport (trailingCommaList props))
226 toExport :: JSObjectProperty -> m (ExportType, String, JSExpression, [Key])
227 toExport (JSPropertyNameandValue name _ [val]) =
228 (,,val,[]) <$> exportType val
229 <*> extractLabel' name
230 toExport _ = err UnsupportedExport
232 exportType :: JSExpression -> m ExportType
233 exportType (JSMemberDot f _ _)
234 | JSIdentifier _ "$foreign" <- f
235 = pure ForeignReexport
236 exportType (JSMemberSquare f _ _ _)
237 | JSIdentifier _ "$foreign" <- f
238 = pure ForeignReexport
239 exportType (JSIdentifier _ s) = pure (RegularExport s)
240 exportType _ = err UnsupportedExport
242 extractLabel' = maybe (err UnsupportedExport) pure . extractLabel
244 toModuleElement other = pure (Other other)
246-- Get a list of all the exported identifiers from a foreign module.
248-- TODO: what if we assign to exports.foo and then later assign to
249-- module.exports (presumably overwriting exports.foo)?
250getExportedIdentifiers :: (MonadError ErrorMessage m)
254getExportedIdentifiers mname top
255 | JSAstProgram stmts _ <- top = concat <$> traverse go stmts
256 | otherwise = err InvalidTopLevel
258 err = throwError . ErrorInModule (ModuleIdentifier mname Foreign)
261 | Just props <- matchExportsAssignment stmt
262 = traverse toIdent (trailingCommaList props)
263 | Just (True, name, _) <- matchMember stmt
268 toIdent (JSPropertyNameandValue name _ [_]) =
271 err UnsupportedExport
273 extractLabel' = maybe (err UnsupportedExport) pure . extractLabel
275-- Matches JS statements like this:
276-- var ModuleName = require("file");
277matchRequire :: S.Set String
280 -> Maybe (String, Either String ModuleIdentifier)
281matchRequire mids mid stmt
282 | JSVariable _ jsInit _ <- stmt
283 , [JSVarInitExpression var varInit] <- commaList jsInit
284 , JSIdentifier _ importName <- var
285 , JSVarInit _ jsInitEx <- varInit
286 , JSMemberExpression req _ argsE _ <- jsInitEx
287 , JSIdentifier _ "require" <- req
288 , [ Just importPath ] <- map fromStringLiteral (commaList argsE)
289 , importPath' <- checkImportPath importPath mid mids
290 = Just (importName, importPath')
294-- Matches JS member declarations.
295matchMember :: JSStatement -> Maybe (Bool, String, JSExpression)
298 | JSVariable _ jsInit _ <- stmt
299 , [JSVarInitExpression var varInit] <- commaList jsInit
300 , JSIdentifier _ name <- var
301 , JSVarInit _ decl <- varInit
302 = Just (False, name, decl)
303 -- exports.foo = expr; exports["foo"] = expr;
304 | JSAssignStatement e (JSAssign _) decl _ <- stmt
305 , Just name <- accessor e
306 = Just (True, name, decl)
310 accessor :: JSExpression -> Maybe String
311 accessor (JSMemberDot exports _ nm)
312 | JSIdentifier _ "exports" <- exports
313 , JSIdentifier _ name <- nm
315 accessor (JSMemberSquare exports _ nm _)
316 | JSIdentifier _ "exports" <- exports
317 , Just name <- fromStringLiteral nm
321-- Matches assignments to module.exports, like this:
322-- module.exports = { ... }
323matchExportsAssignment :: JSStatement -> Maybe JSObjectPropertyList
324matchExportsAssignment stmt
325 | JSAssignStatement e (JSAssign _) decl _ <- stmt
326 , JSMemberDot module' _ exports <- e
327 , JSIdentifier _ "module" <- module'
328 , JSIdentifier _ "exports" <- exports
329 , JSObjectLiteral _ props _ <- decl
334extractLabel :: JSPropertyName -> Maybe String
335extractLabel (JSPropertyString _ nm) = Just (trimStringQuotes nm)
336extractLabel (JSPropertyIdent _ nm) = Just nm
337extractLabel _ = Nothing
339-- | Eliminate unused code based on the specified entry point set.
340compile :: [Module] -> [ModuleIdentifier] -> [Module]
341compile modules [] = modules
342compile modules entryPoints = filteredModules
344 (graph, _, vertexFor) = graphFromEdges verts
347 verts :: [(ModuleElement, Key, [Key])]
349 Module mid els <- modules
350 concatMap (toVertices mid) els
352 -- | Create a set of vertices for a module element.
354 -- Some special cases worth commenting on:
356 -- 1) Regular exports which simply export their own name do not count as dependencies.
357 -- Regular exports which rename and reexport an operator do count, however.
359 -- 2) Require statements don't contribute towards dependencies, since they effectively get
360 -- inlined wherever they are used inside other module elements.
361 toVertices :: ModuleIdentifier -> ModuleElement -> [(ModuleElement, Key, [Key])]
362 toVertices p m@(Member _ _ nm _ deps) = [(m, (p, nm), deps)]
363 toVertices p m@(ExportsList exps) = mapMaybe toVertex exps
365 toVertex (ForeignReexport, nm, _, ks) = Just (m, (p, nm), ks)
366 toVertex (RegularExport nm, nm1, _, ks) | nm /= nm1 = Just (m, (p, nm1), ks)
370 -- | The set of vertices whose connected components we are interested in keeping.
371 entryPointVertices :: [Vertex]
372 entryPointVertices = catMaybes $ do
373 (_, k@(mid, _), _) <- verts
374 guard $ mid `elem` entryPoints
377 -- | The set of vertices reachable from an entry point
378 reachableSet :: S.Set Vertex
379 reachableSet = S.fromList (concatMap (reachable graph) entryPointVertices)
381 filteredModules :: [Module]
382 filteredModules = map filterUsed modules
384 filterUsed :: Module -> Module
385 filterUsed (Module mid ds) = Module mid (map filterExports (go ds))
387 go :: [ModuleElement] -> [ModuleElement]
390 | not (isDeclUsed d) = go rest
391 | otherwise = d : go rest
393 -- | Filter out the exports for members which aren't used.
394 filterExports :: ModuleElement -> ModuleElement
395 filterExports (ExportsList exps) = ExportsList (filter (\(_, nm, _, _) -> isKeyUsed (mid, nm)) exps)
396 filterExports me = me
398 isDeclUsed :: ModuleElement -> Bool
399 isDeclUsed (Member _ _ nm _ _) = isKeyUsed (mid, nm)
402 isKeyUsed :: Key -> Bool
404 | Just me <- vertexFor k = me `S.member` reachableSet
407-- | Topologically sort the module dependency graph, so that when we generate code, modules can be
408-- defined in the right order.
409sortModules :: [Module] -> [Module]
410sortModules modules = map (\v -> case nodeFor v of (n, _, _) -> n) (reverse (topSort graph))
412 (graph, nodeFor, _) = graphFromEdges $ do
413 m@(Module mid els) <- modules
414 return (m, mid, mapMaybe getKey els)
416 getKey :: ModuleElement -> Maybe ModuleIdentifier
417 getKey (Require _ _ (Right mi)) = Just mi
420-- | A module is empty if it contains no exported members (in other words,
421-- if the only things left after dead code elimination are module imports and
422-- "other" foreign code).
424-- If a module is empty, we don't want to generate code for it.
425isModuleEmpty :: Module -> Bool
426isModuleEmpty (Module _ els) = all isElementEmpty els
428 isElementEmpty :: ModuleElement -> Bool
429 isElementEmpty (ExportsList exps) = null exps
430 isElementEmpty Require{} = True
431 isElementEmpty (Other _) = True
432 isElementEmpty _ = False
434-- | Generate code for a set of modules, including a call to main().
436-- Modules get defined on the global PS object, as follows:
439-- (function(exports) {
441-- })(PS["Module.Name"] = PS["Module.Name"] || {});
443-- In particular, a module and its foreign imports share the same namespace inside PS.
444-- This saves us from having to generate unique names for a module and its foreign imports,
445-- and is safe since a module shares a namespace with its foreign imports in PureScript as well
446-- (so there is no way to have overlaps in code generated by psc).
447codeGen :: Maybe String -- ^ main module
448 -> String -- ^ namespace
449 -> [Module] -- ^ input modules
451codeGen optionsMainModule optionsNamespace ms = renderToString (JSAstProgram (prelude : concatMap moduleToJS ms ++ maybe [] runMain optionsMainModule) JSNoAnnot)
453 moduleToJS :: Module -> [JSStatement]
454 moduleToJS (Module mn ds) = wrap (moduleName mn) (indent (concatMap declToJS ds))
456 declToJS :: ModuleElement -> [JSStatement]
457 declToJS (Member n _ _ _ _) = [n]
458 declToJS (Other n) = [n]
459 declToJS (Require _ nm req) =
463 JSVarInitExpression (JSIdentifier sp nm)
464 (JSVarInit sp $ either require (moduleReference sp . moduleName) req )
465 ]) (JSSemi JSNoAnnot)
467 declToJS (ExportsList exps) = map toExport exps
470 toExport :: (ExportType, String, JSExpression, [Key]) -> JSStatement
471 toExport (_, nm, val, _) =
473 (JSMemberSquare (JSIdentifier lfsp "exports") JSNoAnnot
479 -- comma lists are reverse-consed
480 cList :: [a] -> JSCommaList a
483 cList l = go $ reverse l
486 go (h:t)= JSLCons (go t) JSNoAnnot h
487 go [] = error "Invalid case in comma-list"
489 indent :: [JSStatement] -> [JSStatement]
490 indent = everywhere (mkT squash)
492 squash JSNoAnnot = (JSAnnot (TokenPn 0 0 2) [])
493 squash (JSAnnot pos ann) = JSAnnot (keepCol pos) (map splat ann)
495 splat (CommentA pos s) = CommentA (keepCol pos) s
496 splat (WhiteSpace pos w) = WhiteSpace (keepCol pos) w
499 keepCol (TokenPn _ _ c) = TokenPn 0 0 (if c >= 0 then c + 2 else 2)
501 prelude :: JSStatement
502 prelude = JSVariable (JSAnnot tokenPosnEmpty [ CommentA tokenPosnEmpty $ "// Generated by psc-bundle " ++ showVersion Paths.version
503 , WhiteSpace tokenPosnEmpty "\n" ])
505 JSVarInitExpression (JSIdentifier sp optionsNamespace)
506 (JSVarInit sp (emptyObj sp))
507 ]) (JSSemi JSNoAnnot)
509 require :: String -> JSExpression
511 JSMemberExpression (JSIdentifier JSNoAnnot "require") JSNoAnnot (cList [ str mn ]) JSNoAnnot
513 moduleReference :: JSAnnot -> String -> JSExpression
514 moduleReference a mn =
515 JSMemberSquare (JSIdentifier a optionsNamespace) JSNoAnnot
518 str :: String -> JSExpression
519 str s = JSStringLiteral JSNoAnnot $ "\"" ++ s ++ "\""
522 emptyObj :: JSAnnot -> JSExpression
523 emptyObj a = JSObjectLiteral a (JSCTLNone JSLNil) JSNoAnnot
525 wrap :: String -> [JSStatement] -> [JSStatement]
528 JSMethodCall (JSExpressionParen lf (JSFunctionExpression JSNoAnnot JSIdentNone JSNoAnnot
529 (JSLOne (JSIdentName JSNoAnnot "exports")) JSNoAnnot
530 (JSBlock sp (lfHead ds) lf)) -- \n not quite in right place
533 (JSLOne (JSAssignExpression (moduleReference JSNoAnnot mn) (JSAssign sp)
534 (JSExpressionBinary (moduleReference sp mn) (JSBinOpOr sp) (emptyObj sp))))
539 lfHead (h:t) = (addAnn (WhiteSpace tokenPosnEmpty "\n ") h) : t
542 addAnn :: CommentAnnotation -> JSStatement -> JSStatement
543 addAnn a (JSExpressionStatement (JSStringLiteral ann s) _) =
544 (JSExpressionStatement (JSStringLiteral (appendAnn a ann) s) (JSSemi JSNoAnnot))
547 appendAnn a JSNoAnnot = (JSAnnot tokenPosnEmpty [a])
548 appendAnn a (JSAnnot _ anns) = JSAnnot tokenPosnEmpty (a:anns ++ [WhiteSpace tokenPosnEmpty " "])
550 runMain :: String -> [JSStatement]
553 (JSMemberDot (moduleReference lf mn) JSNoAnnot
554 (JSIdentifier JSNoAnnot "main"))
555 JSNoAnnot (cList []) JSNoAnnot (JSSemi JSNoAnnot)]
558 lf = JSAnnot tokenPosnEmpty [ WhiteSpace tokenPosnEmpty "\n" ]
562 lfsp = JSAnnot tokenPosnEmpty [ WhiteSpace tokenPosnEmpty "\n " ]
565 sp = JSAnnot tokenPosnEmpty [ WhiteSpace tokenPosnEmpty " " ]
567-- | The bundling function.
568-- This function performs dead code elimination, filters empty modules
569-- and generates and prints the final Javascript bundle.
570bundle :: (MonadError ErrorMessage m)
571 => [(ModuleIdentifier, String)] -- ^ The input modules. Each module should be javascript rendered from 'Language.PureScript.Make' or @psc@.
572 -> [ModuleIdentifier] -- ^ Entry points. These module identifiers are used as the roots for dead-code elimination
573 -> Maybe String -- ^ An optional main module.
574 -> String -- ^ The namespace (e.g. PS).
576bundle inputStrs entryPoints mainModule namespace = do
577 input <- forM inputStrs $ \(ident, js) -> do
578 ast <- either (throwError . ErrorInModule ident . UnableToParseModule) pure $ parse js (moduleName ident)
581 let mids = S.fromList (map (moduleName . fst) input)
583 modules <- traverse (fmap withDeps . uncurry (toModule mids)) input
585 let compiled = compile modules entryPoints
586 sorted = sortModules (filter (not . isModuleEmpty) compiled)
588 return (codeGen mainModule namespace sorted)
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list