git.iko.soy/rowtype-yoga/purescript-native
on d0ebbcc help sign in
538 lines 22 kB history raw
1-----------------------------------------------------------------------------
2--
3-- Module : psc-bundle
4-- Copyright : (c) Phil Freeman 2015
5-- License : MIT
6--
7-- Maintainer : Phil Freeman <paf31@cantab.net>
8-- Stability : experimental
9-- Portability :
10--
11-- | Bundles compiled PureScript modules for the browser.
12--
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 #-}
22{-# LANGUAGE RecordWildCards #-}
24module Language.PureScript.Bundle (
25 bundle
26 , ModuleIdentifier(..)
27 , moduleName
28 , ModuleType(..)
29 , ErrorMessage(..)
30 , printErrorMessage
31) where
33import Data.List (nub)
34import Data.Maybe (mapMaybe, catMaybes)
35import Data.Generics (everything, everywhere, mkQ, mkT)
36import Data.Graph
37import Data.Version (showVersion)
39import qualified Data.Set as S
41import Control.Applicative
42import Control.Monad
43import Control.Monad.Error.Class
44import Language.JavaScript.Parser
46import qualified Paths_purescript as Paths
48-- | The type of error messages. We separate generation and rendering of errors using a data
49-- type, in case we need to match on error types later.
50data ErrorMessage
51 = UnsupportedModulePath String
52 | InvalidTopLevel
53 | UnableToParseModule
54 | UnsupportedExport
55 | ErrorInFile FilePath ErrorMessage
56 deriving Show
58-- | Modules are either "regular modules" (i.e. those generated by psc-make) or foreign modules.
59data ModuleType
60 = Regular
61 | Foreign
62 deriving (Show, Eq, Ord)
64-- | A module is identified by its module name and its type.
65data ModuleIdentifier = ModuleIdentifier String ModuleType deriving (Show, Eq, Ord)
67moduleName :: ModuleIdentifier -> String
68moduleName (ModuleIdentifier name _) = name
70-- | A piece of code is identified by its module and its name. These keys are used to label vertices
71-- in the dependency graph.
72type Key = (ModuleIdentifier, String)
74-- | An export is either a "regular export", which exports a name from the regular module we are in,
75-- or a reexport of a declaration in the corresponding foreign module.
76--
77-- Regular exports are labelled, since they might re-export an operator with another name.
78data ExportType
79 = RegularExport String
80 | ForeignReexport
81 deriving (Show, Eq, Ord)
83-- | There are four types of module element we are interested in:
84--
85-- 1) Require statements
86-- 2) Member declarations
87-- 3) Export lists
88-- 4) Everything else
89--
90-- Each is labelled with the original AST node which generated it, so that we can dump it back
91-- into the output during codegen.
92data ModuleElement
93 = Require JSNode String ModuleIdentifier
94 | Member JSNode Bool String [JSNode] [Key]
95 | ExportsList [(ExportType, String, JSNode, [Key])]
96 | Other JSNode
97 deriving Show
99-- | A module is just a list of elements of the types listed above.
100data Module = Module ModuleIdentifier [ModuleElement] deriving Show
102-- | Prepare an error message for consumption by humans.
103printErrorMessage :: ErrorMessage -> [String]
104printErrorMessage (UnsupportedModulePath s) =
105 [ "A CommonJS module has an unsupported name (" ++ show s ++ ")."
106 , "The following file names are supported:"
107 , " 1) index.js (psc-make native modules)"
108 , " 2) foreign.js (psc-make foreign modules)"
109 ]
110printErrorMessage InvalidTopLevel =
111 [ "Expected a list of source elements at the top level." ]
112printErrorMessage UnableToParseModule =
113 [ "The module could not be parsed." ]
114printErrorMessage UnsupportedExport =
115 [ "An export was unsupported. Exports can be defined in one of two ways: "
116 , " 1) exports.name = ..."
117 , " 2) exports = { ... }"
118 ]
119printErrorMessage (ErrorInFile filename e) =
120 ("Error in file " ++ show filename ++ ":")
121 : ""
122 : map (" " ++) (printErrorMessage e)
124-- | Unpack the node inside a JSNode. This is useful when pattern matching.
125node :: JSNode -> Node
126node (NN n) = n
127node (NT n _ _) = n
129-- | Calculate the ModuleIdentifier which a require(...) statement imports.
130checkImportPath :: String -> ModuleIdentifier -> S.Set String -> Maybe ModuleIdentifier
131checkImportPath "./foreign" m _ =
132 Just (ModuleIdentifier (moduleName m) Foreign)
133checkImportPath name _ names
134 | name `S.member` names = Just (ModuleIdentifier name Regular)
135checkImportPath _ _ _ = Nothing
137-- | Compute the dependencies of all elements in a module, and add them to the tree.
138--
139-- Members and exports can have dependencies. A dependency is of one of the following forms:
140--
141-- 1) module.name or member["name"]
142--
143-- where module was imported using
144--
145-- var module = require("Module.Name");
146--
147-- 2) name
148--
149-- where name is the name of a member defined in the current module.
150withDeps :: Module -> Module
151withDeps (Module modulePath es) = Module modulePath (map expandDeps es)
152 where
153 -- | Collects all modules which are imported, so that we can identify dependencies of the first type.
154 imports :: [(String, ModuleIdentifier)]
155 imports = mapMaybe toImport es
156 where
157 toImport :: ModuleElement -> Maybe (String, ModuleIdentifier)
158 toImport (Require _ nm mid) = Just (nm, mid)
159 toImport _ = Nothing
161 -- | Collects all member names in scope, so that we can identify dependencies of the second type.
162 boundNames :: [String]
163 boundNames = mapMaybe toBoundName es
164 where
165 toBoundName :: ModuleElement -> Maybe String
166 toBoundName (Member _ _ nm _ _) = Just nm
167 toBoundName _ = Nothing
169 -- | Calculate dependencies and add them to the current element.
170 expandDeps :: ModuleElement -> ModuleElement
171 expandDeps (Member n f nm decl _) = Member n f nm decl (nub (concatMap (dependencies modulePath) decl))
172 expandDeps (ExportsList exps) = ExportsList (map expand exps)
173 where
174 expand (ty, nm, n1, _) = (ty, nm, n1, nub (dependencies modulePath n1))
175 expandDeps other = other
177 dependencies :: ModuleIdentifier -> JSNode -> [(ModuleIdentifier, String)]
178 dependencies m = everything (++) (mkQ [] toReference)
179 where
180 toReference :: Node -> [(ModuleIdentifier, String)]
181 toReference (JSMemberDot [ mn ] _ nm)
182 | JSIdentifier mn' <- node mn
183 , JSIdentifier nm' <- node nm
184 , Just mid <- lookup mn' imports
185 = [(mid, nm')]
186 toReference (JSMemberSquare [ mn ] _ nm _)
187 | JSIdentifier mn' <- node mn
188 , JSExpression [ s ] <- node nm
189 , JSStringLiteral _ nm' <- node s
190 , Just mid <- lookup mn' imports
191 = [(mid, nm')]
192 toReference (JSIdentifier nm)
193 | nm `elem` boundNames
194 = [(m, nm)]
195 toReference _ = []
197-- | Attempt to create a Module from a Javascript AST.
198--
199-- Each type of module element is matched using pattern guards, and everything else is bundled into the
200-- Other constructor.
201toModule :: forall m. (Applicative m, MonadError ErrorMessage m) => S.Set String -> ModuleIdentifier -> JSNode -> m Module
202toModule mids mid top
203 | JSSourceElementsTop ns <- node top = Module mid <$> mapM toModuleElement ns
204 | otherwise = throwError InvalidTopLevel
205 where
206 toModuleElement :: JSNode -> m ModuleElement
207 toModuleElement n
208 | JSVariables var [ varIntro ] _ <- node n
209 , JSLiteral "var" <- node var
210 , JSVarDecl impN [ eq, req, impP ] <- node varIntro
211 , JSIdentifier importName <- node impN
212 , JSLiteral "=" <- node eq
213 , JSIdentifier "require" <- node req
214 , JSArguments _ [ impS ] _ <- node impP
215 , JSStringLiteral _ importPath <- node impS
216 , Just importPath' <- checkImportPath importPath mid mids
217 = pure (Require n importName importPath')
218 toModuleElement n
219 | JSVariables var [ varIntro ] _ <- node n
220 , JSLiteral "var" <- node var
221 , JSVarDecl declN (eq : decl) <- node varIntro
222 , JSIdentifier name <- node declN
223 , JSLiteral "=" <- node eq
224 = pure (Member n False name decl [])
225 toModuleElement n
226 | JSExpression (e : op : decl) <- node n
227 , Just name <- accessor (node e)
228 , JSOperator eq <- node op
229 , JSLiteral "=" <- node eq
230 = pure (Member n True name decl [])
231 where
232 accessor :: Node -> Maybe String
233 accessor (JSMemberDot [ exports ] _ nm)
234 | JSIdentifier "exports" <- node exports
235 , JSIdentifier name <- node nm
236 = Just name
237 accessor (JSMemberSquare [ exports ] _ nm _)
238 | JSIdentifier "exports" <- node exports
239 , JSExpression [e] <- node nm
240 , JSStringLiteral _ name <- node e
241 = Just name
242 accessor _ = Nothing
243 toModuleElement n
244 | JSExpression (mnExp : op : obj: _) <- node n
245 , JSMemberDot [ mn ] _ e <- node mnExp
246 , JSIdentifier "module" <- node mn
247 , JSIdentifier "exports" <- node e
248 , JSOperator eq <- node op
249 , JSLiteral "=" <- node eq
250 , JSObjectLiteral _ props _ <- node obj
251 = ExportsList <$> mapM toExport (filter (not . isSeparator) (map node props))
252 where
253 toExport :: Node -> m (ExportType, String, JSNode, [Key])
254 toExport (JSPropertyNameandValue name _ [val] ) =
255 (,,val,[]) <$> exportType (node val)
256 <*> extractLabel (node name)
257 toExport _ = throwError UnsupportedExport
259 exportType :: Node -> m ExportType
260 exportType (JSMemberDot [f] _ _)
261 | JSIdentifier "$foreign" <- node f
262 = pure ForeignReexport
263 exportType (JSMemberSquare [f] _ _ _)
264 | JSIdentifier "$foreign" <- node f
265 = pure ForeignReexport
266 exportType (JSIdentifier s) = pure (RegularExport s)
267 exportType _ = throwError UnsupportedExport
269 extractLabel :: Node -> m String
270 extractLabel (JSStringLiteral _ nm) = pure nm
271 extractLabel (JSIdentifier nm) = pure nm
272 extractLabel _ = throwError UnsupportedExport
274 isSeparator :: Node -> Bool
275 isSeparator (JSLiteral ",") = True
276 isSeparator _ = False
277 toModuleElement other = pure (Other other)
279-- | Eliminate unused code based on the specified entry point set.
280compile :: [Module] -> [ModuleIdentifier] -> [Module]
281compile modules [] = modules
282compile modules entryPoints = filteredModules
283 where
284 (graph, _, vertexFor) = graphFromEdges verts
286 -- | The vertex set
287 verts :: [(ModuleElement, Key, [Key])]
288 verts = do
289 Module mid els <- modules
290 concatMap (toVertices mid) els
291 where
292 -- | Create a set of vertices for a module element.
293 --
294 -- Some special cases worth commenting on:
295 --
296 -- 1) Regular exports which simply export their own name do not count as dependencies.
297 -- Regular exports which rename and reexport an operator do count, however.
298 --
299 -- 2) Require statements don't contribute towards dependencies, since they effectively get
300 -- inlined wherever they are used inside other module elements.
301 toVertices :: ModuleIdentifier -> ModuleElement -> [(ModuleElement, Key, [Key])]
302 toVertices p m@(Member _ _ nm _ deps) = [(m, (p, nm), deps)]
303 toVertices p m@(ExportsList exps) = mapMaybe toVertex exps
304 where
305 toVertex (ForeignReexport, nm, _, ks) = Just (m, (p, nm), ks)
306 toVertex (RegularExport nm, nm1, _, ks) | nm /= nm1 = Just (m, (p, nm1), ks)
307 toVertex _ = Nothing
308 toVertices _ _ = []
310 -- | The set of vertices whose connected components we are interested in keeping.
311 entryPointVertices :: [Vertex]
312 entryPointVertices = catMaybes $ do
313 (_, k@(mid, _), _) <- verts
314 guard $ mid `elem` entryPoints
315 return (vertexFor k)
317 -- | The set of vertices reachable from an entry point
318 reachableSet :: S.Set Vertex
319 reachableSet = S.fromList (concatMap (reachable graph) entryPointVertices)
321 filteredModules :: [Module]
322 filteredModules = map filterUsed modules
323 where
324 filterUsed :: Module -> Module
325 filterUsed (Module mid ds) = Module mid (map filterExports (go ds))
326 where
327 go :: [ModuleElement] -> [ModuleElement]
328 go [] = []
329 go (d : Other semi : rest)
330 | JSLiteral ";" <- node semi
331 , not (isDeclUsed d)
332 = go rest
333 go (d : rest)
334 | not (isDeclUsed d) = go rest
335 | otherwise = d : go rest
337 -- | Filter out the exports for members which aren't used.
338 filterExports :: ModuleElement -> ModuleElement
339 filterExports (ExportsList exps) = ExportsList (filter (\(_, nm, _, _) -> isKeyUsed (mid, nm)) exps)
340 filterExports me = me
342 isDeclUsed :: ModuleElement -> Bool
343 isDeclUsed (Member _ _ nm _ _) = isKeyUsed (mid, nm)
344 isDeclUsed _ = True
346 isKeyUsed :: Key -> Bool
347 isKeyUsed k
348 | Just me <- vertexFor k = me `S.member` reachableSet
349 | otherwise = False
351-- | Topologically sort the module dependency graph, so that when we generate code, modules can be
352-- defined in the right order.
353sortModules :: [Module] -> [Module]
354sortModules modules = map (\v -> case nodeFor v of (n, _, _) -> n) (reverse (topSort graph))
355 where
356 (graph, nodeFor, _) = graphFromEdges $ do
357 m@(Module mid els) <- modules
358 return (m, mid, mapMaybe getKey els)
360 getKey :: ModuleElement -> Maybe ModuleIdentifier
361 getKey (Require _ _ mi) = Just mi
362 getKey _ = Nothing
364-- | A module is empty if it contains no exported members (in other words,
365-- if the only things left after dead code elimination are module imports and
366-- "other" foreign code).
367--
368-- If a module is empty, we don't want to generate code for it.
369isModuleEmpty :: Module -> Bool
370isModuleEmpty (Module _ els) = all isElementEmpty els
371 where
372 isElementEmpty :: ModuleElement -> Bool
373 isElementEmpty (ExportsList exps) = null exps
374 isElementEmpty (Require _ _ _) = True
375 isElementEmpty (Other _) = True
376 isElementEmpty _ = False
378-- | Generate code for a set of modules, including a call to main().
379--
380-- Modules get defined on the global PS object, as follows:
381--
382-- var PS = { };
383-- (function(exports) {
384-- ...
385-- })(PS["Module.Name"] = PS["Module.Name"] || {});
386--
387-- In particular, a module and its foreign imports share the same namespace inside PS.
388-- This saves us from having to generate unique names for a module and its foreign imports,
389-- and is safe since a module shares a namespace with its foreign imports in PureScript as well
390-- (so there is no way to have overlaps in code generated by psc-make).
391codeGen :: Maybe String -- ^ main module
392 -> String -- ^ namespace
393 -> [Module] -- ^ input modules
394 -> String
395codeGen optionsMainModule optionsNamespace ms = renderToString (NN (JSSourceElementsTop (prelude ++ concatMap moduleToJS ms ++ maybe [] runMain optionsMainModule)))
396 where
397 moduleToJS :: Module -> [JSNode]
398 moduleToJS (Module mn ds) = wrap (moduleName mn) (indent (concatMap declToJS ds))
399 where
400 declToJS :: ModuleElement -> [JSNode]
401 declToJS (Member n _ _ _ _) = [n]
402 declToJS (Other n) = [n]
403 declToJS (Require _ nm req) =
404 [ NN (JSVariables (NT (JSLiteral "var") tokenPosnEmpty [ WhiteSpace tokenPosnEmpty "\n " ])
405 [ NN (JSVarDecl (sp (JSIdentifier nm))
406 [ sp (JSLiteral "=")
407 , moduleReference sp (moduleName req)
408 ])
409 ]
410 (nt (JSLiteral ";"))) ]
411 declToJS (ExportsList exps) = map toExport exps
413 where
414 toExport :: (ExportType, String, JSNode, [Key]) -> JSNode
415 toExport (_, nm, val, _) =
416 NN (JSExpression [ NN (JSMemberSquare [ NT (JSIdentifier "exports") tokenPosnEmpty [ WhiteSpace tokenPosnEmpty "\n " ] ]
417 (nt (JSLiteral "["))
418 (NN (JSExpression [ nt (JSStringLiteral '"' nm) ]))
419 (nt (JSLiteral "]")))
420 , NN (JSOperator (sp (JSLiteral "=")))
421 , reindent val
422 , nt (JSLiteral ";")
423 ])
425 reindent :: JSNode -> JSNode
426 reindent (NT n _ _) = sp n
427 reindent nn = nn
429 indent :: [JSNode] -> [JSNode]
430 indent = everywhere (mkT squash)
431 where
432 squash (NT n pos ann) = NT n (keepCol pos) (map splat ann)
433 squash nn = nn
435 splat (CommentA pos s) = CommentA (keepCol pos) s
436 splat (WhiteSpace pos w) = WhiteSpace (keepCol pos) w
437 splat ann = ann
439 keepCol (TokenPn _ _ c) = TokenPn 0 0 (c + 2)
441 prelude :: [JSNode]
442 prelude =
443 [ NN (JSVariables (NT (JSLiteral "var") tokenPosnEmpty [ CommentA tokenPosnEmpty ("// Generated by psc-bundle " ++ showVersion Paths.version)
444 , WhiteSpace tokenPosnEmpty "\n"
445 ])
446 [ NN (JSVarDecl (sp (JSIdentifier optionsNamespace))
447 [ sp (JSLiteral "=")
448 , NN (JSObjectLiteral (sp (JSLiteral "{"))
449 []
450 (sp (JSLiteral "}")))
451 ])
452 ]
453 (nt (JSLiteral ";")))
454 , lf
455 ]
457 moduleReference :: (Node -> JSNode) -> String -> JSNode
458 moduleReference f mn =
459 NN (JSMemberSquare [ f (JSIdentifier optionsNamespace) ]
460 (nt (JSLiteral "["))
461 (NN (JSExpression [ nt (JSStringLiteral '"' mn) ]))
462 (nt (JSLiteral "]")))
464 wrap :: String -> [JSNode] -> [JSNode]
465 wrap mn ds =
466 [ NN (JSExpression [ NN (JSExpressionParen (nt (JSLiteral "("))
467 (NN (JSExpression [ NN (JSFunctionExpression (nt (JSLiteral "function"))
468 []
469 (nt (JSLiteral "(") ) [nt (JSIdentifier "exports")] (nt (JSLiteral ")"))
470 (NN (JSBlock [sp (JSLiteral "{")]
471 (lf : ds)
472 [nl (JSLiteral "}")])))]))
473 (nt (JSLiteral ")")))
474 , NN (JSArguments (nt (JSLiteral "("))
475 [ NN (JSExpression [ moduleReference nt mn
476 , NN (JSOperator (sp (JSLiteral "=")))
477 , NN (JSExpressionBinary "||"
478 [ moduleReference sp mn ]
479 (sp (JSLiteral "||"))
480 [ emptyObj ])
481 ])
482 ]
483 (nt (JSLiteral ")")))
484 ])
485 , nt (JSLiteral ";")
486 , lf
487 ]
488 where
489 emptyObj = NN (JSObjectLiteral (sp (JSLiteral "{")) [] (nt (JSLiteral "}")))
491 runMain :: String -> [JSNode]
492 runMain mn =
493 [ NN (JSExpression [ NN (JSMemberDot [ NN (JSMemberSquare [ nl (JSIdentifier optionsNamespace) ]
494 (nt (JSLiteral "["))
495 (NN (JSExpression [ nt (JSStringLiteral '"' mn) ]))
496 (nt (JSLiteral "]")))
497 ]
498 (nt (JSLiteral "."))
499 (nt (JSIdentifier "main")))
500 , NN (JSArguments (nt (JSLiteral "(")) [] (nt (JSLiteral ")")))
501 ])
502 , nt (JSLiteral ";")
503 ]
505 nt :: Node -> JSNode
506 nt n = NT n tokenPosnEmpty []
508 lf :: JSNode
509 lf = NT (JSLiteral "") tokenPosnEmpty [ WhiteSpace tokenPosnEmpty "\n" ]
511 sp :: Node -> JSNode
512 sp n = NT n tokenPosnEmpty [ WhiteSpace tokenPosnEmpty " " ]
514 nl :: Node -> JSNode
515 nl n = NT n tokenPosnEmpty [ WhiteSpace tokenPosnEmpty "\n" ]
517-- | The bundling function.
518-- This function performs dead code elimination, filters empty modules
519-- and generates and prints the final Javascript bundle.
520bundle :: forall m. (Applicative m, MonadError ErrorMessage m)
521 => [(ModuleIdentifier, String)] -- ^ The input modules. Each module should be javascript rendered from 'Language.PureScript.Make' or @psc-make@.
522 -> [ModuleIdentifier] -- ^ Entry points. These module identifiers are used as the roots for dead-code elimination
523 -> Maybe String -- ^ An optional main module.
524 -> String -- ^ The namespace (e.g. PS).
525 -> m String
526bundle inputStrs entryPoints mainModule namespace = do
527 input <- forM inputStrs $ \(ident, js) -> do
528 ast <- either (const $ throwError UnableToParseModule) pure $ parse js (moduleName ident)
529 return (ident, ast)
531 let mids = S.fromList (map (moduleName . fst) input)
533 modules <- mapM (fmap withDeps . uncurry (toModule mids)) input
535 let compiled = compile modules entryPoints
536 sorted = sortModules (filter (not . isModuleEmpty) compiled)
538 return (codeGen mainModule namespace sorted)