1-----------------------------------------------------------------------------
4-- Copyright : (c) Phil Freeman 2015
7-- Maintainer : Phil Freeman <paf31@cantab.net>
8-- Stability : experimental
11-- | Bundles compiled PureScript modules for the browser.
13-- This module takes as input the individual generated modules from 'Language.PureScript.Make' and
14-- performs dead code elimination, filters empty modules,
15-- and generates the final Javascript bundle.
16-----------------------------------------------------------------------------
18{-# LANGUAGE PatternGuards #-}
19{-# LANGUAGE FlexibleContexts #-}
20{-# LANGUAGE ScopedTypeVariables #-}
21{-# LANGUAGE TupleSections #-}
23module Language.PureScript.Bundle (
25 , ModuleIdentifier(..)
35import Data.List (nub, stripPrefix)
36import Data.Maybe (mapMaybe, catMaybes, fromMaybe)
37import Data.Generics (everything, everywhere, mkQ, mkT)
39import Data.Version (showVersion)
41import qualified Data.Set as S
44import Control.Monad.Error.Class
45import Language.JavaScript.Parser
46import Language.JavaScript.Parser.AST
48import qualified Paths_purescript as Paths
50-- | The type of error messages. We separate generation and rendering of errors using a data
51-- type, in case we need to match on error types later.
53 = UnsupportedModulePath String
55 | UnableToParseModule String
57 | ErrorInModule ModuleIdentifier ErrorMessage
60-- | Modules are either "regular modules" (i.e. those generated by psc) or foreign modules.
64 deriving (Show, Read, Eq, Ord)
66showModuleType :: ModuleType -> String
67showModuleType Regular = "Regular"
68showModuleType Foreign = "Foreign"
70-- | A module is identified by its module name and its type.
71data ModuleIdentifier = ModuleIdentifier String ModuleType deriving (Show, Read, Eq, Ord)
73moduleName :: ModuleIdentifier -> String
74moduleName (ModuleIdentifier name _) = name
76-- | A piece of code is identified by its module and its name. These keys are used to label vertices
77-- in the dependency graph.
78type Key = (ModuleIdentifier, String)
80-- | An export is either a "regular export", which exports a name from the regular module we are in,
81-- or a reexport of a declaration in the corresponding foreign module.
83-- Regular exports are labelled, since they might re-export an operator with another name.
85 = RegularExport String
87 deriving (Show, Eq, Ord)
89-- | There are four types of module element we are interested in:
91-- 1) Require statements
92-- 2) Member declarations
96-- Each is labelled with the original AST node which generated it, so that we can dump it back
97-- into the output during codegen.
99 = Require JSStatement String (Either String ModuleIdentifier)
100 | Member JSStatement Bool String JSExpression [Key]
101 | ExportsList [(ExportType, String, JSExpression, [Key])]
105-- | A module is just a list of elements of the types listed above.
106data Module = Module ModuleIdentifier [ModuleElement] deriving (Show)
108-- | Prepare an error message for consumption by humans.
109printErrorMessage :: ErrorMessage -> [String]
110printErrorMessage (UnsupportedModulePath s) =
111 [ "A CommonJS module has an unsupported name (" ++ show s ++ ")."
112 , "The following file names are supported:"
113 , " 1) index.js (psc native modules)"
114 , " 2) foreign.js (psc foreign modules)"
116printErrorMessage InvalidTopLevel =
117 [ "Expected a list of source elements at the top level." ]
118printErrorMessage (UnableToParseModule err) =
119 [ "The module could not be parsed:"
122printErrorMessage UnsupportedExport =
123 [ "An export was unsupported. Exports can be defined in one of two ways: "
124 , " 1) exports.name = ..."
125 , " 2) exports = { ... }"
127printErrorMessage (ErrorInModule mid e) =
128 ("Error in module " ++ displayIdentifier mid ++ ":")
130 : map (" " ++) (printErrorMessage e)
132 displayIdentifier (ModuleIdentifier name ty) =
133 name ++ " (" ++ showModuleType ty ++ ")"
135-- | Calculate the ModuleIdentifier which a require(...) statement imports.
136checkImportPath :: Maybe FilePath -> String -> ModuleIdentifier -> S.Set String -> Either String ModuleIdentifier
137checkImportPath _ "./foreign" m _ =
138 Right (ModuleIdentifier (moduleName m) Foreign)
139checkImportPath requirePath name _ names
140 | Just name' <- stripPrefix (fromMaybe "../" requirePath) name
141 , name' `S.member` names = Right (ModuleIdentifier name' Regular)
142checkImportPath _ name _ _ = Left name
144-- | Compute the dependencies of all elements in a module, and add them to the tree.
146-- Members and exports can have dependencies. A dependency is of one of the following forms:
148-- 1) module.name or member["name"]
150-- where module was imported using
152-- var module = require("Module.Name");
156-- where name is the name of a member defined in the current module.
157withDeps :: Module -> Module
158withDeps (Module modulePath es) = Module modulePath (map expandDeps es)
160 -- | Collects all modules which are imported, so that we can identify dependencies of the first type.
161 imports :: [(String, ModuleIdentifier)]
162 imports = mapMaybe toImport es
164 toImport :: ModuleElement -> Maybe (String, ModuleIdentifier)
165 toImport (Require _ nm (Right mid)) = Just (nm, mid)
168 -- | Collects all member names in scope, so that we can identify dependencies of the second type.
169 boundNames :: [String]
170 boundNames = mapMaybe toBoundName es
172 toBoundName :: ModuleElement -> Maybe String
173 toBoundName (Member _ _ nm _ _) = Just nm
174 toBoundName _ = Nothing
176 -- | Calculate dependencies and add them to the current element.
177 expandDeps :: ModuleElement -> ModuleElement
178 expandDeps (Member n f nm decl _) = Member n f nm decl (nub $ dependencies modulePath decl)
179 expandDeps (ExportsList exps) = ExportsList (map expand exps)
181 expand (ty, nm, n1, _) = (ty, nm, n1, nub (dependencies modulePath n1))
182 expandDeps other = other
184 dependencies :: ModuleIdentifier -> JSExpression -> [(ModuleIdentifier, String)]
185 dependencies m = everything (++) (mkQ [] toReference)
187 toReference :: JSExpression -> [(ModuleIdentifier, String)]
188 toReference (JSMemberDot mn _ nm)
189 | JSIdentifier _ mn' <- mn
190 , JSIdentifier _ nm' <- nm
191 , Just mid <- lookup mn' imports
193 toReference (JSMemberSquare mn _ nm _)
194 | JSIdentifier _ mn' <- mn
195 , Just nm' <- fromStringLiteral nm
196 , Just mid <- lookup mn' imports
198 toReference (JSIdentifier _ nm)
199 | nm `elem` boundNames
203-- String literals include the quote chars
204fromStringLiteral :: JSExpression -> Maybe String
205fromStringLiteral (JSStringLiteral _ str) = Just $ trimStringQuotes str
206fromStringLiteral _ = Nothing
208trimStringQuotes :: String -> String
209trimStringQuotes str = reverse $ drop 1 $ reverse $ drop 1 $ str
211commaList :: JSCommaList a -> [a]
213commaList (JSLOne x) = [x]
214commaList (JSLCons l _ x) = commaList l ++ [x]
216trailingCommaList :: JSCommaTrailingList a -> [a]
217trailingCommaList (JSCTLComma l _) = commaList l
218trailingCommaList (JSCTLNone l) = commaList l
220-- | Attempt to create a Module from a Javascript AST.
222-- Each type of module element is matched using pattern guards, and everything else is bundled into the
224toModule :: forall m. (MonadError ErrorMessage m) => Maybe FilePath -> S.Set String -> ModuleIdentifier -> JSAST -> m Module
225toModule requirePath mids mid top
226 | JSAstProgram smts _ <- top = Module mid <$> traverse toModuleElement smts
227 | otherwise = err InvalidTopLevel
229 err = throwError . ErrorInModule mid
231 toModuleElement :: JSStatement -> m ModuleElement
232 -- var ModuleName = require("file");
234 | JSVariable _ jsInit _ <- stmt
235 , [JSVarInitExpression var varInit] <- commaList jsInit
236 , JSIdentifier _ importName <- var
237 , JSVarInit _ jsInitEx <- varInit
238 , JSMemberExpression req _ argsE _ <- jsInitEx
239 , JSIdentifier _ "require" <- req
240 , [ Just importPath ] <- map fromStringLiteral (commaList argsE)
241 , importPath' <- checkImportPath requirePath importPath mid mids
242 = pure (Require stmt importName importPath')
245 | JSVariable _ jsInit _ <- stmt
246 , [JSVarInitExpression var varInit] <- commaList jsInit
247 , JSIdentifier _ name <- var
248 , JSVarInit _ decl <- varInit
249 = pure (Member stmt False name decl [])
250 -- exports.foo = expr; exports["foo"] = expr;
252 | JSAssignStatement e (JSAssign _) decl _ <- stmt
253 , Just name <- accessor e
254 = pure (Member stmt True name decl [])
256 accessor :: JSExpression -> Maybe String
257 accessor (JSMemberDot exports _ nm)
258 | JSIdentifier _ "exports" <- exports
259 , JSIdentifier _ name <- nm
261 accessor (JSMemberSquare exports _ nm _)
262 | JSIdentifier _ "exports" <- exports
263 , Just name <- fromStringLiteral nm
266 -- module.exports = { ... }
268 | JSAssignStatement e (JSAssign _) decl _ <- stmt
269 , JSMemberDot module' _ exports <- e
270 , JSIdentifier _ "module" <- module'
271 , JSIdentifier _ "exports" <- exports
272 , JSObjectLiteral _ props _ <- decl
273 = (ExportsList <$> traverse toExport (trailingCommaList props))
275 toExport :: JSObjectProperty -> m (ExportType, String, JSExpression, [Key])
276 toExport (JSPropertyNameandValue name _ [val]) =
277 (,,val,[]) <$> exportType val
278 <*> extractLabel name
279 toExport _ = err UnsupportedExport
281 exportType :: JSExpression -> m ExportType
282 exportType (JSMemberDot f _ _)
283 | JSIdentifier _ "$foreign" <- f
284 = pure ForeignReexport
285 exportType (JSMemberSquare f _ _ _)
286 | JSIdentifier _ "$foreign" <- f
287 = pure ForeignReexport
288 exportType (JSIdentifier _ s) = pure (RegularExport s)
289 exportType _ = err UnsupportedExport
291 extractLabel :: JSPropertyName -> m String
292 extractLabel (JSPropertyString _ nm) = pure (trimStringQuotes nm)
293 extractLabel (JSPropertyIdent _ nm) = pure nm
294 extractLabel _ = err UnsupportedExport
296 toModuleElement other = pure (Other other)
298-- | Eliminate unused code based on the specified entry point set.
299compile :: [Module] -> [ModuleIdentifier] -> [Module]
300compile modules [] = modules
301compile modules entryPoints = filteredModules
303 (graph, _, vertexFor) = graphFromEdges verts
306 verts :: [(ModuleElement, Key, [Key])]
308 Module mid els <- modules
309 concatMap (toVertices mid) els
311 -- | Create a set of vertices for a module element.
313 -- Some special cases worth commenting on:
315 -- 1) Regular exports which simply export their own name do not count as dependencies.
316 -- Regular exports which rename and reexport an operator do count, however.
318 -- 2) Require statements don't contribute towards dependencies, since they effectively get
319 -- inlined wherever they are used inside other module elements.
320 toVertices :: ModuleIdentifier -> ModuleElement -> [(ModuleElement, Key, [Key])]
321 toVertices p m@(Member _ _ nm _ deps) = [(m, (p, nm), deps)]
322 toVertices p m@(ExportsList exps) = mapMaybe toVertex exps
324 toVertex (ForeignReexport, nm, _, ks) = Just (m, (p, nm), ks)
325 toVertex (RegularExport nm, nm1, _, ks) | nm /= nm1 = Just (m, (p, nm1), ks)
329 -- | The set of vertices whose connected components we are interested in keeping.
330 entryPointVertices :: [Vertex]
331 entryPointVertices = catMaybes $ do
332 (_, k@(mid, _), _) <- verts
333 guard $ mid `elem` entryPoints
336 -- | The set of vertices reachable from an entry point
337 reachableSet :: S.Set Vertex
338 reachableSet = S.fromList (concatMap (reachable graph) entryPointVertices)
340 filteredModules :: [Module]
341 filteredModules = map filterUsed modules
343 filterUsed :: Module -> Module
344 filterUsed (Module mid ds) = Module mid (map filterExports (go ds))
346 go :: [ModuleElement] -> [ModuleElement]
349 | not (isDeclUsed d) = go rest
350 | otherwise = d : go rest
352 -- | Filter out the exports for members which aren't used.
353 filterExports :: ModuleElement -> ModuleElement
354 filterExports (ExportsList exps) = ExportsList (filter (\(_, nm, _, _) -> isKeyUsed (mid, nm)) exps)
355 filterExports me = me
357 isDeclUsed :: ModuleElement -> Bool
358 isDeclUsed (Member _ _ nm _ _) = isKeyUsed (mid, nm)
361 isKeyUsed :: Key -> Bool
363 | Just me <- vertexFor k = me `S.member` reachableSet
366-- | Topologically sort the module dependency graph, so that when we generate code, modules can be
367-- defined in the right order.
368sortModules :: [Module] -> [Module]
369sortModules modules = map (\v -> case nodeFor v of (n, _, _) -> n) (reverse (topSort graph))
371 (graph, nodeFor, _) = graphFromEdges $ do
372 m@(Module mid els) <- modules
373 return (m, mid, mapMaybe getKey els)
375 getKey :: ModuleElement -> Maybe ModuleIdentifier
376 getKey (Require _ _ (Right mi)) = Just mi
379-- | A module is empty if it contains no exported members (in other words,
380-- if the only things left after dead code elimination are module imports and
381-- "other" foreign code).
383-- If a module is empty, we don't want to generate code for it.
384isModuleEmpty :: Module -> Bool
385isModuleEmpty (Module _ els) = all isElementEmpty els
387 isElementEmpty :: ModuleElement -> Bool
388 isElementEmpty (ExportsList exps) = null exps
389 isElementEmpty Require{} = True
390 isElementEmpty (Other _) = True
391 isElementEmpty _ = False
393-- | Generate code for a set of modules, including a call to main().
395-- Modules get defined on the global PS object, as follows:
398-- (function(exports) {
400-- })(PS["Module.Name"] = PS["Module.Name"] || {});
402-- In particular, a module and its foreign imports share the same namespace inside PS.
403-- This saves us from having to generate unique names for a module and its foreign imports,
404-- and is safe since a module shares a namespace with its foreign imports in PureScript as well
405-- (so there is no way to have overlaps in code generated by psc).
406codeGen :: Maybe String -- ^ main module
407 -> String -- ^ namespace
408 -> [Module] -- ^ input modules
410codeGen optionsMainModule optionsNamespace ms = renderToString (JSAstProgram (prelude : concatMap moduleToJS ms ++ maybe [] runMain optionsMainModule) JSNoAnnot)
412 moduleToJS :: Module -> [JSStatement]
413 moduleToJS (Module mn ds) = wrap (moduleName mn) (indent (concatMap declToJS ds))
415 declToJS :: ModuleElement -> [JSStatement]
416 declToJS (Member n _ _ _ _) = [n]
417 declToJS (Other n) = [n]
418 declToJS (Require _ nm req) =
422 JSVarInitExpression (JSIdentifier sp nm)
423 (JSVarInit sp $ either require (moduleReference sp . moduleName) req )
424 ]) (JSSemi JSNoAnnot)
426 declToJS (ExportsList exps) = map toExport exps
429 toExport :: (ExportType, String, JSExpression, [Key]) -> JSStatement
430 toExport (_, nm, val, _) =
432 (JSMemberSquare (JSIdentifier lfsp "exports") JSNoAnnot
438 -- comma lists are reverse-consed
439 cList :: [a] -> JSCommaList a
442 cList l = go $ reverse l
445 go (h:t)= JSLCons (go t) JSNoAnnot h
446 go [] = error "Invalid case in comma-list"
448 indent :: [JSStatement] -> [JSStatement]
449 indent = everywhere (mkT squash)
451 squash JSNoAnnot = (JSAnnot (TokenPn 0 0 2) [])
452 squash (JSAnnot pos ann) = JSAnnot (keepCol pos) (map splat ann)
454 splat (CommentA pos s) = CommentA (keepCol pos) s
455 splat (WhiteSpace pos w) = WhiteSpace (keepCol pos) w
458 keepCol (TokenPn _ _ c) = TokenPn 0 0 (if c >= 0 then c + 2 else 2)
460 prelude :: JSStatement
461 prelude = JSVariable (JSAnnot tokenPosnEmpty [ CommentA tokenPosnEmpty $ "// Generated by psc-bundle " ++ showVersion Paths.version
462 , WhiteSpace tokenPosnEmpty "\n" ])
464 JSVarInitExpression (JSIdentifier sp optionsNamespace)
465 (JSVarInit sp (emptyObj sp))
466 ]) (JSSemi JSNoAnnot)
468 require :: String -> JSExpression
470 JSMemberExpression (JSIdentifier JSNoAnnot "require") JSNoAnnot (cList [ str mn ]) JSNoAnnot
472 moduleReference :: JSAnnot -> String -> JSExpression
473 moduleReference a mn =
474 JSMemberSquare (JSIdentifier a optionsNamespace) JSNoAnnot
477 str :: String -> JSExpression
478 str s = JSStringLiteral JSNoAnnot $ "\"" ++ s ++ "\""
481 emptyObj :: JSAnnot -> JSExpression
482 emptyObj a = JSObjectLiteral a (JSCTLNone JSLNil) JSNoAnnot
484 wrap :: String -> [JSStatement] -> [JSStatement]
487 JSMethodCall (JSExpressionParen lf (JSFunctionExpression JSNoAnnot JSIdentNone JSNoAnnot
488 (JSLOne (JSIdentName JSNoAnnot "exports")) JSNoAnnot
489 (JSBlock sp (lfHead ds) lf)) -- \n not quite in right place
492 (JSLOne (JSAssignExpression (moduleReference JSNoAnnot mn) (JSAssign sp)
493 (JSExpressionBinary (moduleReference sp mn) (JSBinOpOr sp) (emptyObj sp))))
498 lfHead (h:t) = (addAnn (WhiteSpace tokenPosnEmpty "\n ") h) : t
501 addAnn :: CommentAnnotation -> JSStatement -> JSStatement
502 addAnn a (JSExpressionStatement (JSStringLiteral ann s) _) =
503 (JSExpressionStatement (JSStringLiteral (appendAnn a ann) s) (JSSemi JSNoAnnot))
506 appendAnn a JSNoAnnot = (JSAnnot tokenPosnEmpty [a])
507 appendAnn a (JSAnnot _ anns) = JSAnnot tokenPosnEmpty (a:anns ++ [WhiteSpace tokenPosnEmpty " "])
509 runMain :: String -> [JSStatement]
512 (JSMemberDot (moduleReference lf mn) JSNoAnnot
513 (JSIdentifier JSNoAnnot "main"))
514 JSNoAnnot (cList []) JSNoAnnot (JSSemi JSNoAnnot)]
517 lf = JSAnnot tokenPosnEmpty [ WhiteSpace tokenPosnEmpty "\n" ]
521 lfsp = JSAnnot tokenPosnEmpty [ WhiteSpace tokenPosnEmpty "\n " ]
524 sp = JSAnnot tokenPosnEmpty [ WhiteSpace tokenPosnEmpty " " ]
526-- | The bundling function.
527-- This function performs dead code elimination, filters empty modules
528-- and generates and prints the final Javascript bundle.
529bundle :: (MonadError ErrorMessage m)
530 => [(ModuleIdentifier, String)] -- ^ The input modules. Each module should be javascript rendered from 'Language.PureScript.Make' or @psc@.
531 -> [ModuleIdentifier] -- ^ Entry points. These module identifiers are used as the roots for dead-code elimination
532 -> Maybe String -- ^ An optional main module.
533 -> String -- ^ The namespace (e.g. PS).
534 -> Maybe FilePath -- ^ The require path prefix
536bundle inputStrs entryPoints mainModule namespace requirePath = do
537 input <- forM inputStrs $ \(ident, js) -> do
538 ast <- either (throwError . ErrorInModule ident . UnableToParseModule) pure $ parse js (moduleName ident)
541 let mids = S.fromList (map (moduleName . fst) input)
543 modules <- traverse (fmap withDeps . uncurry (toModule requirePath mids)) input
545 let compiled = compile modules entryPoints
546 sorted = sortModules (filter (not . isModuleEmpty) compiled)
548 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