Bug 1810890 - Part 2: update chrome-uri-loader to handle CSS rewriting only r=mstriemer

Depends on D185090

Differential Revision: https://phabricator.services.mozilla.com/D185091
This commit is contained in:
Hanna Jones 2023-09-05 17:48:24 +00:00
Родитель df54b95f5b
Коммит 825c3a61d7
6 изменённых файлов: 173 добавлений и 134 удалений

Просмотреть файл

@ -0,0 +1,145 @@
/* 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`
* <link rel="stylesheet" href="chrome://global/content/elements/moz-toggle.css" />
* ...
* `;
* }
*
* By rewriting the source to this:
*
* import moztoggleStyles from "toolkit/content/widgets/moz-toggle/moz-toggle.css";
* ...
* render() {
* return html`
* <link rel="stylesheet" href=${moztoggleStyles} />
* ...
* `;
* }
*
* It works similarly for vanilla JS custom elements that utilize template
* strings. The following code:
*
* static get markup() {
* return`
* <template>
* <link rel="stylesheet" href="chrome://browser/skin/migration/migration-wizard.css">
* ...
* </template>
* `;
* }
*
* Gets rewritten to:
*
* import migrationwizardStyles from "browser/themes/shared/migration/migration-wizard.css";
* ...
* static get markup() {
* return`
* <template>
* <link rel="stylesheet" href=${migrationwizardStyles}>
* ...
* </template>
* `;
* }
*/
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);
};

Просмотреть файл

@ -1,98 +0,0 @@
/* 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);
};

Просмотреть файл

@ -21,14 +21,6 @@ module.exports = {
// Everything else
"../stories/**/*.stories.@(js|jsx|mjs|ts|tsx|md)",
],
// Additions to the staticDirs might also need to get added to
// MozXULElement.importCss in preview.mjs to enable auto-reloading.
staticDirs: [
`${projectRoot}/toolkit/content/widgets/`,
`${projectRoot}/browser/themes/shared/`,
`${projectRoot}/browser/components/firefoxview/`,
`${projectRoot}/browser/components/aboutlogins/content/components/`,
],
addons: [
"@storybook/addon-links",
{
@ -84,6 +76,28 @@ module.exports = {
type: "asset/source",
});
config.module.rules.push({
test: /\.m?js$/,
exclude: /.storybook/,
use: [{ loader: path.resolve(__dirname, "./chrome-styles-loader.js") }],
});
// Replace the default CSS rule with a rule to emit a separate CSS file and
// export the URL. This allows us to rewrite the source to use CSS imports
// via the chrome-styles-loader.
let cssFileTest = /\.css$/.toString();
let cssRuleIndex = config.module.rules.findIndex(
rule => rule.test.toString() === cssFileTest
);
config.module.rules[cssRuleIndex] = {
test: /\.css$/,
exclude: [/.storybook/, /node_modules/],
type: "asset/resource",
generator: {
filename: "[name].[contenthash].css",
},
};
// We're adding a rule for files matching this pattern in order to support
// writing docs only stories in plain markdown.
const MD_STORY_REGEX = /(stories|story)\.md$/;

Просмотреть файл

@ -14,22 +14,6 @@ connectFluent();
// Any fluent imports should go through MozXULElement.insertFTLIfNeeded.
window.MozXULElement = {
insertFTLIfNeeded,
// For some reason Storybook doesn't watch the static folder. By creating a
// method with a dynamic import we can pull the desired files into the bundle.
async importCss(resourceName) {
// eslint-disable-next-line no-unsanitized/method
let file = await import(
/* webpackInclude: /.*[\/\\].*\.css$/ */
`browser/themes/shared/${resourceName}`
);
// eslint-disable-next-line no-unsanitized/method
file = await import(
/* webpackInclude: /.*[\/\\].*\.css$/ */
`browser/components/firefoxview/${resourceName}`
);
return file;
},
};
/**

Просмотреть файл

@ -38,9 +38,8 @@
commonStyles.href = "chrome://global/skin/in-content/common.css";
const messageBarStyles = document.createElement("link");
messageBarStyles.rel = "stylesheet";
messageBarStyles.href = window.IS_STORYBOOK
? "./message-bar.css"
: "chrome://global/content/elements/message-bar.css";
messageBarStyles.href =
"chrome://global/content/elements/message-bar.css";
template.content.append(commonStyles, messageBarStyles);
// A container for the entire message bar content,

Просмотреть файл

@ -13,10 +13,11 @@
static get fragment() {
if (!this._template) {
let parser = new DOMParser();
let cssPath = "chrome://global/content/elements/panel-list.css";
let doc = parser.parseFromString(
`
<template>
<link rel="stylesheet" href="chrome://global/content/elements/panel-list.css">
<link rel="stylesheet" href=${cssPath}>
<div class="arrow top" role="presentation"></div>
<div class="list" role="presentation">
<slot></slot>
@ -31,11 +32,7 @@
true
);
}
let frag = this._template.content.cloneNode(true);
if (window.IS_STORYBOOK) {
frag.querySelector("link").href = "./panel-list/panel-list.css";
}
return frag;
return this._template.content.cloneNode(true);
}
constructor() {
@ -512,9 +509,7 @@
let style = document.createElement("link");
style.rel = "stylesheet";
style.href = window.IS_STORYBOOK
? "./panel-list/panel-item.css"
: "chrome://global/content/elements/panel-item.css";
style.href = "chrome://global/content/elements/panel-item.css";
this.button = document.createElement("button");
this.button.setAttribute("role", "menuitem");