rnxsim 0.1.427 → 0.1.428

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 (59) hide show
  1. package/cli/commands/control.ts +17 -1
  2. package/cli/commands/flow.ts +5 -9
  3. package/cli/commands/inspect/core.ts +1 -0
  4. package/cli/outbound-endpoints.ts +5 -2
  5. package/cli/self-invocation.ts +24 -0
  6. package/detox/index.ts +10 -36
  7. package/dist-lib/agent-daemon-client.cjs +48 -24
  8. package/dist-lib/agent-events.cjs +1 -1
  9. package/dist-lib/agent-identity.cjs +1 -1
  10. package/dist-lib/agent-sessions.cjs +100 -76
  11. package/dist-lib/attached-projects.cjs +1 -1
  12. package/dist-lib/auth/shared-session.cjs +1 -1
  13. package/dist-lib/backend-origin.cjs +1 -1
  14. package/dist-lib/beta.cjs +1 -1
  15. package/dist-lib/beta.mjs +1 -1
  16. package/dist-lib/bridge-constants.cjs +1 -1
  17. package/dist-lib/bridge-contract-input.cjs +1 -1
  18. package/dist-lib/bridge-contract-input.mjs +1 -1
  19. package/dist-lib/bridge-contract.cjs +1 -1
  20. package/dist-lib/bridge-contract.mjs +1 -1
  21. package/dist-lib/capture-contract.cjs +1 -1
  22. package/dist-lib/capture-contract.mjs +1 -1
  23. package/dist-lib/cli-constants.cjs +1 -1
  24. package/dist-lib/cloud-contract.cjs +1 -1
  25. package/dist-lib/cloud-contract.mjs +1 -1
  26. package/dist-lib/cloud.cjs +1 -1
  27. package/dist-lib/cloud.mjs +1 -1
  28. package/dist-lib/config.cjs +1 -1
  29. package/dist-lib/detox/index.cjs +2 -25
  30. package/dist-lib/dev-bundle-resolution.cjs +1 -1
  31. package/dist-lib/home-paths.cjs +1 -1
  32. package/dist-lib/host/bridge-host.cjs +324 -218
  33. package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
  34. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  35. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  36. package/dist-lib/host/replacement-module-handler.cjs +1 -1
  37. package/dist-lib/host/websocket-proxy.cjs +162 -78
  38. package/dist-lib/index.cjs +1 -1
  39. package/dist-lib/jump-to-source-babel.cjs +1 -1
  40. package/dist-lib/menu.cjs +1 -1
  41. package/dist-lib/menu.mjs +1 -1
  42. package/dist-lib/metro-fingerprint-registry.cjs +1 -1
  43. package/dist-lib/metro-fingerprint-registry.mjs +1 -1
  44. package/dist-lib/metro-production-bundle.cjs +43 -24
  45. package/dist-lib/metro-production-bundle.mjs +43 -24
  46. package/dist-lib/metro.cjs +1 -1
  47. package/dist-lib/profiles.cjs +1 -1
  48. package/dist-lib/public-brand.cjs +1 -1
  49. package/dist-lib/react-native-host-modules.cjs +1 -1
  50. package/dist-lib/react-native-host-modules.mjs +1 -1
  51. package/dist-lib/render-mode.cjs +1 -1
  52. package/dist-lib/scripts/dev-server-scanner.cjs +1 -1
  53. package/dist-lib/sdk.cjs +1 -1
  54. package/dist-lib/sdk.mjs +1 -1
  55. package/dist-lib/skills.cjs +452 -336
  56. package/dist-lib/vite.cjs +1 -1
  57. package/package.json +1 -1
  58. package/src/host/websocket-proxy.ts +197 -92
  59. package/src/metro-production-bundle.ts +55 -30
package/dist-lib/vite.cjs CHANGED
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.427 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.428 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
  let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
3
3
  "use strict";
4
4
  var __create = Object.create;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rnxsim",
3
- "version": "0.1.427",
3
+ "version": "0.1.428",
4
4
  "description": "Vite and Metro plugins, testing drivers, and SDK exports for rnx.",
5
5
  "author": "Tamagui LLC",
