文档
Storybook Docs

Storybook 10.0 插件迁移指南

我们非常感谢插件创作者为保持 Storybook 生态系统的活力和最新所付出的努力。随着 Storybook 发展到 10.0 版本,带来了新功能和改进,本指南旨在帮助您将插件从 9.x 迁移到 10.0。如果您需要从早期版本的 Storybook 迁移插件,请首先参考 Storybook 9.0 插件迁移指南

我们还有一个通用的 Storybook 迁移指南,其中涵盖了您的 Storybook 实例的更新,而不是您的插件代码。

依赖项更新

您需要更新您的 Storybook 依赖项。对等依赖项必须指向 ^10.0.0,以确保您的最终用户具有广泛的兼容性。开发依赖项可以设置为 ^10.0.0,或者设置为 next,如果您想全年尝试最新的预发布版本。

package.json
{
  "devDependencies": {
    "@storybook/addon-docs": "next",
    "@storybook/react-vite": "next",
    "storybook": "next"
  },
  "peerDependencies": {
    "storybook": "^10.0.0"
  }
}

如果您仍然在您的依赖项中有 @storybook/addon-essentials@storybook/addon-interactions@storybook/addon-links@storybook/blocks,您将需要移除它们。自 Storybook 9 起,这些包就为空了,并且将不再发布。

支持早期版本

如果您的插件支持多个主要版本的 Storybook,您可以在对等依赖项中指定更宽的版本范围

package.json
{
  "name": "your-storybook-addon",
  "peerDependencies": {
    "storybook": "^9.0.0 || ^10.0.0"
  },
  "devDependencies": {
    "storybook": ">=10.0.0-0 <11.0.0-0" // For local development
  }
}

但是,我们建议您在发布新版本的 Storybook 的同时,发布新主要版本的插件。这种做法

  1. 使维护您的代码更容易
  2. 使您能够利用新功能和改进
  3. 为您的用户提供更清晰的升级路径

插件的关键更改

以下是 10.0 版本中影响插件开发的变化。

仅 ESM 构建

Storybook 10 要求所有插件都必须构建为仅 ESM。此更改简化了构建过程并减少了维护开销。您需要对 tsup.config.ts 进行许多更改,因此复制 addon-kit 仓库中的参考文件可能会更容易。

此更新带来了以下变化

  • Node 目标从 Node 20.0 移至 Node 20.19
  • 您不再需要构建 CJS 文件
  • 您不再需要传递 globalManagerPackagesglobalPreviewPackages
  • package.json 中的 bundler 配置不再需要手动输入
  • 我们建议您停止使用 exportEntries,并根据用户的使用情况,将导出的条目切换为 previewEntriesmanagerEntries
tsup.config.ts
- import { readFile } from "node:fs/promises";
 
import { defineConfig, type Options } from "tsup";
 
- import { globalPackages as globalManagerPackages } from "storybook/internal/manager/globals";
- import { globalPackages as globalPreviewPackages } from "storybook/internal/preview/globals";
 
- const NODE_TARGET: Options["target"] = "node20";
+ const NODE_TARGET = "node20.19"; // Minimum Node version supported by Storybook 10
 
- type BundlerConfig = {
-   bundler?: {
-     exportEntries?: string[];
-     nodeEntries?: string[];
-     managerEntries?: string[];
-     previewEntries?: string[];
-   };
- };
 
