Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,5 @@ jobs:
run: pnpm install --dangerously-allow-all-builds
- name: Test
run: npm test
env:
MICROLINK_API_KEY: ${{ secrets.MICROLINK_API_KEY }}
6 changes: 4 additions & 2 deletions .npmrc
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
unsafe-perm=true
audit=false
fund=false
loglevel=error
save-prefix=~
shrinkwrap=false
save=false
strict-peer-deps=false
48 changes: 34 additions & 14 deletions src/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,44 @@
import { MqlOptions } from '@microlink/mql'
import { Page, HTTPResponse } from 'puppeteer-core'

export type FunctionResponse = {
isFulfilled: true,
isRejected: false,
export type FunctionProfiling = {
phases?: {
install?: number
build?: number
spawn?: number
run?: number
total?: number
}
cpu?: number
memory?: number
size?: number
}

export type FunctionFulfilled = {
isFulfilled: true
value: any
profiling: FunctionProfiling
logging: Record<string, unknown>
}

export type FunctionArgs = {
page: object;
response: object;
url: string;
export type FunctionRejected = {
isFulfilled: false
value: {
name: string
message: string
[key: string]: unknown
}
profiling: FunctionProfiling
logging: Record<string, unknown>
}

export type FunctionInput = (args: {
page: Page;
response: HTTPResponse;
[key: string]: any;
}) => any;
export type FunctionResponse = FunctionFulfilled | FunctionRejected

export type FunctionInput = (args: {
page: Page
response: HTTPResponse
[key: string]: any
}) => any

declare function microlinkFunction(
fn: FunctionInput,
Expand All @@ -28,6 +48,6 @@ declare function microlinkFunction(
url: string,
mqlOpts?: MqlOptions,
gotOpts?: object
) => Promise<FunctionResponse>;
) => Promise<FunctionResponse>

export default microlinkFunction;
export default microlinkFunction
128 changes: 115 additions & 13 deletions test/index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,39 +4,141 @@ import microlink from '@microlink/function'
import test from 'ava'

const ENDPOINT = 'https://api.microlink.io'
const PRO_ENDPOINT = 'https://pro.microlink.io'
const TARGET_URL = 'https://edge-ping.vercel.app'

test('interact with the page', async t => {
const getTitle = ({ page }) => page.title()
const API_KEY = process.env.MICROLINK_API_KEY
const PRO_GOT_OPTS = API_KEY ? { headers: { 'x-api-key': API_KEY } } : undefined
const proTest = API_KEY ? test : test.skip

const myFn = microlink(getTitle, { endpoint: ENDPOINT })
test('return a computed value', async t => {
const fn = () => 40 + 2

const result = await myFn('https://example.com', { force: true })
const myFn = microlink(fn, { endpoint: ENDPOINT })

const result = await myFn(TARGET_URL, { force: true })

t.true(result.isFulfilled)
t.is(result.value, 'Example Domain')
t.is(result.value, 42)
t.truthy(result.profiling)
})

test('interact with the response', async t => {
const getTitle = ({ response }) => response.status()
test('return a complex value', async t => {
const fn = () => ({ title: 'hello', count: 42 })

const myFn = microlink(getTitle, { endpoint: ENDPOINT })
const myFn = microlink(fn, { endpoint: ENDPOINT })

const result = await myFn('https://example.com', { force: true })
const result = await myFn(TARGET_URL, { force: true })

t.true(result.isFulfilled)
t.is(result.value, 200)
t.deepEqual(result.value, { title: 'hello', count: 42 })
t.truthy(result.profiling)
})

test('interact with user specific parameters', async t => {
const getTitle = ({ greetings }) => greetings
const getGreetings = ({ greetings }) => greetings

const myFn = microlink(getTitle, { endpoint: ENDPOINT })
const myFn = microlink(getGreetings, { endpoint: ENDPOINT })

const result = await myFn('https://example.com', {
const result = await myFn(TARGET_URL, {
greetings: 'hello world',
force: true
})

t.true(result.isFulfilled)
t.is(result.value, 'hello world')
t.truthy(result.profiling)
})

test('async function execution', async t => {
const fn = async () => {
const value = await Promise.resolve(42)
return value * 2
}

const myFn = microlink(fn, { endpoint: ENDPOINT })

const result = await myFn(TARGET_URL, { force: true })

t.true(result.isFulfilled)
t.is(result.value, 84)
t.truthy(result.profiling)
})

test('profiling includes phase timings', async t => {
const fn = () => 420

const myFn = microlink(fn, { endpoint: ENDPOINT })

const result = await myFn(TARGET_URL, { force: true })

t.true(result.isFulfilled)
t.truthy(result.profiling)
t.truthy(result.profiling.phases)
t.is(typeof result.profiling.phases.total, 'number')
})

test('handle execution errors', async t => {
const fn = () => { throw new Error('test error') }

const myFn = microlink(fn, { endpoint: ENDPOINT })

const result = await myFn(TARGET_URL, { force: true })

t.false(result.isFulfilled)
t.is(result.value.name, 'Error')
t.is(result.value.message, 'test error')
t.truthy(result.profiling)
})

test('turn non-Error throws into errors', async t => {
const fn = () => { throw 'oh no' } // eslint-disable-line

const myFn = microlink(fn, { endpoint: ENDPOINT })

const result = await myFn(TARGET_URL, { force: true })

t.false(result.isFulfilled)
t.is(result.value.name, 'NonError')
t.truthy(result.profiling)
})

proTest('interact with a page', async t => {
const fn = ({ page }) => page.title()

const myFn = microlink(fn, { endpoint: PRO_ENDPOINT }, PRO_GOT_OPTS)

const result = await myFn(TARGET_URL, { force: true })

t.true(result.isFulfilled)
t.is(typeof result.value, 'string')
t.truthy(result.profiling)
})

proTest('evaluate javascript on the page', async t => {
const fn = ({ page }) => page.evaluate("performance.getEntriesByType('resource').length")

const myFn = microlink(fn, { endpoint: PRO_ENDPOINT }, PRO_GOT_OPTS)

const result = await myFn(TARGET_URL, { force: true })

t.true(result.isFulfilled)
t.is(typeof result.value, 'number')
t.truthy(result.profiling)
})

proTest('require an external dependency', async t => {
const fn = () => {
const cheerio = require('cheerio')
const $ = cheerio.load('<h1>Hello</h1>')
return $('h1').text()
}

const myFn = microlink(fn, { endpoint: PRO_ENDPOINT }, PRO_GOT_OPTS)

const result = await myFn(TARGET_URL, { force: true })

t.true(result.isFulfilled)
t.is(result.value, 'Hello')
t.truthy(result.profiling)
})
22 changes: 19 additions & 3 deletions test/index.test-d.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,32 @@
import microlinkFn from '@microlink/function'

/** definition */
/** response shape */

{
const fn = microlinkFn(({ page }) => page.title())
const data = await fn('https://microlink.io', { meta: false })
console.log(data.value)
console.log(data.isFulfilled)
console.log(data.isRejected)
console.log(data.profiling)
console.log(data.logging)
}

/** interaction */
/** discriminated union */

{
const fn = microlinkFn(({ page }) => page.title())
const data = await fn('https://microlink.io')
if (data.isFulfilled) {
const value: any = data.value
console.log(value)
} else {
const name: string = data.value.name
const message: string = data.value.message
console.log(name, message)
}
}

/** definition */

microlinkFn(() => document.getElementsByTagName('*').length)
microlinkFn(({ page }) => page.title())
Expand Down