microlink.io 0.1.1 → 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.
package/bin/help.js ADDED
@@ -0,0 +1,334 @@
1
+ 'use strict'
2
+
3
+ const { gray, white } = require('./style')
4
+
5
+ const col = (name, desc) => ` ${white(name.padEnd(21))} ${gray(desc)}`
6
+
7
+ const cmd = (rest, comment) =>
8
+ `${comment ? ` ${gray(`# ${comment}`)}\n` : ''} ${white(
9
+ 'microlink'
10
+ )} ${gray(rest)}`
11
+
12
+ const rows = items => items.map(([name, desc]) => col(name, desc)).join('\n')
13
+
14
+ const CLI = [
15
+ ['--api-key', 'Microlink API key (defaults to MICROLINK_API_KEY env)'],
16
+ ['--endpoint', 'Microlink API endpoint'],
17
+ ['--header, -H', "Extra request header as 'Name: value' (repeatable)"],
18
+ [
19
+ '--http.header.<name>',
20
+ 'HTTP request header (e.g. --http.header.authorization)'
21
+ ],
22
+ ['--trace', 'Print request & response payload (API key masked)'],
23
+ ['--trace-full', 'Same as --trace, including the full API key'],
24
+ ['--help', 'Show this help']
25
+ ]
26
+
27
+ const CLI_NO_TRACE = CLI.filter(
28
+ ([name]) => name !== '--trace' && name !== '--trace-full'
29
+ )
30
+
31
+ const BROWSER = [
32
+ ['--adblock', 'Block ads and trackers'],
33
+ ['--animations', 'Enable CSS animations'],
34
+ ['--cacheKey', 'Custom cache key'],
35
+ ['--click', 'CSS selector(s) to click before capture'],
36
+ ['--colorScheme', 'Color scheme: no-preference, light, dark'],
37
+ ['--device', "Emulate a device (e.g. 'iPhone 11')"],
38
+ ['--filename', 'Suggested download filename'],
39
+ ['--filter', 'Pick response fields'],
40
+ ['--force', 'Bypass the cache'],
41
+ ['--javascript', 'Enable or disable JavaScript'],
42
+ ['--mediaType', 'Emulate media: screen, print'],
43
+ ['--modules', 'Inject ES module URLs'],
44
+ ['--prerender', 'Prerender: auto, true, false'],
45
+ ['--proxy', 'Proxy URL or country'],
46
+ ['--retry', 'Retry count'],
47
+ ['--scripts', 'Inject script URLs'],
48
+ ['--scroll', 'CSS selector to scroll into view'],
49
+ ['--staleTtl', 'Stale-while-revalidate TTL'],
50
+ ['--styles', 'Inject stylesheet URLs'],
51
+ ['--timeout', 'Request timeout'],
52
+ ['--ttl', 'Cache TTL'],
53
+ ['--viewport', 'Viewport as JSON (width, height, ...)'],
54
+ ['--waitForSelector', 'Wait until a CSS selector matches'],
55
+ ['--waitForTimeout', 'Wait for a duration before continuing'],
56
+ ['--waitUntil', 'auto, load, domcontentloaded, networkidle0, networkidle2']
57
+ ]
58
+
59
+ const CONTENT = [
60
+ ['--selector', 'CSS selector to scope the extraction'],
61
+ ['--selectorAll', 'CSS selector(s) matching many nodes'],
62
+ ['--type', 'Cast the extracted value (url, image, ...)']
63
+ ]
64
+
65
+ const COLLECTION = [
66
+ ['--selector', 'CSS selector (single node)'],
67
+ ['--selectorAll', 'CSS selector(s) matching many nodes'],
68
+ ['--attr', 'Attribute to read (href, src, ...)'],
69
+ ['--type', 'Cast each value (url, image, email, ...)']
70
+ ]
71
+
72
+ const ALIAS = { run: 'function' }
73
+
74
+ const content = (name, desc) => ({
75
+ usage: `${name} <url> [options]`,
76
+ desc,
77
+ flags: CONTENT,
78
+ browser: true,
79
+ examples: [[`${name} https://example.com`, desc]]
80
+ })
81
+
82
+ const collection = (name, desc) => ({
83
+ usage: `${name} <url> [options]`,
84
+ desc,
85
+ flags: COLLECTION,
86
+ browser: true,
87
+ examples: [[`${name} https://example.com`, desc]]
88
+ })
89
+
90
+ const PRODUCTS = {
91
+ metadata: {
92
+ usage: ['metadata <url> [options]', '<url> [options]'],
93
+ desc: 'Unified metadata (title, description, image, ...); default',
94
+ flags: [
95
+ ['--palette', 'Also extract dominant colors from images'],
96
+ ['--meta', 'Include metadata fields, or false to skip']
97
+ ],
98
+ browser: true,
99
+ examples: [
100
+ ['https://example.com', 'unified metadata (default)'],
101
+ [
102
+ 'https://example.com --trace',
103
+ 'print request & response, API key masked'
104
+ ]
105
+ ]
106
+ },
107
+ logo: {
108
+ usage: 'logo <url> [options]',
109
+ desc: 'Brand logo of the site (--square prefers the square variant)',
110
+ flags: [
111
+ ['--square', 'Prefer the square logo variant'],
112
+ ['--palette', 'Also extract dominant colors']
113
+ ],
114
+ browser: true,
115
+ examples: [['logo https://github.com --square', 'square brand logo']]
116
+ },
117
+ markdown: content('markdown', 'Page content as Markdown'),
118
+ html: content('html', 'Page content as HTML'),
119
+ text: content('text', 'Page content as plain text'),
120
+ video: {
121
+ usage: 'video <url> [options]',
122
+ desc: 'Primary video of the page (returns the asset object)',
123
+ flags: [['--meta', 'Include metadata fields, or false to skip']],
124
+ browser: true,
125
+ examples: [['video https://example.com', 'primary video asset']]
126
+ },
127
+ audio: {
128
+ usage: 'audio <url> [options]',
129
+ desc: 'Primary audio of the page (returns the asset object)',
130
+ flags: [['--meta', 'Include metadata fields, or false to skip']],
131
+ browser: true,
132
+ examples: [['audio https://example.com', 'primary audio asset']]
133
+ },
134
+ emails: collection('emails', 'Every email address present on the page'),
135
+ links: collection('links', 'Every absolute link URL on the page'),
136
+ images: collection('images', 'Every absolute image URL on the page'),
137
+ videos: collection('videos', 'Every absolute video URL on the page'),
138
+ audios: collection('audios', 'Every absolute audio URL on the page'),
139
+ extract: {
140
+ usage: 'extract <url> --data <json> [options]',
141
+ desc: 'Custom MQL data rules',
142
+ flags: [['--data', 'JSON data rules (e.g. --data \'{"field":{...}}\')']],
143
+ browser: true,
144
+ examples: [
145
+ [
146
+ 'extract https://microlink.io --data \'{"image":{"selector":"meta[property=og:image]","attr":"content","type":"image"}}\'',
147
+ 'extract og:image via MQL'
148
+ ]
149
+ ]
150
+ },
151
+ screenshot: {
152
+ usage: 'screenshot <url> [options]',
153
+ desc: 'Take a screenshot (returns the asset object)',
154
+ flags: [
155
+ ['--fullPage', 'Capture the full scrollable page'],
156
+ ['--type', 'Image format: png, jpeg'],
157
+ ['--element', 'CSS selector of the element to capture'],
158
+ ['--omitBackground', 'Transparent background (png)'],
159
+ ['--optimizeForSpeed', 'Faster encode, larger file'],
160
+ ['--overlay', 'Browser chrome overlay as JSON'],
161
+ ['--codeScheme', 'Syntax theme for code pages'],
162
+ ['--animated', 'Animated screenshot (GIF/MP4)'],
163
+ ['--palette', 'Also extract dominant colors'],
164
+ ['--quality', 'JPEG quality (0–100)']
165
+ ],
166
+ browser: true,
167
+ examples: [
168
+ ['screenshot https://example.com --fullPage', 'full-page screenshot']
169
+ ]
170
+ },
171
+ pdf: {
172
+ usage: 'pdf <url> [options]',
173
+ desc: 'Generate a PDF (returns the asset object)',
174
+ flags: [
175
+ ['--format', 'Page format: Letter, Legal, A4, ...'],
176
+ ['--margin', "Margin (e.g. '0.5cm' or JSON)"],
177
+ ['--scale', 'Scale (0.1–2)'],
178
+ ['--landscape', 'Landscape orientation'],
179
+ ['--pageRanges', "Pages to print (e.g. '1-3')"],
180
+ ['--width', 'Page width'],
181
+ ['--height', 'Page height'],
182
+ ['--printBackground', 'Print background graphics']
183
+ ],
184
+ browser: true,
185
+ examples: [['pdf https://example.com --format A4', 'A4 PDF']]
186
+ },
187
+ embed: {
188
+ usage: 'embed <url> [options]',
189
+ desc: 'oEmbed-style embeddable iframe ({ html, scripts })',
190
+ flags: [
191
+ ['--maxWidth', 'Maximum iframe width'],
192
+ ['--maxHeight', 'Maximum iframe height']
193
+ ],
194
+ browser: true,
195
+ examples: [['embed https://example.com', 'embeddable iframe']]
196
+ },
197
+ technologies: {
198
+ usage: 'technologies <url> [options]',
199
+ desc: 'Detect the tech stack behind the site',
200
+ flags: [],
201
+ browser: true,
202
+ examples: [['technologies https://example.com', 'detect the tech stack']]
203
+ },
204
+ lighthouse: {
205
+ usage: 'lighthouse <url> [options]',
206
+ desc: 'Run a Lighthouse report',
207
+ flags: [
208
+ ['--onlyCategories', 'Limit to these categories'],
209
+ ['--onlyAudits', 'Limit to these audits'],
210
+ ['--skipAudits', 'Skip these audits'],
211
+ ['--output', 'Report format: json, html, csv']
212
+ ],
213
+ browser: true,
214
+ examples: [['lighthouse https://example.com', 'run a Lighthouse report']]
215
+ },
216
+ search: {
217
+ usage: 'search <query> [options]',
218
+ desc: 'Google as structured data (query instead of url)',
219
+ flags: [
220
+ [
221
+ '--type',
222
+ 'news, images, videos, places, maps, shopping, scholar, patents, autocomplete'
223
+ ],
224
+ ['--limit', 'Maximum number of results'],
225
+ ['--location', 'Country or locale (e.g. es)'],
226
+ ['--period', 'Recency: hour, day, week, month, year'],
227
+ ['--timeout', 'Request timeout']
228
+ ],
229
+ cli: CLI_NO_TRACE,
230
+ examples: [
231
+ [
232
+ 'search "best coffee" --limit 10 --location es',
233
+ 'Google results in Spain, limit 10'
234
+ ],
235
+ [
236
+ 'search "open source llm" --type news --period week',
237
+ 'news results from the past week'
238
+ ]
239
+ ]
240
+ },
241
+ function: {
242
+ usage: 'function <url> --file <path> [options]',
243
+ desc: 'Run code remotely with browser access',
244
+ flags: [['--file', 'Path to the code file']],
245
+ browser: true,
246
+ cli: CLI_NO_TRACE,
247
+ note: 'Extra flags are injected as variables in the function scope.',
248
+ examples: [
249
+ [
250
+ 'function https://example.com --file ./fn.js',
251
+ 'run ./fn.js with browser access'
252
+ ]
253
+ ]
254
+ }
255
+ }
256
+
257
+ const productList = Object.entries(PRODUCTS)
258
+ .map(([name, product]) => col(name, product.desc))
259
+ .join('\n')
260
+
261
+ const global = `Usage
262
+ ${cmd('<url> [options]')}
263
+ ${cmd('<product> <url|query> [options]')}
264
+
265
+ Products
266
+ ${productList}
267
+
268
+ Options
269
+ ${rows(CLI)}
270
+
271
+ Examples
272
+ ${cmd('https://example.com', 'unified metadata (default)')}
273
+ ${cmd(
274
+ 'https://example.com --trace',
275
+ 'print request & response, API key masked'
276
+ )}
277
+ ${cmd(
278
+ 'https://example.com --trace-full',
279
+ 'same as --trace, including the full API key'
280
+ )}
281
+ ${cmd('markdown https://example.com', 'page content as Markdown')}
282
+ ${cmd('screenshot https://example.com --fullPage', 'full-page screenshot')}
283
+ ${cmd('logo https://github.com --square', 'square brand logo')}
284
+ ${cmd('links https://example.com', 'every absolute link on the page')}
285
+ ${cmd(
286
+ 'search "best coffee" --limit 10 --location es',
287
+ 'Google results in Spain, limit 10'
288
+ )}
289
+ ${cmd(
290
+ 'search "open source llm" --type news --period week',
291
+ 'news results from the past week'
292
+ )}
293
+ ${cmd(
294
+ 'extract https://microlink.io --data \'{"image":{"selector":"meta[property=og:image]","attr":"content","type":"image"}}\'',
295
+ 'extract og:image via MQL'
296
+ )}
297
+ ${cmd(
298
+ 'function https://example.com --file ./fn.js',
299
+ 'run ./fn.js with browser access'
300
+ )}
301
+ `
302
+
303
+ const render = (name, product) => {
304
+ const usage = []
305
+ .concat(product.usage)
306
+ .map(line => cmd(line))
307
+ .join('\n')
308
+ const cli = product.cli ?? CLI
309
+ const options = [...product.flags, ...cli]
310
+ const parts = [
311
+ 'Usage',
312
+ usage,
313
+ '',
314
+ gray(product.desc),
315
+ '',
316
+ 'Options',
317
+ rows(options)
318
+ ]
319
+ if (product.browser) parts.push('', 'Browser', rows(BROWSER))
320
+ if (product.note) parts.push('', gray(product.note))
321
+ if (product.examples) {
322
+ parts.push(
323
+ '',
324
+ 'Examples',
325
+ ...product.examples.map(([rest, comment]) => cmd(rest, comment))
326
+ )
327
+ }
328
+ return parts.join('\n') + '\n'
329
+ }
330
+
331
+ module.exports = command => {
332
+ const name = ALIAS[command] ?? command
333
+ return PRODUCTS[name] ? render(name, PRODUCTS[name]) : global
334
+ }
package/bin/index.js CHANGED
@@ -1,10 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
  'use strict'
