From d73c418795e79addd7837bbda04667cd0b49ece0 Mon Sep 17 00:00:00 2001 From: Ignatius Bagus Date: Wed, 31 Jul 2024 18:42:47 +0700 Subject: [PATCH] docs: parse jsdoc from exported function (#254) --- workspace/mauss/src/core/index.ts | 5 +- .../mauss/src/core/standard/index.spec.ts | 48 ++++++++++++ workspace/mauss/src/core/standard/index.ts | 24 ++++++ .../mauss/src/core/standard/unique.spec.ts | 56 -------------- workspace/mauss/src/core/standard/unique.ts | 24 ------ workspace/mauss/src/core/tsf/index.spec.ts | 2 +- workspace/mauss/src/core/tsf/index.ts | 2 +- workspace/mauss/src/web/index.ts | 3 +- workspace/mauss/src/web/query/encoder.ts | 37 --------- workspace/mauss/src/web/query/index.spec.ts | 3 +- .../src/web/query/{decoder.ts => index.ts} | 40 +++++++++- .../website/src/routes/docs.json/+server.ts | 77 +++++++++++++++++++ workspace/website/svelte.config.js | 4 + 13 files changed, 197 insertions(+), 128 deletions(-) delete mode 100644 workspace/mauss/src/core/standard/unique.spec.ts delete mode 100644 workspace/mauss/src/core/standard/unique.ts delete mode 100644 workspace/mauss/src/web/query/encoder.ts rename workspace/mauss/src/web/query/{decoder.ts => index.ts} (61%) create mode 100644 workspace/website/src/routes/docs.json/+server.ts diff --git a/workspace/mauss/src/core/index.ts b/workspace/mauss/src/core/index.ts index 1fa6dd6c..545948ba 100644 --- a/workspace/mauss/src/core/index.ts +++ b/workspace/mauss/src/core/index.ts @@ -2,7 +2,6 @@ export { curry, pipe } from './lambda/index.js'; export { debounce, immediate, throttle } from './processor/index.js'; -export { capitalize, identical, inverse, regexp, scope } from './standard/index.js'; -export { default as unique } from './standard/unique.js'; +export { capitalize, identical, inverse, regexp, scope, unique } from './standard/index.js'; -export { default as tsf } from './tsf/index.js'; +export { tsf } from './tsf/index.js'; diff --git a/workspace/mauss/src/core/standard/index.spec.ts b/workspace/mauss/src/core/standard/index.spec.ts index 02a92be5..e4726f64 100644 --- a/workspace/mauss/src/core/standard/index.spec.ts +++ b/workspace/mauss/src/core/standard/index.spec.ts @@ -6,6 +6,9 @@ const suites = { 'capitalize/': suite('std/capitalize'), 'identical/': suite('std/identical'), 'sides/': suite('std/sides'), + + 'unique/simple': suite('unique/simple'), + 'unique/object': suite('unique/object'), }; suites['capitalize/']('change one letter for one word', () => { @@ -82,4 +85,49 @@ suites['sides/']('first and last element', () => { assert.equal(std.sides([{ a: 0 }, { z: 'i' }]), { head: { a: 0 }, last: { z: 'i' } }); }); +suites['unique/simple']('make array items unique', () => { + assert.equal(std.unique([true, false, !0, !1]), [true, false]); + assert.equal(std.unique([1, 1, 2, 3, 2, 4, 5]), [1, 2, 3, 4, 5]); + assert.equal(std.unique(['a', 'a', 'b', 'c', 'b']), ['a', 'b', 'c']); + + const months = ['jan', 'feb', 'mar'] as const; + assert.equal(std.unique(months), ['jan', 'feb', 'mar']); +}); + +suites['unique/object']('make array of object unique', () => { + assert.equal( + std.unique( + [ + { id: 'ab', name: 'A' }, + { id: 'cd' }, + { id: 'ef', name: 'B' }, + { id: 'ab', name: 'C' }, + { id: 'ef', name: 'D' }, + ], + 'id', + ), + [{ id: 'ab', name: 'A' }, { id: 'cd' }, { id: 'ef', name: 'B' }], + ); + + assert.equal( + std.unique( + [ + { id: 'ab', name: { first: 'A' } }, + { id: 'cd', name: { first: 'B' } }, + { id: 'ef', name: { first: 'B' } }, + { id: 'ab', name: { first: 'C' } }, + { id: 'ef', name: { first: 'D' } }, + { id: 'hi', name: { last: 'wa' } }, + ], + 'name.first', + ), + [ + { id: 'ab', name: { first: 'A' } }, + { id: 'cd', name: { first: 'B' } }, + { id: 'ab', name: { first: 'C' } }, + { id: 'ef', name: { first: 'D' } }, + ], + ); +}); + Object.values(suites).forEach((v) => v.run()); diff --git a/workspace/mauss/src/core/standard/index.ts b/workspace/mauss/src/core/standard/index.ts index c02ab6d2..67927329 100644 --- a/workspace/mauss/src/core/standard/index.ts +++ b/workspace/mauss/src/core/standard/index.ts @@ -1,4 +1,6 @@ +import type { IndexSignature } from '../../typings/aliases.js'; import type { AnyFunction, Reverse } from '../../typings/helpers.js'; +import type { Paths } from '../../typings/prototypes.js'; interface CapitalizeOptions { /** only capitalize the very first letter */ @@ -72,3 +74,25 @@ export function scope(fn: () => T) { export function sides(x: T): Record<'head' | 'last', T[0]> { return { head: x[0], last: x[x.length - 1] }; } + +/** + * unique - transform an array to a set and back to array + * @param array items to be inspected + * @returns duplicate-free version of the array input + */ +export function unique< + Inferred extends Record, + Identifier extends Paths, +>(array: readonly Inferred[], key: string & Identifier): Inferred[]; +export function unique(array: readonly T[]): T[]; +export function unique(array: readonly T[], key?: string & I): T[] { + if (!key || typeof array[0] !== 'object') return [...new Set(array)]; + + const trail = key.split('.'); + const filtered = new Map(); + for (const item of array as Record[]) { + const value: any = trail.reduce((r, p) => (r || {})[p], item); + if (value && !filtered.has(value)) filtered.set(value, item); + } + return [...filtered.values()]; +} diff --git a/workspace/mauss/src/core/standard/unique.spec.ts b/workspace/mauss/src/core/standard/unique.spec.ts deleted file mode 100644 index 6a67d2dd..00000000 --- a/workspace/mauss/src/core/standard/unique.spec.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { suite } from 'uvu'; -import * as assert from 'uvu/assert'; - -import unique from './unique.js'; - -const suites = { - 'simple/': suite('unique/simple'), - 'object/': suite('unique/object'), -}; - -suites['simple/']('make array items unique', () => { - assert.equal(unique([true, false, !0, !1]), [true, false]); - assert.equal(unique([1, 1, 2, 3, 2, 4, 5]), [1, 2, 3, 4, 5]); - assert.equal(unique(['a', 'a', 'b', 'c', 'b']), ['a', 'b', 'c']); - - const months = ['jan', 'feb', 'mar'] as const; - assert.equal(unique(months), ['jan', 'feb', 'mar']); -}); - -suites['object/']('make array of object unique', () => { - assert.equal( - unique( - [ - { id: 'ab', name: 'A' }, - { id: 'cd' }, - { id: 'ef', name: 'B' }, - { id: 'ab', name: 'C' }, - { id: 'ef', name: 'D' }, - ], - 'id', - ), - [{ id: 'ab', name: 'A' }, { id: 'cd' }, { id: 'ef', name: 'B' }], - ); - - assert.equal( - unique( - [ - { id: 'ab', name: { first: 'A' } }, - { id: 'cd', name: { first: 'B' } }, - { id: 'ef', name: { first: 'B' } }, - { id: 'ab', name: { first: 'C' } }, - { id: 'ef', name: { first: 'D' } }, - { id: 'hi', name: { last: 'wa' } }, - ], - 'name.first', - ), - [ - { id: 'ab', name: { first: 'A' } }, - { id: 'cd', name: { first: 'B' } }, - { id: 'ab', name: { first: 'C' } }, - { id: 'ef', name: { first: 'D' } }, - ], - ); -}); - -Object.values(suites).forEach((v) => v.run()); diff --git a/workspace/mauss/src/core/standard/unique.ts b/workspace/mauss/src/core/standard/unique.ts deleted file mode 100644 index e690d222..00000000 --- a/workspace/mauss/src/core/standard/unique.ts +++ /dev/null @@ -1,24 +0,0 @@ -import type { IndexSignature } from '../../typings/aliases.js'; -import type { Paths } from '../../typings/prototypes.js'; - -/** - * unique - transform an array to a set and back to array - * @param array items to be inspected - * @returns duplicate-free version of the array input - */ -export default function unique< - Inferred extends Record, - Identifier extends Paths, ->(array: readonly Inferred[], key: string & Identifier): Inferred[]; -export default function unique(array: readonly T[]): T[]; -export default function unique(array: readonly T[], key?: string & I): T[] { - if (!key || typeof array[0] !== 'object') return [...new Set(array)]; - - const trail = key.split('.'); - const filtered = new Map(); - for (const item of array as Record[]) { - const value: any = trail.reduce((r, p) => (r || {})[p], item); - if (value && !filtered.has(value)) filtered.set(value, item); - } - return [...filtered.values()]; -} diff --git a/workspace/mauss/src/core/tsf/index.spec.ts b/workspace/mauss/src/core/tsf/index.spec.ts index e2dd5ff6..034d66b9 100644 --- a/workspace/mauss/src/core/tsf/index.spec.ts +++ b/workspace/mauss/src/core/tsf/index.spec.ts @@ -1,6 +1,6 @@ import { test } from 'uvu'; import * as assert from 'uvu/assert'; -import tsf from './index.js'; +import { tsf } from './index.js'; test.skip('throws on nested braces', () => { assert.throws(() => tsf('/{foo/{bar}}' as string)); diff --git a/workspace/mauss/src/core/tsf/index.ts b/workspace/mauss/src/core/tsf/index.ts index 4f9c2c5d..f40372c1 100644 --- a/workspace/mauss/src/core/tsf/index.ts +++ b/workspace/mauss/src/core/tsf/index.ts @@ -1,7 +1,7 @@ import { UnaryFunction } from '../../typings/helpers.js'; type Parse = T extends `${string}{${infer P}}${infer R}` ? P | Parse : never; -export default function tsf( +export function tsf( template: Input extends `${string}{}${string}` ? 'Empty braces are not allowed in template' : Input extends diff --git a/workspace/mauss/src/web/index.ts b/workspace/mauss/src/web/index.ts index ec917d78..11cf556e 100644 --- a/workspace/mauss/src/web/index.ts +++ b/workspace/mauss/src/web/index.ts @@ -1,4 +1,3 @@ export { cookies } from './cookies/index.js'; export { clipboard } from './navigator/clipboard.js'; -export { default as qsd } from './query/decoder.js'; -export { default as qse } from './query/encoder.js'; +export { qsd, qse } from './query/index.js'; diff --git a/workspace/mauss/src/web/query/encoder.ts b/workspace/mauss/src/web/query/encoder.ts deleted file mode 100644 index 60f27d4a..00000000 --- a/workspace/mauss/src/web/query/encoder.ts +++ /dev/null @@ -1,37 +0,0 @@ -import type { Nullish, Primitives } from '../../typings/aliases.js'; - -type BoundValues = Nullish | Primitives; - -/** - * qse - query string encoder - * @param bound object with key-value pair to be updated in the URL - * @param transformer function that is applied to the final string if it exists - * @returns final query string - */ -export default function qse( - bound: T[keyof T] extends BoundValues | readonly BoundValues[] ? T : never, - transformer = (final: string) => `?${final}`, -): string { - const enc = encodeURIComponent; - - let final = ''; - for (let [k, v] of Object.entries(bound)) { - if (v == null || (typeof v === 'string' && !(v = v.trim()))) continue; - if ((k = enc(k)) && final) final += '&'; - - if (Array.isArray(v)) { - let pointer = 0; - while (pointer < v.length) { - if (pointer) final += '&'; - const item = v[pointer++]; - if (item == null) continue; - final += `${k}=${enc(item)}`; - } - continue; - } - - final += `${k}=${enc(v as Primitives)}`; - } - - return final ? transformer(final) : final; -} diff --git a/workspace/mauss/src/web/query/index.spec.ts b/workspace/mauss/src/web/query/index.spec.ts index d8de9033..4f1e9276 100644 --- a/workspace/mauss/src/web/query/index.spec.ts +++ b/workspace/mauss/src/web/query/index.spec.ts @@ -1,8 +1,7 @@ import { suite } from 'uvu'; import * as assert from 'uvu/assert'; -import qsd from './decoder.js'; -import qse from './encoder.js'; +import { qsd, qse } from './index.js'; const suites = { 'decoder/': suite('query/decoder'), diff --git a/workspace/mauss/src/web/query/decoder.ts b/workspace/mauss/src/web/query/index.ts similarity index 61% rename from workspace/mauss/src/web/query/decoder.ts rename to workspace/mauss/src/web/query/index.ts index 8414ad4d..55a600f7 100644 --- a/workspace/mauss/src/web/query/decoder.ts +++ b/workspace/mauss/src/web/query/index.ts @@ -1,4 +1,4 @@ -import type { IndexSignature, Primitives } from '../../typings/aliases.js'; +import type { IndexSignature, Nullish, Primitives } from '../../typings/aliases.js'; import type { AlsoArray } from '../../typings/extenders.js'; import type { Intersection } from '../../typings/helpers.js'; import type { Flatten } from '../../typings/prototypes.js'; @@ -29,7 +29,7 @@ type QueryDecoder = string extends Query * @param qs query string of a URL with or without the leading `?` * @returns mapped object of decoded query string */ -export default function qsd(qs: Q) { +export function qsd(qs: Q) { if (qs[0] === '?') qs = qs.slice(1) as Q; if (!qs) return {} as QueryDecoder; @@ -52,3 +52,39 @@ export default function qsd(qs: Q) { } return dqs as QueryDecoder; } + +type BoundValues = Nullish | Primitives; + +/** + * qse - query string encoder + * @param bound object with key-value pair to be updated in the URL + * @param transformer function that is applied to the final string if it exists + * @returns final query string + */ +export function qse( + bound: T[keyof T] extends BoundValues | readonly BoundValues[] ? T : never, + transformer = (final: string) => `?${final}`, +): string { + const enc = encodeURIComponent; + + let final = ''; + for (let [k, v] of Object.entries(bound)) { + if (v == null || (typeof v === 'string' && !(v = v.trim()))) continue; + if ((k = enc(k)) && final) final += '&'; + + if (Array.isArray(v)) { + let pointer = 0; + while (pointer < v.length) { + if (pointer) final += '&'; + const item = v[pointer++]; + if (item == null) continue; + final += `${k}=${enc(item)}`; + } + continue; + } + + final += `${k}=${enc(v as Primitives)}`; + } + + return final ? transformer(final) : final; +} diff --git a/workspace/website/src/routes/docs.json/+server.ts b/workspace/website/src/routes/docs.json/+server.ts new file mode 100644 index 00000000..ddb8bc9c --- /dev/null +++ b/workspace/website/src/routes/docs.json/+server.ts @@ -0,0 +1,77 @@ +import { json } from '@sveltejs/kit'; +import ts from 'typescript'; +import { exports } from '$mauss/package.json'; + +export const prerender = true; + +export interface Schema { + [modules: string]: Array<{ + name: string; + docs: string[]; + signature: string; + }>; +} + +export async function GET() { + const program = ts.createProgram( + Object.keys(exports).flatMap((m) => { + if (m.slice(2).includes('.')) return []; + return `../mauss/src/${m.slice(2) || 'core'}/index.ts`; + }), + {}, + ); + + const schema: Schema = {}; + for (const exp in exports) { + if (exp.slice(2).includes('.')) continue; + const module = exp.slice(2) || 'core'; + const source = program.getSourceFile(`../mauss/src/${module}/index.ts`); + if (!source) continue; + + const tc = program.getTypeChecker(); + schema[module] = []; + + ts.forEachChild(source, (node) => { + if (ts.isExportDeclaration(node)) { + const symbols = tc.getExportsOfModule(tc.getSymbolAtLocation(node.moduleSpecifier)); + symbols.forEach((symbol) => { + const decl = symbol.valueDeclaration || symbol.declarations[0]; + if (!ts.isFunctionDeclaration(decl)) return; + + schema[module].push({ + name: symbol.getName(), + docs: parse.jsdoc(decl), + get signature() { + const signature = tc.getSignatureFromDeclaration(decl); + return `function ${this.name}${tc.signatureToString(signature)}`; + }, + }); + }); + } else if ( + ts.isFunctionDeclaration(node) && + node.modifiers?.some((mod) => mod.kind === ts.SyntaxKind.ExportKeyword) + ) { + schema[module].push({ + name: node.name.text, + docs: parse.jsdoc(node), + get signature() { + const signature = tc.getSignatureFromDeclaration(node); + return `function ${this.name}${tc.signatureToString(signature)}`; + }, + }); + } + }); + } + + return json(schema); +} + +const parse = { + jsdoc(declaration: ts.FunctionDeclaration) { + return ts.getJSDocCommentsAndTags(declaration).flatMap((doc) => { + const lines = doc.getText().slice(1, -1).split('\n'); + const clean = lines.map((l) => l.replace(/^[\s*]+|[\s*]+$/g, '')); + return clean.filter((l) => l); + }); + }, +}; diff --git a/workspace/website/svelte.config.js b/workspace/website/svelte.config.js index be772143..39cbc924 100644 --- a/workspace/website/svelte.config.js +++ b/workspace/website/svelte.config.js @@ -10,6 +10,10 @@ const config = { fallback: '404.html', }), + alias: { + $mauss: '../mauss', + }, + paths: { base: process.argv.includes('dev') ? '' : process.env.BASE_PATH, },