npm package: @fleet-frontend/vite-plugin-upload-s3
Description: A universal plugin for uploading assets to AWS S3 (supports Vite and Webpack)
Author: Your Name
Latest version: 0.1.0

# @fleet-frontend/vite-plugin-upload-s3

一个通用的插件，用于自动将构建产物上传到 AWS S3 和兼容 S3 的服务。同时支持 **Vite** 和 **Webpack**。

## 特性

- 🚀 **自动上传**: 构建完成后自动将构建产物上传到 S3
- 🔧 **多构建工具支持**: 同时支持 Vite 和 Webpack
- ⚡ **S3 兼容**: 支持任何 S3 兼容的提供商（AWS、DigitalOcean Spaces 等）
- 🎯 **智能过滤**: 通过文件扩展名、自定义函数或模式过滤文件
- 📦 **灵活配置**: 支持 ES 模块和 CommonJS
- 🔧 **可定制**: 灵活的配置选项，适用于不同的使用场景
- 📊 **实时进度**: 实时显示上传进度，包括文件数量和百分比
- 🔄 **重试机制**: 失败上传自动重试，支持指数退避
- 🔒 **TypeScript**: 完整的 TypeScript 支持，包含类型定义
- 🌐 **多格式**: 支持 ESM 和 CommonJS 模块格式
- 📦 **压缩支持**: 自动处理 .gz、.br 和其他压缩文件

## 安装

```bash
pnpm add -D @fleet-frontend/vite-plugin-upload-s3
# 或
npm install -D @fleet-frontend/vite-plugin-upload-s3
# 或
yarn add -D @fleet-frontend/vite-plugin-upload-s3
```

**注意**: 此插件需要 `vite` (^4.0.0 || ^5.0.0) 或 `webpack` (^5.0.0) 作为 peer dependency。请安装你正在使用的构建工具：

```bash
# Vite 项目
pnpm add -D vite

# Webpack 项目
pnpm add -D webpack
```

## 快速开始

### Vite
```typescript
// vite.config.ts
import { ViteS3 } from '@fleet-frontend/vite-plugin-upload-s3'

export default {
  plugins: [
    ViteS3(true, {
      clientConfig: {
        credentials: {
          accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
          secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
        },
        region: 'us-east-1'
      },
      uploadOptions: {
        Bucket: 'your-bucket-name'
      }
    })
  ]
}
```

### Webpack
```javascript
// webpack.config.js
const { WebpackS3Plugin } = require('@fleet-frontend/vite-plugin-upload-s3')

module.exports = {
  plugins: [
    new WebpackS3Plugin(true, {
      clientConfig: {
        credentials: {
          accessKeyId: process.env.AWS_ACCESS_KEY_ID,
          secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
        },
        region: 'us-east-1'
      },
      uploadOptions: {
        Bucket: 'your-bucket-name'
      }
    })
  ]
}
```

## 基本使用

### Vite

```typescript
// vite.config.ts
import { defineConfig } from 'vite'
import { ViteS3 } from '@fleet-frontend/vite-plugin-upload-s3'
// 或使用统一入口
// import { UploadS3 } from '@fleet-frontend/vite-plugin-upload-s3'

export default defineConfig({
  plugins: [
    // 方式 1: 使用 ViteS3（推荐用于 Vite）
    ViteS3(!!process.env.UPLOAD_ENABLED, {
      basePath: '/build',
      clientConfig: {
        credentials: {
          accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
          secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
        },
        region: 'us-east-1'
      },
      uploadOptions: {
        Bucket: 'your-bucket-name'
      }
    })
    
    // 方式 2: 使用统一入口
    // UploadS3(!!process.env.UPLOAD_ENABLED, { ... }, 'vite')
  ]
})
```

### Webpack

```javascript
// webpack.config.js
const { UploadS3, WebpackS3Plugin } = require('@fleet-frontend/vite-plugin-upload-s3')

module.exports = {
  // ... 其他 webpack 配置
  plugins: [
    // 方式 1: 使用统一入口（推荐）
    UploadS3(!!process.env.UPLOAD_ENABLED, {
      basePath: '/build',
      clientConfig: {
        credentials: {
          accessKeyId: process.env.AWS_ACCESS_KEY_ID,
          secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
        },
        region: 'us-east-1'
      },
      uploadOptions: {
        Bucket: 'your-bucket-name'
      }
    }, 'webpack'),
    
    // 方式 2: 直接使用 WebpackS3Plugin
    // new WebpackS3Plugin(!!process.env.UPLOAD_ENABLED, { ... })
  ]
}
```

