Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
path: root/build/webapp-optimize.js
blob: 8258e3de058413d0be0978b3755c0c7c21216a04 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386

function debug(str) {
  //dump(' -*- webapp-optimize.js: ' + str + '\n');
}


/**
 * Expose a global `win' object and load `l10n.js' in it --
 * note: the `?reload' trick ensures we don't load a cached `l10njs' library.
 */

var win = { navigator: {} };
Services.scriptloader.loadSubScript('file:///' + GAIA_DIR +
    '/shared/js/l10n.js?reload=' + new Date().getTime(), win);


/**
 * Locale list -- by default, only the default one
 */

var l10nLocales = [GAIA_DEFAULT_LOCALE];
var l10nDictionary = {
  locales: {},
  default_locale: GAIA_DEFAULT_LOCALE
};
l10nDictionary.locales[GAIA_DEFAULT_LOCALE] = {};

/**
 * whitelist by app name for javascript asset aggregation.
 */
const JS_AGGREGATION_WHITELIST = [
  'calendar'
];

/**
 * Helpers
 */

function optimize_getFileContent(webapp, htmlFile, relativePath) {
  let paths = relativePath.split('/');
  let file;

  // get starting directory: webapp root, HTML file or /shared/
  if (/^\//.test(relativePath)) {
    paths.shift();
    file = webapp.sourceDirectoryFile.clone();
  } else {
    file = htmlFile.parent.clone();
  }
  if (paths[0] == 'shared') {
    file = getFile(GAIA_DIR);
  }

  paths.forEach(function appendPath(name) {
    file.append(name);
    if (isSubjectToBranding(file.path)) {
      file.append((OFFICIAL == 1) ? 'official' : 'unofficial');
    }
  });

  try {
    return getFileContent(file);
  } catch (e) {
    dump(file.path + ' could not be found.\n');
    return '';
  }
}

/**
 * Aggregates javascript files by type to reduce the IO overhead.
 * Depending on the script tags there are two files made:
 *
 * - defered scripts (<script defer src= ...) :
 *    $(Gaia.aggregatePrefix)defer_$(html_filename).js
 *
 * - normal scripts (<script src=...) :
 *    $(Gaia.aggregatePrefix)$(html_filename).js
 *
 *
 * Also it is possible to skip aggregation on a per script basis:
 *
 *    <script src="..." data-skip-optimize defer></script>
 *
 *
 * This function is somewhat conservative about what it will aggregate and will
 * only group scripts found the documents <head> section.
 *
 * @param {HTMLDocument} doc DOM document of the file.
 * @param {Object} webapp details of current web app.
 * @param {NSFile} htmlFile filename/path of the document.
 */
