You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何为子路径组件配置Rollup打包工具

问题描述

我是Rollup新手,希望打包组件以在多个项目中复用。我的文件结构如下:

src/
├─ Button/
│  ├─ Button.tsx
│  ├─ Button.css
│  ├─ GreenButton.tsx
│  ├─ GreenButton.css
│  ├─ BlueButton.tsx
│  └─ BlueButton.css
├─ Dropdown/
│  ├─ Dropdown.tsx
│  └─ Dropdown.css
└─ ...

我不想将所有组件打包到单个index.js中,目标是创建可通过my-package/Button方式导入的组件包。

我已尝试通过遍历文件生成Rollup入口的方案,但遇到以下问题:

  • JSX保留问题:设置jsx: "preserve"后,外部组件导入被错误修改,出现未使用警告;
  • 导入处理问题:不清楚如何配置外部库、内部组件的排除规则,是否需要重命名内部组件导入;
  • 子模块package.json生成:不知道如何为每个组件自动生成指定结构的package.json。

附当前rollup.config.js代码:

import typescript from "@rollup/plugin-typescript";
import jsx from 'acorn-jsx';
import postcss from "rollup-plugin-postcss";

import path from 'path';
import * as fs from 'fs';
import { fileURLToPath } from 'url';

const options = {
    inDir: "./src",
    outDir: "./dist",
    include: [/.*\\.*\.[jt]sx?$/],
    exclude: [/\.test\./, /\.d\.ts$/]
}

function getFiles(basePath, relPath, include, exclude){
    return fs.readdirSync(path.resolve(basePath, relPath), {withFileTypes: true}).reduce((list, element) => {
        const relElement = path.join(relPath, element.name);
        if(exclude && exclude.some(exclude => exclude.test(relElement))){
            return list;
        }
        if(element.isDirectory()){
            return list.concat(getFiles(basePath, relElement, include, exclude));
        }
        if(include.some(include => include.test(relElement))){
            return list.concat(relElement);
        };
        return list;
    },[]);
}
const __dirname = path.dirname(fileURLToPath(import.meta.url));


let modules = getFiles(path.resolve(__dirname, options.inDir), "./", options.include, options.exclude);

function convertExt(ext){
    switch(ext){
        case ".tsx": return ".jsx";
        case ".ts": return ".js";
        default: return ext;
    }
}

function createRollupEntry(module){
    let ext = path.extname(module);
    let file = path.basename(module, ext);
    let directory = path.dirname(module);
    let folder = path.basename(directory);

    let outputExt = convertExt(ext);

    let input = path.join(options.inDir, module);
    let output = path.join(options.outDir, directory, file === folder ? "" : "file", file + outputExt);

    let rollupConfig = {
        input: input,
        output: {
            file: output,
            format: "esm"
        },
        plugins: [
            postcss({
                extract: true
            })
        ],
        external: id => !/.css$/.test(id)
    }

    if(outputExt === ".jsx") {
        rollupConfig.acornInjectPlugins = [jsx()];
    }

    if(/^\.tsx?$/.test(ext)){
        rollupConfig.plugins.push(
            typescript({
                tsconfig: "./tsconfig.json",
                include: input,
                compilerOptions: {
                    jsx: "preserve",
                    declaration: true
                }
            })
        )
    }

    return rollupConfig;

}
 export default modules.filter((_, index) => index < 1).map(element => {
    let entry = createRollupEntry(element);
    console.log(entry);
    return entry;
});
解决方案

1. 修复JSX保留问题

设置jsx: "preserve"后,TypeScript不会转译JSX,但局部配置覆盖全局tsconfig会导致导入解析异常。调整以下两点:

  • 移除acornInjectPlugins手动配置,@rollup/plugin-typescript已内置JSX支持,无需额外注入;
  • 取消TypeScript插件的include: input配置,让插件基于全局tsconfig.json处理所有文件,避免局部规则破坏导入逻辑。

修改createRollupEntry中的TypeScript插件配置:

typescript({
    tsconfig: "./tsconfig.json",
    compilerOptions: {
        jsx: "preserve",
        declaration: true,
        declarationDir: path.join(options.outDir, directory) // 类型文件输出到对应组件目录
    }
})

同时在项目根目录tsconfig.json中补充配置:

