diff options
Diffstat (limited to 'browser/components/storybook/.storybook')
4 files changed, 245 insertions, 0 deletions
diff --git a/browser/components/storybook/.storybook/chrome-uri-loader.js b/browser/components/storybook/.storybook/chrome-uri-loader.js new file mode 100644 index 0000000000..b558732152 --- /dev/null +++ b/browser/components/storybook/.storybook/chrome-uri-loader.js @@ -0,0 +1,98 @@ +/* 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); +}; diff --git a/browser/components/storybook/.storybook/main.js b/browser/components/storybook/.storybook/main.js new file mode 100644 index 0000000000..625f3ab526 --- /dev/null +++ b/browser/components/storybook/.storybook/main.js @@ -0,0 +1,67 @@ +/* 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 */ + +const path = require("path"); +const projectRoot = path.resolve(__dirname, "../../../../"); + +// ./mach environment --format json +// topobjdir should be the build location + +module.exports = { + stories: [ + "../stories/**/*.stories.mdx", + "../stories/**/*.stories.@(js|jsx|mjs|ts|tsx)", + `${projectRoot}/toolkit/**/*.stories.@(js|jsx|mjs|ts|tsx)`, + ], + // 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/`, + ], + addons: [ + "@storybook/addon-links", + "@storybook/addon-essentials", + "@storybook/addon-a11y", + ], + framework: "@storybook/web-components", + webpackFinal: async (config, { configType }) => { + // `configType` has a value of 'DEVELOPMENT' or 'PRODUCTION' + // You can change the configuration based on that. + // 'PRODUCTION' is used when building the static version of storybook. + + // Make whatever fine-grained changes you need + config.resolve.alias.browser = `${projectRoot}/browser`; + config.resolve.alias.toolkit = `${projectRoot}/toolkit`; + config.resolve.alias[ + "toolkit-widgets" + ] = `${projectRoot}/toolkit/content/widgets/`; + + config.module.rules.push({ + test: /\.ftl$/, + type: "asset/source", + }); + + config.module.rules.push({ + test: /\.mjs/, + loader: path.resolve(__dirname, "./chrome-uri-loader.js"), + }); + + config.optimization = { + splitChunks: false, + runtimeChunk: false, + sideEffects: false, + usedExports: false, + concatenateModules: false, + minimizer: [], + }; + + // Return the altered config + return config; + }, + core: { + builder: "webpack5", + }, +}; diff --git a/browser/components/storybook/.storybook/preview-head.html b/browser/components/storybook/.storybook/preview-head.html new file mode 100644 index 0000000000..484bb64f73 --- /dev/null +++ b/browser/components/storybook/.storybook/preview-head.html @@ -0,0 +1,5 @@ +<!-- 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/. --> + +<link rel="stylesheet" href="chrome://global/skin/in-content/common.css"> diff --git a/browser/components/storybook/.storybook/preview.mjs b/browser/components/storybook/.storybook/preview.mjs new file mode 100644 index 0000000000..49f3bc4c47 --- /dev/null +++ b/browser/components/storybook/.storybook/preview.mjs @@ -0,0 +1,75 @@ +/* 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/. */ + +import { DOMLocalization } from "@fluent/dom"; +import { FluentBundle, FluentResource } from "@fluent/bundle"; + +// Base Fluent set up. +let storybookBundle = new FluentBundle("en-US"); +let loadedResources = new Set(); +function* generateBundles() { + yield* [storybookBundle]; +} +document.l10n = new DOMLocalization([], generateBundles); +document.l10n.connectRoot(document.documentElement); + +// Any fluent imports should go through MozXULElement.insertFTLIfNeeded. +window.MozXULElement = { + async insertFTLIfNeeded(name) { + if (loadedResources.has(name)) { + return; + } + + // This should be browser, locales-preview or toolkit. + let [root, ...rest] = name.split("/"); + let ftlContents; + + // TODO(mstriemer): These seem like they could be combined but I don't want + // to fight with webpack anymore. + if (root == "toolkit") { + // eslint-disable-next-line no-unsanitized/method + let imported = await import( + /* webpackInclude: /.*[\/\\].*\.ftl$/ */ + `toolkit/locales/en-US/${name}` + ); + ftlContents = imported.default; + } else if (root == "browser") { + // eslint-disable-next-line no-unsanitized/method + let imported = await import( + /* webpackInclude: /.*[\/\\].*\.ftl$/ */ + `browser/locales/en-US/${name}` + ); + ftlContents = imported.default; + } else if (root == "locales-preview") { + // eslint-disable-next-line no-unsanitized/method + let imported = await import( + /* webpackInclude: /\.ftl$/ */ + `browser/locales-preview/${rest}` + ); + ftlContents = imported.default; + } + + if (loadedResources.has(name)) { + // Seems possible we've attempted to load this twice before the first call + // resolves, so once the first load is complete we can abandon the others. + return; + } + + let ftlResource = new FluentResource(ftlContents); + storybookBundle.addResource(ftlResource); + loadedResources.add(name); + document.l10n.translateRoots(); + }, + + // 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(name) { + // eslint-disable-next-line no-unsanitized/method + let file = await import( + /* webpackInclude: /.*[\/\\].*\.css$/ */ + `browser/themes/shared/${name}` + ); + return file; + }, +}; |