@9apes/cli 0.1.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 +145 -0
- package/dist/command.d.ts +8 -0
- package/dist/command.d.ts.map +1 -0
- package/dist/command.js +90 -0
- package/dist/command.js.map +1 -0
- package/dist/config.d.ts +7 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +37 -0
- package/dist/config.js.map +1 -0
- package/dist/index-dev.d.ts +3 -0
- package/dist/index-dev.d.ts.map +1 -0
- package/dist/index-dev.js +13 -0
- package/dist/index-dev.js.map +1 -0
- package/dist/index-local.d.ts +3 -0
- package/dist/index-local.d.ts.map +1 -0
- package/dist/index-local.js +13 -0
- package/dist/index-local.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/inject.d.ts +14 -0
- package/dist/inject.d.ts.map +1 -0
- package/dist/inject.js +161 -0
- package/dist/inject.js.map +1 -0
- package/dist/sourcemap.d.ts +48 -0
- package/dist/sourcemap.d.ts.map +1 -0
- package/dist/sourcemap.js +262 -0
- package/dist/sourcemap.js.map +1 -0
- package/dist/status.d.ts +17 -0
- package/dist/status.d.ts.map +1 -0
- package/dist/status.js +72 -0
- package/dist/status.js.map +1 -0
- package/dist/upload.d.ts +25 -0
- package/dist/upload.d.ts.map +1 -0
- package/dist/upload.js +119 -0
- package/dist/upload.js.map +1 -0
- package/dist/vite-plugin.d.ts +22 -0
- package/dist/vite-plugin.d.ts.map +1 -0
- package/dist/vite-plugin.js +95 -0
- package/dist/vite-plugin.js.map +1 -0
- package/package.json +61 -0
- package/src/command.ts +100 -0
- package/src/config.ts +44 -0
- package/src/index-dev.ts +16 -0
- package/src/index-local.ts +16 -0
- package/src/index.ts +12 -0
- package/src/inject.ts +195 -0
- package/src/sourcemap.ts +317 -0
- package/src/status.ts +94 -0
- package/src/upload.ts +190 -0
- package/src/vite-plugin.ts +160 -0
- package/tsconfig.json +30 -0
package/src/sourcemap.ts
ADDED
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
import { Config } from './config.js';
|
|
2
|
+
import { promises as fs } from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import fg from 'fast-glob';
|
|
5
|
+
import { execFile } from 'child_process';
|
|
6
|
+
import { promisify } from 'util';
|
|
7
|
+
import { randomUUID } from 'crypto';
|
|
8
|
+
import pLimit from 'p-limit';
|
|
9
|
+
import { injectDebugIdIntoFile, injectDebugIdIntoMapFile } from './inject.js';
|
|
10
|
+
import { uploadFile, type UploadError } from './upload.js';
|
|
11
|
+
import { createStatusBars } from './status.js';
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
const execFileAsync = promisify(execFile);
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Represents a pair of JavaScript file and its corresponding source map file
|
|
18
|
+
*/
|
|
19
|
+
export interface FilePair {
|
|
20
|
+
jsFile: string;
|
|
21
|
+
mapFile: string | null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Validate and resolve a directory path
|
|
26
|
+
* @param dirPath - Directory path to validate
|
|
27
|
+
* @returns Resolved absolute path
|
|
28
|
+
* @throws Error if path doesn't exist, isn't a directory, or lacks permissions
|
|
29
|
+
*/
|
|
30
|
+
export async function validateDirectoryPath(dirPath: string): Promise<string> {
|
|
31
|
+
// Validate and resolve the directory path
|
|
32
|
+
const resolvedPath = path.resolve(dirPath);
|
|
33
|
+
console.log(`🔍 Resolved path: ${resolvedPath}`);
|
|
34
|
+
|
|
35
|
+
// Check if path exists and get stats
|
|
36
|
+
let stats;
|
|
37
|
+
try {
|
|
38
|
+
stats = await fs.stat(resolvedPath);
|
|
39
|
+
} catch (error) {
|
|
40
|
+
if (error instanceof Error && 'code' in error && error.code === 'ENOENT') {
|
|
41
|
+
throw new Error(`Directory does not exist: ${resolvedPath}`);
|
|
42
|
+
}
|
|
43
|
+
throw error;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Verify it's a directory
|
|
47
|
+
if (!stats.isDirectory()) {
|
|
48
|
+
throw new Error(`Path is not a directory: ${resolvedPath}`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Check read and write permissions - Throws an error if the directory is not readable or writable
|
|
52
|
+
await fs.access(resolvedPath, fs.constants.R_OK | fs.constants.W_OK);
|
|
53
|
+
|
|
54
|
+
console.log(`✅ Directory validated: ${resolvedPath}`);
|
|
55
|
+
return resolvedPath;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Scan directory for JavaScript files and their corresponding source maps
|
|
60
|
+
* @param dirPath - Directory path to scan
|
|
61
|
+
* @returns Tuple containing file pairs with both .js and .js.map files, and JS files without maps
|
|
62
|
+
*/
|
|
63
|
+
export async function scanForJavaScriptFiles(dirPath: string): Promise<FilePair[]> {
|
|
64
|
+
try {
|
|
65
|
+
// Find all .js.map files directly
|
|
66
|
+
const [jsFiles, mapFiles] = await Promise.all([
|
|
67
|
+
fg('**/*.js', {
|
|
68
|
+
cwd: dirPath,
|
|
69
|
+
absolute: true,
|
|
70
|
+
ignore: ['**/node_modules/**', '**/.git/**', '**/.*/**']
|
|
71
|
+
}),
|
|
72
|
+
fg('**/*.js.map', {
|
|
73
|
+
cwd: dirPath,
|
|
74
|
+
absolute: true,
|
|
75
|
+
ignore: ['**/node_modules/**', '**/.git/**', '**/.*/**']
|
|
76
|
+
})
|
|
77
|
+
]);
|
|
78
|
+
|
|
79
|
+
console.log(`🔍 Found ${jsFiles.length} JavaScript files`);
|
|
80
|
+
|
|
81
|
+
// Create a Set for O(1) lookup
|
|
82
|
+
const mapFileSet = new Set(mapFiles);
|
|
83
|
+
const results: FilePair[] = [];
|
|
84
|
+
|
|
85
|
+
for (const jsFile of jsFiles) {
|
|
86
|
+
const mapFile = jsFile + '.map';
|
|
87
|
+
|
|
88
|
+
if (mapFileSet.has(mapFile)) {
|
|
89
|
+
results.push({ jsFile, mapFile });
|
|
90
|
+
} else {
|
|
91
|
+
results.push({ jsFile, mapFile: null });
|
|
92
|
+
console.warn(`⚠️ ${path.relative(dirPath, jsFile)} has no corresponding .js.map file`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
console.log(`📊 Total file pairs found: ${results.length}`);
|
|
97
|
+
return results;
|
|
98
|
+
|
|
99
|
+
} catch (error) {
|
|
100
|
+
console.error('❌ Error scanning for JavaScript files:', error);
|
|
101
|
+
throw error;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Get the current Git commit hash (HEAD)
|
|
107
|
+
* @param dirPath - Directory path to check for git repository
|
|
108
|
+
* @returns Git commit hash or undefined if not available
|
|
109
|
+
*/
|
|
110
|
+
export async function getGitCommitHash(dirPath: string): Promise<string | undefined> {
|
|
111
|
+
try {
|
|
112
|
+
const { stdout } = await execFileAsync('git', ['rev-parse', 'HEAD'], {
|
|
113
|
+
cwd: dirPath,
|
|
114
|
+
encoding: 'utf8',
|
|
115
|
+
// maxBuffer: 1024 * 1024 // Optional: if you expect large output
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
const commitHash = stdout.trim();
|
|
119
|
+
console.log(`📝 Git commit hash: ${commitHash}`);
|
|
120
|
+
return commitHash;
|
|
121
|
+
} catch {
|
|
122
|
+
console.log('ℹ️ No git commit hash available (not a git repository or no commits)');
|
|
123
|
+
return undefined;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Generate a unique debug ID using UUID v4
|
|
129
|
+
* @returns UUID string
|
|
130
|
+
*/
|
|
131
|
+
export function generateDebugId(): string {
|
|
132
|
+
return randomUUID();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Get the debug ID snippet for the given debug ID
|
|
137
|
+
* @param debugId - Debug ID to get the snippet for
|
|
138
|
+
* @returns Debug ID snippet
|
|
139
|
+
*/
|
|
140
|
+
export function getDebugIdSnippet(debugId: string): string {
|
|
141
|
+
return `;{try{(function(){var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._apesDebugIds=e._apesDebugIds||{},e._apesDebugIds[n]="${debugId}",e._apesDebugIdIdentifier="apes-dbid-${debugId}");})();}catch(e){}};`;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Inject debug IDs into JavaScript files and their corresponding source maps
|
|
148
|
+
* @param dirPath - Directory path to process
|
|
149
|
+
* @param config - Configuration object containing API key
|
|
150
|
+
* @param projectId - Project ID (required)
|
|
151
|
+
* @param baseUrl - Base URL for the API
|
|
152
|
+
* @param releaseVersion - Release version (optional)
|
|
153
|
+
*/
|
|
154
|
+
export async function injectDebugIdsAndUpload(
|
|
155
|
+
dirPath: string,
|
|
156
|
+
config: Config,
|
|
157
|
+
projectId: string,
|
|
158
|
+
baseUrl: string,
|
|
159
|
+
releaseVersion?: string
|
|
160
|
+
): Promise<void> {
|
|
161
|
+
try {
|
|
162
|
+
console.log(`📁 Processing directory: ${dirPath}`);
|
|
163
|
+
console.log(`🔑 Using API key: ${config.apiKey.substring(0, 8)}...`);
|
|
164
|
+
console.log(`🔑 Project ID: ${projectId}`);
|
|
165
|
+
if (releaseVersion) {
|
|
166
|
+
console.log(`📦 Release version: ${releaseVersion}`);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Validate and resolve the directory path
|
|
170
|
+
const resolvedPath = await validateDirectoryPath(dirPath);
|
|
171
|
+
|
|
172
|
+
// Find all JavaScript and source map files, and get git commit hash concurrently
|
|
173
|
+
const [filePairs, commitHash] = await Promise.all([
|
|
174
|
+
scanForJavaScriptFiles(resolvedPath),
|
|
175
|
+
getGitCommitHash(resolvedPath)
|
|
176
|
+
]);
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
if (filePairs.length === 0) {
|
|
180
|
+
console.log('ℹ️ No JavaScript files with corresponding source maps found');
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Calculate total upload count (JS files + map files)
|
|
185
|
+
const totalUploadCount = filePairs.reduce((count, pair) => {
|
|
186
|
+
return count + 1 + (pair.mapFile ? 1 : 0);
|
|
187
|
+
}, 0);
|
|
188
|
+
|
|
189
|
+
// Create status bars
|
|
190
|
+
const statusBars = createStatusBars(filePairs.length, totalUploadCount);
|
|
191
|
+
|
|
192
|
+
// Setup concurrency limiters
|
|
193
|
+
const injectLimiter = pLimit(15);
|
|
194
|
+
const uploadLimiter = pLimit(10);
|
|
195
|
+
|
|
196
|
+
// Track progress counters
|
|
197
|
+
let injectCompleted = 0;
|
|
198
|
+
let injectFailed = 0;
|
|
199
|
+
let uploadCompleted = 0;
|
|
200
|
+
let uploadFailed = 0;
|
|
201
|
+
|
|
202
|
+
// Process files with concurrent injection and upload pipeline
|
|
203
|
+
const uploadPromises: Promise<UploadError[]>[] = [];
|
|
204
|
+
const allUploadErrors: UploadError[] = [];
|
|
205
|
+
|
|
206
|
+
const injectionPromises = filePairs.map((filePair) =>
|
|
207
|
+
injectLimiter(async () => {
|
|
208
|
+
const debugId = generateDebugId();
|
|
209
|
+
|
|
210
|
+
try {
|
|
211
|
+
// Inject debugId into JavaScript file and source map file concurrently
|
|
212
|
+
await Promise.all([
|
|
213
|
+
injectDebugIdIntoFile(filePair.jsFile, getDebugIdSnippet(debugId), debugId),
|
|
214
|
+
injectDebugIdIntoMapFile(filePair.mapFile, debugId)
|
|
215
|
+
]);
|
|
216
|
+
|
|
217
|
+
// Update progress after successful injection
|
|
218
|
+
injectCompleted++;
|
|
219
|
+
statusBars.updateInjectProgress(injectCompleted, injectFailed);
|
|
220
|
+
} catch (error) {
|
|
221
|
+
// Update progress after failed injection
|
|
222
|
+
injectFailed++;
|
|
223
|
+
statusBars.updateInjectProgress(injectCompleted, injectFailed);
|
|
224
|
+
throw error;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Immediately queue for upload (don't await here)
|
|
228
|
+
const uploadPromise = uploadLimiter(async (): Promise<UploadError[]> => {
|
|
229
|
+
try {
|
|
230
|
+
const errors = await uploadFile(
|
|
231
|
+
filePair,
|
|
232
|
+
debugId,
|
|
233
|
+
config,
|
|
234
|
+
projectId,
|
|
235
|
+
baseUrl,
|
|
236
|
+
releaseVersion,
|
|
237
|
+
commitHash,
|
|
238
|
+
(success) => {
|
|
239
|
+
if (success) {
|
|
240
|
+
uploadCompleted++;
|
|
241
|
+
} else {
|
|
242
|
+
uploadFailed++;
|
|
243
|
+
}
|
|
244
|
+
statusBars.updateUploadProgress(uploadCompleted, uploadFailed);
|
|
245
|
+
}
|
|
246
|
+
);
|
|
247
|
+
return errors;
|
|
248
|
+
} catch (error) {
|
|
249
|
+
// Handle unexpected errors - count failed uploads for this file pair
|
|
250
|
+
const uploadOpsCount = 1 + (filePair.mapFile ? 1 : 0);
|
|
251
|
+
uploadFailed += uploadOpsCount;
|
|
252
|
+
statusBars.updateUploadProgress(uploadCompleted, uploadFailed);
|
|
253
|
+
// Return a structured error for unexpected exceptions
|
|
254
|
+
return [{
|
|
255
|
+
fileName: path.basename(filePair.jsFile),
|
|
256
|
+
errorMessage: error instanceof Error ? error.message : String(error)
|
|
257
|
+
}];
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
uploadPromises.push(uploadPromise);
|
|
262
|
+
})
|
|
263
|
+
);
|
|
264
|
+
|
|
265
|
+
try {
|
|
266
|
+
// Wait for all injections to complete
|
|
267
|
+
await Promise.all(injectionPromises);
|
|
268
|
+
|
|
269
|
+
// Wait for all uploads to complete and collect errors
|
|
270
|
+
const uploadResults = await Promise.allSettled(uploadPromises);
|
|
271
|
+
for (const result of uploadResults) {
|
|
272
|
+
if (result.status === 'fulfilled') {
|
|
273
|
+
allUploadErrors.push(...result.value);
|
|
274
|
+
} else {
|
|
275
|
+
// Handle case where upload promise itself was rejected
|
|
276
|
+
allUploadErrors.push({
|
|
277
|
+
fileName: 'unknown',
|
|
278
|
+
errorMessage: result.reason instanceof Error ? result.reason.message : String(result.reason)
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Stop status bars
|
|
284
|
+
statusBars.stop();
|
|
285
|
+
|
|
286
|
+
// Print collated errors if any
|
|
287
|
+
if (allUploadErrors.length > 0) {
|
|
288
|
+
console.error('\n❌ Server Error Responses:');
|
|
289
|
+
console.error('═'.repeat(60));
|
|
290
|
+
for (const error of allUploadErrors) {
|
|
291
|
+
console.error(`\n📁 File: ${error.fileName}`);
|
|
292
|
+
if (error.statusCode) {
|
|
293
|
+
console.error(` Status Code: ${error.statusCode}`);
|
|
294
|
+
}
|
|
295
|
+
if (error.responseData) {
|
|
296
|
+
console.error(` Response Data: ${JSON.stringify(error.responseData, null, 2)}`);
|
|
297
|
+
}
|
|
298
|
+
console.error(` Error: ${error.errorMessage}`);
|
|
299
|
+
console.error('─'.repeat(60));
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (uploadFailed > 0) {
|
|
304
|
+
throw new Error(`${uploadFailed} upload${uploadFailed === 1 ? '' : 's'} failed`);
|
|
305
|
+
} else {
|
|
306
|
+
console.log('✅ Debug ID injection completed successfully');
|
|
307
|
+
}
|
|
308
|
+
} catch (error) {
|
|
309
|
+
statusBars.stop();
|
|
310
|
+
throw error;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
} catch (error) {
|
|
314
|
+
console.error('❌ Error during debug ID injection:', error);
|
|
315
|
+
throw error;
|
|
316
|
+
}
|
|
317
|
+
}
|
package/src/status.ts
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import cliProgress from 'cli-progress';
|
|
2
|
+
|
|
3
|
+
interface StatusBars {
|
|
4
|
+
injectBar: cliProgress.SingleBar;
|
|
5
|
+
uploadBar: cliProgress.SingleBar;
|
|
6
|
+
updateInjectProgress: (completed: number, failed: number) => void;
|
|
7
|
+
updateUploadProgress: (completed: number, failed: number) => void;
|
|
8
|
+
stop: () => void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Create status bars for inject and upload operations
|
|
13
|
+
* @param injectTotal - Total number of file pairs to inject
|
|
14
|
+
* @param uploadTotal - Total number of files to upload
|
|
15
|
+
* @returns Object with update functions and stop function
|
|
16
|
+
*/
|
|
17
|
+
export function createStatusBars(
|
|
18
|
+
injectTotal: number,
|
|
19
|
+
uploadTotal: number
|
|
20
|
+
): StatusBars {
|
|
21
|
+
// Use MultiBar to display multiple progress bars simultaneously
|
|
22
|
+
const multiBar = new cliProgress.MultiBar(
|
|
23
|
+
{
|
|
24
|
+
clearOnComplete: false,
|
|
25
|
+
hideCursor: true,
|
|
26
|
+
format: '{label} [{bar}] {percentage}% | Completed: {completed}/{total} | Failed: {failed}',
|
|
27
|
+
barCompleteChar: '\u2588',
|
|
28
|
+
barIncompleteChar: '\u2591',
|
|
29
|
+
},
|
|
30
|
+
cliProgress.Presets.shades_classic
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
const injectBar = multiBar.create(injectTotal, 0, {
|
|
34
|
+
label: 'Inject ',
|
|
35
|
+
completed: 0,
|
|
36
|
+
failed: 0,
|
|
37
|
+
total: injectTotal,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const uploadBar = multiBar.create(uploadTotal, 0, {
|
|
41
|
+
label: 'Upload ',
|
|
42
|
+
completed: 0,
|
|
43
|
+
failed: 0,
|
|
44
|
+
total: uploadTotal,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
let injectCompleted = 0;
|
|
48
|
+
let injectFailed = 0;
|
|
49
|
+
let uploadCompleted = 0;
|
|
50
|
+
let uploadFailed = 0;
|
|
51
|
+
|
|
52
|
+
function updateInjectProgress(completed: number, failed: number): void {
|
|
53
|
+
injectCompleted = completed;
|
|
54
|
+
injectFailed = failed;
|
|
55
|
+
injectBar.update(completed + failed, {
|
|
56
|
+
completed,
|
|
57
|
+
failed,
|
|
58
|
+
total: injectTotal,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function updateUploadProgress(completed: number, failed: number): void {
|
|
63
|
+
uploadCompleted = completed;
|
|
64
|
+
uploadFailed = failed;
|
|
65
|
+
uploadBar.update(completed + failed, {
|
|
66
|
+
completed,
|
|
67
|
+
failed,
|
|
68
|
+
total: uploadTotal,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function stop(): void {
|
|
73
|
+
injectBar.update(injectTotal, {
|
|
74
|
+
completed: injectCompleted,
|
|
75
|
+
failed: injectFailed,
|
|
76
|
+
total: injectTotal,
|
|
77
|
+
});
|
|
78
|
+
uploadBar.update(uploadTotal, {
|
|
79
|
+
completed: uploadCompleted,
|
|
80
|
+
failed: uploadFailed,
|
|
81
|
+
total: uploadTotal,
|
|
82
|
+
});
|
|
83
|
+
multiBar.stop();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
injectBar,
|
|
88
|
+
uploadBar,
|
|
89
|
+
updateInjectProgress,
|
|
90
|
+
updateUploadProgress,
|
|
91
|
+
stop,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
package/src/upload.ts
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import { promises as fs } from 'fs';
|
|
3
|
+
import axios, { AxiosInstance, AxiosError } from 'axios';
|
|
4
|
+
import { Agent } from 'http';
|
|
5
|
+
import FormData from 'form-data';
|
|
6
|
+
import { Config } from './config.js';
|
|
7
|
+
import type { FilePair } from './sourcemap.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Structured error information from server responses
|
|
11
|
+
*/
|
|
12
|
+
export interface UploadError {
|
|
13
|
+
fileName: string;
|
|
14
|
+
statusCode?: number;
|
|
15
|
+
responseData?: any;
|
|
16
|
+
errorMessage: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Create axios instance with connection pooling and configurable base URL
|
|
21
|
+
*/
|
|
22
|
+
function createHttpClient(baseUrl: string): AxiosInstance {
|
|
23
|
+
return axios.create({
|
|
24
|
+
baseURL: baseUrl,
|
|
25
|
+
timeout: 30000,
|
|
26
|
+
httpAgent: new Agent({
|
|
27
|
+
keepAlive: true,
|
|
28
|
+
maxSockets: 20,
|
|
29
|
+
maxFreeSockets: 5
|
|
30
|
+
})
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Upload a single file with retry logic
|
|
36
|
+
* @param filePath - Path to the file to upload
|
|
37
|
+
* @param fileName - Name of the file for logging
|
|
38
|
+
* @param debugId - Debug ID associated with the file
|
|
39
|
+
* @param config - Configuration object containing API key
|
|
40
|
+
* @param projectId - Project ID
|
|
41
|
+
* @param httpClient - Axios instance to use for HTTP requests
|
|
42
|
+
* @param releaseVersion - Optional release version
|
|
43
|
+
* @param commitHash - Optional git commit hash
|
|
44
|
+
* @param maxRetries - Maximum number of retry attempts
|
|
45
|
+
* @param onProgress - Optional callback called on success or failure
|
|
46
|
+
* @returns UploadError if all retries failed, undefined on success
|
|
47
|
+
*/
|
|
48
|
+
async function uploadSingleFile(
|
|
49
|
+
filePath: string,
|
|
50
|
+
fileName: string,
|
|
51
|
+
debugId: string,
|
|
52
|
+
config: Config,
|
|
53
|
+
projectId: string,
|
|
54
|
+
httpClient: AxiosInstance,
|
|
55
|
+
releaseVersion?: string,
|
|
56
|
+
commitHash?: string,
|
|
57
|
+
maxRetries: number = 3,
|
|
58
|
+
onProgress?: (success: boolean) => void
|
|
59
|
+
): Promise<UploadError | undefined> {
|
|
60
|
+
let lastError: UploadError | null = null;
|
|
61
|
+
|
|
62
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
63
|
+
try {
|
|
64
|
+
// Read file as buffer
|
|
65
|
+
const fileBuffer = await fs.readFile(filePath);
|
|
66
|
+
|
|
67
|
+
// Create FormData
|
|
68
|
+
const formData = new FormData();
|
|
69
|
+
formData.append('file', fileBuffer, fileName);
|
|
70
|
+
formData.append('project_id', projectId);
|
|
71
|
+
formData.append('debug_id', debugId);
|
|
72
|
+
|
|
73
|
+
if (releaseVersion) {
|
|
74
|
+
formData.append('release_version', releaseVersion);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (commitHash) {
|
|
78
|
+
formData.append('commit_hash', commitHash);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Make the request
|
|
82
|
+
await httpClient.post('/sourcemaps/upload', formData, {
|
|
83
|
+
headers: {
|
|
84
|
+
...formData.getHeaders(),
|
|
85
|
+
'X-API-Key': config.apiKey
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
onProgress?.(true);
|
|
90
|
+
return undefined; // Success, exit retry loop
|
|
91
|
+
|
|
92
|
+
} catch (error) {
|
|
93
|
+
// Extract structured error information from axios error
|
|
94
|
+
if (axios.isAxiosError(error)) {
|
|
95
|
+
const axiosError = error as AxiosError;
|
|
96
|
+
lastError = {
|
|
97
|
+
fileName,
|
|
98
|
+
statusCode: axiosError.response?.status,
|
|
99
|
+
responseData: axiosError.response?.data,
|
|
100
|
+
errorMessage: axiosError.message
|
|
101
|
+
};
|
|
102
|
+
} else {
|
|
103
|
+
lastError = {
|
|
104
|
+
fileName,
|
|
105
|
+
errorMessage: error instanceof Error ? error.message : String(error)
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (attempt < maxRetries) {
|
|
110
|
+
// Wait a bit before retrying (simple delay, no exponential backoff as requested)
|
|
111
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// If we get here, all retries failed
|
|
117
|
+
onProgress?.(false);
|
|
118
|
+
return lastError!;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Upload file via HTTP
|
|
123
|
+
* @param filePair - File pair containing JavaScript file and optional source map file
|
|
124
|
+
* @param debugId - Debug ID associated with the files
|
|
125
|
+
* @param config - Configuration object containing API key
|
|
126
|
+
* @param projectId - Project ID
|
|
127
|
+
* @param baseUrl - Base URL for the API
|
|
128
|
+
* @param releaseVersion - Optional release version
|
|
129
|
+
* @param commitHash - Optional git commit hash
|
|
130
|
+
* @param onProgress - Optional callback called on success or failure for each file
|
|
131
|
+
* @returns Array of UploadError objects for failed uploads
|
|
132
|
+
*/
|
|
133
|
+
export async function uploadFile(
|
|
134
|
+
filePair: FilePair,
|
|
135
|
+
debugId: string,
|
|
136
|
+
config: Config,
|
|
137
|
+
projectId: string,
|
|
138
|
+
baseUrl: string,
|
|
139
|
+
releaseVersion?: string,
|
|
140
|
+
commitHash?: string,
|
|
141
|
+
onProgress?: (success: boolean) => void
|
|
142
|
+
): Promise<UploadError[]> {
|
|
143
|
+
const httpClient = createHttpClient(baseUrl);
|
|
144
|
+
const uploadPromises: Promise<UploadError | undefined>[] = [];
|
|
145
|
+
const errors: UploadError[] = [];
|
|
146
|
+
|
|
147
|
+
// Upload JS file
|
|
148
|
+
uploadPromises.push(
|
|
149
|
+
uploadSingleFile(
|
|
150
|
+
filePair.jsFile,
|
|
151
|
+
path.basename(filePair.jsFile),
|
|
152
|
+
debugId,
|
|
153
|
+
config,
|
|
154
|
+
projectId,
|
|
155
|
+
httpClient,
|
|
156
|
+
releaseVersion,
|
|
157
|
+
commitHash,
|
|
158
|
+
3,
|
|
159
|
+
onProgress
|
|
160
|
+
)
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
// Upload map file if it exists
|
|
164
|
+
if (filePair.mapFile) {
|
|
165
|
+
uploadPromises.push(
|
|
166
|
+
uploadSingleFile(
|
|
167
|
+
filePair.mapFile,
|
|
168
|
+
path.basename(filePair.mapFile),
|
|
169
|
+
debugId,
|
|
170
|
+
config,
|
|
171
|
+
projectId,
|
|
172
|
+
httpClient,
|
|
173
|
+
releaseVersion,
|
|
174
|
+
commitHash,
|
|
175
|
+
3,
|
|
176
|
+
onProgress
|
|
177
|
+
)
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Wait for both uploads to complete and collect errors
|
|
182
|
+
const results = await Promise.allSettled(uploadPromises);
|
|
183
|
+
for (const result of results) {
|
|
184
|
+
if (result.status === 'fulfilled' && result.value !== undefined) {
|
|
185
|
+
errors.push(result.value);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return errors;
|
|
190
|
+
}
|