function optimize_aggregateJsResources(doc, webapp, htmlFile) {
  // Everyone should be putting their scripts in head with defer.
  // The best case is that only l10n.js is put into a normal.
  let scripts = Array.slice(
    doc.head.querySelectorAll('script[src]')
  );

  let deferred = {
    prefix: 'defer_',
    content: '',
    lastNode: null
  };

  let normal = {
    prefix: '',
    content: '',
    lastNode: null
  };

  scripts.forEach(function(script, idx) {
    let html = script.outerHTML;

    // per-script out see comment in function header.
    if ('skipOptimize' in script.dataset) {
      // remove from scripts so it will not be commented out...
      debug(
        '[optimize ' + webapp.sourceDirectoryName + '] ' +
        'skipping script "' + html + '"'
      );
      scripts.splice(idx, 1);
      return;
    }

    // we inject the whole outerHTML into the comment for debugging so
    // if there is something valuable in the html that effects the script
    // that broke the app it should be fairly easy to tell what happened.
    let content = '; /* "' + html + ' "*/\n\n';

    // fetch the whole file append it to the comment.
    content += optimize_getFileContent(webapp, htmlFile, script.src);

    let config = normal;

    if (script.defer)
      config = deferred;

    config.content += content;
    config.lastNode = script;

    // some apps (email) use version in the script types
    //  (text/javascript;version=x).
    //
    // If we don't have the same version in the aggregate the
    //  app will not load correctly.
    if (script.type.indexOf('version') !== -1) {
      config.type = script.type;
    }
  });

  // root name like index or oncall, etc...
  let baseName = htmlFile.path.split('/').pop().split('.')[0];

  // used as basis for aggregated scripts...
  let rootDirectory = htmlFile.parent;

  // find the absolute root of the app's html file.
  let rootUrl = htmlFile.parent.path;
  rootUrl = rootUrl.replace(webapp.manifestFile.parent.path, '');
  // the above will yield something like: '', '/facebook/', '/contacts/', etc...

  function writeAggregatedScript(config) {
    // skip if we don't have any content to write.
    if (!config.content)
      return;

    // prefix the file we are about to write content to.
    let scriptBaseName =
      Gaia.aggregatePrefix + config.prefix + baseName + '.js';

    let target = rootDirectory.clone();
    target.append(scriptBaseName);

    debug('writing aggregated source file: ' + target.path);

    // write the contents of the aggregated script
    writeContent(target, config.content);

    let script = doc.createElement('script');
    let lastScript = config.lastNode;

    script.src = rootUrl + '/' + scriptBaseName;
    script.defer = lastScript.defer;
    // use the config's type if given (for text/javascript;version=x)
    script.type = config.type || lastScript.type;

    debug('writing to path="' + target.path + '" src="' + script.src + '"');

    // insert after the last script node of this type...
    let parent = lastScript.parentNode;
    parent.insertBefore(script, lastScript.nextSibling);
  }

  writeAggregatedScript(deferred);
  writeAggregatedScript(normal);

  function commentScript(script) {
    script.outerHTML = '<!-- ' + script.outerHTML + ' -->';
  }

  // comment out all scripts
  scripts.forEach(commentScript);
}

function optimize_embedl10nResources(doc, dictionary) {
  // remove all external l10n resource nodes
  var resources = doc.querySelectorAll('link[type="application/l10n"]');
  for (let i = 0; i < resources.length; i++) {
    let res = resources[i].outerHTML;
    resources[i].outerHTML = '<!-- ' + res + ' -->';
  }

  // put the current dictionary in an inline JSON script
  let script = doc.createElement('script');
  script.type = 'application/l10n';
  script.innerHTML = '\n  ' + JSON.stringify(dictionary) + '\n';
  doc.documentElement.appendChild(script);
}

function optimize_serializeHTMLDocument(doc, file) {
  debug('saving: ' + file.path);

  // the doctype string should always be '<!DOCTYPE html>' but just in case...
  let doctypeStr = '';
  let dt = doc.doctype;
  if (dt && dt.name) {
    doctypeStr = '<!DOCTYPE ' + dt.name;
    if (dt.publicId) {
      doctypeStr += ' PUBLIC ' + dt.publicId;
    }
    if (dt.systemId) {
      doctypeStr += ' ' + dt.systemId;
    }
    doctypeStr += '>\n';
  }

  // outerHTML breaks the formating, so let's use innerHTML instead
  let htmlStr = '<html';
  let docElt = doc.documentElement;
  let attrs = docElt.attributes;
  for (let i = 0; i < attrs.length; i++) {
    htmlStr += ' ' + attrs[i].nodeName.toLowerCase() +
               '="' + attrs[i].nodeValue + '"';
  }
  let innerHTML = docElt.innerHTML.replace(/  \n*<\/body>\n*/, '  </body>\n');
  htmlStr += '>\n  ' + innerHTML + '\n</html>\n';

  writeContent(file, doctypeStr + htmlStr);
}

