@jchip/error 2.1.8 → 3.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.
package/README.md CHANGED
@@ -91,6 +91,7 @@ AggregateError: test
91
91
 
92
92
  - Subclass of the built-in [AggregateError]; it does not patch the global
93
93
  - `stack` includes the stacks of the aggregated errors, indented
94
+ - Assigning `err.stack` replaces the top part and keeps the aggregated stacks
94
95
  - Takes any iterable of errors and the native `{ cause }` option
95
96
 
96
97
  Example with `cleanErrorStack`:
package/dist/index.js CHANGED
@@ -14,8 +14,9 @@ export function cleanErrorStack(error, { replacePath = `${process.cwd()}/`, igno
14
14
  if (!stack) {
15
15
  return String(stack);
16
16
  }
17
+ // normalize CRLF so every output line ends the same way
17
18
  const result = stack
18
- .split("\n")
19
+ .split(/\r?\n/)
19
20
  .map((line) => {
20
21
  // keep all non stack tracing lines
21
22
  if (!line.match(/ {4,}at/)) {
@@ -37,10 +38,11 @@ export function cleanErrorStack(error, { replacePath = `${process.cwd()}/`, igno
37
38
  }
38
39
  let path2 = path;
39
40
  if (replacePath && replacePath.length > 1) {
40
- // an ESM location is a file:// URL, so drop the scheme along with replacePath
41
- path2 = path.startsWith(`file://${replacePath}`)
42
- ? path.slice(`file://${replacePath}`.length)
43
- : path.replace(replacePath, "");
41
+ // strip replacePath only as a leading prefix. An ESM location is a file:// URL, so drop
42
+ // the scheme along with it.
43
+ const prefix = path.startsWith(`file://${replacePath}`) ? `file://${replacePath}` : replacePath;
44
+ if (path.startsWith(prefix))
45
+ path2 = path.slice(prefix.length);
44
46
  }
45
47
  return `${at}${name}${path2}${tail}`;
46
48
  })
@@ -56,14 +58,13 @@ export function cleanErrorStack(error, { replacePath = `${process.cwd()}/`, igno
56
58
  * @returns aggregate stack
57
59
  */
58
60
  export function aggregateStack(stack, errors) {
59
- return [stack]
60
- .concat(errors &&
61
- errors.map &&
62
- errors.map((e) => {
61
+ const stacks = errors && errors.map
62
+ ? errors.map((e) => {
63
63
  const s = e && (e.stack || e.message);
64
64
  return (s || String(e)).replace(/^/gm, " ");
65
- }))
66
- .join("\n");
65
+ })
66
+ : [];
67
+ return [stack].concat(stacks).join("\n");
67
68
  }
68
69
  /**
69
70
  * build the aggregate stack of an AggregateError
@@ -102,13 +103,18 @@ export class AggregateError extends globalThis.AggregateError {
102
103
  // save original stack
103
104
  __stack: {
104
105
  enumerable: false,
106
+ writable: true,
105
107
  value: this.stack,
106
108
  },
107
- // make aggregate stack
109
+ // make aggregate stack. Writing it replaces the header and keeps the errors' stacks.
108
110
  stack: {
109
111
  get() {
110
112
  return aggStack || (aggStack = aggregateErrorStack(this));
111
113
  },
114
+ set(value) {
115
+ this.__stack = value;
116
+ aggStack = "";
117
+ },
112
118
  },
113
119
  });
114
120
  }
package/docs/reference.md CHANGED
@@ -33,7 +33,7 @@ Returns the text of `error.stack` with noisy stack frames removed and file paths
33
33
 
34
34
  | option | type | default | behavior |
35
35
  | --- | --- | --- | --- |
36
- | `replacePath` | `false \| string` | `` `${process.cwd()}/` `` (evaluated on each call) | Substring removed from each kept frame path. Ignored when `false`, empty, or exactly 1 character long (so `"/"` does nothing). |
36
+ | `replacePath` | `false \| string` | `` `${process.cwd()}/` `` (evaluated on each call) | Leading prefix removed from each kept frame path. Ignored when `false`, empty, or exactly 1 character long (so `"/"` does nothing). |
37
37
  | `ignorePathFilter` | `(string \| RegExp)[]` | `[]` | Extra filters. A frame whose path matches any filter is dropped. Added to the built-in filter. |
38
38
 
39
39
  Input selection:
@@ -42,16 +42,16 @@ Input selection:
42
42
  - When neither exists (or `error` is `null`/`undefined`), nothing is cleaned and the return value is `String(error && (error.stack || error.message))`. That gives `"null"` for `null` and `"undefined"` for `undefined`, `{}`, or an object with empty stack and no message.
43
43
  - A non-string, truthy `stack` (such as a number) throws `TypeError` (`stack.split is not a function`).
44
44
 
45
- Processing, applied to each line of the text split on `"\n"`:
45
+ Processing, applied to each line of the text split on `"\n"` or `"\r\n"`:
46
46
 
47
47
  1. A line that does not match `/ {4,}at/` (an `at` preceded by 4 or more spaces anywhere in the line) is kept unchanged. This covers the message line, `Require stack:` lines, and frames indented with fewer than 4 spaces.
48
48
  2. A frame line is parsed with `/( {4,}at )(?:([^(]*\()(.+)(\).*)|(.+))/`. It has two forms. `at name (location)` takes the location from the first `(` to the last `)`, so a `)` inside a path does not cut it short. `at location` has no parentheses. Node uses it for anonymous functions and ES module top-level code.
49
49
  3. The frame is dropped when parsing fails (such as a bare ` at`), or unless the location contains `scheme://` (regex `/[^:]+:\/\//`) or `path.isAbsolute(location)` is true. That removes `node:internal/...` frames in either form, `(<anonymous>)`, `(index 0)`, and relative paths. `path.isAbsolute` is platform specific, so a Windows path like `C:\a\b.js` is dropped on POSIX.
50
50
  4. Backslashes in the location are replaced with `/`.
51
51
  5. The frame is dropped if the location matches the built-in filter or any `ignorePathFilter` entry. A `RegExp` entry uses `location.match(re)`. A string entry uses `location.includes(str)`. Falsy entries (such as `""`) are skipped.
52
- 6. Otherwise `replacePath` (when usable) is removed. A location that starts with `file://` + `replacePath` loses both, so an ES module URL under the current directory becomes a relative path. Any other location has the first occurrence of `replacePath` removed with `String.prototype.replace` (plain string, not a regex). It can match anywhere in the location, not only at the start. A `file://` URL outside `replacePath` therefore stays whole.
52
+ 6. Otherwise `replacePath` (when usable) is removed. A location that starts with `file://` + `replacePath` loses both, so an ES module URL under the current directory becomes a relative path. Any other location loses `replacePath` only when it starts with it (plain string, not a regex). A match in the middle of a path is left alone, and so is a `file://` URL outside `replacePath`.
53
53
  7. The kept frame is rebuilt as `at` + name + new location + the text after the last `)`. Text before the 4-space indent is not kept.
54
- 8. Dropped frames are removed and the remaining lines are joined with `"\n"`. Blank lines in the original text are kept.
54
+ 8. Dropped frames are removed and the remaining lines are joined with `"\n"`. Blank lines in the original text are kept. A CRLF stack therefore comes back with LF line endings throughout.
55
55
 
56
56
  Real stacks from CommonJS and ES modules are covered by tests on Node 22, 24 and 26 (`test/spec/real-stack.spec.ts`).
57
57
 
@@ -87,8 +87,8 @@ Nested aggregate stacks indent again, so each nesting level adds 2 spaces.
87
87
 
88
88
  Edge cases on `errors`:
89
89
 
90
- - An array (or any object with a `map` function) is mapped. `[]` returns `stack` alone with no trailing newline.
91
- - A falsy `errors` (`null`, `undefined`, `""`) or a value with no `map` function (such as a string or a plain object without `map`) is treated as no errors, but the result gets a trailing `"\n"`, for example `aggregateStack("t", null) === "t\n"`.
90
+ - An array (or any object with a `map` function) is mapped. `[]` returns `stack` alone.
91
+ - A falsy `errors` (`null`, `undefined`, `""`) or a value with no `map` function (such as a string or a plain object without `map`) is treated as no errors, and also returns `stack` alone, for example `aggregateStack("t", null) === "t"`.
92
92
  - A truthy `errors.map` that is not a function throws `TypeError`.
93
93
  - `stack` is not checked. A non-string is joined as is.
94
94
 
@@ -141,13 +141,13 @@ Instance properties (all defined with `Object.defineProperty`, so `Object.keys(e
141
141
  | --- | --- | --- |
142
142
  | `name` | non-enumerable, non-writable, non-configurable own property | `"AggregateError"`. A subclass cannot override it by assignment. |
143
143
  | `errors` | non-enumerable, writable, configurable | `Array.from(errors)`: a new array, even when `errors` is an array. |
144
- | `__stack` | non-enumerable, non-writable, non-configurable | `this.stack` read right after `super()`: the native stack text of the error. |
145
- | `stack` | non-enumerable, configurable accessor | Getter returning `aggregateErrorStack(this)`, computed on first read and cached. |
144
+ | `__stack` | non-enumerable, writable, non-configurable | `this.stack` read right after `super()`: the native stack text of the error. Writing `stack` replaces it. |
145
+ | `stack` | non-enumerable, configurable accessor | Getter returning `aggregateErrorStack(this)`, computed on first read and cached. The setter replaces the header. |
146
146
 
147
147
  `stack` details:
148
148
 
149
149
  - The cache means `errors` changes made after the first read of `stack` are not reflected. Changes made before the first read are.
150
- - Only a getter is defined. The existing native setter of the engine remains, so assigning to `err.stack` does not throw and has no effect on the getter result (observed on Node 24).
150
+ - Assigning `err.stack = text` replaces the header: `__stack` becomes `text` and the cache is cleared. The next read gives `text` followed by the errors' indented stacks, so the aggregated stacks are kept.
151
151
  - `instanceof` works against the native class (`new AggregateError([]) instanceof globalThis.AggregateError` is `true`). A native instance is not an instance of this class.
152
152
  - `console.log(err)` and `err.stack` in a debugger show the aggregate form. `cleanErrorStack(err)` therefore also cleans the nested error stacks, and the nested lines keep their 2-space indent.
153
153
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jchip/error",
3
- "version": "2.1.8",
3
+ "version": "3.0.0",
4
4
  "description": "utilities and polyfill for node.js errors",
5
5
  "homepage": "https://fynjs.pages.dev/api/modules/_jchip_error",
6
6
  "type": "module",