isomorphic-jj 1.0.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.
Files changed (37) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +971 -0
  3. package/dist/index.cjs +12609 -0
  4. package/dist/index.cjs.map +1 -0
  5. package/dist/index.mjs +12600 -0
  6. package/dist/index.mjs.map +1 -0
  7. package/package.json +90 -0
  8. package/src/api/repository.js +6885 -0
  9. package/src/backend/isomorphic-git-backend.js +687 -0
  10. package/src/backend/lazy-git-backend.js +180 -0
  11. package/src/browser/helpers.js +189 -0
  12. package/src/browser/index.d.ts +174 -0
  13. package/src/browser/index.js +12 -0
  14. package/src/core/background-ops.js +272 -0
  15. package/src/core/bookmark-store.js +232 -0
  16. package/src/core/change-graph.js +285 -0
  17. package/src/core/conflict-model.js +505 -0
  18. package/src/core/jj-checkout.js +78 -0
  19. package/src/core/jj-operation-store.js +120 -0
  20. package/src/core/jj-tree-state.js +94 -0
  21. package/src/core/jj-view-store.js +105 -0
  22. package/src/core/merge-driver-registry.js +432 -0
  23. package/src/core/operation-log.js +272 -0
  24. package/src/core/revset-engine.js +1081 -0
  25. package/src/core/storage-manager.js +207 -0
  26. package/src/core/tag-store.js +167 -0
  27. package/src/core/user-config.js +142 -0
  28. package/src/core/working-copy.js +314 -0
  29. package/src/core/workspace-manager.js +283 -0
  30. package/src/drivers/built-in-drivers.js +296 -0
  31. package/src/index.js +15 -0
  32. package/src/protos/local_working_copy.proto +76 -0
  33. package/src/protos/simple_op_store.proto +147 -0
  34. package/src/types.d.ts +1129 -0
  35. package/src/utils/errors.js +21 -0
  36. package/src/utils/id-generation.js +54 -0
  37. package/src/utils/validation.js +124 -0