**注意**: Webpack 插件仅在生产模式下上传。开发构建会自动跳过。

## 配置

### 必需选项

| 选项 | 类型 | 描述 |
|--------|------|-------------|
| `clientConfig` | `S3ClientConfig` | S3Client 类构造函数的配置接口 |
| `uploadOptions` | `PutObjectRequest` | PutObjectRequest 选项（除了 Body 和 Key） |

### 可选选项

| 选项 | 类型 | 默认值 | 描述 |
|--------|------|---------|-------------|
| `basePath` | `string` | `null` | 上传文件在 S3 上的命名空间 |
| `directory` | `string` | `build.outDir` | 要上传的目录 |
| `include` | `string\|RegExp\|Function\|Array` | `null` | 用于匹配包含内容的模式 |
| `exclude` | `string\|RegExp\|Function\|Array` | `null` | 用于匹配排除内容的模式（支持文件扩展名如 `.map`、`.txt` 和 glob 模式如 `**/*.map`） |
| `sequentialUploads` | `boolean` | `false` | 如果为 true，文件将按顺序上传 |
| `retryAttempts` | `number` | `3` | 失败上传的重试次数 |
| `retryDelay` | `number` | `1000` | 重试尝试之间的延迟（毫秒） |
| `forceOverwrite` | `string\|RegExp\|Function\|Array` | `null` | 应强制覆盖的文件，无需检查它们是否已存在（支持文件扩展名、glob 模式和自定义函数） |
| `skipDirectoryPrefix` | `boolean` | `false` | 如果为 true，跳过目录名作为 S3 路径前缀（直接上传目录内容） |
| `failOnError` | `boolean` | `true` | 如果为 true，当文件上传失败时抛出错误并中断构建；如果为 false，上传失败时只记录警告，不中断构建 |

## 高级使用

```typescript
// vite.config.ts
import { defineConfig } from 'vite'
import { ViteS3 } from '@fleet-frontend/vite-plugin-upload-s3'

export default defineConfig({
  plugins: [
    ViteS3(!!process.env.UPLOAD_ENABLED, {
      basePath: '/cdn/assets',
      clientConfig: {
        credentials: {
          accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
          secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
        },
        region: 'us-west-2'
      },
      uploadOptions: {
        Bucket: 'your-bucket-name',
        CacheControl: 'public, max-age=31536000',
        ContentEncoding: 'gzip'
      },
      include: [
        /.*\.(css|js)$/,
        function(path) { 
          return path.includes('assets/') && !path.includes('test')
        }
      ],
      exclude: ["**/*.map", "**/*.txt", "**/*.log"], // 排除所有 .map, .txt, .log 文件
      forceOverwrite: ["index.html", "**/*.html"], // 强制覆盖所有 HTML 文件
      sequentialUploads: false,
    })
  ]
})
```

### DigitalOcean Spaces 示例

```typescript
// vite.config.ts
import { defineConfig } from 'vite'
import { ViteS3 } from '@fleet-frontend/vite-plugin-upload-s3'

export default defineConfig({
  plugins: [
    ViteS3(!!process.env.UPLOAD_ENABLED, {
      clientConfig: {
        credentials: {
          accessKeyId: process.env.DO_ACCESS_KEY_ID!,
          secretAccessKey: process.env.DO_SECRET_ACCESS_KEY!,
        },
        endpoint: 'https://fra1.digitaloceanspaces.com',
        region: 'fra1'
      },
      uploadOptions: {
        Bucket: 'your-bucket-name'
      }
    })
  ]
})
```

## 环境变量

将你的 AWS 凭证设置为环境变量：

```bash
export AWS_ACCESS_KEY_ID="your-access-key-id"
export AWS_SECRET_ACCESS_KEY="your-secret-access-key"
export UPLOAD_ENABLED="true"
```

