@tourmind-frontend/monitor-plugin-webpack 1.4.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,9 +90,13 @@ 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
101
  if (!options.authToken) throw new Error(`${LOG_PREFIX} "authToken" is required`);
86
102
  if (!options.authToken.startsWith("fm_at_")) {
@@ -89,6 +105,13 @@ var UploadSourceMapPlugin = class {
89
105
  if (!options.commit) throw new Error(`${LOG_PREFIX} "commit" is required`);
90
106
  this.options = options;
91
107
  }
108
+ /**
109
+ * 等待所有已启动的上传完成(上传失败已被内部 catch 吞掉,不会 reject)。
110
+ * deferWait 模式下由调用方在构建尾声调用,确保进程退出前上传完成。
111
+ */
112
+ waitForUpload() {
113
+ return Promise.all(this.pendingUploads).then(() => void 0);
114
+ }
92
115
  apply(compiler) {
93
116
  var _a;
94
117
  const options = this.options;
@@ -96,7 +119,6 @@ var UploadSourceMapPlugin = class {
96
119
  if (compiler.options.mode !== "production") return;
97
120
  const uploadUrl = `${options.url.replace(/\/+$/, "")}/api/upload`;
98
121
  compiler.options.devtool = "hidden-source-map";
99
- let uploadPromise = null;
100
122
  compiler.hooks.emit.tap("UploadSourceMapPlugin", (compilation) => {
101
123
  var _a2;
102
124
  const files = extractSourceMaps(compilation);
@@ -111,10 +133,17 @@ var UploadSourceMapPlugin = class {
111
133
  timestamp: String(timestamp)
112
134
  };
113
135
  query.commit = commit;
114
- 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));
115
- });
116
- compiler.hooks.done.tapPromise("UploadSourceMapPlugin", async () => {
117
- 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
+ );
118
144
  });
145
+ if (!options.deferWait) {
146
+ compiler.hooks.done.tapPromise("UploadSourceMapPlugin", () => this.waitForUpload());
147
+ }
119
148
  }
120
149
  };
package/dist/index.d.ts CHANGED
@@ -10,9 +10,22 @@ export interface UploadSourceMapOptions {
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,9 +56,13 @@ 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
67
  if (!options.authToken) throw new Error(`${LOG_PREFIX} "authToken" is required`);
52
68
  if (!options.authToken.startsWith("fm_at_")) {
@@ -55,6 +71,13 @@ var UploadSourceMapPlugin = class {
55
71
  if (!options.commit) throw new Error(`${LOG_PREFIX} "commit" is required`);
56
72
  this.options = options;
57
73
  }
74
+ /**
75
+ * 等待所有已启动的上传完成(上传失败已被内部 catch 吞掉,不会 reject)。
76
+ * deferWait 模式下由调用方在构建尾声调用,确保进程退出前上传完成。
77
+ */
78
+ waitForUpload() {
79
+ return Promise.all(this.pendingUploads).then(() => void 0);
80
+ }
58
81
  apply(compiler) {
59
82
  var _a;
60
83
  const options = this.options;
@@ -62,7 +85,6 @@ var UploadSourceMapPlugin = class {
62
85
  if (compiler.options.mode !== "production") return;
63
86
  const uploadUrl = `${options.url.replace(/\/+$/, "")}/api/upload`;
64
87
  compiler.options.devtool = "hidden-source-map";
65
- let uploadPromise = null;
66
88
  compiler.hooks.emit.tap("UploadSourceMapPlugin", (compilation) => {
67
89
  var _a2;
68
90
  const files = extractSourceMaps(compilation);
@@ -77,11 +99,18 @@ var UploadSourceMapPlugin = class {
77
99
  timestamp: String(timestamp)
78
100
  };
79
101
  query.commit = commit;
80
- 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));
81
- });
82
- compiler.hooks.done.tapPromise("UploadSourceMapPlugin", async () => {
83
- 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
+ );
84
110
  });
111
+ if (!options.deferWait) {
112
+ compiler.hooks.done.tapPromise("UploadSourceMapPlugin", () => this.waitForUpload());
113
+ }
85
114
  }
86
115
  };
87
116
  export {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tourmind-frontend/monitor-plugin-webpack",
3
- "version": "1.4.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",