From 1f4c708185e5f1e8313c491bdeba2fadd2d9309a Mon Sep 17 00:00:00 2001 From: Kiko Beats Date: Fri, 22 May 2026 17:00:55 +0200 Subject: [PATCH 1/2] feat: profiling --- .npmrc | 6 +++-- src/index.d.ts | 48 ++++++++++++++++++++++++++----------- test/index.mjs | 56 ++++++++++++++++++++++++++++++++++---------- test/index.test-d.ts | 22 ++++++++++++++--- 4 files changed, 100 insertions(+), 32 deletions(-) diff --git a/.npmrc b/.npmrc index e03e941..47bda9f 100644 --- a/.npmrc +++ b/.npmrc @@ -1,4 +1,6 @@ -unsafe-perm=true +audit=false +fund=false +loglevel=error save-prefix=~ -shrinkwrap=false save=false +strict-peer-deps=false diff --git a/src/index.d.ts b/src/index.d.ts index c462d45..d98dcc3 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -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 } -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 } -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, @@ -28,6 +48,6 @@ declare function microlinkFunction( url: string, mqlOpts?: MqlOptions, gotOpts?: object -) => Promise; +) => Promise -export default microlinkFunction; +export default microlinkFunction diff --git a/test/index.mjs b/test/index.mjs index 7d980dd..66cc133 100644 --- a/test/index.mjs +++ b/test/index.mjs @@ -4,39 +4,69 @@ import microlink from '@microlink/function' import test from 'ava' const ENDPOINT = 'https://api.microlink.io' +const TARGET_URL = 'https://edge-ping.vercel.app' -test('interact with the page', async t => { - const getTitle = ({ page }) => page.title() +test('return a computed value', async t => { + const fn = () => 40 + 2 - 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, '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('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) }) diff --git a/test/index.test-d.ts b/test/index.test-d.ts index 72b0d69..4f3c8a0 100644 --- a/test/index.test-d.ts +++ b/test/index.test-d.ts @@ -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()) From ac2bf72a9fdbdfd48dae773b24cba258d6f23b66 Mon Sep 17 00:00:00 2001 From: Kiko Beats Date: Fri, 22 May 2026 18:11:39 +0200 Subject: [PATCH 2/2] test: more tests --- .github/workflows/test.yml | 2 ++ test/index.mjs | 72 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 65dc5e4..ff7308d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -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 }} diff --git a/test/index.mjs b/test/index.mjs index 66cc133..99094ec 100644 --- a/test/index.mjs +++ b/test/index.mjs @@ -4,8 +4,13 @@ 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' +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 + test('return a computed value', async t => { const fn = () => 40 + 2 @@ -45,6 +50,21 @@ test('interact with user specific parameters', async t => { 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 @@ -70,3 +90,55 @@ test('handle execution errors', async t => { 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('

Hello

') + 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) +})