如果在 EC2/ECS/Lambda 上运行，也可以使用 AWS IAM 角色。

## 统一入口函数

对于可能在不同构建工具之间切换的项目，你可以使用统一的 `UploadS3` 函数：

```typescript
import { UploadS3 } from '@fleet-frontend/vite-plugin-upload-s3'

// 用于 Vite
UploadS3(true, options, 'vite')

// 用于 Webpack
UploadS3(true, options, 'webpack')
```

## 模块支持

此插件同时支持 ES 模块和 CommonJS：

### ES 模块 (ESM)
```typescript
import { ViteS3, UploadS3, WebpackS3Plugin } from '@fleet-frontend/vite-plugin-upload-s3'
```

### CommonJS
```javascript
const { ViteS3, UploadS3, WebpackS3Plugin } = require('@fleet-frontend/vite-plugin-upload-s3')
```

## 目录路径控制

插件支持控制是否在 S3 路径中包含目录名作为前缀。当启用 `skipDirectoryPrefix` 时，只会上传指定目录内的文件和子目录，而不会在 S3 路径中包含目录名本身。

### 目录路径控制配置

```typescript
// vite.config.ts
import { defineConfig } from 'vite'
import { ViteS3 } from '@fleet-frontend/vite-plugin-upload-s3'

export default defineConfig({
  plugins: [
    ViteS3(true, {
      clientConfig: {
        credentials: {
          accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
          secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
        },
        region: 'us-east-1',
      },
      uploadOptions: {
        Bucket: 'your-bucket-name',
      },
      // 指定要上传的目录
      directory: 'dist',
      // 跳过目录名作为 S3 路径前缀
      skipDirectoryPrefix: true
    })
  ]
})
```

### 目录路径控制示例

#### 不使用 skipDirectoryPrefix（默认行为）
```
本地结构:
dist/
├── index.html
├── assets/
│   ├── style.css
│   └── app.js
└── images/
    └── logo.png

S3 结构:
your-bucket/
├── dist/index.html
├── dist/assets/style.css
├── dist/assets/app.js
└── dist/images/logo.png
```

#### 使用 skipDirectoryPrefix: true
```
本地结构:
dist/
├── index.html
├── assets/
│   ├── style.css
│   └── app.js
└── images/
    └── logo.png

S3 结构:
your-bucket/
├── index.html
├── assets/style.css
├── assets/app.js
└── images/logo.png
```

### 使用场景

- **静态网站托管**: 当你想要直接从 S3 存储桶的根目录提供文件时
- **CDN 分发**: 当你的 CDN 期望文件位于根级别时
- **简洁 URL**: 当你想要更简洁、更短的 URL，不包含构建目录名时
- **自定义路径结构**: 当你想要使用 `basePath` 来控制 S3 路径结构而不是目录名时

## 强制覆盖

插件支持强制覆盖特定文件，而无需检查它们是否已存在于 S3 上。这对于应该始终更新的文件（如 `index.html` 或其他关键文件）很有用。

### 强制覆盖配置

```typescript
// vite.config.ts
import { defineConfig } from 'vite'
import { ViteS3 } from '@fleet-frontend/vite-plugin-upload-s3'

export default defineConfig({
  plugins: [
    ViteS3(true, {
      clientConfig: {
        credentials: {
          accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
          secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
        },
        region: 'us-east-1',
      },
      uploadOptions: {
        Bucket: 'your-bucket-name',
      },
      // 强制覆盖特定文件
      forceOverwrite: [
        "index.html",           // 强制覆盖 index.html
        "**/*.html",           // 强制覆盖所有 HTML 文件
        /\.html$/,             // 强制覆盖以 .html 结尾的文件
        (path) => path.includes('critical') // 强制覆盖包含 'critical' 的文件
      ]
    })
  ]
})
```

### 强制覆盖模式类型

`forceOverwrite` 选项支持与 `include` 和 `exclude` 相同的模式类型：

#### 文件扩展名过滤
```typescript
forceOverwrite: [".html", ".css", ".js"] // 强制覆盖特定文件扩展名
```