export default defineConfig(async (options) => {
  // reading the three types of entries from package.json, which has the following structure:
  // {
  //  ...
  //   "bundler": {
-  //     "exportEntries": ["./src/index.ts"],
  //     "managerEntries": ["./src/manager.ts"],
-  //     "previewEntries": ["./src/preview.ts"],
+  //     "previewEntries": ["./src/preview.ts", "./src/index.ts"],
  //     "nodeEntries": ["./src/preset.ts"]
  //   }
  // }
-   const packageJson = (await readFile("./package.json", "utf8").then(
-     JSON.parse,
-   )) as BundlerConfig;
+   const packageJson = (
+     await import("./package.json", { with: { type: "json" } })
+   ).default;
+ 
  const {
    bundler: {
-      exportEntries = [],
      managerEntries = [],
      previewEntries = [],
      nodeEntries = [],
-    } = {},
+    },
  } = packageJson;
 
  const commonConfig: Options = {
-    splitting: false,
+    splitting: true,
+    format: ["esm"],
-    minify: !options.watch,
    treeshake: true,
-    sourcemap: true,
    // keep this line commented until https://github.com/egoist/tsup/issues/1270 is resolved
    // clean: options.watch ? false : true,
    clean: false,
+    // The following packages are provided by Storybook and should always be externalized
+    // Meaning they shouldn't be bundled with the addon, and they shouldn't be regular dependencies either
+    external: ["react", "react-dom", "@storybook/icons"],
  };
 
  const configs: Options[] = [];
- 
-   // export entries are entries meant to be manually imported by the user
-   // they are not meant to be loaded by the manager or preview
-   // they'll be usable in both node and browser environments, depending on which features and modules they depend on
-   if (exportEntries.length) {
-     configs.push({
-       ...commonConfig,
-       entry: exportEntries,
-       dts: {
-         resolve: true,
-       },
-       format: ["esm", "cjs"],
-       platform: "neutral",
-       target: NODE_TARGET,
-       external: [...globalManagerPackages, ...globalPreviewPackages],
-     });
-   }
 
  // manager entries are entries meant to be loaded into the manager UI
  // they'll have manager-specific packages externalized and they won't be usable in node
  // they won't have types generated for them as they're usually loaded automatically by Storybook
  if (managerEntries.length) {
    configs.push({
      ...commonConfig,
      entry: managerEntries,
-      format: ["esm"],
      platform: "browser",
-      target: BROWSER_TARGETS,
+      target: "esnext", // we can use esnext for manager entries since Storybook will bundle the addon's manager entries again anyway
-      external: globalManagerPackages,
    });
  }
 
  // preview entries are entries meant to be loaded into the preview iframe
  // they'll have preview-specific packages externalized and they won't be usable in node
  // they'll have types generated for them so they can be imported when setting up Portable Stories
  if (previewEntries.length) {
    configs.push({
      ...commonConfig,
      entry: previewEntries,
-      dts: {
-        resolve: true,
-      },
-      format: ["esm", "cjs"],
      platform: "browser",
-      target: BROWSER_TARGETS,
+      target: "esnext", // we can use esnext for preview entries since Storybook will bundle the addon's preview entries again anyway
-      external: globalPreviewPackages,
+      dts: true,
    });
  }
 
  // node entries are entries meant to be used in node-only
  // this is useful for presets, which are loaded by Storybook when setting up configurations
  // they won't have types generated for them as they're usually loaded automatically by Storybook
  if (nodeEntries.length) {
    configs.push({
      ...commonConfig,
      entry: nodeEntries,
-      format: ["cjs"],
      platform: "node",
      target: NODE_TARGET,
    });

接下来,更新 package.json 中的 exports 字段,以删除对 CJS 文件的提及。

package.json
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
-      "import": "./dist/index.js",
-      "require": "./dist/index.cjs"
+      "default": "./dist/index.js"
    },
    "./preview": {
-      "types": "./dist/index.d.ts",
-      "import": "./dist/preview.js",
-      "require": "./dist/preview.cjs"
+      "types": "./dist/preview.d.ts",
+      "default": "./dist/preview.js"
    },
-    "./preset": "./dist/preset.cjs",
+    "./preset": "./dist/preset.js",
    "./manager": "./dist/manager.js",
    "./package.json": "./package.json"
  },

更新 tsconfig.json

tsconfig.json
{
  "compilerOptions": {
    // ...
-    "target": "es2023",
+    "target": "esnext",
    // ...
-    "lib": ["es2023", "dom", "dom.iterable"],
+    "lib": ["esnext", "dom", "dom.iterable"],
    // ...
-    "rootDir": "./src",
+    "rootDir": ".",
  },
-  "include": ["src/**/*"]
+  "include": ["src/**/*", "tsup.config.ts"]
 }

最后,将插件顶层目录下的 preset.js 文件更改为 ESM。该文件以前是 CJS,因为 Storybook Node 应用只支持 CJS。

preset.js
-// this file is slightly misleading. It needs to be CJS, and thus in this "type": "module" package it should be named preset.cjs
-// but Storybook won't pick that filename up so we have to name it preset.js instead
-
-module.exports = require('./dist/preset.cjs');
+export * from './dist/preset.js';

本地插件加载

由于插件现在仅限 ESM,您必须更改在开发 Storybook 实例中加载自己插件的方式。导入和导出现在必须遵循 ESM 语法,并且相对路径必须使用 import.meta.resolve

