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,如果您想全年尝试最新的预发布版本。
{
"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,您可以在对等依赖项中指定更宽的版本范围
{
"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 的同时,发布新主要版本的插件。这种做法
- 使维护您的代码更容易
- 使您能够利用新功能和改进
- 为您的用户提供更清晰的升级路径
插件的关键更改
以下是 10.0 版本中影响插件开发的变化。
仅 ESM 构建
Storybook 10 要求所有插件都必须构建为仅 ESM。此更改简化了构建过程并减少了维护开销。您需要对 tsup.config.ts 进行许多更改,因此复制 addon-kit 仓库中的参考文件可能会更容易。
此更新带来了以下变化
- Node 目标从 Node 20.0 移至 Node 20.19
- 您不再需要构建 CJS 文件
- 您不再需要传递
globalManagerPackages和globalPreviewPackages package.json中的bundler配置不再需要手动输入- 我们建议您停止使用
exportEntries,并根据用户的使用情况,将导出的条目切换为previewEntries和managerEntries。
- 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 文件的提及。
"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。
{
"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。
-// 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,内容如下
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 以引用新的预设文件
- 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 中编写故事的默认方式。
- // make it work with --isolatedModules
- export default {};
+ import { definePreviewAddon } from "storybook/internal/csf";
+ import addonAnnotations from "./preview";
+
+ export default () => definePreviewAddon(addonAnnotations);移除 exportEntries
在 package.json 的 bundler 配置中的 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,如下所示
"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 脚本中
{
"scripts": {
+ "prebuild": "node -e \"fs.rmSync('./dist', { recursive: true, force: true })\"",
}
}包关键字更改
我们在 Storybook addon-kit 模板中更新了插件的默认关键字。
"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 与我们交流。
