expansive.es 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374
  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/**.json', '!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 hashed 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. script = Path(script).portable
  182. if (service.absolute) {
  183. if (!script.startsWith('http') && !script.startsWith('..')) {
  184. script = '/' + script
  185. }
  186. } else {
  187. script = meta.top.join(script).trimStart('./')
  188. }
  189. write('<script src="' + script + '"></script>\n ')
  190. }
  191. if (extras && extras is String) {
  192. extras = [extras]
  193. }
  194. if (collections.remoteScripts) {
  195. extras = extras + collections.remoteScripts
  196. }
  197. if (service.states) {
  198. let extracted = service.states[meta.destPath]
  199. if (extracted && extracted.href) {
  200. let jt = expansive.transforms.js
  201. extras.push(jt.resolve(extracted.href, jt))
  202. }
  203. }
  204. for each (script in extras) {
  205. let async = ''
  206. if (script.startsWith('async ')) {
  207. async = 'async '
  208. script = script.split('async ')[1]
  209. }
  210. if (service.absolute) {
  211. if (!script.startsWith('http') && !script.startsWith('..')) {
  212. script = '/' + script
  213. }
  214. } else {
  215. script = meta.top.join(script).trimStart('./')
  216. // script = Path(script).portable
  217. }
  218. write('<script ' + async + 'src="' + script + '"></script>\n ')
  219. }
  220. }
  221. },
  222. pre: function(transform) {
  223. if (expansive.modified.everything) {
  224. transform.service.hash = {}
  225. }
  226. },
  227. }, {
  228. name: 'extract',
  229. mappings: 'html',
  230. init: function(transform) {
  231. transform.nextId = 0
  232. transform.service.states = {}
  233. },
  234. render: function (contents, meta, transform) {
  235. let service = transform.service
  236. /*
  237. Local function to extract inline script elements
  238. */
  239. function handleScriptElements(contents, meta, state): String {
  240. let service = expansive.services.js
  241. let re = /<script[^>]*>(.*)<\/script>/smg
  242. let start = 0, end = 0
  243. let output = ''
  244. let matches
  245. while (matches = re.exec(contents, start)) {
  246. let elt = matches[0]
  247. end = re.lastIndex - elt.length
  248. output += contents.slice(start, end)
  249. if (elt.match(/src=['"]/)) {
  250. output += matches[0]
  251. } else {
  252. state.scripts ||= []
  253. state.scripts.push(matches[1].trimEnd(';'))
  254. }
  255. start = re.lastIndex
  256. }
  257. output += contents.slice(start)
  258. return output
  259. }
  260. /*
  261. Local function to extract onclick attributes
  262. */
  263. function handleScriptAttributes(contents, meta, state): String {
  264. let result = ''
  265. let re = /<([\w_\-:.]+)([^>]*>)/g
  266. let start = 0, end = 0
  267. let matches
  268. while (matches = re.exec(contents, start)) {
  269. let elt = matches[0]
  270. end = re.lastIndex - matches[0].length
  271. /* Emit prior contents */
  272. result += contents.slice(start, end)
  273. let clickre = /(.*) +(onclick=)(["'])(.*?)(\3)(.*)/m
  274. if ((cmatches = clickre.exec(elt)) != null) {
  275. elt = cmatches[1] + cmatches[6]
  276. let id
  277. if ((ematches = elt.match(/id=['"]([^'"]+)['"]/)) != null) {
  278. id = ematches[1]
  279. } else {
  280. let service = expansive.services.js
  281. id = 'exp-' + ++transform.nextId
  282. elt = cmatches[1] + ' id="' + id + '"' + cmatches[6]
  283. }
  284. state.onclick ||= {}
  285. state.onclick[id] = cmatches[4].trimEnd(';')
  286. }
  287. result += elt
  288. start = re.lastIndex
  289. }
  290. return result + contents.slice(start)
  291. }
  292. let state = {}
  293. contents = handleScriptElements(contents, meta, state)
  294. contents = handleScriptAttributes(contents, meta, state)
  295. if (state.scripts || state.onclick) {
  296. let ss = service.states[meta.destPath] ||= {}
  297. if (service.extract === true) {
  298. ss.href = meta.destPath.replaceExt('js')
  299. } else {
  300. ss.href = service.extract
  301. }
  302. if (state.scripts) {
  303. ss.scripts ||= []
  304. ss.scripts += state.scripts
  305. vtrace('Extract', 'Scripts from ' + meta.document)
  306. }
  307. if (state.onclick) {
  308. ss.onclick ||= {}
  309. blend(ss.onclick, state.onclick)
  310. vtrace('Extract', 'Onclick from ' + meta.document)
  311. }
  312. }
  313. return contents
  314. }
  315. /*
  316. Post process and create external script files for inline scripts
  317. */
  318. post: function(transform) {
  319. let service = transform.service
  320. let perdoc = (service.extract === true)
  321. let scripts = '/*\n Inline scripts for \n */\n'
  322. for (let [file, state] in service.states) {
  323. if (perdoc) {
  324. scripts = '/*\n Inline scripts for ' + file + '\n */\n'
  325. }
  326. if (state.scripts) {
  327. scripts += state.scripts.unique().join(';\n\n') + ';\n\n'
  328. }
  329. for (let [id, code] in state.onclick) {
  330. scripts += 'document.getElementById("' + id + '").addEventListener("click", function() { ' +
  331. code + '});\n\n'
  332. }
  333. if (perdoc) {
  334. let destPath = Path(file).replaceExt('js')
  335. let dest = directories.dist.join(destPath)
  336. let meta = blend(expansive.topMeta.clone(), { document: destPath, layout: null })
  337. scripts = renderContents(scripts, meta)
  338. writeDest(scripts, meta)
  339. }
  340. }
  341. if (!perdoc) {
  342. let destPath = service.extract
  343. let dest = directories.dist.join(destPath)
  344. let meta = blend(expansive.topMeta.clone(), { document: destPath, layout: null })
  345. scripts = renderContents(scripts, meta)
  346. writeDest(scripts, meta)
  347. }
  348. },
  349. } ]
  350. }
  351. })