@cjser/clean-stack 6.0.0-cjser.2 → 7.0.0-cjser.2

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.
@@ -26,7 +26,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
26
26
  ));
27
27
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
28
28
 
29
- // packages/@cjser/clean-stack/index.js
29
+ // packages/@cjser/clean-stack.tmp-24-1793723115467/index.js
30
30
  var index_exports = {};
31
31
  __export(index_exports, {
32
32
  default: () => cleanStack
@@ -34,52 +34,102 @@ __export(index_exports, {
34
34
  module.exports = __toCommonJS(index_exports);
35
35
  var import_escape_string_regexp = __toESM(require("@cjser/escape-string-regexp"), 1);
36
36
  var import_url_extras = require("@cjser/url-extras");
37
- var extractPathRegex = /\s+at.*[(\s](.*)\)?/;
38
- var pathRegex = /^(?:(?:(?:node|node:[\w/]+|(?:(?:node:)?internal\/[\w/]*|.*node_modules\/(?:babel-polyfill|pirates)\/.*)?\w+)(?:\.js)?:\d+:\d+)|native)/;
39
- var simplePathRegex = /^\w+\.js:\d+:\d+$/;
37
+ var frameStartRegex = new RegExp("^\\s+at\\b", "v");
38
+ var pathRegex = new RegExp("^(?:(?:node|node:[\\w\\-\\/]+|internal\\/[\\w\\/]+(?<!\\/)|\\w+)(?:\\.js)?:\\d+:\\d+|native$)", "v");
39
+ var asyncPrefixRegex = new RegExp("^(?:async|new) ", "v");
40
+ var bareFrameEndRegex = new RegExp(":(?:\\d+:\\d+|0x[\\da-f]+)$", "v");
41
+ var simplePathRegex = new RegExp("^\\w+\\.js:\\d+:\\d+$", "v");
42
+ function getFrame(line) {
43
+ const frameStart = frameStartRegex.exec(line);
44
+ if (!frameStart) {
45
+ return void 0;
46
+ }
47
+ const end = line.trimEnd().length;
48
+ if (line[end - 1] === ")") {
49
+ let depth = 0;
50
+ let pathStart = -1;
51
+ for (let index = frameStart[0].length; index < end; index++) {
52
+ const character = line[index];
53
+ if (character === "(") {
54
+ if (depth === 0) {
55
+ pathStart = index + 1;
56
+ }
57
+ depth++;
58
+ } else if (character === ")" && depth > 0) {
59
+ depth--;
60
+ }
61
+ }
62
+ if (pathStart !== -1) {
63
+ const path2 = line.slice(pathStart, end - 1);
64
+ return path2 === "" ? void 0 : { path: path2, pathIndex: pathStart, hasFunctionName: true };
65
+ }
66
+ }
67
+ const rest = line.slice(frameStart[0].length, end).trimStart();
68
+ const path = asyncPrefixRegex.test(rest) ? rest.slice(rest.indexOf(" ") + 1) : rest;
69
+ return path === "native" || bareFrameEndRegex.test(path) ? { path, pathIndex: end - path.length, hasFunctionName: false } : void 0;
70
+ }
40
71
  function cleanStack(stack, { pretty = false, basePath, pathFilter } = {}) {
41
- var _a;
72
+ var _a, _b;
42
73
  if (typeof stack !== "string") {
43
74
  return void 0;
44
75
  }
45
- const basePathRegex = basePath && new RegExp(`(file://)?${(0, import_escape_string_regexp.default)(basePath.replaceAll("\\", "/"))}/?`, "g");
76
+ const slashedBasePath = basePath == null ? void 0 : basePath.replaceAll("\\", "/");
77
+ const isAbsoluteBasePath = Boolean(slashedBasePath == null ? void 0 : slashedBasePath.startsWith("/"));
78
+ const isWindowsDrivePath = new RegExp("^[A-Za-z]:", "v").test(slashedBasePath ?? "");
79
+ const normalizedBasePath = slashedBasePath == null ? void 0 : slashedBasePath.split("/").filter((segment) => segment !== "").join("/");
80
+ let basePathPrefix = "";
81
+ if (isAbsoluteBasePath) {
82
+ basePathPrefix = "(?:file:///?|/{1,2})";
83
+ } else if (isWindowsDrivePath) {
84
+ basePathPrefix = "(?:file:///)?";
85
+ }
86
+ const basePathBoundary = normalizedBasePath === "" ? "" : "(?:/|$)";
87
+ const basePathRegex = basePath && new RegExp(`^${basePathPrefix}${(0, import_escape_string_regexp.default)(normalizedBasePath)}${basePathBoundary}`, "gv");
46
88
  let homeDirectory = "";
47
89
  if (pretty) {
48
- const os = (_a = process.getBuiltinModule) == null ? void 0 : _a.call(process, "node:os");
90
+ const os = (_b = (_a = globalThis.process) == null ? void 0 : _a.getBuiltinModule) == null ? void 0 : _b.call(_a, "node:os");
49
91
  homeDirectory = os ? os.homedir().replaceAll("\\", "/") : "";
92
+ while (homeDirectory.length > 1 && homeDirectory.endsWith("/")) {
93
+ homeDirectory = homeDirectory.slice(0, -1);
94
+ }
50
95
  }
51
- return stack.replaceAll("\\", "/").split("\n").filter((line) => {
52
- const pathMatches = line.match(extractPathRegex);
53
- if (!(pathMatches == null ? void 0 : pathMatches[1])) {
96
+ const lines = stack.split("\n").map((rawLine) => {
97
+ const frame = getFrame(rawLine);
98
+ return {
99
+ line: frame ? rawLine.replaceAll("\\", "/") : rawLine,
100
+ path: frame == null ? void 0 : frame.path.replaceAll("\\", "/"),
101
+ pathIndex: frame == null ? void 0 : frame.pathIndex,
102
+ hasFunctionName: frame == null ? void 0 : frame.hasFunctionName
103
+ };
104
+ });
105
+ return lines.filter(({ path, hasFunctionName }) => {
106
+ if (path === void 0) {
54
107
  return true;
55
108
  }
56
- const match = pathMatches[1];
57
- if (match.includes(".app/Contents/Resources/electron.asar") || match.includes(".app/Contents/Resources/default_app.asar") || match.includes("node_modules/electron/dist/resources/electron.asar") || match.includes("node_modules/electron/dist/resources/default_app.asar")) {
109
+ const isInternalPath = pathRegex.test(path) && (hasFunctionName || !simplePathRegex.test(path));
110
+ if (isInternalPath || path.includes(".app/Contents/Resources/electron.asar") || path.includes(".app/Contents/Resources/default_app.asar") || path.includes("node_modules/electron/dist/resources/electron.asar") || path.includes("node_modules/electron/dist/resources/default_app.asar") || path.includes("node_modules/babel-polyfill/") || path.includes("node_modules/pirates/")) {
58
111
  return false;
59
112
  }
60
- if (simplePathRegex.test(match)) {
61
- return true;
62
- }
63
- if (pathRegex.test(match)) {
64
- return false;
113
+ return pathFilter ? pathFilter(path) : true;
114
+ }).filter(({ line }) => line.trim() !== "").map(({ line, path, pathIndex }) => {
115
+ if (path === void 0 || !basePathRegex && !pretty) {
116
+ return line;
65
117
  }
66
- return pathFilter ? pathFilter(match) : true;
67
- }).filter((line) => line.trim() !== "").map((line) => {
118
+ let newPath = path;
68
119
  if (basePathRegex) {
69
- line = line.replace(basePathRegex, "");
120
+ newPath = newPath.replaceAll(basePathRegex, "");
70
121
  }
71
122
  if (pretty) {
72
- line = line.replace(extractPathRegex, (m, p1) => {
73
- let filePath = p1;
74
- if (filePath.startsWith("file://")) {
75
- filePath = (0, import_url_extras.fileURLToPath)(filePath);
76
- }
77
- if (homeDirectory) {
78
- filePath = filePath.replace(homeDirectory, "~");
123
+ if (newPath.startsWith("file://") && !new RegExp("[#?]|%0[ad]", "iv").test(newPath)) {
124
+ try {
125
+ newPath = (0, import_url_extras.fileURLToPath)(newPath).replaceAll("\\", "/");
126
+ } catch {
79
127
  }
80
- return m.replace(p1, filePath);
81
- });
128
+ }
129
+ if (homeDirectory && (newPath === homeDirectory || newPath.startsWith(homeDirectory + "/"))) {
130
+ newPath = "~" + newPath.slice(homeDirectory.length);
131
+ }
82
132
  }
83
- return line;
133
+ return line.slice(0, pathIndex) + newPath + line.slice(pathIndex + path.length);
84
134
  }).join("\n");
85
135
  }
package/index.d.ts CHANGED
@@ -5,23 +5,33 @@ export type Options = {
5
5
  - `/Users/sindresorhus/dev/clean-stack/unicorn.js:2:15` → `~/dev/clean-stack/unicorn.js:2:15`
6
6
  - `file:///Users/sindresorhus/dev/clean-stack/unicorn.js:2:15` → `~/dev/clean-stack/unicorn.js:2:15`
7
7
 
8
- When enabled, file URLs are converted to regular paths for better readability and IDE integration.
8
+ When enabled, file URLs are converted to regular paths for better readability and IDE integration. URLs that cannot be converted, contain `?` or `#`, or decode to a line break are left unchanged.
9
+
10
+ `pretty` and `basePath` only rewrite frame paths. Backslashes become `/` throughout each frame. Error message text is preserved except for blank lines; lines shaped like frames are treated as frames.
9
11
 
10
12
  @default false
11
13
  */
12
14
  readonly pretty?: boolean;
13
15
 
14
16
  /**
15
- Remove the given base path from stack trace file paths, effectively turning absolute paths into relative ones.
17
+ Remove the given base path from stack trace file paths, effectively turning absolute paths into relative ones. It will also transform absolute file URLs into relative paths.
18
+
19
+ Supports absolute or relative paths, either separator, and the filesystem root. A leading slash only matches absolute paths. Relative base paths do not match inside `file:///`, except Windows drive paths such as `C:/Users`.
16
20
 
17
- Example with `'/Users/sindresorhus/dev/clean-stack/'` as `basePath`:
21
+ Example with `'/Users/sindresorhus/dev/clean-stack'` as `basePath`:
18
22
 
19
23
  `/Users/sindresorhus/dev/clean-stack/unicorn.js:2:15` → `unicorn.js:2:15`
24
+
25
+ `basePath` matches before file URLs are decoded, so `/Users/John Doe/dev` does not match `file:///Users/John%20Doe/dev`. After a match, remaining URL escapes stay encoded, even with `pretty`.
26
+
27
+ Frames like `at fn (unicorn.js:2:15)` are removed because they resemble old internal Node.js frames. Cleaning again after `basePath` produces this form can therefore drop frames for files directly in the base path. Paths with a directory, like `at fn (src/unicorn.js:2:15)`, and bare frames like `at unicorn.js:2:15` are unaffected.
20
28
  */
21
29
  readonly basePath?: string;
22
30
 
23
31
  /**
24
- Remove the stack lines where the given function returns `false`. The function receives the path part of the stack line.
32
+ Remove stack lines where the callback returns `false`. It receives the path without the closing parenthesis, with `/` separators, before `basePath` or `pretty` is applied. A pathless frame such as `at async Promise.all (index 0)` yields `index 0`.
33
+
34
+ Balanced parentheses in function names, such as `at render (partial) (/base/a.js:6:9)`, and paths, such as `C:/Program Files (x86)/app.js:1:1`, are supported. Unbalanced parentheses in a function name, such as `at a(b (/base/a.js:6:9)`, can cause part of the name to be included in the path.
25
35
 
26
36
  @example
27
37
  ```
@@ -47,8 +57,8 @@ export type Options = {
47
57
  /**
48
58
  Clean up error stack traces. Removes the mostly unhelpful internal Node.js entries.
49
59
 
50
- @param stack - The `stack` property of an `Error`.
51
- @returns The cleaned stack or `undefined` if the given `stack` is `undefined`.
60
+ @param stack - The `stack` property of an `Error`, or `undefined`.
61
+ @returns The cleaned stack or `undefined` if the given `stack` is not a string.
52
62
 
53
63
  @example
54
64
  ```
package/index.js CHANGED
@@ -1,81 +1,194 @@
1
1
  import escapeStringRegexp from '@cjser/escape-string-regexp';
2
2
  import {fileURLToPath} from '@cjser/url-extras';
3
3
 
4
- const extractPathRegex = /\s+at.*[(\s](.*)\)?/;
5
- const pathRegex = /^(?:(?:(?:node|node:[\w/]+|(?:(?:node:)?internal\/[\w/]*|.*node_modules\/(?:babel-polyfill|pirates)\/.*)?\w+)(?:\.js)?:\d+:\d+)|native)/;
6
- const simplePathRegex = /^\w+\.js:\d+:\d+$/;
4
+ // A frame is indented, so a line that starts with `at` in the first column is text.
5
+ const frameStartRegex = /^\s+at\b/v;
6
+ // A `node:` module name is not limited to word characters, as names such as
7
+ // `node:internal/fs/cp/cp-sync` and `node:internal/tls/secure-context` contain a dash. A
8
+ // bare `internal/` path keeps its old shape, so that a project of its own keeps at
9
+ // least the frames whose name holds a dash.
10
+ const pathRegex = /^(?:(?:node|node:[\w\-\/]+|internal\/[\w\/]+(?<!\/)|\w+)(?:\.js)?:\d+:\d+|native$)/v;
11
+ // The only prefixes V8 puts on a frame that has no function name.
12
+ const asyncPrefixRegex = /^(?:async|new) /v;
13
+ // A bare frame has no parentheses to end it, so it is recognised by the location it ends
14
+ // in: `:line:column` for a file, as in `at /a/b.js:1:2`, or `:0x…` for the offset of a
15
+ // WebAssembly function. The colon is part of that, so `/a/0xdeadbeef` is text, and it
16
+ // keeps the scan to the colons of the line.
17
+ const bareFrameEndRegex = /:(?:\d+:\d+|0x[\da-f]+)$/v;
18
+ const simplePathRegex = /^\w+\.js:\d+:\d+$/v;
19
+
20
+ // A stack frame is `at <function name> (<path>)`, or just `at <path>` when there is no
21
+ // function name. A line has to start with `at` for either form, so an error message that
22
+ // merely holds ` at ` is never mistaken for a frame, which is what `frameStartRegex` is
23
+ // for. A frame also has to end the way a frame ends, so that an indented line of a
24
+ // multi-line error message, such as ` at least one plugin is broken`, is text and can
25
+ // never be handed to `pathFilter`.
26
+ //
27
+ // The path is found by index and not by a regex, as a path can hold both a space and a
28
+ // parenthesis, as in `C:/Program Files (x86)/app.js:1:1`, which a regex has to
29
+ // backtrack over. `pathIndex` is where the path starts in the line, and
30
+ // `hasFunctionName` tells a bare simple relative path, which may be the user's own, from
31
+ // the same name inside `at fn (…)`, which is an old internal Node.js frame.
32
+ function getFrame(line) {
33
+ const frameStart = frameStartRegex.exec(line);
34
+ if (!frameStart) {
35
+ return undefined;
36
+ }
37
+
38
+ // Trailing whitespace is part of neither form of the path, and a CRLF line keeps
39
+ // its `\r`.
40
+ const end = line.trimEnd().length;
41
+
42
+ // `at fn (/a/b.js:1:2)` — the path is what sits inside the parenthesis the frame ends
43
+ // with. A function name can hold a balanced parenthesis of its own, as V8 prints a
44
+ // computed key verbatim, and a path can hold one too, as in
45
+ // `C:/Program Files (x86)/app.js:1:1` or `paren)dir/app.js`, so the one to take is the
46
+ // last that is not inside another.
47
+ if (line[end - 1] === ')') {
48
+ let depth = 0;
49
+ let pathStart = -1;
50
+ for (let index = frameStart[0].length; index < end; index++) {
51
+ const character = line[index];
52
+ if (character === '(') {
53
+ if (depth === 0) {
54
+ pathStart = index + 1;
55
+ }
56
+
57
+ depth++;
58
+ } else if (character === ')' && depth > 0) {
59
+ depth--;
60
+ }
61
+ }
62
+
63
+ if (pathStart !== -1) {
64
+ const path = line.slice(pathStart, end - 1);
65
+ return path === '' ? undefined : {path, pathIndex: pathStart, hasFunctionName: true};
66
+ }
67
+ }
68
+
69
+ // `at /a/b.js:1:2`, and the `async` or `new` form of it. Everything after `at` is
70
+ // the path, so that a path holding a space survives. `at native` is the one bare
71
+ // frame without a line and column.
72
+ const rest = line.slice(frameStart[0].length, end).trimStart();
73
+ const path = asyncPrefixRegex.test(rest) ? rest.slice(rest.indexOf(' ') + 1) : rest;
74
+ // The path is the tail of the line, so `pathIndex` holds for the trimmed and the
75
+ // `async`/`new` form too.
76
+ return path === 'native' || bareFrameEndRegex.test(path)
77
+ ? {path, pathIndex: end - path.length, hasFunctionName: false}
78
+ : undefined;
79
+ }
7
80
 
8
81
  export default function cleanStack(stack, {pretty = false, basePath, pathFilter} = {}) {
9
82
  if (typeof stack !== 'string') {
10
83
  return undefined;
11
84
  }
12
85
 
13
- const basePathRegex = basePath && new RegExp(`(file://)?${escapeStringRegexp(basePath.replaceAll('\\', '/'))}/?`, 'g');
86
+ // Windows separators become `/`, and empty segments are dropped so that `/dev`,
87
+ // `//server/dev`, and `/dev/` name the same base path. A base path that started with
88
+ // a slash only ever matches an absolute path, as `dev/a.js` is not below `/dev`.
89
+ const slashedBasePath = basePath?.replaceAll('\\', '/');
90
+ const isAbsoluteBasePath = Boolean(slashedBasePath?.startsWith('/'));
91
+ const isWindowsDrivePath = /^[A-Za-z]:/v.test(slashedBasePath ?? '');
92
+ const normalizedBasePath = slashedBasePath?.split('/').filter(segment => segment !== '').join('/');
93
+
94
+ // The base path must be the *whole* leading part of a path, otherwise
95
+ // `/Users/foo/dev` would truncate `/Users/foo/development` into `elopment`. A file
96
+ // URL carries its own leading slash, so `file:///dev` matches as `/dev` does. A
97
+ // Windows drive path is absolute in practice, and a relative base path is not, so
98
+ // only the former may reach behind `file:///`.
99
+ let basePathPrefix = '';
100
+ if (isAbsoluteBasePath) {
101
+ basePathPrefix = '(?:file:///?|/{1,2})';
102
+ } else if (isWindowsDrivePath) {
103
+ basePathPrefix = '(?:file:///)?';
104
+ }
105
+
106
+ const basePathBoundary = normalizedBasePath === '' ? '' : '(?:/|$)';
107
+ const basePathRegex = basePath && new RegExp(`^${basePathPrefix}${escapeStringRegexp(normalizedBasePath)}${basePathBoundary}`, 'gv');
14
108
 
15
109
  let homeDirectory = '';
16
110
  if (pretty) {
17
- const os = process.getBuiltinModule?.('node:os');
111
+ const os = globalThis.process?.getBuiltinModule?.('node:os');
18
112
  homeDirectory = os ? os.homedir().replaceAll('\\', '/') : '';
113
+ // `HOME` can end with separators, which the comparison below would double.
114
+ while (homeDirectory.length > 1 && homeDirectory.endsWith('/')) {
115
+ homeDirectory = homeDirectory.slice(0, -1);
116
+ }
19
117
  }
20
118
 
21
- return stack.replaceAll('\\', '/')
22
- .split('\n')
23
- .filter(line => {
24
- const pathMatches = line.match(extractPathRegex);
25
- if (!pathMatches?.[1]) {
119
+ const lines = stack.split('\n').map(rawLine => {
120
+ const frame = getFrame(rawLine);
121
+
122
+ // Windows separators become `/`, but only in a stack frame. An error message can
123
+ // hold a path of its own, and that text must survive untouched. The separators
124
+ // do not change the length of the line, so `pathIndex` stays valid.
125
+ return {
126
+ line: frame ? rawLine.replaceAll('\\', '/') : rawLine,
127
+ path: frame?.path.replaceAll('\\', '/'),
128
+ pathIndex: frame?.pathIndex,
129
+ hasFunctionName: frame?.hasFunctionName,
130
+ };
131
+ });
132
+
133
+ return lines
134
+ .filter(({path, hasFunctionName}) => {
135
+ if (path === undefined) {
26
136
  return true; // Keep lines without paths (like error messages)
27
137
  }
28
138
 
29
- const match = pathMatches[1];
30
-
31
- // Filter out Electron internal paths
139
+ // Filter out Electron internal paths, and internal Node.js paths. A bare simple
140
+ // relative path like `foo.js:1:1` matches the Node.js pattern, so it is
141
+ // exempted from that part only. The user's `pathFilter` still applies to it.
142
+ const isInternalPath = pathRegex.test(path) && (hasFunctionName || !simplePathRegex.test(path));
32
143
  if (
33
- match.includes('.app/Contents/Resources/electron.asar')
34
- || match.includes('.app/Contents/Resources/default_app.asar')
35
- || match.includes('node_modules/electron/dist/resources/electron.asar')
36
- || match.includes('node_modules/electron/dist/resources/default_app.asar')
144
+ isInternalPath
145
+ || path.includes('.app/Contents/Resources/electron.asar')
146
+ || path.includes('.app/Contents/Resources/default_app.asar')
147
+ || path.includes('node_modules/electron/dist/resources/electron.asar')
148
+ || path.includes('node_modules/electron/dist/resources/default_app.asar')
149
+ || path.includes('node_modules/babel-polyfill/')
150
+ || path.includes('node_modules/pirates/')
37
151
  ) {
38
152
  return false;
39
153
  }
40
154
 
41
- // Keep simple relative paths like 'foo.js:1:1'
42
- if (simplePathRegex.test(match)) {
43
- return true;
155
+ return pathFilter ? pathFilter(path) : true;
156
+ })
157
+ .filter(({line}) => line.trim() !== '')
158
+ .map(({line, path, pathIndex}) => {
159
+ if (path === undefined || (!basePathRegex && !pretty)) {
160
+ return line;
44
161
  }
45
162
 
46
- // Filter out internal Node.js paths
47
- if (pathRegex.test(match)) {
48
- return false;
49
- }
163
+ // `basePath` and `pretty` only ever apply to the path of a stack frame,
164
+ // never to the error message or the function name.
165
+ let newPath = path;
50
166
 
51
- // Apply user's path filter if provided
52
- return pathFilter ? pathFilter(match) : true;
53
- })
54
- .filter(line => line.trim() !== '')
55
- .map(line => {
56
167
  if (basePathRegex) {
57
- line = line.replace(basePathRegex, '');
168
+ newPath = newPath.replaceAll(basePathRegex, '');
58
169
  }
59
170
 
60
171
  if (pretty) {
61
- line = line.replace(extractPathRegex, (m, p1) => {
62
- let filePath = p1;
63
-
64
- // Convert file:// URLs to regular paths first
65
- if (filePath.startsWith('file://')) {
66
- filePath = fileURLToPath(filePath);
67
- }
68
-
69
- // Then replace home directory with ~ (only if homeDirectory is not empty)
70
- if (homeDirectory) {
71
- filePath = filePath.replace(homeDirectory, '~');
72
- }
172
+ // Convert file:// URLs to regular paths first. A stack can hold anything,
173
+ // so a URL we cannot convert is left as-is instead of throwing. `?` and
174
+ // `#` are left alone too, as they truncate the frame at that character,
175
+ // and so is `%0A`/`%0D`, as decoding it splits the frame into two lines.
176
+ if (newPath.startsWith('file://') && !/[#?]|%0[ad]/iv.test(newPath)) {
177
+ try {
178
+ // The path a file URL stands for is native, and so uses `\` on
179
+ // Windows, while the rest of the stack has already become `/`.
180
+ newPath = fileURLToPath(newPath).replaceAll('\\', '/');
181
+ } catch {}
182
+ }
73
183
 
74
- return m.replace(p1, filePath);
75
- });
184
+ // Then replace home directory with ~, but only when the path really
185
+ // lives in it, so `/Users/sindresorhus-2` is left alone.
186
+ if (homeDirectory && (newPath === homeDirectory || newPath.startsWith(homeDirectory + '/'))) {
187
+ newPath = '~' + newPath.slice(homeDirectory.length);
188
+ }
76
189
  }
77
190
 
78
- return line;
191
+ return line.slice(0, pathIndex) + newPath + line.slice(pathIndex + path.length);
79
192
  })
80
193
  .join('\n');
81
194
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cjser/clean-stack",
3
- "version": "6.0.0-cjser.2",
3
+ "version": "7.0.0-cjser.2",
4
4
  "description": "Clean up error stack traces",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -8,11 +8,7 @@
8
8
  "url": "https://code.moenext.com/3rdeye/cjser.git"
9
9
  },
10
10
  "funding": "https://github.com/sponsors/sindresorhus",
11
- "author": {
12
- "name": "Sindre Sorhus",
13
- "email": "sindresorhus@gmail.com",
14
- "url": "https://sindresorhus.com"
15
- },
11
+ "author": "Sindre Sorhus <sindresorhus@gmail.com> (https://sindresorhus.com)",
16
12
  "type": "module",
17
13
  "exports": {
18
14
  "types": "./index.d.ts",
@@ -21,10 +17,10 @@
21
17
  },
22
18
  "sideEffects": false,
23
19
  "engines": {
24
- "node": ">=20"
20
+ "node": ">=22.3"
25
21
  },
26
22
  "scripts": {
27
- "test": "xo && ava"
23
+ "test": "xo && node --test test.js"
28
24
  },
29
25
  "files": [
30
26
  "index.js",
@@ -44,8 +40,7 @@
44
40
  "@cjser/url-extras": "0.1.0-cjser.2"
45
41
  },
46
42
  "devDependencies": {
47
- "ava": "^6.4.1",
48
- "xo": "^0.56.0"
43
+ "xo": "^5.0.1"
49
44
  },
50
45
  "xo": {
51
46
  "rules": {
@@ -55,18 +50,18 @@
55
50
  "types": "./index.d.ts",
56
51
  "main": "./dist-cjser/index.cjs",
57
52
  "cjser": {
58
- "sourceVersion": "6.0.0",
53
+ "sourceVersion": "7.0.0",
59
54
  "cjserVersion": 2,
60
55
  "original": {
61
56
  "name": "clean-stack",
62
- "version": "6.0.0",
57
+ "version": "7.0.0",
63
58
  "exports": {
64
59
  "types": "./index.d.ts",
65
60
  "default": "./index.js"
66
61
  },
67
62
  "repository": "sindresorhus/clean-stack",
68
63
  "dependencies": {
69
- "escape-string-regexp": "5.0.0",
64
+ "escape-string-regexp": "^5.0.0",
70
65
  "url-extras": "^0.1.0"
71
66
  },
72
67
  "files": [
@@ -74,7 +69,7 @@
74
69
  "index.d.ts"
75
70
  ],
76
71
  "scripts": {
77
- "test": "xo && ava"
72
+ "test": "xo && node --test test.js"
78
73
  }
79
74
  }
80
75
  }
package/readme.md CHANGED
@@ -42,13 +42,13 @@ Error: Missing unicorn
42
42
 
43
43
  ### cleanStack(stack, options?)
44
44
 
45
- Returns the cleaned stack or `undefined` if the given `stack` is `undefined`.
45
+ Returns the cleaned stack or `undefined` if the given `stack` is not a string.
46
46
 
47
47
  #### stack
48
48
 
49
49
  Type: `string | undefined`
50
50
 
51
- The `stack` property of an [`Error`](https://github.com/microsoft/TypeScript/blob/eac073894b172ec719ca7f28b0b94fc6e6e7d4cf/lib/lib.es5.d.ts#L972-L976).
51
+ The `stack` property of an [`Error`](https://github.com/microsoft/TypeScript/blob/eac073894b172ec719ca7f28b0b94fc6e6e7d4cf/lib/lib.es5.d.ts#L972-L976), or `undefined`.
52
52
 
53
53
  #### options
54
54
 
@@ -64,7 +64,9 @@ Prettify the file paths in the stack:
64
64
  - `/Users/sindresorhus/dev/clean-stack/unicorn.js:2:15` → `~/dev/clean-stack/unicorn.js:2:15`
65
65
  - `file:///Users/sindresorhus/dev/clean-stack/unicorn.js:2:15` → `~/dev/clean-stack/unicorn.js:2:15`
66
66
 
67
- When enabled, file URLs are converted to regular paths for better readability and IDE integration.
67
+ When enabled, file URLs are converted to regular paths for better readability and IDE integration. URLs that cannot be converted, contain `?` or `#`, or decode to a line break are left unchanged.
68
+
69
+ `pretty` and `basePath` only rewrite frame paths. Backslashes become `/` throughout each frame. Error message text is preserved except for blank lines; lines shaped like frames are treated as frames.
68
70
 
69
71
  ##### basePath
70
72
 
@@ -72,15 +74,26 @@ Type: `string?`
72
74
 
73
75
  Remove the given base path from stack trace file paths, effectively turning absolute paths into relative ones. It will also transform absolute file URLs into relative paths.
74
76
 
77
+ Supports absolute or relative paths, either separator, and the filesystem root. A leading slash only matches absolute paths. Relative base paths do not match inside `file:///`, except Windows drive paths such as `C:/Users`.
78
+
75
79
  Example with `'/Users/sindresorhus/dev/clean-stack'` as `basePath`:
76
80
 
77
81
  `/Users/sindresorhus/dev/clean-stack/unicorn.js:2:15` → `unicorn.js:2:15`
78
82
 
83
+ > [!NOTE]
84
+ > `basePath` matches before file URLs are decoded, so `/Users/John Doe/dev` does not match `file:///Users/John%20Doe/dev`. After a match, remaining URL escapes stay encoded, even with `pretty`.
85
+
86
+ > [!NOTE]
87
+ > Frames like `at fn (unicorn.js:2:15)` are removed because they resemble old internal Node.js frames. Cleaning again after `basePath` produces this form can therefore drop frames for files directly in the base path. Paths with a directory, like `at fn (src/unicorn.js:2:15)`, and bare frames like `at unicorn.js:2:15` are unaffected.
88
+
79
89
  ##### pathFilter
80
90
 
81
91
  Type: `(path: string) => boolean`
82
92
 
83
- Remove the stack lines where the given function returns `false`. The function receives the path part of the stack line.
93
+ Remove stack lines where the callback returns `false`. It receives the path without the closing parenthesis, with `/` separators, before `basePath` or `pretty` is applied. A pathless frame such as `at async Promise.all (index 0)` yields `index 0`.
94
+
95
+ > [!NOTE]
96
+ > Balanced parentheses in function names, such as `at render (partial) (/base/a.js:6:9)`, and paths, such as `C:/Program Files (x86)/app.js:1:1`, are supported. Unbalanced parentheses in a function name, such as `at a(b (/base/a.js:6:9)`, can cause part of the name to be included in the path.
84
97
 
85
98
  ```js
86
99
  import cleanStack from 'clean-stack';