render-activity-stream-html.js 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235
  1. /* eslint-disable no-console */
  2. const fs = require("fs");
  3. const {mkdir} = require("shelljs");
  4. const path = require("path");
  5. const {CENTRAL_LOCALES, DEFAULT_LOCALE} = require("./locales");
  6. // Note: DEFAULT_OPTIONS.baseUrl should match BASE_URL in aboutNewTabService.js
  7. // in mozilla-central.
  8. const DEFAULT_OPTIONS = {
  9. addonPath: "..",
  10. baseUrl: "resource://activity-stream/",
  11. };
  12. // Locales that should be displayed RTL
  13. const RTL_LIST = ["ar", "he", "fa", "ur"];
  14. /**
  15. * Get the language part of the locale.
  16. */
  17. function getLanguage(locale) {
  18. return locale.split("-")[0];
  19. }
  20. /**
  21. * Get the best strings for a single provided locale using similar locales and
  22. * DEFAULT_LOCALE as fallbacks.
  23. */
  24. function getStrings(locale, allStrings) {
  25. const availableLocales = Object.keys(allStrings);
  26. const language = getLanguage(locale);
  27. const similarLocales = availableLocales.filter(other =>
  28. other !== locale && getLanguage(other) === language);
  29. // Rank locales from least desired to most desired
  30. const localeFallbacks = [DEFAULT_LOCALE, ...similarLocales, locale];
  31. // Get strings from each locale replacing with those from more desired ones
  32. const desired = Object.assign({}, ...localeFallbacks.map(l => allStrings[l]));
  33. // Only include strings that are currently used (defined by default locale)
  34. return Object.assign({}, ...Object.keys(allStrings[DEFAULT_LOCALE]).map(
  35. key => ({[key]: desired[key]})));
  36. }
  37. /**
  38. * Get the text direction of the locale.
  39. */
  40. function getTextDirection(locale) {
  41. return RTL_LIST.includes(locale.split("-")[0]) ? "rtl" : "ltr";
  42. }
  43. /**
  44. * templateHTML - Generates HTML for activity stream, given some options and
  45. * prerendered HTML if necessary.
  46. *
  47. * @param {obj} options
  48. * {str} options.locale The locale to render in lang="" attribute
  49. * {str} options.direction The language direction to render in dir="" attribute
  50. * {str} options.baseUrl The base URL for all local assets
  51. * {bool} options.debug Should we use dev versions of JS libraries?
  52. * {bool} options.noscripts Should we include scripts in the prerendered files?
  53. * @return {str} An HTML document as a string
  54. */
  55. function templateHTML(options) {
  56. const debugString = options.debug ? "-dev" : "";
  57. const scripts = [
  58. "chrome://browser/content/contentSearchUI.js",
  59. "chrome://browser/content/contentTheme.js",
  60. `${options.baseUrl}vendor/react${debugString}.js`,
  61. `${options.baseUrl}vendor/react-dom${debugString}.js`,
  62. `${options.baseUrl}vendor/prop-types.js`,
  63. `${options.baseUrl}vendor/react-intl.js`,
  64. `${options.baseUrl}vendor/redux.js`,
  65. `${options.baseUrl}vendor/react-redux.js`,
  66. `${options.baseUrl}prerendered/${options.locale}/activity-stream-strings.js`,
  67. `${options.baseUrl}data/content/activity-stream.bundle.js`,
  68. ];
  69. // Add spacing and script tags
  70. const scriptRender = `\n${scripts.map(script => ` <script src="${script}"></script>`).join("\n")}`;
  71. return `<!doctype html>
  72. <html lang="${options.locale}" dir="${options.direction}">
  73. <head>
  74. <meta charset="utf-8">
  75. <meta http-equiv="Content-Security-Policy" content="default-src 'none'; object-src 'none'; script-src resource: chrome:; connect-src https:; img-src https: data: blob:; style-src 'unsafe-inline';">
  76. <title>${options.strings.newtab_page_title}</title>
  77. <link rel="icon" type="image/png" href="chrome://branding/content/icon32.png"/>
  78. <link rel="stylesheet" href="chrome://browser/content/contentSearchUI.css" />
  79. <link rel="stylesheet" href="${options.baseUrl}css/activity-stream.css" />
  80. </head>
  81. <body class="activity-stream">
  82. <div id="header-asrouter-container" role="presentation"></div>
  83. <div id="root"></div>
  84. <div id="footer-asrouter-container" role="presentation"></div>${options.noscripts ? "" : scriptRender}
  85. </body>
  86. </html>
  87. `;
  88. }
  89. /**
  90. * templateJs - Generates a js file that passes the initial state of the prerendered
  91. * DOM to the React version. This is necessary to ensure the checksum matches when
  92. * React mounts so that it can attach to the prerendered elements instead of blowing
  93. * them away.
  94. *
  95. * Note that this may no longer be necessary in React 16 and we should review whether
  96. * it is still necessary.
  97. *
  98. * @param {string} name The name of the global to expose
  99. * @param {string} desc Extra description to include in a js comment
  100. * @param {obj} state The data to expose as a window global
  101. * @return {str} The js file as a string
  102. */
  103. function templateJs(name, desc, state) {
  104. return `// Note - this is a generated ${desc} file.
  105. window.${name} = ${JSON.stringify(state, null, 2)};
  106. `;
  107. }
  108. /**
  109. * writeFiles - Writes to the desired files the result of a template given
  110. * various prerendered data and options.
  111. *
  112. * @param {string} name Something to identify in the console
  113. * @param {string} destPath Path to write the files to
  114. * @param {Map} filesMap Mapping of a string file name to templater
  115. * @param {Object} options Various options for the templater
  116. */
  117. function writeFiles(name, destPath, filesMap, options) {
  118. for (const [file, templater] of filesMap) {
  119. fs.writeFileSync(path.join(destPath, file), templater({options}));
  120. }
  121. console.log("\x1b[32m", `✓ ${name}`, "\x1b[0m");
  122. }
  123. const STATIC_FILES = new Map([
  124. ["activity-stream-debug.html", ({options}) => templateHTML(options)],
  125. ]);
  126. const LOCALIZED_FILES = new Map([
  127. ["activity-stream-strings.js", ({options: {locale, strings}}) => templateJs("gActivityStreamStrings", locale, strings)],
  128. ["activity-stream.html", ({options}) => templateHTML(options)],
  129. ["activity-stream-noscripts.html", ({options}) => templateHTML(Object.assign({}, options, {noscripts: true}))],
  130. ]);
  131. /**
  132. * main - Parses command line arguments, generates html and js with templates,
  133. * and writes files to their specified locations.
  134. */
  135. function main() { // eslint-disable-line max-statements
  136. // This code parses command line arguments passed to this script.
  137. // Note: process.argv.slice(2) is necessary because the first two items in
  138. // process.argv are paths
  139. const args = require("minimist")(process.argv.slice(2), {
  140. alias: {
  141. addonPath: "a",
  142. baseUrl: "b",
  143. },
  144. });
  145. const baseOptions = Object.assign({debug: false}, DEFAULT_OPTIONS, args || {});
  146. const addonPath = path.resolve(__dirname, baseOptions.addonPath);
  147. const allStrings = require(`${baseOptions.addonPath}/data/locales.json`);
  148. const extraLocales = Object.keys(allStrings).filter(locale =>
  149. locale !== DEFAULT_LOCALE && !CENTRAL_LOCALES.includes(locale));
  150. const prerenderedPath = path.join(addonPath, "prerendered");
  151. console.log(`Writing prerendered files to individual directories under ${prerenderedPath}:`);
  152. // Save default locale's strings to compare against other locales' strings
  153. let defaultStrings;
  154. let langStrings;
  155. const isSubset = (strings, existing) => existing &&
  156. Object.keys(strings).every(key => strings[key] === existing[key]);
  157. // Process the default locale first then all the ones from mozilla-central
  158. const localizedLocales = [];
  159. const skippedLocales = [];
  160. for (const locale of [DEFAULT_LOCALE, ...CENTRAL_LOCALES]) {
  161. // Skip the locale if it would have resulted in duplicate packaged files
  162. const strings = getStrings(locale, allStrings);
  163. if (isSubset(strings, defaultStrings) || isSubset(strings, langStrings)) {
  164. skippedLocales.push(locale);
  165. continue;
  166. }
  167. const options = Object.assign({}, baseOptions, {
  168. direction: getTextDirection(locale),
  169. locale,
  170. strings,
  171. });
  172. // Put locale-specific files in their own directory
  173. const localePath = path.join(prerenderedPath, "locales", locale);
  174. mkdir("-p", localePath);
  175. writeFiles(locale, localePath, LOCALIZED_FILES, options);
  176. // Only write static files once for the default locale
  177. if (locale === DEFAULT_LOCALE) {
  178. const staticPath = path.join(prerenderedPath, "static");
  179. mkdir("-p", staticPath);
  180. writeFiles(`${locale} (static)`, staticPath, STATIC_FILES,
  181. Object.assign({}, options, {debug: true}));
  182. // Save the default strings to compare against other locales' strings
  183. defaultStrings = strings;
  184. }
  185. // Save the language's strings to maybe reuse for the next similar locales
  186. if (getLanguage(locale) === locale) {
  187. langStrings = strings;
  188. }
  189. localizedLocales.push(locale);
  190. }
  191. if (skippedLocales.length) {
  192. console.log("\x1b[33m", `Skipped the following locales because they use the same strings as ${DEFAULT_LOCALE} or its language locale: ${skippedLocales.join(", ")}`, "\x1b[0m");
  193. }
  194. if (extraLocales.length) {
  195. console.log("\x1b[33m", `Skipped the following locales because they are not in CENTRAL_LOCALES: ${extraLocales.join(", ")}`, "\x1b[0m");
  196. }
  197. // Convert ja-JP-mac lang tag to ja-JP-macos bcp47 to work around bug 1478930
  198. const bcp47String = localizedLocales.join(" ").replace(/(ja-JP-mac)/, "$1os");
  199. // Provide some help to copy/paste locales if tests are failing
  200. console.log(`\nIf aboutNewTabService tests are failing for unexpected locales, make sure its list is updated:\nconst ACTIVITY_STREAM_BCP47 = "${bcp47String}".split(" ");`);
  201. }
  202. main();