OLD | NEW |
(Empty) | |
| 1 /** |
| 2 * @license |
| 3 * Copyright (c) 2014 The Polymer Project Authors. All rights reserved. |
| 4 * This code may only be used under the BSD style license found at http://polyme
r.github.io/LICENSE.txt |
| 5 * The complete set of authors may be found at http://polymer.github.io/AUTHORS.
txt |
| 6 * The complete set of contributors may be found at http://polymer.github.io/CON
TRIBUTORS.txt |
| 7 * Code distributed by Google as part of the polymer project is also |
| 8 * subject to an additional IP rights grant found at http://polymer.github.io/PA
TENTS.txt |
| 9 */ |
| 10 |
| 11 // jshint node: true |
| 12 |
| 13 var cssom = require('cssom'); |
| 14 var fs = require('fs'); |
| 15 var path = require('path'); |
| 16 var uglify = require('uglify-js'); |
| 17 var url = require('url'); |
| 18 var whacko = require('whacko'); |
| 19 |
| 20 var constants = require('./constants.js'); |
| 21 var optparser = require('./optparser.js'); |
| 22 var pathresolver = require('./pathresolver'); |
| 23 var utils = require('./utils'); |
| 24 var setTextContent = utils.setTextContent; |
| 25 var getTextContent = utils.getTextContent; |
| 26 var searchAll = utils.searchAll; |
| 27 |
| 28 var read = {}; |
| 29 var options = {}; |
| 30 |
| 31 // validate options with boolean return |
| 32 function setOptions(optHash, callback) { |
| 33 optparser.processOptions(optHash, function(err, o) { |
| 34 if (err) { |
| 35 return callback(err); |
| 36 } |
| 37 options = o; |
| 38 callback(); |
| 39 }); |
| 40 } |
| 41 |
| 42 function exclude(regexes, href) { |
| 43 return regexes.some(function(r) { |
| 44 return r.test(href); |
| 45 }); |
| 46 } |
| 47 |
| 48 function excludeImport(href) { |
| 49 return exclude(options.excludes.imports, href); |
| 50 } |
| 51 |
| 52 function excludeScript(href) { |
| 53 return exclude(options.excludes.scripts, href); |
| 54 } |
| 55 |
| 56 function excludeStyle(href) { |
| 57 return exclude(options.excludes.styles, href); |
| 58 } |
| 59 |
| 60 function readFile(file) { |
| 61 var content = fs.readFileSync(file, 'utf8'); |
| 62 return content.replace(/^\uFEFF/, ''); |
| 63 } |
| 64 |
| 65 // inline relative linked stylesheets into <style> tags |
| 66 function inlineSheets($, inputPath, outputPath) { |
| 67 searchAll($, 'link[rel="stylesheet"]').each(function() { |
| 68 var el = $(this); |
| 69 var href = el.attr('href'); |
| 70 if (href && !excludeStyle(href)) { |
| 71 |
| 72 var rel = href; |
| 73 var inputPath = path.dirname(options.input); |
| 74 if (constants.ABS_URL.test(rel)) { |
| 75 var abs = path.resolve(inputPath, path.join(options.abspath, rel)); |
| 76 rel = path.relative(options.outputDir, abs); |
| 77 } |
| 78 |
| 79 var filepath = path.resolve(options.outputDir, rel); |
| 80 // fix up paths in the stylesheet to be relative to the location of the st
yle |
| 81 var content = pathresolver.rewriteURL(path.dirname(filepath), outputPath,
readFile(filepath)); |
| 82 var styleEl = whacko('<style>' + content + '</style>'); |
| 83 // clone attributes |
| 84 styleEl.attr(el.attr()); |
| 85 // don't set href or rel on the <style> |
| 86 styleEl.attr('href', null); |
| 87 styleEl.attr('rel', null); |
| 88 el.replaceWith(whacko.html(styleEl)); |
| 89 } |
| 90 }); |
| 91 } |
| 92 |
| 93 function inlineScripts($, dir) { |
| 94 searchAll($, constants.JS_SRC).each(function() { |
| 95 var el = $(this); |
| 96 var src = el.attr('src'); |
| 97 if (src && !excludeScript(src)) { |
| 98 |
| 99 var rel = src; |
| 100 var inputPath = path.dirname(options.input); |
| 101 if (constants.ABS_URL.test(rel)) { |
| 102 var abs = path.resolve(inputPath, path.join(options.abspath, rel)); |
| 103 rel = path.relative(options.outputDir, abs); |
| 104 } |
| 105 |
| 106 var filepath = path.resolve(dir, rel); |
| 107 var content = readFile(filepath); |
| 108 // NOTE: reusing UglifyJS's inline script printer (not exported from Outpu
tStream :/) |
| 109 content = content.replace(/<\x2fscript([>\/\t\n\f\r ])/gi, "<\\/script$1")
; |
| 110 el.replaceWith('<script>' + content + '</script>'); |
| 111 } |
| 112 }); |
| 113 } |
| 114 |
| 115 function concat(filename) { |
| 116 if (!read[filename]) { |
| 117 read[filename] = true; |
| 118 var $ = whacko.load(readFile(filename)); |
| 119 var dir = path.dirname(filename); |
| 120 pathresolver.resolvePaths($, dir, options.outputDir, options.abspath); |
| 121 processImports($); |
| 122 inlineSheets($, dir, options.outputDir); |
| 123 return $; |
| 124 } else if (options.verbose) { |
| 125 console.log('Dependency deduplicated'); |
| 126 } |
| 127 } |
| 128 |
| 129 function processImports($, mainDoc) { |
| 130 var bodyContent = []; |
| 131 searchAll($, constants.IMPORTS).each(function() { |
| 132 var el = $(this); |
| 133 var href = el.attr('href'); |
| 134 if (!excludeImport(href)) { |
| 135 var rel = href; |
| 136 var inputPath = path.dirname(options.input); |
| 137 if (constants.ABS_URL.test(rel)) { |
| 138 var abs = path.resolve(inputPath, path.join(options.abspath, rel)); |
| 139 rel = path.relative(options.outputDir, abs); |
| 140 } |
| 141 var $$ = concat(path.resolve(options.outputDir, rel)); |
| 142 if (!$$) { |
| 143 // remove import link |
| 144 el.remove(); |
| 145 return; |
| 146 } |
| 147 // append import document head to main document head |
| 148 el.replaceWith($$('head').html()); |
| 149 var bodyHTML = $$('body').html(); |
| 150 // keep the ordering of the import body in main document, before main docu
ment's body |
| 151 bodyContent.push(bodyHTML); |
| 152 } else if (!options.keepExcludes) { |
| 153 // if the path is excluded for being absolute, then the import link must r
emain |
| 154 var absexclude = options.abspath ? constants.REMOTE_ABS_URL : constants.AB
S_URL; |
| 155 if (!absexclude.test(href)) { |
| 156 el.remove(); |
| 157 } |
| 158 } |
| 159 }); |
| 160 // prepend the import document body contents to the main document, in order |
| 161 var content = bodyContent.join('\n'); |
| 162 // hide import body content in the main document |
| 163 if (mainDoc && content) { |
| 164 content = '<div hidden>' + content + '</div>'; |
| 165 } |
| 166 $('body').prepend(content); |
| 167 } |
| 168 |
| 169 function findScriptLocation($) { |
| 170 var pos = $('body').last(); |
| 171 if (!pos.length) { |
| 172 pos = $.root(); |
| 173 } |
| 174 return pos; |
| 175 } |
| 176 |
| 177 function isCommentOrEmptyTextNode(node) { |
| 178 if (node.type === 'comment'){ |
| 179 return true; |
| 180 } else if (node.type === 'text') { |
| 181 // return true if the node is only whitespace |
| 182 return !((/\S/).test(node.data)); |
| 183 } |
| 184 } |
| 185 |
| 186 function compressJS(content, inline) { |
| 187 try { |
| 188 var ast = uglify.parse(content); |
| 189 return ast.print_to_string({inline_script: inline}); |
| 190 } catch (e) { |
| 191 // return a useful error |
| 192 var js_err = new Error('Compress JS Error'); |
| 193 js_err.detail = e.message + ' at line: ' + e.line + ' col: ' + e.col; |
| 194 js_err.content = content; |
| 195 js_err.toString = function() { |
| 196 return this.message + '\n' + this.detail + '\n' + this.content; |
| 197 }; |
| 198 throw js_err; |
| 199 } |
| 200 } |
| 201 |
| 202 function compressCSS(content) { |
| 203 var out; |
| 204 try { |
| 205 var ast = cssom.parse(content); |
| 206 out = ast.toString(); |
| 207 } catch (e) { |
| 208 if (options.verbose) { |
| 209 console.log('Error parsing CSS:', e.toString()); |
| 210 console.log('Falling back to removing newlines only'); |
| 211 } |
| 212 out = content; |
| 213 } finally { |
| 214 return out.replace(/[\r\n]/g, ''); |
| 215 } |
| 216 } |
| 217 |
| 218 function removeCommentsAndWhitespace($) { |
| 219 function walk(node) { |
| 220 var content, c; |
| 221 if (!node) { |
| 222 return; |
| 223 } else if (isCommentOrEmptyTextNode(node)) { |
| 224 $(node).remove(); |
| 225 return true; |
| 226 } else if (node.type == 'script') { |
| 227 // only run uglify on inline javascript scripts |
| 228 if (!node.attribs.src && (!node.attribs.type || node.attribs.type == "text
/javascript")) { |
| 229 content = getTextContent(node); |
| 230 setTextContent(node, compressJS(content, true)); |
| 231 } |
| 232 } else if (node.type == 'style') { |
| 233 content = getTextContent(node); |
| 234 setTextContent(node, compressCSS(content)); |
| 235 } else if ((c = node.children)) { |
| 236 for (var i = 0; i < c.length; i++) { |
| 237 // since .remove() will modify this array, decrement `i` on successful c
omment removal |
| 238 if (walk(c[i])) { |
| 239 i--; |
| 240 } |
| 241 } |
| 242 } |
| 243 } |
| 244 |
| 245 // walk the whole AST from root |
| 246 walk($.root().get(0)); |
| 247 } |
| 248 |
| 249 function writeFileSync(filename, data, eop) { |
| 250 if (!options.outputHandler) { |
| 251 fs.writeFileSync(filename, data, 'utf8'); |
| 252 } else { |
| 253 options.outputHandler(filename, data, eop); |
| 254 } |
| 255 } |
| 256 |
| 257 function handleMainDocument() { |
| 258 // reset shared buffers |
| 259 read = {}; |
| 260 var content = options.inputSrc ? options.inputSrc.toString() : readFile(option
s.input); |
| 261 var $ = whacko.load(content); |
| 262 var dir = path.dirname(options.input); |
| 263 pathresolver.resolvePaths($, dir, options.outputDir, options.abspath); |
| 264 processImports($, true); |
| 265 if (options.inline) { |
| 266 inlineSheets($, dir, options.outputDir); |
| 267 } |
| 268 |
| 269 if (options.inline) { |
| 270 inlineScripts($, options.outputDir); |
| 271 } |
| 272 |
| 273 searchAll($, constants.JS_INLINE).each(function() { |
| 274 var el = $(this); |
| 275 var content = getTextContent(el); |
| 276 // find ancestor polymer-element node |
| 277 var parentElement = el.closest('polymer-element').get(0); |
| 278 if (parentElement) { |
| 279 var match = constants.POLYMER_INVOCATION.exec(content); |
| 280 var elementName = $(parentElement).attr('name'); |
| 281 if (match) { |
| 282 var invocation = utils.processPolymerInvocation(elementName, match); |
| 283 content = content.replace(match[0], invocation); |
| 284 setTextContent(el, content); |
| 285 } |
| 286 } |
| 287 }); |
| 288 |
| 289 // strip noscript from elements, and instead inject explicit Polymer() invocat
ion |
| 290 // script, so registration order is preserved |
| 291 searchAll($, constants.ELEMENTS_NOSCRIPT).each(function() { |
| 292 var el = $(this); |
| 293 var name = el.attr('name'); |
| 294 if (options.verbose) { |
| 295 console.log('Injecting explicit Polymer invocation for noscript element "'
+ name + '"'); |
| 296 } |
| 297 el.append('<script>Polymer(\'' + name + '\');</script>'); |
| 298 el.attr('noscript', null); |
| 299 }); |
| 300 |
| 301 // strip scripts into a separate file |
| 302 if (options.csp) { |
| 303 if (options.verbose) { |
| 304 console.log('Separating scripts into separate file'); |
| 305 } |
| 306 |
| 307 // CSPify main page by removing inline scripts |
| 308 var scripts = []; |
| 309 searchAll($, constants.JS_INLINE).each(function() { |
| 310 var el = $(this); |
| 311 var content = getTextContent(el); |
| 312 scripts.push(content); |
| 313 el.remove(); |
| 314 }); |
| 315 |
| 316 // join scripts with ';' to prevent breakages due to EOF semicolon insertion |
| 317 var scriptName = path.basename(options.output, '.html') + '.js'; |
| 318 var scriptContent = scripts.join(';' + constants.EOL); |
| 319 if (options.strip) { |
| 320 scriptContent = compressJS(scriptContent, false); |
| 321 } |
| 322 |
| 323 writeFileSync(path.resolve(options.outputDir, scriptName), scriptContent); |
| 324 // insert out-of-lined script into document |
| 325 findScriptLocation($).append('<script charset="utf-8" src="' + scriptName +
'"></script>'); |
| 326 } |
| 327 |
| 328 deduplicateImports($); |
| 329 |
| 330 if (options.strip) { |
| 331 removeCommentsAndWhitespace($); |
| 332 } |
| 333 |
| 334 writeFileSync(options.output, $.html(), true); |
| 335 } |
| 336 |
| 337 function deduplicateImports($) { |
| 338 var imports = {}; |
| 339 searchAll($, constants.IMPORTS).each(function() { |
| 340 var el = $(this); |
| 341 var href = el.attr('href'); |
| 342 // TODO(dfreedm): allow a user defined base url? |
| 343 var abs = url.resolve('http://', href); |
| 344 if (!imports[abs]) { |
| 345 imports[abs] = true; |
| 346 } else { |
| 347 if(options.verbose) { |
| 348 console.log('Import Dependency deduplicated'); |
| 349 } |
| 350 el.remove(); |
| 351 } |
| 352 }); |
| 353 } |
| 354 |
| 355 exports.processDocument = handleMainDocument; |
| 356 exports.setOptions = setOptions; |
OLD | NEW |