@frontend-monitor/upload-sourcemaps 0.2.0 → 0.2.1

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/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@frontend-monitor/upload-sourcemaps",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "CLI to upload Source Map files to frontend-monitor API (for CI/CD)",
5
5
  "type": "module",
6
6
  "main": "dist/upload.js",
7
7
  "bin": {
8
- "upload-sourcemaps": "./bin/upload-sourcemaps.js"
8
+ "upload-sourcemaps": "./dist/upload-sourcemaps.js"
9
9
  },
10
10
  "scripts": {
11
11
  "build": "node build.js",
@@ -1,338 +0,0 @@
1
- #!/usr/bin/env node
2
- /** upload-sourcemaps [build] [options] - 上传 Source Map 到监控 API */
3
- import { readFile, writeFile, access } from 'fs/promises';
4
- import { resolve, dirname, join } from 'path';
5
- import { fileURLToPath } from 'url';
6
- import { randomUUID } from 'crypto';
7
- import { spawn } from 'child_process';
8
- import { uploadAll, uploadArchiveThenDelete, uploadArchiveChunkedThenDelete } from '../lib/upload.js';
9
- import { getBundlerSourceMapConfig, findProjectRoot } from '../lib/detect-sourcemap-config.js';
10
- import { prepareForceSourcemap, restoreFromBackup, PATCH_BACKUP_FILENAME } from '../lib/force-sourcemap-build.js';
11
-
12
- const __dirname = dirname(fileURLToPath(import.meta.url));
13
-
14
- function getArg(name, envName) {
15
- const env = process.env[envName];
16
- if (env) return env;
17
- const i = process.argv.indexOf(name);
18
- if (i !== -1 && process.argv[i + 1]) return process.argv[i + 1];
19
- return null;
20
- }
21
-
22
- function hasFlag(name) {
23
- return process.argv.includes(name);
24
- }
25
-
26
- /** 解析分片大小字符串(如 5M、5MB、1048576)为字节数。@param {string|number} v @returns {number|null} */
27
- function parseChunkSizeFromValue(v) {
28
- if (v == null || v === '') return null;
29
- const s = String(v).trim().toUpperCase();
30
- const num = parseInt(s.replace(/[^0-9]/g, ''), 10) || 0;
31
- if (s.endsWith('G')) return num * 1024 * 1024 * 1024;
32
- if (s.endsWith('MB') || s.endsWith('M')) return num * 1024 * 1024;
33
- if (s.endsWith('KB') || s.endsWith('K')) return num * 1024;
34
- if (/^\d+$/.test(String(v).trim())) return parseInt(String(v).trim(), 10) || null;
35
- return num > 0 ? num * 1024 * 1024 : null;
36
- }
37
-
38
- /** 从 --chunk-size / 环境变量 / config 解析分片大小(字节)。@param {object} [cfg] cfg.chunkSize @returns {number|null} */
39
- function parseChunkSize(cfg = {}) {
40
- const v = getArg('--chunk-size', 'UPLOAD_SOURCEMAPS_CHUNK_SIZE') || cfg.chunkSize;
41
- return parseChunkSizeFromValue(v);
42
- }
43
-
44
- /** 加载上传配置(monitor.config.js + .upload-sourcemaps.json 或 --config 指定文件)。@returns {Promise<object>} */
45
- async function loadConfig() {
46
- const { resolveUploadConfig } = await import('../lib/load-monitor-config.js');
47
- return resolveUploadConfig(process.cwd(), getArg('--config', 'UPLOAD_SOURCEMAPS_CONFIG') || undefined);
48
- }
49
-
50
- /** 从目录向上查找 package.json 取 version。@param {string} dir 起始目录 @returns {Promise<string|null>} */
51
- async function getVersionFromPackageJson(dir) {
52
- const candidates = [dir, resolve(dir, '..'), process.cwd()];
53
- for (const d of candidates) {
54
- try {
55
- const raw = await readFile(join(d, 'package.json'), 'utf-8');
56
- const pkg = JSON.parse(raw);
57
- if (pkg.version && typeof pkg.version === 'string') {
58
- return pkg.version.trim();
59
- }
60
- } catch {
61
- continue;
62
- }
63
- }
64
- return null;
65
- }
66
-
67
- /** 解析 release:显式 > config > package.json version > 'default'。@param {string} [explicitOrConfig] @param {string} [dir] @returns {Promise<string>} */
68
- async function resolveRelease(explicitOrConfig, dir) {
69
- if (explicitOrConfig && explicitOrConfig !== 'default') return explicitOrConfig;
70
- const fromPkg = await getVersionFromPackageJson(dir);
71
- return fromPkg || 'default';
72
- }
73
-
74
- let config = {};
75
- try {
76
- config = await loadConfig();
77
- } catch {
78
- config = {};
79
- }
80
- const isBuild = process.argv[2] === 'build';
81
- const isPatch = process.argv[2] === 'patch';
82
- const isRestore = process.argv[2] === 'restore';
83
- const isInit = process.argv[2] === 'init';
84
-
85
- function effectiveNoEncrypt(noEncryptFlag, configNoEncrypt) {
86
- return noEncryptFlag || configNoEncrypt === true;
87
- }
88
-
89
- async function runBuildThenUpload() {
90
- const projectId = getArg('--project-id', 'PROJECT_ID') || config.projectId;
91
- const secretKey = getArg('--secret-key', 'SECRET_KEY') || config.secretKey;
92
- const authCert = getArg('--auth-cert', 'MONITOR_AUTH_CERT') || config.authCert;
93
- const endpoint = (getArg('--endpoint', 'ENDPOINT') || config.endpoint || '').replace(/\/$/, '');
94
- if (!endpoint) {
95
- throw new Error('build subcommand requires endpoint (config file or env/args).');
96
- }
97
- if (!authCert && (!projectId || !secretKey)) {
98
- throw new Error('build subcommand requires projectId+secretKey or authCert (config file or env/args).');
99
- }
100
- const buildCommand = config.buildCommand || process.env.UPLOAD_SOURCEMAPS_BUILD_CMD || 'npm run build';
101
- const buildOutputDir = config.buildOutputDir || config.dir || getArg('--dir', 'DIR') || process.cwd();
102
- const useArchive = hasFlag('--archive') || config.uploadAsArchive === true;
103
- const noEncrypt = effectiveNoEncrypt(hasFlag('--no-encrypt'), config.noEncrypt);
104
- let uploadModeBuild = getArg('--upload-mode', 'UPLOAD_MODE') || config.uploadMode || 'mapOnly';
105
- if (uploadModeBuild === 'all') uploadModeBuild = 'backup';
106
- const bundlerConfig = await getBundlerSourceMapConfig(buildOutputDir);
107
- const deleteAfter = !bundlerConfig.keepMapInOutput || hasFlag('--delete-after-upload') || hasFlag('-d') || config.deleteAfterUpload === true;
108
- const buildId = getArg('--build-id', 'BUILD_ID') || config.buildId || randomUUID();
109
- const forceRestore = await prepareForceSourcemap(buildOutputDir);
110
- try {
111
- console.log('Step 1: Running build (forcing source map generation):', buildCommand);
112
- console.log('Build ID:', buildId);
113
- const [cmd, ...args] = buildCommand.split(/\s+/);
114
- await new Promise((res, rej) => {
115
- const child = spawn(cmd, args, {
116
- stdio: 'inherit',
117
- shell: true,
118
- env: { ...process.env, BUILD_SOURCEMAP: '1', SOURCEMAP: '1', MONITOR_BUILD_ID: buildId, VITE_MONITOR_BUILD_ID: buildId },
119
- });
120
- child.on('exit', (code) => (code === 0 ? res() : rej(new Error(`Build exited ${code}`))));
121
- });
122
- console.log('Step 2: Uploading source maps...');
123
- const release = await resolveRelease(getArg('--release', 'RELEASE') || config.release, buildOutputDir);
124
- if (release !== 'default') console.log('Release (from package.json or config):', release);
125
- const chunkSizeBytes = parseChunkSize(config);
126
- if (useArchive) {
127
- const archiveOpts = { dir: buildOutputDir, endpoint, projectId, secretKey, authCert, release, buildId, deleteEvenOnFailure: false, noEncrypt, uploadMode: uploadModeBuild, keepMapInOutput: bundlerConfig.keepMapInOutput };
128
- const archiveResult = chunkSizeBytes
129
- ? await uploadArchiveChunkedThenDelete({ ...archiveOpts, chunkSizeBytes }, console)
130
- : await uploadArchiveThenDelete(archiveOpts, console);
131
- if (archiveResult.uploaded === 0) {
132
- console.log('No .map files in output. For Vite we patch config to force source map; check build output dir and README.');
133
- }
134
- } else {
135
- const buildConcurrency = Math.max(1, Math.min(64, parseInt(getArg('--concurrency', 'CONCURRENCY') || config.concurrency || '12', 10) || 12));
136
- const { ok, fail } = await uploadAll(
137
- { endpoint, projectId, secretKey, authCert, release, buildId, dir: buildOutputDir, deleteAfterUpload: deleteAfter, deleteEvenOnFailure: false, concurrency: buildConcurrency },
138
- console
139
- );
140
- if (fail > 0) {
141
- throw new Error('Upload had failures. Local .map files were NOT deleted. Fix errors and re-run.');
142
- }
143
- if (ok === 0) {
144
- console.log('No .map files in output. For Vite we patch config to force source map; check build output dir and README.');
145
- }
146
- }
147
- console.log('Done. Output dir:', buildOutputDir);
148
- } catch (err) {
149
- console.error('Error:', err.message);
150
- if (deleteAfter) {
151
- console.error('If the failure was during upload, local .map files may have been removed. Fix the issue and re-run.');
152
- } else {
153
- console.error('Local files were not modified. Fix the issue and re-run.');
154
- }
155
- throw err;
156
- } finally {
157
- await forceRestore.restore();
158
- }
159
- }
160
-
161
- /** 内置推荐的 npm scripts,减轻使用方配置成本 */
162
- const RECOMMENDED_SCRIPTS = {
163
- 'build:upload': 'upload-sourcemaps build',
164
- 'build:upload:archive': 'upload-sourcemaps build --archive',
165
- };
166
-
167
- async function runInit() {
168
- const cwd = process.cwd();
169
- const root = await findProjectRoot(cwd);
170
- if (!root) {
171
- console.error('[upload-sourcemaps] Cannot find project root (no package.json from:', cwd, '). Run from project root.');
172
- process.exitCode = 1;
173
- return;
174
- }
175
- const pkgPath = join(root, 'package.json');
176
- let pkg;
177
- try {
178
- const raw = await readFile(pkgPath, 'utf-8');
179
- pkg = JSON.parse(raw);
180
- } catch (e) {
181
- console.error('[upload-sourcemaps] Failed to read package.json:', e.message);
182
- process.exitCode = 1;
183
- return;
184
- }
185
- const scripts = pkg.scripts || {};
186
- let added = 0;
187
- for (const [name, cmd] of Object.entries(RECOMMENDED_SCRIPTS)) {
188
- if (!scripts[name]) {
189
- scripts[name] = cmd;
190
- added++;
191
- }
192
- }
193
- pkg.scripts = scripts;
194
- try {
195
- await writeFile(pkgPath, JSON.stringify(pkg, null, 2) + '\n', 'utf-8');
196
- } catch (e) {
197
- console.error('[upload-sourcemaps] Failed to write package.json:', e.message);
198
- process.exitCode = 1;
199
- return;
200
- }
201
- let hasJson = false;
202
- try {
203
- await access(join(root, '.upload-sourcemaps.json'));
204
- hasJson = true;
205
- } catch {
206
- // no file
207
- }
208
- console.log('[upload-sourcemaps] init ok. Added', added, 'script(s) to package.json.');
209
- if (added > 0) {
210
- console.log(' Use: npm run build:upload (build + upload .map)');
211
- console.log(' Or: npm run build:upload:archive (build + pack & upload, recommended)');
212
- }
213
- if (!hasJson) {
214
- console.log(' Config: add monitor.config.js or .upload-sourcemaps.json (see README).');
215
- }
216
- }
217
-
218
- if (isInit) {
219
- await runInit();
220
- } else if (isPatch) {
221
- const patchDir = getArg('--dir', 'DIR') || config.dir || process.cwd();
222
- const root = await findProjectRoot(patchDir);
223
- if (!root) {
224
- console.error('[upload-sourcemaps] Could not find project root (no package.json from dir:', patchDir, '). Fix --dir or run from project root.');
225
- process.exitCode = 1;
226
- } else {
227
- const backupPath = join(root, PATCH_BACKUP_FILENAME);
228
- const result = await prepareForceSourcemap(patchDir, { writeBackupTo: backupPath });
229
- if (result.patchedFile) {
230
- try {
231
- const { readFile } = await import('fs/promises');
232
- const patchedContent = await readFile(result.patchedFile, 'utf-8');
233
- const hasTop = /sourcemap\s*:\s*\{\s*[\s\S]*?server\s*:\s*true[\s\S]*?client\s*:\s*true/.test(patchedContent) || /sourcemap\s*:\s*\{\s*[\s\S]*?client\s*:\s*true[\s\S]*?server\s*:\s*true/.test(patchedContent);
234
- const hasViteTrue = /sourcemap\s*:\s*true/.test(patchedContent);
235
- console.log('[upload-sourcemaps] Verified:', hasTop ? 'top-level sourcemap' : '(no top-level)', hasViteTrue ? 'vite.build.sourcemap=true' : '');
236
- } catch (_) {}
237
- console.log('Run your build, then run: upload-sourcemaps restore');
238
- } else {
239
- console.log('No config needed patching (source map already enabled or unsupported bundler).');
240
- }
241
- }
242
- } else if (isRestore) {
243
- const backupPath = getArg('--backup', 'UPLOAD_SOURCEMAPS_PATCH_BACKUP') || join(process.cwd(), PATCH_BACKUP_FILENAME);
244
- const restored = await restoreFromBackup(backupPath);
245
- if (!restored) {
246
- console.error('Restore failed or no backup file found at', backupPath);
247
- process.exitCode = 1;
248
- }
249
- } else if (isBuild) {
250
- try {
251
- await runBuildThenUpload();
252
- } catch (err) {
253
- process.exitCode = 1;
254
- }
255
- } else {
256
- const projectId = getArg('--project-id', 'PROJECT_ID') || config.projectId;
257
- const secretKey = getArg('--secret-key', 'SECRET_KEY') || config.secretKey;
258
- const authCert = getArg('--auth-cert', 'MONITOR_AUTH_CERT') || config.authCert;
259
- const endpoint = (getArg('--endpoint', 'ENDPOINT') || config.endpoint || '').replace(/\/$/, '');
260
- const dir = getArg('--dir', 'DIR') || config.dir || process.cwd();
261
- const release = await resolveRelease(getArg('--release', 'RELEASE') || config.release, dir);
262
- const buildId = getArg('--build-id', 'BUILD_ID') || config.buildId || randomUUID();
263
- const useArchive = hasFlag('--archive') || config.uploadAsArchive === true;
264
- const noEncrypt = effectiveNoEncrypt(hasFlag('--no-encrypt'), config.noEncrypt);
265
- const chunkSizeBytes = parseChunkSize(config);
266
- const concurrency = Math.max(1, Math.min(64, parseInt(getArg('--concurrency', 'CONCURRENCY') || config.concurrency || '12', 10) || 12));
267
- let uploadMode = getArg('--upload-mode', 'UPLOAD_MODE') || config.uploadMode || 'mapOnly';
268
- if (uploadMode === 'all') uploadMode = 'backup';
269
- const bundlerConfig = await getBundlerSourceMapConfig(dir);
270
- const deleteAfter = !bundlerConfig.keepMapInOutput || hasFlag('--delete-after-upload') || hasFlag('-d') || config.deleteAfterUpload === true;
271
-
272
- if (!endpoint) {
273
- console.error('Usage: ENDPOINT is required.');
274
- } else if (!authCert && (!projectId || !secretKey)) {
275
- console.error('Usage: PROJECT_ID and SECRET_KEY are required, or use AUTH_CERT (certificate auth).');
276
- console.error('');
277
- console.error(' Arguments:');
278
- console.error(' --project-id <id> Project ID');
279
- console.error(' --secret-key <key> Secret Key');
280
- console.error(' --endpoint <url> API base URL (e.g. https://api.example.com/monitorApi)');
281
- console.error(' --auth-cert <cert> Certificate content (or MONITOR_AUTH_CERT); use with monitor.config.js, no secretKey needed');
282
- console.error(' --release <tag> Release version (default: read from package.json)');
283
- console.error(' --build-id <id> Build ID (default: auto-generated UUID)');
284
- console.error(' --dir <path> Directory (default: cwd)');
285
- console.error(' --concurrency <n> Max parallel uploads when not using --archive (default: 12)');
286
- console.error(' --delete-after-upload, -d Delete local .map after upload (from deployment output)');
287
- console.error(' --archive Pack as .tar.gz (encrypted by default) → upload → server decrypts and extracts');
288
- console.error(' --upload-mode <mode> mapOnly (default) | backup (code tar, .map 依项目配置是否保留)');
289
- console.error(' --chunk-size <size> With --archive: chunked upload (e.g. 1M, 5M), default 1MB, enables resumable and retry');
290
- console.error(' --no-encrypt With --archive: upload plain tar.gz (certificate auth implies no-encrypt)');
291
- console.error(' --config <path> Config file (default: monitor.config.js + .upload-sourcemaps.json)');
292
- console.error('');
293
- console.error(' Subcommands:');
294
- console.error(' init Add recommended scripts to package.json (build:upload, build:upload:archive)');
295
- console.error(' build One-step: run build then upload source maps');
296
- console.error(' patch / restore Temporarily enable source map in bundler config, then restore');
297
- console.error('');
298
- console.error(' Whether to delete .map after upload: inferred from project bundler config (Vite/Webpack etc.). Use -d to force delete.');
299
- process.exitCode = 1;
300
- } else {
301
- if (release !== 'default') console.log('Release:', release);
302
- console.log('Build ID:', buildId);
303
- try {
304
- if (useArchive) {
305
- const archiveOpts = { dir, endpoint, projectId, secretKey, authCert, release, buildId, deleteEvenOnFailure: false, noEncrypt, uploadMode, keepMapInOutput: bundlerConfig.keepMapInOutput };
306
- if (chunkSizeBytes) {
307
- await uploadArchiveChunkedThenDelete({ ...archiveOpts, chunkSizeBytes }, console);
308
- } else {
309
- await uploadArchiveThenDelete(archiveOpts, console);
310
- }
311
- } else {
312
- const { ok, fail, deleted } = await uploadAll(
313
- { endpoint, projectId, secretKey, authCert, release, buildId, dir, deleteAfterUpload: deleteAfter, deleteEvenOnFailure: deleteAfter, concurrency },
314
- console
315
- );
316
- if (deleted > 0) {
317
- console.log('Deleted', deleted, 'local .map file(s) from output.');
318
- }
319
- if (fail > 0) {
320
- if (deleteAfter) {
321
- console.error('Upload had failures. Local .map files were still deleted (per project config). Fix errors and re-run to re-upload.');
322
- } else {
323
- console.error('Upload had failures. Local .map files were NOT deleted. Fix errors and re-run.');
324
- }
325
- process.exitCode = 1;
326
- }
327
- }
328
- } catch (err) {
329
- console.error('Error:', err.message);
330
- if (deleteAfter) {
331
- console.error('Local .map files were still deleted. Fix the issue and re-run.');
332
- } else {
333
- console.error('Local files were not modified. Fix the issue and re-run.');
334
- }
335
- process.exitCode = 1;
336
- }
337
- }
338
- }