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 -