@naturalcycles/nodejs-lib 15.107.1 → 15.107.3

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.
@@ -7,7 +7,7 @@ declare class Git2 {
7
7
  commitMessageToTitleMessage(msg: string): string;
8
8
  hasUncommittedChanges(): boolean;
9
9
  /**
10
- * Returns true if there were changes
10
+ * @returns true if there were changes
11
11
  */
12
12
  commitAll(msg: string): boolean;
13
13
  /**
@@ -22,6 +22,14 @@ declare class Git2 {
22
22
  getCurrentBranchName(): string;
23
23
  getCurrentRepoName(): string;
24
24
  getAllBranchesNames(): string[];
25
+ gitRefExists(ref: string): boolean;
26
+ getTrackedFiles(): string[];
27
+ getUntrackedFiles(): string[];
28
+ getTrackedChangedFiles(diffBase: string, diffFilter?: string): string[];
29
+ /**
30
+ * @returns both tracked changed and untracked files
31
+ */
32
+ getAllChangedFiles(diffBase: string): string[];
25
33
  }
26
34
  export declare const git2: Git2;
27
35
  export {};
package/dist/util/git2.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { execSync } from 'node:child_process';
2
2
  import { basename } from 'node:path';
3
+ import { _uniq } from '@naturalcycles/js-lib/array';
3
4
  import { exec2 } from '../exec2/exec2.js';
4
5
  /**
5
6
  * Set of utility functions to work with git.
@@ -26,7 +27,7 @@ class Git2 {
26
27
  }
27
28
  }
28
29
  /**
29
- * Returns true if there were changes
30
+ * @returns true if there were changes
30
31
  */
31
32
  commitAll(msg) {
32
33
  // git commit -a -m "style(lint-all): $GIT_MSG" || true
@@ -120,5 +121,56 @@ class Git2 {
120
121
  .filter(s => !s.includes(' -> '))
121
122
  .map(s => s.split('/')[1]);
122
123
  }
124
+ gitRefExists(ref) {
125
+ try {
126
+ exec2.exec(`git rev-parse --verify --quiet ${ref}`);
127
+ return true;
128
+ }
129
+ catch {
130
+ return false;
131
+ }
132
+ }
133
+ getTrackedFiles() {
134
+ try {
135
+ return exec2
136
+ .exec('git ls-files')
137
+ .split('\n')
138
+ .filter(s => s.trim().length);
139
+ }
140
+ catch {
141
+ return [];
142
+ }
143
+ }
144
+ getUntrackedFiles() {
145
+ try {
146
+ return exec2
147
+ .exec('git ls-files --others --exclude-standard')
148
+ .split('\n')
149
+ .filter(s => s.trim().length);
150
+ }
151
+ catch {
152
+ return [];
153
+ }
154
+ }
155
+ getTrackedChangedFiles(diffBase, diffFilter = 'AMR') {
156
+ try {
157
+ return exec2
158
+ .exec(`git diff --name-only --diff-filter=${diffFilter} ${diffBase}`)
159
+ .split('\n')
160
+ .filter(s => s.trim().length);
161
+ }
162
+ catch {
163
+ return [];
164
+ }
165
+ }
166
+ /**
167
+ * @returns both tracked changed and untracked files
168
+ */
169
+ getAllChangedFiles(diffBase) {
170
+ const tracked = this.getTrackedChangedFiles(diffBase);
171
+ const untracked = this.getUntrackedFiles();
172
+ const changes = [...tracked, ...untracked];
173
+ return _uniq(changes);
174
+ }
123
175
  }
124
176
  export const git2 = new Git2();