#### Glob 模式过滤
```typescript
forceOverwrite: [
  "index.html",           // 强制覆盖 index.html
  "**/*.html",           // 强制覆盖所有 HTML 文件
  "dist/assets/*.js",    // 强制覆盖 dist/assets/ 中的 JS 文件
  "**/critical/**"       // 强制覆盖 critical 目录中的文件
]
```

#### 正则表达式过滤
```typescript
forceOverwrite: [/\.html$/, /index\./, /critical/] // 使用正则表达式
```

#### 函数过滤
```typescript
forceOverwrite: [
  (path) => path.includes('index'),
  (path) => path.endsWith('.html'),
  (path) => path.includes('critical')
]
```

#### 混合模式过滤
```typescript
forceOverwrite: [
  "index.html",                    // 文件名
  "**/*.html",                    // glob 模式
  /\.critical\./,                 // 正则表达式
  (path) => path.includes('temp') // 函数
]
```

## 文件过滤

插件支持使用多种模式类型进行灵活的文件过滤：

### 文件扩展名过滤
```typescript
exclude: [".map", ".txt", ".log"] // 排除特定扩展名的文件
```

### Glob 模式过滤
```typescript
exclude: [
  "**/*.map",           // 排除所有 .map 文件
  "**/test/**",         // 排除 test 目录下的所有文件
  "**/*.test.*",        // 排除测试文件
  "dist/assets/*.js"    // 排除特定路径下的 .js 文件
]
```

### 正则表达式过滤
```typescript
exclude: [/\.map$/, /\.txt$/, /test/] // 使用正则表达式
```

### 函数过滤
```typescript
exclude: [
  (path) => path.includes('test'),
  (path) => path.endsWith('.map')
]
```

### 混合模式过滤
```typescript
exclude: [
  ".map",                    // 文件扩展名
  "**/*.txt",               // glob 模式
  /\.log$/,                 // 正则表达式
  (path) => path.includes('temp') // 函数
]
```

## 错误处理配置

插件支持配置上传失败时的行为。默认情况下，当文件上传失败时，插件会抛出错误并中断构建流程。你可以通过 `failOnError` 选项来控制这个行为。

### 默认行为（failOnError: true）

当 `failOnError` 为 `true`（默认值）时，如果文件上传失败，构建流程会被中断：

```typescript
// vite.config.ts
import { defineConfig } from 'vite'
import { ViteS3 } from '@fleet-frontend/vite-plugin-upload-s3'

export default defineConfig({
  plugins: [
    ViteS3(true, {
      clientConfig: {
        credentials: {
          accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
          secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
        },
        region: 'us-east-1',
      },
      uploadOptions: {
        Bucket: 'your-bucket-name',
      },
      // failOnError 默认为 true，上传失败会中断构建
      // failOnError: true
    })
  ]
})
```

**注意**：文件已存在（跳过上传）的情况不算失败，不会中断构建。只有真正需要上传但上传失败的文件才会触发错误。

### 允许上传失败继续构建（failOnError: false）

如果你希望即使上传失败也继续构建流程，可以设置 `failOnError: false`：

```typescript
// vite.config.ts
import { defineConfig } from 'vite'
import { ViteS3 } from '@fleet-frontend/vite-plugin-upload-s3'

export default defineConfig({
  plugins: [
    ViteS3(true, {
      clientConfig: {
        credentials: {
          accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
          secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
        },
        region: 'us-east-1',
      },
      uploadOptions: {
        Bucket: 'your-bucket-name',
      },
      // 上传失败时不中断构建，只记录警告
      failOnError: false
    })
  ]
})
```

### 使用场景

- **failOnError: true（默认）**：适用于生产环境，确保所有文件都成功上传到 CDN
- **failOnError: false**：适用于开发环境或测试场景，允许构建继续完成，即使上传失败

## 插件顺序和压缩支持

### Vite

插件现在按照你在 Vite 配置中定义的顺序执行，使其能够与压缩插件正常工作：

