@fz-firefly/unplugin 0.0.0-stage → 0.2.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/README.md +198 -2
- package/dist/index.cjs +6 -0
- package/dist/index.d.cts +4 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +3 -0
- package/dist/options-Dn99v-iv.d.cts +100 -0
- package/dist/options-Dn99v-iv.d.ts +100 -0
- package/dist/run-CU6mcdw9.cjs +576 -0
- package/dist/run-CXBdNHC1.js +536 -0
- package/dist/vite.cjs +73 -0
- package/dist/vite.d.cts +12 -0
- package/dist/vite.d.ts +12 -0
- package/dist/vite.js +68 -0
- package/dist/webpack.cjs +170 -0
- package/dist/webpack.d.cts +91 -0
- package/dist/webpack.d.ts +91 -0
- package/dist/webpack.js +164 -0
- package/package.json +56 -3
|
@@ -0,0 +1,536 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { dirname, join, relative, resolve, sep } from "node:path";
|
|
3
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
4
|
+
import { randomUUID } from "node:crypto";
|
|
5
|
+
import { readFile, readdir, unlink } from "node:fs/promises";
|
|
6
|
+
import http from "node:http";
|
|
7
|
+
import https from "node:https";
|
|
8
|
+
import { URL as URL$1 } from "node:url";
|
|
9
|
+
// src/script.ts
|
|
10
|
+
const requireFromProject = createRequire(resolve(process.cwd(), "package.json"));
|
|
11
|
+
/** 缓存从 @fz-firefly/web 读取的 IIFE 代码,避免每次 HTML 注入都读盘。 */
|
|
12
|
+
let cachedIifeCode = null;
|
|
13
|
+
function warn(message) {
|
|
14
|
+
console.warn(`[firefly] ${message}`);
|
|
15
|
+
}
|
|
16
|
+
function inferEnvironment() {
|
|
17
|
+
return process.env.NODE_ENV === "production" ? "production" : "development";
|
|
18
|
+
}
|
|
19
|
+
/** 读取浏览器 SDK 的 IIFE 产物。 */
|
|
20
|
+
function loadIifeCode(code) {
|
|
21
|
+
if (code) return code;
|
|
22
|
+
if (cachedIifeCode) return cachedIifeCode;
|
|
23
|
+
try {
|
|
24
|
+
const entry = requireFromProject.resolve("@fz-firefly/web");
|
|
25
|
+
const file = join(dirname(entry), "firefly.iife.js");
|
|
26
|
+
if (!existsSync(file)) {
|
|
27
|
+
warn(`找不到内联 SDK:${file},请先构建 @fz-firefly/web`);
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
cachedIifeCode = readFileSync(file, "utf8");
|
|
31
|
+
return cachedIifeCode;
|
|
32
|
+
} catch {
|
|
33
|
+
warn("找不到 @fz-firefly/web,无法内联 SDK;请安装它或通过 script.code 传入 IIFE 代码");
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* 生成 HTML 内联脚本。
|
|
39
|
+
*
|
|
40
|
+
* IIFE 产物必须暴露 `Firefly.initFirefly`;初始化后把 handle 挂到
|
|
41
|
+
* `window.__firefly__`,业务入口可以复用它来挂 Vue / React / 小程序 hooks。
|
|
42
|
+
*/
|
|
43
|
+
function buildInlineScript(options) {
|
|
44
|
+
var _script$dsn, _options$endpoint, _script$environment;
|
|
45
|
+
const script = options.script;
|
|
46
|
+
if (!script) return null;
|
|
47
|
+
if (!((_script$dsn = script.dsn) === null || _script$dsn === void 0 ? void 0 : _script$dsn.trim())) {
|
|
48
|
+
warn("跳过内联 SDK:缺少 script.dsn");
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
if (!((_options$endpoint = options.endpoint) === null || _options$endpoint === void 0 ? void 0 : _options$endpoint.trim())) {
|
|
52
|
+
warn("跳过内联 SDK:缺少 endpoint(或 FIREFLY_ENDPOINT)");
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
const sdk = loadIifeCode(script.code);
|
|
56
|
+
if (!sdk) return null;
|
|
57
|
+
if (sdk.includes("<\/script")) {
|
|
58
|
+
warn("跳过内联 SDK:IIFE 代码包含 <\/script>,无法安全注入 HTML");
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
const config = {
|
|
62
|
+
dsn: script.dsn,
|
|
63
|
+
endpoint: options.endpoint,
|
|
64
|
+
environment: (_script$environment = script.environment) !== null && _script$environment !== void 0 ? _script$environment : inferEnvironment(),
|
|
65
|
+
...options.release ? { release: options.release } : {},
|
|
66
|
+
...options.dist ? { dist: options.dist } : {}
|
|
67
|
+
};
|
|
68
|
+
return `${sdk}\n;window.__firefly__ = Firefly.initFirefly(${JSON.stringify(config).replace(/</g, "\\u003c")});`;
|
|
69
|
+
}
|
|
70
|
+
function escapeAttribute(value) {
|
|
71
|
+
return value.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">");
|
|
72
|
+
}
|
|
73
|
+
/** 生成带 CSP nonce 的 script 标签。 */
|
|
74
|
+
function inlineScriptTag(code, nonce) {
|
|
75
|
+
return `<script data-firefly-inline${nonce ? ` nonce="${escapeAttribute(nonce)}"` : ""}>${code}<\/script>`;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* 把内联脚本插到 <head> 最前面。
|
|
79
|
+
*
|
|
80
|
+
* Webpack 系构建没有 Vite 的 transformIndexHtml,统一在 emit 阶段
|
|
81
|
+
* 直接修改 HTML asset。已经注入过的 HTML 不重复插入。
|
|
82
|
+
*/
|
|
83
|
+
function injectInlineScriptHtml(html, code, nonce) {
|
|
84
|
+
if (html.includes("data-firefly-inline")) return html;
|
|
85
|
+
const tag = inlineScriptTag(code, nonce);
|
|
86
|
+
if (/<head[^>]*>/i.test(html)) return html.replace(/<head[^>]*>/i, (head) => `${head}\n${tag}`);
|
|
87
|
+
return `${tag}\n${html}`;
|
|
88
|
+
}
|
|
89
|
+
// src/sourcemap/collect.ts
|
|
90
|
+
function matches(value, filter) {
|
|
91
|
+
if (!filter) return false;
|
|
92
|
+
if (filter instanceof RegExp) return filter.test(value);
|
|
93
|
+
return filter.some((item) => value.includes(item));
|
|
94
|
+
}
|
|
95
|
+
/** 去掉 sourcemap source 的协议和相对路径前缀,便于判断是否来自 node_modules。 */
|
|
96
|
+
function normalizeSourcePath(source) {
|
|
97
|
+
var _source$split$0$split, _source$split$;
|
|
98
|
+
let value = (_source$split$0$split = (_source$split$ = source.split("?")[0]) === null || _source$split$ === void 0 ? void 0 : _source$split$.split("#")[0]) !== null && _source$split$0$split !== void 0 ? _source$split$0$split : source;
|
|
99
|
+
value = value.replace(/^(webpack|vite|uni-app|file):\/\/+/, "");
|
|
100
|
+
value = value.replace(/\\/g, "/");
|
|
101
|
+
value = value.replace(/^\/+/, "");
|
|
102
|
+
while (value.startsWith("../") || value.startsWith("./")) value = value.slice(value.indexOf("/") + 1);
|
|
103
|
+
return value;
|
|
104
|
+
}
|
|
105
|
+
/** 判断一个 source 是否来自第三方依赖或框架运行时。 */
|
|
106
|
+
function isVendorSource(source) {
|
|
107
|
+
const value = normalizeSourcePath(source);
|
|
108
|
+
return value.startsWith("node_modules/") || value.includes("/node_modules/") || value.startsWith("webpack/runtime/") || value.startsWith("vite/preload-helper") || value.startsWith("@vue/") || value.startsWith("vue/") || value.startsWith("react/") || value.startsWith("react-dom/");
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* 只包含 vendor source 的 map 才跳过。
|
|
112
|
+
*
|
|
113
|
+
* 注意不能判断成「包含任意 vendor source 就跳过」:主包通常是
|
|
114
|
+
* 业务代码 + node_modules 混合,按 any 判断会把业务 map 全部误删。
|
|
115
|
+
* sources 缺失或为空时无法判断,保守保留。
|
|
116
|
+
*/
|
|
117
|
+
function hasOnlyVendorSources(map) {
|
|
118
|
+
const sources = map.sources;
|
|
119
|
+
if (!Array.isArray(sources) || sources.length === 0) return false;
|
|
120
|
+
const stringSources = sources.filter((source) => typeof source === "string");
|
|
121
|
+
if (stringSources.length === 0) return false;
|
|
122
|
+
return stringSources.every(isVendorSource);
|
|
123
|
+
}
|
|
124
|
+
async function walk(dir) {
|
|
125
|
+
const result = [];
|
|
126
|
+
let entries;
|
|
127
|
+
try {
|
|
128
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
129
|
+
} catch {
|
|
130
|
+
return result;
|
|
131
|
+
}
|
|
132
|
+
for (const entry of entries) {
|
|
133
|
+
const fullPath = resolve(dir, entry.name);
|
|
134
|
+
if (entry.isDirectory()) result.push(...await walk(fullPath));
|
|
135
|
+
else if (entry.isFile()) result.push(fullPath);
|
|
136
|
+
}
|
|
137
|
+
return result;
|
|
138
|
+
}
|
|
139
|
+
/** 归一化 URL 前缀,保证以 / 开头、以 / 结尾。 */
|
|
140
|
+
function normalizeBase(base) {
|
|
141
|
+
let value = (base === null || base === void 0 ? void 0 : base.trim()) || "/";
|
|
142
|
+
if (/^https?:\/\//i.test(value)) try {
|
|
143
|
+
value = new URL(value).pathname;
|
|
144
|
+
} catch {
|
|
145
|
+
value = "/";
|
|
146
|
+
}
|
|
147
|
+
if (value === "auto" || value.startsWith(".")) value = "/";
|
|
148
|
+
if (!value.startsWith("/")) value = `/${value}`;
|
|
149
|
+
value = value.replace(/\/{2,}/g, "/");
|
|
150
|
+
if (!value.endsWith("/")) value += "/";
|
|
151
|
+
return value;
|
|
152
|
+
}
|
|
153
|
+
function toUrlPath(base, relativePath) {
|
|
154
|
+
const joined = `${base}${relativePath}`.replace(/\/{2,}/g, "/");
|
|
155
|
+
return joined.startsWith("/") ? joined : `/${joined}`;
|
|
156
|
+
}
|
|
157
|
+
/** 把 sourcemap 的 source 转成项目里的源码文件候选路径。 */
|
|
158
|
+
function sourceCandidates(source, sourceRoot) {
|
|
159
|
+
var _source$split$0$split2, _source$split$2;
|
|
160
|
+
let value = (_source$split$0$split2 = (_source$split$2 = source.split("?")[0]) === null || _source$split$2 === void 0 ? void 0 : _source$split$2.split("#")[0]) !== null && _source$split$0$split2 !== void 0 ? _source$split$0$split2 : source;
|
|
161
|
+
value = value.replace(/^(webpack|uni-app|vite|file):\/\/+/, "");
|
|
162
|
+
value = value.replace(/^\.\//, "").replace(/^\/+/, "");
|
|
163
|
+
if (!value || value.startsWith("webpack/") || value.startsWith("node_modules/") || value.includes("/node_modules/")) return [];
|
|
164
|
+
const resolved = resolve(sourceRoot, value);
|
|
165
|
+
const root = sourceRoot.endsWith(sep) ? sourceRoot : `${sourceRoot}${sep}`;
|
|
166
|
+
if (!resolved.startsWith(root)) return [];
|
|
167
|
+
return [resolved];
|
|
168
|
+
}
|
|
169
|
+
/** 给 map 补 sourcesContent,方便 Firefly 在后台展示源码上下文。 */
|
|
170
|
+
async function enrichSourcesContent(map, sourceRoot) {
|
|
171
|
+
const sources = map.sources;
|
|
172
|
+
if (!Array.isArray(sources) || Array.isArray(map.sourcesContent)) return;
|
|
173
|
+
const contents = await Promise.all(sources.map(async (source) => {
|
|
174
|
+
if (typeof source !== "string") return null;
|
|
175
|
+
for (const candidate of sourceCandidates(source, sourceRoot)) {
|
|
176
|
+
if (!existsSync(candidate)) continue;
|
|
177
|
+
try {
|
|
178
|
+
return await readFile(candidate, "utf8");
|
|
179
|
+
} catch {
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return null;
|
|
184
|
+
}));
|
|
185
|
+
if (contents.some((content) => typeof content === "string")) map.sourcesContent = contents;
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* 扫描 source map,并把 .map 路径换算成 JS 路径。
|
|
189
|
+
*
|
|
190
|
+
* Firefly 服务端按 JS 路径匹配,所以上传的是 `xxx.js`,内容却是
|
|
191
|
+
* `xxx.js.map`。
|
|
192
|
+
*/
|
|
193
|
+
async function collectSourcemaps(options) {
|
|
194
|
+
const mapFiles = await walk(options.mapDir);
|
|
195
|
+
const result = [];
|
|
196
|
+
const base = normalizeBase(options.base);
|
|
197
|
+
const skipVendorOnly = options.skipVendorOnly !== false;
|
|
198
|
+
for (const mapFile of mapFiles) {
|
|
199
|
+
if (!mapFile.endsWith(".js.map")) continue;
|
|
200
|
+
const relativePath = relative(options.mapDir, mapFile).split(sep).join("/");
|
|
201
|
+
if (options.include && !matches(relativePath, options.include)) continue;
|
|
202
|
+
if (options.exclude && matches(relativePath, options.exclude)) continue;
|
|
203
|
+
const jsRelativePath = relativePath.replace(/\.map$/, "");
|
|
204
|
+
const jsFile = resolve(options.outDir, ...jsRelativePath.split("/"));
|
|
205
|
+
if (!existsSync(jsFile)) continue;
|
|
206
|
+
let content = await readFile(mapFile, "utf8");
|
|
207
|
+
let parsed = null;
|
|
208
|
+
if (skipVendorOnly || options.sourcesContent && options.sourceRoot) try {
|
|
209
|
+
parsed = JSON.parse(content);
|
|
210
|
+
} catch {}
|
|
211
|
+
if (skipVendorOnly && parsed && hasOnlyVendorSources(parsed)) continue;
|
|
212
|
+
if (options.sourcesContent && options.sourceRoot && parsed) {
|
|
213
|
+
await enrichSourcesContent(parsed, options.sourceRoot);
|
|
214
|
+
content = JSON.stringify(parsed);
|
|
215
|
+
}
|
|
216
|
+
result.push({
|
|
217
|
+
filePath: toUrlPath(base, jsRelativePath),
|
|
218
|
+
content,
|
|
219
|
+
size: Buffer.byteLength(content),
|
|
220
|
+
mapFile
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
return result.sort((left, right) => left.filePath.localeCompare(right.filePath));
|
|
224
|
+
}
|
|
225
|
+
// src/sourcemap/upload.ts
|
|
226
|
+
const DEFAULT_BATCH_BYTES = 20971520;
|
|
227
|
+
const DEFAULT_MAX_FILES = 100;
|
|
228
|
+
const DEFAULT_RETRIES = 3;
|
|
229
|
+
const DEFAULT_TIMEOUT_MS = 6e4;
|
|
230
|
+
const MAX_RETRY_DELAY_MS = 8e3;
|
|
231
|
+
const MAX_RESPONSE_SNIPPET = 300;
|
|
232
|
+
var SourcemapUploadError = class extends Error {
|
|
233
|
+
constructor(message, status, retryAfterMs) {
|
|
234
|
+
super(message);
|
|
235
|
+
this.status = status;
|
|
236
|
+
this.retryAfterMs = retryAfterMs;
|
|
237
|
+
this.name = "SourcemapUploadError";
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
function sleep(ms) {
|
|
241
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
242
|
+
}
|
|
243
|
+
/** 把字节数格式化成日志里易读的大小。 */
|
|
244
|
+
function formatBytes(bytes) {
|
|
245
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
246
|
+
if (bytes < 1048576) return `${Math.round(bytes / 1024)} KB`;
|
|
247
|
+
return `${(bytes / 1024 / 1024).toFixed(2)} MB`;
|
|
248
|
+
}
|
|
249
|
+
/** 截取响应内容,避免把整页 HTML 都打进构建日志。 */
|
|
250
|
+
function responseSnippet(body) {
|
|
251
|
+
const text = body.replace(/\s+/g, " ").trim();
|
|
252
|
+
if (!text) return "(空响应)";
|
|
253
|
+
return text.length > MAX_RESPONSE_SNIPPET ? `${text.slice(0, MAX_RESPONSE_SNIPPET)}...` : text;
|
|
254
|
+
}
|
|
255
|
+
function retryAfterMs(headers) {
|
|
256
|
+
const value = headers["retry-after"];
|
|
257
|
+
const raw = Array.isArray(value) ? value[0] : value;
|
|
258
|
+
const seconds = Number(raw);
|
|
259
|
+
if (!Number.isFinite(seconds) || seconds <= 0) return void 0;
|
|
260
|
+
return seconds * 1e3;
|
|
261
|
+
}
|
|
262
|
+
/** 用 Node http/https 发送 JSON,兼容没有全局 fetch 的 Node 16。 */
|
|
263
|
+
function postJson(url, body, headers, timeoutMs) {
|
|
264
|
+
return new Promise((resolve, reject) => {
|
|
265
|
+
let target;
|
|
266
|
+
try {
|
|
267
|
+
target = new URL$1(url);
|
|
268
|
+
} catch {
|
|
269
|
+
reject(new SourcemapUploadError(`Firefly: endpoint 不合法:${url}`));
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
const request = (target.protocol === "https:" ? https : http).request(target, {
|
|
273
|
+
method: "POST",
|
|
274
|
+
headers
|
|
275
|
+
}, (response) => {
|
|
276
|
+
const chunks = [];
|
|
277
|
+
response.on("data", (chunk) => {
|
|
278
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
279
|
+
});
|
|
280
|
+
response.on("end", () => {
|
|
281
|
+
var _response$statusCode;
|
|
282
|
+
resolve({
|
|
283
|
+
status: (_response$statusCode = response.statusCode) !== null && _response$statusCode !== void 0 ? _response$statusCode : 0,
|
|
284
|
+
headers: response.headers,
|
|
285
|
+
body: Buffer.concat(chunks).toString("utf8")
|
|
286
|
+
});
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
request.setTimeout(timeoutMs, () => {
|
|
290
|
+
request.destroy(new SourcemapUploadError(`Firefly: sourcemap 上传超时(POST ${url})`));
|
|
291
|
+
});
|
|
292
|
+
request.on("error", (error) => {
|
|
293
|
+
reject(error instanceof SourcemapUploadError ? error : new SourcemapUploadError(`Firefly: sourcemap 上传失败(POST ${url}):${error.message}`));
|
|
294
|
+
});
|
|
295
|
+
request.write(body);
|
|
296
|
+
request.end();
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
function createBatches(files, batchBytes, maxFiles) {
|
|
300
|
+
const batches = [];
|
|
301
|
+
let current = [];
|
|
302
|
+
let currentBytes = 0;
|
|
303
|
+
for (const file of files) {
|
|
304
|
+
const wouldOverflowBytes = current.length > 0 && currentBytes + file.size > batchBytes;
|
|
305
|
+
const wouldOverflowFiles = current.length >= maxFiles;
|
|
306
|
+
if (wouldOverflowBytes || wouldOverflowFiles) {
|
|
307
|
+
batches.push(current);
|
|
308
|
+
current = [];
|
|
309
|
+
currentBytes = 0;
|
|
310
|
+
}
|
|
311
|
+
current.push(file);
|
|
312
|
+
currentBytes += file.size;
|
|
313
|
+
}
|
|
314
|
+
if (current.length > 0) batches.push(current);
|
|
315
|
+
return batches;
|
|
316
|
+
}
|
|
317
|
+
function parseResponseBody(body, status, url) {
|
|
318
|
+
try {
|
|
319
|
+
var _payload$error$messag, _payload$error;
|
|
320
|
+
const payload = JSON.parse(body);
|
|
321
|
+
if (payload.ok) return void 0;
|
|
322
|
+
return (_payload$error$messag = (_payload$error = payload.error) === null || _payload$error === void 0 ? void 0 : _payload$error.message) !== null && _payload$error$messag !== void 0 ? _payload$error$messag : `Firefly: 上传失败(HTTP ${status},POST ${url})`;
|
|
323
|
+
} catch {
|
|
324
|
+
return `Firefly: 上传响应不是 JSON(HTTP ${status},POST ${url},响应: ${responseSnippet(body)})`;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
async function sendBatch(batch, options, attempt) {
|
|
328
|
+
var _options$dist;
|
|
329
|
+
const url = `${options.endpoint.replace(/\/+$/, "")}/api/ingest/v1/sourcemaps`;
|
|
330
|
+
const body = JSON.stringify({
|
|
331
|
+
version: options.release,
|
|
332
|
+
dist: (_options$dist = options.dist) !== null && _options$dist !== void 0 ? _options$dist : "",
|
|
333
|
+
buildId: options.buildId,
|
|
334
|
+
files: batch.map(({ filePath, content }) => ({
|
|
335
|
+
filePath,
|
|
336
|
+
content
|
|
337
|
+
}))
|
|
338
|
+
});
|
|
339
|
+
let response;
|
|
340
|
+
try {
|
|
341
|
+
var _options$timeoutMs;
|
|
342
|
+
response = await postJson(url, body, {
|
|
343
|
+
"authorization": `Bearer ${options.token}`,
|
|
344
|
+
"content-type": "application/json"
|
|
345
|
+
}, (_options$timeoutMs = options.timeoutMs) !== null && _options$timeoutMs !== void 0 ? _options$timeoutMs : DEFAULT_TIMEOUT_MS);
|
|
346
|
+
} catch (error) {
|
|
347
|
+
var _options$retries;
|
|
348
|
+
if (attempt < ((_options$retries = options.retries) !== null && _options$retries !== void 0 ? _options$retries : DEFAULT_RETRIES)) {
|
|
349
|
+
await sleep(Math.min(500 * 2 ** attempt, MAX_RETRY_DELAY_MS));
|
|
350
|
+
return sendBatch(batch, options, attempt + 1);
|
|
351
|
+
}
|
|
352
|
+
throw error;
|
|
353
|
+
}
|
|
354
|
+
if (response.status === 413) {
|
|
355
|
+
if (batch.length <= 1) {
|
|
356
|
+
var _file$size;
|
|
357
|
+
const file = batch[0];
|
|
358
|
+
throw new SourcemapUploadError(`Firefly: 单个 sourcemap(${formatBytes((_file$size = file === null || file === void 0 ? void 0 : file.size) !== null && _file$size !== void 0 ? _file$size : 0)})仍超过上传 body 上限,请检查服务端和网关的 bodyLimit`, 413);
|
|
359
|
+
}
|
|
360
|
+
const middle = Math.ceil(batch.length / 2);
|
|
361
|
+
return await sendBatch(batch.slice(0, middle), options, 0) + await sendBatch(batch.slice(middle), options, 0);
|
|
362
|
+
}
|
|
363
|
+
if (response.status === 429) {
|
|
364
|
+
var _options$retries2;
|
|
365
|
+
if (attempt < ((_options$retries2 = options.retries) !== null && _options$retries2 !== void 0 ? _options$retries2 : DEFAULT_RETRIES)) {
|
|
366
|
+
var _retryAfterMs;
|
|
367
|
+
await sleep((_retryAfterMs = retryAfterMs(response.headers)) !== null && _retryAfterMs !== void 0 ? _retryAfterMs : Math.min(1e3 * 2 ** attempt, MAX_RETRY_DELAY_MS));
|
|
368
|
+
return sendBatch(batch, options, attempt + 1);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
if (response.status >= 500) {
|
|
372
|
+
var _options$retries3;
|
|
373
|
+
if (attempt < ((_options$retries3 = options.retries) !== null && _options$retries3 !== void 0 ? _options$retries3 : DEFAULT_RETRIES)) {
|
|
374
|
+
await sleep(Math.min(500 * 2 ** attempt, MAX_RETRY_DELAY_MS));
|
|
375
|
+
return sendBatch(batch, options, attempt + 1);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
if (response.status < 200 || response.status >= 300) {
|
|
379
|
+
var _parseResponseBody;
|
|
380
|
+
throw new SourcemapUploadError((_parseResponseBody = parseResponseBody(response.body, response.status, url)) !== null && _parseResponseBody !== void 0 ? _parseResponseBody : `Firefly: 上传失败(HTTP ${response.status},POST ${url})`, response.status);
|
|
381
|
+
}
|
|
382
|
+
const message = parseResponseBody(response.body, response.status, url);
|
|
383
|
+
if (message) throw new SourcemapUploadError(message, response.status);
|
|
384
|
+
try {
|
|
385
|
+
var _payload$data$uploade, _payload$data;
|
|
386
|
+
return (_payload$data$uploade = (_payload$data = JSON.parse(response.body).data) === null || _payload$data === void 0 ? void 0 : _payload$data.uploaded) !== null && _payload$data$uploade !== void 0 ? _payload$data$uploade : batch.length;
|
|
387
|
+
} catch {
|
|
388
|
+
return batch.length;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
/** 分批上传 source map。 */
|
|
392
|
+
async function uploadSourcemaps(files, options) {
|
|
393
|
+
var _options$batchBytes, _options$maxFiles;
|
|
394
|
+
if (files.length === 0) return {
|
|
395
|
+
uploaded: 0,
|
|
396
|
+
batches: 0,
|
|
397
|
+
bytes: 0
|
|
398
|
+
};
|
|
399
|
+
const batches = createBatches(files, (_options$batchBytes = options.batchBytes) !== null && _options$batchBytes !== void 0 ? _options$batchBytes : DEFAULT_BATCH_BYTES, (_options$maxFiles = options.maxFiles) !== null && _options$maxFiles !== void 0 ? _options$maxFiles : DEFAULT_MAX_FILES);
|
|
400
|
+
let uploaded = 0;
|
|
401
|
+
for (let index = 0; index < batches.length; index++) {
|
|
402
|
+
const batch = batches[index];
|
|
403
|
+
if (options.log !== false) console.log(`[firefly] sourcemap upload ${index + 1}/${batches.length} (${batch.length} files)`);
|
|
404
|
+
uploaded += await sendBatch(batch, options, 0);
|
|
405
|
+
}
|
|
406
|
+
return {
|
|
407
|
+
uploaded,
|
|
408
|
+
batches: batches.length,
|
|
409
|
+
bytes: files.reduce((sum, file) => sum + file.size, 0)
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
/** 上传成功后删除本地 .map。只删除 map,不删除 JS。 */
|
|
413
|
+
async function deleteSourcemapFiles(files, log = true) {
|
|
414
|
+
for (const file of files) try {
|
|
415
|
+
await unlink(file.mapFile);
|
|
416
|
+
} catch (error) {
|
|
417
|
+
if (log) console.warn(`[firefly] 删除 sourcemap 失败:${file.mapFile}(${error instanceof Error ? error.message : String(error)})`);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
// src/sourcemap/run.ts
|
|
421
|
+
function isCI() {
|
|
422
|
+
return process.env.CI === "true" || process.env.CI === "1";
|
|
423
|
+
}
|
|
424
|
+
function unique(values) {
|
|
425
|
+
return [...new Set(values)];
|
|
426
|
+
}
|
|
427
|
+
function errorMessage(error) {
|
|
428
|
+
return error instanceof Error ? error.message : String(error);
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* 构建结束后的统一 source map 上传流程。
|
|
432
|
+
*
|
|
433
|
+
* webpack 和 Vite adapter 只负责算出 outDir / mapDirs / base,
|
|
434
|
+
* 其余逻辑都在这里复用。
|
|
435
|
+
*/
|
|
436
|
+
async function runSourcemapUpload(input) {
|
|
437
|
+
var _input$mode, _sourcemap$enabled, _options$endpoint, _sourcemap$token, _options$release, _options$buildId, _sourcemap$platform, _ref, _ref2, _options$dist, _ref3, _sourcemap$sourceRoot;
|
|
438
|
+
const { options, sourcemap } = input;
|
|
439
|
+
const log = (message) => {
|
|
440
|
+
if (sourcemap.log !== false) console.log(`[firefly] ${message}`);
|
|
441
|
+
};
|
|
442
|
+
const warn = (message) => {
|
|
443
|
+
if (sourcemap.log !== false) console.warn(`[firefly] ${message}`);
|
|
444
|
+
};
|
|
445
|
+
if (((_input$mode = input.mode) !== null && _input$mode !== void 0 ? _input$mode : process.env.NODE_ENV) === "development" && !sourcemap.uploadInDevelopment) {
|
|
446
|
+
log("跳过 sourcemap 上传:当前不是生产构建");
|
|
447
|
+
return null;
|
|
448
|
+
}
|
|
449
|
+
if (input.isWatch && !sourcemap.uploadOnWatch) {
|
|
450
|
+
log("跳过 sourcemap 上传:watch 模式默认不上传");
|
|
451
|
+
return null;
|
|
452
|
+
}
|
|
453
|
+
if (!((_sourcemap$enabled = sourcemap.enabled) !== null && _sourcemap$enabled !== void 0 ? _sourcemap$enabled : isCI())) {
|
|
454
|
+
log("跳过 sourcemap 上传:sourcemap.enabled 未开启");
|
|
455
|
+
return null;
|
|
456
|
+
}
|
|
457
|
+
const endpoint = (_options$endpoint = options.endpoint) !== null && _options$endpoint !== void 0 ? _options$endpoint : process.env.FIREFLY_ENDPOINT;
|
|
458
|
+
const token = (_sourcemap$token = sourcemap.token) !== null && _sourcemap$token !== void 0 ? _sourcemap$token : process.env.FIREFLY_TOKEN;
|
|
459
|
+
const release = (_options$release = options.release) !== null && _options$release !== void 0 ? _options$release : process.env.FIREFLY_RELEASE;
|
|
460
|
+
const buildId = ((_options$buildId = options.buildId) === null || _options$buildId === void 0 ? void 0 : _options$buildId.trim()) || randomUUID();
|
|
461
|
+
const platform = (_sourcemap$platform = sourcemap.platform) !== null && _sourcemap$platform !== void 0 ? _sourcemap$platform : process.env.UNI_PLATFORM;
|
|
462
|
+
const dist = (_ref = (_ref2 = (_options$dist = options.dist) !== null && _options$dist !== void 0 ? _options$dist : process.env.FIREFLY_DIST) !== null && _ref2 !== void 0 ? _ref2 : platform === "h5" ? "web" : platform) !== null && _ref !== void 0 ? _ref : "";
|
|
463
|
+
if (!endpoint) {
|
|
464
|
+
warn("跳过 sourcemap 上传:缺少 endpoint(或 FIREFLY_ENDPOINT)");
|
|
465
|
+
return null;
|
|
466
|
+
}
|
|
467
|
+
if (!token) {
|
|
468
|
+
warn("跳过 sourcemap 上传:缺少 token(或 FIREFLY_TOKEN)");
|
|
469
|
+
return null;
|
|
470
|
+
}
|
|
471
|
+
if (!release) {
|
|
472
|
+
warn("跳过 sourcemap 上传:缺少 release(或 FIREFLY_RELEASE)");
|
|
473
|
+
return null;
|
|
474
|
+
}
|
|
475
|
+
const outDir = resolve(input.context, input.outDir);
|
|
476
|
+
const sourceRoot = (_ref3 = (_sourcemap$sourceRoot = sourcemap.sourceRoot) !== null && _sourcemap$sourceRoot !== void 0 ? _sourcemap$sourceRoot : input.sourceRoot) !== null && _ref3 !== void 0 ? _ref3 : input.context;
|
|
477
|
+
try {
|
|
478
|
+
let files = [];
|
|
479
|
+
let usedMapDir = "";
|
|
480
|
+
for (const candidate of unique(input.mapDirs)) {
|
|
481
|
+
const mapDir = resolve(input.context, candidate);
|
|
482
|
+
const collected = await collectSourcemaps({
|
|
483
|
+
mapDir,
|
|
484
|
+
outDir,
|
|
485
|
+
base: input.base,
|
|
486
|
+
include: sourcemap.include,
|
|
487
|
+
exclude: sourcemap.exclude,
|
|
488
|
+
skipVendorOnly: sourcemap.skipVendorOnly,
|
|
489
|
+
sourcesContent: sourcemap.sourcesContent,
|
|
490
|
+
sourceRoot
|
|
491
|
+
});
|
|
492
|
+
if (collected.length > 0) {
|
|
493
|
+
files = collected;
|
|
494
|
+
usedMapDir = mapDir;
|
|
495
|
+
break;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
if (files.length === 0) {
|
|
499
|
+
warn(`跳过 sourcemap 上传:${outDir} 及其 .sourcemap 下没有可上传的 map`);
|
|
500
|
+
return null;
|
|
501
|
+
}
|
|
502
|
+
const bytes = files.reduce((sum, file) => sum + file.size, 0);
|
|
503
|
+
log(`sourcemap upload url: ${`${endpoint.replace(/\/+$/, "")}/api/ingest/v1/sourcemaps`}`);
|
|
504
|
+
log(`sourcemap upload: ${files.length} files, ${(bytes / 1024 / 1024).toFixed(2)} MB, release=${release}, dist=${dist || "(empty)"}, build=${buildId}, mapDir=${usedMapDir}`);
|
|
505
|
+
const result = await uploadSourcemaps(files, {
|
|
506
|
+
endpoint,
|
|
507
|
+
token,
|
|
508
|
+
release,
|
|
509
|
+
dist,
|
|
510
|
+
buildId,
|
|
511
|
+
batchBytes: sourcemap.batchBytes,
|
|
512
|
+
maxFiles: sourcemap.maxFiles,
|
|
513
|
+
retries: sourcemap.retries,
|
|
514
|
+
timeoutMs: sourcemap.timeoutMs,
|
|
515
|
+
log: sourcemap.log
|
|
516
|
+
});
|
|
517
|
+
log(`sourcemap upload done: ${result.uploaded} files, ${result.batches} batches`);
|
|
518
|
+
if (sourcemap.deleteAfterUpload) {
|
|
519
|
+
await deleteSourcemapFiles(files, sourcemap.log !== false);
|
|
520
|
+
log("sourcemap 文件已删除");
|
|
521
|
+
}
|
|
522
|
+
return {
|
|
523
|
+
files,
|
|
524
|
+
mapDir: usedMapDir,
|
|
525
|
+
result
|
|
526
|
+
};
|
|
527
|
+
} catch (error) {
|
|
528
|
+
const message = `sourcemap 上传失败:${errorMessage(error)}`;
|
|
529
|
+
if (sourcemap.failOnError) throw error;
|
|
530
|
+
warn(message);
|
|
531
|
+
return null;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
export { buildInlineScript as n, injectInlineScriptHtml as r, runSourcemapUpload as t };
|
|
535
|
+
|
|
536
|
+
//# sourceMappingURL=run-CXBdNHC1.js.map
|
package/dist/vite.cjs
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
Object.defineProperties(exports, {
|
|
2
|
+
__esModule: { value: true },
|
|
3
|
+
[Symbol.toStringTag]: { value: "Module" }
|
|
4
|
+
});
|
|
5
|
+
const require_run = require("./run-CU6mcdw9.cjs");
|
|
6
|
+
let node_path = require("node:path");
|
|
7
|
+
// src/vite.ts
|
|
8
|
+
/**
|
|
9
|
+
* Firefly Vite 插件。
|
|
10
|
+
*
|
|
11
|
+
* - `sourcemap.generate: true` 时自动设置 `build.sourcemap = true`
|
|
12
|
+
* - `closeBundle` 后扫描并上传 source map
|
|
13
|
+
* - 只处理 build,不影响 dev server
|
|
14
|
+
*/
|
|
15
|
+
function fireflyVite(options = {}) {
|
|
16
|
+
var _options$sourcemap;
|
|
17
|
+
const sourcemap = options.sourcemap === false ? null : (_options$sourcemap = options.sourcemap) !== null && _options$sourcemap !== void 0 ? _options$sourcemap : null;
|
|
18
|
+
let resolvedConfig = null;
|
|
19
|
+
return {
|
|
20
|
+
name: "firefly",
|
|
21
|
+
apply: "build",
|
|
22
|
+
config() {
|
|
23
|
+
if (!(sourcemap === null || sourcemap === void 0 ? void 0 : sourcemap.generate)) return void 0;
|
|
24
|
+
return { build: { sourcemap: true } };
|
|
25
|
+
},
|
|
26
|
+
configResolved(config) {
|
|
27
|
+
resolvedConfig = config;
|
|
28
|
+
},
|
|
29
|
+
transformIndexHtml: {
|
|
30
|
+
order: "pre",
|
|
31
|
+
handler() {
|
|
32
|
+
const code = require_run.buildInlineScript(options);
|
|
33
|
+
if (!code) return void 0;
|
|
34
|
+
return [{
|
|
35
|
+
tag: "script",
|
|
36
|
+
attrs: {
|
|
37
|
+
"data-firefly-inline": "",
|
|
38
|
+
...options.script && options.script.nonce ? { nonce: options.script.nonce } : {}
|
|
39
|
+
},
|
|
40
|
+
children: code,
|
|
41
|
+
injectTo: "head-prepend"
|
|
42
|
+
}];
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
async closeBundle() {
|
|
46
|
+
var _sourcemap$platform, _sourcemap$base, _sourcemap$sourceRoot;
|
|
47
|
+
if (!sourcemap || !resolvedConfig) return;
|
|
48
|
+
const root = resolvedConfig.root;
|
|
49
|
+
const outDir = (0, node_path.resolve)(root, resolvedConfig.build.outDir);
|
|
50
|
+
const platform = (_sourcemap$platform = sourcemap.platform) !== null && _sourcemap$platform !== void 0 ? _sourcemap$platform : process.env.UNI_PLATFORM;
|
|
51
|
+
const mapDirs = [
|
|
52
|
+
sourcemap.mapDir,
|
|
53
|
+
platform ? (0, node_path.resolve)(outDir, "..", ".sourcemap", platform) : "",
|
|
54
|
+
outDir
|
|
55
|
+
].filter((value) => Boolean(value));
|
|
56
|
+
await require_run.runSourcemapUpload({
|
|
57
|
+
options,
|
|
58
|
+
sourcemap,
|
|
59
|
+
outDir,
|
|
60
|
+
mapDirs,
|
|
61
|
+
base: (_sourcemap$base = sourcemap.base) !== null && _sourcemap$base !== void 0 ? _sourcemap$base : resolvedConfig.base,
|
|
62
|
+
mode: resolvedConfig.mode,
|
|
63
|
+
isWatch: resolvedConfig.build.watch != null,
|
|
64
|
+
context: root,
|
|
65
|
+
sourceRoot: (_sourcemap$sourceRoot = sourcemap.sourceRoot) !== null && _sourcemap$sourceRoot !== void 0 ? _sourcemap$sourceRoot : root
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
exports.default = fireflyVite;
|
|
71
|
+
exports.fireflyVite = fireflyVite;
|
|
72
|
+
|
|
73
|
+
//# sourceMappingURL=vite.cjs.map
|
package/dist/vite.d.cts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { i as FireflyViteOptions } from "./options-Dn99v-iv.cjs";
|
|
2
|
+
import { Plugin } from "vite";
|
|
3
|
+
/**
|
|
4
|
+
* Firefly Vite 插件。
|
|
5
|
+
*
|
|
6
|
+
* - `sourcemap.generate: true` 时自动设置 `build.sourcemap = true`
|
|
7
|
+
* - `closeBundle` 后扫描并上传 source map
|
|
8
|
+
* - 只处理 build,不影响 dev server
|
|
9
|
+
*/
|
|
10
|
+
export declare function fireflyVite(options?: FireflyViteOptions): Plugin;
|
|
11
|
+
export { fireflyVite as default };
|
|
12
|
+
//# sourceMappingURL=vite.d.cts.map
|
package/dist/vite.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { i as FireflyViteOptions } from "./options-Dn99v-iv.js";
|
|
2
|
+
import { Plugin } from "vite";
|
|
3
|
+
/**
|
|
4
|
+
* Firefly Vite 插件。
|
|
5
|
+
*
|
|
6
|
+
* - `sourcemap.generate: true` 时自动设置 `build.sourcemap = true`
|
|
7
|
+
* - `closeBundle` 后扫描并上传 source map
|
|
8
|
+
* - 只处理 build,不影响 dev server
|
|
9
|
+
*/
|
|
10
|
+
export declare function fireflyVite(options?: FireflyViteOptions): Plugin;
|
|
11
|
+
export { fireflyVite as default };
|
|
12
|
+
//# sourceMappingURL=vite.d.ts.map
|