function optimize_compile(webapp, file) {
  let mozL10n = win.navigator.mozL10n;

  let processedLocales = 0;
  let dictionary = l10nDictionary;

  // catch console.[log|warn|info] calls and redirect them to `dump()'
  // XXX for some reason, this won't work if gDEBUG >= 2 in l10n.js
  function optimize_dump(str) {
    dump(file.path.replace(GAIA_DIR, '') + ': ' + str + '\n');
  }

  win.console = {
    log: optimize_dump,
    warn: optimize_dump,
    info: optimize_dump
  };

  // catch the XHR in `loadResource' and use a local file reader instead
  win.XMLHttpRequest = function() {
    debug('loadResource');

    function open(type, url, async) {
      this.readyState = 4;
      this.status = 200;
      this.responseText = optimize_getFileContent(webapp, file, url);
    }

    function send() {
      this.onreadystatechange();
    }

    return {
      open: open,
      send: send,
      onreadystatechange: null
    };
  };

  // catch the `localized' event dispatched by `fireL10nReadyEvent()'
  win.dispatchEvent = function() {
    processedLocales++;
    debug('fireL10nReadyEvent - ' +
        processedLocales + '/' + l10nLocales.length);

    let docElt = win.document.documentElement;
    dictionary.locales[mozL10n.language.code] = mozL10n.dictionary;

    if (processedLocales < l10nLocales.length) {
      // load next locale
      mozL10n.language.code = l10nLocales[processedLocales];
    } else {
      // we expect the last locale to be the default one:
      // set the lang/dir attributes of the current document
      docElt.dir = mozL10n.language.direction;
      docElt.lang = mozL10n.language.code;

      // save localized document
      let newPath = file.path + '.' + GAIA_DEFAULT_LOCALE;
      let newFile = new FileUtils.File(newPath);
      optimize_embedl10nResources(win.document, dictionary);

      if (JS_AGGREGATION_WHITELIST.indexOf(webapp.sourceDirectoryName) !== -1) {
        optimize_aggregateJsResources(win.document, webapp, newFile);
        dump(
          '[optimize] aggregating javascript for : "' +
          webapp.sourceDirectoryName + '" \n'
        );
      }

      optimize_serializeHTMLDocument(win.document, newFile);
    }
  };

  // load and parse the HTML document
  let DOMParser = CC('@mozilla.org/xmlextras/domparser;1', 'nsIDOMParser');
  win.document = (new DOMParser()).
      parseFromString(getFileContent(file), 'text/html');

  // if this HTML document uses l10n.js, pre-localize it --
  // selecting a language triggers `XMLHttpRequest' and `dispatchEvent' above
  if (win.document.querySelector('script[src$="l10n.js"]')) {
    debug('localizing: ' + file.path);
    mozL10n.language.code = l10nLocales[processedLocales];
  }
}


/**
 * Pre-translate all HTML files for the default locale
 */

debug('Begin');

if (GAIA_INLINE_LOCALES === '1') {
  l10nLocales = [];
  l10nDictionary.locales = {};

  // LOCALES_FILE is a relative path by default: shared/resources/languages.json
  // -- but it can be an absolute path when doing a multilocale build.
  // LOCALES_FILE is using unix separator, ensure working fine on win32
  let abs_path_chunks = [GAIA_DIR].concat(LOCALES_FILE.split('/'));
  let file = getFile.apply(null, abs_path_chunks);
  if (!file.exists()) {
    file = getFile(LOCALES_FILE);
  }
  let locales = JSON.parse(getFileContent(file));

  // we keep the default locale order for `l10nDictionary.locales',
  // but we ensure the default locale comes last in `l10nLocales'.
  for (let lang in locales) {
    if (lang != GAIA_DEFAULT_LOCALE) {
      l10nLocales.push(lang);
    }
    l10nDictionary.locales[lang] = {};
  }
  l10nLocales.push(GAIA_DEFAULT_LOCALE);
}

Gaia.webapps.forEach(function(webapp) {
  // if BUILD_APP_NAME isn't `*`, we only accept one webapp
  if (BUILD_APP_NAME != '*' && webapp.sourceDirectoryName != BUILD_APP_NAME)
    return;

  debug(webapp.sourceDirectoryName);

  let files = ls(webapp.sourceDirectoryFile, true, /^(shared|tests?)$/);
  files.forEach(function(file) {
    if (/\.html$/.test(file.leafName)) {
      optimize_compile(webapp, file);
    }
  });
});

debug('End');