@nanyujun/rum-vite-plugin-sourcemap 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of @nanyujun/rum-vite-plugin-sourcemap might be problematic. Click here for more details.

package/README.md ADDED
@@ -0,0 +1,144 @@
1
+ # @nanyujun/rum-vite-plugin-sourcemap
2
+
3
+ RUM(前端性能监控平台)的 **Vite 插件**,在 `vite build` 完成后自动把产物中的 `*.map` Source Map 上传到 RUM collector,无需手动跑 CLI。上传后线上报错即可在 RUM 后台反混淆。
4
+
5
+ ## 安装
6
+
7
+ ```bash
8
+ npm i -D @nanyujun/rum-vite-plugin-sourcemap
9
+ ```
10
+
11
+ ## 用法
12
+
13
+ ```ts
14
+ import { defineConfig } from 'vite'
15
+ import { rumSourcemap } from '@nanyujun/rum-vite-plugin-sourcemap'
16
+
17
+ export default defineConfig({
18
+ build: {
19
+ sourcemap: 'hidden', // 必须:生成但不随产物发布 .map,避免源码泄漏
20
+ },
21
+ plugins: [
22
+ rumSourcemap({
23
+ app: 'your-app-id',
24
+ release: '1.0.0',
25
+ endpoint: 'https://rum.example.com',
26
+ token: process.env.RUM_ADMIN_TOKEN,
27
+ // distDir: 'dist', // 可选,默认 build.outDir
28
+ // concurrency: 4, // 可选,上传并发
29
+ // disabled: false, // 可选,强制开/关
30
+ // cleanupAfterUpload: false, // 可选,默认 false;true 时上传成功后删除本地 *.map 避免产物残留
31
+ }),
32
+ ],
33
+ })
34
+ ```
35
+
36
+ 上传成功后如需自动删除构建目录中的 `*.map`(减少产物体积、避免误部署),开启 `cleanupAfterUpload`:
37
+
38
+ ```ts
39
+ rumSourcemap({
40
+ app: 'your-app-id',
41
+ release: '1.0.0',
42
+ endpoint: 'https://rum.example.com',
43
+ token: process.env.RUM_ADMIN_TOKEN,
44
+ cleanupAfterUpload: true,
45
+ })
46
+ ```
47
+
48
+ ## 参数
49
+
50
+ | 参数 | 必填 | 说明 |
51
+ | --- | --- | --- |
52
+ | app | 是 | RUM 应用 ID |
53
+ | release | 是 | 版本号,必须与前端 SDK `init({ release })` 一致 |
54
+ | endpoint | 是 | collector 基地址(自动拼 `/admin/sourcemaps`) |
55
+ | token | 是 | `Authorization: Bearer <token>`,缺失则跳过上传 |
56
+ | distDir | 否 | 默认 `config.build.outDir` |
57
+ | concurrency | 否 | 上传并发数 |
58
+ | disabled | 否 | 覆盖默认的「仅 production 启用」策略 |
59
+ | cleanupAfterUpload | 否 | 默认 `false`;上传成功后删除本地 `*.map` 文件,避免产物残留 |
60
+
61
+ ## 注意事项
62
+
63
+ - 默认仅在 `NODE_ENV=production` 触发上传,可用 `disabled: false` 强制开启
64
+ - `build.sourcemap` 必须设为 `'hidden'`,否则 `.map` 会随产物部署到 CDN,造成源码泄漏
65
+ - `release` 必须与前端 SDK 上报错误时携带的 `release` 一致,否则后台无法匹配对应 map
66
+
67
+ ## Webpack 用法
68
+
69
+ Webpack 项目推荐直接使用专用插件 [`@nanyujun/rum-webpack-plugin-sourcemap`](https://www.npmjs.com/package/@nanyujun/rum-webpack-plugin-sourcemap),行为与 Vite 插件一致(`afterEmit` 阶段上传、支持 `cleanupAfterUpload`):
70
+
71
+ ```js
72
+ // webpack.config.js
73
+ const { RumSourcemapPlugin } = require('@nanyujun/rum-webpack-plugin-sourcemap')
74
+
75
+ module.exports = {
76
+ mode: 'production',
77
+ devtool: 'hidden-source-map', // 生成但不注入 sourceMappingURL,避免源码泄漏
78
+ plugins: [
79
+ new RumSourcemapPlugin({
80
+ app: 'your-app-id',
81
+ release: '1.0.0',
82
+ endpoint: 'https://rum.example.com',
83
+ token: process.env.RUM_ADMIN_TOKEN,
84
+ cleanupAfterUpload: true,
85
+ }),
86
+ ],
87
+ }
88
+ ```
89
+
90
+ 如果你暂时不想额外安装插件,也可以直接用底层 CLI 的 `uploadDir` Node API 挂到 `compiler.hooks.afterEmit`(等价实现):
91
+
92
+ ```js
93
+ // webpack.config.js
94
+ const { uploadDir } = require('@nanyujun/rum-sourcemap-cli')
95
+ const path = require('node:path')
96
+
97
+ // 上传完成后删除本地 *.map(对应 cleanupAfterUpload: true)
98
+ async function removeMaps(distDir) {
99
+ const { readdir } = require('node:fs/promises')
100
+ const walk = async (dir) => {
101
+ for (const entry of await readdir(dir, { withFileTypes: true })) {
102
+ const p = path.join(dir, entry.name)
103
+ if (entry.isDirectory()) await walk(p)
104
+ else if (entry.name.endsWith('.map')) require('node:fs').unlinkSync(p)
105
+ }
106
+ }
107
+ await walk(distDir)
108
+ }
109
+
110
+ class RumSourcemapPlugin {
111
+ constructor(options) { this.options = options }
112
+ apply(compiler) {
113
+ compiler.hooks.afterEmit.tapPromise('RumSourcemapPlugin', async () => {
114
+ const distDir = this.options.distDir ?? compiler.options.output.path
115
+ await uploadDir({
116
+ appId: this.options.app,
117
+ release: this.options.release,
118
+ endpoint: this.options.endpoint,
119
+ token: this.options.token,
120
+ distDir,
121
+ ...(this.options.concurrency !== undefined && { concurrency: this.options.concurrency }),
122
+ })
123
+ if (this.options.cleanupAfterUpload) await removeMaps(distDir)
124
+ })
125
+ }
126
+ }
127
+
128
+ module.exports = {
129
+ devtool: 'hidden-source-map',
130
+ plugins: [
131
+ new RumSourcemapPlugin({
132
+ app: 'your-app-id',
133
+ release: '1.0.0',
134
+ endpoint: 'https://rum.example.com',
135
+ token: process.env.RUM_ADMIN_TOKEN,
136
+ cleanupAfterUpload: true,
137
+ }),
138
+ ],
139
+ }
140
+ ```
141
+
142
+ ## 相关
143
+
144
+ - `@nanyujun/rum-sourcemap-cli`:底层上传 CLI / Node API
package/dist/index.cjs CHANGED
@@ -24,137 +24,8 @@ __export(index_exports, {
24
24
  });
25
25
  module.exports = __toCommonJS(index_exports);
26
26
  var import_node_path = require("path");
27
-
28
- // ../sourcemap-cli/dist/index.js
29
27
  var import_promises = require("fs/promises");
30
- var import_path = require("path");
31
- var SourceMapUploadError = class extends Error {
32
- constructor(message, status) {
33
- super(message);
34
- this.name = "SourceMapUploadError";
35
- this.status = status;
36
- }
37
- };
38
- var DEFAULT_RETRIES = 3;
39
- var DEFAULT_RETRY_DELAY_MS = 1e3;
40
- var DEFAULT_CONCURRENCY = 4;
41
- function sourceMapUrl(endpoint) {
42
- return `${endpoint.replace(/\/+$/, "")}/admin/sourcemaps`;
43
- }
44
- function sleep(ms) {
45
- if (ms <= 0) return Promise.resolve();
46
- return new Promise((resolve2) => setTimeout(resolve2, ms));
47
- }
48
- function retryDelay(baseMs, retryIndex) {
49
- return baseMs * 2 ** retryIndex;
50
- }
51
- async function responseMessage(response) {
52
- try {
53
- const body = await response.json();
54
- if (typeof body.error === "string" && body.error.length > 0) return body.error;
55
- } catch {
56
- }
57
- return response.statusText || `HTTP ${response.status}`;
58
- }
59
- function isRetryable(status) {
60
- return status >= 500 || status === 408 || status === 429;
61
- }
62
- async function readUploadResult(response) {
63
- const body = await response.json();
64
- return {
65
- id: Number(body.id),
66
- sha256: String(body.sha256),
67
- sizeBytes: Number(body.sizeBytes)
68
- };
69
- }
70
- async function uploadFile(options) {
71
- const fetcher = options.fetchImpl ?? fetch;
72
- const retries = options.retries ?? DEFAULT_RETRIES;
73
- const retryDelayMs = options.retryDelayMs ?? DEFAULT_RETRY_DELAY_MS;
74
- let lastError;
75
- for (let attempt = 0; attempt <= retries; attempt++) {
76
- try {
77
- const content = await (0, import_promises.readFile)(options.mapPath);
78
- const form = new FormData();
79
- form.set("appId", options.appId);
80
- form.set("release", options.release);
81
- form.set("file", options.file);
82
- form.set("map", new Blob([new Uint8Array(content)]), (0, import_path.basename)(options.mapPath));
83
- const response = await fetcher(sourceMapUrl(options.endpoint), {
84
- method: "POST",
85
- headers: { Authorization: `Bearer ${options.token}` },
86
- body: form
87
- });
88
- if (response.ok) return readUploadResult(response);
89
- const message = await responseMessage(response);
90
- if (!isRetryable(response.status) || attempt >= retries) {
91
- throw new SourceMapUploadError(message, response.status);
92
- }
93
- lastError = new SourceMapUploadError(message, response.status);
94
- } catch (err) {
95
- if (err instanceof SourceMapUploadError && !isRetryable(err.status ?? 0)) throw err;
96
- lastError = err;
97
- if (attempt >= retries) break;
98
- }
99
- await sleep(retryDelay(retryDelayMs, attempt));
100
- }
101
- if (lastError instanceof Error) throw lastError;
102
- throw new SourceMapUploadError("source map upload failed");
103
- }
104
- async function collectMapFiles(dir) {
105
- const files = [];
106
- const entries = await (0, import_promises.opendir)(dir);
107
- for await (const entry of entries) {
108
- const path = (0, import_path.join)(dir, entry.name);
109
- if (entry.isDirectory()) {
110
- files.push(...await collectMapFiles(path));
111
- } else if (entry.isFile() && entry.name.endsWith(".map")) {
112
- files.push(path);
113
- }
114
- }
115
- return files.sort();
116
- }
117
- function toSourceFile(distDir, mapPath) {
118
- return (0, import_path.relative)(distDir, mapPath).split("\\").join("/").replace(/\.map$/, "");
119
- }
120
- async function runPool(items, concurrency, worker) {
121
- const results = [];
122
- let cursor = 0;
123
- const workerCount = Math.max(1, Math.min(concurrency, items.length));
124
- await Promise.all(
125
- Array.from({ length: workerCount }, async () => {
126
- for (; ; ) {
127
- const index = cursor++;
128
- const item = items[index];
129
- if (item === void 0) break;
130
- results[index] = await worker(item);
131
- }
132
- })
133
- );
134
- return results;
135
- }
136
- async function uploadDir(options) {
137
- const mapPaths = await collectMapFiles(options.distDir);
138
- const concurrency = options.concurrency ?? DEFAULT_CONCURRENCY;
139
- const uploaded = await runPool(mapPaths, concurrency, async (mapPath) => {
140
- const file = toSourceFile(options.distDir, mapPath);
141
- const result = await uploadFile({
142
- appId: options.appId,
143
- release: options.release,
144
- endpoint: options.endpoint,
145
- token: options.token,
146
- file,
147
- mapPath,
148
- retries: options.retries,
149
- retryDelayMs: options.retryDelayMs,
150
- fetchImpl: options.fetchImpl
151
- });
152
- return { ...result, file, mapPath };
153
- });
154
- return { uploaded };
155
- }
156
-
157
- // src/index.ts
28
+ var import_rum_sourcemap_cli = require("@nanyujun/rum-sourcemap-cli");
158
29
  function isEnabled(options) {
159
30
  if (options.disabled !== void 0) return !options.disabled;
160
31
  return process.env.NODE_ENV === "production";
@@ -180,7 +51,8 @@ function rumSourcemap(options) {
180
51
  config.logger.warn("[rum-sourcemap] token is missing; skip source map upload.");
181
52
  return;
182
53
  }
183
- await uploadDir({
54
+ const logger = config.logger;
55
+ const result = await (0, import_rum_sourcemap_cli.uploadDir)({
184
56
  appId: options.app,
185
57
  release: options.release,
186
58
  endpoint: options.endpoint,
@@ -188,6 +60,23 @@ function rumSourcemap(options) {
188
60
  distDir: resolveDistDir(config, options.distDir),
189
61
  ...options.concurrency !== void 0 && { concurrency: options.concurrency }
190
62
  });
63
+ if (!options.cleanupAfterUpload) return;
64
+ let removed = 0;
65
+ let failed = 0;
66
+ await Promise.allSettled(
67
+ result.uploaded.map(async (item) => {
68
+ try {
69
+ await (0, import_promises.unlink)(item.mapPath);
70
+ removed += 1;
71
+ } catch (err) {
72
+ failed += 1;
73
+ logger.warn(`[rum-sourcemap] failed to remove ${item.mapPath}: ${String(err)}`);
74
+ }
75
+ })
76
+ );
77
+ logger.info(
78
+ `[rum-sourcemap] cleanupAfterUpload: removed ${removed} .map file(s)` + (failed > 0 ? `, ${failed} failed` : "")
79
+ );
191
80
  }
192
81
  };
193
82
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../../sourcemap-cli/src/index.ts"],"sourcesContent":["import { isAbsolute, resolve } from 'node:path';\nimport type { Plugin, ResolvedConfig } from 'vite';\nimport { uploadDir } from '@rum/sourcemap-cli';\n\nexport interface RumSourcemapPluginOptions {\n app: string;\n release: string;\n endpoint: string;\n token?: string;\n distDir?: string;\n concurrency?: number;\n disabled?: boolean;\n}\n\nfunction isEnabled(options: RumSourcemapPluginOptions): boolean {\n if (options.disabled !== undefined) return !options.disabled;\n return process.env.NODE_ENV === 'production';\n}\n\nfunction resolveDistDir(config: ResolvedConfig, distDir: string | undefined): string {\n const dir = distDir ?? config.build.outDir;\n return isAbsolute(dir) ? dir : resolve(config.root, dir);\n}\n\nexport function rumSourcemap(options: RumSourcemapPluginOptions): Plugin {\n let config: ResolvedConfig | undefined;\n\n return {\n name: 'rum-sourcemap',\n apply: 'build',\n\n configResolved(resolved) {\n config = resolved;\n if (isEnabled(options) && resolved.build.sourcemap !== 'hidden') {\n resolved.logger.warn(\"[rum-sourcemap] build.sourcemap should be 'hidden' to avoid publishing .map files.\");\n }\n },\n\n async closeBundle() {\n if (!config || config.command !== 'build' || !isEnabled(options)) return;\n if (!options.token) {\n config.logger.warn('[rum-sourcemap] token is missing; skip source map upload.');\n return;\n }\n\n await uploadDir({\n appId: options.app,\n release: options.release,\n endpoint: options.endpoint,\n token: options.token,\n distDir: resolveDistDir(config, options.distDir),\n ...(options.concurrency !== undefined && { concurrency: options.concurrency }),\n });\n },\n };\n}\n","import { opendir, readFile } from 'node:fs/promises';\nimport { basename, join, relative } from 'node:path';\n\nexport interface UploadFileResult {\n id: number;\n sha256: string;\n sizeBytes: number;\n}\n\nexport interface UploadFileOptions {\n appId: string;\n release: string;\n endpoint: string;\n token: string;\n file: string;\n mapPath: string;\n retries?: number;\n retryDelayMs?: number;\n fetchImpl?: typeof fetch;\n}\n\nexport interface UploadDirOptions {\n appId: string;\n release: string;\n endpoint: string;\n token: string;\n distDir: string;\n concurrency?: number;\n retries?: number;\n retryDelayMs?: number;\n fetchImpl?: typeof fetch;\n}\n\nexport interface UploadedSourceMap extends UploadFileResult {\n file: string;\n mapPath: string;\n}\n\nexport interface UploadDirResult {\n uploaded: UploadedSourceMap[];\n}\n\nexport interface ParsedUploadCommand {\n command: 'upload';\n appId: string;\n release: string;\n distDir: string;\n endpoint: string;\n token: string;\n concurrency?: number;\n}\n\nexport class SourceMapUploadError extends Error {\n status?: number;\n\n constructor(message: string, status?: number) {\n super(message);\n this.name = 'SourceMapUploadError';\n this.status = status;\n }\n}\n\nexport class SourceMapCliUsageError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'SourceMapCliUsageError';\n }\n}\n\nconst DEFAULT_RETRIES = 3;\nconst DEFAULT_RETRY_DELAY_MS = 1000;\nconst DEFAULT_CONCURRENCY = 4;\n\nfunction sourceMapUrl(endpoint: string): string {\n return `${endpoint.replace(/\\/+$/, '')}/admin/sourcemaps`;\n}\n\nfunction sleep(ms: number): Promise<void> {\n if (ms <= 0) return Promise.resolve();\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\nfunction retryDelay(baseMs: number, retryIndex: number): number {\n return baseMs * 2 ** retryIndex;\n}\n\nasync function responseMessage(response: Response): Promise<string> {\n try {\n const body = (await response.json()) as { error?: unknown };\n if (typeof body.error === 'string' && body.error.length > 0) return body.error;\n } catch {\n // Ignore malformed error bodies; status text is enough for CLI output.\n }\n return response.statusText || `HTTP ${response.status}`;\n}\n\nfunction isRetryable(status: number): boolean {\n return status >= 500 || status === 408 || status === 429;\n}\n\nasync function readUploadResult(response: Response): Promise<UploadFileResult> {\n const body = (await response.json()) as Partial<UploadFileResult>;\n return {\n id: Number(body.id),\n sha256: String(body.sha256),\n sizeBytes: Number(body.sizeBytes),\n };\n}\n\nexport async function uploadFile(options: UploadFileOptions): Promise<UploadFileResult> {\n const fetcher = options.fetchImpl ?? fetch;\n const retries = options.retries ?? DEFAULT_RETRIES;\n const retryDelayMs = options.retryDelayMs ?? DEFAULT_RETRY_DELAY_MS;\n let lastError: unknown;\n\n for (let attempt = 0; attempt <= retries; attempt++) {\n try {\n const content = await readFile(options.mapPath);\n const form = new FormData();\n form.set('appId', options.appId);\n form.set('release', options.release);\n form.set('file', options.file);\n form.set('map', new Blob([new Uint8Array(content)]), basename(options.mapPath));\n\n const response = await fetcher(sourceMapUrl(options.endpoint), {\n method: 'POST',\n headers: { Authorization: `Bearer ${options.token}` },\n body: form,\n });\n\n if (response.ok) return readUploadResult(response);\n const message = await responseMessage(response);\n if (!isRetryable(response.status) || attempt >= retries) {\n throw new SourceMapUploadError(message, response.status);\n }\n lastError = new SourceMapUploadError(message, response.status);\n } catch (err) {\n if (err instanceof SourceMapUploadError && !isRetryable(err.status ?? 0)) throw err;\n lastError = err;\n if (attempt >= retries) break;\n }\n\n await sleep(retryDelay(retryDelayMs, attempt));\n }\n\n if (lastError instanceof Error) throw lastError;\n throw new SourceMapUploadError('source map upload failed');\n}\n\nasync function collectMapFiles(dir: string): Promise<string[]> {\n const files: string[] = [];\n const entries = await opendir(dir);\n for await (const entry of entries) {\n const path = join(dir, entry.name);\n if (entry.isDirectory()) {\n files.push(...(await collectMapFiles(path)));\n } else if (entry.isFile() && entry.name.endsWith('.map')) {\n files.push(path);\n }\n }\n return files.sort();\n}\n\nfunction toSourceFile(distDir: string, mapPath: string): string {\n return relative(distDir, mapPath).split('\\\\').join('/').replace(/\\.map$/, '');\n}\n\nasync function runPool<T, R>(\n items: T[],\n concurrency: number,\n worker: (item: T) => Promise<R>,\n): Promise<R[]> {\n const results: R[] = [];\n let cursor = 0;\n const workerCount = Math.max(1, Math.min(concurrency, items.length));\n\n await Promise.all(\n Array.from({ length: workerCount }, async () => {\n for (;;) {\n const index = cursor++;\n const item = items[index];\n if (item === undefined) break;\n results[index] = await worker(item);\n }\n }),\n );\n\n return results;\n}\n\nexport async function uploadDir(options: UploadDirOptions): Promise<UploadDirResult> {\n const mapPaths = await collectMapFiles(options.distDir);\n const concurrency = options.concurrency ?? DEFAULT_CONCURRENCY;\n const uploaded = await runPool(mapPaths, concurrency, async (mapPath) => {\n const file = toSourceFile(options.distDir, mapPath);\n const result = await uploadFile({\n appId: options.appId,\n release: options.release,\n endpoint: options.endpoint,\n token: options.token,\n file,\n mapPath,\n retries: options.retries,\n retryDelayMs: options.retryDelayMs,\n fetchImpl: options.fetchImpl,\n });\n return { ...result, file, mapPath };\n });\n return { uploaded };\n}\n\nfunction readFlag(argv: string[], flag: string): string | undefined {\n const index = argv.indexOf(flag);\n if (index === -1) return undefined;\n return argv[index + 1];\n}\n\nfunction required(value: string | undefined, flag: string): string {\n if (!value) throw new SourceMapCliUsageError(`missing ${flag}`);\n return value;\n}\n\nexport function parseCliArgs(argv: string[]): ParsedUploadCommand {\n const command = argv[0];\n if (command !== 'upload') {\n throw new SourceMapCliUsageError('usage: rum-sourcemap upload --app <appId> --release <release> --dist <dir> --endpoint <url> --token <token>');\n }\n\n const concurrencyRaw = readFlag(argv, '--concurrency');\n return {\n command: 'upload',\n appId: required(readFlag(argv, '--app') ?? readFlag(argv, '--app-id'), '--app'),\n release: required(readFlag(argv, '--release'), '--release'),\n distDir: required(readFlag(argv, '--dist'), '--dist'),\n endpoint: required(readFlag(argv, '--endpoint'), '--endpoint'),\n token: required(readFlag(argv, '--token'), '--token'),\n ...(concurrencyRaw !== undefined && { concurrency: Number(concurrencyRaw) }),\n };\n}\n\nexport async function runCli(argv: string[], log: (message: string) => void = console.warn) {\n const command = parseCliArgs(argv);\n const result = await uploadDir(command);\n log(`uploaded ${result.uploaded.length} source maps`);\n return result;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,uBAAoC;;;ACApC,sBAAkC;AAClC,kBAAyC;AAmDlC,IAAM,uBAAN,cAAmC,MAAM;EAG9C,YAAY,SAAiB,QAAiB;AAC5C,UAAM,OAAO;AACb,SAAK,OAAO;AACZ,SAAK,SAAS;EAChB;AACF;AASA,IAAM,kBAAkB;AACxB,IAAM,yBAAyB;AAC/B,IAAM,sBAAsB;AAE5B,SAAS,aAAa,UAA0B;AAC9C,SAAO,GAAG,SAAS,QAAQ,QAAQ,EAAE,CAAC;AACxC;AAEA,SAAS,MAAM,IAA2B;AACxC,MAAI,MAAM,EAAG,QAAO,QAAQ,QAAQ;AACpC,SAAO,IAAI,QAAQ,CAACA,aAAY,WAAWA,UAAS,EAAE,CAAC;AACzD;AAEA,SAAS,WAAW,QAAgB,YAA4B;AAC9D,SAAO,SAAS,KAAK;AACvB;AAEA,eAAe,gBAAgB,UAAqC;AAClE,MAAI;AACF,UAAM,OAAQ,MAAM,SAAS,KAAK;AAClC,QAAI,OAAO,KAAK,UAAU,YAAY,KAAK,MAAM,SAAS,EAAG,QAAO,KAAK;EAC3E,QAAQ;EAER;AACA,SAAO,SAAS,cAAc,QAAQ,SAAS,MAAM;AACvD;AAEA,SAAS,YAAY,QAAyB;AAC5C,SAAO,UAAU,OAAO,WAAW,OAAO,WAAW;AACvD;AAEA,eAAe,iBAAiB,UAA+C;AAC7E,QAAM,OAAQ,MAAM,SAAS,KAAK;AAClC,SAAO;IACL,IAAI,OAAO,KAAK,EAAE;IAClB,QAAQ,OAAO,KAAK,MAAM;IAC1B,WAAW,OAAO,KAAK,SAAS;EAClC;AACF;AAEA,eAAsB,WAAW,SAAuD;AACtF,QAAM,UAAU,QAAQ,aAAa;AACrC,QAAM,UAAU,QAAQ,WAAW;AACnC,QAAM,eAAe,QAAQ,gBAAgB;AAC7C,MAAI;AAEJ,WAAS,UAAU,GAAG,WAAW,SAAS,WAAW;AACnD,QAAI;AACF,YAAM,UAAU,UAAM,0BAAS,QAAQ,OAAO;AAC9C,YAAM,OAAO,IAAI,SAAS;AAC1B,WAAK,IAAI,SAAS,QAAQ,KAAK;AAC/B,WAAK,IAAI,WAAW,QAAQ,OAAO;AACnC,WAAK,IAAI,QAAQ,QAAQ,IAAI;AAC7B,WAAK,IAAI,OAAO,IAAI,KAAK,CAAC,IAAI,WAAW,OAAO,CAAC,CAAC,OAAG,sBAAS,QAAQ,OAAO,CAAC;AAE9E,YAAM,WAAW,MAAM,QAAQ,aAAa,QAAQ,QAAQ,GAAG;QAC7D,QAAQ;QACR,SAAS,EAAE,eAAe,UAAU,QAAQ,KAAK,GAAG;QACpD,MAAM;MACR,CAAC;AAED,UAAI,SAAS,GAAI,QAAO,iBAAiB,QAAQ;AACjD,YAAM,UAAU,MAAM,gBAAgB,QAAQ;AAC9C,UAAI,CAAC,YAAY,SAAS,MAAM,KAAK,WAAW,SAAS;AACvD,cAAM,IAAI,qBAAqB,SAAS,SAAS,MAAM;MACzD;AACA,kBAAY,IAAI,qBAAqB,SAAS,SAAS,MAAM;IAC/D,SAAS,KAAK;AACZ,UAAI,eAAe,wBAAwB,CAAC,YAAY,IAAI,UAAU,CAAC,EAAG,OAAM;AAChF,kBAAY;AACZ,UAAI,WAAW,QAAS;IAC1B;AAEA,UAAM,MAAM,WAAW,cAAc,OAAO,CAAC;EAC/C;AAEA,MAAI,qBAAqB,MAAO,OAAM;AACtC,QAAM,IAAI,qBAAqB,0BAA0B;AAC3D;AAEA,eAAe,gBAAgB,KAAgC;AAC7D,QAAM,QAAkB,CAAC;AACzB,QAAM,UAAU,UAAM,yBAAQ,GAAG;AACjC,mBAAiB,SAAS,SAAS;AACjC,UAAM,WAAO,kBAAK,KAAK,MAAM,IAAI;AACjC,QAAI,MAAM,YAAY,GAAG;AACvB,YAAM,KAAK,GAAI,MAAM,gBAAgB,IAAI,CAAE;IAC7C,WAAW,MAAM,OAAO,KAAK,MAAM,KAAK,SAAS,MAAM,GAAG;AACxD,YAAM,KAAK,IAAI;IACjB;EACF;AACA,SAAO,MAAM,KAAK;AACpB;AAEA,SAAS,aAAa,SAAiB,SAAyB;AAC9D,aAAO,sBAAS,SAAS,OAAO,EAAE,MAAM,IAAI,EAAE,KAAK,GAAG,EAAE,QAAQ,UAAU,EAAE;AAC9E;AAEA,eAAe,QACb,OACA,aACA,QACc;AACd,QAAM,UAAe,CAAC;AACtB,MAAI,SAAS;AACb,QAAM,cAAc,KAAK,IAAI,GAAG,KAAK,IAAI,aAAa,MAAM,MAAM,CAAC;AAEnE,QAAM,QAAQ;IACZ,MAAM,KAAK,EAAE,QAAQ,YAAY,GAAG,YAAY;AAC9C,iBAAS;AACP,cAAM,QAAQ;AACd,cAAM,OAAO,MAAM,KAAK;AACxB,YAAI,SAAS,OAAW;AACxB,gBAAQ,KAAK,IAAI,MAAM,OAAO,IAAI;MACpC;IACF,CAAC;EACH;AAEA,SAAO;AACT;AAEA,eAAsB,UAAU,SAAqD;AACnF,QAAM,WAAW,MAAM,gBAAgB,QAAQ,OAAO;AACtD,QAAM,cAAc,QAAQ,eAAe;AAC3C,QAAM,WAAW,MAAM,QAAQ,UAAU,aAAa,OAAO,YAAY;AACvE,UAAM,OAAO,aAAa,QAAQ,SAAS,OAAO;AAClD,UAAM,SAAS,MAAM,WAAW;MAC9B,OAAO,QAAQ;MACf,SAAS,QAAQ;MACjB,UAAU,QAAQ;MAClB,OAAO,QAAQ;MACf;MACA;MACA,SAAS,QAAQ;MACjB,cAAc,QAAQ;MACtB,WAAW,QAAQ;IACrB,CAAC;AACD,WAAO,EAAE,GAAG,QAAQ,MAAM,QAAQ;EACpC,CAAC;AACD,SAAO,EAAE,SAAS;AACpB;;;ADnMA,SAAS,UAAU,SAA6C;AAC9D,MAAI,QAAQ,aAAa,OAAW,QAAO,CAAC,QAAQ;AACpD,SAAO,QAAQ,IAAI,aAAa;AAClC;AAEA,SAAS,eAAe,QAAwB,SAAqC;AACnF,QAAM,MAAM,WAAW,OAAO,MAAM;AACpC,aAAO,6BAAW,GAAG,IAAI,UAAM,0BAAQ,OAAO,MAAM,GAAG;AACzD;AAEO,SAAS,aAAa,SAA4C;AACvE,MAAI;AAEJ,SAAO;AAAA,IACL,MAAM;AAAA,IACN,OAAO;AAAA,IAEP,eAAe,UAAU;AACvB,eAAS;AACT,UAAI,UAAU,OAAO,KAAK,SAAS,MAAM,cAAc,UAAU;AAC/D,iBAAS,OAAO,KAAK,oFAAoF;AAAA,MAC3G;AAAA,IACF;AAAA,IAEA,MAAM,cAAc;AAClB,UAAI,CAAC,UAAU,OAAO,YAAY,WAAW,CAAC,UAAU,OAAO,EAAG;AAClE,UAAI,CAAC,QAAQ,OAAO;AAClB,eAAO,OAAO,KAAK,2DAA2D;AAC9E;AAAA,MACF;AAEA,YAAM,UAAU;AAAA,QACd,OAAO,QAAQ;AAAA,QACf,SAAS,QAAQ;AAAA,QACjB,UAAU,QAAQ;AAAA,QAClB,OAAO,QAAQ;AAAA,QACf,SAAS,eAAe,QAAQ,QAAQ,OAAO;AAAA,QAC/C,GAAI,QAAQ,gBAAgB,UAAa,EAAE,aAAa,QAAQ,YAAY;AAAA,MAC9E,CAAC;AAAA,IACH;AAAA,EACF;AACF;","names":["resolve"]}
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["import { isAbsolute, resolve } from 'node:path';\nimport { unlink } from 'node:fs/promises';\nimport type { Plugin, ResolvedConfig } from 'vite';\nimport { uploadDir, type UploadedSourceMap } from '@nanyujun/rum-sourcemap-cli';\n\nexport interface RumSourcemapPluginOptions {\n app: string;\n release: string;\n endpoint: string;\n token?: string;\n distDir?: string;\n concurrency?: number;\n disabled?: boolean;\n /** 上传成功后删除本地 .map 文件,避免构建产物残留。默认 false。 */\n cleanupAfterUpload?: boolean;\n}\n\nfunction isEnabled(options: RumSourcemapPluginOptions): boolean {\n if (options.disabled !== undefined) return !options.disabled;\n return process.env.NODE_ENV === 'production';\n}\n\nfunction resolveDistDir(config: ResolvedConfig, distDir: string | undefined): string {\n const dir = distDir ?? config.build.outDir;\n return isAbsolute(dir) ? dir : resolve(config.root, dir);\n}\n\nexport function rumSourcemap(options: RumSourcemapPluginOptions): Plugin {\n let config: ResolvedConfig | undefined;\n\n return {\n name: 'rum-sourcemap',\n apply: 'build',\n\n configResolved(resolved) {\n config = resolved;\n if (isEnabled(options) && resolved.build.sourcemap !== 'hidden') {\n resolved.logger.warn(\"[rum-sourcemap] build.sourcemap should be 'hidden' to avoid publishing .map files.\");\n }\n },\n\n async closeBundle() {\n if (!config || config.command !== 'build' || !isEnabled(options)) return;\n if (!options.token) {\n config.logger.warn('[rum-sourcemap] token is missing; skip source map upload.');\n return;\n }\n const logger = config.logger;\n\n const result = await uploadDir({\n appId: options.app,\n release: options.release,\n endpoint: options.endpoint,\n token: options.token,\n distDir: resolveDistDir(config, options.distDir),\n ...(options.concurrency !== undefined && { concurrency: options.concurrency }),\n });\n\n if (!options.cleanupAfterUpload) return;\n\n let removed = 0;\n let failed = 0;\n await Promise.allSettled(\n result.uploaded.map(async (item: UploadedSourceMap) => {\n try {\n await unlink(item.mapPath);\n removed += 1;\n } catch (err) {\n failed += 1;\n logger.warn(`[rum-sourcemap] failed to remove ${item.mapPath}: ${String(err)}`);\n }\n }),\n );\n logger.info(\n `[rum-sourcemap] cleanupAfterUpload: removed ${removed} .map file(s)` +\n (failed > 0 ? `, ${failed} failed` : ''),\n );\n },\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,uBAAoC;AACpC,sBAAuB;AAEvB,+BAAkD;AAclD,SAAS,UAAU,SAA6C;AAC9D,MAAI,QAAQ,aAAa,OAAW,QAAO,CAAC,QAAQ;AACpD,SAAO,QAAQ,IAAI,aAAa;AAClC;AAEA,SAAS,eAAe,QAAwB,SAAqC;AACnF,QAAM,MAAM,WAAW,OAAO,MAAM;AACpC,aAAO,6BAAW,GAAG,IAAI,UAAM,0BAAQ,OAAO,MAAM,GAAG;AACzD;AAEO,SAAS,aAAa,SAA4C;AACvE,MAAI;AAEJ,SAAO;AAAA,IACL,MAAM;AAAA,IACN,OAAO;AAAA,IAEP,eAAe,UAAU;AACvB,eAAS;AACT,UAAI,UAAU,OAAO,KAAK,SAAS,MAAM,cAAc,UAAU;AAC/D,iBAAS,OAAO,KAAK,oFAAoF;AAAA,MAC3G;AAAA,IACF;AAAA,IAEA,MAAM,cAAc;AAClB,UAAI,CAAC,UAAU,OAAO,YAAY,WAAW,CAAC,UAAU,OAAO,EAAG;AAClE,UAAI,CAAC,QAAQ,OAAO;AAClB,eAAO,OAAO,KAAK,2DAA2D;AAC9E;AAAA,MACF;AACA,YAAM,SAAS,OAAO;AAEtB,YAAM,SAAS,UAAM,oCAAU;AAAA,QAC7B,OAAO,QAAQ;AAAA,QACf,SAAS,QAAQ;AAAA,QACjB,UAAU,QAAQ;AAAA,QAClB,OAAO,QAAQ;AAAA,QACf,SAAS,eAAe,QAAQ,QAAQ,OAAO;AAAA,QAC/C,GAAI,QAAQ,gBAAgB,UAAa,EAAE,aAAa,QAAQ,YAAY;AAAA,MAC9E,CAAC;AAED,UAAI,CAAC,QAAQ,mBAAoB;AAEjC,UAAI,UAAU;AACd,UAAI,SAAS;AACb,YAAM,QAAQ;AAAA,QACZ,OAAO,SAAS,IAAI,OAAO,SAA4B;AACrD,cAAI;AACF,sBAAM,wBAAO,KAAK,OAAO;AACzB,uBAAW;AAAA,UACb,SAAS,KAAK;AACZ,sBAAU;AACV,mBAAO,KAAK,oCAAoC,KAAK,OAAO,KAAK,OAAO,GAAG,CAAC,EAAE;AAAA,UAChF;AAAA,QACF,CAAC;AAAA,MACH;AACA,aAAO;AAAA,QACL,+CAA+C,OAAO,mBACnD,SAAS,IAAI,KAAK,MAAM,YAAY;AAAA,MACzC;AAAA,IACF;AAAA,EACF;AACF;","names":[]}
package/dist/index.d.cts CHANGED
@@ -8,6 +8,8 @@ interface RumSourcemapPluginOptions {
8
8
  distDir?: string;
9
9
  concurrency?: number;
10
10
  disabled?: boolean;
11
+ /** 上传成功后删除本地 .map 文件,避免构建产物残留。默认 false。 */
12
+ cleanupAfterUpload?: boolean;
11
13
  }
12
14
  declare function rumSourcemap(options: RumSourcemapPluginOptions): Plugin;
13
15
 
package/dist/index.d.ts CHANGED
@@ -8,6 +8,8 @@ interface RumSourcemapPluginOptions {
8
8
  distDir?: string;
9
9
  concurrency?: number;
10
10
  disabled?: boolean;
11
+ /** 上传成功后删除本地 .map 文件,避免构建产物残留。默认 false。 */
12
+ cleanupAfterUpload?: boolean;
11
13
  }
12
14
  declare function rumSourcemap(options: RumSourcemapPluginOptions): Plugin;
13
15
 
package/dist/index.js CHANGED
@@ -1,136 +1,7 @@
1
1
  // src/index.ts
2
2
  import { isAbsolute, resolve } from "path";
3
-
4
- // ../sourcemap-cli/dist/index.js
5
- import { opendir, readFile } from "fs/promises";
6
- import { basename, join, relative } from "path";
7
- var SourceMapUploadError = class extends Error {
8
- constructor(message, status) {
9
- super(message);
10
- this.name = "SourceMapUploadError";
11
- this.status = status;
12
- }
13
- };
14
- var DEFAULT_RETRIES = 3;
15
- var DEFAULT_RETRY_DELAY_MS = 1e3;
16
- var DEFAULT_CONCURRENCY = 4;
17
- function sourceMapUrl(endpoint) {
18
- return `${endpoint.replace(/\/+$/, "")}/admin/sourcemaps`;
19
- }
20
- function sleep(ms) {
21
- if (ms <= 0) return Promise.resolve();
22
- return new Promise((resolve2) => setTimeout(resolve2, ms));
23
- }
24
- function retryDelay(baseMs, retryIndex) {
25
- return baseMs * 2 ** retryIndex;
26
- }
27
- async function responseMessage(response) {
28
- try {
29
- const body = await response.json();
30
- if (typeof body.error === "string" && body.error.length > 0) return body.error;
31
- } catch {
32
- }
33
- return response.statusText || `HTTP ${response.status}`;
34
- }
35
- function isRetryable(status) {
36
- return status >= 500 || status === 408 || status === 429;
37
- }
38
- async function readUploadResult(response) {
39
- const body = await response.json();
40
- return {
41
- id: Number(body.id),
42
- sha256: String(body.sha256),
43
- sizeBytes: Number(body.sizeBytes)
44
- };
45
- }
46
- async function uploadFile(options) {
47
- const fetcher = options.fetchImpl ?? fetch;
48
- const retries = options.retries ?? DEFAULT_RETRIES;
49
- const retryDelayMs = options.retryDelayMs ?? DEFAULT_RETRY_DELAY_MS;
50
- let lastError;
51
- for (let attempt = 0; attempt <= retries; attempt++) {
52
- try {
53
- const content = await readFile(options.mapPath);
54
- const form = new FormData();
55
- form.set("appId", options.appId);
56
- form.set("release", options.release);
57
- form.set("file", options.file);
58
- form.set("map", new Blob([new Uint8Array(content)]), basename(options.mapPath));
59
- const response = await fetcher(sourceMapUrl(options.endpoint), {
60
- method: "POST",
61
- headers: { Authorization: `Bearer ${options.token}` },
62
- body: form
63
- });
64
- if (response.ok) return readUploadResult(response);
65
- const message = await responseMessage(response);
66
- if (!isRetryable(response.status) || attempt >= retries) {
67
- throw new SourceMapUploadError(message, response.status);
68
- }
69
- lastError = new SourceMapUploadError(message, response.status);
70
- } catch (err) {
71
- if (err instanceof SourceMapUploadError && !isRetryable(err.status ?? 0)) throw err;
72
- lastError = err;
73
- if (attempt >= retries) break;
74
- }
75
- await sleep(retryDelay(retryDelayMs, attempt));
76
- }
77
- if (lastError instanceof Error) throw lastError;
78
- throw new SourceMapUploadError("source map upload failed");
79
- }
80
- async function collectMapFiles(dir) {
81
- const files = [];
82
- const entries = await opendir(dir);
83
- for await (const entry of entries) {
84
- const path = join(dir, entry.name);
85
- if (entry.isDirectory()) {
86
- files.push(...await collectMapFiles(path));
87
- } else if (entry.isFile() && entry.name.endsWith(".map")) {
88
- files.push(path);
89
- }
90
- }
91
- return files.sort();
92
- }
93
- function toSourceFile(distDir, mapPath) {
94
- return relative(distDir, mapPath).split("\\").join("/").replace(/\.map$/, "");
95
- }
96
- async function runPool(items, concurrency, worker) {
97
- const results = [];
98
- let cursor = 0;
99
- const workerCount = Math.max(1, Math.min(concurrency, items.length));
100
- await Promise.all(
101
- Array.from({ length: workerCount }, async () => {
102
- for (; ; ) {
103
- const index = cursor++;
104
- const item = items[index];
105
- if (item === void 0) break;
106
- results[index] = await worker(item);
107
- }
108
- })
109
- );
110
- return results;
111
- }
112
- async function uploadDir(options) {
113
- const mapPaths = await collectMapFiles(options.distDir);
114
- const concurrency = options.concurrency ?? DEFAULT_CONCURRENCY;
115
- const uploaded = await runPool(mapPaths, concurrency, async (mapPath) => {
116
- const file = toSourceFile(options.distDir, mapPath);
117
- const result = await uploadFile({
118
- appId: options.appId,
119
- release: options.release,
120
- endpoint: options.endpoint,
121
- token: options.token,
122
- file,
123
- mapPath,
124
- retries: options.retries,
125
- retryDelayMs: options.retryDelayMs,
126
- fetchImpl: options.fetchImpl
127
- });
128
- return { ...result, file, mapPath };
129
- });
130
- return { uploaded };
131
- }
132
-
133
- // src/index.ts
3
+ import { unlink } from "fs/promises";
4
+ import { uploadDir } from "@nanyujun/rum-sourcemap-cli";
134
5
  function isEnabled(options) {
135
6
  if (options.disabled !== void 0) return !options.disabled;
136
7
  return process.env.NODE_ENV === "production";
@@ -156,7 +27,8 @@ function rumSourcemap(options) {
156
27
  config.logger.warn("[rum-sourcemap] token is missing; skip source map upload.");
157
28
  return;
158
29
  }
159
- await uploadDir({
30
+ const logger = config.logger;
31
+ const result = await uploadDir({
160
32
  appId: options.app,
161
33
  release: options.release,
162
34
  endpoint: options.endpoint,
@@ -164,6 +36,23 @@ function rumSourcemap(options) {
164
36
  distDir: resolveDistDir(config, options.distDir),
165
37
  ...options.concurrency !== void 0 && { concurrency: options.concurrency }
166
38
  });
39
+ if (!options.cleanupAfterUpload) return;
40
+ let removed = 0;
41
+ let failed = 0;
42
+ await Promise.allSettled(
43
+ result.uploaded.map(async (item) => {
44
+ try {
45
+ await unlink(item.mapPath);
46
+ removed += 1;
47
+ } catch (err) {
48
+ failed += 1;
49
+ logger.warn(`[rum-sourcemap] failed to remove ${item.mapPath}: ${String(err)}`);
50
+ }
51
+ })
52
+ );
53
+ logger.info(
54
+ `[rum-sourcemap] cleanupAfterUpload: removed ${removed} .map file(s)` + (failed > 0 ? `, ${failed} failed` : "")
55
+ );
167
56
  }
168
57
  };
169
58
  }
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../../sourcemap-cli/src/index.ts"],"sourcesContent":["import { isAbsolute, resolve } from 'node:path';\nimport type { Plugin, ResolvedConfig } from 'vite';\nimport { uploadDir } from '@rum/sourcemap-cli';\n\nexport interface RumSourcemapPluginOptions {\n app: string;\n release: string;\n endpoint: string;\n token?: string;\n distDir?: string;\n concurrency?: number;\n disabled?: boolean;\n}\n\nfunction isEnabled(options: RumSourcemapPluginOptions): boolean {\n if (options.disabled !== undefined) return !options.disabled;\n return process.env.NODE_ENV === 'production';\n}\n\nfunction resolveDistDir(config: ResolvedConfig, distDir: string | undefined): string {\n const dir = distDir ?? config.build.outDir;\n return isAbsolute(dir) ? dir : resolve(config.root, dir);\n}\n\nexport function rumSourcemap(options: RumSourcemapPluginOptions): Plugin {\n let config: ResolvedConfig | undefined;\n\n return {\n name: 'rum-sourcemap',\n apply: 'build',\n\n configResolved(resolved) {\n config = resolved;\n if (isEnabled(options) && resolved.build.sourcemap !== 'hidden') {\n resolved.logger.warn(\"[rum-sourcemap] build.sourcemap should be 'hidden' to avoid publishing .map files.\");\n }\n },\n\n async closeBundle() {\n if (!config || config.command !== 'build' || !isEnabled(options)) return;\n if (!options.token) {\n config.logger.warn('[rum-sourcemap] token is missing; skip source map upload.');\n return;\n }\n\n await uploadDir({\n appId: options.app,\n release: options.release,\n endpoint: options.endpoint,\n token: options.token,\n distDir: resolveDistDir(config, options.distDir),\n ...(options.concurrency !== undefined && { concurrency: options.concurrency }),\n });\n },\n };\n}\n","import { opendir, readFile } from 'node:fs/promises';\nimport { basename, join, relative } from 'node:path';\n\nexport interface UploadFileResult {\n id: number;\n sha256: string;\n sizeBytes: number;\n}\n\nexport interface UploadFileOptions {\n appId: string;\n release: string;\n endpoint: string;\n token: string;\n file: string;\n mapPath: string;\n retries?: number;\n retryDelayMs?: number;\n fetchImpl?: typeof fetch;\n}\n\nexport interface UploadDirOptions {\n appId: string;\n release: string;\n endpoint: string;\n token: string;\n distDir: string;\n concurrency?: number;\n retries?: number;\n retryDelayMs?: number;\n fetchImpl?: typeof fetch;\n}\n\nexport interface UploadedSourceMap extends UploadFileResult {\n file: string;\n mapPath: string;\n}\n\nexport interface UploadDirResult {\n uploaded: UploadedSourceMap[];\n}\n\nexport interface ParsedUploadCommand {\n command: 'upload';\n appId: string;\n release: string;\n distDir: string;\n endpoint: string;\n token: string;\n concurrency?: number;\n}\n\nexport class SourceMapUploadError extends Error {\n status?: number;\n\n constructor(message: string, status?: number) {\n super(message);\n this.name = 'SourceMapUploadError';\n this.status = status;\n }\n}\n\nexport class SourceMapCliUsageError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'SourceMapCliUsageError';\n }\n}\n\nconst DEFAULT_RETRIES = 3;\nconst DEFAULT_RETRY_DELAY_MS = 1000;\nconst DEFAULT_CONCURRENCY = 4;\n\nfunction sourceMapUrl(endpoint: string): string {\n return `${endpoint.replace(/\\/+$/, '')}/admin/sourcemaps`;\n}\n\nfunction sleep(ms: number): Promise<void> {\n if (ms <= 0) return Promise.resolve();\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\nfunction retryDelay(baseMs: number, retryIndex: number): number {\n return baseMs * 2 ** retryIndex;\n}\n\nasync function responseMessage(response: Response): Promise<string> {\n try {\n const body = (await response.json()) as { error?: unknown };\n if (typeof body.error === 'string' && body.error.length > 0) return body.error;\n } catch {\n // Ignore malformed error bodies; status text is enough for CLI output.\n }\n return response.statusText || `HTTP ${response.status}`;\n}\n\nfunction isRetryable(status: number): boolean {\n return status >= 500 || status === 408 || status === 429;\n}\n\nasync function readUploadResult(response: Response): Promise<UploadFileResult> {\n const body = (await response.json()) as Partial<UploadFileResult>;\n return {\n id: Number(body.id),\n sha256: String(body.sha256),\n sizeBytes: Number(body.sizeBytes),\n };\n}\n\nexport async function uploadFile(options: UploadFileOptions): Promise<UploadFileResult> {\n const fetcher = options.fetchImpl ?? fetch;\n const retries = options.retries ?? DEFAULT_RETRIES;\n const retryDelayMs = options.retryDelayMs ?? DEFAULT_RETRY_DELAY_MS;\n let lastError: unknown;\n\n for (let attempt = 0; attempt <= retries; attempt++) {\n try {\n const content = await readFile(options.mapPath);\n const form = new FormData();\n form.set('appId', options.appId);\n form.set('release', options.release);\n form.set('file', options.file);\n form.set('map', new Blob([new Uint8Array(content)]), basename(options.mapPath));\n\n const response = await fetcher(sourceMapUrl(options.endpoint), {\n method: 'POST',\n headers: { Authorization: `Bearer ${options.token}` },\n body: form,\n });\n\n if (response.ok) return readUploadResult(response);\n const message = await responseMessage(response);\n if (!isRetryable(response.status) || attempt >= retries) {\n throw new SourceMapUploadError(message, response.status);\n }\n lastError = new SourceMapUploadError(message, response.status);\n } catch (err) {\n if (err instanceof SourceMapUploadError && !isRetryable(err.status ?? 0)) throw err;\n lastError = err;\n if (attempt >= retries) break;\n }\n\n await sleep(retryDelay(retryDelayMs, attempt));\n }\n\n if (lastError instanceof Error) throw lastError;\n throw new SourceMapUploadError('source map upload failed');\n}\n\nasync function collectMapFiles(dir: string): Promise<string[]> {\n const files: string[] = [];\n const entries = await opendir(dir);\n for await (const entry of entries) {\n const path = join(dir, entry.name);\n if (entry.isDirectory()) {\n files.push(...(await collectMapFiles(path)));\n } else if (entry.isFile() && entry.name.endsWith('.map')) {\n files.push(path);\n }\n }\n return files.sort();\n}\n\nfunction toSourceFile(distDir: string, mapPath: string): string {\n return relative(distDir, mapPath).split('\\\\').join('/').replace(/\\.map$/, '');\n}\n\nasync function runPool<T, R>(\n items: T[],\n concurrency: number,\n worker: (item: T) => Promise<R>,\n): Promise<R[]> {\n const results: R[] = [];\n let cursor = 0;\n const workerCount = Math.max(1, Math.min(concurrency, items.length));\n\n await Promise.all(\n Array.from({ length: workerCount }, async () => {\n for (;;) {\n const index = cursor++;\n const item = items[index];\n if (item === undefined) break;\n results[index] = await worker(item);\n }\n }),\n );\n\n return results;\n}\n\nexport async function uploadDir(options: UploadDirOptions): Promise<UploadDirResult> {\n const mapPaths = await collectMapFiles(options.distDir);\n const concurrency = options.concurrency ?? DEFAULT_CONCURRENCY;\n const uploaded = await runPool(mapPaths, concurrency, async (mapPath) => {\n const file = toSourceFile(options.distDir, mapPath);\n const result = await uploadFile({\n appId: options.appId,\n release: options.release,\n endpoint: options.endpoint,\n token: options.token,\n file,\n mapPath,\n retries: options.retries,\n retryDelayMs: options.retryDelayMs,\n fetchImpl: options.fetchImpl,\n });\n return { ...result, file, mapPath };\n });\n return { uploaded };\n}\n\nfunction readFlag(argv: string[], flag: string): string | undefined {\n const index = argv.indexOf(flag);\n if (index === -1) return undefined;\n return argv[index + 1];\n}\n\nfunction required(value: string | undefined, flag: string): string {\n if (!value) throw new SourceMapCliUsageError(`missing ${flag}`);\n return value;\n}\n\nexport function parseCliArgs(argv: string[]): ParsedUploadCommand {\n const command = argv[0];\n if (command !== 'upload') {\n throw new SourceMapCliUsageError('usage: rum-sourcemap upload --app <appId> --release <release> --dist <dir> --endpoint <url> --token <token>');\n }\n\n const concurrencyRaw = readFlag(argv, '--concurrency');\n return {\n command: 'upload',\n appId: required(readFlag(argv, '--app') ?? readFlag(argv, '--app-id'), '--app'),\n release: required(readFlag(argv, '--release'), '--release'),\n distDir: required(readFlag(argv, '--dist'), '--dist'),\n endpoint: required(readFlag(argv, '--endpoint'), '--endpoint'),\n token: required(readFlag(argv, '--token'), '--token'),\n ...(concurrencyRaw !== undefined && { concurrency: Number(concurrencyRaw) }),\n };\n}\n\nexport async function runCli(argv: string[], log: (message: string) => void = console.warn) {\n const command = parseCliArgs(argv);\n const result = await uploadDir(command);\n log(`uploaded ${result.uploaded.length} source maps`);\n return result;\n}\n"],"mappings":"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","names":["resolve"]}
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["import { isAbsolute, resolve } from 'node:path';\nimport { unlink } from 'node:fs/promises';\nimport type { Plugin, ResolvedConfig } from 'vite';\nimport { uploadDir, type UploadedSourceMap } from '@nanyujun/rum-sourcemap-cli';\n\nexport interface RumSourcemapPluginOptions {\n app: string;\n release: string;\n endpoint: string;\n token?: string;\n distDir?: string;\n concurrency?: number;\n disabled?: boolean;\n /** 上传成功后删除本地 .map 文件,避免构建产物残留。默认 false。 */\n cleanupAfterUpload?: boolean;\n}\n\nfunction isEnabled(options: RumSourcemapPluginOptions): boolean {\n if (options.disabled !== undefined) return !options.disabled;\n return process.env.NODE_ENV === 'production';\n}\n\nfunction resolveDistDir(config: ResolvedConfig, distDir: string | undefined): string {\n const dir = distDir ?? config.build.outDir;\n return isAbsolute(dir) ? dir : resolve(config.root, dir);\n}\n\nexport function rumSourcemap(options: RumSourcemapPluginOptions): Plugin {\n let config: ResolvedConfig | undefined;\n\n return {\n name: 'rum-sourcemap',\n apply: 'build',\n\n configResolved(resolved) {\n config = resolved;\n if (isEnabled(options) && resolved.build.sourcemap !== 'hidden') {\n resolved.logger.warn(\"[rum-sourcemap] build.sourcemap should be 'hidden' to avoid publishing .map files.\");\n }\n },\n\n async closeBundle() {\n if (!config || config.command !== 'build' || !isEnabled(options)) return;\n if (!options.token) {\n config.logger.warn('[rum-sourcemap] token is missing; skip source map upload.');\n return;\n }\n const logger = config.logger;\n\n const result = await uploadDir({\n appId: options.app,\n release: options.release,\n endpoint: options.endpoint,\n token: options.token,\n distDir: resolveDistDir(config, options.distDir),\n ...(options.concurrency !== undefined && { concurrency: options.concurrency }),\n });\n\n if (!options.cleanupAfterUpload) return;\n\n let removed = 0;\n let failed = 0;\n await Promise.allSettled(\n result.uploaded.map(async (item: UploadedSourceMap) => {\n try {\n await unlink(item.mapPath);\n removed += 1;\n } catch (err) {\n failed += 1;\n logger.warn(`[rum-sourcemap] failed to remove ${item.mapPath}: ${String(err)}`);\n }\n }),\n );\n logger.info(\n `[rum-sourcemap] cleanupAfterUpload: removed ${removed} .map file(s)` +\n (failed > 0 ? `, ${failed} failed` : ''),\n );\n },\n };\n}\n"],"mappings":";AAAA,SAAS,YAAY,eAAe;AACpC,SAAS,cAAc;AAEvB,SAAS,iBAAyC;AAclD,SAAS,UAAU,SAA6C;AAC9D,MAAI,QAAQ,aAAa,OAAW,QAAO,CAAC,QAAQ;AACpD,SAAO,QAAQ,IAAI,aAAa;AAClC;AAEA,SAAS,eAAe,QAAwB,SAAqC;AACnF,QAAM,MAAM,WAAW,OAAO,MAAM;AACpC,SAAO,WAAW,GAAG,IAAI,MAAM,QAAQ,OAAO,MAAM,GAAG;AACzD;AAEO,SAAS,aAAa,SAA4C;AACvE,MAAI;AAEJ,SAAO;AAAA,IACL,MAAM;AAAA,IACN,OAAO;AAAA,IAEP,eAAe,UAAU;AACvB,eAAS;AACT,UAAI,UAAU,OAAO,KAAK,SAAS,MAAM,cAAc,UAAU;AAC/D,iBAAS,OAAO,KAAK,oFAAoF;AAAA,MAC3G;AAAA,IACF;AAAA,IAEA,MAAM,cAAc;AAClB,UAAI,CAAC,UAAU,OAAO,YAAY,WAAW,CAAC,UAAU,OAAO,EAAG;AAClE,UAAI,CAAC,QAAQ,OAAO;AAClB,eAAO,OAAO,KAAK,2DAA2D;AAC9E;AAAA,MACF;AACA,YAAM,SAAS,OAAO;AAEtB,YAAM,SAAS,MAAM,UAAU;AAAA,QAC7B,OAAO,QAAQ;AAAA,QACf,SAAS,QAAQ;AAAA,QACjB,UAAU,QAAQ;AAAA,QAClB,OAAO,QAAQ;AAAA,QACf,SAAS,eAAe,QAAQ,QAAQ,OAAO;AAAA,QAC/C,GAAI,QAAQ,gBAAgB,UAAa,EAAE,aAAa,QAAQ,YAAY;AAAA,MAC9E,CAAC;AAED,UAAI,CAAC,QAAQ,mBAAoB;AAEjC,UAAI,UAAU;AACd,UAAI,SAAS;AACb,YAAM,QAAQ;AAAA,QACZ,OAAO,SAAS,IAAI,OAAO,SAA4B;AACrD,cAAI;AACF,kBAAM,OAAO,KAAK,OAAO;AACzB,uBAAW;AAAA,UACb,SAAS,KAAK;AACZ,sBAAU;AACV,mBAAO,KAAK,oCAAoC,KAAK,OAAO,KAAK,OAAO,GAAG,CAAC,EAAE;AAAA,UAChF;AAAA,QACF,CAAC;AAAA,MACH;AACA,aAAO;AAAA,QACL,+CAA+C,OAAO,mBACnD,SAAS,IAAI,KAAK,MAAM,YAAY;AAAA,MACzC;AAAA,IACF;AAAA,EACF;AACF;","names":[]}
package/package.json CHANGED
@@ -1,6 +1,8 @@
1
1
  {
2
2
  "name": "@nanyujun/rum-vite-plugin-sourcemap",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
+ "description": "RUM 前端性能监控平台的 Vite 插件:vite build 完成后自动把产物 *.map 上传到 collector,无需手动跑 CLI。",
5
+ "keywords": ["rum", "sourcemap", "source-map", "vite", "vite-plugin", "upload", "monitoring", "error-tracking"],
4
6
  "type": "module",
5
7
  "publishConfig": {
6
8
  "access": "public"
@@ -16,7 +18,8 @@
16
18
  }
17
19
  },
18
20
  "files": [
19
- "dist"
21
+ "dist",
22
+ "README.md"
20
23
  ],
21
24
  "scripts": {
22
25
  "build": "tsup",
@@ -29,7 +32,7 @@
29
32
  "vite": "^5.0.0 || ^6.0.0"
30
33
  },
31
34
  "dependencies": {
32
- "@nanyujun/rum-sourcemap-cli": "^0.1.0"
35
+ "@nanyujun/rum-sourcemap-cli": "^0.1.1"
33
36
  },
34
37
  "devDependencies": {
35
38
  "vite": "^5.4.0"