dsh-remote 0.8.9 → 0.8.10

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/lib/ignore.js CHANGED
@@ -1,123 +1,123 @@
1
- // dsh-remote — gitignore-style ignore matcher for mirror sync / remote search.
2
- import { readFileSync } from 'node:fs'
3
- //
4
- // Deliberately a SUBSET of gitignore semantics (documented, no surprises):
5
- // - blank lines and `#` comments are skipped
6
- // - `!` negation is NOT supported (later rules never un-ignore)
7
- // - a trailing `/` matches directories only (and everything under them)
8
- // - a pattern containing `/` (or starting with `/`) is anchored to the
9
- // workspace root; otherwise it matches the basename at ANY depth
10
- // - `*` matches any run of chars except `/`; `**` matches any run incl. `/`;
11
- // `?` matches one char except `/`
12
- // - character classes (`[abc]`) are not supported (treated literally)
13
-
14
- /** Convert one glob segment to a regex source (no `/` crossing). A leading
15
- * double-star-slash (`**` + `/`) matches zero or more directories (gitignore). */
16
- function globToRe(glob) {
17
- let out = ''
18
- let i = 0
19
- const s = String(glob)
20
- if (s.startsWith('**/')) { out = '(?:.*/)?'; i = 3 }
21
- while (i < s.length) {
22
- const c = s[i]
23
- if (c === '*') {
24
- if (s[i + 1] === '*') { out += '.*'; i += 2; continue }
25
- out += '[^/]*'; i++; continue
26
- }
27
- if (c === '?') { out += '[^/]'; i++; continue }
28
- if ('\\^$+{}[]()|.'.includes(c)) out += '\\' + c
29
- else out += c
30
- i++
31
- }
32
- return out
33
- }
34
-
35
- /**
36
- * Compile a list of ignore patterns into a matcher.
37
- * @param {string[]|string} patterns lines or an array of lines
38
- * @returns {(relPath: string, isDir?: boolean) => boolean} true = ignored.
39
- * `relPath` is POSIX-style ('/' separated, no leading '/').
40
- */
41
- export function compileIgnore(patterns) {
42
- const rules = []
43
- const lines = Array.isArray(patterns) ? patterns : String(patterns || '').split('\n')
44
- for (let raw of lines) {
45
- let line = String(raw || '').trim()
46
- if (!line || line.startsWith('#')) continue
47
- let dirOnly = false
48
- if (line.endsWith('/')) { dirOnly = true; line = line.slice(0, -1) }
49
- let anchored = line.startsWith('/')
50
- if (anchored) line = line.slice(1)
51
- if (!line) continue
52
- const hasSlash = line.includes('/')
53
- const isAnchored = anchored || hasSlash
54
- const segRe = globToRe(line)
55
- let re
56
- if (isAnchored) {
57
- // Root-anchored: match the full path from the root.
58
- re = dirOnly
59
- ? new RegExp('^' + segRe + '(/.*)?$')
60
- : new RegExp('^' + segRe + '$')
61
- } else {
62
- // Basename at any depth; a dir pattern also covers everything under it.
63
- re = dirOnly
64
- ? new RegExp('(^|/)' + segRe + '(/|$)')
65
- : new RegExp('(^|/)' + segRe + '$')
66
- }
67
- rules.push({ re, dirOnly })
68
- }
69
- return function isIgnored(relPath, isDir) {
70
- const rel = String(relPath || '').replace(/^\/+/, '').replace(/\\/g, '/')
71
- if (!rel) return false
72
- for (const r of rules) {
73
- if (r.re.test(rel)) return true
74
- }
75
- return false
76
- }
77
- }
78
-
79
- /** Built-in defaults: never pull/push these into/from a mirror. */
80
- export const DEFAULT_IGNORE = [
81
- '.git/',
82
- '.svn/',
83
- '.hg/',
84
- 'node_modules/',
85
- 'target/',
86
- 'dist/',
87
- 'build/',
88
- '.venv/',
89
- 'venv/',
90
- '__pycache__/',
91
- '.idea/',
92
- '.vscode/',
93
- '*.o',
94
- '*.a',
95
- '*.so',
96
- '*.dylib',
97
- '*.exe',
98
- '*.dll',
99
- '*.class',
100
- '*.jar',
101
- '*.log',
102
- '*.tmp',
103
- '.DS_Store',
104
- 'Thumbs.db',
105
- '.dsh-remote-meta.json',
106
- '.dsh-remote-sync-state.json',
107
- ]
108
-
109
- /**
110
- * Load the user ignore file (gitignore syntax) and merge with defaults.
111
- * Returns `{ patterns, fromFile }`.
112
- */
113
- export function loadIgnore(ignoreFile) {
114
- let fromFile = []
115
- if (ignoreFile) {
116
- try {
117
- const text = readFileSync(ignoreFile, 'utf8')
118
- fromFile = text.split('\n').map((l) => l.trim()).filter((l) => l && !l.startsWith('#'))
119
- } catch { /* missing file → defaults only */ }
120
- }
121
- const patterns = DEFAULT_IGNORE.concat(fromFile)
122
- return { patterns, fromFile, matcher: compileIgnore(patterns) }
123
- }
1
+ // dsh-remote — gitignore-style ignore matcher for mirror sync / remote search.
2
+ import { readFileSync } from 'node:fs'
3
+ //
4
+ // Deliberately a SUBSET of gitignore semantics (documented, no surprises):
5
+ // - blank lines and `#` comments are skipped
6
+ // - `!` negation is NOT supported (later rules never un-ignore)
7
+ // - a trailing `/` matches directories only (and everything under them)
8
+ // - a pattern containing `/` (or starting with `/`) is anchored to the
9
+ // workspace root; otherwise it matches the basename at ANY depth
10
+ // - `*` matches any run of chars except `/`; `**` matches any run incl. `/`;
11
+ // `?` matches one char except `/`
12
+ // - character classes (`[abc]`) are not supported (treated literally)
13
+
14
+ /** Convert one glob segment to a regex source (no `/` crossing). A leading
15
+ * double-star-slash (`**` + `/`) matches zero or more directories (gitignore). */
16
+ function globToRe(glob) {
17
+ let out = ''
18
+ let i = 0
19
+ const s = String(glob)
20
+ if (s.startsWith('**/')) { out = '(?:.*/)?'; i = 3 }
21
+ while (i < s.length) {
22
+ const c = s[i]
23
+ if (c === '*') {
24
+ if (s[i + 1] === '*') { out += '.*'; i += 2; continue }
25
+ out += '[^/]*'; i++; continue
26
+ }
27
+ if (c === '?') { out += '[^/]'; i++; continue }
28
+ if ('\\^$+{}[]()|.'.includes(c)) out += '\\' + c
29
+ else out += c
30
+ i++
31
+ }
32
+ return out
33
+ }
34
+
35
+ /**
36
+ * Compile a list of ignore patterns into a matcher.
37
+ * @param {string[]|string} patterns lines or an array of lines
38
+ * @returns {(relPath: string, isDir?: boolean) => boolean} true = ignored.
39
+ * `relPath` is POSIX-style ('/' separated, no leading '/').
40
+ */
41
+ export function compileIgnore(patterns) {
42
+ const rules = []
43
+ const lines = Array.isArray(patterns) ? patterns : String(patterns || '').split('\n')
44
+ for (let raw of lines) {
45
+ let line = String(raw || '').trim()
46
+ if (!line || line.startsWith('#')) continue
47
+ let dirOnly = false
48
+ if (line.endsWith('/')) { dirOnly = true; line = line.slice(0, -1) }
49
+ let anchored = line.startsWith('/')
50
+ if (anchored) line = line.slice(1)
51
+ if (!line) continue
52
+ const hasSlash = line.includes('/')
53
+ const isAnchored = anchored || hasSlash
54
+ const segRe = globToRe(line)
55
+ let re
56
+ if (isAnchored) {
57
+ // Root-anchored: match the full path from the root.
58
+ re = dirOnly
59
+ ? new RegExp('^' + segRe + '(/.*)?$')
60
+ : new RegExp('^' + segRe + '$')
61
+ } else {
62
+ // Basename at any depth; a dir pattern also covers everything under it.
63
+ re = dirOnly
64
+ ? new RegExp('(^|/)' + segRe + '(/|$)')
65
+ : new RegExp('(^|/)' + segRe + '$')
66
+ }
67
+ rules.push({ re, dirOnly })
68
+ }
69
+ return function isIgnored(relPath, isDir) {
70
+ const rel = String(relPath || '').replace(/^\/+/, '').replace(/\\/g, '/')
71
+ if (!rel) return false
72
+ for (const r of rules) {
73
+ if (r.re.test(rel)) return true
74
+ }
75
+ return false
76
+ }
77
+ }
78
+
79
+ /** Built-in defaults: never pull/push these into/from a mirror. */
80
+ export const DEFAULT_IGNORE = [
81
+ '.git/',
82
+ '.svn/',
83
+ '.hg/',
84
+ 'node_modules/',
85
+ 'target/',
86
+ 'dist/',
87
+ 'build/',
88
+ '.venv/',
89
+ 'venv/',
90
+ '__pycache__/',
91
+ '.idea/',
92
+ '.vscode/',
93
+ '*.o',
94
+ '*.a',
95
+ '*.so',
96
+ '*.dylib',
97
+ '*.exe',
98
+ '*.dll',
99
+ '*.class',
100
+ '*.jar',
101
+ '*.log',
102
+ '*.tmp',
103
+ '.DS_Store',
104
+ 'Thumbs.db',
105
+ '.dsh-remote-meta.json',
106
+ '.dsh-remote-sync-state.json',
107
+ ]
108
+
109
+ /**
110
+ * Load the user ignore file (gitignore syntax) and merge with defaults.
111
+ * Returns `{ patterns, fromFile }`.
112
+ */
113
+ export function loadIgnore(ignoreFile) {
114
+ let fromFile = []
115
+ if (ignoreFile) {
116
+ try {
117
+ const text = readFileSync(ignoreFile, 'utf8')
118
+ fromFile = text.split('\n').map((l) => l.trim()).filter((l) => l && !l.startsWith('#'))
119
+ } catch { /* missing file → defaults only */ }
120
+ }
121
+ const patterns = DEFAULT_IGNORE.concat(fromFile)
122
+ return { patterns, fromFile, matcher: compileIgnore(patterns) }
123
+ }