3
3
 
4
- const { styleText } = require('node:util')
5
4
  const { readFileSync } = require('fs')
6
5
  const path = require('path')
7
6
  const mri = require('mri')
7
+ const helpText = require('./help')
8
+ const { gray, white, green, red, styleText } = require('./style')
8
9
 
9
10
  const create = require('../src')
10
11
 
@@ -13,10 +14,6 @@ const HIDE_CURSOR = '\u001b[?25l'
13
14
  const CLEAR_LINE = '\r\u001b[K'
14
15
  const FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']
15
16
 
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
17
  const label = (text, color) =>
21
18
  styleText(['inverse', 'bold', color], ` ${text.toUpperCase()} `)
22
19
  const keyValue = (key, value) => key + ' ' + gray(value)
@@ -98,8 +95,10 @@ const tracePayload = ({
98
95
  const rest = { ...requestOptions }
99
96
  delete rest.responseType
100
97
  const headers = { ...rest.headers }
101
- if (!full && headers['x-api-key']) {
102
- headers['x-api-key'] = humanizeApiKey(headers['x-api-key'])
98
+ if (!full) {
99
+ for (const key of ['x-api-key', 'authorization', 'cookie']) {
100
+ if (headers[key]) headers[key] = humanizeApiKey(headers[key])
101
+ }
103
102
  }
104
103
  return {
105
104
  request: { url: requestUrl, ...rest, headers },
@@ -214,11 +213,13 @@ const printFail = error => {
214
213
  if (error.more) console.error(' ', keyValue(red('more'), error.more))
215
214
  }
216
215
 
217
- const showHelp = () => {
218
- console.log(readFileSync(path.join(__dirname, 'help.txt'), 'utf8'))
216
+ const showHelp = command => {
217
+ console.log(helpText(command).trimEnd())
219
218
  process.exit(0)
220
219
  }
221
220
 
221
+ const HTTP_HEADER = 'http.header.'
222
+
222
223
  const parseHeaders = input => {
223
224
  const headers = {}
224
225
  for (const item of [].concat(input ?? [])) {
@@ -231,10 +232,24 @@ const parseHeaders = input => {
231
232
  return headers
232
233
  }
233
234
 
235
+ const takeHttpHeaders = flags => {
236
+ const headers = {}
237
+ for (const key of Object.keys(flags)) {
238
+ if (!key.startsWith(HTTP_HEADER)) continue
239
+ let value = flags[key]
240
+ delete flags[key]
241
+ if (Array.isArray(value)) value = value.at(-1)
242
+ if (typeof value !== 'string' && typeof value !== 'number') continue
243
+ const name = key.slice(HTTP_HEADER.length).toLowerCase()
244
+ if (name) headers[name] = String(value)
245
+ }
246
+ return headers
247
+ }
248
+
234
249
  const argv = mri(process.argv.slice(2), {
235
250
  alias: { H: 'header' },
236
- boolean: ['trace', 'trace-full'],
237
- string: ['header', 'api-key', 'data', 'file']
251
+ boolean: ['trace', 'trace-full', 'help'],
252
+ string: ['header', 'api-key', 'data', 'file', 'endpoint']
238
253
  })
239
254
 
240
255
  let {
@@ -245,6 +260,7 @@ let {
245
260
  file,
246
261
  'api-key': apiKeyFlag,
247
262
  apiKey: apiKeyCamel,
263
+ endpoint: endpointFlag,
248
264
  trace,
249
265
  'trace-full': traceFull,
250
266
  ...flags
@@ -252,15 +268,21 @@ let {
252
268
 
253
269
  const isTrace = trace || traceFull
254
270
 
255
- if (help || !command) showHelp()
271
+ if (!command) showHelp()
256
272
 
257
273
  const apiKey = apiKeyFlag || apiKeyCamel || process.env.MICROLINK_API_KEY
258
- const client = create(apiKey ? { apiKey } : {})
274
+ const endpoint = endpointFlag
275
+ const client = create({
276
+ ...(apiKey && { apiKey }),
277
+ ...(endpoint && { endpoint })
278
+ })
259
279
 
260
280
  if (typeof client[command] !== 'function') {
261
281
  if (!target && URL.canParse(command)) {
262
282
  target = command
263
283
  command = 'metadata'
284
+ } else if (help) {
285
+ showHelp()
264
286
  } else {
265
287
  console.error(
266
288
  `Unknown command \`${command}\`. Run \`microlink --help\` to see the available commands.`
@@ -269,6 +291,8 @@ if (typeof client[command] !== 'function') {
269
291
  }
270
292
  }
271
293
 
294
+ if (help || !target) showHelp(command)
295
+
272
296
  if (
273
297
  isTrace &&
274
298
  (command === 'search' || command === 'function' || command === 'run')
@@ -278,7 +302,7 @@ if (
278
302
  }
279
303
 
280
304
  const options = { ...flags }
281
- const headers = parseHeaders(header)
305
+ const headers = { ...takeHttpHeaders(options), ...parseHeaders(header) }
282
306
  if (Object.keys(headers).length > 0) options.headers = headers
283
307
 
284
308
  const invoke = () => {
package/bin/style.js ADDED
@@ -0,0 +1,10 @@
1
+ 'use strict'
2
+
3
+ const { styleText } = require('node:util')
4
+
5
+ const gray = str => styleText('gray', str)
6
+ const white = str => styleText('white', str)
7
+ const green = str => styleText('green', str)
8
+ const red = str => styleText('red', str)
9
+
10
+ module.exports = { gray, white, green, red, styleText }
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.1.1",
5
+ "version": "0.2.0",
6
6
  "types": "./src/index.d.ts",
7
7
  "main": "./src/index.js",
8
8
  "exports": {
@@ -76,5 +76,5 @@
76
76
  },
77
77
  "directory": "test"
78
78
  },
79
- "gitHead": "a12fcd2a7bc918e741a71b50fd2cd93876f4ca8f"
79
+ "gitHead": "3766602b11599a8284e4aa31815d409e0ffa0788"
80
80
  }
package/bin/help.txt DELETED
@@ -1,53 +0,0 @@
1
- Usage
2
- $ microlink <url> [options]
3
- $ microlink <product> <url|query> [options]
4
-
5
- Products
6
- metadata Unified metadata (title, description, image, ...); default
7
- logo Brand logo of the site (--square prefers the square variant)
8
- markdown Page content as Markdown
9
- html Page content as HTML
10
- text Page content as plain text
11
- video Primary video of the page (returns the asset object)
12
- audio Primary audio of the page (returns the asset object)
13
- emails Every email address present on the page
14
- links Every absolute link URL on the page
15
- images Every absolute image URL on the page
16
- videos Every absolute video URL on the page
17
- audios Every absolute audio URL on the page
18
- extract Custom MQL data rules (--data '{"field":{...}}')
19
- screenshot Take a screenshot (returns the asset object)
20
- pdf Generate a PDF (returns the asset object)
21
- embed oEmbed-style embeddable iframe ({ html, scripts })
22
- technologies Detect the tech stack behind the site
23
- lighthouse Run a Lighthouse report
24
- search Google as structured data (query instead of url); --type
25
- routes to news, images, videos, places, maps, shopping,
26
- scholar, patents or autocomplete
27
- function Run code remotely with browser access (--file ./fn.js);
28
- extra flags are injected as variables in the function scope
29
-
30
- Options
31
- --api-key Microlink API key (defaults to MICROLINK_API_KEY env)
32
- --header, -H Extra request header as 'Name: value' (repeatable)
33
- --data JSON data rules for the extract command
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
37
- --help Show this help
38
-
39
- Any other flag is passed as an option to the product, e.g. --fullPage,
40
- --device 'iPhone 11', --waitUntil networkidle0, --selector article.
41
-
42
- Examples
43
- $ microlink https://example.com
44
- $ microlink https://example.com --trace
45
- $ microlink https://example.com --trace-full
46
- $ microlink markdown https://example.com
47
- $ microlink screenshot https://example.com --fullPage
48
- $ microlink logo https://github.com --square
49
- $ microlink links https://example.com
50
- $ microlink search "best coffee" --limit 10 --location es
51
- $ microlink search "open source llm" --type news --period week
52
- $ microlink extract https://microlink.io --data '{"image":{"selector":"meta[property=og:image]","attr":"content","type":"image"}}'
53
- $ microlink function https://example.com --file ./fn.js