From 053d67c4f0979a3ae6bd4c6c4045bf81d3fb4f37 Mon Sep 17 00:00:00 2001 From: Etheryte Date: Tue, 27 Aug 2024 22:39:41 +0300 Subject: [PATCH] Add build time tooling to bundle stories --- .../build/plugins/generate-stories-plugin.js | 99 +++++++++++++++++++ web/html/src/build/webpack.config.js | 31 +++++- 2 files changed, 125 insertions(+), 5 deletions(-) create mode 100644 web/html/src/build/plugins/generate-stories-plugin.js diff --git a/web/html/src/build/plugins/generate-stories-plugin.js b/web/html/src/build/plugins/generate-stories-plugin.js new file mode 100644 index 000000000000..fb36f2532d8a --- /dev/null +++ b/web/html/src/build/plugins/generate-stories-plugin.js @@ -0,0 +1,99 @@ +const fs = require("fs").promises; +const path = require("path"); + +// eslint-disable-next-line no-unused-vars +const webpack = require("webpack"); + +/** Automatically gather all imports for story files */ +module.exports = class GenerateStoriesPlugin { + didApply = false; + outputFile = undefined; + + constructor({ outputFile }) { + if (!outputFile) { + throw new Error("GenerateStoriesPlugin: `outputFile` is not set"); + } + this.outputFile = outputFile; + } + + /** + * @param {webpack.Compiler} compiler + */ + apply(compiler) { + // See https://webpack.js.org/api/compiler-hooks/#hooks + compiler.hooks.watchRun.tapAsync("GenerateStoriesPlugin", async (params, callback) => + this.beforeOrWatchRun(params, callback) + ); + compiler.hooks.beforeRun.tapAsync("GenerateStoriesPlugin", async (params, callback) => + this.beforeOrWatchRun(params, callback) + ); + } + + /** + * + * @param {webpack.Compiler} compiler + * @param {Function} callback + */ + async beforeOrWatchRun(compiler, callback) { + if (this.didApply) { + callback(); + return; + } + this.didApply = true; + + /** Source directory for the compilation, an absolute path to `/web/html/src` */ + const webHtmlSrc = compiler.context; + if (!this.outputFile.startsWith(webHtmlSrc)) { + throw new RangeError("GenerateStoriesPlugin: `outputFile` is outside of the source code directory"); + } + + const files = await fs.readdir(webHtmlSrc, { recursive: true }); + const storyFilePaths = files + .filter( + (item) => !item.startsWith("node_modules") && (item.endsWith(".stories.ts") || item.endsWith(".stories.tsx")) + ) + .sort(); + + const stories = storyFilePaths.map((filePath) => { + const safeName = this.wordify(filePath); + // We use the parent directory name as the group name + const groupName = path.dirname(filePath).split("/").pop() ?? "Unknown"; + return storyTemplate(filePath, safeName, groupName); + }); + + const output = fileTemplate(stories.join("")); + await fs.writeFile(this.outputFile, output, "utf-8"); + console.log(`GenerateStoriesPlugin: wrote ${storyFilePaths.length} stories to ${this.outputFile}`); + callback(); + } + + wordify(input) { + return input.replace(/[\W_]+/g, "_"); + } +}; + +const storyTemplate = (filePath, safeName, groupName) => +` +import ${safeName}_component from "${filePath}"; +import ${safeName}_raw from "${filePath}?raw"; + +export const ${safeName} = { + path: "${filePath}", + title: "${path.basename(filePath)}", + groupName: "${groupName}", + component: ${safeName}_component, + raw: ${safeName}_raw, +}; +`; + +const fileTemplate = (content) => +` +/** + * NB! This is a generated file! + * Any changes you make here will be lost. + * See: web/html/src/build/plugins/generate-stories-plugin.js + */ + +/* eslint-disable */ +${content} +`.trim(); diff --git a/web/html/src/build/webpack.config.js b/web/html/src/build/webpack.config.js index ad197445b9b4..8c1347e1de98 100644 --- a/web/html/src/build/webpack.config.js +++ b/web/html/src/build/webpack.config.js @@ -7,6 +7,8 @@ const MiniCssExtractPlugin = require("mini-css-extract-plugin"); const SpeedMeasurePlugin = require("speed-measure-webpack-plugin"); const autoprefixer = require("autoprefixer"); +const GenerateStoriesPlugin = require("./plugins/generate-stories-plugin"); + const DEVSERVER_WEBSOCKET_PATHNAME = "/ws"; module.exports = (env, argv) => { @@ -82,6 +84,10 @@ module.exports = (env, argv) => { new MiniCssExtractPlugin({ chunkFilename: "css/[name].css", }), + new GenerateStoriesPlugin({ + inputDir: path.resolve(__dirname, "../manager"), + outputFile: path.resolve(__dirname, "../manager/storybook/stories.generated.ts"), + }), ]; if (isProductionMode) { @@ -118,15 +124,30 @@ module.exports = (env, argv) => { publicPath: "/", hashFunction: "md5", }, + // context: __dirname, + node: { + __filename: true, + __dirname: true, + }, devtool: isProductionMode ? "source-map" : "eval-source-map", module: { rules: [ { - test: /\.(ts|js)x?$/, - exclude: /node_modules/, - use: { - loader: "babel-loader", - }, + oneOf: [ + { + resourceQuery: /raw/, + type: "asset/source", + }, + { + test: /\.(ts|js)x?$/, + exclude: /node_modules/, + use: [ + { + loader: "babel-loader", + }, + ], + }, + ], }, { // Stylesheets that are imported directly by components