@poppinss/utils 7.0.1 → 7.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 CHANGED
@@ -119,6 +119,55 @@ import lodash from '@poppinss/utils/lodash'
119
119
  lodash.pick(collection, keys)
120
120
  ```
121
121
 
122
+ ## Number helpers
123
+
124
+ The number helpers are available from the `@poppinss/utils/number` subpath.
125
+
126
+ ```ts
127
+ import number from '@poppinss/utils/number'
128
+ ```
129
+
130
+ ### clamp
131
+
132
+ Constrain a number to an inclusive range.
133
+
134
+ ```ts
135
+ number.clamp(15, 0, 10) // 10
136
+ number.clamp(-2, 0, 10) // 0
137
+ number.clamp(5, 0, 10) // 5
138
+ ```
139
+
140
+ ### between
141
+
142
+ Check whether a number is inside an inclusive range. You may specify the bounds in either order.
143
+
144
+ ```ts
145
+ number.between(5, 0, 10) // true
146
+ number.between(5, 10, 0) // true
147
+ number.between(11, 0, 10) // false
148
+ ```
149
+
150
+ ### toFinite
151
+
152
+ Convert a value to a finite number. The method returns `0` when conversion fails or produces a non-finite number. You may provide a different fallback as the second argument.
153
+
154
+ ```ts
155
+ number.toFinite('42') // 42
156
+ number.toFinite('invalid') // 0
157
+ number.toFinite(Number.POSITIVE_INFINITY, 10) // 10
158
+ ```
159
+
160
+ ### parse
161
+
162
+ Convert a value to a finite number without using a fallback. The method returns `null` for empty input, failed conversions, and non-finite numbers.
163
+
164
+ ```ts
165
+ number.parse('42') // 42
166
+ number.parse('') // null
167
+ number.parse('invalid') // null
168
+ number.parse(Number.POSITIVE_INFINITY) // null
169
+ ```
170
+
122
171
  ## FS helpers
123
172
 
124
173
  ### fsReadAll
@@ -457,6 +506,17 @@ flatten({
457
506
  // }
458
507
  ```
459
508
 
509
+ ## unique
510
+
511
+ Return a new array with duplicate values removed while preserving the first occurrence order.
512
+
513
+ ```ts
514
+ import { unique } from '@poppinss/utils'
515
+
516
+ unique(['foo', 'bar', 'foo', 'baz'])
517
+ // ['foo', 'bar', 'baz']
518
+ ```
519
+
460
520
  ## isScriptFile
461
521
 
462
522
  A filter to know if the file path ends with `.js`, `.json`, `.cjs`, `.mjs`, or `.ts`. In the case of `.ts` files, the `.d.ts` returns false.
@@ -496,6 +556,32 @@ const values = ['1_foo_bar', '12_foo_bar'].sort(naturalSort)
496
556
  // Default sorting: ['1_foo_bar', '12_foo_bar']
