summaryrefslogtreecommitdiffstats
path: root/browser/components/storybook/.storybook/chrome-uri-loader.js
blob: b5587321528846bcda4ab40747fcf18e019c0321 (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
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/* eslint-env node */

/**
 * This file contains a webpack loader which has the goal of rewriting chrome://
 * URIs to local paths. This allows JS files loaded in Storybook to load JS
 * files using their chrome:// URI. Using the chrome:// URI is avoidable in many
 * cases, however in some cases such as importing the lit.all.mjs file from
 * browser/components/ there is no way to avoid it on the Firefox side.
 *
 * This loader depends on the `./mach storybook manifest` step to generate the
 * rewrites.js file. That file exports an object with the files we know how to
 * rewrite chrome:// URIs for.
 *
 * This loader allows code like this to work with storybook:
 *
 *     import { html } from "chrome://global/content/vendor/lit.all.mjs";
 *     import "chrome://global/content/elements/moz-button-group.mjs";
 *
 * In this example the file would be rewritten in the webpack bundle as:
 *
 *     import { html } from "toolkit/content/widgets/vendor/lit.all.mjs";
 *     import "toolkit/content/widgets/moz-button-group/moz-button-group.mjs";
 */

const path = require("path");

// Object<ChromeURI, LocalPath> - This is generated by `./mach storybook manifest`.
const rewrites = require("./rewrites.js");

const projectRoot = path.join(process.cwd(), "../../..");

/**
 * Return an array of the unique chrome:// URIs referenced in this file.
 *
 * @param {string} source - The source file to scan.
 * @returns {string[]} Unique list of chrome:// URIs
 */
function getReferencedChromeUris(source) {
  // We can only rewrite files that get imported. Which means currently we only
  // support .js and .mjs. In the future we hope to rewrite .css and .svg.
  const chromeRegex = /chrome:\/\/.*?\.(js|mjs)/g;
  const matches = new Set();
  for (let match of source.matchAll(chromeRegex)) {
    // Add the full URI to the set of matches.
    matches.add(match[0]);
  }
  return [...matches];
}

/**
 * Replace references to chrome:// URIs with the relative path on disk from the
 * project root.
 *
 * @this {WebpackLoader} https://webpack.js.org/api/loaders/
 * @param {string} source - The source file to update.
 * @returns {string} The updated source.
 */
async function rewriteChromeUris(source) {
  const chromeUriToLocalPath = new Map();
  // We're going to rewrite the chrome:// URIs, find all referenced URIs.
  let chromeDependencies = getReferencedChromeUris(source);
  for (let chromeUri of chromeDependencies) {
    let localRelativePath = rewrites[chromeUri];
    if (localRelativePath) {
      localRelativePath = localRelativePath.replaceAll("\\", "/");
      // Store the mapping to a local path for this chrome URI.
      chromeUriToLocalPath.set(chromeUri, localRelativePath);
      // Tell webpack the file being handled depends on the referenced file.
      this.addDependency(path.join(projectRoot, localRelativePath));
    }
  }
  // Rewrite the source file with mapped chrome:// URIs.
  let rewrittenSource = source;
  for (let [chromeUri, localPath] of chromeUriToLocalPath.entries()) {
    rewrittenSource = rewrittenSource.replaceAll(chromeUri, localPath);
  }
  return rewrittenSource;
}

/**
 * The WebpackLoader export. Runs async since apparently that's preferred.
 *
 * @param {string} source - The source to rewrite.
 * @param {Map} sourceMap - Source map data, unused.
 * @param {Object} meta - Metadata, unused.
 */
module.exports = async function chromeUriLoader(source) {
  // Get a callback to tell webpack when we're done.
  const callback = this.async();
  // Rewrite the source async since that appears to be preferred (and will be
  // necessary once we support rewriting CSS/SVG/etc).
  const newSource = await rewriteChromeUris.call(this, source);
  // Give webpack the rewritten content.
  callback(null, newSource);
};