microlink.io 0.0.5 → 0.1.1
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/bin/help.txt +7 -1
- package/bin/index.js +247 -14
- package/package.json +2 -3
- package/src/index.js +25 -11
package/bin/help.txt
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
Usage
|
|
2
|
+
$ microlink <url> [options]
|
|
2
3
|
$ microlink <product> <url|query> [options]
|
|
3
4
|
|
|
4
5
|
Products
|
|
5
|
-
metadata Unified metadata (title, description, image, ...)
|
|
6
|
+
metadata Unified metadata (title, description, image, ...); default
|
|
6
7
|
logo Brand logo of the site (--square prefers the square variant)
|
|
7
8
|
markdown Page content as Markdown
|
|
8
9
|
html Page content as HTML
|
|
@@ -31,12 +32,17 @@ Options
|
|
|
31
32
|
--header, -H Extra request header as 'Name: value' (repeatable)
|
|
32
33
|
--data JSON data rules for the extract command
|
|
33
34
|
--file Path to the code file for the function command
|
|
35
|
+
--trace Print request & response payload (API key masked)
|
|
36
|
+
--trace-full Same as --trace, including the full API key
|
|
34
37
|
--help Show this help
|
|
35
38
|
|
|
36
39
|
Any other flag is passed as an option to the product, e.g. --fullPage,
|
|
37
40
|
--device 'iPhone 11', --waitUntil networkidle0, --selector article.
|
|
38
41
|
|
|
39
42
|
Examples
|
|
43
|
+
$ microlink https://example.com
|
|
44
|
+
$ microlink https://example.com --trace
|
|
45
|
+
$ microlink https://example.com --trace-full
|
|
40
46
|
$ microlink markdown https://example.com
|
|
41
47
|
$ microlink screenshot https://example.com --fullPage
|
|
42
48
|
$ microlink logo https://github.com --square
|
package/bin/index.js
CHANGED
|
@@ -1,13 +1,219 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
'use strict'
|
|
3
3
|
|
|
4
|
+
const { styleText } = require('node:util')
|
|
4
5
|
const { readFileSync } = require('fs')
|
|
5
6
|
const path = require('path')
|
|
6
|
-
const jsome = require('jsome')
|
|
7
7
|
const mri = require('mri')
|
|
8
8
|
|
|
9
9
|
const create = require('../src')
|
|
10
10
|
|
|
11
|
+
const SHOW_CURSOR = '\u001b[?25h'
|
|
12
|
+
const HIDE_CURSOR = '\u001b[?25l'
|
|
13
|
+
const CLEAR_LINE = '\r\u001b[K'
|
|
14
|
+
const FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']
|
|
15
|
+
|
|
16
|
+
const gray = str => styleText('gray', str)
|
|
17
|
+
const white = str => styleText('white', str)
|
|
18
|
+
const green = str => styleText('green', str)
|
|
19
|
+
const red = str => styleText('red', str)
|
|
20
|
+
const label = (text, color) =>
|
|
21
|
+
styleText(['inverse', 'bold', color], ` ${text.toUpperCase()} `)
|
|
22
|
+
const keyValue = (key, value) => key + ' ' + gray(value)
|
|
23
|
+
|
|
24
|
+
const prettyMs = ms => {
|
|
25
|
+
if (!Number.isFinite(ms)) return 'unknown'
|
|
26
|
+
const sign = ms < 0 ? '-' : ''
|
|
27
|
+
let n = Math.abs(ms)
|
|
28
|
+
if (n < 1000) return `${sign}${Math.round(n)}ms`
|
|
29
|
+
n /= 1000
|
|
30
|
+
if (n < 60) return `${sign}${n.toFixed(1).replace(/\.0$/, '')}s`
|
|
31
|
+
const hours = Math.floor(n / 3600)
|
|
32
|
+
n %= 3600
|
|
33
|
+
const mins = Math.floor(n / 60)
|
|
34
|
+
const secs = (n % 60).toFixed(1).replace(/\.0$/, '')
|
|
35
|
+
if (hours) return `${sign}${hours}h ${mins}m ${secs}s`
|
|
36
|
+
return secs === '0' ? `${sign}${mins}m` : `${sign}${mins}m ${secs}s`
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const prettyBytes = n => {
|
|
40
|
+
if (!Number.isFinite(n) || n < 1000) return `${Math.round(n || 0)} B`
|
|
41
|
+
if (n < 1e6) {
|
|
42
|
+
const val = n / 1000
|
|
43
|
+
return `${
|
|
44
|
+
val >= 100 ? Math.round(val) : val.toFixed(1).replace(/\.0$/, '')
|
|
45
|
+
} kB`
|
|
46
|
+
}
|
|
47
|
+
return `${(n / 1e6).toFixed(1).replace(/\.0$/, '')} MB`
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const toPlainHeaders = headers => {
|
|
51
|
+
if (!headers) return {}
|
|
52
|
+
if (typeof headers.entries === 'function') {
|
|
53
|
+
return Object.fromEntries(headers.entries())
|
|
54
|
+
}
|
|
55
|
+
return headers
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const humanizeApiKey = apiKey => `${String(apiKey).slice(0, 5)}…`
|
|
59
|
+
|
|
60
|
+
const quote = str =>
|
|
61
|
+
gray('"') + white(JSON.stringify(str).slice(1, -1)) + gray('"')
|
|
62
|
+
|
|
63
|
+
const printPretty = (value, indent = 0) => {
|
|
64
|
+
if (value === null) return white('null')
|
|
65
|
+
if (typeof value === 'string') return quote(value)
|
|
66
|
+
if (typeof value !== 'object') return white(String(value))
|
|
67
|
+
|
|
68
|
+
const isArray = Array.isArray(value)
|
|
69
|
+
const keys = isArray ? value : Object.keys(value)
|
|
70
|
+
if (keys.length === 0) return gray(isArray ? '[]' : '{}')
|
|
71
|
+
|
|
72
|
+
const pad = ' '.repeat(indent)
|
|
73
|
+
const inner = ' '.repeat(indent + 1)
|
|
74
|
+
const open = gray(isArray ? '[' : '{')
|
|
75
|
+
const close = gray(isArray ? ']' : '}')
|
|
76
|
+
const lines = keys.map(key => {
|
|
77
|
+
if (isArray) return inner + printPretty(key, indent + 1)
|
|
78
|
+
const name = /^[A-Za-z_$][\w$]*$/.test(key) ? white(key) : quote(key)
|
|
79
|
+
return inner + name + gray(':') + ' ' + printPretty(value[key], indent + 1)
|
|
80
|
+
})
|
|
81
|
+
return open + '\n' + lines.join(gray(',') + '\n') + '\n' + pad + close
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const printJson = payload => {
|
|
85
|
+
console.log(
|
|
86
|
+
process.stdout.hasColors?.()
|
|
87
|
+
? printPretty(payload)
|
|
88
|
+
: JSON.stringify(payload, null, 2)
|
|
89
|
+
)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const tracePayload = ({
|
|
93
|
+
requestUrl,
|
|
94
|
+
requestOptions = {},
|
|
95
|
+
response,
|
|
96
|
+
full = false
|
|
97
|
+
}) => {
|
|
98
|
+
const rest = { ...requestOptions }
|
|
99
|
+
delete rest.responseType
|
|
100
|
+
const headers = { ...rest.headers }
|
|
101
|
+
if (!full && headers['x-api-key']) {
|
|
102
|
+
headers['x-api-key'] = humanizeApiKey(headers['x-api-key'])
|
|
103
|
+
}
|
|
104
|
+
return {
|
|
105
|
+
request: { url: requestUrl, ...rest, headers },
|
|
106
|
+
response: {
|
|
107
|
+
...response,
|
|
108
|
+
headers: toPlainHeaders(response?.headers)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const shouldSpin = () =>
|
|
114
|
+
!process.env.NO_COLOR &&
|
|
115
|
+
process.env.FORCE_COLOR !== '0' &&
|
|
116
|
+
Boolean(process.stdout?.hasColors?.())
|
|
117
|
+
|
|
118
|
+
const spinner = () => {
|
|
119
|
+
const now = Date.now()
|
|
120
|
+
let i = 0
|
|
121
|
+
let timer
|
|
122
|
+
const draw = () => {
|
|
123
|
+
process.stderr.write(
|
|
124
|
+
`${CLEAR_LINE}${FRAMES[i++ % FRAMES.length]} ${prettyMs(
|
|
125
|
+
Date.now() - now
|
|
126
|
+
)}`
|
|
127
|
+
)
|
|
128
|
+
}
|
|
129
|
+
return {
|
|
130
|
+
start () {
|
|
131
|
+
process.stderr.write(HIDE_CURSOR)
|
|
132
|
+
draw()
|
|
133
|
+
process.on('SIGINT', () => {
|
|
134
|
+
process.stderr.write(CLEAR_LINE + SHOW_CURSOR)
|
|
135
|
+
process.exit(130)
|
|
136
|
+
})
|
|
137
|
+
timer = setInterval(draw, 50)
|
|
138
|
+
},
|
|
139
|
+
stop () {
|
|
140
|
+
clearInterval(timer)
|
|
141
|
+
process.stderr.write(CLEAR_LINE + SHOW_CURSOR)
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const printFooter = ({ duration, response }) => {
|
|
147
|
+
const headers = toPlainHeaders(response?.headers)
|
|
148
|
+
const time = prettyMs(duration)
|
|
149
|
+
const size = Number(headers['content-length']) || 0
|
|
150
|
+
const serverTiming = headers['server-timing']
|
|
151
|
+
const id = headers['x-request-id']
|
|
152
|
+
const edgeCacheStatus = headers['cf-cache-status']
|
|
153
|
+
const unifiedCacheStatus = headers['x-cache-status']
|
|
154
|
+
const cacheStatus =
|
|
155
|
+
unifiedCacheStatus === 'MISS' && edgeCacheStatus === 'HIT'
|
|
156
|
+
? edgeCacheStatus
|
|
157
|
+
: unifiedCacheStatus
|
|
158
|
+
const timestamp = Number(headers['x-timestamp'])
|
|
159
|
+
const ttl = Number(headers['x-cache-ttl'])
|
|
160
|
+
const expires = timestamp + ttl - Date.now()
|
|
161
|
+
const expiredAt =
|
|
162
|
+
cacheStatus === 'HIT' && Number.isFinite(expires)
|
|
163
|
+
? `(${prettyMs(expires)})`
|
|
164
|
+
: ''
|
|
165
|
+
const fetchMode = headers['x-fetch-mode']
|
|
166
|
+
const fetchTime = fetchMode && `(${headers['x-fetch-time']})`
|
|
167
|
+
const uri = response?.url
|
|
168
|
+
|
|
169
|
+
if (process.stdout.isTTY) console.error()
|
|
170
|
+
console.error(
|
|
171
|
+
label('success', 'green'),
|
|
172
|
+
gray(`${prettyBytes(size)} in ${time}`)
|
|
173
|
+
)
|
|
174
|
+
console.error()
|
|
175
|
+
|
|
176
|
+
if (serverTiming) {
|
|
177
|
+
console.error(' ', keyValue(green('timing'), serverTiming))
|
|
178
|
+
}
|
|
179
|
+
if (cacheStatus) {
|
|
180
|
+
console.error(
|
|
181
|
+
' ',
|
|
182
|
+
keyValue(green('cache'), `${cacheStatus} ${gray(expiredAt)}`.trim())
|
|
183
|
+
)
|
|
184
|
+
}
|
|
185
|
+
if (fetchMode) {
|
|
186
|
+
console.error(
|
|
187
|
+
' ',
|
|
188
|
+
keyValue(green('mode'), `${fetchMode} ${gray(fetchTime)}`.trim())
|
|
189
|
+
)
|
|
190
|
+
}
|
|
191
|
+
if (uri) console.error(' ', keyValue(green('uri'), uri))
|
|
192
|
+
if (id) console.error(' ', keyValue(green('id'), id))
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const printFail = error => {
|
|
196
|
+
if (process.stdout.isTTY) console.error()
|
|
197
|
+
console.error(
|
|
198
|
+
label(error.status || 'fail', 'red'),
|
|
199
|
+
gray(String(error.message).replace(`${error.code}, `, ''))
|
|
200
|
+
)
|
|
201
|
+
console.error()
|
|
202
|
+
const id = error.headers?.['x-request-id']
|
|
203
|
+
if (id) console.error(' ', keyValue(red('id'), id))
|
|
204
|
+
if (error.url) console.error(' ', keyValue(red('uri'), error.url))
|
|
205
|
+
if (error.code) {
|
|
206
|
+
console.error(
|
|
207
|
+
' ',
|
|
208
|
+
keyValue(
|
|
209
|
+
red('code'),
|
|
210
|
+
`${error.code}${error.statusCode ? ` (${error.statusCode})` : ''}`
|
|
211
|
+
)
|
|
212
|
+
)
|
|
213
|
+
}
|
|
214
|
+
if (error.more) console.error(' ', keyValue(red('more'), error.more))
|
|
215
|
+
}
|
|
216
|
+
|
|
11
217
|
const showHelp = () => {
|
|
12
218
|
console.log(readFileSync(path.join(__dirname, 'help.txt'), 'utf8'))
|
|
13
219
|
process.exit(0)
|
|
@@ -27,10 +233,11 @@ const parseHeaders = input => {
|
|
|
27
233
|
|
|
28
234
|
const argv = mri(process.argv.slice(2), {
|
|
29
235
|
alias: { H: 'header' },
|
|
236
|
+
boolean: ['trace', 'trace-full'],
|
|
30
237
|
string: ['header', 'api-key', 'data', 'file']
|
|
31
238
|
})
|
|
32
239
|
|
|
33
|
-
|
|
240
|
+
let {
|
|
34
241
|
_: [command, target],
|
|
35
242
|
header,
|
|
36
243
|
help,
|
|
@@ -38,18 +245,35 @@ const {
|
|
|
38
245
|
file,
|
|
39
246
|
'api-key': apiKeyFlag,
|
|
40
247
|
apiKey: apiKeyCamel,
|
|
248
|
+
trace,
|
|
249
|
+
'trace-full': traceFull,
|
|
41
250
|
...flags
|
|
42
251
|
} = argv
|
|
43
252
|
|
|
253
|
+
const isTrace = trace || traceFull
|
|
254
|
+
|
|
44
255
|
if (help || !command) showHelp()
|
|
45
256
|
|
|
46
257
|
const apiKey = apiKeyFlag || apiKeyCamel || process.env.MICROLINK_API_KEY
|
|
47
258
|
const client = create(apiKey ? { apiKey } : {})
|
|
48
259
|
|
|
49
260
|
if (typeof client[command] !== 'function') {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
261
|
+
if (!target && URL.canParse(command)) {
|
|
262
|
+
target = command
|
|
263
|
+
command = 'metadata'
|
|
264
|
+
} else {
|
|
265
|
+
console.error(
|
|
266
|
+
`Unknown command \`${command}\`. Run \`microlink --help\` to see the available commands.`
|
|
267
|
+
)
|
|
268
|
+
process.exit(1)
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (
|
|
273
|
+
isTrace &&
|
|
274
|
+
(command === 'search' || command === 'function' || command === 'run')
|
|
275
|
+
) {
|
|
276
|
+
console.error(`\`--trace\` is not supported for \`${command}\`.`)
|
|
53
277
|
process.exit(1)
|
|
54
278
|
}
|
|
55
279
|
|
|
@@ -68,14 +292,23 @@ const invoke = () => {
|
|
|
68
292
|
return client[command](target, options)
|
|
69
293
|
}
|
|
70
294
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
295
|
+
const spin = !isTrace && shouldSpin() ? spinner() : null
|
|
296
|
+
|
|
297
|
+
;(async () => {
|
|
298
|
+
spin?.start()
|
|
299
|
+
const started = Date.now()
|
|
300
|
+
try {
|
|
301
|
+
const result = await invoke()
|
|
302
|
+
const duration = Date.now() - started
|
|
303
|
+
spin?.stop()
|
|
304
|
+
if (isTrace) printJson(tracePayload({ ...client.last, full: traceFull }))
|
|
305
|
+
else if (typeof result === 'string') console.log(result)
|
|
306
|
+
else printJson({ status: 'success', data: result })
|
|
307
|
+
if (!isTrace) printFooter({ duration, response: client.last.response })
|
|
76
308
|
process.exit(0)
|
|
77
|
-
})
|
|
78
|
-
|
|
79
|
-
|
|
309
|
+
} catch (error) {
|
|
310
|
+
spin?.stop()
|
|
311
|
+
printFail(error)
|
|
80
312
|
process.exit(1)
|
|
81
|
-
}
|
|
313
|
+
}
|
|
314
|
+
})()
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "microlink.io",
|
|
3
3
|
"description": "The Microlink API organized into products, each returning a direct result",
|
|
4
4
|
"homepage": "https://github.com/microlinkhq/microlink",
|
|
5
|
-
"version": "0.
|
|
5
|
+
"version": "0.1.1",
|
|
6
6
|
"types": "./src/index.d.ts",
|
|
7
7
|
"main": "./src/index.js",
|
|
8
8
|
"exports": {
|
|
@@ -46,7 +46,6 @@
|
|
|
46
46
|
"@microlink/function": "0.3.0",
|
|
47
47
|
"@microlink/google": "1.0.6",
|
|
48
48
|
"@microlink/mql": "0.18.0",
|
|
49
|
-
"jsome": "~2.5.0",
|
|
50
49
|
"mri": "~1.2.0"
|
|
51
50
|
},
|
|
52
51
|
"devDependencies": {
|
|
@@ -77,5 +76,5 @@
|
|
|
77
76
|
},
|
|
78
77
|
"directory": "test"
|
|
79
78
|
},
|
|
80
|
-
"gitHead": "
|
|
79
|
+
"gitHead": "a12fcd2a7bc918e741a71b50fd2cd93876f4ca8f"
|
|
81
80
|
}
|
package/src/index.js
CHANGED
|
@@ -41,6 +41,17 @@ const LIGHTHOUSE_KEYS = ['onlyCategories', 'onlyAudits', 'skipAudits', 'output']
|
|
|
41
41
|
const isEmpty = obj => Object.keys(obj).length === 0
|
|
42
42
|
|
|
43
43
|
const create = (ctx = {}) => {
|
|
44
|
+
const last = {}
|
|
45
|
+
const request = (...args) => {
|
|
46
|
+
const [requestUrl, requestOptions] = mql.getApiUrl(...args)
|
|
47
|
+
last.requestUrl = requestUrl
|
|
48
|
+
last.requestOptions = requestOptions
|
|
49
|
+
return mql(...args).then(result => {
|
|
50
|
+
last.response = result.response
|
|
51
|
+
return result
|
|
52
|
+
})
|
|
53
|
+
}
|
|
54
|
+
|
|
44
55
|
/**
|
|
45
56
|
* Split the single options bag into the three destinations mql has:
|
|
46
57
|
* `got.headers` (HTTP layer, 3rd arg), `sub` (capability nested keys)
|
|
@@ -61,7 +72,7 @@ const create = (ctx = {}) => {
|
|
|
61
72
|
|
|
62
73
|
const content = field => (url, options) => {
|
|
63
74
|
const { top, sub, got } = route(options, CONTENT_KEYS)
|
|
64
|
-
return
|
|
75
|
+
return request(
|
|
65
76
|
url,
|
|
66
77
|
{ ...top, meta: false, data: { [field]: { attr: field, ...sub } } },
|
|
67
78
|
got
|
|
@@ -70,7 +81,7 @@ const create = (ctx = {}) => {
|
|
|
70
81
|
|
|
71
82
|
const collection = (field, rule) => (url, options) => {
|
|
72
83
|
const { top, sub, got } = route(options, COLLECTION_KEYS)
|
|
73
|
-
return
|
|
84
|
+
return request(
|
|
74
85
|
url,
|
|
75
86
|
{ ...top, meta: false, data: { [field]: { ...rule, ...sub } } },
|
|
76
87
|
got
|
|
@@ -79,7 +90,7 @@ const create = (ctx = {}) => {
|
|
|
79
90
|
|
|
80
91
|
const capability = (field, nested) => (url, options) => {
|
|
81
92
|
const { top, sub, got } = route(options, nested)
|
|
82
|
-
return
|
|
93
|
+
return request(
|
|
83
94
|
url,
|
|
84
95
|
{ ...top, meta: false, [field]: isEmpty(sub) ? true : sub },
|
|
85
96
|
got
|
|
@@ -89,7 +100,7 @@ const create = (ctx = {}) => {
|
|
|
89
100
|
/* Primary media detection (`data.video` / `data.audio`). */
|
|
90
101
|
const media = field => (url, options) => {
|
|
91
102
|
const { top, got } = route(options)
|
|
92
|
-
return
|
|
103
|
+
return request(url, { ...top, meta: false, [field]: true }, got).then(
|
|
93
104
|
({ data }) => data[field]
|
|
94
105
|
)
|
|
95
106
|
}
|
|
@@ -101,14 +112,14 @@ const create = (ctx = {}) => {
|
|
|
101
112
|
return fn(code, top, got)(url)
|
|
102
113
|
}
|
|
103
114
|
|
|
104
|
-
|
|
115
|
+
const client = {
|
|
105
116
|
metadata: (url, options) => {
|
|
106
117
|
const { top, got } = route(options)
|
|
107
|
-
return
|
|
118
|
+
return request(url, top, got).then(({ data }) => data)
|
|
108
119
|
},
|
|
109
120
|
logo: (url, options) => {
|
|
110
121
|
const { top, sub, got } = route(options, LOGO_KEYS)
|
|
111
|
-
return
|
|
122
|
+
return request(
|
|
112
123
|
url,
|
|
113
124
|
{ ...top, meta: isEmpty(sub) ? true : { logo: sub } },
|
|
114
125
|
got
|
|
@@ -142,7 +153,7 @@ const create = (ctx = {}) => {
|
|
|
142
153
|
}),
|
|
143
154
|
extract: (url, rules, options) => {
|
|
144
155
|
const { top, got } = route(options)
|
|
145
|
-
return
|
|
156
|
+
return request(url, { ...top, meta: false, data: rules }, got).then(
|
|
146
157
|
({ data }) => data
|
|
147
158
|
)
|
|
148
159
|
},
|
|
@@ -150,7 +161,7 @@ const create = (ctx = {}) => {
|
|
|
150
161
|
pdf: capability('pdf', PDF_KEYS),
|
|
151
162
|
embed: (url, options) => {
|
|
152
163
|
const { top, sub, got } = route(options, EMBED_KEYS)
|
|
153
|
-
return
|
|
164
|
+
return request(
|
|
154
165
|
url,
|
|
155
166
|
{ ...top, meta: false, iframe: isEmpty(sub) ? true : sub },
|
|
156
167
|
got
|
|
@@ -158,7 +169,7 @@ const create = (ctx = {}) => {
|
|
|
158
169
|
},
|
|
159
170
|
technologies: (url, options) => {
|
|
160
171
|
const { top, got } = route(options)
|
|
161
|
-
return
|
|
172
|
+
return request(
|
|
162
173
|
url,
|
|
163
174
|
{
|
|
164
175
|
...top,
|
|
@@ -170,7 +181,7 @@ const create = (ctx = {}) => {
|
|
|
170
181
|
},
|
|
171
182
|
lighthouse: (url, options) => {
|
|
172
183
|
const { top, sub, got } = route(options, LIGHTHOUSE_KEYS)
|
|
173
|
-
return
|
|
184
|
+
return request(
|
|
174
185
|
url,
|
|
175
186
|
{
|
|
176
187
|
...top,
|
|
@@ -190,6 +201,9 @@ const create = (ctx = {}) => {
|
|
|
190
201
|
function: run,
|
|
191
202
|
run
|
|
192
203
|
}
|
|
204
|
+
|
|
205
|
+
Object.defineProperty(client, 'last', { value: last })
|
|
206
|
+
return client
|
|
193
207
|
}
|
|
194
208
|
|
|
195
209
|
module.exports = create
|