expansive.es 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352
  1. /*
  2. expansive.es - Configuration for exp-js
  3. Transform by minifying.
  4. */
  5. Expansive.load({
  6. services: {
  7. name: 'js',
  8. compress: true,
  9. dotmin: true,
  10. extract: false,
  11. files: [ 'lib/**.js*', '!lib**.map' ],
  12. minify: false,
  13. options: '--mangle --compress dead_code=true,conditionals=true,booleans=true,unused=true,if_return=true,join_vars=true,drop_console=true'
  14. usemap: true,
  15. usemin: true,
  16. transforms: [{
  17. mappings: {
  18. 'js',
  19. 'min.js',
  20. 'min.map',
  21. 'min.js.map'
  22. },
  23. init: function(transform) {
  24. let service = transform.service
  25. if (service.usemap) {
  26. service.usemin ||= true
  27. }
  28. if (service.files) {
  29. if (!(service.files is Array)) {
  30. service.files = [ service.files ]
  31. }
  32. expansive.control.collections.scripts =
  33. (expansive.control.collections.scripts + service.files).unique()
  34. }
  35. if (!service.extract) {
  36. expansive.transforms['js-extract'].enable = false
  37. }
  38. },
  39. resolve: function(path: Path, transform): Path? {
  40. let service = transform.service
  41. let vfile = directories.contents.join(path)
  42. if (vfile.endsWith('.min.js')) {
  43. let base = vfile.trimEnd('.min.js')
  44. if (service.usemin && !(service.minify && base.joinExt('js', true).exists)) {
  45. if (service.usemap) {
  46. if (base.joinExt('min.map', true).exists || base.joinExt('min.js.map', true).exists) {
  47. return terminal(path)
  48. }
  49. } else {
  50. return terminal(path)
  51. }
  52. }
  53. } else if (vfile.endsWith('.min.map') || vfile.endsWith('.min.js.map')) {
  54. if (service.usemin) {
  55. let base = vfile.trimEnd('.min.map').trimEnd('.min.js.map')
  56. if (service.usemap && !(service.minify && base.joinExt('js', true).exists)) {
  57. if (base.joinExt('min.js', true).exists) {
  58. return terminal(path)
  59. }
  60. }
  61. }
  62. } else {
  63. let minified = vfile.replaceExt('min.js')
  64. /*
  65. Minify if required, or a suitable minfied version does not exist or !usemin
  66. */
  67. if (service.minify || !(minified.exists && service.usemin && (!service.usemap ||
  68. (vfile.replaceExt('min.map').exists || vfile.replaceExt('min.js.map').exists)))) {
  69. if (service.minify && service.dotmin) {
  70. return terminal(path.trimExt().joinExt('min.js', true))
  71. }
  72. return terminal(path)
  73. }
  74. }
  75. return null
  76. },
  77. }, {
  78. name: 'minify',
  79. mappings: 'js',
  80. init: function(transform) {
  81. let service = transform.service
  82. let cmd = Cmd.locate('uglifyjs')
  83. if (!cmd) {
  84. trace('Warn', 'Cannot find uglifyjs')
  85. transform.enable = false
  86. } else if (!service.minify) {
  87. transform.enable = false
  88. } else {
  89. if (service.options) {
  90. cmd += ' ' + service.options
  91. }
  92. transform.cmd = cmd
  93. }
  94. },
  95. render: function (contents, meta, transform) {
  96. trace('Minify', meta.source)
  97. let cmd = transform.cmd
  98. if (transform.service.usemap) {
  99. let mapFile = meta.dest.replaceExt('map')
  100. mapFile.dirname.makeDir()
  101. cmd += ' --source-map ' + mapFile
  102. contents = runFile(cmd, contents, meta)
  103. let map = mapFile.readJSON()
  104. map.sources = [ meta.dest ]
  105. mapFile.write(serialize(map))
  106. } else {
  107. contents = run(cmd, contents)
  108. }
  109. return contents
  110. },
  111. }, {
  112. name: 'render',
  113. /*
  114. Default to only scripts from packages under contents/lib
  115. Required scripts may vary on a page by page basis.
  116. */
  117. mappings: {
  118. 'js',
  119. 'min.js'
  120. },
  121. init: function(transform) {
  122. let service = transform.service
  123. service.hash = {}
  124. /*
  125. Render Scripts is based on 'collections.scripts' which defaults to 'lib/**.js' and is modified
  126. via expansive.json and addItems.
  127. */
  128. global.renderScripts = function(filter = null, extras = []) {
  129. let collections = expansive.collections
  130. if (!collections.scripts) {
  131. return
  132. }
  133. function buildScriptList(files: Array): Array {
  134. let directories = expansive.directories
  135. let service = expansive.services.js
  136. let scripts = []
  137. for each (script in files) {
  138. let before = script
  139. if ((script = expansive.getDestPath(script)) == null) {
  140. continue
  141. }
  142. let vfile = directories.contents.join(script)
  143. let base = vfile.trimEnd('.min.js').trimEnd('.js')
  144. let map = base.joinExt('min.map', true).exists || base.joinExt('js.map', true).exists ||
  145. base.joinExt('.min.js.map', true).exists
  146. if (vfile.endsWith('min.js')) {
  147. if ((service.minify || service.usemin) && (!service.usemap || map)) {
  148. scripts.push(script)
  149. }
  150. } else {
  151. let minified = vfile.replaceExt('min.js').exists
  152. let map = vfile.replaceExt('min.map').exists || vfile.replaceExt('js.map').exists ||
  153. vfile.replaceExt('.min.js.map').exists
  154. if (service.minify || !minified || !(service.usemap && map)) {
  155. if (service.minify && service.dotmin) {
  156. scripts.push(script.replaceExt('min.js'))
  157. } else {
  158. scripts.push(script)
  159. }
  160. }
  161. }
  162. }
  163. return scripts
  164. }
  165. /*
  166. Pages have different scripts and so must compute script list per page.
  167. This is hased and saved.
  168. */
  169. let directories = expansive.directories
  170. let service = expansive.services.js
  171. if (!service.hash[collections.scripts]) {
  172. let files = directories.contents.files(collections.scripts,
  173. { contents: true, directories: false, relative: true})
  174. files = expansive.orderFiles(files, "js")
  175. service.hash[collections.scripts] = buildScriptList(files).unique()
  176. }
  177. for each (script in service.hash[collections.scripts]) {
  178. if (filter && !Path(script).glob(filter)) {
  179. continue
  180. }
  181. let uri = meta.top.join(script).trimStart('./')
  182. write('<script src="' + uri + '"></script>\n ')
  183. }
  184. if (extras && extras is String) {
  185. extras = [extras]
  186. }
  187. if (service.states) {
  188. let extracted = service.states[meta.destPath]
  189. if (extracted && extracted.href) {
  190. let jt = expansive.transforms.js
  191. extras.push(jt.resolve(extracted.href, jt))
  192. }
  193. }
  194. for each (script in extras) {
  195. let uri = meta.top.join(script).trimStart('./')
  196. write('<script src="' + uri + '"></script>\n ')
  197. }
  198. }
  199. },
  200. pre: function(transform) {
  201. if (expansive.modified.everything) {
  202. transform.service.hash = {}
  203. }
  204. },
  205. }, {
  206. name: 'extract',
  207. mappings: 'html',
  208. init: function(transform) {
  209. transform.nextId = 0
  210. transform.service.states = {}
  211. },
  212. render: function (contents, meta, transform) {
  213. let service = transform.service
  214. /*
  215. Local function to extract inline script elements
  216. */
  217. function handleScriptElements(contents, meta, state): String {
  218. let service = expansive.services.js
  219. let re = /<script[^>]*>(.*)<\/script>/smg
  220. let start = 0, end = 0
  221. let output = ''
  222. let matches
  223. while (matches = re.exec(contents, start)) {
  224. let elt = matches[0]
  225. end = re.lastIndex - elt.length
  226. output += contents.slice(start, end)
  227. if (elt.match(/src=['"]/)) {
  228. output += matches[0]
  229. } else {
  230. state.scripts ||= []
  231. state.scripts.push(matches[1].trimEnd(';'))
  232. }
  233. start = re.lastIndex
  234. }
  235. output += contents.slice(start)
  236. return output
  237. }
  238. /*
  239. Local function to extract onclick attributes
  240. */
  241. function handleScriptAttributes(contents, meta, state): String {
  242. let result = ''
  243. let re = /<([\w_\-:.]+)([^>]*>)/g
  244. let start = 0, end = 0
  245. let matches
  246. while (matches = re.exec(contents, start)) {
  247. let elt = matches[0]
  248. end = re.lastIndex - matches[0].length
  249. /* Emit prior contents */
  250. result += contents.slice(start, end)
  251. let clickre = /(.*) +(onclick=)(["'])(.*?)(\3)(.*)/m
  252. if ((cmatches = clickre.exec(elt)) != null) {
  253. elt = cmatches[1] + cmatches[6]
  254. let id
  255. if ((ematches = elt.match(/id=['"]([^'"]+)['"]/)) != null) {
  256. id = ematches[1]
  257. } else {
  258. let service = expansive.services.js
  259. id = 'exp-' + ++transform.nextId
  260. elt = cmatches[1] + ' id="' + id + '"' + cmatches[6]
  261. }
  262. state.onclick ||= {}
  263. state.onclick[id] = cmatches[4].trimEnd(';')
  264. }
  265. result += elt
  266. start = re.lastIndex
  267. }
  268. return result + contents.slice(start)
  269. }
  270. let state = {}
  271. contents = handleScriptElements(contents, meta, state)
  272. contents = handleScriptAttributes(contents, meta, state)
  273. if (state.scripts || state.onclick) {
  274. let ss = service.states[meta.destPath] ||= {}
  275. if (service.extract === true) {
  276. ss.href = meta.destPath.replaceExt('js')
  277. } else {
  278. ss.href = service.extract
  279. }
  280. if (state.scripts) {
  281. ss.scripts ||= []
  282. ss.scripts += state.scripts
  283. vtrace('Extract', 'Scripts from ' + meta.document)
  284. }
  285. if (state.onclick) {
  286. ss.onclick ||= {}
  287. blend(ss.onclick, state.onclick)
  288. vtrace('Extract', 'Onclick from ' + meta.document)
  289. }
  290. }
  291. return contents
  292. }
  293. /*
  294. Post process and create external script files for inline scripts
  295. */
  296. post: function(transform) {
  297. let service = transform.service
  298. let perdoc = (service.extract === true)
  299. let scripts = '/*\n Inline scripts for \n */\n'
  300. for (let [file, state] in service.states) {
  301. if (perdoc) {
  302. scripts = '/*\n Inline scripts for ' + file + '\n */\n'
  303. }
  304. if (state.scripts) {
  305. scripts += state.scripts.unique().join(';\n\n') + ';\n\n'
  306. }
  307. for (let [id, code] in state.onclick) {
  308. scripts += 'document.getElementById("' + id + '").addEventListener("click", function() { ' +
  309. code + '});\n\n'
  310. }
  311. if (perdoc) {
  312. let destPath = Path(file).replaceExt('js')
  313. let dest = directories.dist.join(destPath)
  314. let meta = blend(expansive.topMeta.clone(), { document: destPath, layout: null })
  315. scripts = renderContents(scripts, meta)
  316. writeDest(scripts, meta)
  317. }
  318. }
  319. if (!perdoc) {
  320. let destPath = service.extract
  321. let dest = directories.dist.join(destPath)
  322. let meta = blend(expansive.topMeta.clone(), { document: destPath, layout: null })
  323. scripts = renderContents(scripts, meta)
  324. writeDest(scripts, meta)
  325. }
  326. },
  327. } ]
  328. }
  329. })