diff --git a/browser/components/storybook/.storybook/main.js b/browser/components/storybook/.storybook/main.js
index 53bfa047724ba..747625b01981e 100644
--- a/browser/components/storybook/.storybook/main.js
+++ b/browser/components/storybook/.storybook/main.js
@@ -11,8 +11,9 @@ const projectRoot = path.resolve(__dirname, "../../../../");
module.exports = {
stories: [
+ "../**/*.stories.md",
"../stories/**/*.stories.mdx",
- "../stories/**/*.stories.@(js|jsx|mjs|ts|tsx)",
+ "../stories/**/*.stories.@(js|jsx|mjs|ts|tsx|md)",
`${projectRoot}/toolkit/**/*.stories.@(js|jsx|mjs|ts|tsx)`,
],
// Additions to the staticDirs might also need to get added to
@@ -48,6 +49,12 @@ module.exports = {
config.resolve.alias[
"lit.all.mjs"
] = `${projectRoot}/toolkit/content/widgets/vendor/lit.all.mjs`;
+ // @mdx-js/react@1.x.x versions don't get hoisted to the root node_modules
+ // folder due to the versions of React it accepts as a peer dependency. That
+ // means we have to go one level deeper and look in the node_modules of
+ // @storybook/addon-docs, which depends on @mdx-js/react.
+ config.resolve.alias["@mdx-js/react"] =
+ "browser/components/storybook/node_modules/@storybook/addon-docs/node_modules/@mdx-js/react";
// The @storybook/web-components project uses lit-html. Redirect it to our
// bundled version.
@@ -64,6 +71,41 @@ module.exports = {
loader: path.resolve(__dirname, "./chrome-uri-loader.js"),
});
+ // 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$/;
+
+ // Find the existing rule for MDX stories.
+ let mdxStoryTest = /(stories|story)\.mdx$/.toString();
+ let mdxRule = config.module.rules.find(
+ rule => rule.test.toString() === mdxStoryTest
+ );
+
+ // Use a custom Webpack loader to transform our markdown stories into MDX,
+ // then run our new MDX through the same loaders that Storybook usually uses
+ // for MDX files. This is how we get a docs page from plain markdown.
+ config.module.rules.push({
+ test: MD_STORY_REGEX,
+ use: [
+ ...mdxRule.use,
+ { loader: path.resolve(__dirname, "./markdown-story-loader.js") },
+ ],
+ });
+
+ // Find the existing rule for markdown files.
+ let markdownTest = /\.md$/.toString();
+ let markdownRuleIndex = config.module.rules.findIndex(
+ rule => rule.test.toString() === markdownTest
+ );
+ let markdownRule = config.module.rules[markdownRuleIndex];
+
+ // Modify the existing markdown rule so it doesn't process .stories.md
+ // files, but still treats any other markdown files as asset/source.
+ config.module.rules[markdownRuleIndex] = {
+ ...markdownRule,
+ exclude: MD_STORY_REGEX,
+ };
+
config.optimization = {
splitChunks: false,
runtimeChunk: false,
diff --git a/browser/components/storybook/.storybook/markdown-story-loader.js b/browser/components/storybook/.storybook/markdown-story-loader.js
new file mode 100644
index 0000000000000..9a22ff5d5a427
--- /dev/null
+++ b/browser/components/storybook/.storybook/markdown-story-loader.js
@@ -0,0 +1,79 @@
+/* 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 that takes markdown as its source and
+ * outputs a docs only MDX Storybook story. This enables us to write docs only
+ * pages in plain markdown by specifying a `.stories.md` extension.
+ *
+ * For more context on docs only stories, see:
+ * https://storybook.js.org/docs/web-components/writing-docs/mdx#documentation-only-mdx
+ *
+ * The MDX generated by the loader will then get run through the same loaders
+ * Storybook usually uses to transform MDX files.
+ */
+
+const path = require("path");
+
+/**
+ * Takes a file path and returns a string to use as the story title, capitalized
+ * and split into multiple words. The file name gets transformed into the story
+ * name, which will be visible in the Storybook sidebar. For example, either:
+ *
+ * /stories/hello-world.stories.md or /stories/helloWorld.md
+ *
+ * will result in a story named "Hello World".
+ *
+ * @param {string} filePath - path of the file being processed.
+ * @returns {string} The title of the story.
+ */
+function getDocsStoryTitle(filePath) {
+ let fileName = path.basename(filePath, ".stories.md");
+ let pascalCaseName = toPascalCase(fileName);
+ return pascalCaseName.match(/[A-Z][a-z]+/g)?.join(" ") || pascalCaseName;
+}
+
+/**
+ * Transforms a string into PascalCase e.g. hello-world becomes HelloWorld.
+ * @param {string} str - String in any case.
+ * @returns {string} The string converted to PascalCase.
+ */
+function toPascalCase(str) {
+ return str
+ .match(/[a-z0-9]+/gi)
+ .map(text => text[0].toUpperCase() + text.substring(1))
+ .join("");
+}
+
+/**
+ * The WebpackLoader export. Takes markdown as its source and returns a docs
+ * only MDX story. For now we're filing all docs only stories under "Docs", but
+ * that likely won't be desireable long term.
+ *
+ * @param {string} source - The markdown source to rewrite to MDX.
+ */
+module.exports = function markdownStoryLoader(source) {
+ // `this.resourcePath` is the path of the file being processed.
+ let storyTitle = getDocsStoryTitle(this.resourcePath);
+
+ // Unfortunately the indentation/spacing here seems to be important for the
+ // MDX parser to know what to do in the next step of the Webpack process.
+ let mdxSource = `
+import { Meta, Description } from "@storybook/addon-docs";
+
+
+
+${source}`;
+
+ return mdxSource;
+};
diff --git a/browser/components/storybook/README.md b/browser/components/storybook/README.stories.md
similarity index 94%
rename from browser/components/storybook/README.md
rename to browser/components/storybook/README.stories.md
index 71ff1274b6df7..af7a70b5d81ee 100644
--- a/browser/components/storybook/README.md
+++ b/browser/components/storybook/README.stories.md
@@ -27,24 +27,18 @@ commands, or with your personal npm/node that happens to be compatible.
This is the recommended approach for installing dependencies and running
Storybook locally.
-To start Storybook the first time (or if it's been a while since you last
-installed), run:
+To install dependencies and start Storybook, just run:
```sh
# This uses npm ci under the hood to install the package-lock.json exactly.
-./mach storybook install
-```
-
-Once you've got your dependencies installed you can start Storybook. You should
-run your local build to test in Storybook since chrome:// URLs are currently
-being pulled from the running browser, so any changes to common-shared.css for
-example will come from your build.
-
-```sh
-# Start the Storybook server.
./mach storybook
```
+This single command will first install any missing dependencies then start the
+local Storybook server. You should run your local build to test in Storybook
+since chrome:// URLs are currently being pulled from the running browser, so any
+changes to common-shared.css for example will come from your build.
+
The Storybook server will continue running and will watch for component file
changes. To access your local Storybook preview you can use the `launch`
subcommand:
@@ -181,7 +175,7 @@ throughout Firefox. You can import the component into `html`/`xhtml` files via a
`script` tag with `type="module"`:
```html
-