497
557
  ```
498
558
 
559
+ ## getGitWorktree
560
+
561
+ Returns information about the linked Git worktree containing the current working directory. The method returns `null` when called from the primary worktree, outside a Git repository, or when Git is unavailable.
562
+
563
+ ```ts
564
+ import { getGitWorktree } from '@poppinss/utils'
565
+
566
+ const worktree = await getGitWorktree()
567
+
568
+ if (worktree) {
569
+ console.log(worktree)
570
+ // {
571
+ // name: 'feature-auth',
572
+ // slug: 'feature-auth',
573
+ // hash: '8d31cbe490f2',
574
+ // path: '/projects/worktrees/feature-auth'
575
+ // }
576
+ }
577
+ ```
578
+
579
+ You may pass a different directory as the first argument. The `slug` is URL-safe, and the `hash` contains the first twelve characters of a SHA-256 hash generated from the canonical worktree path.
580
+
581
+ ```ts
582
+ await getGitWorktree('/projects/worktrees/feature-auth/server')
583
+ ```
584
+
499
585
  ## safeEqual
500
586
 
501
587
  Check if two buffer or string values are the same. This method does not leak any timing information and prevents [timing attack](https://javascript.plainenglish.io/what-are-timing-attacks-and-how-to-prevent-them-using-nodejs-158cc7e2d70c).
package/build/index.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  export { Secret } from './src/secret.js';
2
2
  export { compose } from './src/compose.js';
3
3
  export { flatten } from './src/flatten.js';
4
+ export { unique } from './src/unique.js';
4
5
  export { safeEqual } from './src/safe_equal.js';
5
6
  export { naturalSort } from './src/natural_sort.js';
6
7
  export { isScriptFile } from './src/is_script_file.js';
@@ -9,3 +10,4 @@ export { MessageBuilder } from './src/message_builder.js';
9
10
  export { ImportsBag, type ImportInfo } from './src/imports_bag.js';
10
11
  export { defineStaticProperty } from './src/define_static_property.js';
11
12
  export { detectAIAgent, isRunningInAIAgent } from './src/detect_ai_agent.js';
13
+ export { getGitWorktree, type GitWorktree } from './src/get_git_worktree.js';
package/build/index.js CHANGED
@@ -1,14 +1,27 @@
1
- import { n as safeParse, t as safeStringify } from "./safe_stringify-DenlQd3-.js";
2
- import { n as naturalSort, t as isScriptFile } from "./is_script_file-CbSEfxE_.js";
1
+ import { n as safeParse, t as safeStringify } from "./main-B9qlOEvt.js";
2
+ import { n as naturalSort, t as isScriptFile } from "./is_script_file-D0Om5zlQ.js";
3
3
  import { RuntimeException } from "./modules/exception.js";
4
- import "./modules/json/main.js";
5
4
  import { flattie } from "flattie";
6
5
  import { Buffer } from "node:buffer";
7
- import { timingSafeEqual } from "node:crypto";
6
+ import { createHash, timingSafeEqual } from "node:crypto";
7
+ import { basename, isAbsolute, relative, sep } from "node:path";
8
8
  import string from "@poppinss/string";
9
9
  import lodash from "@poppinss/utils/lodash";
10
+ import { promisify } from "node:util";
11
+ import { realpath } from "node:fs/promises";
12
+ import { execFile } from "node:child_process";
13
+ //#region src/secret.ts
10
14
  const REDACTED = "[redacted]";
15
+ /**
16
+ * Define a Secret value that hides itself from the logs or the console
17
+ * statements.
18
+ *
19
+ * The idea is to prevent accidental leaking of sensitive information.
20
+ * Idea borrowed from.
21
+ * https://transcend.io/blog/keep-sensitive-values-out-of-your-logs-with-types
22
+ */
11
23
  var Secret = class Secret {
24
+ /** The secret value */
12
25
  #value;
13
26
  #keyword;
14
27
  constructor(value, redactedKeyword) {
@@ -30,64 +43,156 @@ var Secret = class Secret {
30
43
  toString() {
31
44
  return this.#keyword;
32
45
  }
46
+ /**
47
+ * Returns the original value
48
+ */
33
49
  release() {
34
50
  return this.#value;
35
51
  }
52
+ /**
53
+ * Transform the original value and create a new
54
+ * secret from it.
55
+ */
36
56
  map(transformFunc) {
37
57
  return new Secret(transformFunc(this.#value));
38
58
  }
39
59
  };
60
+ //#endregion
61
+ //#region src/compose.ts
40
62
  function compose(superclass, ...mixins) {
41
63
  return mixins.reduce((c, mixin) => mixin(c), superclass);
42
64
  }
65
+ //#endregion
66
+ //#region src/flatten.ts
67
+ /**
68
+ * Recursively flatten an object/array.
69
+ */
43
70
  function flatten(input, glue, keepNullish) {
44
71
  return flattie(input, glue, keepNullish);
45
72
  }
73
+ //#endregion
74
+ //#region src/unique.ts
75
+ /**
76
+ * Return a new array with duplicate values removed while preserving order.
77
+ */
78
+ function unique(values) {
79
+ return Array.from(new Set(values));
80
+ }
81
+ //#endregion
82
+ //#region src/safe_equal.ts
83
+ /**
84
+ * Compare two values to see if they are equal. The comparison is done in
85
+ * a way to avoid timing-attacks.
86
+ */
46
87
  function safeEqual(trustedValue, userInput) {
47
88
  if (typeof trustedValue === "string" && typeof userInput === "string") {
89
+ /**
90
+ * The length of the comparison value.
91
+ */
48
92
  const trustedLength = Buffer.byteLength(trustedValue);
93
+ /**
94
+ * Expected value
95
+ */
49
96
  const trustedValueBuffer = Buffer.alloc(trustedLength, 0, "utf-8");
50
97
  trustedValueBuffer.write(trustedValue);
98
+ /**
99
+ * Actual value (taken from user input)
100
+ */
51
101
  const userValueBuffer = Buffer.alloc(trustedLength, 0, "utf-8");
52
102
  userValueBuffer.write(userInput);
103
+ /**
104
+ * Ensure values are same and also have same length
105
+ */
53
106
  return timingSafeEqual(trustedValueBuffer, userValueBuffer) && trustedLength === Buffer.byteLength(userInput);
54
107
  }
55
108
  return timingSafeEqual(Buffer.from(trustedValue), Buffer.from(userInput));
56
109
  }
110
+ //#endregion
111
+ //#region src/import_default.ts
112
+ /**
113
+ * Dynamically import a module and ensure it has a default export
114
+ */
57
115
  async function importDefault(importFn, filePath) {
58
116
  const moduleExports = await importFn();
59
- if (!("default" in moduleExports)) throw new RuntimeException(filePath ? `Missing "export default" in module "${filePath}"` : `Missing "export default" from lazy import "${importFn}"`, { cause: { source: importFn } });
117
+ /**
118
+ * Make sure a default export exists
119
+ */
120
+ if (!("default" in moduleExports)) {
121
+ const errorMessage = filePath ? `Missing "export default" in module "${filePath}"` : `Missing "export default" from lazy import "${importFn}"`;
122
+ throw new RuntimeException(errorMessage, { cause: { source: importFn } });
123
+ }
60
124
  return moduleExports.default;
61
125
  }
126
+ //#endregion
127
+ //#region src/message_builder.ts
128
+ /**
129
+ * Message builder exposes an API to "JSON.stringify" values by
130
+ * encoding purpose and expiry date inside them.
131
+ *
132
+ * The return value must be further encrypted to prevent tempering.
133
+ */
62
134
  var MessageBuilder = class {
63
135
  #getExpiryDate(expiresIn) {
64
136
  if (!expiresIn) return;
65
137
  const expiryMs = string.milliseconds.parse(expiresIn);
66
138
  return new Date(Date.now() + expiryMs);
67
139
  }
140
+ /**
141
+ * Returns a boolean telling, if message has been expired or not
142
+ */
68
143
  #isExpired(message) {
69
144
  if (!message.expiryDate) return false;
70
145
  const expiryDate = new Date(message.expiryDate);
71
146
  return Number.isNaN(expiryDate.getTime()) || expiryDate < /* @__PURE__ */ new Date();
72
147
  }
148
+ /**
149
+ * Builds a message by encoding expiry date and purpose inside it.
150
+ */
73
151
  build(message, expiresIn, purpose) {
152
+ const expiryDate = this.#getExpiryDate(expiresIn);
74
153
  return safeStringify({
75
154
  message,
76
155
  purpose,
77
- expiryDate: this.#getExpiryDate(expiresIn)
156
+ expiryDate
78
157
  });
79
158
  }
159
+ /**
160
+ * Verifies the message for expiry and purpose.
161
+ */
80
162
  verify(message, purpose) {
81
163
  const parsed = safeParse(message);
164
+ /**
165
+ * After JSON.parse we do not receive a valid object
166
+ */
82
167
  if (typeof parsed !== "object" || !parsed) return null;
168
+ /**
169
+ * Missing ".message" property
170
+ */
83
171
  if (!parsed.message) return null;
172
+ /**
173
+ * Ensure purposes are same.
174
+ */
84
175
  if (parsed.purpose !== purpose) return null;
176
+ /**
177
+ * Ensure isn't expired
178
+ */
85
179
  if (this.#isExpired(parsed)) return null;
86
180
  return parsed.message;
87
181
  }
88
182
  };
183
+ //#endregion
184
+ //#region src/imports_bag.ts
185
+ /**
186
+ * ImportsBag manages and deduplicates imports from the same source
187
+ */
89
188
  var ImportsBag = class {
189
+ /**
190
+ * Map of source to ImportInfo
191
+ */
90
192
  #imports = /* @__PURE__ */ new Map();
193
+ /**
194
+ * Generate import statement from ImportInfo
195
+ */
91
196
  #generateImportStatement(imp) {
92
197
  const parts = [];
93
198
  if (imp.defaultImport || imp.namedImports && imp.namedImports.length > 0) {
@@ -104,15 +209,30 @@ var ImportsBag = class {
104
209
  }
105
210
  return parts.join("\n");
106
211
  }
212
+ /**
213
+ * Add an import to the bag
214
+ */
107
215
  add(importInfo) {
108
216
  const existing = this.#imports.get(importInfo.source);
109
217
  if (existing) {
218
+ /**
219
+ * Set default import (replaces existing if present)
220
+ */
110
221
  if (importInfo.defaultImport) existing.defaultImport = importInfo.defaultImport;
222
+ /**
223
+ * Set default type import (replaces existing if present)
224
+ */
111
225
  if (importInfo.defaultTypeImport) existing.defaultTypeImport = importInfo.defaultTypeImport;
226
+ /**
227
+ * Merge named imports without deduplication (deduplication happens in toArray)
228
+ */
112
229
  if (importInfo.namedImports) {
113
230
  if (!existing.namedImports) existing.namedImports = [];
114
231
  existing.namedImports.push(...importInfo.namedImports);
115
232
  }
233
+ /**
234
+ * Merge type imports without deduplication (deduplication happens in toArray)
235
+ */
116
236
  if (importInfo.typeImports) {
117
237
  if (!existing.typeImports) existing.typeImports = [];
118
238
  existing.typeImports.push(...importInfo.typeImports);
@@ -126,22 +246,38 @@ var ImportsBag = class {
126
246
  });
127
247
  return this;
128
248
  }
249
+ /**
250
+ * Get deduplicated imports as an array
251
+ */
129
252
  toArray() {
130
253
  return Array.from(this.#imports.values()).map((imp) => ({
131
254
  source: imp.source,
132
255
  defaultImport: imp.defaultImport,
133
256
  defaultTypeImport: imp.defaultTypeImport,
134
- namedImports: imp.namedImports ? [...new Set(imp.namedImports)] : void 0,
135
- typeImports: imp.typeImports ? [...new Set(imp.typeImports)] : void 0
257
+ namedImports: imp.namedImports ? unique(imp.namedImports) : void 0,
258
+ typeImports: imp.typeImports ? unique(imp.typeImports) : void 0
136
259
  }));
137
260
  }
261
+ /**
262
+ * Get deduplicated imports as a formatted string
263
+ */
138
264
  toString() {
139
265
  return this.toArray().map((imp) => this.#generateImportStatement(imp)).join("\n");
140
266
  }
141
267
  };
268
+ //#endregion
269
+ //#region src/define_static_property.ts
270
+ /**
271
+ * Define static properties on a class with inheritance in play.
272
+ */
142
273
  function defineStaticProperty(self, propertyName, { initialValue, strategy }) {
143
274
  if (!self.hasOwnProperty(propertyName)) {
144
275
  const value = self[propertyName];
276
+ /**
277
+ * Define the property as it is when the strategy is set
278
+ * to "define". Or the value on the prototype chain
279
+ * is set to undefined.
280
+ */
145
281
  if (strategy === "define" || value === void 0) {
146
282
  Object.defineProperty(self, propertyName, {
147
283
  value: initialValue,
@@ -159,6 +295,19 @@ function defineStaticProperty(self, propertyName, { initialValue, strategy }) {
159
295
  });
160
296
  }
161
297
  }
298
+ //#endregion
299
+ //#region src/detect_ai_agent.ts
300
+ /**
301
+ * Detects which AI coding agent the code is running under.
302
+ * Checks for environment variables set by different AI coding assistants:
303
+ * - CLAUDECODE='1' for Claude Code
304
+ * - GEMINI_CLI='1' for Gemini
305
+ * - GITHUB_COPILOT_CLI_MODE='1' for GitHub Copilot
306
+ * - WINDSURF_SESSION='1' or TERM_PROGRAM='windsurf' for Windsurf
307
+ * - CODEX_CLI='1' or CODEX_SANDBOX='1' for Codex
308
+ * - OPENCODE='1' for OpenCode
309
+ * - CURSOR_AGENT='1' for Cursor
310
+ */
162
311
  function detectAIAgent() {
163
312
  if (process.env.CLAUDECODE === "1") return "claude";
164
313
  if (process.env.GEMINI_CLI === "1") return "gemini";
@@ -169,7 +318,67 @@ function detectAIAgent() {
169
318
  if (process.env.CURSOR_AGENT === "1") return "cursor";
170
319
  return null;
171
320
  }
321
+ /**
322
+ * Returns true if the code is running within any AI coding agent.
323
+ */
172
324
  function isRunningInAIAgent() {
173
325
  return detectAIAgent() !== null;
174
326
  }
175
- export { ImportsBag, MessageBuilder, Secret, compose, defineStaticProperty, detectAIAgent, flatten, importDefault, isRunningInAIAgent, isScriptFile, naturalSort, safeEqual };
327
+ //#endregion
328
+ //#region src/get_git_worktree.ts
329
+ const execFileAsync = promisify(execFile);
330
+ /**
331
+ * Returns true when the target path is inside the parent path.
332
+ */
333
+ function isPathInside(parent, target) {
334
+ const relativePath = relative(parent, target);
335
+ return relativePath === "" || relativePath !== ".." && !relativePath.startsWith(`..${sep}`) && !isAbsolute(relativePath);
336
+ }
337
+ /**
338
+ * Returns information about the linked Git worktree containing the given
339
+ * directory. Returns null for the main worktree and directories outside a
340
+ * Git repository.
341
+ */
342
+ async function getGitWorktree(cwd = process.cwd()) {
343
+ try {
344
+ const [{ stdout }, resolvedCwd] = await Promise.all([execFileAsync("git", [
345
+ "worktree",
346
+ "list",
347
+ "--porcelain",
348
+ "-z"
349
+ ], {
350
+ cwd,
351
+ encoding: "utf8",
352
+ windowsHide: true
353
+ }), realpath(cwd)]);
354
+ const worktreePaths = stdout.split("\0").filter((field) => field.startsWith("worktree ")).map((field) => field.slice(9));
355
+ const currentWorktree = (await Promise.all(worktreePaths.map(async (worktreePath, index) => {
356
+ try {
357
+ return {
358
+ index,
359
+ path: await realpath(worktreePath)
360
+ };
361
+ } catch {
362
+ return null;
363
+ }
364
+ }))).filter((worktree) => {
365
+ return worktree !== null && isPathInside(worktree.path, resolvedCwd);
366
+ }).sort((current, next) => next.path.length - current.path.length)[0];
367
+ if (!currentWorktree || currentWorktree.index === 0) return null;
368
+ const name = basename(currentWorktree.path);
369
+ const hash = createHash("sha256").update(currentWorktree.path).digest("hex").slice(0, 12);
370
+ return {
371
+ name,
372
+ hash,
373
+ path: currentWorktree.path,
374
+ slug: string.slug(name, {
375
+ lower: true,
376
+ strict: true
377
+ }) || `worktree-${hash}`
378
+ };
379
+ } catch {
380
+ return null;
381
+ }
382
+ }
383
+ //#endregion
384
+ export { ImportsBag, MessageBuilder, Secret, compose, defineStaticProperty, detectAIAgent, flatten, getGitWorktree, importDefault, isRunningInAIAgent, isScriptFile, naturalSort, safeEqual, unique };
@@ -1,20 +1,33 @@
1
1
  import { extname } from "node:path";
2
+ //#region src/natural_sort.ts
3
+ let collator;
4
+ /**
5
+ * Perform natural sorting with "Array.sort()" method
6
+ */
2
7
  function naturalSort(current, next) {
3
- return current.localeCompare(next, void 0, {
8
+ collator ??= new Intl.Collator(void 0, {
4
9
  numeric: true,
5
10
  sensitivity: "base"
6
11
  });
12
+ return collator.compare(current, next);
7
13
  }
14
+ //#endregion
15
+ //#region src/is_script_file.ts
8
16
  const JS_MODULES = [
9
17
  ".js",
10
18
  ".json",
11
19
  ".cjs",
12
20
  ".mjs"
13
21
  ];
22
+ /**
23
+ * Returns `true` when file ends with `.js`, `.json` or
24
+ * `.ts` but not `.d.ts`.
25
+ */
14
26
  function isScriptFile(filePath) {
15
27
  const ext = extname(filePath);
16
28
  if (JS_MODULES.includes(ext)) return true;
17
29
  if (ext === ".ts" && !filePath.endsWith(".d.ts")) return true;
18
30
  return false;
19
31
  }
32
+ //#endregion
20
33
  export { naturalSort as n, isScriptFile as t };
@@ -1,11 +1,11 @@
1
- import "node:module";
1
+ //#region \0rolldown/runtime.js
2
2
  var __create = Object.create;
3
3
  var __defProp = Object.defineProperty;
4
4
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
6
  var __getProtoOf = Object.getPrototypeOf;
7
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
8
- var __commonJSMin = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
8
+ var __commonJSMin = (cb, mod) => () => (mod || (cb((mod = { exports: {} }).exports, mod), cb = null), mod.exports);
9
9
  var __copyProps = (to, from, except, desc) => {
10
10
  if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
11
11
  key = keys[i];
@@ -16,14 +16,26 @@ var __copyProps = (to, from, except, desc) => {
16
16
  }
17
17
  return to;
18
18
  };
19
- var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
19
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule || !__hasOwnProp.call(mod, "default") ? __defProp(target, "default", {
20
20
  value: mod,
21
21
  enumerable: true
22
22
  }) : target, mod));
23
+ //#endregion
24
+ //#region modules/json/safe_parse.ts
23
25
  var import_secure_json_parse = (/* @__PURE__ */ __commonJSMin(((exports, module) => {
24
26
  const hasBuffer = typeof Buffer !== "undefined";
25
27
  const suspectProtoRx = /"(?:_|\\u005[Ff])(?:_|\\u005[Ff])(?:p|\\u0070)(?:r|\\u0072)(?:o|\\u006[Ff])(?:t|\\u0074)(?:o|\\u006[Ff])(?:_|\\u005[Ff])(?:_|\\u005[Ff])"\s*:/;
26
28
  const suspectConstructorRx = /"(?:c|\\u0063)(?:o|\\u006[Ff])(?:n|\\u006[Ee])(?:s|\\u0073)(?:t|\\u0074)(?:r|\\u0072)(?:u|\\u0075)(?:c|\\u0063)(?:t|\\u0074)(?:o|\\u006[Ff])(?:r|\\u0072)"\s*:/;
29
+ /**
30
+ * @description Internal parse function that parses JSON text with security checks.
31
+ * @private
32
+ * @param {string|Buffer} text - The JSON text string or Buffer to parse.
33
+ * @param {Function} [reviver] - The JSON.parse() optional reviver argument.
34
+ * @param {import('./types').ParseOptions} [options] - Optional configuration object.
35
+ * @returns {*} The parsed object.
36
+ * @throws {SyntaxError} If a forbidden prototype property is found and `options.protoAction` or
37
+ * `options.constructorAction` is `'error'`.
38
+ */
27
39
  function _parse(text, reviver, options) {
28
40
  if (options == null) {
29
41
  if (reviver !== null && typeof reviver === "object") {
@@ -49,6 +61,14 @@ var import_secure_json_parse = (/* @__PURE__ */ __commonJSMin(((exports, module)
49
61
  safe: options && options.safe
50
62
  });
51
63
  }
64
+ /**
65
+ * @description Scans and filters an object for forbidden prototype properties.
66
+ * @param {Object} obj - The object being scanned.
67
+ * @param {import('./types').ParseOptions} [options] - Optional configuration object.
68
+ * @returns {Object|null} The filtered object, or `null` if safe mode is enabled and issues are found.
69
+ * @throws {SyntaxError} If a forbidden prototype property is found and `options.protoAction` or
70
+ * `options.constructorAction` is `'error'`.
71
+ */
52
72
  function filter(obj, { protoAction = "error", constructorAction = "error", safe } = {}) {
53
73
  let next = [obj];
54
74
  while (next.length) {
@@ -73,6 +93,15 @@ var import_secure_json_parse = (/* @__PURE__ */ __commonJSMin(((exports, module)
73
93
  }
74
94
  return obj;
75
95
  }
96
+ /**
97
+ * @description Parses a given JSON-formatted text into an object.
98
+ * @param {string|Buffer} text - The JSON text string or Buffer to parse.
99
+ * @param {Function} [reviver] - The `JSON.parse()` optional reviver argument, or options object.
100
+ * @param {import('./types').ParseOptions} [options] - Optional configuration object.
101
+ * @returns {*} The parsed object.
102
+ * @throws {SyntaxError} If the JSON text is malformed or contains forbidden prototype properties
103
+ * when `options.protoAction` or `options.constructorAction` is `'error'`.
104
+ */
76
105
  function parse(text, reviver, options) {
77
106
  const { stackTraceLimit } = Error;
78
107
  Error.stackTraceLimit = 0;
@@ -82,6 +111,12 @@ var import_secure_json_parse = (/* @__PURE__ */ __commonJSMin(((exports, module)
82
111
  Error.stackTraceLimit = stackTraceLimit;
83
112
  }
84
113
  }
114
+ /**
115
+ * @description Safely parses a given JSON-formatted text into an object.
116
+ * @param {string|Buffer} text - The JSON text string or Buffer to parse.
117
+ * @param {Function} [reviver] - The `JSON.parse()` optional reviver argument.
118
+ * @returns {*|null|undefined} The parsed object, `null` if security issues found, or `undefined` on parse error.
119
+ */
85
120
  function safeParse(text, reviver) {
86
121
  const { stackTraceLimit } = Error;
87
122
  Error.stackTraceLimit = 0;
@@ -99,12 +134,17 @@ var import_secure_json_parse = (/* @__PURE__ */ __commonJSMin(((exports, module)
99
134
  module.exports.safeParse = safeParse;
100
135
  module.exports.scan = filter;
101
136
  })))();
137
+ /**
138
+ * A drop-in replacement for JSON.parse with prototype poisoning protection.
139
+ */
102
140
  function safeParse(jsonString, reviver) {
103
141
  return (0, import_secure_json_parse.parse)(jsonString, reviver, {
104
142
  protoAction: "remove",
105
143
  constructorAction: "remove"
106
144
  });
107
145
  }
146
+ //#endregion
147
+ //#region node_modules/safe-stable-stringify/esm/wrapper.js
108
148
  var import_safe_stable_stringify = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin(((exports, module) => {
109
149
  const { hasOwnProperty } = Object.prototype;
110
150
  const stringify = configure();
@@ -132,7 +172,7 @@ var import_safe_stable_stringify = /* @__PURE__ */ __toESM((/* @__PURE__ */ __co
132
172
  }
133
173
  return array;
134
174
  }
135
- const typedArrayPrototypeGetSymbolToStringTag = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(Object.getPrototypeOf(new Int8Array())), Symbol.toStringTag).get;
175
+ const typedArrayPrototypeGetSymbolToStringTag = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(Object.getPrototypeOf(/* @__PURE__ */ new Int8Array())), Symbol.toStringTag).get;
136
176
  function isTypedArrayWithEntries(value) {
137
177
  return typedArrayPrototypeGetSymbolToStringTag.call(value) !== void 0 && value.length !== 0;
138
178
  }
@@ -513,15 +553,22 @@ var import_safe_stable_stringify = /* @__PURE__ */ __toESM((/* @__PURE__ */ __co
513
553
  }
514
554
  return stringify;
515
555
  }
516
- })))(), 1);
556
+ })))());
517
557
  const configure = import_safe_stable_stringify.configure;
518
558
  import_safe_stable_stringify.default;
519
559
  import_safe_stable_stringify.default;
560
+ //#endregion
561
+ //#region modules/json/safe_stringify.ts
520
562
  const stringify = configure({
521
563
  bigint: false,
522
564
  circularValue: void 0,
523
565
  deterministic: false
524
566
  });
567
+ /**
568
+ * Replacer to handle custom data types.
569
+ *
570
+ * - Bigints are converted to string
571
+ */
525
572
  function jsonStringifyReplacer(replacer) {
526
573
  return function(key, value) {
527
574
  const val = replacer ? replacer.call(this, key, value) : value;
@@ -529,7 +576,12 @@ function jsonStringifyReplacer(replacer) {
529
576
  return val;
530
577
  };
531
578
  }
579
+ /**
580
+ * String Javascript values to a JSON string. Handles circular
581
+ * references and bigints
582
+ */
532
583
  function safeStringify(value, replacer, space) {
533
584
  return stringify(value, jsonStringifyReplacer(replacer), space);
534
585
  }
586
+ //#endregion
535
587
  export { safeParse as n, safeStringify as t };
@@ -1,18 +1,37 @@
1
1
  import { inspect } from "node:util";
2
2
  import { AssertionError } from "node:assert";
3
+ //#region modules/assert.ts
4
+ /**
5
+ * @alias "assertExists"
6
+ */
3
7
  function assert(value, message) {
4
8
  return assertExists(value, message);
5
9
  }
10
+ /**
11
+ * Assert the value is turthy or raise an exception.
12
+ *
13
+ * Truthy value excludes, undefined, null, and false values.
14
+ */
6
15
  function assertExists(value, message) {
7
16
  if (!value) throw new AssertionError({ message: message ?? "value is falsy" });
8
17
  }
18
+ /**
19
+ * Throws error when method is called
20
+ */
9
21
  function assertUnreachable(x) {
10
22
  throw new AssertionError({ message: `unreachable code executed: ${inspect(x)}` });
11
23
  }
24
+ /**
25
+ * Assert the value is not null.
26
+ */
12
27
  function assertNotNull(value, message) {
13
28
  if (value === null) throw new AssertionError({ message: message ?? "unexpected null value" });
14
29
  }
30
+ /**
31
+ * Assert the value is not undefined.
32
+ */
15
33
  function assertIsDefined(value, message) {
16
34
  if (value === void 0) throw new AssertionError({ message: message ?? "unexpected undefined value" });
17
35
  }
36
+ //#endregion
18
37
  export { assert, assertExists, assertIsDefined, assertNotNull, assertUnreachable };
@@ -1,3 +1,8 @@
1
+ //#region modules/base64.ts
2
+ /**
3
+ * Helper class to base64 encode/decode values with option
4
+ * for url encoding and decoding
5
+ */
1
6
  var Base64 = class {
2
7
  encode(data, encoding) {
3
8
  if (typeof data === "string") return Buffer.from(data, encoding).toString("base64");
@@ -23,4 +28,5 @@ var Base64 = class {
23
28
  }
24
29
  };
25
30
  const base64 = new Base64();
31
+ //#endregion
26
32
  export { base64 as default };
@@ -1,9 +1,27 @@
1
- import { n as naturalSort, t as isScriptFile } from "../../is_script_file-CbSEfxE_.js";
1
+ import { n as naturalSort, t as isScriptFile } from "../../is_script_file-D0Om5zlQ.js";
2
2
  import { extname, join, relative, sep } from "node:path";
3
3
  import string from "@poppinss/string";
4
4
  import lodash from "@poppinss/utils/lodash";
5
5
  import { readdir, stat } from "node:fs/promises";
6
6
  import { fileURLToPath, pathToFileURL } from "node:url";
7
+ //#region modules/fs/fs_read_all.ts
8
+ /**
9
+ * Returns an array of file paths from the given location. You can
10
+ * optionally filter and sort files by passing relevant options
11
+ *
12
+ * ```ts
13
+ * await fsReadAll(new URL('./', import.meta.url))
14
+ *
15
+ * await fsReadAll(new URL('./', import.meta.url), {
16
+ * filter: (filePath) => filePath.endsWith('.js')
17
+ * })
18
+
19
+ * await fsReadAll(new URL('./', import.meta.url), {
20
+ * absolute: true,
21
+ * unixPaths: true
22
+ * })
23
+ * ```
24
+ */
7
25
  async function fsReadAll(location, options) {
8
26
  const normalizedLocation = typeof location === "string" ? location : fileURLToPath(location);
9
27
  const normalizedOptions = Object.assign({
@@ -11,6 +29,10 @@ async function fsReadAll(location, options) {
11
29
  sort: naturalSort
12
30
  }, options);
13
31
  const pathType = normalizedOptions.pathType || "relative";
32
+ /**
33
+ * Check to see if the root directory exists and ignore
34
+ * error when "ignoreMissingRoot" is set to true
35
+ */
14
36
  try {
15
37
  await stat(normalizedLocation);
16
38
  } catch (error) {
@@ -38,13 +60,45 @@ async function fsReadAll(location, options) {
38
60
  if (normalizedOptions.filter) return files.filter(normalizedOptions.filter).sort(normalizedOptions.sort);
39
61
  return files.sort(normalizedOptions.sort);
40
62
  }
63
+ //#endregion
64
+ //#region modules/fs/fs_import_all.ts
65
+ /**
66
+ * Import the file and update the values collection with the default
67
+ * export.
68
+ */
41
69
  async function importFile(basePath, fileURL, values, options) {
70
+ /**
71
+ * Converting URL to file path
72
+ */
42
73
  const filePath = fileURLToPath(fileURL);
74
+ /**
75
+ * Grab file extension
76
+ */
43
77
  const fileExtension = extname(filePath);
44
78
  const collectionKey = relative(basePath, filePath).replace(new RegExp(`${fileExtension}$`), "").split(sep);
79
+ /**
80
+ * Import module
81
+ */
45
82
  const exportedValue = fileExtension === ".json" ? await import(fileURL, { with: { type: "json" } }) : await import(fileURL);
46
83
  lodash.set(values, options.transformKeys ? options.transformKeys(collectionKey) : collectionKey, exportedValue.default ? exportedValue.default : { ...exportedValue });
47
84
  }
85
+ /**
86
+ * Returns an array of file paths from the given location. You can
87
+ * optionally filter and sort files by passing relevant options
88
+ *
89
+ * ```ts
90
+ * await fsReadAll(new URL('./', import.meta.url))
91
+ *
92
+ * await fsReadAll(new URL('./', import.meta.url), {
93
+ * filter: (filePath) => filePath.endsWith('.js')
94
+ * })
95
+
96
+ * await fsReadAll(new URL('./', import.meta.url), {
97
+ * absolute: true,
98
+ * unixPaths: true
99
+ * })
100
+ * ```
101
+ */
48
102
  async function fsImportAll(location, options) {
49
103
  options = options || {};
50
104
  const collection = {};
@@ -54,7 +108,11 @@ async function fsImportAll(location, options) {
54
108
  ...options,
55
109
  pathType: "url"
56
110
  });
111
+ /**
112
+ * Parallelly import all the files and mutate the values collection
113
+ */
57
114
  await Promise.all(files.map((file) => importFile(normalizedLocation, file, collection, options)));
58
115
  return collection;
59
116
  }
117
+ //#endregion
60
118
  export { fsImportAll, fsReadAll };
@@ -1,2 +1,2 @@
1
- import { n as safeParse, t as safeStringify } from "../../safe_stringify-DenlQd3-.js";
1
+ import { n as safeParse, t as safeStringify } from "../../main-B9qlOEvt.js";
2
2
  export { safeParse, safeStringify };
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Collection of number helpers to clamp and parse numeric values.
3
+ *
4
+ * @example
5
+ * number.clamp(15, 0, 10) // 10
6
+ * number.between(5, 0, 10) // true
7
+ * number.parse('42') // 42
8
+ * number.toFinite('abc', 0) // 0
9
+ */
10
+ declare const number: {
11
+ /**
12
+ * Constrain a number to stay within the given bounds.
13
+ *
14
+ * @param value - The number to constrain
15
+ * @param min - The lower bound
16
+ * @param max - The upper bound
17
+ *
18
+ * @example
19
+ * number.clamp(15, 0, 10) // 10
20
+ * number.clamp(-2, 0, 10) // 0
21
+ * number.clamp(5, 0, 10) // 5
22
+ */
23
+ clamp(value: number, min: number, max: number): number;
24
+ /**
25
+ * Check if a number is inside an inclusive range. The bounds may be
26
+ * passed in either order.
27
+ *
28
+ * @param value - The number to test
29
+ * @param min - One end of the range
30
+ * @param max - The other end of the range
31
+ *
32
+ * @example
33
+ * number.between(5, 0, 10) // true
34
+ * number.between(0, 0, 10) // true
35
+ * number.between(11, 0, 10) // false
36
+ * number.between(5, 10, 0) // true
37
+ */
38
+ between(value: number, min: number, max: number): boolean;
39
+ /**
40
+ * Convert a value to a finite number. Returns the fallback when the
41
+ * result is `NaN` or `Infinity`.
42
+ *
43
+ * @param value - The value to convert
44
+ * @param fallback - Value to return when conversion fails. Defaults to `0`
45
+ *
46
+ * @example
47
+ * number.toFinite(5) // 5
48
+ * number.toFinite('42') // 42
49
+ * number.toFinite(Number.NaN) // 0
50
+ * number.toFinite(Number.POSITIVE_INFINITY) // 0
51
+ * number.toFinite('abc', 10) // 10
52
+ */
53
+ toFinite(value: unknown, fallback?: number): number;
54
+ /**
55
+ * Parse a value into a finite number. Returns `null` for empty input,
56
+ * `NaN`, and `Infinity` instead of substituting a fallback.
57
+ *
58
+ * @param value - The value to parse
59
+ *
60
+ * @example
61
+ * number.parse(5) // 5
62
+ * number.parse('42') // 42
63
+ * number.parse('') // null
64
+ * number.parse(null) // null
65
+ * number.parse(Number.NaN) // null
66
+ * number.parse(Number.POSITIVE_INFINITY) // null
67
+ */
68
+ parse(value: unknown): number | null;
69
+ };
70
+ export default number;
@@ -0,0 +1,95 @@
1
+ //#region modules/number.ts
2
+ /**
3
+ * Coerce an unknown value to a finite number without allowing numeric
4
+ * conversion errors to escape.
5
+ */
6
+ function toFiniteNumber(value) {
7
+ try {
8
+ const numericValue = typeof value === "number" ? value : Number(value);
9
+ return Number.isFinite(numericValue) ? numericValue : null;
10
+ } catch {
11
+ return null;
12
+ }
13
+ }
14
+ /**
15
+ * Collection of number helpers to clamp and parse numeric values.
16
+ *
17
+ * @example
18
+ * number.clamp(15, 0, 10) // 10
19
+ * number.between(5, 0, 10) // true
20
+ * number.parse('42') // 42
21
+ * number.toFinite('abc', 0) // 0
22
+ */
23
+ const number = {
24
+ /**
25
+ * Constrain a number to stay within the given bounds.
26
+ *
27
+ * @param value - The number to constrain
28
+ * @param min - The lower bound
29
+ * @param max - The upper bound
30
+ *
31
+ * @example
32
+ * number.clamp(15, 0, 10) // 10
33
+ * number.clamp(-2, 0, 10) // 0
34
+ * number.clamp(5, 0, 10) // 5
35
+ */
36
+ clamp(value, min, max) {
37
+ if (value < min) return min;
38
+ else if (value > max) return max;
39
+ else return value;
40
+ },
41
+ /**
42
+ * Check if a number is inside an inclusive range. The bounds may be
43
+ * passed in either order.
44
+ *
45
+ * @param value - The number to test
46
+ * @param min - One end of the range
47
+ * @param max - The other end of the range
48
+ *
49
+ * @example
50
+ * number.between(5, 0, 10) // true
51
+ * number.between(0, 0, 10) // true
52
+ * number.between(11, 0, 10) // false
53
+ * number.between(5, 10, 0) // true
54
+ */
55
+ between(value, min, max) {
56
+ return value >= Math.min(min, max) && value <= Math.max(min, max);
57
+ },
58
+ /**
59
+ * Convert a value to a finite number. Returns the fallback when the
60
+ * result is `NaN` or `Infinity`.
61
+ *
62
+ * @param value - The value to convert
63
+ * @param fallback - Value to return when conversion fails. Defaults to `0`
64
+ *
65
+ * @example
66
+ * number.toFinite(5) // 5
67
+ * number.toFinite('42') // 42
68
+ * number.toFinite(Number.NaN) // 0
69
+ * number.toFinite(Number.POSITIVE_INFINITY) // 0
70
+ * number.toFinite('abc', 10) // 10
71
+ */
72
+ toFinite(value, fallback = 0) {
73
+ return toFiniteNumber(value) ?? fallback;
74
+ },
75
+ /**
76
+ * Parse a value into a finite number. Returns `null` for empty input,
77
+ * `NaN`, and `Infinity` instead of substituting a fallback.
78
+ *
79
+ * @param value - The value to parse
80
+ *
81
+ * @example
82
+ * number.parse(5) // 5
83
+ * number.parse('42') // 42
84
+ * number.parse('') // null
85
+ * number.parse(null) // null
86
+ * number.parse(Number.NaN) // null
87
+ * number.parse(Number.POSITIVE_INFINITY) // null
88
+ */
89
+ parse(value) {
90
+ if (value === null || value === void 0 || value === "") return null;
91
+ return toFiniteNumber(value);
92
+ }
93
+ };
94
+ //#endregion
95
+ export { number as default };
@@ -1,3 +1,5 @@
1
1
  import string from "@poppinss/string";
2
+ //#region modules/string/main.ts
2
3
  var main_default = string;
4
+ //#endregion
3
5
  export { main_default as default };
@@ -1,3 +1,5 @@
1
1
  import StringBuilder from "@poppinss/string/builder";
2
+ //#region modules/string/string_builder.ts
2
3
  var string_builder_default = StringBuilder;
4
+ //#endregion
3
5
  export { string_builder_default as default };
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Information about a linked Git worktree.
3
+ */
4
+ export type GitWorktree = {
5
+ /**
6
+ * Basename of the worktree directory.
7
+ */
8
+ name: string;
9
+ /**
10
+ * URL-safe slug generated from the worktree name.
11
+ */
12
+ slug: string;
13
+ /**
14
+ * First twelve characters of the SHA-256 hash for the canonical path.
15
+ */
16
+ hash: string;
17
+ /**
18
+ * Canonical absolute path to the worktree.
19
+ */
20
+ path: string;
21
+ };
22
+ /**
23
+ * Returns information about the linked Git worktree containing the given
24
+ * directory. Returns null for the main worktree and directories outside a
25
+ * Git repository.
26
+ */
27
+ export declare function getGitWorktree(cwd?: string): Promise<GitWorktree | null>;
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Return a new array with duplicate values removed while preserving order.
3
+ */
4
+ export declare function unique<Value>(values: readonly Value[]): Value[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@poppinss/utils",
3
- "version": "7.0.1",
3
+ "version": "7.1.0",
4
4
  "description": "Handy utilities for repetitive work",
5
5
  "main": "build/index.js",
6
6
  "type": "module",
@@ -24,6 +24,7 @@
24
24
  "./assert": "./build/modules/assert.js",
25
25
  "./base64": "./build/modules/base64.js",
26
26
  "./exception": "./build/modules/exception.js",
27
+ "./number": "./build/modules/number.js",
27
28
  "./types": "./build/modules/types.js"
28
29
  },
29
30
  "scripts": {
@@ -42,34 +43,34 @@
42
43
  "quick:test": "node --import=@poppinss/ts-exec bin/test.ts"
43
44
  },
44
45
  "devDependencies": {
45
- "@adonisjs/eslint-config": "^3.0.0",
46
- "@adonisjs/logger": "^6.0.7",
47
- "@adonisjs/prettier-config": "^1.4.5",
46
+ "@adonisjs/eslint-config": "^3.1.0",
47
+ "@adonisjs/logger": "^7.1.1",
48
+ "@adonisjs/prettier-config": "^1.5.0",
48
49
  "@adonisjs/tsconfig": "^2.0.0",
49
50
  "@japa/assert": "^4.2.0",
50
51
  "@japa/expect-type": "^2.0.4",
51
52
  "@japa/runner": "^5.3.0",
52
53
  "@poppinss/ts-exec": "^1.4.4",
53
- "@release-it/conventional-changelog": "^10.0.5",
54
+ "@release-it/conventional-changelog": "^12.0.0",
54
55
  "@types/fs-extra": "^11.0.4",
55
- "@types/node": "^25.3.0",
56
- "c8": "^10.1.3",
57
- "eslint": "^10.0.2",
58
- "fs-extra": "^11.3.3",
59
- "lodash": "^4.17.23",
56
+ "@types/node": "^26.4.1",
57
+ "c8": "^12.0.0",
58
+ "eslint": "^10.10.0",
59
+ "fs-extra": "^11.4.0",
60
+ "lodash": "^4.18.1",
60
61
  "lodash-cli": "^4.17.5",
61
62
  "move-file-cli": "^3.0.0",
62
- "prettier": "^3.8.1",
63
- "release-it": "^19.2.4",
63
+ "prettier": "^3.9.6",
64
+ "release-it": "^21.0.2",
64
65
  "safe-stable-stringify": "^2.5.0",
65
66
  "secure-json-parse": "^4.1.0",
66
- "tsdown": "^0.20.3",
67
- "typescript": "^5.9.3"
67
+ "tsdown": "^0.23.0",
68
+ "typescript": "^6.0.3"
68
69
  },
69
70
  "dependencies": {
70
71
  "@poppinss/exception": "^1.2.3",
71
72
  "@poppinss/object-builder": "^1.1.0",
72
- "@poppinss/string": "^1.7.1",
73
+ "@poppinss/string": "^1.7.2",
73
74
  "@poppinss/types": "^1.2.1",
74
75
  "flattie": "^1.1.1"
75
76
  },
@@ -101,6 +102,7 @@
101
102
  "./modules/assert.ts",
102
103
  "./modules/base64.ts",
103
104
  "./modules/exception.ts",
105
+ "./modules/number.ts",
104
106
  "./modules/types.ts"
105
107
  ],
106
108
  "external": [