tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

moz-styles-loader.js (6654B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 /* eslint-env node */
      5 
      6 /**
      7 * This file contains a webpack loader which rewrites JS source files to use
      8 * CSS imports when running in Storybook. This allows JS files loaded in
      9 * Storybook to use chrome:// and moz-src:/// URIs when loading external
     10 * stylesheets without having to worry about Storybook being able to find and
     11 * detect changes to the files.
     12 *
     13 * This loader allows Lit-based custom element code like this to work with
     14 * Storybook:
     15 *
     16 *    render() {
     17 *      return html`
     18 *        <link rel="stylesheet" href="chrome://global/content/elements/moz-toggle.css" />
     19 *        ...
     20 *      `;
     21 *    }
     22 *
     23 * By rewriting the source to this:
     24 *
     25 *    import moztoggleStyles from "toolkit/content/widgets/moz-toggle/moz-toggle.css";
     26 *    ...
     27 *    render() {
     28 *      return html`
     29 *        <link rel="stylesheet" href=${moztoggleStyles} />
     30 *        ...
     31 *      `;
     32 *    }
     33 *
     34 * It works similarly for vanilla JS custom elements that utilize template
     35 * strings. The following code:
     36 *
     37 *    static get markup() {
     38 *      return`
     39 *        <template>
     40 *          <link rel="stylesheet" href="chrome://browser/skin/migration/migration-wizard.css">
     41 *          ...
     42 *        </template>
     43 *      `;
     44 *    }
     45 *
     46 * Gets rewritten to:
     47 *
     48 *    import migrationwizardStyles from "browser/themes/shared/migration/migration-wizard.css";
     49 *    ...
     50 *    static get markup() {
     51 *      return`
     52 *        <template>
     53 *          <link rel="stylesheet" href=${migrationwizardStyles}>
     54 *          ...
     55 *        </template>
     56 *      `;
     57 *    }
     58 *
     59 * For moz-src:/// URIs the path is resolved relative to the importing file:
     60 *
     61 *    render() {
     62 *      return html`
     63 *        <link rel="stylesheet" href="moz-src:///third_party/js/prosemirror/prosemirror-view/style/prosemirror.css" />
     64 *        ...
     65 *      `;
     66 *    }
     67 *
     68 * Gets rewritten to:
     69 *
     70 *    import prosemirrorStyles from "../../../../third_party/js/prosemirror/prosemirror-view/style/prosemirror.css";
     71 *    ...
     72 *    render() {
     73 *      return html`
     74 *        <link rel="stylesheet" href=${prosemirrorStyles} />
     75 *        ...
     76 *      `;
     77 *    }
     78 */
     79 
     80 const path = require("path");
     81 const projectRoot = path.resolve(__dirname, "../../../../");
     82 const { rewriteChromeUri, rewriteMozSrcUri } = require("./moz-uri-utils.js");
     83 
     84 /**
     85 * Return an array of the unique chrome:// and moz-src:/// CSS URIs referenced in this file.
     86 *
     87 * @param {string} source - The source file to scan.
     88 * @returns {string[]} Unique list of chrome:// and moz-src:/// CSS URIs
     89 */
     90 function getReferencedCssUris(source) {
     91  const cssRegexes = [/chrome:\/\/.*?\.css/g, /moz-src:\/\/\/.*?\.css/g];
     92  const matches = new Set();
     93  for (let regex of cssRegexes) {
     94    for (let match of source.matchAll(regex)) {
     95      // Add the full URI to the set of matches.
     96      matches.add(match[0]);
     97    }
     98  }
     99  return [...matches];
    100 }
    101 
    102 /**
    103 * Resolve a CSS URI to a local path and its absolute dependency path.
    104 *
    105 * @param {string} cssUri - The CSS URI to resolve.
    106 * @param {string} resourcePath - The path of the file.
    107 * @returns {{localPath: string, dependencyPath: string}} The local relative path and absolute dependency path.
    108 */
    109 function resolveCssUri(cssUri, resourcePath) {
    110  let localPath = "";
    111  let dependencyPath = "";
    112 
    113  if (cssUri.startsWith("chrome://")) {
    114    localPath = rewriteChromeUri(cssUri);
    115    if (localPath) {
    116      dependencyPath = path.join(projectRoot, localPath);
    117    }
    118  }
    119  if (cssUri.startsWith("moz-src:///")) {
    120    const absolutePath = rewriteMozSrcUri(cssUri);
    121    if (absolutePath) {
    122      localPath = path.relative(path.dirname(resourcePath), absolutePath);
    123      // Ensure the path is treated as a relative file and not a package when imported.
    124      if (!localPath.startsWith(".")) {
    125        localPath = `./${localPath}`;
    126      }
    127      dependencyPath = absolutePath;
    128    }
    129  }
    130 
    131  return { localPath, dependencyPath };
    132 }
    133 
    134 /**
    135 * Replace references to chrome:// and moz-src:/// URIs with the relative path
    136 * on disk from the project root.
    137 *
    138 * @this {WebpackLoader} https://webpack.js.org/api/loaders/
    139 * @param {string} source - The source file to update.
    140 * @returns {string} The updated source.
    141 */
    142 async function rewriteCssUris(source) {
    143  const cssUriToLocalPath = new Map();
    144  // We're going to rewrite the chrome:// and moz-src:/// URIs, find all referenced URIs.
    145  let cssDependencies = getReferencedCssUris(source);
    146  for (let cssUri of cssDependencies) {
    147    const { localPath, dependencyPath } = resolveCssUri(
    148      cssUri,
    149      this.resourcePath
    150    );
    151    if (localPath) {
    152      // Store the mapping to a local path for this URI.
    153      cssUriToLocalPath.set(cssUri, localPath);
    154      // Tell webpack the file being handled depends on the referenced file.
    155      this.addMissingDependency(dependencyPath);
    156    }
    157  }
    158  // Rewrite the source file with mapped chrome:// and moz-src:/// URIs.
    159  let rewrittenSource = source;
    160  for (let [cssUri, localPath] of cssUriToLocalPath.entries()) {
    161    // Generate an import friendly variable name for the default export from
    162    // the CSS file e.g. __chrome_styles_loader__moztoggleStyles.
    163    let cssImport = `__chrome_styles_loader__${path
    164      .basename(localPath, ".css")
    165      .replaceAll("-", "")}Styles`;
    166 
    167    // MozTextLabel is a special case for now since we don't use a template.
    168    if (
    169      path.basename(this.resourcePath) == "moz-label.mjs" ||
    170      this.resourcePath.endsWith(".js")
    171    ) {
    172      rewrittenSource = rewrittenSource.replaceAll(`"${cssUri}"`, cssImport);
    173    } else {
    174      rewrittenSource = rewrittenSource.replaceAll(
    175        cssUri,
    176        `\$\{${cssImport}\}`
    177      );
    178    }
    179 
    180    // Add a CSS import statement as the first line in the file.
    181    rewrittenSource =
    182      `import ${cssImport} from "${localPath}";\n` + rewrittenSource;
    183  }
    184  return rewrittenSource;
    185 }
    186 
    187 /**
    188 * The WebpackLoader export. Runs async since apparently that's preferred.
    189 *
    190 * @param {string} source - The source to rewrite.
    191 * @param {Map} sourceMap - Source map data, unused.
    192 * @param {object} meta - Metadata, unused.
    193 */
    194 module.exports = async function mozUriLoader(source) {
    195  // Get a callback to tell webpack when we're done.
    196  const callback = this.async();
    197  // Rewrite the source async since that appears to be preferred (and will be
    198  // necessary once we support rewriting CSS/SVG/etc).
    199  const newSource = await rewriteCssUris.call(this, source);
    200  // Give webpack the rewritten content.
    201  callback(null, newSource);
    202 };