diff --git a/packages/core/bin/docs.js b/packages/core/bin/docs.js new file mode 100644 index 0000000..783d4c7 --- /dev/null +++ b/packages/core/bin/docs.js @@ -0,0 +1,12 @@ +'use strict' + +const url = product => `https://microlink.io/docs/sdk/methods/${product}.md` + +const load = async (product, fetchFn = fetch) => { + const href = url(product) + const res = await fetchFn(href, { signal: AbortSignal.timeout(10_000) }) + if (!res.ok) throw new Error(`Failed to fetch ${href} (${res.status})`) + return res.text() +} + +module.exports = { load, url } diff --git a/packages/core/bin/help.js b/packages/core/bin/help.js index 09b468d..57ddb2e 100644 --- a/packages/core/bin/help.js +++ b/packages/core/bin/help.js @@ -301,6 +301,7 @@ const commandList = Object.entries(COMMANDS) const global = `Usage ${cmd(' [options]')} ${cmd(' [options]')} +${cmd(' docs')} ${cmd('help')} ${cmd('login')} ${cmd('logout')} @@ -316,6 +317,7 @@ ${rows(CLI)} Examples ${cmd('login', 'save an API key from your account')} +${cmd('markdown docs', 'print the markdown docs page')} ${cmd('https://example.com', 'unified metadata (default)')} ${cmd( 'https://example.com --trace', @@ -348,21 +350,24 @@ ${cmd( ` const render = (name, product) => { - const usage = [] - .concat(product.usage) - .map(line => cmd(line)) - .join('\n') + const usageLines = [].concat(product.usage) + const examples = [...(product.examples ?? [])] + if (PRODUCTS[name]) { + usageLines.push(`${name} docs`) + examples.push([`${name} docs`, 'print the docs page']) + } + const usage = usageLines.map(line => cmd(line)).join('\n') const cli = product.cli ?? CLI const options = [...product.flags, ...cli] const parts = ['Usage', usage, '', gray(product.desc)] if (options.length > 0) parts.push('', 'Options', rows(options)) if (product.browser) parts.push('', 'Browser', rows(BROWSER)) if (product.note) parts.push('', gray(product.note)) - if (product.examples) { + if (examples.length > 0) { parts.push( '', 'Examples', - ...product.examples.map(([rest, comment]) => cmd(rest, comment)) + ...examples.map(([rest, comment]) => cmd(rest, comment)) ) } return parts.join('\n') + '\n' diff --git a/packages/core/bin/run.js b/packages/core/bin/run.js index 88c67c0..7178f3d 100644 --- a/packages/core/bin/run.js +++ b/packages/core/bin/run.js @@ -6,6 +6,7 @@ const spinner = require('./spinner') const parseArgv = require('./argv') const helpText = require('./help') const { asUrl } = require('./url') +const docs = require('./docs') const create = require('../src') const run = async (argvInput, host) => { @@ -86,6 +87,20 @@ const run = async (argvInput, host) => { } if (help || !target) return showHelp(command) + + if (target === 'docs') { + try { + writeLine( + stdout, + (await docs.load(command, host.fetch ?? fetch)).trimEnd() + ) + return finish(0) + } catch (error) { + writeLine(stderr, error.message) + return finish(1) + } + } + if (command !== 'search') target = asUrl(target) ?? target if (isTrace && (command === 'search' || command === 'function')) { diff --git a/packages/core/test/cli.mjs b/packages/core/test/cli.mjs index 43c4aff..f20fd99 100644 --- a/packages/core/test/cli.mjs +++ b/packages/core/test/cli.mjs @@ -23,6 +23,7 @@ test('prints help with no arguments', async t => { t.true(stdout.includes('--endpoint')) t.true(stdout.includes('login')) t.true(stdout.includes('logout')) + t.true(stdout.includes(' docs')) }) test('help command matches --help', async t => { @@ -50,6 +51,12 @@ test('prints command help for logout', async t => { t.true(stdout.includes('Remove the saved API key')) }) +test('product help includes the docs usage', async t => { + const { stdout } = await $('node', [bin, 'markdown', '--help']) + t.true(stdout.includes('markdown docs')) + t.true(stdout.includes('print the docs page')) +}) + test('prints command help for a product with no url', async t => { const { stdout } = await $('node', [bin, 'metadata']) t.true(stdout.includes('metadata ')) @@ -509,6 +516,20 @@ test('run reports unknown commands through the host', async t => { t.true(host.stderrText().includes('Unknown command')) }) +test('run docs writes the fetched markdown through the host', async t => { + const host = memoryHost({ + fetch: url => { + t.is(url, 'https://microlink.io/docs/sdk/methods/markdown.md') + return Promise.resolve({ + ok: true, + text: () => Promise.resolve('# markdown\n') + }) + } + }) + t.is(await run(['markdown', 'docs'], host), 0) + t.is(host.stdoutText().trim(), '# markdown') +}) + test('run reports missing --file through the host', async t => { const host = memoryHost() t.is(await run(['function', 'https://example.com'], host), 1) diff --git a/packages/core/test/docs.mjs b/packages/core/test/docs.mjs new file mode 100644 index 0000000..fb5726f --- /dev/null +++ b/packages/core/test/docs.mjs @@ -0,0 +1,28 @@ +import { createRequire } from 'module' +import test from 'ava' + +const require = createRequire(import.meta.url) +const { load, url } = require('../bin/docs') + +test('points at the SDK method markdown file', t => { + t.is(url('markdown'), 'https://microlink.io/docs/sdk/methods/markdown.md') +}) + +test('load fetches the markdown file', async t => { + const text = await load('markdown', (href, opts) => { + t.is(href, url('markdown')) + t.true(opts.signal instanceof AbortSignal) + return Promise.resolve({ + ok: true, + text: () => Promise.resolve('# markdown\n') + }) + }) + t.is(text, '# markdown\n') +}) + +test('load throws when the page is missing', async t => { + const error = await t.throwsAsync(() => + load('markdown', () => Promise.resolve({ ok: false, status: 404 })) + ) + t.true(error.message.includes('404')) +})