@usebruno/js 0.46.1 → 0.48.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/package.json +9 -7
- package/src/bru.js +173 -75
- package/src/bruno-request.js +15 -7
- package/src/bruno-response.js +4 -0
- package/src/cookie-list.js +272 -0
- package/src/header-list.js +497 -0
- package/src/index.js +27 -2
- package/src/property-list.js +184 -0
- package/src/readonly-property-list.js +227 -0
- package/src/runtime/assert-runtime.js +164 -10
- package/src/runtime/script-runtime.js +55 -6
- package/src/runtime/test-runtime.js +19 -1
- package/src/runtime/vars-runtime.js +20 -3
- package/src/sandbox/bundle-browser-rollup.js +72 -66
- package/src/sandbox/bundle-libraries.js +10 -2
- package/src/sandbox/quickjs/index.js +8 -41
- package/src/sandbox/quickjs/shims/bru.js +31 -1
- package/src/sandbox/quickjs/shims/bruno-request.js +24 -3
- package/src/sandbox/quickjs/shims/bruno-response.js +57 -5
- package/src/sandbox/quickjs/shims/bruno-response.spec.js +91 -0
- package/src/sandbox/quickjs/shims/lib/uuid.spec.js +166 -0
- package/src/sandbox/quickjs/shims/require.js +56 -0
- package/src/sandbox/quickjs/shims/require.spec.js +154 -0
- package/src/sandbox/quickjs/shims/test.js +175 -2
- package/src/sandbox/quickjs/utils/property-list-bridge.js +190 -0
- package/src/sandbox/quickjs/utils/test-helpers.js +31 -0
- package/src/utils/error-formatter.js +345 -20
- package/src/utils/error-formatter.spec.js +683 -1
|
@@ -0,0 +1,497 @@
|
|
|
1
|
+
const ReadOnlyPropertyList = require('./readonly-property-list');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* HeaderList — the `req.headerList` / `res.headerList` API in scripts.
|
|
5
|
+
*
|
|
6
|
+
* Extends PropertyList in dynamic mode: the header list is freshly read from the
|
|
7
|
+
* request's headers object on every access, and write operations manipulate the
|
|
8
|
+
* request config directly (preserving `__headersToDelete` tracking).
|
|
9
|
+
*
|
|
10
|
+
* Key differences from the base PropertyList:
|
|
11
|
+
* - **Case-insensitive** key lookups (HTTP headers are case-insensitive)
|
|
12
|
+
* - **Disabled headers** surfaced with `disabled: true`
|
|
13
|
+
* - **Read-only mode** for response headers (write methods throw)
|
|
14
|
+
* - Write operations manipulate the request config directly (preserving `__headersToDelete`)
|
|
15
|
+
*
|
|
16
|
+
* Accepts the raw request config object (`req`) directly — no dependency on BrunoRequest.
|
|
17
|
+
* Access: `req.headerList` (PropertyList API) vs `req.headers` (raw headers object).
|
|
18
|
+
*
|
|
19
|
+
* ---
|
|
20
|
+
*
|
|
21
|
+
* ## Header object shape
|
|
22
|
+
*
|
|
23
|
+
* Every header surfaced by this list is a plain object:
|
|
24
|
+
*
|
|
25
|
+
* ```js
|
|
26
|
+
* { key, value } // enabled header
|
|
27
|
+
* { key, value, disabled: true } // disabled header
|
|
28
|
+
* ```
|
|
29
|
+
*
|
|
30
|
+
* ---
|
|
31
|
+
*
|
|
32
|
+
* ## Read methods (case-insensitive key matching)
|
|
33
|
+
*
|
|
34
|
+
* | Method | Description | Example return value |
|
|
35
|
+
* |--------------------|----------------------------------------------------|-------------------------------------------------|
|
|
36
|
+
* | `get(name)` | Value of the header with matching key | `'application/json'` |
|
|
37
|
+
* | `one(name)` | Full header object for matching key | `{ key: 'Content-Type', value: 'application/json' }` |
|
|
38
|
+
* | `all()` | Cloned array of all header objects | `[{ key: 'Content-Type', … }, …]` |
|
|
39
|
+
* | `count()` | Number of headers | `3` |
|
|
40
|
+
*
|
|
41
|
+
* ## Search methods (case-insensitive key matching)
|
|
42
|
+
*
|
|
43
|
+
* | Method | Description | Example return value |
|
|
44
|
+
* |--------------------|----------------------------------------------------|----------------------|
|
|
45
|
+
* | `has(name)` | `true` if a header with that key exists | `true` |
|
|
46
|
+
* | `has(name, value)` | `true` if key exists **and** value matches | `false` |
|
|
47
|
+
* | `has(object)` | `true` if a header with `object.key` exists | `true` |
|
|
48
|
+
* | `find(fn, context?)` | First header matching the predicate function | `{ key: … }` |
|
|
49
|
+
* | `filter(fn, context?)` | Array of headers matching the predicate | `[{ key: … }, …]` |
|
|
50
|
+
* | `indexOf(item)` | Index of a header by string key or object, or `-1` | `0` |
|
|
51
|
+
*
|
|
52
|
+
* ## Iteration methods (optional `context` binds `this` in callbacks)
|
|
53
|
+
*
|
|
54
|
+
* | Method | Description |
|
|
55
|
+
* |------------------------------|----------------------------------------------|
|
|
56
|
+
* | `each(fn, context?)` | Calls `fn(header, index)` for every header |
|
|
57
|
+
* | `map(fn, context?)` | Returns a new array of mapped values |
|
|
58
|
+
* | `reduce(fn, initial?, context?)` | Reduces headers to a single value |
|
|
59
|
+
*
|
|
60
|
+
* ## Transform methods
|
|
61
|
+
*
|
|
62
|
+
* | Method | Description |
|
|
63
|
+
* |---------------------------------------------------------------|-------------------------------------------------------|
|
|
64
|
+
* | `toObject(excludeDisabled?, caseSensitive?, multiValue?, sanitizeKeys?)` | `{ key: value }` map of all headers |
|
|
65
|
+
* | `toString()` | HTTP wire format `Key: Value\n...`, skips disabled |
|
|
66
|
+
* | `toJSON()` | Same as `all()` — suitable for `JSON.stringify()` |
|
|
67
|
+
*
|
|
68
|
+
* ## Write methods (HeaderList overrides — synchronous, case-insensitive)
|
|
69
|
+
*
|
|
70
|
+
* | Method | Description |
|
|
71
|
+
* |-----------------------------------|----------------------------------------------------------|
|
|
72
|
+
* | `add(headerObj\|name, value?)` | Sets a header; accepts `{key,value}`, `"Key: Value"`, or `(name, value)` |
|
|
73
|
+
* | `upsert(headerObj\|name, value?)` | Sets (or replaces) a header; returns true/false/null |
|
|
74
|
+
* | `remove(predicate, context?)` | Deletes header(s) by name, predicate, or object |
|
|
75
|
+
* | `clear()` | Removes **all** headers (enabled and disabled) |
|
|
76
|
+
* | `populate(items\|string)` | Adds items, skipping keys that already exist |
|
|
77
|
+
* | `repopulate(items)` | Clears all, then populates with new items |
|
|
78
|
+
* | `assimilate(source, prune?)` | Merges headers; prune removes items not in source |
|
|
79
|
+
*/
|
|
80
|
+
class HeaderList extends ReadOnlyPropertyList {
|
|
81
|
+
#req;
|
|
82
|
+
#writable;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* @param {object} source - Request config (dynamic mode) or response object
|
|
86
|
+
* (static mode). Both must have a `headers` property.
|
|
87
|
+
* @param {object} [options]
|
|
88
|
+
* @param {boolean} [options.writable=true] - When false, write methods throw.
|
|
89
|
+
*/
|
|
90
|
+
constructor(source, { writable = true } = {}) {
|
|
91
|
+
if (writable) {
|
|
92
|
+
// Dynamic mode — reads always reflect current req.headers
|
|
93
|
+
super({
|
|
94
|
+
keyProperty: 'key',
|
|
95
|
+
valueProperty: 'value',
|
|
96
|
+
dataSource: () => {
|
|
97
|
+
const headers = source.headers || {};
|
|
98
|
+
const enabled = Object.entries(headers).map(([key, value]) => ({ key, value }));
|
|
99
|
+
const disabled = (source.disabledHeaders || []).map((h) => ({
|
|
100
|
+
key: h.name,
|
|
101
|
+
value: h.value,
|
|
102
|
+
disabled: true
|
|
103
|
+
}));
|
|
104
|
+
return [...disabled, ...enabled];
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
this.#req = source;
|
|
108
|
+
} else {
|
|
109
|
+
// Static read-only mode — snapshot of response headers
|
|
110
|
+
const rawHeaders = (source && source.headers) || {};
|
|
111
|
+
super({
|
|
112
|
+
keyProperty: 'key',
|
|
113
|
+
valueProperty: 'value',
|
|
114
|
+
items: Object.entries(rawHeaders).map(([key, value]) => ({ key, value }))
|
|
115
|
+
});
|
|
116
|
+
this.#req = null;
|
|
117
|
+
}
|
|
118
|
+
this.#writable = writable;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
#assertWritable() {
|
|
122
|
+
if (!this.#writable) {
|
|
123
|
+
throw new Error('HeaderList is read-only (response headers cannot be modified)');
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ── Case-insensitive key helpers ──────────────────────────────────────
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Case-insensitive string comparison.
|
|
131
|
+
* @param {string} a
|
|
132
|
+
* @param {string} b
|
|
133
|
+
* @returns {boolean}
|
|
134
|
+
*/
|
|
135
|
+
static #ciEquals(a, b) {
|
|
136
|
+
return typeof a === 'string' && typeof b === 'string'
|
|
137
|
+
? a.toLowerCase() === b.toLowerCase()
|
|
138
|
+
: a === b;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Parse a "Key: Value" string into a { key, value } object.
|
|
143
|
+
* @param {string} str
|
|
144
|
+
* @returns {object|null}
|
|
145
|
+
*/
|
|
146
|
+
static #parseHeaderString(str) {
|
|
147
|
+
if (typeof str !== 'string') return null;
|
|
148
|
+
const idx = str.indexOf(':');
|
|
149
|
+
if (idx === -1) return null;
|
|
150
|
+
return { key: str.substring(0, idx).trim(), value: str.substring(idx + 1).trim() };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ── Blocked inherited methods ─────────────────────────────────────────
|
|
154
|
+
// idx is inherited from ReadOnlyPropertyList but not part of the
|
|
155
|
+
// HeaderList API. Set to undefined so it is not callable.
|
|
156
|
+
idx = undefined;
|
|
157
|
+
|
|
158
|
+
// ── Read method overrides (case-insensitive) ──────────────────────────
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Get the value of a header by key (case-insensitive).
|
|
162
|
+
* @param {string} name
|
|
163
|
+
* @returns {*}
|
|
164
|
+
*/
|
|
165
|
+
get(name) {
|
|
166
|
+
const item = this.all().findLast((i) => HeaderList.#ciEquals(i.key, name));
|
|
167
|
+
return item ? item.value : undefined;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Get the full header object by key (case-insensitive).
|
|
172
|
+
* @param {string} name
|
|
173
|
+
* @returns {object|undefined}
|
|
174
|
+
*/
|
|
175
|
+
one(name) {
|
|
176
|
+
return this.all().findLast((i) => HeaderList.#ciEquals(i.key, name));
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Check if a header exists (case-insensitive).
|
|
181
|
+
* Accepts a string key, a string key + value, or an object with `key`.
|
|
182
|
+
* @param {string|object} name - Header key string or object with `key` property
|
|
183
|
+
* @param {*} [value]
|
|
184
|
+
* @returns {boolean}
|
|
185
|
+
*/
|
|
186
|
+
has(name, value) {
|
|
187
|
+
if (name && typeof name === 'object' && name.key) {
|
|
188
|
+
return this.all().some((i) => HeaderList.#ciEquals(i.key, name.key));
|
|
189
|
+
}
|
|
190
|
+
const items = this.all();
|
|
191
|
+
if (value !== undefined) {
|
|
192
|
+
return items.some((i) => HeaderList.#ciEquals(i.key, name) && i.value === value);
|
|
193
|
+
}
|
|
194
|
+
return items.some((i) => HeaderList.#ciEquals(i.key, name));
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Get the index of an item (case-insensitive key matching).
|
|
199
|
+
* Accepts a string key or an object with { key, value }.
|
|
200
|
+
* @param {string|object} item
|
|
201
|
+
* @returns {number} -1 if not found
|
|
202
|
+
*/
|
|
203
|
+
indexOf(item) {
|
|
204
|
+
const items = this.all();
|
|
205
|
+
if (typeof item === 'string') {
|
|
206
|
+
return items.findIndex((i) => HeaderList.#ciEquals(i.key, item));
|
|
207
|
+
}
|
|
208
|
+
if (!item || typeof item !== 'object') return -1;
|
|
209
|
+
return items.findIndex(
|
|
210
|
+
(i) => HeaderList.#ciEquals(i.key, item.key) && i.value === item.value
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// ── Iteration overrides (optional context binding) ─────────────────
|
|
215
|
+
|
|
216
|
+
/** @param {Function} fn @param {*} [context] */
|
|
217
|
+
each(fn, context) {
|
|
218
|
+
super.each(context !== undefined ? fn.bind(context) : fn);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** @param {Function} fn @param {*} [context] @returns {Array} */
|
|
222
|
+
filter(fn, context) {
|
|
223
|
+
return super.filter(context !== undefined ? fn.bind(context) : fn);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** @param {Function} fn @param {*} [context] @returns {object|undefined} */
|
|
227
|
+
find(fn, context) {
|
|
228
|
+
return super.find(context !== undefined ? fn.bind(context) : fn);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** @param {Function} fn @param {*} [context] @returns {Array} */
|
|
232
|
+
map(fn, context) {
|
|
233
|
+
return super.map(context !== undefined ? fn.bind(context) : fn);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** @param {Function} fn @param {*} [accumulator] @param {*} [context] @returns {*} */
|
|
237
|
+
reduce(fn, ...args) {
|
|
238
|
+
const hasAccumulator = args.length > 0;
|
|
239
|
+
const hasContext = args.length > 1;
|
|
240
|
+
const bound = hasContext ? fn.bind(args[1]) : fn;
|
|
241
|
+
return hasAccumulator ? super.reduce(bound, args[0]) : super.reduce(bound);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// ── Write methods (direct request config manipulation) ────────────────
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Add a header. Accepts a { key, value } object, a "Key: Value" string,
|
|
248
|
+
* or two arguments (name, value). Delegates to upsert().
|
|
249
|
+
*
|
|
250
|
+
* @param {object|string} itemOrName - Header object, "Key: Value" string, or header name
|
|
251
|
+
* @param {string} [value] - Header value (when using two-arg form)
|
|
252
|
+
*/
|
|
253
|
+
add(itemOrName, value) {
|
|
254
|
+
if (typeof itemOrName === 'string' && value !== undefined) {
|
|
255
|
+
this.upsert({ key: itemOrName, value });
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
if (typeof itemOrName === 'string') {
|
|
259
|
+
itemOrName = HeaderList.#parseHeaderString(itemOrName);
|
|
260
|
+
}
|
|
261
|
+
this.upsert(itemOrName);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Set (or replace) a header on the request (case-insensitive key match).
|
|
266
|
+
* Accepts a { key, value } object or two arguments (name, value).
|
|
267
|
+
* @param {object|string} itemOrName - Header object with `key` and `value`, or header name
|
|
268
|
+
* @param {string} [value] - Header value (when using two-arg form)
|
|
269
|
+
* @returns {boolean|null} `true` if added, `false` if updated, `null` if input was nil
|
|
270
|
+
*/
|
|
271
|
+
upsert(itemOrName, value) {
|
|
272
|
+
this.#assertWritable();
|
|
273
|
+
let item = itemOrName;
|
|
274
|
+
if (typeof itemOrName === 'string') {
|
|
275
|
+
item = { key: itemOrName, value };
|
|
276
|
+
}
|
|
277
|
+
if (!item || typeof item !== 'object' || !item.key) return null;
|
|
278
|
+
const headers = this.#req.headers || {};
|
|
279
|
+
const existingKey = Object.keys(headers).find(
|
|
280
|
+
(k) => HeaderList.#ciEquals(k, item.key)
|
|
281
|
+
);
|
|
282
|
+
const existed = existingKey !== undefined;
|
|
283
|
+
// Remove old-cased key if casing differs, tracking it for the axios interceptor
|
|
284
|
+
if (existed && existingKey !== item.key) {
|
|
285
|
+
this.#deleteHeader(existingKey);
|
|
286
|
+
}
|
|
287
|
+
headers[item.key] = item.value;
|
|
288
|
+
// Remove from __headersToDelete since we just (re-)added this header
|
|
289
|
+
const toDelete = this.#req.__headersToDelete;
|
|
290
|
+
if (toDelete) {
|
|
291
|
+
const idx = toDelete.findIndex((k) => HeaderList.#ciEquals(k, item.key));
|
|
292
|
+
if (idx !== -1) toDelete.splice(idx, 1);
|
|
293
|
+
}
|
|
294
|
+
return !existed;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Remove header(s) matching a predicate, key string, or item reference.
|
|
299
|
+
* String and object removal are case-insensitive.
|
|
300
|
+
* @param {Function|string|object} predicate
|
|
301
|
+
* @param {*} [context] - Bind `this` for function predicates
|
|
302
|
+
*/
|
|
303
|
+
remove(predicate, context) {
|
|
304
|
+
this.#assertWritable();
|
|
305
|
+
if (typeof predicate === 'function') {
|
|
306
|
+
const bound = context !== undefined ? predicate.bind(context) : predicate;
|
|
307
|
+
const headers = this.all();
|
|
308
|
+
for (const header of headers) {
|
|
309
|
+
if (bound(header)) {
|
|
310
|
+
if (header.disabled) {
|
|
311
|
+
this.#removeDisabledHeader(header.key);
|
|
312
|
+
} else {
|
|
313
|
+
this.#deleteHeaderCI(header.key);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
} else if (typeof predicate === 'string') {
|
|
318
|
+
this.#deleteHeaderCI(predicate);
|
|
319
|
+
this.#removeDisabledHeader(predicate);
|
|
320
|
+
} else if (predicate && typeof predicate === 'object' && predicate.key) {
|
|
321
|
+
this.#deleteHeaderCI(predicate.key);
|
|
322
|
+
this.#removeDisabledHeader(predicate.key);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Delete a header by exact key and track it in `__headersToDelete`
|
|
328
|
+
* so the axios interceptor can suppress default headers added later.
|
|
329
|
+
* @param {string} name
|
|
330
|
+
*/
|
|
331
|
+
#deleteHeader(name) {
|
|
332
|
+
delete this.#req.headers[name];
|
|
333
|
+
if (!this.#req.__headersToDelete) {
|
|
334
|
+
this.#req.__headersToDelete = [];
|
|
335
|
+
}
|
|
336
|
+
if (!this.#req.__headersToDelete.includes(name)) {
|
|
337
|
+
this.#req.__headersToDelete.push(name);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Delete an enabled header by key (case-insensitive).
|
|
343
|
+
* @param {string} key
|
|
344
|
+
*/
|
|
345
|
+
#deleteHeaderCI(key) {
|
|
346
|
+
const headers = this.#req.headers || {};
|
|
347
|
+
const matchingKey = Object.keys(headers).find(
|
|
348
|
+
(k) => HeaderList.#ciEquals(k, key)
|
|
349
|
+
);
|
|
350
|
+
if (matchingKey) {
|
|
351
|
+
this.#deleteHeader(matchingKey);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Remove all disabled headers matching a key (case-insensitive).
|
|
357
|
+
* @param {string} key
|
|
358
|
+
*/
|
|
359
|
+
#removeDisabledHeader(key) {
|
|
360
|
+
const arr = this.#req.disabledHeaders;
|
|
361
|
+
if (!arr) return;
|
|
362
|
+
this.#req.disabledHeaders = arr.filter(
|
|
363
|
+
(h) => !HeaderList.#ciEquals(h.name, key)
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Remove all headers (enabled and disabled) from the request.
|
|
369
|
+
*/
|
|
370
|
+
clear() {
|
|
371
|
+
this.#assertWritable();
|
|
372
|
+
const headers = this.all();
|
|
373
|
+
for (const header of headers) {
|
|
374
|
+
if (!header.disabled) {
|
|
375
|
+
this.#deleteHeader(header.key);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
if (this.#req.disabledHeaders) {
|
|
379
|
+
this.#req.disabledHeaders = [];
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Load one or more headers into the list (without clearing existing ones).
|
|
385
|
+
* Accepts an array of { key, value } objects or a multi-line "Key: Value" string.
|
|
386
|
+
*
|
|
387
|
+
* Headers whose key already exists are skipped (case-insensitive).
|
|
388
|
+
* Note: Postman's populate adds duplicate keys because Postman supports
|
|
389
|
+
* multiple headers with the same name. Bruno does not, so we skip
|
|
390
|
+
* existing keys to preserve the current value.
|
|
391
|
+
*
|
|
392
|
+
* @param {Array|string} items
|
|
393
|
+
*/
|
|
394
|
+
populate(items) {
|
|
395
|
+
this.#assertWritable();
|
|
396
|
+
if (typeof items === 'string') {
|
|
397
|
+
const lines = items.split(/\r?\n/).filter((l) => l.trim());
|
|
398
|
+
for (const line of lines) {
|
|
399
|
+
const parsed = HeaderList.#parseHeaderString(line);
|
|
400
|
+
if (parsed && !this.has(parsed.key)) {
|
|
401
|
+
this.add(parsed);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
const list = Array.isArray(items) ? items : [];
|
|
407
|
+
for (const item of list) {
|
|
408
|
+
if (item && item.key && !this.has(item.key)) {
|
|
409
|
+
this.add(item);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Clear all headers and repopulate with new items.
|
|
416
|
+
* @param {Array|string} items
|
|
417
|
+
*/
|
|
418
|
+
repopulate(items) {
|
|
419
|
+
this.clear();
|
|
420
|
+
this.populate(items);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// ── Transform overrides ───────────────────────────────────────────────
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Convert to a plain object. Matches Postman's PropertyList.toObject() signature.
|
|
427
|
+
* @param {boolean} [excludeDisabled=false] - If true, skip disabled headers
|
|
428
|
+
* @param {boolean} [caseSensitive=true] - If false, lowercase all keys
|
|
429
|
+
* @param {boolean} [multiValue=false] - If true, only the first value of a duplicate key is kept
|
|
430
|
+
* @param {boolean} [sanitizeKeys=false] - If true, skip headers with falsy keys
|
|
431
|
+
* @returns {object}
|
|
432
|
+
*/
|
|
433
|
+
toObject(excludeDisabled, caseSensitive, multiValue, sanitizeKeys) {
|
|
434
|
+
const result = {};
|
|
435
|
+
for (const item of this.all()) {
|
|
436
|
+
if (excludeDisabled && item.disabled) continue;
|
|
437
|
+
const key = caseSensitive === false ? item.key.toLowerCase() : item.key;
|
|
438
|
+
if (sanitizeKeys && !key) continue;
|
|
439
|
+
if (multiValue) {
|
|
440
|
+
if (!(key in result)) {
|
|
441
|
+
result[key] = item.value;
|
|
442
|
+
}
|
|
443
|
+
} else {
|
|
444
|
+
result[key] = item.value;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
return result;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* Convert to HTTP wire-format string, skipping disabled headers.
|
|
452
|
+
* Matches Postman's Header.unparse() behavior: `Key: Value\n...`
|
|
453
|
+
* @returns {string}
|
|
454
|
+
*/
|
|
455
|
+
toString() {
|
|
456
|
+
const headers = this.all().filter((h) => !h.disabled);
|
|
457
|
+
if (headers.length === 0) return '';
|
|
458
|
+
return headers.map((h) => `${h.key}: ${h.value}`).join('\n') + '\n';
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* Merge items from another PropertyList or array.
|
|
463
|
+
* @param {PropertyList|Array} source - Source of items to merge
|
|
464
|
+
* @param {boolean} [prune=false] - If true, remove items not present in source after merging
|
|
465
|
+
*/
|
|
466
|
+
assimilate(source, prune) {
|
|
467
|
+
this.#assertWritable();
|
|
468
|
+
let items;
|
|
469
|
+
if (ReadOnlyPropertyList.isPropertyList(source)) {
|
|
470
|
+
items = source.all();
|
|
471
|
+
} else if (Array.isArray(source)) {
|
|
472
|
+
items = source;
|
|
473
|
+
} else {
|
|
474
|
+
items = [];
|
|
475
|
+
}
|
|
476
|
+
// Merge source items into this list
|
|
477
|
+
for (const item of items) {
|
|
478
|
+
this.add(item);
|
|
479
|
+
}
|
|
480
|
+
// Prune: remove items from this list that are not in source
|
|
481
|
+
if (prune && items.length > 0) {
|
|
482
|
+
const sourceKeys = new Set(items.map((i) => (i.key || '').toLowerCase()));
|
|
483
|
+
const toRemove = this.all().filter(
|
|
484
|
+
(h) => !sourceKeys.has(h.key.toLowerCase())
|
|
485
|
+
);
|
|
486
|
+
for (const header of toRemove) {
|
|
487
|
+
if (header.disabled) {
|
|
488
|
+
this.#removeDisabledHeader(header.key);
|
|
489
|
+
} else {
|
|
490
|
+
this.#deleteHeader(header.key);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
module.exports = HeaderList;
|
package/src/index.js
CHANGED
|
@@ -3,7 +3,21 @@ const TestRuntime = require('./runtime/test-runtime');
|
|
|
3
3
|
const VarsRuntime = require('./runtime/vars-runtime');
|
|
4
4
|
const AssertRuntime = require('./runtime/assert-runtime');
|
|
5
5
|
const { runScriptInNodeVm } = require('./sandbox/node-vm');
|
|
6
|
-
const {
|
|
6
|
+
const {
|
|
7
|
+
formatErrorWithContext,
|
|
8
|
+
formatErrorWithContextV2,
|
|
9
|
+
SCRIPT_TYPES,
|
|
10
|
+
parseErrorLocation,
|
|
11
|
+
adjustLineNumber,
|
|
12
|
+
resolveSegmentError,
|
|
13
|
+
getSourceContext,
|
|
14
|
+
adjustStackTrace,
|
|
15
|
+
getErrorTypeName,
|
|
16
|
+
findScriptBlockStartLine,
|
|
17
|
+
findScriptBlockEndLine,
|
|
18
|
+
findYmlScriptBlockStartLine,
|
|
19
|
+
findYmlScriptBlockEndLine
|
|
20
|
+
} = require('./utils/error-formatter');
|
|
7
21
|
|
|
8
22
|
module.exports = {
|
|
9
23
|
ScriptRuntime,
|
|
@@ -12,5 +26,16 @@ module.exports = {
|
|
|
12
26
|
AssertRuntime,
|
|
13
27
|
runScriptInNodeVm,
|
|
14
28
|
formatErrorWithContext,
|
|
15
|
-
|
|
29
|
+
formatErrorWithContextV2,
|
|
30
|
+
SCRIPT_TYPES,
|
|
31
|
+
parseErrorLocation,
|
|
32
|
+
adjustLineNumber,
|
|
33
|
+
resolveSegmentError,
|
|
34
|
+
getSourceContext,
|
|
35
|
+
adjustStackTrace,
|
|
36
|
+
getErrorTypeName,
|
|
37
|
+
findScriptBlockStartLine,
|
|
38
|
+
findScriptBlockEndLine,
|
|
39
|
+
findYmlScriptBlockStartLine,
|
|
40
|
+
findYmlScriptBlockEndLine
|
|
16
41
|
};
|