moz-styles-loader.js (6654B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 /* eslint-env node */ 5 6 /** 7 * This file contains a webpack loader which rewrites JS source files to use 8 * CSS imports when running in Storybook. This allows JS files loaded in 9 * Storybook to use chrome:// and moz-src:/// URIs when loading external 10 * stylesheets without having to worry about Storybook being able to find and 11 * detect changes to the files. 12 * 13 * This loader allows Lit-based custom element code like this to work with 14 * Storybook: 15 * 16 * render() { 17 * return html` 18 * <link rel="stylesheet" href="chrome://global/content/elements/moz-toggle.css" /> 19 * ... 20 * `; 21 * } 22 * 23 * By rewriting the source to this: 24 * 25 * import moztoggleStyles from "toolkit/content/widgets/moz-toggle/moz-toggle.css"; 26 * ... 27 * render() { 28 * return html` 29 * <link rel="stylesheet" href=${moztoggleStyles} /> 30 * ... 31 * `; 32 * } 33 * 34 * It works similarly for vanilla JS custom elements that utilize template 35 * strings. The following code: 36 * 37 * static get markup() { 38 * return` 39 * <template> 40 * <link rel="stylesheet" href="chrome://browser/skin/migration/migration-wizard.css"> 41 * ... 42 * </template> 43 * `; 44 * } 45 * 46 * Gets rewritten to: 47 * 48 * import migrationwizardStyles from "browser/themes/shared/migration/migration-wizard.css"; 49 * ... 50 * static get markup() { 51 * return` 52 * <template> 53 * <link rel="stylesheet" href=${migrationwizardStyles}> 54 * ... 55 * </template> 56 * `; 57 * } 58 * 59 * For moz-src:/// URIs the path is resolved relative to the importing file: 60 * 61 * render() { 62 * return html` 63 * <link rel="stylesheet" href="moz-src:///third_party/js/prosemirror/prosemirror-view/style/prosemirror.css" /> 64 * ... 65 * `; 66 * } 67 * 68 * Gets rewritten to: 69 * 70 * import prosemirrorStyles from "../../../../third_party/js/prosemirror/prosemirror-view/style/prosemirror.css"; 71 * ... 72 * render() { 73 * return html` 74 * <link rel="stylesheet" href=${prosemirrorStyles} /> 75 * ... 76 * `; 77 * } 78 */ 79 80 const path = require("path"); 81 const projectRoot = path.resolve(__dirname, "../../../../"); 82 const { rewriteChromeUri, rewriteMozSrcUri } = require("./moz-uri-utils.js"); 83 84 /** 85 * Return an array of the unique chrome:// and moz-src:/// CSS URIs referenced in this file. 86 * 87 * @param {string} source - The source file to scan. 88 * @returns {string[]} Unique list of chrome:// and moz-src:/// CSS URIs 89 */ 90 function getReferencedCssUris(source) { 91 const cssRegexes = [/chrome:\/\/.*?\.css/g, /moz-src:\/\/\/.*?\.css/g]; 92 const matches = new Set(); 93 for (let regex of cssRegexes) { 94 for (let match of source.matchAll(regex)) { 95 // Add the full URI to the set of matches. 96 matches.add(match[0]); 97 } 98 } 99 return [...matches]; 100 } 101 102 /** 103 * Resolve a CSS URI to a local path and its absolute dependency path. 104 * 105 * @param {string} cssUri - The CSS URI to resolve. 106 * @param {string} resourcePath - The path of the file. 107 * @returns {{localPath: string, dependencyPath: string}} The local relative path and absolute dependency path. 108 */ 109 function resolveCssUri(cssUri, resourcePath) { 110 let localPath = ""; 111 let dependencyPath = ""; 112 113 if (cssUri.startsWith("chrome://")) { 114 localPath = rewriteChromeUri(cssUri); 115 if (localPath) { 116 dependencyPath = path.join(projectRoot, localPath); 117 } 118 } 119 if (cssUri.startsWith("moz-src:///")) { 120 const absolutePath = rewriteMozSrcUri(cssUri); 121 if (absolutePath) { 122 localPath = path.relative(path.dirname(resourcePath), absolutePath); 123 // Ensure the path is treated as a relative file and not a package when imported. 124 if (!localPath.startsWith(".")) { 125 localPath = `./${localPath}`; 126 } 127 dependencyPath = absolutePath; 128 } 129 } 130 131 return { localPath, dependencyPath }; 132 } 133 134 /** 135 * Replace references to chrome:// and moz-src:/// URIs with the relative path 136 * on disk from the project root. 137 * 138 * @this {WebpackLoader} https://webpack.js.org/api/loaders/ 139 * @param {string} source - The source file to update. 140 * @returns {string} The updated source. 141 */ 142 async function rewriteCssUris(source) { 143 const cssUriToLocalPath = new Map(); 144 // We're going to rewrite the chrome:// and moz-src:/// URIs, find all referenced URIs. 145 let cssDependencies = getReferencedCssUris(source); 146 for (let cssUri of cssDependencies) { 147 const { localPath, dependencyPath } = resolveCssUri( 148 cssUri, 149 this.resourcePath 150 ); 151 if (localPath) { 152 // Store the mapping to a local path for this URI. 153 cssUriToLocalPath.set(cssUri, localPath); 154 // Tell webpack the file being handled depends on the referenced file. 155 this.addMissingDependency(dependencyPath); 156 } 157 } 158 // Rewrite the source file with mapped chrome:// and moz-src:/// URIs. 159 let rewrittenSource = source; 160 for (let [cssUri, localPath] of cssUriToLocalPath.entries()) { 161 // Generate an import friendly variable name for the default export from 162 // the CSS file e.g. __chrome_styles_loader__moztoggleStyles. 163 let cssImport = `__chrome_styles_loader__${path 164 .basename(localPath, ".css") 165 .replaceAll("-", "")}Styles`; 166 167 // MozTextLabel is a special case for now since we don't use a template. 168 if ( 169 path.basename(this.resourcePath) == "moz-label.mjs" || 170 this.resourcePath.endsWith(".js") 171 ) { 172 rewrittenSource = rewrittenSource.replaceAll(`"${cssUri}"`, cssImport); 173 } else { 174 rewrittenSource = rewrittenSource.replaceAll( 175 cssUri, 176 `\$\{${cssImport}\}` 177 ); 178 } 179 180 // Add a CSS import statement as the first line in the file. 181 rewrittenSource = 182 `import ${cssImport} from "${localPath}";\n` + rewrittenSource; 183 } 184 return rewrittenSource; 185 } 186 187 /** 188 * The WebpackLoader export. Runs async since apparently that's preferred. 189 * 190 * @param {string} source - The source to rewrite. 191 * @param {Map} sourceMap - Source map data, unused. 192 * @param {object} meta - Metadata, unused. 193 */ 194 module.exports = async function mozUriLoader(source) { 195 // Get a callback to tell webpack when we're done. 196 const callback = this.async(); 197 // Rewrite the source async since that appears to be preferred (and will be 198 // necessary once we support rewriting CSS/SVG/etc). 199 const newSource = await rewriteCssUris.call(this, source); 200 // Give webpack the rewritten content. 201 callback(null, newSource); 202 };