Skip to content

Commit

Permalink
docs: parse jsdoc from exported function (#254)
Browse files Browse the repository at this point in the history
  • Loading branch information
ignatiusmb authored Jul 31, 2024
1 parent 641df79 commit d73c418
Show file tree
Hide file tree
Showing 13 changed files with 197 additions and 128 deletions.
5 changes: 2 additions & 3 deletions workspace/mauss/src/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
48 changes: 48 additions & 0 deletions workspace/mauss/src/core/standard/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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());
24 changes: 24 additions & 0 deletions workspace/mauss/src/core/standard/index.ts
Original file line number Diff line number Diff line change
@@ -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 */
Expand Down Expand Up @@ -72,3 +74,25 @@ export function scope<T>(fn: () => T) {
export function sides<T extends string | any[]>(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<IndexSignature, any>,
Identifier extends Paths<Inferred>,
>(array: readonly Inferred[], key: string & Identifier): Inferred[];
export function unique<T>(array: readonly T[]): T[];
export function unique<T, I>(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<string, any>();
for (const item of array as Record<IndexSignature, any>[]) {
const value: any = trail.reduce((r, p) => (r || {})[p], item);
if (value && !filtered.has(value)) filtered.set(value, item);
}
return [...filtered.values()];
}
56 changes: 0 additions & 56 deletions workspace/mauss/src/core/standard/unique.spec.ts

This file was deleted.

24 changes: 0 additions & 24 deletions workspace/mauss/src/core/standard/unique.ts

This file was deleted.

2 changes: 1 addition & 1 deletion workspace/mauss/src/core/tsf/index.spec.ts
Original file line number Diff line number Diff line change
@@ -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));
Expand Down
2 changes: 1 addition & 1 deletion workspace/mauss/src/core/tsf/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { UnaryFunction } from '../../typings/helpers.js';

type Parse<T> = T extends `${string}{${infer P}}${infer R}` ? P | Parse<R> : never;
export default function tsf<Input extends string>(
export function tsf<Input extends string>(
template: Input extends `${string}{}${string}`
? 'Empty braces are not allowed in template'
: Input extends
Expand Down
3 changes: 1 addition & 2 deletions workspace/mauss/src/web/index.ts
Original file line number Diff line number Diff line change
@@ -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';
37 changes: 0 additions & 37 deletions workspace/mauss/src/web/query/encoder.ts

This file was deleted.

3 changes: 1 addition & 2 deletions workspace/mauss/src/web/query/index.spec.ts
Original file line number Diff line number Diff line change
@@ -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'),
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -29,7 +29,7 @@ type QueryDecoder<Query extends string> = 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<Q extends string>(qs: Q) {
export function qsd<Q extends string>(qs: Q) {
if (qs[0] === '?') qs = qs.slice(1) as Q;
if (!qs) return {} as QueryDecoder<Q>;

Expand All @@ -52,3 +52,39 @@ export default function qsd<Q extends string>(qs: Q) {
}
return dqs as QueryDecoder<Q>;
}

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<T extends object>(
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;
}
77 changes: 77 additions & 0 deletions workspace/website/src/routes/docs.json/+server.ts
Original file line number Diff line number Diff line change
@@ -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);
});
},
};
4 changes: 4 additions & 0 deletions workspace/website/svelte.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ const config = {
fallback: '404.html',
}),

alias: {
$mauss: '../mauss',
},

paths: {
base: process.argv.includes('dev') ? '' : process.env.BASE_PATH,
},
Expand Down

0 comments on commit d73c418

Please sign in to comment.