@tourmind-frontend/monitor-plugin-webpack 1.2.0 → 1.6.0

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.
package/dist/index.cjs CHANGED
@@ -33,6 +33,7 @@ __export(src_exports, {
33
33
  default: () => UploadSourceMapPlugin
34
34
  });
35
35
  module.exports = __toCommonJS(src_exports);
36
+ var import_zlib = require("zlib");
36
37
  var import_axios = __toESM(require("axios"), 1);
37
38
  var import_form_data = __toESM(require("form-data"), 1);
38
39
  var LOG_PREFIX = "[frontend-monitor]";
@@ -43,6 +44,11 @@ function defaultLogger(level, msg, extra) {
43
44
  else if (level === "warn") console.warn(line, tail);
44
45
  else console.error(line, tail);
45
46
  }
47
+ function formatSize(bytes) {
48
+ if (bytes >= 1024 * 1024) return `${(bytes / 1024 / 1024).toFixed(1)}MB`;
49
+ if (bytes >= 1024) return `${(bytes / 1024).toFixed(1)}KB`;
50
+ return `${bytes}B`;
51
+ }
46
52
  function extractSourceMaps(compilation) {
47
53
  const result = [];
48
54
  const assets = compilation.assets;
@@ -65,10 +71,16 @@ function extractSourceMaps(compilation) {
65
71
  }
66
72
  async function uploadFiles({ url, query, files }) {
67
73
  const form = new import_form_data.default();
74
+ let rawBytes = 0;
75
+ let gzipBytes = 0;
68
76
  for (const [filename, content] of files) {
69
- form.append("file", Buffer.from(content, "utf-8"), { filename });
77
+ const raw = Buffer.from(content, "utf-8");
78
+ const gz = (0, import_zlib.gzipSync)(raw);
79
+ rawBytes += raw.byteLength;
80
+ gzipBytes += gz.byteLength;
81
+ form.append("file", gz, { filename });
70
82
  }
71
- const qs = new URLSearchParams(query).toString();
83
+ const qs = new URLSearchParams({ ...query, encoding: "gzip" }).toString();
72
84
  const fullUrl = `${url}${url.includes("?") ? "&" : "?"}${qs}`;
73
85
  await (0, import_axios.default)({
74
86
  method: "POST",
@@ -78,14 +90,28 @@ async function uploadFiles({ url, query, files }) {
78
90
  maxBodyLength: Infinity,
79
91
  maxContentLength: Infinity
80
92
  });
93
+ return { rawBytes, gzipBytes };
81
94
  }
82
95
  var UploadSourceMapPlugin = class {
83
96
  constructor(options) {
97
+ // emit 阶段 fire-and-forget 启动的上传任务;用数组而非单值,兼容同一实例被挂到
98
+ // 多个 compiler(如 Nuxt 2 modern 模式 client + modern 双客户端构建)的情况。
99
+ this.pendingUploads = [];
84
100
  if (!options.url) throw new Error(`${LOG_PREFIX} "url" is required`);
85
- if (!options.token) throw new Error(`${LOG_PREFIX} "token" is required`);
101
+ if (!options.authToken) throw new Error(`${LOG_PREFIX} "authToken" is required`);
102
+ if (!options.authToken.startsWith("fm_at_")) {
103
+ throw new Error(`${LOG_PREFIX} "authToken" must start with "fm_at_"; do not use the client key here`);
104
+ }
86
105
  if (!options.commit) throw new Error(`${LOG_PREFIX} "commit" is required`);
87
106
  this.options = options;
88
107
  }
108
+ /**
109
+ * 等待所有已启动的上传完成(上传失败已被内部 catch 吞掉,不会 reject)。
110
+ * deferWait 模式下由调用方在构建尾声调用,确保进程退出前上传完成。
111
+ */
112
+ waitForUpload() {
113
+ return Promise.all(this.pendingUploads).then(() => void 0);
114
+ }
89
115
  apply(compiler) {
90
116
  var _a;
91
117
  const options = this.options;
@@ -93,7 +119,6 @@ var UploadSourceMapPlugin = class {
93
119
  if (compiler.options.mode !== "production") return;
94
120
  const uploadUrl = `${options.url.replace(/\/+$/, "")}/api/upload`;
95
121
  compiler.options.devtool = "hidden-source-map";
96
- let uploadPromise = null;
97
122
  compiler.hooks.emit.tap("UploadSourceMapPlugin", (compilation) => {
98
123
  var _a2;
99
124
  const files = extractSourceMaps(compilation);
@@ -104,14 +129,21 @@ var UploadSourceMapPlugin = class {
104
129
  const { commit } = options;
105
130
  const timestamp = (_a2 = options.timestamp) != null ? _a2 : Date.now();
106
131
  const query = {
107
- token: options.token,
132
+ token: options.authToken,
108
133
  timestamp: String(timestamp)
109
134
  };
110
135
  query.commit = commit;
111
- uploadPromise = uploadFiles({ url: uploadUrl, query, files }).then(() => log("info", `uploaded ${files.length} sourcemap file(s) (commit=${commit})`)).catch((err) => log("error", "upload failed", err instanceof Error ? err.message : err));
112
- });
113
- compiler.hooks.done.tapPromise("UploadSourceMapPlugin", async () => {
114
- if (uploadPromise) await uploadPromise;
136
+ this.pendingUploads.push(
137
+ uploadFiles({ url: uploadUrl, query, files }).then(
138
+ ({ rawBytes, gzipBytes }) => log(
139
+ "info",
140
+ `uploaded ${files.length} sourcemap file(s) (commit=${commit}, ${formatSize(rawBytes)} -> ${formatSize(gzipBytes)} gzipped)`
141
+ )
142
+ ).catch((err) => log("error", "upload failed", err instanceof Error ? err.message : err))
143
+ );
115
144
  });
145
+ if (!options.deferWait) {
146
+ compiler.hooks.done.tapPromise("UploadSourceMapPlugin", () => this.waitForUpload());
147
+ }
116
148
  }
117
149
  };
package/dist/index.d.ts CHANGED
@@ -2,17 +2,30 @@ import { Compiler } from "webpack";
2
2
  export interface UploadSourceMapOptions {
3
3
  /** monitor 服务的 base URL,例如 `https://monitor.example.com`。内部拼接 `/api/upload`。 */
4
4
  url: string;
5
- /** monitor 项目 token,从 web 控制台创建项目后获取,内部已绑定仓库 + 分支。 */
6
- token: string;
5
+ /** monitor 项目 auth token(前缀 `fm_at_`),仅供 CI / 构建机使用,从 web 控制台「项目」页获取。 */
6
+ authToken: string;
7
7
  /** Commit hash,由调用方自行解析(如 `git rev-parse HEAD`)。 */
8
8
  commit: string;
9
9
  /** 构建时间戳(毫秒),未设置时取上传时刻的 `Date.now()`。 */
10
10
  timestamp?: number;
11
11
  /** 自定义 logger,默认走 `console.log` / `console.warn` / `console.error`。 */
12
12
  logger?: (level: "info" | "warn" | "error", msg: string, extra?: unknown) => void;
13
+ /**
14
+ * 为 true 时本插件不在 compiler 的 done 阶段等待上传完成,由调用方在更晚的时机
15
+ * (如 Nuxt 2 的 `build:done`,所有 compiler 都结束后)await `waitForUpload()`,
16
+ * 让上传与后续其它 compiler 的编译并行。
17
+ * 注意:调用方必须在构建进程退出前 await,否则 CI 退出会掐断未完成的上传。
18
+ */
19
+ deferWait?: boolean;
13
20
  }
14
21
  export default class UploadSourceMapPlugin {
15
22
  options: UploadSourceMapOptions;
23
+ private pendingUploads;
16
24
  constructor(options: UploadSourceMapOptions);
25
+ /**
26
+ * 等待所有已启动的上传完成(上传失败已被内部 catch 吞掉,不会 reject)。
27
+ * deferWait 模式下由调用方在构建尾声调用,确保进程退出前上传完成。
28
+ */
29
+ waitForUpload(): Promise<void>;
17
30
  apply(compiler: Compiler): void;
18
31
  }
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  // extensions/plugin-webpack/src/index.ts
2
+ import { gzipSync } from "zlib";
2
3
  import axios from "axios";
3
4
  import FormData from "form-data";
4
5
  var LOG_PREFIX = "[frontend-monitor]";
@@ -9,6 +10,11 @@ function defaultLogger(level, msg, extra) {
9
10
  else if (level === "warn") console.warn(line, tail);
10
11
  else console.error(line, tail);
11
12
  }
13
+ function formatSize(bytes) {
14
+ if (bytes >= 1024 * 1024) return `${(bytes / 1024 / 1024).toFixed(1)}MB`;
15
+ if (bytes >= 1024) return `${(bytes / 1024).toFixed(1)}KB`;
16
+ return `${bytes}B`;
17
+ }
12
18
  function extractSourceMaps(compilation) {
13
19
  const result = [];
14
20
  const assets = compilation.assets;
@@ -31,10 +37,16 @@ function extractSourceMaps(compilation) {
31
37
  }
32
38
  async function uploadFiles({ url, query, files }) {
33
39
  const form = new FormData();
40
+ let rawBytes = 0;
41
+ let gzipBytes = 0;
34
42
  for (const [filename, content] of files) {
35
- form.append("file", Buffer.from(content, "utf-8"), { filename });
43
+ const raw = Buffer.from(content, "utf-8");
44
+ const gz = gzipSync(raw);
45
+ rawBytes += raw.byteLength;
46
+ gzipBytes += gz.byteLength;
47
+ form.append("file", gz, { filename });
36
48
  }
37
- const qs = new URLSearchParams(query).toString();
49
+ const qs = new URLSearchParams({ ...query, encoding: "gzip" }).toString();
38
50
  const fullUrl = `${url}${url.includes("?") ? "&" : "?"}${qs}`;
39
51
  await axios({
40
52
  method: "POST",
@@ -44,14 +56,28 @@ async function uploadFiles({ url, query, files }) {
44
56
  maxBodyLength: Infinity,
45
57
  maxContentLength: Infinity
46
58
  });
59
+ return { rawBytes, gzipBytes };
47
60
  }
48
61
  var UploadSourceMapPlugin = class {
49
62
  constructor(options) {
63
+ // emit 阶段 fire-and-forget 启动的上传任务;用数组而非单值,兼容同一实例被挂到
64
+ // 多个 compiler(如 Nuxt 2 modern 模式 client + modern 双客户端构建)的情况。
65
+ this.pendingUploads = [];
50
66
  if (!options.url) throw new Error(`${LOG_PREFIX} "url" is required`);
51
- if (!options.token) throw new Error(`${LOG_PREFIX} "token" is required`);
67
+ if (!options.authToken) throw new Error(`${LOG_PREFIX} "authToken" is required`);
68
+ if (!options.authToken.startsWith("fm_at_")) {
69
+ throw new Error(`${LOG_PREFIX} "authToken" must start with "fm_at_"; do not use the client key here`);
70
+ }
52
71
  if (!options.commit) throw new Error(`${LOG_PREFIX} "commit" is required`);
53
72
  this.options = options;
54
73
  }
74
+ /**
75
+ * 等待所有已启动的上传完成(上传失败已被内部 catch 吞掉,不会 reject)。
76
+ * deferWait 模式下由调用方在构建尾声调用,确保进程退出前上传完成。
77
+ */
78
+ waitForUpload() {
79
+ return Promise.all(this.pendingUploads).then(() => void 0);
80
+ }
55
81
  apply(compiler) {
56
82
  var _a;
57
83
  const options = this.options;
@@ -59,7 +85,6 @@ var UploadSourceMapPlugin = class {
59
85
  if (compiler.options.mode !== "production") return;
60
86
  const uploadUrl = `${options.url.replace(/\/+$/, "")}/api/upload`;
61
87
  compiler.options.devtool = "hidden-source-map";
62
- let uploadPromise = null;
63
88
  compiler.hooks.emit.tap("UploadSourceMapPlugin", (compilation) => {
64
89
  var _a2;
65
90
  const files = extractSourceMaps(compilation);
@@ -70,15 +95,22 @@ var UploadSourceMapPlugin = class {
70
95
  const { commit } = options;
71
96
  const timestamp = (_a2 = options.timestamp) != null ? _a2 : Date.now();
72
97
  const query = {
73
- token: options.token,
98
+ token: options.authToken,
74
99
  timestamp: String(timestamp)
75
100
  };
76
101
  query.commit = commit;
77
- uploadPromise = uploadFiles({ url: uploadUrl, query, files }).then(() => log("info", `uploaded ${files.length} sourcemap file(s) (commit=${commit})`)).catch((err) => log("error", "upload failed", err instanceof Error ? err.message : err));
78
- });
79
- compiler.hooks.done.tapPromise("UploadSourceMapPlugin", async () => {
80
- if (uploadPromise) await uploadPromise;
102
+ this.pendingUploads.push(
103
+ uploadFiles({ url: uploadUrl, query, files }).then(
104
+ ({ rawBytes, gzipBytes }) => log(
105
+ "info",
106
+ `uploaded ${files.length} sourcemap file(s) (commit=${commit}, ${formatSize(rawBytes)} -> ${formatSize(gzipBytes)} gzipped)`
107
+ )
108
+ ).catch((err) => log("error", "upload failed", err instanceof Error ? err.message : err))
109
+ );
81
110
  });
111
+ if (!options.deferWait) {
112
+ compiler.hooks.done.tapPromise("UploadSourceMapPlugin", () => this.waitForUpload());
113
+ }
82
114
  }
83
115
  };
84
116
  export {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tourmind-frontend/monitor-plugin-webpack",
3
- "version": "1.2.0",
3
+ "version": "1.6.0",
4
4
  "description": "Webpack plugin that uploads sourcemaps to a frontend-monitor server.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",