UNPKG

@triviality/core

Version:
455 lines (402 loc) 14.1 kB
import { create } from 'filehound'; import * as fs from 'fs'; import { EOL } from 'os'; import { __, always, applySpec, cond, curry, curryN, identity, is, map, match, nth, pipe, range, replace, T, } from 'ramda'; import * as yup from 'yup'; export const generateRecurringString = curry((total: number, empty: boolean, createString: (nr: number) => string, separator: string) => range(empty ? 0 : 1, total + 1).map(createString).join(separator)); export const parseNumber: (str: string) => number = curryN(2, parseInt)(__, 10); /** * Return all generators of a single document. * * Generators need to be inside multi-doc block comment. */ export const findAnnotationsString: (annotation: string) => (document: string) => string[] = (annotation: string) => match(new RegExp(`^.*@${annotation}\\([\\s\\S.*]*?\\).*[.\\s\\S]*?\\*\\/$`, 'gm')); /** * Return all generators of a single document. * * Generators need to be inside multi-doc block comment. */ export const findFunctionGeneratorAnnotationsString: (document: string) => string[] = findAnnotationsString('typeGenerator'); interface GeneratorTemplate { length: number; empty: boolean; templates: string[]; removeNextLines: RegExp; } const matchEmptyOrStatement = () => /(.*; *$|^ *$)/; const GeneratorTemplateSchema = yup.object({ length: yup.number().positive().default(10), templates: yup.array(yup.string().required()).required(), removeNextLines: yup.mixed().default(matchEmptyOrStatement), empty: yup.boolean().default(false), }).noUnknown(); /** * Remove any doc block characters from a string * * // * * /** * * * * / */ export const stripDocBlock = replace(/^ *(\/?\*+|\/\/)/gm, ''); /** * Remove all ,< from empty generic functions. * * This: * <> * <,C extends string> * <D extends number,, C extends string> * <D extends number,, C extends string>(,dd: string) * <D extends number,, C extends string>(dd: string,,d: string) * * Would be this: * <C extends string> * <D extends number, C extends string> * <D extends number, C extends string>(dd: string) * <D extends number, C extends string>(dd: string,d: string) */ const stripEmptyGeneric = replace(/<>|(<|\(), *|( *, *),|, *(\))/gm, '$1$2$3'); /** * Parse generator function. * * @typeGenerator({ length: 10, templates: ["services<{K% extends keyof T}>({t%: K%}): [{T[K%]}];"], removeNextLines: /; *$/ }) */ export const parseFunctionGeneratorAnnotation: (template: string) => GeneratorTemplate = pipe( match(/@typeGenerator\(([\s\S.]*)\)/), pipe(nth(1) as any, (target: string) => { const striped = stripDocBlock(target); try { // tslint:disable-next-line:no-eval return eval(`(${striped});`); } catch (e) { throw new Error(`${e} => (${striped});`); } }), GeneratorTemplateSchema.cast.bind(GeneratorTemplateSchema) as any, ); /** * Find all tags "{t%: K%}" of a function template. */ export const findGeneratorTagsStrings: (template: string) => string[] = match(/{{.+?}}/gm); interface GeneratorTag { tag: string; template: string; separator: string; } /** * Parse a single tag and return variables. */ export const parseGeneratorTag: (tag: string) => GeneratorTag = pipe( match(/{{(.*?) *(-(.*))?}}/), applySpec<GeneratorTag>({ tag: nth(0), template: nth(1), separator: pipe(nth(3) as any, cond([ [is(String), identity], [T, always(', ')], ])), }), ); export const findGeneratorTags: (template: string) => GeneratorTag[] = pipe(findGeneratorTagsStrings, map(parseGeneratorTag)); /** * Fill tag template. * - Replaces % for i given. * - Strip template tokens. */ const populateGeneratorTagTemplate = curry((template: string, i: number) => replace(/%/g, i.toString(10), template)); export const generateFunction = curry((template: string, i: number): string => { const g = generateRecurringString(i, false); const result = findGeneratorTags(template).reduce( (acc, { tag, separator, template: t }) => { const f: string = g(populateGeneratorTagTemplate(t), separator); return acc.replace(tag, f); }, template, ); return i === 0 ? stripEmptyGeneric(result) : result; }); export function generateFunctions(length: number, emptyArgs: boolean, templates: string[]): string { return generateRecurringString( length, emptyArgs, (i) => { return templates.reduce<string | null>( (acc, template) => { if (!acc) { return generateFunction(template, i); } return acc + EOL + generateFunction(template, i); }, null, ) || ''; }, EOL, ); } function removeStartLine(text: string, removeNextLines: RegExp) { return match(/^.*$/gm, text) .reduce<{ document: string, done: boolean }>( ({ document, done }, line) => { if (done || !removeNextLines.test(line)) { return { document: document === '' ? line : document + EOL + line, done: true, }; } return { document, done, }; }, { document: '', done: false }, ).document; } export function generateFunctionsInDocument(document: string) { return findFunctionGeneratorAnnotationsString(document) .reduce( (acc: string, generator) => { const { templates, length, removeNextLines, empty } = parseFunctionGeneratorAnnotation(generator); const index = acc.indexOf(generator); const end = index + generator.length; const cleaned = acc.slice(0, end) + EOL + removeStartLine(acc.slice(end), removeNextLines); return cleaned.replace(generator, generator + EOL + generateFunctions(length, empty, templates)); }, document, ); } /** * Replaces multiple generators of set of files. */ export function generateFunctionsOfTemplatesInDirectory(...directories: string[]) { create() .paths(...directories) .ext(['ts', 'tsx']) .discard(/__test__/ as any) .findSync() .forEach((file) => { const content = fs.readFileSync(file).toString(); const updates = generateFunctionsInDocument(content); if (content !== updates) { fs.writeFileSync(file, updates); } }); } it('generateRecurringString should generate string based on length and template', () => { const g = generateRecurringString(3, false); expect(g((i) => `[${i}]`, ', ')).toEqual('[1], [2], [3]'); const gE = generateRecurringString(3, true); expect(gE((i) => `[${i}]`, ', ')).toEqual('[0], [1], [2], [3]'); }); it('parseNumber', () => { expect(parseNumber('10')).toEqual(10); expect(parseNumber('-10')).toEqual(-10); }); it('findFunctionGeneratorAnnotationsString', () => { expect(findFunctionGeneratorAnnotationsString( ` /** * @typeGenerator({ length: 10, templates: ["services<{K% extends keyof T}>({t%: K%}): [{T[K%]}];"] }) */ export function servives(tags: any): any[]; /** * @typeGenerator({ length: 10, templates: ["export function toInt({a%i: number}): void;"], removeNextLines: /;/}) */ export toInt(...args: number[]): void; `, )).toMatchSnapshot(); }); it('findFunctionGeneratorAnnotationsString multiline', () => { expect(findFunctionGeneratorAnnotationsString( ` /** * @typeGenerator({ length: 10, templates: * ["services<{K% extends keyof T}>({t%: K%}): [{T[K%]}];"] * } ) */ `, )).toMatchSnapshot(); }); it('parseFunctionGeneratorAnnotation', () => { expect(parseFunctionGeneratorAnnotation('@typeGenerator({ empty: true, length: 10, templates: [\'export function toInt({a%i: number}): void;\'], removeNextLines: /;/ })')).toEqual({ empty: true, length: 10, removeNextLines: /;/, templates: ['export function toInt({a%i: number}): void;'], } as GeneratorTemplate); }); it('parseFunctionGeneratorAnnotation with defaults', () => { expect(parseFunctionGeneratorAnnotation('@typeGenerator({ length: 5, templates: [\'export function toInt({a%i: number}): void;\'] })')).toEqual({ empty: false, length: 5, removeNextLines: /(.*; *$|^ *$)/, templates: ['export function toInt({a%i: number}): void;'], } as GeneratorTemplate); }); it('findGeneratorTagsStrings', () => { expect(findGeneratorTagsStrings('export function toInt({{a%i: number}}): { data: {{a%i}} };')).toEqual([ '{{a%i: number}}', '{{a%i}}', ]); }); it('parseGeneratorTag', () => { expect(parseGeneratorTag('{{a%}}')).toEqual({ template: 'a%', separator: ', ', tag: '{{a%}}', } as GeneratorTag); expect(parseGeneratorTag('{{b%- | }}')).toEqual({ template: 'b%', separator: ' | ', tag: '{{b%- | }}', } as GeneratorTag); }); it('findGeneratorTags', () => { expect(findGeneratorTags('foobar({{a%: number}}): { data: [{{typeof a%}}] };')).toEqual([ { template: 'a%: number', separator: ', ', tag: '{{a%: number}}', }, { template: 'typeof a%', separator: ', ', tag: '{{typeof a%}}', }, ] as GeneratorTag[]); }); it('populateGeneratorTagTemplate', () => { expect(populateGeneratorTagTemplate('a%: number', 2)).toEqual('a2: number'); expect(populateGeneratorTagTemplate('c: typeof a%, a%: number', 5)).toEqual('c: typeof a5, a5: number'); }); it('generateFunction', () => { expect(generateFunction('foobar({{a%: number}}): { data: [{{typeof a%}}] };', 2)).toEqual('foobar(a1: number, a2: number): { data: [typeof a1, typeof a2] };'); }); it('generateFunctions', () => { expect(generateFunctions(2, false, ['foo({{a%: string}}): void;', 'bar({{b%:number}}): void;'])) .toEqual( `foo(a1: string): void; bar(b1:number): void; foo(a1: string, a2: string): void; bar(b1:number, b2:number): void;`); }); it('generateFunctions with empty function', () => { expect(generateFunctions(2, true, ['foo({{a%: string}}): void;', 'bar({{b%:number}}): void;'])) .toEqual( `foo(): void; bar(): void; foo(a1: string): void; bar(b1:number): void; foo(a1: string, a2: string): void; bar(b1:number, b2:number): void;`); }); it('generateFunctions should remove empty generic types <>', () => { expect(generateFunctions(1, true, ['foo<{{A% extends string}}>({{a%: A1}}): void;'])) .toEqual( `foo(): void; foo<A1 extends string>(a1: A1): void;`); expect(generateFunctions(1, true, ['foo<{{A% extends string}}, D extends number>({{a%: A1}}, d: D): void;'])) .toEqual( `foo<D extends number>(d: D): void; foo<A1 extends string, D extends number>(a1: A1, d: D): void;`); expect(generateFunctions(1, true, ['foo<C extends Object,{{ A% extends string}}, D extends number>({{a%: A1}}, d: D, c: C): void;'])) .toEqual( `foo<C extends Object, D extends number>(d: D, c: C): void; foo<C extends Object, A1 extends string, D extends number>(a1: A1, d: D, c: C): void;`); }); it('generateFunctionsInDocument', () => { expect(generateFunctionsInDocument(` /** * @typeGenerator({ length: 4, templates: ["compose<{{t% extends keyof T}}}, F extends ({{d%: SFT<T[t%]>}}) => S, S>(f: F, {{k%: t%}}}): () => S;"] }) */ `)) .toEqual( ` /** * @typeGenerator({ length: 4, templates: ["compose<{{t% extends keyof T}}}, F extends ({{d%: SFT<T[t%]>}}) => S, S>(f: F, {{k%: t%}}}): () => S;"] }) */ compose<t1 extends keyof T}, F extends (d1: SFT<T[t1]>) => S, S>(f: F, k1: t1}): () => S; compose<t1 extends keyof T, t2 extends keyof T}, F extends (d1: SFT<T[t1]>, d2: SFT<T[t2]>) => S, S>(f: F, k1: t1, k2: t2}): () => S; compose<t1 extends keyof T, t2 extends keyof T, t3 extends keyof T}, F extends (d1: SFT<T[t1]>, d2: SFT<T[t2]>, d3: SFT<T[t3]>) => S, S>(f: F, k1: t1, k2: t2, k3: t3}): () => S; compose<t1 extends keyof T, t2 extends keyof T, t3 extends keyof T, t4 extends keyof T}, F extends (d1: SFT<T[t1]>, d2: SFT<T[t2]>, d3: SFT<T[t3]>, d4: SFT<T[t4]>) => S, S>(f: F, k1: t1, k2: t2, k3: t3, k4: t4}): () => S; `); }); it('generateFunctionsInDocument multiline', () => { expect(generateFunctionsInDocument(` /** * @typeGenerator({ length: 2, templates: [ * "Foo{{%}}", * "Bar{{%}}", * ]}) */ `)) .toEqual( ` /** * @typeGenerator({ length: 2, templates: [ * "Foo{{%}}", * "Bar{{%}}", * ]}) */ Foo1 Bar1 Foo1, 2 Bar1, 2 `); }); it('removeStartLine', () => { expect(removeStartLine( `d; ddsad; sad;; Do not remove! Don't ignore this; `, matchEmptyOrStatement(), )).toEqual(`Do not remove! Don't ignore this; `); }); it('generateFunctionsInDocument should remove old lines', () => { expect(generateFunctionsInDocument(` /** * @typeGenerator({ length: 4, templates: ["compose<{{t% extends keyof T}}}, F extends ({{d%: SFT<T[t%]>}}) => S, S>(f: F, {{k%: t%}}}): () => S;"] }) */ compose<t1 extends keyof T, t2 extends keyof T}, F extends (d1: SFT<T[t1]>, d2: SFT<T[t2]>) => S, S>(f: F, k1: t1, k2: t2}): () => S; Remove this; compose<t1 extends keyof T, t2 extends keyof T, t3 extends keyof T, t4 extends keyof T}, F extends (d1: SFT<T[t1]>, d2: SFT<T[t2]>, d3: SFT<T[t3]>, d4: SFT<T[t4]>) => S, S>(f: F, k1: t1, k2: t2, k3: t3, k4: t4}): () => S; preserve this `)) .toEqual( ` /** * @typeGenerator({ length: 4, templates: ["compose<{{t% extends keyof T}}}, F extends ({{d%: SFT<T[t%]>}}) => S, S>(f: F, {{k%: t%}}}): () => S;"] }) */ compose<t1 extends keyof T}, F extends (d1: SFT<T[t1]>) => S, S>(f: F, k1: t1}): () => S; compose<t1 extends keyof T, t2 extends keyof T}, F extends (d1: SFT<T[t1]>, d2: SFT<T[t2]>) => S, S>(f: F, k1: t1, k2: t2}): () => S; compose<t1 extends keyof T, t2 extends keyof T, t3 extends keyof T}, F extends (d1: SFT<T[t1]>, d2: SFT<T[t2]>, d3: SFT<T[t3]>) => S, S>(f: F, k1: t1, k2: t2, k3: t3}): () => S; compose<t1 extends keyof T, t2 extends keyof T, t3 extends keyof T, t4 extends keyof T}, F extends (d1: SFT<T[t1]>, d2: SFT<T[t2]>, d3: SFT<T[t3]>, d4: SFT<T[t4]>) => S, S>(f: F, k1: t1, k2: t2, k3: t3, k4: t4}): () => S; preserve this `); }); it('!!', () => { generateFunctionsOfTemplatesInDirectory(`${__dirname}/../../`); });