@@ -0,0 +1,94 @@
1
+ /**
2
+ * JJ TreeState File Manager
3
+ *
4
+ * Handles encoding/decoding of .jj/working_copy/tree_state file using protobuf.
5
+ * This file tracks the working copy tree and file states.
6
+ */
7
+
8
+ import protobuf from 'protobufjs';
9
+ import path from 'path';
10
+ import { fileURLToPath } from 'url';
11
+
12
+ const __filename = fileURLToPath(import.meta.url);
13
+ const __dirname = path.dirname(__filename);
14
+
15
+ export class JJTreeState {
16
+ constructor(fs, dir) {
17
+ this.fs = fs;
18
+ this.dir = dir;
19
+ this.protoPath = path.join(__dirname, '..', 'protos', 'local_working_copy.proto');
20
+ }
21
+
22
+ /**
23
+ * Write tree_state file
24
+ *
25
+ * @param {string} treeId - Tree ID as hex string (40 characters for SHA-1)
26
+ * @param {Array} fileStates - Array of file state objects
27
+ */
28
+ async writeTreeState(treeId, fileStates = []) {
29
+ // Load protobuf schema
30
+ const root = await protobuf.load(this.protoPath);
31
+ const TreeState = root.lookupType('local_working_copy.TreeState');
32
+
33
+ // Convert hex tree ID to bytes
34
+ const treeIdBuffer = Buffer.from(treeId, 'hex');
35
+
36
+ // Convert file states to protobuf format
37
+ const fileStateEntries = fileStates.map(fs => ({
38
+ path: fs.path,
39
+ state: {
40
+ mtimeMillisSinceEpoch: fs.mtime, // Use camelCase for protobufjs
41
+ size: fs.size,
42
+ fileType: fs.fileType || 0 // 0 = Normal
43
+ }
44
+ }));
45
+
46
+ // Create message (use camelCase for protobufjs)
47
+ const message = TreeState.create({
48
+ treeIds: [treeIdBuffer], // Single tree ID (no conflicts)
49
+ fileStates: fileStateEntries,
50
+ isFileStatesSorted: false, // We don't sort them
51
+ sparsePatterns: {
52
+ prefixes: [] // No sparse patterns
53
+ }
54
+ });
55
+
56
+ // Verify the message
57
+ const errMsg = TreeState.verify(message);
58
+ if (errMsg) {
59
+ throw new Error(`TreeState message verification failed: ${errMsg}`);
60
+ }
61
+
62
+ // Encode to binary
63
+ const buffer = TreeState.encode(message).finish();
64
+
65
+ // Write to .jj/working_copy/tree_state
66
+ const treeStatePath = `${this.dir}/.jj/working_copy/tree_state`;
67
+ await this.fs.promises.writeFile(treeStatePath, buffer);
68
+ }
69
+
70
+ /**
71
+ * Read tree_state file
72
+ *
73
+ * @returns {Object} Decoded tree state data
74
+ */
75
+ async readTreeState() {
76
+ const treeStatePath = `${this.dir}/.jj/working_copy/tree_state`;
77
+ const buffer = await this.fs.promises.readFile(treeStatePath);
78
+
79
+ // Load protobuf schema
80
+ const root = await protobuf.load(this.protoPath);
81
+ const TreeState = root.lookupType('local_working_copy.TreeState');
82
+
83
+ // Decode
84
+ const message = TreeState.decode(buffer);
85
+
86
+ // Return the decoded message with proper field access
87
+ return {
88
+ tree_ids: message.treeIds,
89
+ file_states: message.fileStates,
90
+ is_file_states_sorted: message.isFileStatesSorted,
91
+ sparse_patterns: message.sparsePatterns
92
+ };
93
+ }
94
+ }
@@ -0,0 +1,105 @@
1
+ /**
2
+ * JJ View Store Manager
3
+ *
4
+ * Handles encoding/decoding of .jj/repo/op_store/views/* files using protobuf.
5
+ * Views represent the state of the repository at a given operation.
6
+ */
7
+
8
+ import protobuf from 'protobufjs';
9
+ import path from 'path';
10
+ import { fileURLToPath } from 'url';
11
+
12
+ const __filename = fileURLToPath(import.meta.url);
13
+ const __dirname = path.dirname(__filename);
14
+
15
+ export class JJViewStore {
16
+ constructor(fs, dir) {
17
+ this.fs = fs;
18
+ this.dir = dir;
19
+ this.protoPath = path.join(__dirname, '..', 'protos', 'simple_op_store.proto');
20
+ }
21
+
22
+ /**
23
+ * Write view file
24
+ *
25
+ * @param {string} viewId - View ID as hex string (128 characters)
26
+ * @param {Array<string>} headIds - Head commit IDs as hex strings
27
+ * @param {Object} wcCommitIds - Working copy commit IDs by workspace name
28
+ */
29
+ async writeView(viewId, headIds, wcCommitIds = {}) {
30
+ // Load protobuf schema
31
+ const root = await protobuf.load(this.protoPath);
32
+ const View = root.lookupType('simple_op_store.View');
33
+
34
+ // Convert hex IDs to bytes
35
+ const headBuffers = headIds.map(id => Buffer.from(id, 'hex'));
36
+
37
+ // Convert wcCommitIds object to map
38
+ const wcCommitIdsMap = {};
39
+ for (const [workspace, commitId] of Object.entries(wcCommitIds)) {
40
+ wcCommitIdsMap[workspace] = Buffer.from(commitId, 'hex');
41
+ }
42
+
43
+ // Create view message (use camelCase for protobufjs)
44
+ const message = View.create({
45
+ headIds: headBuffers,
46
+ wcCommitIds: wcCommitIdsMap,
47
+ bookmarks: [],
48
+ localTags: [],
49
+ remoteViews: [],
50
+ gitRefs: [],
51
+ gitHead: null,
52
+ hasGitRefsMigratedToRemoteTags: false
53
+ });
54
+
55
+ // Verify the message
56
+ const errMsg = View.verify(message);
57
+ if (errMsg) {
58
+ throw new Error(`View message verification failed: ${errMsg}`);
59
+ }
60
+
61
+ // Encode to binary
62
+ const buffer = View.encode(message).finish();
63
+
64
+ // Write to .jj/repo/op_store/views/VIEWID
65
+ const viewPath = `${this.dir}/.jj/repo/op_store/views/${viewId}`;
66
+ await this.fs.promises.writeFile(viewPath, buffer);
67
+ }
68
+
69
+ /**
70
+ * Read view file
71
+ *
72
+ * @param {string} viewId - View ID as hex string (128 characters)
73
+ * @returns {Object} Decoded view data
74
+ */
75
+ async readView(viewId) {
76
+ // Read from .jj/repo/op_store/views/VIEWID
77
+ const viewPath = `${this.dir}/.jj/repo/op_store/views/${viewId}`;
78
+ const buffer = await this.fs.promises.readFile(viewPath);
79
+
80
+ // Load protobuf schema
81
+ const root = await protobuf.load(this.protoPath);
82
+ const View = root.lookupType('simple_op_store.View');
83
+
84
+ // Decode
85
+ const message = View.decode(buffer);
86
+
87
+ // Convert wcCommitIds map back to object
88
+ const wcCommitIds = {};
89
+ for (const [workspace, commitIdBuffer] of Object.entries(message.wcCommitIds || {})) {
90
+ wcCommitIds[workspace] = commitIdBuffer;
91
+ }
92
+
93
+ // Return the decoded message with proper field access
94
+ return {
95
+ head_ids: message.headIds,
96
+ wc_commit_ids: wcCommitIds,
97
+ bookmarks: message.bookmarks,
98
+ local_tags: message.localTags,
99
+ remote_views: message.remoteViews,
100
+ git_refs: message.gitRefs,
101
+ git_head: message.gitHead,
102
+ has_git_refs_migrated_to_remote_tags: message.hasGitRefsMigratedToRemoteTags
103
+ };
104
+ }
105
+ }
@@ -0,0 +1,432 @@
1
+ /**
2
+ * MergeDriverRegistry - Registry for custom merge drivers
3
+ *
4
+ * Manages registration, lookup, and execution of merge drivers.
5
+ * Drivers are matched against file paths using glob patterns.
6
+ */
7
+
8
+ import { JJError } from '../utils/errors.js';
9
+
10
+ /**
11
+ * Check if a file path matches a glob pattern
12
+ * Simple implementation - for complex patterns, consider using minimatch
13
+ *
14
+ * @param {string} path - File path
15
+ * @param {string} pattern - Glob pattern
16
+ * @returns {boolean} Whether path matches pattern
17
+ */
18
+ function matchesPattern(path, pattern) {
19
+ // Exact match
20
+ if (pattern === path) return true;
21
+
22
+ // Convert glob pattern to regex
23
+ // Support: *, **, ?, [abc]
24
+ const regexPattern = pattern
25
+ .replace(/\*\*/g, '__DOUBLESTAR__')
26
+ .replace(/\*/g, '[^/]*')
27
+ .replace(/__DOUBLESTAR__/g, '.*')
28
+ .replace(/\?/g, '[^/]')
29
+ .replace(/\./g, '\\.');
30
+
31
+ const regex = new RegExp(`^${regexPattern}$`);
32
+ return regex.test(path);
33
+ }
34
+
35
+ /**
36
+ * Check if a file is likely binary based on content
37
+ *
38
+ * @param {string|Buffer} content - File content
39
+ * @returns {boolean} Whether content appears binary
40
+ */
41
+ function isBinaryContent(content) {
42
+ if (Buffer.isBuffer(content)) return true;
43
+ if (typeof content !== 'string') return false;
44
+
45
+ // Check for null bytes (common in binary files)
46
+ if (content.indexOf('\0') !== -1) return true;
47
+
48
+ // Check for high ratio of non-printable characters
49
+ const nonPrintable = content.split('').filter(
50
+ (char) => {
51
+ const code = char.charCodeAt(0);
52
+ return code < 32 && code !== 9 && code !== 10 && code !== 13;
53
+ }
54
+ ).length;
55
+
56
+ const ratio = nonPrintable / content.length;
57
+ return ratio > 0.3;
58
+ }
59
+
60
+ /**
61
+ * Default merge driver - standard three-way text merge
62
+ *
63
+ * @param {MergeContext} context
64
+ * @returns {Promise<MergeResult>}
65
+ */
66
+ async function defaultMergeDriver(context) {
67
+ const { content } = context;
68
+ const { base, ours, theirs } = content;
69
+
70
+ // Simple cases
71
+ if (ours === theirs) {
72
+ return { content: ours, hasConflict: false };
73
+ }
74
+
75
+ if (ours === base) {
76
+ // No changes on our side, take theirs
77
+ return { content: theirs, hasConflict: false };
78
+ }
79
+
80
+ if (theirs === base) {
81
+ // No changes on their side, take ours
82
+ return { content: ours, hasConflict: false };
83
+ }
84
+
85
+ // Both sides changed - conflict
86
+ const conflictMarkers = `<<<<<<< ours\n${ours || ''}\n=======\n${theirs || ''}\n>>>>>>> theirs`;
87
+
88
+ return {
89
+ content: conflictMarkers,
90
+ hasConflict: true,
91
+ conflicts: [{
92
+ type: 'content',
93
+ sides: ['ours', 'theirs'],
94
+ }],
95
+ };
96
+ }
97
+
98
+ /**
99
+ * Wrap a driver with timeout and error handling
100
+ *
101
+ * @param {Function} driver - Driver function
102
+ * @param {Object} options - Wrapper options
103
+ * @param {number} [options.timeout=5000] - Timeout in ms
104
+ * @param {boolean} [options.strict=false] - Throw on error instead of falling back
105
+ * @param {Object} [options.jj] - JJ instance for event emission
106
+ * @param {string} [options.pattern] - Pattern that matched this driver
107
+ * @returns {Function} Wrapped driver
108
+ */
109
+ function wrapDriver(driver, options = {}) {
110
+ const { timeout = 5000, strict = false, jj = null, pattern = null } = options;
111
+
112
+ return async (context) => {
113
+ let timeoutId;
114
+ const timeoutPromise = new Promise((_, reject) => {
115
+ timeoutId = setTimeout(() => reject(new Error('Driver timeout')), timeout);
116
+ // Use unref() to allow process to exit if this is the only pending timer
117
+ if (timeoutId.unref) {
118
+ timeoutId.unref();
119
+ }
120
+ });
121
+
122
+ try {
123
+ const result = await Promise.race([
124
+ driver(context),
125
+ timeoutPromise,
126
+ ]);
127
+
128
+ // Clear timeout if driver completed successfully
129
+ clearTimeout(timeoutId);
130
+
131
+ // Validate result
132
+ if (!result || typeof result.hasConflict !== 'boolean') {
133
+ throw new Error('Driver returned invalid result');
134
+ }
135
+
136
+ return result;
137
+ } catch (error) {
138
+ // Clear timeout on error too
139
+ clearTimeout(timeoutId);
140
+
141
+ // Emit driver:failed event if jj instance available
142
+ if (jj && jj.dispatchEvent) {
143
+ jj.dispatchEvent(new CustomEvent('driver:failed', {
144
+ detail: {
145
+ path: context.path,
146
+ pattern: pattern,
147
+ error: error.message,
148
+ timestamp: new Date().toISOString(),
149
+ },
150
+ }));
151
+ }
152
+
153
+ // In strict mode, throw the error
154
+ if (strict) {
155
+ throw error;
156
+ }
157
+
158
+ // Fall back to default merge with error metadata
159
+ const result = await defaultMergeDriver(context);
160
+ return {
161
+ ...result,
162
+ driverFailed: true,
163
+ driverError: error.message,
164
+ };
165
+ }
166
+ };
167
+ }
168
+
169
+ /**
170
+ * MergeDriverRegistry manages custom merge drivers
171
+ */
172
+ export class MergeDriverRegistry {
173
+ constructor(jj = null) {
174
+ /** @type {Array<{pattern: string, driver: Function, accepts: Object, strict: boolean}>} */
175
+ this.drivers = [];
176
+ this.jj = jj; // JJ instance for event emission
177
+ }
178
+
179
+ /**
180
+ * Register one or more merge drivers
181
+ *
182
+ * @param {Object<string, Function|Object>} drivers - Pattern -> driver map
183
+ *
184
+ * @example
185
+ * registry.register({
186
+ * 'package.json': packageJsonDriver,
187
+ * '*.md': markdownDriver,
188
+ * 'critical.txt': { driver: myDriver, strict: true },
189
+ * });
190
+ */
191
+ register(drivers) {
192
+ for (const [pattern, driverOrConfig] of Object.entries(drivers)) {
193
+ // Driver can be function or { driver, accepts, timeout, strict }
194
+ let driver, accepts, timeout, strict;
195
+
196
+ if (typeof driverOrConfig === 'function') {
197
+ driver = driverOrConfig;
198
+ accepts = { text: true, binary: false }; // Default: text only
199
+ timeout = 5000;
200
+ strict = false;
201
+ } else {
202
+ driver = driverOrConfig.driver || driverOrConfig.merge;
203
+ accepts = driverOrConfig.accepts || { text: true, binary: false };
204
+ timeout = driverOrConfig.timeout || 5000;
205
+ strict = driverOrConfig.strict || false;
206
+ }
207
+
208
+ if (typeof driver !== 'function') {
209
+ throw new JJError(
210
+ 'INVALID_DRIVER',
211
+ `Driver for pattern "${pattern}" must be a function`,
212
+ { pattern }
213
+ );
214
+ }
215
+
216
+ // Wrap driver with error handling
217
+ const wrappedDriver = wrapDriver(driver, {
218
+ timeout,
219
+ strict,
220
+ jj: this.jj,
221
+ pattern,
222
+ });
223
+
224
+ this.drivers.push({ pattern, driver: wrappedDriver, accepts, strict });
225
+ }
226
+ }
227
+
228
+ /**
229
+ * Unregister a driver by pattern
230
+ *
231
+ * @param {string} pattern - Pattern to unregister
232
+ */
233
+ unregister(pattern) {
234
+ this.drivers = this.drivers.filter((d) => d.pattern !== pattern);
235
+ }
236
+
237
+ /**
238
+ * Get a specific driver by pattern
239
+ *
240
+ * @param {string} pattern - Pattern to look up
241
+ * @returns {Function|undefined} Driver function or undefined
242
+ */
243
+ get(pattern) {
244
+ const entry = this.drivers.find((d) => d.pattern === pattern);
245
+ return entry?.driver;
246
+ }
247
+
248
+ /**
249
+ * List all registered drivers
250
+ *
251
+ * @returns {Array<{pattern: string, accepts: Object}>} Driver list
252
+ */
253
+ list() {
254
+ return this.drivers.map(({ pattern, accepts }) => ({ pattern, accepts }));
255
+ }
256
+
257
+ /**
258
+ * Find the best matching driver for a file path
259
+ *
260
+ * Priority:
261
+ * 1. Per-merge custom drivers (from merge options)
262
+ * 2. Registered drivers (by pattern specificity)
263
+ * 3. Default driver (three-way merge)
264
+ *
265
+ * @param {string} filePath - File path
266
+ * @param {Object} customDrivers - Per-merge custom drivers
267
+ * @param {boolean} isBinary - Whether file is binary
268
+ * @returns {Function} Driver function
269
+ */
270
+ findDriver(filePath, customDrivers = {}, isBinary = false) {
271
+ // 1. Check custom drivers (highest priority)
272
+ for (const [pattern, driverOrConfig] of Object.entries(customDrivers)) {
273
+ if (matchesPattern(filePath, pattern)) {
274
+ let driver, accepts, timeout, strict;
275
+
276
+ if (typeof driverOrConfig === 'function') {
277
+ driver = driverOrConfig;
278
+ accepts = { text: true, binary: false };
279
+ timeout = 5000;
280
+ strict = false;
281
+ } else {
282
+ driver = driverOrConfig.driver || driverOrConfig.merge;
283
+ accepts = driverOrConfig.accepts || { text: true, binary: false };
284
+ timeout = driverOrConfig.timeout || 5000;
285
+ strict = driverOrConfig.strict || false;
286
+ }
287
+
288
+ // Check if driver accepts this file type
289
+ if ((isBinary && accepts.binary) || (!isBinary && accepts.text)) {
290
+ return wrapDriver(driver, {
291
+ timeout,
292
+ strict,
293
+ jj: this.jj,
294
+ pattern,
295
+ });
296
+ }
297
+ }
298
+ }
299
+
300
+ // 2. Check registered drivers (by specificity)
301
+ const matches = this.drivers
302
+ .filter(({ pattern, accepts }) => {
303
+ const matchesPath = matchesPattern(filePath, pattern);
304
+ const acceptsType = (isBinary && accepts.binary) || (!isBinary && accepts.text);
305
+ return matchesPath && acceptsType;
306
+ })
307
+ .sort((a, b) => b.pattern.length - a.pattern.length); // Most specific first
308
+
309
+ if (matches.length > 0) {
310
+ return matches[0].driver;
311
+ }
312
+
313
+ // 3. Fall back to default driver
314
+ return defaultMergeDriver;
315
+ }
316
+
317
+ /**
318
+ * Execute a driver with proper content encoding
319
+ *
320
+ * @param {Function} driver - Driver function
321
+ * @param {Object} context - Merge context
322
+ * @param {string} context.path - File path
323
+ * @param {string|Buffer|null} context.base - Base content
324
+ * @param {string|Buffer|null} context.ours - Our content
325
+ * @param {string|Buffer|null} context.theirs - Their content
326
+ * @param {Object} context.metadata - Metadata
327
+ * @param {boolean} isBinary - Whether content is binary
328
+ * @returns {Promise<MergeResult>}
329
+ */
330
+ async executeDriver(driver, context, isBinary) {
331
+ const { base, ours, theirs, ...rest } = context;
332
+
333
+ // Prepare content based on binary/text
334
+ const content = {
335
+ base: this.prepareContent(base, isBinary),
336
+ ours: this.prepareContent(ours, isBinary),
337
+ theirs: this.prepareContent(theirs, isBinary),
338
+ };
339
+
340
+ const driverContext = {
341
+ ...rest,
342
+ content,
343
+ isBinary,
344
+ };
345
+
346
+ return await driver(driverContext);
347
+ }
348
+
349
+ /**
350
+ * Prepare content for driver based on binary/text
351
+ *
352
+ * @param {string|Buffer|null} content - Raw content
353
+ * @param {boolean} isBinary - Whether to return as Buffer
354
+ * @returns {string|Buffer|null} Prepared content
355
+ */
356
+ prepareContent(content, isBinary) {
357
+ if (content === null || content === undefined) {
358
+ return null;
359
+ }
360
+
361
+ if (isBinary) {
362
+ // Convert to Buffer if needed
363
+ if (Buffer.isBuffer(content)) return content;
364
+ if (typeof content === 'string') return Buffer.from(content, 'utf-8');
365
+ return Buffer.from(String(content));
366
+ } else {
367
+ // Convert to string if needed
368
+ if (typeof content === 'string') return content;
369
+ if (Buffer.isBuffer(content)) return content.toString('utf-8');
370
+ return String(content);
371
+ }
372
+ }
373
+
374
+ /**
375
+ * Check if file is binary based on path or content
376
+ *
377
+ * @param {string} path - File path
378
+ * @param {string|Buffer} content - File content
379
+ * @returns {boolean} Whether file is binary
380
+ */
381
+ isBinaryFile(path, content) {
382
+ // Check by extension first
383
+ const binaryExtensions = [
384
+ '.png', '.jpg', '.jpeg', '.gif', '.bmp', '.ico',
385
+ '.zip', '.tar', '.gz', '.bz2', '.7z',
386
+ '.pdf', '.doc', '.docx', '.xls', '.xlsx',
387
+ '.exe', '.dll', '.so', '.dylib',
388
+ '.mp3', '.mp4', '.avi', '.mov',
389
+ ];
390
+
391
+ const ext = path.substring(path.lastIndexOf('.')).toLowerCase();
392
+ if (binaryExtensions.includes(ext)) {
393
+ return true;
394
+ }
395
+
396
+ // Check content
397
+ if (content) {
398
+ return isBinaryContent(content);
399
+ }
400
+
401
+ return false;
402
+ }
403
+ }
404
+
405
+ /**
406
+ * Standalone helper to check if file is binary
407
+ * (exported for external use)
408
+ *
409
+ * @param {string} path - File path
410
+ * @param {string|Buffer} content - File content
411
+ * @returns {boolean} Whether file is binary
412
+ */
413
+ export function isBinaryFile(path, content) {
414
+ const binaryExtensions = [
415
+ '.png', '.jpg', '.jpeg', '.gif', '.bmp', '.ico',
416
+ '.zip', '.tar', '.gz', '.bz2', '.7z',
417
+ '.pdf', '.doc', '.docx', '.xls', '.xlsx',
418
+ '.exe', '.dll', '.so', '.dylib',
419
+ '.mp3', '.mp4', '.avi', '.mov',
420
+ ];
421
+
422
+ const ext = path.substring(path.lastIndexOf('.')).toLowerCase();
423
+ if (binaryExtensions.includes(ext)) {
424
+ return true;
425
+ }
426
+
427
+ if (content) {
428
+ return isBinaryContent(content);
429
+ }
430
+
431
+ return false;
432
+ }