/* 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 rewrites JS source files to use CSS
* imports when running in Storybook. This allows JS files loaded in Storybook to use
* chrome:// URIs when loading external stylesheets without having to worry
* about Storybook being able to find and detect changes to the files.
*
* This loader allows Lit-based custom element code like this to work with
* Storybook:
*
* render() {
* return html`
*
* ...
* `;
* }
*
* By rewriting the source to this:
*
* import moztoggleStyles from "toolkit/content/widgets/moz-toggle/moz-toggle.css";
* ...
* render() {
* return html`
*
* ...
* `;
* }
*
* It works similarly for vanilla JS custom elements that utilize template
* strings. The following code:
*
* static get markup() {
* return`
*
*
* ...
*
* `;
* }
*
* Gets rewritten to:
*
* import migrationwizardStyles from "browser/themes/shared/migration/migration-wizard.css";
* ...
* static get markup() {
* return`
*
*
* ...
*
* `;
* }
*/
const path = require("path");
const projectRoot = path.join(process.cwd(), "../../..");
const rewriteChromeUri = require("./chrome-uri-utils.js");
/**
* Return an array of the unique chrome:// CSS URIs referenced in this file.
*
* @param {string} source - The source file to scan.
* @returns {string[]} Unique list of chrome:// CSS URIs
*/
function getReferencedChromeUris(source) {
const chromeRegex = /chrome:\/\/.*?\.css/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 = rewriteChromeUri(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.addMissingDependency(path.join(projectRoot, localRelativePath));
}
}
// Rewrite the source file with mapped chrome:// URIs.
let rewrittenSource = source;
for (let [chromeUri, localPath] of chromeUriToLocalPath.entries()) {
// Generate an import friendly variable name for the default export from
// the CSS file e.g. __chrome_styles_loader__moztoggleStyles.
let cssImport = `__chrome_styles_loader__${path
.basename(localPath, ".css")
.replaceAll("-", "")}Styles`;
// MozTextLabel is a special case for now since we don't use a template.
if (
this.resourcePath.endsWith("/moz-label.mjs") ||
this.resourcePath.endsWith(".js")
) {
rewrittenSource = rewrittenSource.replaceAll(`"${chromeUri}"`, cssImport);
} else {
rewrittenSource = rewrittenSource.replaceAll(
chromeUri,
`\$\{${cssImport}\}`
);
}
// Add a CSS import statement as the first line in the file.
rewrittenSource =
`import ${cssImport} from "${localPath}";\n` + rewrittenSource;
}
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);
};