```typescript
import { defineConfig } from 'vite'
import { ViteS3 } from '@fleet-frontend/vite-plugin-upload-s3'
import { compression } from 'vite-plugin-compression'

export default defineConfig({
  plugins: [
    // 其他插件...
    
    // 压缩插件 - 生成 .gz 文件
    compression({
      algorithm: 'gzip',
      ext: '.gz'
    }),
    
    // S3 上传插件 - 在压缩插件之后执行
    ViteS3(true, {
      clientConfig: {
        credentials: {
          accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
          secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
        },
        region: 'us-east-1',
      },
      uploadOptions: {
        Bucket: 'your-bucket-name',
      },
      // 包含压缩文件
      include: [/\.(js|css|html|gz|br)$/],
      // 排除源映射文件
      exclude: ["**/*.map"]
    })
  ]
})
```

### Webpack

对于 Webpack，插件在构建完成后自动运行。你可以将其与其他 Webpack 插件一起使用：

```javascript
const { WebpackS3Plugin } = require('@fleet-frontend/vite-plugin-upload-s3')
const CompressionPlugin = require('compression-webpack-plugin')

module.exports = {
  plugins: [
    // 压缩插件
    new CompressionPlugin({
      algorithm: 'gzip',
      test: /\.(js|css|html|svg)$/,
    }),
    
    // S3 上传插件 - 在构建完成后运行
    new WebpackS3Plugin(!!process.env.UPLOAD_ENABLED, {
      clientConfig: {
        credentials: {
          accessKeyId: process.env.AWS_ACCESS_KEY_ID,
          secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
        },
        region: 'us-east-1',
      },
      uploadOptions: {
        Bucket: 'your-bucket-name',
      },
      // 包含压缩文件
      include: [/\.(js|css|html|gz|br)$/],
      // 排除源映射文件
      exclude: ['**/*.map']
    })
  ]
}
```

## 进度显示

插件在上传过程中提供实时进度更新：

```
🚀 Uploading 15 files to S3...
[8/15] 53% ✅ 6 ❌ 2 | 📁 style.css
```

**进度信息:**
- `[8/15]` - 当前文件编号 / 总文件数
- `53%` - 完成百分比
- `✅ 6` - 成功上传的文件数
- `❌ 2` - 失败的上传数
- `📁 style.css` - 正在处理的当前文件

## 构建工具支持

此插件使用相同的配置选项同时支持 Vite 和 Webpack。主要区别在于如何导入和使用插件：

| 特性 | Vite | Webpack |
|---------|------|---------|
| **导入** | `import { ViteS3 } from '...'` | `const { WebpackS3Plugin } = require('...')` |
| **使用** | `ViteS3(enabled, options)` | `new WebpackS3Plugin(enabled, options)` |
| **统一入口** | `UploadS3(enabled, options, 'vite')` | `UploadS3(enabled, options, 'webpack')` |
| **构建钩子** | `closeBundle` | `done` |
| **开发模式** | 由 `enabled` 参数控制 | 自动跳过 |
| **输出检测** | 从 `config.build.outDir` | 从 `compiler.options.output.path` |

### Vite 支持

插件完全支持 Vite 的所有功能：
- 使用 Vite 的 `closeBundle` 钩子确保可靠的上传时机
- 自动从 Vite 配置中检测构建输出目录
- 与其他 Vite 插件无缝协作
- 尊重 `enabled` 参数进行条件上传

### Webpack 支持

插件支持 Webpack 5：
- 使用 Webpack 的 `done` 钩子在构建完成后上传
- 在开发模式下自动跳过上传（无论 `enabled` 参数如何）
- 从 Webpack 配置中读取输出目录
- 支持所有 Webpack 5 功能

**重要**: 使用 Webpack 时，请确保你使用的是 Webpack 5 或更高版本。不支持 Webpack 4。

## 示例

查看 `examples/` 目录以获取更详细的使用示例：

- [基本使用](./examples/basic-usage/) - 简单的 Vite 配置
- [高级使用](./examples/advanced-usage/) - 高级 Vite 功能和自定义

有关 Webpack 示例，请参阅上面的 [Webpack](#webpack) 部分中的使用模式。

## 开发

```bash
# 安装依赖
pnpm install

# 构建插件
pnpm build

# 监听模式
pnpm dev

# 代码检查
pnpm lint

# 类型检查
pnpm type-check
```

## 许可证

MIT

## 贡献

欢迎贡献！请随时提交 Pull Request。