/* 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:// and moz-src:/// 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`
*
*
* ...
*
* `;
* }
*
* For moz-src:/// URIs the path is resolved relative to the importing file:
*
* render() {
* return html`
*
* ...
* `;
* }
*
* Gets rewritten to:
*
* import prosemirrorStyles from "../../../../third_party/js/prosemirror/prosemirror-view/style/prosemirror.css";
* ...
* render() {
* return html`
*
* ...
* `;
* }
*/
const path = require("path");
const projectRoot = path.resolve(__dirname, "../../../../");
const { rewriteChromeUri, rewriteMozSrcUri } = require("./moz-uri-utils.js");
/**
* Return an array of the unique chrome:// and moz-src:/// CSS URIs referenced in this file.
*
* @param {string} source - The source file to scan.
* @returns {string[]} Unique list of chrome:// and moz-src:/// CSS URIs
*/
function getReferencedCssUris(source) {
const cssRegexes = [/chrome:\/\/.*?\.css/g, /moz-src:\/\/\/.*?\.css/g];
const matches = new Set();
for (let regex of cssRegexes) {
for (let match of source.matchAll(regex)) {
// Add the full URI to the set of matches.
matches.add(match[0]);
}
}
return [...matches];
}
/**
* Resolve a CSS URI to a local path and its absolute dependency path.
*
* @param {string} cssUri - The CSS URI to resolve.
* @param {string} resourcePath - The path of the file.
* @returns {{localPath: string, dependencyPath: string}} The local relative path and absolute dependency path.
*/
function resolveCssUri(cssUri, resourcePath) {
let localPath = "";
let dependencyPath = "";
if (cssUri.startsWith("chrome://")) {
localPath = rewriteChromeUri(cssUri);
if (localPath) {
dependencyPath = path.join(projectRoot, localPath);
}
}
if (cssUri.startsWith("moz-src:///")) {
const absolutePath = rewriteMozSrcUri(cssUri);
if (absolutePath) {
localPath = path.relative(path.dirname(resourcePath), absolutePath);
// Ensure the path is treated as a relative file and not a package when imported.
if (!localPath.startsWith(".")) {
localPath = `./${localPath}`;
}
dependencyPath = absolutePath;
}
}
return { localPath, dependencyPath };
}
/**
* Replace references to chrome:// and moz-src:/// 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 rewriteCssUris(source) {
const cssUriToLocalPath = new Map();
// We're going to rewrite the chrome:// and moz-src:/// URIs, find all referenced URIs.
let cssDependencies = getReferencedCssUris(source);
for (let cssUri of cssDependencies) {
const { localPath, dependencyPath } = resolveCssUri(
cssUri,
this.resourcePath
);
if (localPath) {
// Store the mapping to a local path for this URI.
cssUriToLocalPath.set(cssUri, localPath);
// Tell webpack the file being handled depends on the referenced file.
this.addMissingDependency(dependencyPath);
}
}
// Rewrite the source file with mapped chrome:// and moz-src:/// URIs.
let rewrittenSource = source;
for (let [cssUri, localPath] of cssUriToLocalPath.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 (
path.basename(this.resourcePath) == "moz-label.mjs" ||
this.resourcePath.endsWith(".js")
) {
rewrittenSource = rewrittenSource.replaceAll(`"${cssUri}"`, cssImport);
} else {
rewrittenSource = rewrittenSource.replaceAll(
cssUri,
`\$\{${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 mozUriLoader(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 rewriteCssUris.call(this, source);
// Give webpack the rewritten content.
callback(null, newSource);
};