6
6
  "license": "SEE LICENSE IN LICENSE",
@@ -1,8 +1,19 @@
1
- import { WebSocket, WebSocketServer } from 'ws'
2
- import type { IncomingMessage } from 'http'
1
+ import { createHash } from 'crypto'
2
+ import { request as requestHttp, type IncomingMessage } from 'http'
3
+ import { request as requestHttps } from 'https'
3
4
  import type { Duplex } from 'stream'
4
5
 
5
6
  export const WEBSOCKET_PROXY_PATH = '/__websocket-proxy'
7
+ export const WEBSOCKET_PROXY_HANDSHAKE_TIMEOUT_MS = 10_000
8
+
9
+ const WEBSOCKET_PROTOCOL_TOKEN = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/
10
+ const WEBSOCKET_KEY = /^[+/0-9A-Za-z]{22}==$/
11
+
12
+ type UpgradeRejection = {
13
+ status: number
14
+ message: string
15
+ headers?: Record<string, string>
16
+ }
6
17
 
7
18
  const STRIP_UPSTREAM_HEADERS = new Set([
8
19
  'host',
@@ -17,15 +28,74 @@ const STRIP_UPSTREAM_HEADERS = new Set([
17
28
  'sec-websocket-version',
18
29
  ])
19
30
 
20
- function rejectUpgrade(socket: Duplex, status: number, message: string) {
31
+ function rejectUpgrade(
32
+ socket: Duplex,
33
+ status: number,
34
+ message: string,
35
+ headers: Record<string, string> = {},
36
+ ) {
21
37
  try {
38
+ const extraHeaders = Object.entries(headers)
39
+ .map(([name, value]) => `${name}: ${value}\r\n`)
40
+ .join('')
22
41
  socket.write(
23
- `HTTP/1.1 ${status} ${message}\r\nConnection: close\r\nContent-Type: text/plain\r\nContent-Length: ${message.length}\r\n\r\n${message}`,
42
+ `HTTP/1.1 ${status} ${message}\r\nConnection: close\r\nContent-Type: text/plain\r\nContent-Length: ${message.length}\r\n${extraHeaders}\r\n${message}`,
24
43
  )
25
44
  } catch {}
26
45
  socket.destroy()
27
46
  }
28
47
 
48
+ function hasHeaderToken(value: string | undefined, token: string): boolean {
49
+ return (value || '').split(',').some((part) => part.trim().toLowerCase() === token)
50
+ }
51
+
52
+ function hasValidProtocolOffer(value: string | undefined): boolean {
53
+ if (value === undefined) return true
54
+ if (!WEBSOCKET_PROTOCOL_TOKEN.test(value[0] || '')) return false
55
+ if (!WEBSOCKET_PROTOCOL_TOKEN.test(value[value.length - 1] || '')) return false
56
+ const protocols = new Set<string>()
57
+ for (const part of value.split(',')) {
58
+ const protocol = part.trim()
59
+ if (!WEBSOCKET_PROTOCOL_TOKEN.test(protocol) || protocols.has(protocol)) {
60
+ return false
61
+ }
62
+ protocols.add(protocol)
63
+ }
64
+ return true
65
+ }
66
+
67
+ function validateUpgradeRequest(req: IncomingMessage): UpgradeRejection | null {
68
+ if (req.method !== 'GET') {
69
+ return { status: 405, message: 'invalid websocket http method' }
70
+ }
71
+ if (
72
+ !hasHeaderToken(req.headers.connection, 'upgrade') ||
73
+ !hasHeaderToken(req.headers.upgrade, 'websocket')
74
+ ) {
75
+ return { status: 400, message: 'invalid websocket upgrade headers' }
76
+ }
77
+ const key = req.headers['sec-websocket-key']
78
+ if (
79
+ typeof key !== 'string' ||
80
+ !WEBSOCKET_KEY.test(key) ||
81
+ Buffer.from(key, 'base64').byteLength !== 16 ||
82
+ Buffer.from(key, 'base64').toString('base64') !== key
83
+ ) {
84
+ return { status: 400, message: 'invalid websocket key' }
85
+ }
86
+ if (req.headers['sec-websocket-version'] !== '13') {
87
+ return {
88
+ status: 400,
89
+ message: 'invalid websocket version',
90
+ headers: { 'Sec-WebSocket-Version': '13' },
91
+ }
92
+ }
93
+ if (!hasValidProtocolOffer(req.headers['sec-websocket-protocol'])) {
94
+ return { status: 400, message: 'invalid websocket protocol offer' }
95
+ }
96
+ return null
97
+ }
98
+
29
99
  function isSameOriginUpgrade(req: IncomingMessage): boolean {
30
100
  const origin = req.headers.origin
31
101
  const host = req.headers.host
@@ -58,66 +128,12 @@ function getDefaultWebSocketOrigin(targetUrl: URL): string {
58
128
  return origin.origin
59
129
  }
60
130
 
61
- function getRequestedProtocols(req: IncomingMessage): string[] {
62
- const header = req.headers['sec-websocket-protocol']
63
- const value = Array.isArray(header) ? header.join(',') : header || ''
64
- return value
65
- .split(',')
66
- .map((part) => part.trim())
67
- .filter(Boolean)
68
- }
69
-
70
- function safeClose(ws: WebSocket, code: number, reason: string) {
71
- if (ws.readyState === WebSocket.CLOSED || ws.readyState === WebSocket.CLOSING) {
72
- return
73
- }
74
- try {
75
- ws.close(code, reason)
76
- } catch {
77
- ws.terminate()
78
- }
79
- }
80
-
81
- function connectProxyPair(clientWs: WebSocket, upstream: WebSocket) {
82
- let closing = false
83
-
84
- const closeBoth = (
85
- source: WebSocket,
86
- target: WebSocket,
87
- code: number,
88
- reason: Buffer,
89
- ) => {
90
- if (closing) return
91
- closing = true
92
- safeClose(target, code, reason.toString())
93
- if (source.readyState === WebSocket.OPEN) {
94
- safeClose(source, code, reason.toString())
95
- }
96
- }
97
-
98
- clientWs.on('message', (data, isBinary) => {
99
- if (upstream.readyState === WebSocket.OPEN) {
100
- upstream.send(data, { binary: isBinary })
101
- }
102
- })
103
- upstream.on('message', (data, isBinary) => {
104
- if (clientWs.readyState === WebSocket.OPEN) {
105
- clientWs.send(data, { binary: isBinary })
106
- }
107
- })
108
-
109
- clientWs.on('close', (code, reason) => closeBoth(clientWs, upstream, code, reason))
110
- upstream.on('close', (code, reason) => closeBoth(upstream, clientWs, code, reason))
111
- clientWs.on('error', () => safeClose(upstream, 1011, 'proxy client error'))
112
- upstream.on('error', () => safeClose(clientWs, 1011, 'upstream websocket error'))
113
- }
114
-
115
- function createUpstreamWebSocket(
131
+ function createUpstreamHeaders(
132
+ req: IncomingMessage,
116
133
  targetUrl: URL,
117
- protocols: string[],
118
134
  headers: Record<string, string>,
119
135
  forwardOrigin: boolean,
120
- ): WebSocket {
136
+ ): Record<string, string> {
121
137
  const upstreamHeaders = { ...headers }
122
138
  const originKeys = Object.keys(upstreamHeaders).filter(
123
139
  (key) => key.toLowerCase() === 'origin',
@@ -127,11 +143,19 @@ function createUpstreamWebSocket(
127
143
  } else if (!forwardOrigin) {
128
144
  for (const key of originKeys) delete upstreamHeaders[key]
129
145
  }
130
- if (Object.keys(upstreamHeaders).length === 0)
131
- return new WebSocket(targetUrl.href, protocols)
132
- return new WebSocket(targetUrl.href, protocols, {
133
- headers: upstreamHeaders,
134
- })
146
+ upstreamHeaders.connection = 'Upgrade'
147
+ upstreamHeaders.upgrade = 'websocket'
148
+ const key = req.headers['sec-websocket-key']
149
+ if (typeof key === 'string') upstreamHeaders['sec-websocket-key'] = key
150
+ const version = req.headers['sec-websocket-version']
151
+ if (typeof version === 'string') upstreamHeaders['sec-websocket-version'] = version
152
+ const protocol = req.headers['sec-websocket-protocol']
153
+ if (typeof protocol === 'string') upstreamHeaders['sec-websocket-protocol'] = protocol
154
+ const extensions = req.headers['sec-websocket-extensions']
155
+ if (typeof extensions === 'string') {
156
+ upstreamHeaders['sec-websocket-extensions'] = extensions
157
+ }
158
+ return upstreamHeaders
135
159
  }
136
160
 
137
161
  export function isWebSocketProxyRequestUrl(rawUrl: string | undefined): boolean {
@@ -154,6 +178,16 @@ export function handleWebSocketProxyUpgrade(
154
178
  rejectUpgrade(socket, 403, 'forbidden websocket proxy origin')
155
179
  return true
156
180
  }
181
+ const upgradeRejection = validateUpgradeRequest(req)
182
+ if (upgradeRejection) {
183
+ rejectUpgrade(
184
+ socket,
185
+ upgradeRejection.status,
186
+ upgradeRejection.message,
187
+ upgradeRejection.headers,
188
+ )
189
+ return true
190
+ }
157
191
 
158
192
  let targetUrl: URL
159
193
  let headers: Record<string, string>
@@ -175,36 +209,107 @@ export function handleWebSocketProxyUpgrade(
175
209
  return true
176
210
  }
177
211
 
178
- const protocols = getRequestedProtocols(req)
179
- const upstream = createUpstreamWebSocket(targetUrl, protocols, headers, forwardOrigin)
180
- let completed = false
212
+ const upstreamHeaders = createUpstreamHeaders(req, targetUrl, headers, forwardOrigin)
213
+ let settled = false
214
+ const request = targetUrl.protocol === 'wss:' ? requestHttps : requestHttp
215
+ const upstreamRequestUrl = new URL(targetUrl)
216
+ upstreamRequestUrl.protocol = targetUrl.protocol === 'wss:' ? 'https:' : 'http:'
217
+ const upstreamRequest = request(upstreamRequestUrl, {
218
+ method: 'GET',
219
+ headers: upstreamHeaders,
220
+ })
221
+ let destroyUpstreamSocket = () => {}
222
+ upstreamRequest.once('socket', (upstreamSocket) => {
223
+ destroyUpstreamSocket = () => upstreamSocket.destroy()
224
+ })
225
+ const fail = (status: number, message: string) => {
226
+ if (settled) return
227
+ settled = true
228
+ clearTimeout(timer)
229
+ upstreamRequest.destroy()
230
+ destroyUpstreamSocket()
231
+ rejectUpgrade(socket, status, message)
232
+ }
233
+ const timer = setTimeout(() => {
234
+ fail(504, 'upstream websocket handshake timeout')
235
+ }, WEBSOCKET_PROXY_HANDSHAKE_TIMEOUT_MS)
181
236
  socket.once('close', () => {
182
- if (!completed) upstream.terminate()
237
+ if (!settled) {
238
+ settled = true
239
+ clearTimeout(timer)
240
+ }
241
+ upstreamRequest.destroy()
242
+ destroyUpstreamSocket()
183
243
  })
184
- upstream.once('open', () => {
185
- if (completed) return
186
- completed = true
187
- const selectedProtocol = upstream.protocol
188
- const proxyServer = new WebSocketServer({
189
- noServer: true,
190
- clientTracking: false,
191
- handleProtocols(requestedProtocols) {
192
- return selectedProtocol || requestedProtocols.values().next().value || false
193
- },
194
- })
195
- proxyServer.handleUpgrade(req, socket, head, (clientWs) => {
196
- connectProxyPair(clientWs, upstream)
197
- })
244
+ upstreamRequest.once('response', (response) => {
245
+ response.resume()
246
+ fail(502, 'upstream websocket rejected upgrade')
198
247
  })
199
- upstream.once('error', () => {
200
- if (completed) return
201
- completed = true
202
- rejectUpgrade(socket, 502, 'upstream websocket error')
248
+ upstreamRequest.once('error', () => {
249
+ fail(502, 'upstream websocket error')
203
250
  })
204
- upstream.once('close', () => {
205
- if (completed) return
206
- completed = true
207
- rejectUpgrade(socket, 502, 'upstream websocket closed')
251
+ upstreamRequest.once('upgrade', (response, upstreamSocket, upstreamHead) => {
252
+ if (settled) {
253
+ upstreamSocket.destroy()
254
+ return
255
+ }
256
+ const accept = response.headers['sec-websocket-accept']
257
+ const upgrade = response.headers.upgrade
258
+ const requestKey = req.headers['sec-websocket-key']
259
+ const expectedAccept =
260
+ typeof requestKey === 'string'
261
+ ? createHash('sha1')
262
+ .update(`${requestKey}258EAFA5-E914-47DA-95CA-C5AB0DC85B11`)
263
+ .digest('base64')
264
+ : ''
265
+ if (
266
+ typeof accept !== 'string' ||
267
+ accept !== expectedAccept ||
268
+ typeof upgrade !== 'string' ||
269
+ upgrade.toLowerCase() !== 'websocket'
270
+ ) {
271
+ upstreamSocket.destroy()
272
+ fail(502, 'upstream websocket handshake is invalid')
273
+ return
274
+ }
275
+ const selectedProtocol = response.headers['sec-websocket-protocol']
276
+ const requestedProtocols = (req.headers['sec-websocket-protocol'] || '')
277
+ .split(',')
278
+ .map((protocol) => protocol.trim())
279
+ if (
280
+ typeof selectedProtocol === 'string' &&
281
+ !requestedProtocols.includes(selectedProtocol)
282
+ ) {
283
+ upstreamSocket.destroy()
284
+ fail(502, 'upstream websocket selected an unrequested protocol')
285
+ return
286
+ }
287
+ settled = true
288
+ clearTimeout(timer)
289
+ const responseHeaders = [
290
+ 'HTTP/1.1 101 Switching Protocols',
291
+ 'Upgrade: websocket',
292
+ 'Connection: Upgrade',
293
+ `Sec-WebSocket-Accept: ${accept}`,
294
+ ]
295
+ if (typeof selectedProtocol === 'string') {
296
+ responseHeaders.push(`Sec-WebSocket-Protocol: ${selectedProtocol}`)
297
+ }
298
+ const selectedExtensions = response.headers['sec-websocket-extensions']
299
+ if (typeof selectedExtensions === 'string') {
300
+ responseHeaders.push(`Sec-WebSocket-Extensions: ${selectedExtensions}`)
301
+ }
302
+ // the upstream and browser used the same websocket key and extension offer,
303
+ // so their negotiated byte streams can be relayed without decoding frames.
304
+ socket.write(`${responseHeaders.join('\r\n')}\r\n\r\n`)
305
+ if (head.length > 0) upstreamSocket.write(head)
306
+ if (upstreamHead.length > 0) socket.write(upstreamHead)
307
+ socket.once('close', () => upstreamSocket.destroy())
308
+ socket.once('error', () => upstreamSocket.destroy())
309
+ upstreamSocket.once('error', () => socket.destroy())
310
+ socket.pipe(upstreamSocket)
311
+ upstreamSocket.pipe(socket)
208
312
  })
313
+ upstreamRequest.end()
209
314
  return true
210
315
  }
@@ -130,11 +130,34 @@ export function readRNXMetroModuleIdentity(
130
130
  }
131
131
 
132
132
  const version = Reflect.get(parsed, 'version')
133
- // stored artifacts from before the versioned footer carry a bare module-path
134
- // map with no version field. they are development bundles whose identity
135
- // comes from Metro's verbose names, so treat their footer as absent rather
136
- // than failing every stored replay.
137
- if (version === undefined) return null
133
+ // stored artifacts from before the versioned footer carry a bare
134
+ // `{"<moduleId>":"<path>", ...}` map with no wrapper the exact shape
135
+ // appendRNXModulePaths wrote before the footer was versioned, and the only
136
+ // writer of that shape used identitySource 'rnx-plugin'. normalize it into
137
+ // the current in-memory identity so callers get the same exact modulePaths
138
+ // map instead of falling back to name-scanning or fingerprint inference.
139
+ // validateRNXMetroModuleIdentity still detects a missing, extra, duplicate,
140
+ // or truncated module ID against the bundle's own __d() defines, so those
141
+ // legacy-map corruptions are caught there, not trusted here — it does not
142
+ // corroborate that a path or factory is itself correct.
143
+ if (version === undefined) {
144
+ const modulePaths: RNXMetroModulePathMap = Object.fromEntries(
145
+ Object.entries(parsed).map(([moduleId, modulePath]) => {
146
+ if (typeof modulePath !== 'string') {
147
+ throw new Error(`Metro module identity has an invalid path for ${moduleId}`)
148
+ }
149
+ return [moduleId, modulePath]
150
+ }),
151
+ )
152
+ return {
153
+ source: withoutMap.slice(0, footerStart),
154
+ version: RNX_METRO_MODULE_IDENTITY_VERSION,
155
+ identitySource: 'rnx-plugin',
156
+ modulePaths,
157
+ logicalSpecifiers: {},
158
+ sourceMappingUrl: detached.sourceMappingUrl,
159
+ }
160
+ }
138
161
  const identitySource = Reflect.get(parsed, 'identitySource')
139
162
  const parsedModulePaths = Reflect.get(parsed, 'modulePaths')
140
163
  const parsedLogicalSpecifiers = Reflect.get(parsed, 'logicalSpecifiers')
@@ -153,32 +176,34 @@ export function readRNXMetroModuleIdentity(
153
176
  throw new Error('Metro module identity footer is malformed')
154
177
  }
155
178
 
156
- const modulePaths: RNXMetroModulePathMap = {}
157
- for (const [moduleId, modulePath] of Object.entries(parsedModulePaths)) {
158
- if (typeof modulePath !== 'string') {
159
- throw new Error(`Metro module identity has an invalid path for ${moduleId}`)
160
- }
161
- modulePaths[moduleId] = modulePath
162
- }
163
- const logicalSpecifiers: RNXMetroLogicalSpecifierMap = {}
164
- for (const [moduleId, specifiers] of Object.entries(parsedLogicalSpecifiers)) {
165
- if (
166
- !Array.isArray(specifiers) ||
167
- specifiers.length === 0 ||
168
- !specifiers.every(
169
- (specifier) => typeof specifier === 'string' && specifier.length > 0,
179
+ const modulePaths: RNXMetroModulePathMap = Object.fromEntries(
180
+ Object.entries(parsedModulePaths).map(([moduleId, modulePath]) => {
181
+ if (typeof modulePath !== 'string') {
182
+ throw new Error(`Metro module identity has an invalid path for ${moduleId}`)
183
+ }
184
+ return [moduleId, modulePath]
185
+ }),
186
+ )
187
+ const logicalSpecifiers: RNXMetroLogicalSpecifierMap = Object.fromEntries(
188
+ Object.entries(parsedLogicalSpecifiers).map(([moduleId, specifiers]) => {
189
+ if (
190
+ !Array.isArray(specifiers) ||
191
+ specifiers.length === 0 ||
192
+ !specifiers.every(
193
+ (specifier) => typeof specifier === 'string' && specifier.length > 0,
194
+ )
195
+ ) {
196
+ throw new Error(`Metro module identity has invalid specifiers for ${moduleId}`)
197
+ }
198
+ const unique = [...new Set(specifiers)].sort((left, right) =>
199
+ left.localeCompare(right),
170
200
  )
171
- ) {
172
- throw new Error(`Metro module identity has invalid specifiers for ${moduleId}`)
173
- }
174
- const unique = [...new Set(specifiers)].sort((left, right) =>
175
- left.localeCompare(right),
176
- )
177
- if (unique.length !== specifiers.length) {
178
- throw new Error(`Metro module identity repeats a specifier for ${moduleId}`)
179
- }
180
- logicalSpecifiers[moduleId] = unique
181
- }
201
+ if (unique.length !== specifiers.length) {
202
+ throw new Error(`Metro module identity repeats a specifier for ${moduleId}`)
203
+ }
204
+ return [moduleId, unique]
205
+ }),
206
+ )
182
207
  return {
183
208
  source: withoutMap.slice(0, footerStart),
184
209
  version,