@@ -1178,6 +1178,9 @@ function executeValidation(fn, builtSchema, input, opt = {}, defaultInputName) {
1178
1178
  const errors = fn.errors;
1179
1179
  const { inputId = _isObject(input) ? input['id'] : undefined, inputName = defaultInputName || 'Object', } = opt;
1180
1180
  const dataVar = [inputName, inputId].filter(Boolean).join('.');
1181
+ // Build fingerprint before applyImprovementsOnErrorMessages: after it, /items/0/name becomes
1182
+ // .items[0].name, embedding the index into the segment and making it harder to strip without regex
1183
+ const fingerprint = buildAjvErrorFingerprint(errors[0], inputName);
1181
1184
  applyImprovementsOnErrorMessages(errors, builtSchema);
1182
1185
  let message = getAjv().errorsText(errors, {
1183
1186
  dataVar,
@@ -1187,9 +1190,6 @@ function executeValidation(fn, builtSchema, input, opt = {}, defaultInputName) {
1187
1190
  // the error message Input would contain already mutated object print, such as Input: {}
1188
1191
  // Unless `getOriginalInput` function is provided - then it will be used to preserve the Input pureness.
1189
1192
  const inputStringified = _inspect(opt.getOriginalInput?.() || input, { maxLen: 4000 });
1190
- // fingerprint is captured before appending the dynamic Input snippet,
1191
- // so we can group repeated validation errors by rule rather than by unique request content.
1192
- const fingerprint = message;
1193
1193
  message = [message, 'Input: ' + inputStringified].join(separator);
1194
1194
  const err = new AjvValidationError(message, _filterNullishValues({
1195
1195
  errors,
@@ -1222,6 +1222,22 @@ function applyImprovementsOnErrorMessages(errors, schema) {
1222
1222
  error.instancePath = error.instancePath.replaceAll(/\/(\d+)/g, `[$1]`).replaceAll('/', '.');
1223
1223
  }
1224
1224
  }
1225
+ /**
1226
+ * Groups repeated validation errors by rule rather than by unique request content.
1227
+ * Excludes instance-specific data like record IDs and array indices.
1228
+ */
1229
+ function buildAjvErrorFingerprint(e, inputName) {
1230
+ const value = Object.values(e.params || {})[0];
1231
+ let rule = e.keyword;
1232
+ if (value !== undefined)
1233
+ rule += `:${value}`;
1234
+ const path = e.instancePath
1235
+ .split('/')
1236
+ .filter(s => s && isNaN(Number(s)))
1237
+ .join('.');
1238
+ const location = [inputName, path].filter(Boolean).join('.');
1239
+ return [location, rule].join(' ');
1240
+ }
1225
1241
  /**
1226
1242
  * Filters out noisy errors produced by nullable anyOf patterns.
1227
1243
  * When `nullable()` wraps a schema in `anyOf: [realSchema, { type: 'null' }]`,
@@ -1362,10 +1378,7 @@ function hasNoObjectSchemas(schema) {
1362
1378
  else if (schema.type === 'array') {
1363
1379
  return !schema.items || hasNoObjectSchemas(schema.items);
1364
1380
  }
1365
- else {
1366
- return !!schema.type && ['string', 'number', 'integer', 'boolean', 'null'].includes(schema.type);
1367
- }
1368
- return false;
1381
+ return !!schema.type && ['string', 'number', 'integer', 'boolean', 'null'].includes(schema.type);
1369
1382
  }
1370
1383
  /**
1371
1384
  * Deep copy that preserves functions in customValidations/customConversions.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@naturalcycles/nodejs-lib",
3
3
  "type": "module",
4
- "version": "15.107.1",
4
+ "version": "15.107.3",
5
5
  "dependencies": {
6
6
  "@naturalcycles/js-lib": "^15",
7
7
  "@standard-schema/spec": "^1",
@@ -17,7 +17,7 @@
17
17
  "yargs": "^18"
18
18
  },
19
19
  "devDependencies": {
20
- "@typescript/native-preview": "7.0.0-dev.20260401.1",
20
+ "@typescript/native-preview": "beta",
21
21
  "@naturalcycles/dev-lib": "18.4.2"
22
22
  },
23
23
  "exports": {
package/src/util/git2.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { execSync } from 'node:child_process'
2
2
  import { basename } from 'node:path'
3
+ import { _uniq } from '@naturalcycles/js-lib/array'
3
4
  import type { UnixTimestamp } from '@naturalcycles/js-lib/types'
4
5
  import { exec2 } from '../exec2/exec2.js'
5
6
 
@@ -30,7 +31,7 @@ class Git2 {
30
31
  }
31
32
 
32
33
  /**
33
- * Returns true if there were changes
34
+ * @returns true if there were changes
34
35
  */
35
36
  commitAll(msg: string): boolean {
36
37
  // git commit -a -m "style(lint-all): $GIT_MSG" || true
@@ -134,6 +135,59 @@ class Git2 {
134
135
  .filter(s => !s.includes(' -> '))
135
136
  .map(s => s.split('/')[1]!)
136
137
  }
138
+
139
+ gitRefExists(ref: string): boolean {
140
+ try {
141
+ exec2.exec(`git rev-parse --verify --quiet ${ref}`)
142
+ return true
143
+ } catch {
144
+ return false
145
+ }
146
+ }
147
+
148
+ getTrackedFiles(): string[] {
149
+ try {
150
+ return exec2
151
+ .exec('git ls-files')
152
+ .split('\n')
153
+ .filter(s => s.trim().length)
154
+ } catch {
155
+ return []
156
+ }
157
+ }
158
+
159
+ getUntrackedFiles(): string[] {
160
+ try {
161
+ return exec2
162
+ .exec('git ls-files --others --exclude-standard')
163
+ .split('\n')
164
+ .filter(s => s.trim().length)
165
+ } catch {
166
+ return []
167
+ }
168
+ }
169
+
170
+ getTrackedChangedFiles(diffBase: string, diffFilter = 'AMR'): string[] {
171
+ try {
172
+ return exec2
173
+ .exec(`git diff --name-only --diff-filter=${diffFilter} ${diffBase}`)
174
+ .split('\n')
175
+ .filter(s => s.trim().length)
176
+ } catch {
177
+ return []
178
+ }
179
+ }
180
+
181
+ /**
182
+ * @returns both tracked changed and untracked files
183
+ */
184
+ getAllChangedFiles(diffBase: string): string[] {
185
+ const tracked = this.getTrackedChangedFiles(diffBase)
186
+ const untracked = this.getUntrackedFiles()
187
+
188
+ const changes = [...tracked, ...untracked]
189
+ return _uniq(changes)
190
+ }
137
191
  }
138
192
 
139
193
  export const git2 = new Git2()
@@ -1692,6 +1692,10 @@ function executeValidation<OUT>(
1692
1692
  } = opt
1693
1693
  const dataVar = [inputName, inputId].filter(Boolean).join('.')
1694
1694
 
1695
+ // Build fingerprint before applyImprovementsOnErrorMessages: after it, /items/0/name becomes
1696
+ // .items[0].name, embedding the index into the segment and making it harder to strip without regex
1697
+ const fingerprint = buildAjvErrorFingerprint(errors[0], inputName)
1698
+
1695
1699
  applyImprovementsOnErrorMessages(errors, builtSchema)
1696
1700
 
1697
1701
  let message = getAjv().errorsText(errors, {
@@ -1703,9 +1707,6 @@ function executeValidation<OUT>(
1703
1707
  // the error message Input would contain already mutated object print, such as Input: {}
1704
1708
  // Unless `getOriginalInput` function is provided - then it will be used to preserve the Input pureness.
1705
1709
  const inputStringified = _inspect(opt.getOriginalInput?.() || input, { maxLen: 4000 })
1706
- // fingerprint is captured before appending the dynamic Input snippet,
1707
- // so we can group repeated validation errors by rule rather than by unique request content.
1708
- const fingerprint = message
1709
1710
  message = [message, 'Input: ' + inputStringified].join(separator)
1710
1711
 
1711
1712
  const err = new AjvValidationError(
@@ -1723,7 +1724,7 @@ function executeValidation<OUT>(
1723
1724
  // ==== Error formatting helpers ====
1724
1725
 
1725
1726
  function applyImprovementsOnErrorMessages(
1726
- errors: ErrorObject<string, Record<string, any>, unknown>[] | null | undefined,
1727
+ errors: ErrorObject[] | null | undefined,
1727
1728
  schema: JsonSchema,
1728
1729
  ): void {
1729
1730
  if (!errors) return
@@ -1750,16 +1751,29 @@ function applyImprovementsOnErrorMessages(
1750
1751
  }
1751
1752
  }
1752
1753
 
1754
+ /**
1755
+ * Groups repeated validation errors by rule rather than by unique request content.
1756
+ * Excludes instance-specific data like record IDs and array indices.
1757
+ */
1758
+ function buildAjvErrorFingerprint(e: ErrorObject, inputName: string): string {
1759
+ const value = Object.values(e.params || {})[0]
1760
+ let rule = e.keyword
1761
+ if (value !== undefined) rule += `:${value}`
1762
+ const path = e.instancePath
1763
+ .split('/')
1764
+ .filter(s => s && isNaN(Number(s)))
1765
+ .join('.')
1766
+ const location = [inputName, path].filter(Boolean).join('.')
1767
+ return [location, rule].join(' ')
1768
+ }
1769
+
1753
1770
  /**
1754
1771
  * Filters out noisy errors produced by nullable anyOf patterns.
1755
1772
  * When `nullable()` wraps a schema in `anyOf: [realSchema, { type: 'null' }]`,
1756
1773
  * AJV produces "must be null" and "must match a schema in anyOf" errors
1757
1774
  * that are confusing. This method splices them out, keeping only the real errors.
1758
1775
  */
1759
- function filterNullableAnyOfErrors(
1760
- errors: ErrorObject<string, Record<string, any>, unknown>[],
1761
- schema: JsonSchema,
1762
- ): void {
1776
+ function filterNullableAnyOfErrors(errors: ErrorObject[], schema: JsonSchema): void {
1763
1777
  // Collect exact schemaPaths to remove (anyOf aggregates) and prefixes (null branches)
1764
1778
  const exactPaths: string[] = []
1765
1779
  const nullBranchPrefixes: string[] = []
@@ -1919,11 +1933,9 @@ function hasNoObjectSchemas(schema: JsonSchema): boolean {
1919
1933
  return true
1920
1934
  } else if (schema.type === 'array') {
1921
1935
  return !schema.items || hasNoObjectSchemas(schema.items)
1922
- } else {
1923
- return !!schema.type && ['string', 'number', 'integer', 'boolean', 'null'].includes(schema.type)
1924
1936
  }
1925
1937
 
1926
- return false
1938
+ return !!schema.type && ['string', 'number', 'integer', 'boolean', 'null'].includes(schema.type)
1927
1939
  }
1928
1940
 
1929
1941
  type EnumBaseType = 'string' | 'number' | 'other'