peekinside 0.0.0-stage → 1.2.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.
Files changed (83) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +45 -2
  3. package/bin/peekinside.js +8 -0
  4. package/index.d.ts +389 -0
  5. package/index.js +3 -0
  6. package/package.json +77 -4
  7. package/src/PeekInside.js +731 -0
  8. package/src/capture/body-capture.js +86 -0
  9. package/src/capture/body-decoder.js +188 -0
  10. package/src/capture/exchange-recorder.js +263 -0
  11. package/src/capture/http-message.js +138 -0
  12. package/src/capture/ignored-paths.js +49 -0
  13. package/src/capture/request-body-tap.js +40 -0
  14. package/src/capture/response-tap.js +83 -0
  15. package/src/cli.js +254 -0
  16. package/src/config.js +183 -0
  17. package/src/crypto.js +189 -0
  18. package/src/deployment.js +183 -0
  19. package/src/exceptions/exception-builder.js +339 -0
  20. package/src/exceptions/exception-capture.js +407 -0
  21. package/src/exceptions/exception-record.js +118 -0
  22. package/src/exceptions/fingerprint.js +60 -0
  23. package/src/exceptions/frame-classifier.js +114 -0
  24. package/src/exceptions/limits.js +95 -0
  25. package/src/exceptions/process-monitor.js +192 -0
  26. package/src/exceptions/rate-limiter.js +50 -0
  27. package/src/exceptions/request-context.js +337 -0
  28. package/src/exceptions/safe-json.js +186 -0
  29. package/src/exceptions/source-context.js +144 -0
  30. package/src/exceptions/source-maps.js +313 -0
  31. package/src/exceptions/stack-parser.js +179 -0
  32. package/src/hub/brand/peekinside-span-core-mark-dark.svg +7 -0
  33. package/src/hub/brand/peekinside-span-core-mark-light.svg +7 -0
  34. package/src/hub/client.js +184 -0
  35. package/src/hub/entry.js +4 -0
  36. package/src/hub/exception-state.js +97 -0
  37. package/src/hub/ingest-payload.js +362 -0
  38. package/src/hub/process.js +175 -0
  39. package/src/hub/project-log.js +559 -0
  40. package/src/hub/segment-log.js +282 -0
  41. package/src/hub/server.js +538 -0
  42. package/src/hub/spool-ingester.js +110 -0
  43. package/src/hub/store.js +404 -0
  44. package/src/instrumentation/console.js +83 -0
  45. package/src/instrumentation/http.js +146 -0
  46. package/src/instrumentation/index.js +234 -0
  47. package/src/instrumentation/jobs.js +186 -0
  48. package/src/instrumentation/loggers.js +135 -0
  49. package/src/instrumentation/mongodb.js +221 -0
  50. package/src/instrumentation/mysql2.js +161 -0
  51. package/src/instrumentation/operation.js +70 -0
  52. package/src/instrumentation/patch.js +72 -0
  53. package/src/instrumentation/pg.js +218 -0
  54. package/src/instrumentation/prisma.js +181 -0
  55. package/src/integrations/express-errors.js +135 -0
  56. package/src/integrations/express.js +57 -0
  57. package/src/integrations/fastify.js +72 -0
  58. package/src/integrations/framework-adapter.js +53 -0
  59. package/src/integrations/http-server.js +32 -0
  60. package/src/integrations/koa.js +72 -0
  61. package/src/integrations/server-binder.js +120 -0
  62. package/src/jobs/job-context.js +40 -0
  63. package/src/jobs/job-run-record.js +104 -0
  64. package/src/jobs/job-runner.js +173 -0
  65. package/src/logs/log-capture.js +143 -0
  66. package/src/logs/log-record.js +74 -0
  67. package/src/project.js +76 -0
  68. package/src/record.js +181 -0
  69. package/src/release.js +157 -0
  70. package/src/runtime/loop-monitor.js +162 -0
  71. package/src/runtime/sampler.js +93 -0
  72. package/src/spool.js +195 -0
  73. package/src/timeline/caller.js +129 -0
  74. package/src/timeline/execution-hash.js +110 -0
  75. package/src/timeline/lru.js +50 -0
  76. package/src/timeline/masking.js +266 -0
  77. package/src/timeline/shape.js +77 -0
  78. package/src/timeline/sql.js +396 -0
  79. package/src/timeline/timeline.js +642 -0
  80. package/src/transport/batch.js +324 -0
  81. package/src/transport/cloud.js +363 -0
  82. package/src/transport/local.js +354 -0
  83. package/src/user.js +83 -0
