njsparser 0.1.0 → 0.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.
@@ -1,333 +1,216 @@
1
- import { make_tree } from '../utils.js';
2
- import { resolve_type } from './types.js';
1
+ /**
2
+ * Flight data extraction and parsing
3
+ */
3
4
 
4
- const _raw_f_data = []; // Type annotation placeholder
5
- const _re_f_init = /\(self\.__next_f\s?=\s?self\.__next_f\s?\|\|\s?\[\]\)\.push\((\[.+?\])\)/;
6
- const _re_f_payload = /self\.__next_f\.push\((\[.+)\)$/;
5
+ import { makeTree } from '../utils.js';
6
+ import { resolveType } from './types.js';
7
+
8
+ // Regex patterns for matching flight data scripts
9
+ const RE_F_INIT = /\(self\.__next_f\s?=\s?self\.__next_f\s?\|\|\s?\[\]\)\.push\((\[.+?\])\)/;
10
+ const RE_F_PAYLOAD = /self\.__next_f\.push\((\[.+)\)$/;
11
+
12
+ // Segment types
13
+ const Segment = {
14
+ is_bootstrap: 0,
15
+ is_not_bootstrap: 1,
16
+ is_form_state: 2,
17
+ is_binary: 3
18
+ };
7
19
 
8
20
  /**
9
- * Tells if a given page contains any flight data.
10
- * @param {any} value
11
- * @returns {boolean}
21
+ * Check if HTML contains flight data
22
+ * @param {string} html - HTML string
23
+ * @param {DOMParser} DOMParser - DOMParser instance
24
+ * @returns {boolean} True if flight data exists
12
25
  */
13
- export const has_flight_data = (value) => {
14
- const $ = make_tree(value);
15
- const scripts = $('script');
16
- let found = false;
17
- scripts.each((_, el) => {
18
- const text = $(el).text(); // Use text() instead of html() for scripts content?
19
- // cheerio .text() gets text content. .html() gets innerHTML.
20
- // Usually safer to use .html() or .text() depending on encoding.
21
- // Python lxml xpath text() gets text.
22
- if (_re_f_init.test(text)) {
23
- found = true;
24
- return false; // break
25
- }
26
- });
27
- return found;
28
- };
26
+ export function hasFlightData(html, DOMParser) {
27
+ const doc = makeTree(html, DOMParser);
28
+ const scripts = Array.from(doc.querySelectorAll('script')).map(s => s.textContent || '');
29
+ return scripts.some(script => RE_F_INIT.test(script));
30
+ }
29
31
 
30
32
  /**
31
- * Will return the raw flight data.
32
- * @param {any} value
33
- * @returns {Array | null}
33
+ * Extract raw flight data from HTML
34
+ * @param {string} html - HTML string
35
+ * @param {DOMParser} DOMParser - DOMParser instance
36
+ * @returns {Array|null} Raw flight data array or null
34
37
  */
35
- export const get_raw_flight_data = (value) => {
36
- const $ = make_tree(value);
37
- const result = [];
38
- let found_init = false;
38
+ export function getRawFlightData(html, DOMParser) {
39
+ const doc = makeTree(html, DOMParser);
40
+ const scripts = Array.from(doc.querySelectorAll('script')).map(s => s.textContent || '');
41
+
42
+ const result = [];
43
+ let foundInit = false;
44
+
45
+ for (const script of scripts) {
46
+ const trimmed = script.trim();
39
47
 
40
- $('script').each((_, el) => {
41
- const script = $(el).text()?.trim();
42
- if (!script) return;
43
-
44
- let init_match;
45
- if (!found_init && (init_match = script.match(_re_f_init))) {
46
- found_init = true;
47
- try {
48
- result.push(JSON.parse(init_match[1])); // Match group 1 is `[...]`
49
- } catch (e) {
50
- console.warn("Failed to parse init flight data", e);
51
- }
52
- }
53
-
54
- // Note: The python regex for payload might match the same script as init if it has both?
55
- // Actually the init regex is for the push call structure used initially.
56
- // The payload regex matches `self.__next_f.push([...])` at the end of string.
57
- let payload_match;
58
- if ((payload_match = script.match(_re_f_payload))) {
59
- try {
60
- result.push(JSON.parse(payload_match[1]));
61
- } catch (e) {
62
- console.warn("Failed to parse payload flight data", e);
63
- }
64
- }
65
- });
48
+ // Check for initialization script
49
+ if (!foundInit) {
50
+ const initMatch = trimmed.match(RE_F_INIT);
51
+ if (initMatch) {
52
+ foundInit = true;
53
+ result.push(JSON.parse(initMatch[1]));
54
+ }
55
+ }
66
56
 
67
- return result.length > 0 ? result : null;
68
- };
69
-
70
- export const Segment = {
71
- is_bootstrap: 0,
72
- is_not_bootstrap: 1,
73
- is_form_state: 2,
74
- is_binary: 3
75
- };
57
+ // Check for payload script
58
+ const payloadMatch = trimmed.match(RE_F_PAYLOAD);
59
+ if (payloadMatch) {
60
+ result.push(JSON.parse(payloadMatch[1]));
61
+ }
62
+ }
63
+
64
+ return result.length > 0 ? result : null;
65
+ }
76
66
 
77
67
  /**
78
- * Decodes the raw flight data.
79
- * @param {Array} raw_flight_data
80
- * @returns {string[]}
68
+ * Decode raw flight data segments
69
+ * @param {Array} rawFlightData - Raw flight data array
70
+ * @returns {Array<string>} Decoded flight data chunks
81
71
  */
82
- export const decode_raw_flight_data = (raw_flight_data) => {
83
- let initial_server_data_buffer = null;
84
- let initial_form_state_data = null;
72
+ export function decodeRawFlightData(rawFlightData) {
73
+ let initialServerDataBuffer;
74
+ let initialFormStateData;
75
+
76
+ for (const seg of rawFlightData) {
77
+ const segmentType = seg[0];
85
78
 
86
- for (const seg of raw_flight_data) {
87
- if (seg[0] === Segment.is_bootstrap) {
88
- initial_server_data_buffer = [];
89
- } else if (seg[0] === Segment.is_not_bootstrap) {
90
- if (initial_server_data_buffer === null) {
91
- throw new Error('UnboundLocalError: initial_server_data_buffer was not yet initialized');
92
- }
93
- initial_server_data_buffer.push(seg[1]);
94
- } else if (seg[0] === Segment.is_form_state) {
95
- initial_form_state_data = seg[1];
96
- } else if (seg[0] === Segment.is_binary) {
97
- if (initial_server_data_buffer === null) {
98
- throw new Error('UnboundLocalError: initial_server_data_buffer was not yet initialized');
99
- }
100
- const buffer = Buffer.from(seg[1], 'base64'); // base64 decode
101
- initial_server_data_buffer.push(buffer.toString('utf-8'));
102
- } else {
103
- throw new Error(`Unknown segment type ${seg[0]}`);
104
- }
79
+ if (segmentType === Segment.is_bootstrap) {
80
+ initialServerDataBuffer = [];
81
+ } else if (segmentType === Segment.is_not_bootstrap) {
82
+ if (initialServerDataBuffer === undefined) {
83
+ throw new Error(
84
+ 'The `initialServerDataBuffer` was not yet initialized and a segment tried to append its data to it. ' +
85
+ 'This should not be happening if the flight data starts correctly with a the `is_bootstrap` segment.'
86
+ );
87
+ }
88
+ initialServerDataBuffer.push(seg[1]);
89
+ } else if (segmentType === Segment.is_form_state) {
90
+ initialFormStateData = seg[1];
91
+ } else if (segmentType === Segment.is_binary) {
92
+ if (initialServerDataBuffer === undefined) {
93
+ throw new Error(
94
+ 'The `initialServerDataBuffer` was not yet initialized and a segment tried to append its data to it. ' +
95
+ 'This should not be happening if the flight data starts correctly with a the `is_bootstrap` segment.'
96
+ );
97
+ }
98
+ // Decode base64
99
+ const decodedChunk = atob(seg[1]);
100
+ initialServerDataBuffer.push(decodedChunk);
101
+ } else {
102
+ throw new Error(`Unknown segment type seg[0]=${segmentType}`);
105
103
  }
106
-
107
- if (initial_server_data_buffer === null) {
108
- // As per python logic, it raises Error if used before init.
109
- // But if loop finishes without init, it just returns null in JS?
110
- // Python returns `initial_server_data_buffer` which would be uninitialized if first loop didn't run or verify.
111
- // But the first segment SHOULD be bootstrap.
112
- // If raw_flight_data is empty, it returns unbound error in Python?
113
- // Actually Python code: `return initial_server_data_buffer` -> if not assigned, UnboundLocalError.
114
- throw new Error('UnboundLocalError: initial_server_data_buffer not initialized (empty data?)');
104
+ }
105
+
106
+ return initialServerDataBuffer;
107
+ }
108
+
109
+ /**
110
+ * Parse decoded raw flight data into structured objects
111
+ * @param {Array<string>} decodedRawFlightData - Decoded flight data chunks
112
+ * @returns {Object} Dictionary mapping indices to parsed elements
113
+ */
114
+ export function parseDecodedRawFlightData(decodedRawFlightData) {
115
+ // Join and encode to bytes
116
+ const compiledRawFlightData = new TextEncoder().encode(decodedRawFlightData.join(''));
117
+ const indexedResult = {};
118
+ let pos = 0;
119
+
120
+ while (true) {
121
+ const indexStringEnd = compiledRawFlightData.indexOf(58, pos); // ':'
122
+ if (indexStringEnd === -1) {
123
+ break;
115
124
  }
116
125
 
117
- return initial_server_data_buffer;
118
- };
126
+ const indexStringRaw = compiledRawFlightData.slice(pos, indexStringEnd);
127
+ let index = null;
128
+ if (indexStringRaw.length > 0) {
129
+ const indexStr = new TextDecoder().decode(indexStringRaw);
130
+ index = parseInt(indexStr, 16);
131
+ }
132
+ pos = indexStringEnd + 1;
119
133
 
120
- const _split_points = /(?<!\\)\n[a-f0-9]*:/g;
121
- // JS regex lookbehind support depends on engine. Bun supports it (V8/JSC).
122
- // But `\n` matching might be issue if buffer.
123
- // We are working with bytes in Python logic.
124
- // JS regex works on strings.
125
- // But we need to find split points in the *encoded* bytes?
126
- // Python: `_split_points = re.compile(rb"(?<!\\)\n[a-f0-9]*:")` (bytes regex)
134
+ // Extract value class (uppercase letters)
135
+ let valueClass = '';
136
+ while (pos < compiledRawFlightData.length) {
137
+ const char = String.fromCharCode(compiledRawFlightData[pos]);
138
+ if (/[A-Z]/.test(char)) {
139
+ valueClass += char;
140
+ pos++;
141
+ } else {
142
+ break;
143
+ }
144
+ }
145
+ valueClass = valueClass || null;
127
146
 
128
- // We need to implement `parse_decoded_raw_flight_data` using Buffers to match Python logic precisely.
147
+ let value;
129
148
 
130
- /**
131
- * Parses decoded raw flight data.
132
- * @param {string[]} decoded_raw_flight_data
133
- * @returns {object}
134
- */
135
- export const parse_decoded_raw_flight_data = (decoded_raw_flight_data) => {
136
- const combinedString = decoded_raw_flight_data.join("");
137
- const buffer = Buffer.from(combinedString); // UTF-8 encoded buffer
138
-
139
- const indexed_result = {};
140
- let pos = 0;
141
-
142
- while (true) {
143
- // Find index string end ":", starting from pos
144
- const colonIndex = buffer.indexOf(58, pos); // 58 is ':'
145
- if (colonIndex === -1) {
146
- break;
147
- }
148
-
149
- const indexStringBuf = buffer.subarray(pos, colonIndex);
150
- let index = null;
151
- if (indexStringBuf.length > 0) {
152
- const indexString = indexStringBuf.toString();
153
- try {
154
- index = parseInt(indexString, 16);
155
- } catch (e) {
156
- // Ignore?
149
+ if (valueClass === 'T') {
150
+ const textLengthStringEnd = compiledRawFlightData.indexOf(44, pos); // ','
151
+ const textLengthHex = compiledRawFlightData.slice(pos, textLengthStringEnd);
152
+ const textLength = parseInt(new TextDecoder().decode(textLengthHex), 16);
153
+ const textStart = textLengthStringEnd + 1;
154
+ value = new TextDecoder().decode(compiledRawFlightData.slice(textStart, textStart + textLength));
155
+ pos = textStart + textLength;
156
+ } else {
157
+ // Find next split point
158
+ let dataEnd = -1;
159
+ for (let i = pos; i < compiledRawFlightData.length - 1; i++) {
160
+ if (compiledRawFlightData[i] === 10) { // '\n'
161
+ if (i === 0 || compiledRawFlightData[i - 1] !== 92) { // not escaped
162
+ let j = i + 1;
163
+ while (j < compiledRawFlightData.length && /[0-9a-f]/.test(String.fromCharCode(compiledRawFlightData[j]))) {
164
+ j++;
157
165
  }
158
- }
159
-
160
- pos = colonIndex + 1;
161
-
162
- // Iterate while char is uppercase letter
163
- let value_class = "";
164
- while (pos < buffer.length) {
165
- const byte = buffer[pos];
166
- const char = String.fromCharCode(byte);
167
- if (/[A-Z]/.test(char)) {
168
- value_class += char;
169
- pos++;
170
- } else {
171
- break;
172
- }
173
- }
174
- if (value_class === "") value_class = null;
175
-
176
- let raw_value_str;
177
- let value;
178
-
179
- if (value_class === "T") {
180
- // Find comma
181
- const commaIndex = buffer.indexOf(44, pos); // 44 is ','
182
- if (commaIndex === -1) throw new Error("Expected comma after 'T' class size");
183
-
184
- const lenHexBuf = buffer.subarray(pos, commaIndex);
185
- const textLength = parseInt(lenHexBuf.toString(), 16);
186
-
187
- const textStart = commaIndex + 1;
188
- const textEnd = textStart + textLength;
189
-
190
- const textBuf = buffer.subarray(textStart, textEnd);
191
- raw_value_str = textBuf.toString('utf-8');
192
- value = raw_value_str;
193
-
194
- pos = textEnd;
195
- } else {
196
- // Search for next split point: `\n` followed by hex+colon
197
- // We can search for `\n` and check pattern.
198
- // Loop until found or end
199
- let nextSplitPos = -1;
200
- let searchPos = pos;
201
-
202
- while (true) {
203
- const newlineIndex = buffer.indexOf(10, searchPos); // 10 is '\n'
204
- if (newlineIndex === -1) {
205
- break;
206
- }
207
-
208
- // Check lookbehind: `(?<!\\)` -> char before `\n` should not be `\` (92)
209
- let isEscaped = false;
210
- if (newlineIndex > 0 && buffer[newlineIndex - 1] === 92) {
211
- isEscaped = true;
212
- }
213
-
214
- if (!isEscaped) {
215
- // Check lookahead: `[a-f0-9]*:`
216
- // We scan from newlineIndex + 1 for hex chars then colon
217
- let p = newlineIndex + 1;
218
- let isMatch = true;
219
- while (p < buffer.length) {
220
- const b = buffer[p];
221
- if (b === 58) { // found colon
222
- break;
223
- }
224
- const c = String.fromCharCode(b);
225
- if (!/[a-f0-9]/.test(c)) {
226
- isMatch = false;
227
- break;
228
- }
229
- p++;
230
- }
231
- // If we stopped at colon, it's a match
232
- if (isMatch && p < buffer.length && buffer[p] === 58) {
233
- nextSplitPos = newlineIndex; // The split starts at `\n`
234
- break;
235
- }
236
- }
237
-
238
- searchPos = newlineIndex + 1;
239
- }
240
-
241
- if (nextSplitPos !== -1) {
242
- const valBuf = buffer.subarray(pos, nextSplitPos);
243
- raw_value_str = valBuf.toString('utf-8');
244
- pos = nextSplitPos + 1; // Skip the newline
245
- } else {
246
- // Until end
247
- // Python: `raw_value = compiled_raw_flight_data[pos:-1]`
248
- // Wait, [pos:-1] removes the LAST byte?
249
- // Why?
250
- // Ah, because `compiled_raw_flight_data` in python might have a trailing `\n` or similar?
251
- // Or maybe just generic slice logic?
252
- // `raw_value = compiled_raw_flight_data[pos:data_end]` (excludes `\n`)
253
- // If it goes to end, `pos:-1`.
254
- // Let's assume it means "up to the last character".
255
- // Python slice `[pos:-1]` includes from pos up to (but not including) the last item.
256
- // Does flight data always end with a newline valid as split point?
257
- // If the stream ends, it might not have the next split marker.
258
- // But why exclude the last char?
259
- // Maybe the stream ends with a newline?
260
- // Let's check Python code again.
261
- // `raw_value = compiled_raw_flight_data[pos:-1]`
262
- // `pos += len(raw_value)`
263
- // This implies it consumes everything EXCEPT the very last byte.
264
- // Is there a phantom byte at the end? `compiled_raw_flight_data` is just `.join().encode()`.
265
- // If I decode `raw_flight_data`, join them, encode them.
266
- // Maybe I should match strict Python behavior.
267
- // `combinedString` in JS vs Python.
268
- // If I have "foo", [0:-1] is "fo".
269
-
270
- // Let's assume for now I should take everything. The `-1` in Python is suspicious unless I know why.
271
- // Maybe `_split_points` regex matching behavior in loop?
272
- // If I am at the last chunk, it might not end with `\n...:`.
273
- // If I simply take everything `buffer.subarray(pos)`, I might include a trailing newline that effectively belongs to the "next" but nonexistent chunk?
274
- // But `pos` is advanced.
275
-
276
- // WAIT. If `_split_points` finds a match, `data_end` is the start of `\n`.
277
- // `raw_value = ...[pos:data_end]`.
278
- // If `else` (no match), `raw_value = [pos:-1]`.
279
- // This definitely drops the last byte.
280
- // I will replicate this behavior: `buffer.subarray(pos, buffer.length - 1)`.
281
-
282
- const valBuf = buffer.subarray(pos, buffer.length - 1);
283
- raw_value_str = valBuf.toString('utf-8');
284
- pos += valBuf.length;
285
- // And loop will terminate because pos vs buffer.length check or colonIndex search
286
- }
287
-
288
- try {
289
- value = JSON.parse(raw_value_str);
290
- } catch (e) {
291
- // If JSON parse fails, keep string? Python code: `value = orjson.loads(raw_value)`
292
- // It assumes valid JSON.
293
- value = raw_value_str;
166
+ if (j < compiledRawFlightData.length && compiledRawFlightData[j] === 58) {
167
+ dataEnd = i;
168
+ break;
294
169
  }
170
+ }
295
171
  }
296
-
297
- const resolved = resolve_type({
298
- value: value,
299
- value_class: value_class,
300
- index: index
301
- });
302
-
303
- if (index === null) {
304
- // Wait, why index as key if it is null?
305
- // Python: `if index not in indexed_result: ...` -> index is None.
306
- // `indexed_result[None] = []`
307
- // JS objects keys are strings. "null".
308
- // If index is null, key is "null".
309
- if (!indexed_result["null"]) {
310
- indexed_result["null"] = [];
311
- }
312
- indexed_result["null"].push(resolved);
313
- } else {
314
- indexed_result[index] = resolved;
315
- }
172
+ }
173
+
174
+ const rawValue = dataEnd !== -1
175
+ ? compiledRawFlightData.slice(pos, dataEnd)
176
+ : compiledRawFlightData.slice(pos);
177
+
178
+ pos = dataEnd !== -1 ? dataEnd + 1 : compiledRawFlightData.length;
179
+
180
+ const rawText = new TextDecoder().decode(rawValue);
181
+ if (rawText.length === 0) {
182
+ value = null;
183
+ } else {
184
+ value = JSON.parse(rawText);
185
+ }
316
186
  }
317
-
318
- return indexed_result;
319
- };
187
+
188
+ const resolved = resolveType(value, valueClass, index);
189
+
190
+ if (index === null) {
191
+ if (!(index in indexedResult)) {
192
+ indexedResult[index] = [];
193
+ }
194
+ indexedResult[index].push(resolved);
195
+ } else {
196
+ indexedResult[index] = resolved;
197
+ }
198
+ }
199
+
200
+ return indexedResult;
201
+ }
320
202
 
321
203
  /**
322
- * Returns the flight data of the page.
323
- * @param {any} value
324
- * @returns {object | null}
204
+ * Get parsed flight data from HTML
205
+ * @param {string} html - HTML string
206
+ * @param {DOMParser} DOMParser - DOMParser instance
207
+ * @returns {Object|null} Parsed flight data or null
325
208
  */
326
- export const get_flight_data = (value) => {
327
- const raw = get_raw_flight_data(value);
328
- if (raw) {
329
- const decoded = decode_raw_flight_data(raw);
330
- return parse_decoded_raw_flight_data(decoded);
331
- }
209
+ export function getFlightData(html, DOMParser) {
210
+ const rawFlightData = getRawFlightData(html, DOMParser);
211
+ if (rawFlightData === null) {
332
212
  return null;
333
- };
213
+ }
214
+ const decodedRawFlightData = decodeRawFlightData(rawFlightData);
215
+ return parseDecodedRawFlightData(decodedRawFlightData);
216
+ }
@@ -1,46 +1,46 @@
1
+ /**
2
+ * Build manifest parsing
3
+ */
4
+
1
5
  import { join } from '../utils.js';
2
- import { _NS } from './urls.js';
3
6
 
4
- export const _build_manifest_name = "_buildManifest.js";
5
- export const _ssg_manifest_name = "_ssgManifest.js";
6
- export const _build_manifest_path = `/${_build_manifest_name}`;
7
- export const _ssg_manifest_path = `/${_ssg_manifest_name}`;
7
+ const _NS = '/_next/static/';
8
+ const _build_manifest_name = '_buildManifest.js';
9
+ const _ssg_manifest_name = '_ssgManifest.js';
10
+ const _build_manifest_path = `/${_build_manifest_name}`;
11
+ const _ssg_manifest_path = `/${_ssg_manifest_name}`;
12
+
8
13
  export const _manifest_paths = [_build_manifest_path, _ssg_manifest_path];
9
14
 
10
15
  /**
11
- * Parses the buildmanifest script (`"/_buildManifest.js"`).
12
- * @param {string} script
13
- * @returns {object | null}
16
+ * Parse build manifest script
17
+ * @param {string} script - Build manifest script content
18
+ * @returns {Object} Parsed manifest object
14
19
  */
15
- export const parse_buildmanifest = (script) => {
16
- const s = script.trim();
17
- if (!s.startsWith("self.__BUILD_MANIFEST")) {
18
- throw new Error('Invalid build manifest (not starting by `"self.__BUILD_MANIFEST"`).');
19
- }
20
-
21
- // We can use a simple evaluation mechanism or regex, but `eval` in JS is dangerous.
22
- // However, since we are porting Python's pythonmonkey.eval (which is spidermonkey),
23
- // it seems the intention is to execute the code.
24
- // In JS context (Bun/Node), we can use `vm` or `new Function`.
25
- // The script looks like: self.__BUILD_MANIFEST={...};self.__BUILD_MANIFEST_CB&&self.__BUILD_MANIFEST_CB()
26
-
27
- // Let's try to emulate the browser environment slightly.
28
- const mockSelf = {};
29
- const func = new Function('self', s + '; return self.__BUILD_MANIFEST;');
30
- try {
31
- return func(mockSelf);
32
- } catch (e) {
33
- console.warn(`Could not parse the given build manifest \`${s}\``);
34
- return null;
35
- }
36
- };
20
+ export function parseBuildManifest(script) {
21
+ const s = script.trim();
22
+
23
+ if (!s.startsWith('self.__BUILD_MANIFEST')) {
24
+ throw new Error('Invalid build manifest (not starting by `"self.__BUILD_MANIFEST"`).');
25
+ }
26
+
27
+ // Wrap in IIFE and evaluate
28
+ const func = `(function() {self={};${s.replace(/;$/, '')};return self.__BUILD_MANIFEST})();`;
29
+
30
+ try {
31
+ return eval(func);
32
+ } catch (e) {
33
+ console.warn(`Could not parse the given build manifest \`${s}\``);
34
+ throw e;
35
+ }
36
+ }
37
37
 
38
38
  /**
39
- * Gives the path of the build manifest based on the given build id and base path.
40
- * @param {string} build_id
41
- * @param {string} [base_path]
42
- * @returns {string}
39
+ * Get build manifest path
40
+ * @param {string} buildId - Build ID
41
+ * @param {string} basePath - Base path (optional)
42
+ * @returns {string} Build manifest path
43
43
  */
44
- export const get_build_manifest_path = (build_id, base_path = "") => {
45
- return join(base_path, _NS, build_id, _build_manifest_name);
46
- };
44
+ export function getBuildManifestPath(buildId, basePath = '') {
45
+ return join(basePath, _NS, buildId, _build_manifest_name);
46
+ }