{
  "compilerOptions": {
    "module": "ESNext",
    "target": "ESNext",
    "jsx": "preserve",
    "strict": true,
    "declaration": true,
    "moduleResolution": "NodeNext"
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}

2. 配置外部库与内部组件导入规则

外部库处理

external配置需要区分外部依赖和内部组件:

  • 匹配react、react-dom等第三方包名,标记为外部;
  • 内部组件用相对导入,无需标记为外部,Rollup会自动处理。

修改rollupConfig中的external:

external: id => 
  // 匹配React类第三方包
  /^react($|\/)/.test(id) || 
  /^react-dom($|\/)/.test(id) ||
  // 排除内部相对/绝对路径导入
  !id.startsWith('.') && !path.isAbsolute(id)

内部组件导入处理

内部组件直接使用相对路径导入(如import Button from './Button')即可,不需要重命名。Rollup打包时会保留这些路径,确保子模块间引用正常。

3. 自动生成子模块package.json

要实现my-package/Button的导入方式,需在每个组件目录下生成package.json指定入口。可以通过Rollup自定义插件实现:

在rollup.config.js中添加生成插件:

function createPackageJsonPlugin(directory, fileName) {
  return {
    name: 'generate-package-json',
    writeBundle() {
      const componentName = path.basename(directory);
      const packageJsonContent = {
        name: `my-package/${componentName}`,
        main: `${fileName}.jsx`,
        module: `${fileName}.jsx`,
        types: `${fileName}.d.ts`
      };
      const packageJsonPath = path.join(options.outDir, directory, 'package.json');
      fs.writeFileSync(packageJsonPath, JSON.stringify(packageJsonContent, null, 2));
    }
  };
}

然后在rollupConfig.plugins中引入该插件:

rollupConfig.plugins.push(createPackageJsonPlugin(directory, file));

完整修改后的rollup.config.js

import typescript from "@rollup/plugin-typescript";
import postcss from "rollup-plugin-postcss";
import path from 'path';
import * as fs from 'fs';
import { fileURLToPath } from 'url';

const options = {
    inDir: "./src",
    outDir: "./dist",
    include: [/.*\.[jt]sx?$/],
    exclude: [/\.test\./, /\.d\.ts$/]
}

function getFiles(basePath, relPath, include, exclude){
    return fs.readdirSync(path.resolve(basePath, relPath), {withFileTypes: true}).reduce((list, element) => {
        const relElement = path.join(relPath, element.name);
        if(exclude && exclude.some(exclude => exclude.test(relElement))){
            return list;
        }
        if(element.isDirectory()){
            return list.concat(getFiles(basePath, relElement, include, exclude));
        }
        if(include.some(include => include.test(relElement))){
            return list.concat(relElement);
        };
        return list;
    },[]);
}

const __dirname = path.dirname(fileURLToPath(import.meta.url));
let modules = getFiles(path.resolve(__dirname, options.inDir), "./", options.include, options.exclude);

function convertExt(ext){
    switch(ext){
        case ".tsx": return ".jsx";
        case ".ts": return ".js";
        default: return ext;
    }
}

function createPackageJsonPlugin(directory, fileName) {
  return {
    name: 'generate-package-json',
    writeBundle() {
      const componentName = path.basename(directory);
      const packageJsonContent = {
        name: `my-package/${componentName}`,
        main: `${fileName}.jsx`,
        module: `${fileName}.jsx`,
        types: `${fileName}.d.ts`
      };
      const packageJsonPath = path.join(options.outDir, directory, 'package.json');
      fs.writeFileSync(packageJsonPath, JSON.stringify(packageJsonContent, null, 2));
    }
  };
}

function createRollupEntry(module){
    let ext = path.extname(module);
    let file = path.basename(module, ext);
    let directory = path.dirname(module);

    let outputExt = convertExt(ext);
    let input = path.join(options.inDir, module);
    let output = path.join(options.outDir, directory, file + outputExt);

    let rollupConfig = {
        input: input,
        output: {
            file: output,
            format: "esm"
        },
        plugins: [
            postcss({
                extract: true,
                filename: `${file}.css`
            }),
            createPackageJsonPlugin(directory, file)
        ],
        external: id => 
          /^react($|\/)/.test(id) || 
          /^react-dom($|\/)/.test(id) ||
          !id.startsWith('.') && !path.isAbsolute(id)
    }

    if(/^\.tsx?$/.test(ext)){
        rollupConfig.plugins.push(
            typescript({
                tsconfig: "./tsconfig.json",
                compilerOptions: {
                    jsx: "preserve",
                    declaration: true,
                    declarationDir: path.join(options.outDir, directory)
                }
            })
        )
    }

    return rollupConfig;

}

export default modules.map(element => createRollupEntry(element));

内容的提问来源于stack exchange,提问作者Nico Richter

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.29 15:45:06