@@ -0,0 +1,186 @@
1
+ /**
2
+ * JSON for values the app hands over without knowing their size or shape: report()'s `extra`, and
3
+ * thrown values that aren't errors.
4
+ *
5
+ * JSON.stringify would throw on cycles and BigInts, run getters that throw, and serialize a huge
6
+ * object in full before anything could be cut. This writer stops once it has produced enough
7
+ * text, marks cycles, and skips what can't be read, so it never throws and never does more work
8
+ * than the limit allows.
9
+ */
10
+
11
+ const MAX_DEPTH = 20;
12
+
13
+ /**
14
+ * Bounded, never-throwing JSON serialization.
15
+ */
16
+ export class SafeJson {
17
+ /**
18
+ * Serializes a value, stopping after `maxLength` characters.
19
+ *
20
+ * @param {*} value
21
+ * @param {number} maxLength - Characters produced at most (the result can be cut mid-value)
22
+ * @returns {{ json: string|undefined, truncated: boolean }} `json` is undefined for values JSON
23
+ * has no form for (undefined, functions, symbols); when `truncated`, it is the cut text, not
24
+ * valid JSON
25
+ */
26
+ static stringify(value, maxLength) {
27
+ try {
28
+ const writer = new Writer(maxLength);
29
+ const written = writer.value(value, []);
30
+ if (!written) return { json: undefined, truncated: false };
31
+ return { json: writer.text(), truncated: writer.full };
32
+ } catch (error) {
33
+ // Every read is guarded already; this is the last line of defense.
34
+ return { json: '"[unserializable]"', truncated: false };
35
+ }
36
+ }
37
+ }
38
+
39
+ /**
40
+ * Writes JSON text into a bounded buffer. Once full, everything else is skipped, so the work
41
+ * stops where the output does.
42
+ */
43
+ class Writer {
44
+ /**
45
+ * @param {number} maxLength - Characters kept at most
46
+ */
47
+ constructor(maxLength) {
48
+ this.maxLength = maxLength;
49
+ this.parts = [];
50
+ this.length = 0;
51
+ this.full = false;
52
+ }
53
+
54
+ text() {
55
+ return this.parts.join('').slice(0, this.maxLength);
56
+ }
57
+
58
+ push(text) {
59
+ if (this.full) return;
60
+ this.parts.push(text);
61
+ this.length += text.length;
62
+ if (this.length > this.maxLength) this.full = true;
63
+ }
64
+
65
+ // Writes one value; returns false when it has no JSON form (and nothing was written).
66
+ value(value, ancestors) {
67
+ if (this.full) return true;
68
+ let current = value;
69
+
70
+ if (current !== null && typeof current === 'object' && typeof safeGet(current, 'toJSON') === 'function') {
71
+ try {
72
+ current = current.toJSON();
73
+ } catch (error) {
74
+ current = String(safeGet(current, 'message') ?? '[unserializable]');
75
+ }
76
+ }
77
+
78
+ switch (typeof current) {
79
+ case 'string':
80
+ this.push(JSON.stringify(current));
81
+ return true;
82
+ case 'number':
83
+ this.push(Number.isFinite(current) ? String(current) : 'null');
84
+ return true;
85
+ case 'boolean':
86
+ this.push(current ? 'true' : 'false');
87
+ return true;
88
+ case 'bigint':
89
+ this.push(JSON.stringify(current.toString()));
90
+ return true;
91
+ case 'undefined':
92
+ case 'function':
93
+ case 'symbol':
94
+ return false;
95
+ default:
96
+ break;
97
+ }
98
+
99
+ if (current === null) {
100
+ this.push('null');
101
+ return true;
102
+ }
103
+ if (ancestors.includes(current)) {
104
+ this.push('"[Circular]"');
105
+ return true;
106
+ }
107
+ if (ancestors.length >= MAX_DEPTH) {
108
+ this.push('"[Too deep]"');
109
+ return true;
110
+ }
111
+
112
+ ancestors.push(current);
113
+ try {
114
+ if (Array.isArray(current)) this.array(current, ancestors);
115
+ else this.object(current, ancestors);
116
+ } finally {
117
+ ancestors.pop();
118
+ }
119
+ return true;
120
+ }
121
+
122
+ array(items, ancestors) {
123
+ this.push('[');
124
+ for (let i = 0; i < items.length && !this.full; i++) {
125
+ if (i > 0) this.push(',');
126
+ let item;
127
+ try {
128
+ item = items[i];
129
+ } catch (error) {
130
+ item = undefined;
131
+ }
132
+ if (!this.value(item, ancestors)) this.push('null');
133
+ }
134
+ this.push(']');
135
+ }
136
+
137
+ object(source, ancestors) {
138
+ // Errors keep their name, message and stack, which JSON.stringify would drop (they aren't
139
+ // enumerable).
140
+ const isError = isNativeError(source);
141
+ let keys;
142
+ try {
143
+ keys = Object.keys(source);
144
+ } catch (error) {
145
+ keys = [];
146
+ }
147
+ if (isError) keys = ['name', 'message', 'stack', ...keys.filter((key) => key !== 'name' && key !== 'message' && key !== 'stack')];
148
+
149
+ this.push('{');
150
+ let first = true;
151
+ for (const key of keys) {
152
+ if (this.full) break;
153
+ const item = safeGet(source, key);
154
+ const mark = this.parts.length;
155
+ const length = this.length;
156
+ this.push(`${first ? '' : ','}${JSON.stringify(key)}:`);
157
+ if (this.value(item, ancestors)) {
158
+ first = false;
159
+ } else {
160
+ // No JSON form: drop the key again, as JSON.stringify does.
161
+ this.parts.length = mark;
162
+ this.length = length;
163
+ this.full = this.length > this.maxLength;
164
+ }
165
+ }
166
+ this.push('}');
167
+ }
168
+ }
169
+
170
+ // instanceof runs the getPrototypeOf trap of a proxy, which may throw.
171
+ function isNativeError(value) {
172
+ try {
173
+ return value instanceof Error;
174
+ } catch (error) {
175
+ return false;
176
+ }
177
+ }
178
+
179
+ // Reads a property, treating getters that throw (and revoked proxies) as missing.
180
+ function safeGet(target, key) {
181
+ try {
182
+ return target[key];
183
+ } catch (error) {
184
+ return undefined;
185
+ }
186
+ }
@@ -0,0 +1,144 @@
1
+ /**
2
+ * The source lines around in-app frames, read from disk when an exception is recorded.
3
+ *
4
+ * Reads are synchronous: exceptions are also recorded while the process is crashing, when nothing
5
+ * asynchronous runs anymore. They stay cheap and safe: only source files of the app itself (inside
6
+ * the project folder, outside node_modules, after resolving symlinks), at most 1 MB each, and a
7
+ * few recently read files are kept in memory.
8
+ */
9
+
10
+ import fs from 'fs';
11
+ import path from 'path';
12
+ import { LIMITS, truncate } from './limits.js';
13
+ import { contextFrom } from './source-maps.js';
14
+
15
+ const SOURCE_FILE = /\.(?:js|mjs|cjs|ts|mts|cts|jsx|tsx)$/i;
16
+ const MAX_FILE_SIZE = 1024 * 1024;
17
+ const MAX_CACHED_FILES = 20;
18
+
19
+ /**
20
+ * Reads source lines of the app's files, with a small LRU cache.
21
+ */
22
+ export class SourceContext {
23
+ /**
24
+ * @param {string} root - The project folder; nothing outside it is read
25
+ * @param {Object} [options]
26
+ * @param {number} [options.maxFiles=20] - Files kept in memory
27
+ * @param {number} [options.maxFileSize=1048576] - Larger files are never read
28
+ */
29
+ constructor(root, { maxFiles = MAX_CACHED_FILES, maxFileSize = MAX_FILE_SIZE } = {}) {
30
+ this.root = path.resolve(root || process.cwd());
31
+ this.maxFiles = maxFiles;
32
+ this.maxFileSize = maxFileSize;
33
+ // file -> { mtimeMs, size, lines (null when the file can't be shown) }, least recent first.
34
+ this._cache = new Map();
35
+ this._realRoot = null;
36
+ }
37
+
38
+ /**
39
+ * Adds source lines to the first in-app frames.
40
+ *
41
+ * @param {import('./frame-classifier.js').Frame[]} frames - Changed in place
42
+ * @param {number} [count=5] - In-app frames that get lines
43
+ * @param {(frame: Object) => string[]|null} [linesOf] - A frame's lines when they are already
44
+ * known (a source map that carries its sources); others are read from disk
45
+ * @returns {void}
46
+ */
47
+ addTo(frames, count = LIMITS.contextFrames, linesOf = null) {
48
+ let remaining = count;
49
+ for (const frame of frames) {
50
+ if (remaining === 0) break;
51
+ if (!frame.inApp) continue;
52
+ remaining--;
53
+ try {
54
+ const known = linesOf ? linesOf(frame) : null;
55
+ frame.context = known ? contextFrom(known, frame.line) : this.around(frame.file, frame.line);
56
+ } catch (error) {
57
+ frame.context = null;
58
+ }
59
+ }
60
+ }
61
+
62
+ /**
63
+ * The lines around one line of a file.
64
+ *
65
+ * @param {string|null} file - Absolute path
66
+ * @param {number|null} line - 1-based
67
+ * @param {number} [radius=5] - Lines on each side
68
+ * @returns {{ start: number, lines: string[] }|null} `start` is the number of the first line;
69
+ * null when the file can't be shown
70
+ */
71
+ around(file, line, radius = LIMITS.contextLines) {
72
+ if (!file || !Number.isInteger(line) || line < 1 || !SOURCE_FILE.test(file)) return null;
73
+ const lines = this._lines(file);
74
+ if (!lines || line > lines.length) return null;
75
+
76
+ const start = Math.max(1, line - radius);
77
+ const end = Math.min(lines.length, line + radius);
78
+ return {
79
+ start,
80
+ lines: lines.slice(start - 1, end).map((text) => truncate(text, LIMITS.contextLineLength))
81
+ };
82
+ }
83
+
84
+ // The file's lines, from the cache while the file is unchanged. Checked on every call, so an
85
+ // edited file (a dev server that doesn't restart) shows its current lines.
86
+ _lines(file) {
87
+ let stat;
88
+ try {
89
+ stat = fs.statSync(file);
90
+ } catch (error) {
91
+ return null;
92
+ }
93
+
94
+ const cached = this._cache.get(file);
95
+ if (cached && cached.mtimeMs === stat.mtimeMs && cached.size === stat.size) {
96
+ this._cache.delete(file);
97
+ this._cache.set(file, cached);
98
+ return cached.lines;
99
+ }
100
+
101
+ let lines = null;
102
+ if (stat.isFile() && stat.size <= this.maxFileSize && this._isAppFile(file)) {
103
+ try {
104
+ lines = fs.readFileSync(file, 'utf8').split(/\r?\n/);
105
+ } catch (error) {
106
+ lines = null;
107
+ }
108
+ }
109
+
110
+ this._cache.delete(file);
111
+ this._cache.set(file, { mtimeMs: stat.mtimeMs, size: stat.size, lines });
112
+ while (this._cache.size > this.maxFiles) {
113
+ this._cache.delete(this._cache.keys().next().value);
114
+ }
115
+ return lines;
116
+ }
117
+
118
+ // Inside the project folder and not under node_modules, once symlinks are resolved: a link in
119
+ // the project must not expose a file elsewhere on the machine.
120
+ _isAppFile(file) {
121
+ let real;
122
+ try {
123
+ real = fs.realpathSync(file);
124
+ } catch (error) {
125
+ return false;
126
+ }
127
+ const relative = path.relative(this._root(), real);
128
+ if (relative === '' || relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
129
+ return false;
130
+ }
131
+ return !relative.split(path.sep).includes('node_modules');
132
+ }
133
+
134
+ _root() {
135
+ if (this._realRoot === null) {
136
+ try {
137
+ this._realRoot = fs.realpathSync(this.root);
138
+ } catch (error) {
139
+ this._realRoot = this.root;
140
+ }
141
+ }
142
+ return this._realRoot;
143
+ }
144
+ }
@@ -0,0 +1,313 @@
1
+ /**
2
+ * Maps the stack frames of compiled code (TypeScript output, bundles) back to the files people
3
+ * edit, with the source maps their build wrote: a `.map` file named by a sourceMappingURL comment,
4
+ * a map inlined in that comment, or `<file>.js.map` next to the file ("hidden" source maps).
5
+ *
6
+ * Like SourceContext, reads are synchronous (crashes are recorded too) and bounded: only the app's
7
+ * own compiled files are mapped (inside the project folder, outside node_modules), maps are read
8
+ * from the project folder only, at most 16 MB each, and a few are kept in memory. Node's own
9
+ * parser (module.SourceMap) reads them. Apps that run with --enable-source-maps, tsx or ts-node
10
+ * already have mapped stacks: their frames point at .ts files and are left alone.
11
+ */
12
+
13
+ import fs from 'fs';
14
+ import { SourceMap } from 'module';
15
+ import path from 'path';
16
+ import { fileURLToPath } from 'url';
17
+ import { LIMITS, truncate } from './limits.js';
18
+
19
+ const GENERATED_FILE = /\.(?:js|mjs|cjs)$/i;
20
+ // The end of a compiled file holds its sourceMappingURL comment; an inline map can be large.
21
+ const MAX_TAIL = 8 * 1024 * 1024;
22
+ const MAX_MAP_SIZE = 16 * 1024 * 1024;
23
+ const MAX_CACHED_FILES = 8;
24
+ const COMMENT = /\/\/[#@][ \t]*sourceMappingURL=([^\s'"]+)[ \t]*$/;
25
+ const DATA_URL = /^data:application\/json(?:;charset=[\w-]+)?(;base64)?,(.*)$/i;
26
+ // Bundler URLs for sources: webpack://app/./src/x.ts, turbopack:///[project]/src/x.ts
27
+ const BUNDLER_URL = /^[a-z][a-z0-9+.-]*:\/\/[^/]*\/(?:\[project\]\/)?/i;
28
+
29
+ /**
30
+ * @typedef {Object} LoadedMap
31
+ * @property {SourceMap} map
32
+ * @property {string} dir - Where relative sources are resolved from
33
+ * @property {string} sourceRoot
34
+ * @property {Map<string, number>} indexOf - Source as written in the map -> its index
35
+ * @property {Array<string|null>} contents - sourcesContent
36
+ * @property {Map<number, string[]>} lines - Split contents, by source index
37
+ */
38
+
39
+ /**
40
+ * Applies source maps to frames, with a small LRU cache of parsed maps.
41
+ */
42
+ export class SourceMaps {
43
+ /**
44
+ * @param {import('./frame-classifier.js').FrameClassifier} classifier - Classifies mapped frames
45
+ * @param {Object} [options]
46
+ * @param {number} [options.maxFiles=8] - Compiled files whose maps are kept in memory
47
+ * @param {number} [options.maxMapSize=16777216] - Larger maps are never read
48
+ */
49
+ constructor(classifier, { maxFiles = MAX_CACHED_FILES, maxMapSize = MAX_MAP_SIZE } = {}) {
50
+ this.classifier = classifier;
51
+ this.maxFiles = maxFiles;
52
+ this.maxMapSize = maxMapSize;
53
+ // compiled file -> { mtimeMs, size, loaded (null without a usable map) }, least recent first
54
+ this._cache = new Map();
55
+ // Mapped frame -> the lines of its original file, when the map carries them (sourcesContent)
56
+ this._sourceLines = new WeakMap();
57
+ this._realRoot = null;
58
+ }
59
+
60
+ /**
61
+ * Maps frames of the app's compiled files to their sources, in place. A mapped frame keeps
62
+ * where it ran in `generated` and is classified again (a bundle can hold node_modules code).
63
+ *
64
+ * @param {import('./frame-classifier.js').Frame[]} frames
65
+ * @returns {import('./frame-classifier.js').Frame[]} The same array
66
+ */
67
+ apply(frames) {
68
+ for (let index = 0; index < frames.length; index++) {
69
+ const frame = frames[index];
70
+ if (!frame.inApp || !frame.file || !GENERATED_FILE.test(frame.file) || !Number.isInteger(frame.line)) continue;
71
+ let mapped = null;
72
+ try {
73
+ mapped = this._map(frame);
74
+ } catch (error) {
75
+ mapped = null;
76
+ }
77
+ if (mapped) frames[index] = mapped;
78
+ }
79
+ return frames;
80
+ }
81
+
82
+ /**
83
+ * The lines of a mapped frame's original file, when its map carried them.
84
+ *
85
+ * @param {import('./frame-classifier.js').Frame} frame
86
+ * @returns {string[]|null}
87
+ */
88
+ linesOf(frame) {
89
+ return this._sourceLines.get(frame) || null;
90
+ }
91
+
92
+ _map(frame) {
93
+ const loaded = this._load(frame.file);
94
+ if (!loaded) return null;
95
+ const line = frame.line - 1;
96
+ const column = Math.max(0, (frame.column || 1) - 1);
97
+ let entry = loaded.map.findEntry(line, column);
98
+ // A column before the line's first mapping finds an earlier line: take the line's own.
99
+ if (entry && entry.generatedLine !== line) entry = loaded.map.findEntry(line, Number.MAX_SAFE_INTEGER);
100
+ if (!entry || entry.generatedLine !== line || typeof entry.originalSource !== 'string' || !Number.isInteger(entry.originalLine)) {
101
+ return null;
102
+ }
103
+
104
+ const file = resolveSource(entry.originalSource, loaded, this.classifier.root);
105
+ if (!file) return null;
106
+ const mapped = this.classifier.classify({
107
+ function: frame.function,
108
+ file,
109
+ line: entry.originalLine + 1,
110
+ column: Number.isInteger(entry.originalColumn) ? entry.originalColumn + 1 : null,
111
+ native: false,
112
+ async: frame.async
113
+ });
114
+ // Sources outside the project folder (absolute paths of the build machine) are still the
115
+ // app's own code, unless they come from node_modules.
116
+ if (!mapped.relativeFile && !mapped.module && !file.startsWith('node:')) {
117
+ mapped.inApp = true;
118
+ mapped.relativeFile = looseRelative(entry.originalSource);
119
+ }
120
+ mapped.generated = { file: frame.relativeFile || frame.file, line: frame.line, column: frame.column };
121
+
122
+ const index = loaded.indexOf.get(entry.originalSource);
123
+ const content = index === undefined ? null : loaded.contents[index];
124
+ if (typeof content === 'string') {
125
+ let lines = loaded.lines.get(index);
126
+ if (!lines) {
127
+ lines = content.split(/\r?\n/);
128
+ loaded.lines.set(index, lines);
129
+ }
130
+ this._sourceLines.set(mapped, lines);
131
+ }
132
+ return mapped;
133
+ }
134
+
135
+ // The map of a compiled file, from the cache while the file is unchanged.
136
+ _load(file) {
137
+ let stat;
138
+ try {
139
+ stat = fs.statSync(file);
140
+ } catch (error) {
141
+ return null;
142
+ }
143
+ const cached = this._cache.get(file);
144
+ if (cached && cached.mtimeMs === stat.mtimeMs && cached.size === stat.size) {
145
+ this._cache.delete(file);
146
+ this._cache.set(file, cached);
147
+ return cached.loaded;
148
+ }
149
+
150
+ let loaded = null;
151
+ try {
152
+ loaded = stat.isFile() && this._inProject(file) ? this._read(file, stat.size) : null;
153
+ } catch (error) {
154
+ loaded = null;
155
+ }
156
+ this._cache.delete(file);
157
+ this._cache.set(file, { mtimeMs: stat.mtimeMs, size: stat.size, loaded });
158
+ while (this._cache.size > this.maxFiles) this._cache.delete(this._cache.keys().next().value);
159
+ return loaded;
160
+ }
161
+
162
+ _read(file, size) {
163
+ const url = SourceMaps.mappingUrl(readTail(file, size, MAX_TAIL));
164
+ let text = null;
165
+ let dir = path.dirname(file);
166
+ if (url && DATA_URL.test(url)) {
167
+ const [, base64, data] = DATA_URL.exec(url);
168
+ if (data.length > this.maxMapSize * 1.4) return null;
169
+ text = base64 ? Buffer.from(data, 'base64').toString('utf8') : decodeURIComponent(data);
170
+ } else {
171
+ const mapFile = url ? mapPath(url, dir) : `${file}.map`;
172
+ if (!mapFile || !this._inProject(mapFile)) return null;
173
+ text = readSmallFile(mapFile, this.maxMapSize);
174
+ dir = path.dirname(mapFile);
175
+ }
176
+ if (!text) return null;
177
+
178
+ const payload = JSON.parse(text);
179
+ if (!payload || typeof payload !== 'object' || (!Array.isArray(payload.sources) && !Array.isArray(payload.sections))) return null;
180
+ const map = new SourceMap(payload);
181
+ const sources = Array.isArray(payload.sources) ? payload.sources : [];
182
+ const indexOf = new Map();
183
+ sources.forEach((source, index) => {
184
+ if (typeof source === 'string' && !indexOf.has(source)) indexOf.set(source, index);
185
+ });
186
+ return {
187
+ map,
188
+ dir,
189
+ sourceRoot: typeof payload.sourceRoot === 'string' ? payload.sourceRoot : '',
190
+ indexOf,
191
+ contents: Array.isArray(payload.sourcesContent) ? payload.sourcesContent : [],
192
+ lines: new Map()
193
+ };
194
+ }
195
+
196
+ /**
197
+ * The URL in a compiled file's last sourceMappingURL comment.
198
+ *
199
+ * @param {string} text - The end of the file
200
+ * @returns {string|null}
201
+ */
202
+ static mappingUrl(text) {
203
+ if (!text) return null;
204
+ const at = text.lastIndexOf('sourceMappingURL=');
205
+ if (at === -1) return null;
206
+ const lineStart = text.lastIndexOf('\n', at) + 1;
207
+ let lineEnd = text.indexOf('\n', at);
208
+ if (lineEnd === -1) lineEnd = text.length;
209
+ const match = COMMENT.exec(text.slice(lineStart, lineEnd).replace(/\r$/, '').trim());
210
+ return match ? match[1] : null;
211
+ }
212
+
213
+ // Inside the project folder and outside node_modules, once symlinks are resolved.
214
+ _inProject(file) {
215
+ let real;
216
+ try {
217
+ real = fs.realpathSync(file);
218
+ } catch (error) {
219
+ return false;
220
+ }
221
+ if (this._realRoot === null) {
222
+ try {
223
+ this._realRoot = fs.realpathSync(this.classifier.root);
224
+ } catch (error) {
225
+ this._realRoot = this.classifier.root;
226
+ }
227
+ }
228
+ const relative = path.relative(this._realRoot, real);
229
+ if (relative === '' || relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) return false;
230
+ return !relative.split(path.sep).includes('node_modules');
231
+ }
232
+ }
233
+
234
+ /**
235
+ * Source lines around a line, as SourceContext reads them from disk.
236
+ *
237
+ * @param {string[]} lines
238
+ * @param {number|null} line - 1-based
239
+ * @returns {{ start: number, lines: string[] }|null}
240
+ */
241
+ export function contextFrom(lines, line) {
242
+ if (!Number.isInteger(line) || line < 1 || line > lines.length) return null;
243
+ const start = Math.max(1, line - LIMITS.contextLines);
244
+ const end = Math.min(lines.length, line + LIMITS.contextLines);
245
+ return { start, lines: lines.slice(start - 1, end).map((text) => truncate(text, LIMITS.contextLineLength)) };
246
+ }
247
+
248
+ // Where a source named in a map is: bundler URLs from the project folder, file URLs and absolute
249
+ // paths as they are, the rest next to the map (after its sourceRoot).
250
+ function resolveSource(source, loaded, root) {
251
+ if (!source) return null;
252
+ if (source.startsWith('file://')) {
253
+ try {
254
+ return fileURLToPath(source);
255
+ } catch (error) {
256
+ return null;
257
+ }
258
+ }
259
+ if (source.startsWith('node:')) return source;
260
+ if (BUNDLER_URL.test(source)) return path.resolve(root, source.replace(BUNDLER_URL, ''));
261
+ if (path.isAbsolute(source)) return path.normalize(source);
262
+ const sourceRoot = loaded.sourceRoot;
263
+ if (sourceRoot.startsWith('file://')) {
264
+ try {
265
+ return path.resolve(fileURLToPath(sourceRoot), source);
266
+ } catch (error) {
267
+ return null;
268
+ }
269
+ }
270
+ if (BUNDLER_URL.test(sourceRoot)) return path.resolve(root, sourceRoot.replace(BUNDLER_URL, ''), source);
271
+ return path.resolve(loaded.dir, sourceRoot, source);
272
+ }
273
+
274
+ // A stable name for a source outside the project folder: its path without leading "../" or "/".
275
+ function looseRelative(source) {
276
+ return source
277
+ .replace(BUNDLER_URL, '')
278
+ .replace(/^file:\/\//, '')
279
+ .split(/[\\/]+/)
280
+ .filter((part) => part && part !== '.' && part !== '..')
281
+ .join('/') || null;
282
+ }
283
+
284
+ function mapPath(url, dir) {
285
+ if (/^[a-z][a-z0-9+.-]*:/i.test(url) && !url.startsWith('file:')) return null; // http:, data: of other types
286
+ try {
287
+ return url.startsWith('file:') ? fileURLToPath(url) : path.resolve(dir, decodeURIComponent(url));
288
+ } catch (error) {
289
+ return null;
290
+ }
291
+ }
292
+
293
+ function readTail(file, size, max) {
294
+ const length = Math.min(size, max);
295
+ const fd = fs.openSync(file, 'r');
296
+ try {
297
+ const buffer = Buffer.alloc(length);
298
+ const read = fs.readSync(fd, buffer, 0, length, size - length);
299
+ return buffer.subarray(0, read).toString('utf8');
300
+ } finally {
301
+ fs.closeSync(fd);
302
+ }
303
+ }
304
+
305
+ function readSmallFile(file, max) {
306
+ try {
307
+ const stat = fs.statSync(file);
308
+ if (!stat.isFile() || stat.size > max) return null;
309
+ return fs.readFileSync(file, 'utf8');
310
+ } catch (error) {
311
+ return null;
312
+ }
313
+ }