/* 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 - 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); };