删除 .storybook/local-preset.cjs 并创建 .storybook/local-preset.ts,内容如下

.storybook/local-preset.ts
import { fileURLToPath } from "node:url";
 
export function previewAnnotations(entry = []) {
  return [...entry, fileURLToPath(import.meta.resolve("../dist/preview.js"))];
}
 
export function managerEntries(entry = []) {
  return [...entry, fileURLToPath(import.meta.resolve("../dist/manager.js"))];
}
 
export * from "../dist/preset.js";

接下来,更新您的 main.ts 以引用新的预设文件

.storybook/main.ts
- addons: ["@storybook/addon-docs", "./local-preset.cjs"],
+ addons: ["@storybook/addon-docs", import.meta.resolve("./local-preset.ts")],

可选更改

以下更改目前不是严格必需的,但我们建议进行这些更改以改善用户的体验。

CSF Factories 支持

要支持插件中的 CSF Factories 注释,您需要更新 src/index.ts 文件以使用新的 definePreviewAddon。此功能将成为 CSF Next 的一部分。强烈建议进行此更改,因为它将帮助您的用户从 CSF Factories 中获益。

使用 CSF Factories,用户可以链接他们的预览配置,并从更好的类型提示和更大的灵活性中受益。插件必须导出注释才能与此新语法兼容。CSF Factories 将是 Storybook 11 中编写故事的默认方式。

src/index.ts
- // make it work with --isolatedModules
- export default {};
+ import { definePreviewAddon } from "storybook/internal/csf";
+ import addonAnnotations from "./preview";
+ 
+ export default () => definePreviewAddon(addonAnnotations);

移除 exportEntries

package.jsonbundler 配置中的 exportEntries 属性曾用于从 src/index.ts 生成 index.js 构建输出。它与 Node.js 兼容,而不是严格与浏览器兼容。当插件作者在 index.js 中导出代码供 Storybook 预览或管理器使用时,此构建配置可能会导致细微的错误。

在 Storybook 10 addon-kit 中,我们从 bundler 配置中移除了 exportEntries,并将 src/index.ts 移至 previewEntries 的一部分。这样,从 src/index.ts 导出的任何代码都将为浏览器打包,并可与 CSF Next 一起使用。如果您需要导出其他要在预览中运行的代码(例如可选的装饰器),可以将其添加到 src/index.ts 中。

如果您需要导出供管理器使用的代码(例如侧边栏的 renderLabel 函数),您可以创建一个新的 src/manager-helpers.ts 文件并将其添加到 managerEntries,如下所示

package.json
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "default": "./dist/index.js"
    },
    "./preview": {
      "types": "./dist/preview.d.ts",
      "default": "./dist/preview.js"
    },
    "./preset": "./dist/preset.js",
    "./manager": "./dist/manager.js",
+    "./manager-helpers": "./dist/manager-helpers.js",
    "./package.json": "./package.json"
  },
  "bundler": {
    "managerEntries": [
+      "src/manager-helpers.ts",
      "src/manager.tsx"
    ]
  }

构建文件清理

我们建议在构建时移除旧的构建文件。这将避免您的 dist 文件夹因过期的 JS 块而不断增长。您可以将以下内容添加到您的 package.json 脚本中

package.json
{
  "scripts": {
+    "prebuild": "node -e \"fs.rmSync('./dist', { recursive: true, force: true })\"",
  }
}

包关键字更改

我们在 Storybook addon-kit 模板中更新了插件的默认关键字。

package.json
  "keywords": [
-    "storybook-addons",
+    "storybook-addon",
  ],

10.0.0 完全迁移指南

有关更改的完整列表,请访问 Migration.md 文件

迁移示例

有关已更新以支持 Storybook 10.0 的插件的完整示例,请参阅 Addon Kit 迁移 PR。合并后,它将演示 Storybook 10 所需的所有必要和推荐的更改。

发布

为了支持 Storybook 10.0,我们鼓励您发布新主要版本的插件。对于实验性功能或测试,请使用 next 标签。这使您能够在发布稳定版本之前收集反馈。

支持

如果您在遵循本指南后遇到插件问题,请在我们的 GitHub 存储库中开启一个新讨论,或在我们的 Discord 上的专门的插件开发者频道 #addons 与我们交流。