From 7c55a4010c6a71b7cc98903a1c79c3b42330bf6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Costa?= Date: Sun, 14 Apr 2024 22:57:35 -0700 Subject: [PATCH] refactor: auto-sort libraries by stars Summary: Test Plan: --- .gitignore | 4 +- README.md | 20 +- turbo/generators/config.ts | 249 ++++++------------ turbo/generators/libraries.ts | 129 +++++++++ turbo/generators/templates/README.md.hbs | 8 +- .../generators/templates/all/package.json.hbs | 6 +- .../templates/main/package.json.hbs | 8 +- .../templates/main/src/adapters.ts.hbs | 4 +- .../templates/main/src/serialization.ts.hbs | 4 +- .../templates/main/src/validation.ts.hbs | 4 +- 10 files changed, 234 insertions(+), 202 deletions(-) create mode 100644 turbo/generators/libraries.ts diff --git a/.gitignore b/.gitignore index b05ecda4..32df5775 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,6 @@ node_modules dist/ *_dist/ -*.tmp \ No newline at end of file +*.tmp + +/turbo/generators/stars.json \ No newline at end of file diff --git a/README.md b/README.md index 839669f9..9efae736 100644 --- a/README.md +++ b/README.md @@ -164,6 +164,16 @@ We value flexibility, which is why there are multiple ways of using TypeSchema: @typeschema/typebox npm downloads + + effect + GitHub stars + ✅ + ✅ + ✅ + ✅ + @typeschema/effect + npm downloads + typia GitHub stars @@ -184,16 +194,6 @@ We value flexibility, which is why there are multiple ways of using TypeSchema: @typeschema/ow npm downloads - - effect - GitHub stars - ✅ - ✅ - ✅ - ✅ - @typeschema/effect - npm downloads - arktype GitHub stars diff --git a/turbo/generators/config.ts b/turbo/generators/config.ts index 163e5fc5..ee596c64 100644 --- a/turbo/generators/config.ts +++ b/turbo/generators/config.ts @@ -4,6 +4,11 @@ import {ESLint} from 'eslint'; import * as fs from 'fs'; import * as prettier from 'prettier'; +import {genLibraries} from './libraries'; + +const MULTI_ADAPTER_NAMES = ['main', 'all']; +const NON_ADAPTER_NAMES = ['core']; + const DISCLAIMER = 'This file is generated. Do not modify it manually!'; const PARTIAL_DISCLAIMER = 'This file is partially generated. Only some fields can be modified manually!'; @@ -21,7 +26,7 @@ const eslint = new ESLint({ const generatedFilePaths: Array = []; -function maybeReadFile(path: string): string | null { +export function maybeReadFile(path: string): string | null { return fs.existsSync(path) ? fs.readFileSync(path, 'utf-8') : null; } @@ -42,7 +47,7 @@ function getAddAction(config: { fs.writeFileSync(tempPath, `${SLASH_STAR_HEADER}\n\n${content}`); const results = await eslint.lintFiles(tempPath); const output = - results[0].output ?? fs.readFileSync(tempPath, 'utf-8'); + results[0]?.output ?? fs.readFileSync(tempPath, 'utf-8'); fs.unlinkSync(tempPath); generatedFilePaths.push(config.path); return output; @@ -99,39 +104,50 @@ function getAddActions(config: { ); } -function getAdapters(adapterNames: Array) { - return adapterNames.map(adapterName => { +type Adapter = { + canInfer: {[key: string]: boolean}; + example: string | null; + name: string; + hasModule: {[key: string]: boolean}; + packageJson: {[key: string]: unknown}; +}; + +function getAdapters(adapterNames: Array): {[key: string]: Adapter} { + return adapterNames.reduce((adapters, adapterName) => { const resolver = maybeReadFile(`packages/${adapterName}/src/resolver.ts`); return { - canInfer: ['input', 'output'].reduce( - (result, type) => ({ - ...result, - [type]: - resolver?.includes( - `${type}: this['schema'] extends this['base'] ? unknown : never;`, - ) === false, - }), - {}, - ), - example: maybeReadFile( - `packages/${adapterName}/src/__tests__/example.ts`, - )?.replace("'..'", `'@typeschema/${adapterName}'`), - hasModule: ['validation', 'serialization'].reduce( - (result, moduleName) => ({ - ...result, - [moduleName]: - fs.statSync(`packages/${adapterName}/src/${moduleName}.ts`, { - throwIfNoEntry: false, - }) != null, - }), - {}, - ), - name: adapterName, - packageJson: JSON.parse( - maybeReadFile(`packages/${adapterName}/package.json`) ?? '{}', - ), + ...adapters, + [adapterName]: { + canInfer: ['input', 'output'].reduce( + (result, type) => ({ + ...result, + [type]: + resolver?.includes( + `${type}: this['schema'] extends this['base'] ? unknown : never;`, + ) === false, + }), + {}, + ), + example: maybeReadFile( + `packages/${adapterName}/src/__tests__/example.ts`, + )?.replace("'..'", `'@typeschema/${adapterName}'`), + hasModule: ['validation', 'serialization'].reduce( + (result, moduleName) => ({ + ...result, + [moduleName]: + fs.statSync(`packages/${adapterName}/src/${moduleName}.ts`, { + throwIfNoEntry: false, + }) != null, + }), + {}, + ), + name: adapterName, + packageJson: JSON.parse( + maybeReadFile(`packages/${adapterName}/package.json`) ?? '{}', + ), + }, }; - }); + }, {}); } export default function generator(plop: PlopTypes.NodePlopAPI): void { @@ -158,33 +174,27 @@ export default function generator(plop: PlopTypes.NodePlopAPI): void { plop.addHelper('isEmpty', (value: object) => Object.keys(value).length === 0); plop.setGenerator('all', { - actions: () => { - const packageNames = fs - .readdirSync('packages', {withFileTypes: true}) - .filter(file => file.isDirectory()) - .map(file => file.name); - const adapters = getAdapters( - packageNames.filter(packageName => packageName !== 'core'), - ); - const multiAdapterNames = ['main', 'all']; - const singleAdapters = adapters.filter( - adapter => !multiAdapterNames.includes(adapter.name), - ); + actions: answers => { + const {adapters, libraries, singleAdapters} = answers as { + adapters: Record; + libraries: Record; + singleAdapters: Array; + }; const actions: Array = [ - ...adapters.flatMap(adapter => + ...Object.values(adapters).flatMap(adapter => getAddActions({ base: 'templates/adapter', data: { ...adapter, - isMultipleAdapter: multiAdapterNames.includes(adapter.name), + isMultipleAdapter: MULTI_ADAPTER_NAMES.includes(adapter.name), }, destination: `packages/${adapter.name}`, }), ), - ...multiAdapterNames.flatMap(multiAdapterName => [ + ...MULTI_ADAPTER_NAMES.flatMap(multiAdapterName => [ ...getAddActions({ base: `templates/${multiAdapterName}`, - data: {adapters: singleAdapters}, + data: {singleAdapters}, destination: `packages/${multiAdapterName}`, }), ...singleAdapters.map(singleAdapter => @@ -199,131 +209,7 @@ export default function generator(plop: PlopTypes.NodePlopAPI): void { templateFile: 'templates/adapter/tsconfig.json.hbs', }), getAddAction({ - data: { - adapter: adapters.find(adapter => adapter.name === 'main'), - libraries: [ - { - adapter: adapters.find(adapter => adapter.name === 'zod'), - github: 'colinhacks/zod', - name: 'zod', - url: 'https://zod.dev', - }, - { - adapter: adapters.find(adapter => adapter.name === 'yup'), - github: 'jquense/yup', - name: 'yup', - url: 'https://github.com/jquense/yup', - }, - { - adapter: adapters.find(adapter => adapter.name === 'joi'), - github: 'hapijs/joi', - name: 'joi', - url: 'https://joi.dev', - }, - { - adapter: adapters.find(adapter => adapter.name === 'json'), - github: 'ajv-validator/ajv', - name: 'ajv', - url: 'https://ajv.js.org', - }, - { - adapter: adapters.find( - adapter => adapter.name === 'class-validator', - ), - github: 'typestack/class-validator', - name: 'class-validator', - url: 'https://github.com/typestack/class-validator', - }, - { - adapter: adapters.find( - adapter => adapter.name === 'superstruct', - ), - github: 'ianstormtaylor/superstruct', - name: 'superstruct', - url: 'https://docs.superstructjs.org', - }, - { - adapter: adapters.find(adapter => adapter.name === 'io-ts'), - github: 'gcanti/io-ts', - name: 'io-ts', - url: 'https://gcanti.github.io/io-ts', - }, - { - adapter: adapters.find(adapter => adapter.name === 'valibot'), - github: 'fabian-hiller/valibot', - name: 'valibot', - url: 'https://valibot.dev', - }, - { - adapter: adapters.find(adapter => adapter.name === 'typebox'), - github: 'sinclairzx81/typebox', - name: 'typebox', - url: 'https://github.com/sinclairzx81/typebox', - }, - { - adapter: adapters.find(adapter => adapter.name === 'function'), - github: 'samchon/typia', - name: 'typia', - url: 'https://typia.io', - }, - { - adapter: adapters.find(adapter => adapter.name === 'ow'), - github: 'sindresorhus/ow', - name: 'ow', - url: 'https://sindresorhus.com/ow', - }, - { - adapter: adapters.find(adapter => adapter.name === 'effect'), - github: 'effect-ts/effect', - name: 'effect', - url: 'https://effect.website', - }, - { - adapter: adapters.find(adapter => adapter.name === 'arktype'), - github: 'arktypeio/arktype', - name: 'arktype', - url: 'https://arktype.io', - }, - { - adapter: adapters.find(adapter => adapter.name === 'deepkit'), - github: 'deepkit/deepkit-framework', - name: 'deepkit', - url: 'https://deepkit.io', - }, - { - adapter: adapters.find(adapter => adapter.name === 'runtypes'), - github: 'pelotom/runtypes', - name: 'runtypes', - url: 'https://github.com/pelotom/runtypes', - }, - { - adapter: adapters.find( - adapter => adapter.name === 'fastest-validator', - ), - github: 'icebob/fastest-validator', - name: 'fastest-validator', - url: 'https://github.com/icebob/fastest-validator', - }, - { - adapter: adapters.find(adapter => adapter.name === 'vine'), - github: 'vinejs/vine', - name: 'vine', - url: 'https://vinejs.dev', - }, - { - adapter: adapters.find(adapter => adapter.name === 'suretype'), - github: 'grantila/suretype', - name: 'suretype', - url: 'https://github.com/grantila/suretype', - }, - { - adapter: adapters.find(adapter => adapter.name === 'valita'), - github: 'badrap/valita', - name: 'valita', - url: 'https://github.com/badrap/valita', - }, - ], - }, + data: {libraries, mainAdapter: adapters.main}, path: 'README.md', templateFile: 'templates/README.md.hbs', }), @@ -332,7 +218,22 @@ export default function generator(plop: PlopTypes.NodePlopAPI): void { return actions; }, description: 'Re-generates files', - prompts: [], + prompts: async () => { + const packageNames = fs + .readdirSync('packages', {withFileTypes: true}) + .filter(file => file.isDirectory()) + .map(file => file.name); + const adapters = getAdapters( + packageNames.filter( + packageName => !NON_ADAPTER_NAMES.includes(packageName), + ), + ); + const libraries = await genLibraries(adapters); + const singleAdapters = Object.values(adapters).filter( + adapter => !MULTI_ADAPTER_NAMES.includes(adapter.name), + ); + return {adapters, libraries, singleAdapters}; + }, }); plop.setGenerator('create-adapter', { diff --git a/turbo/generators/libraries.ts b/turbo/generators/libraries.ts new file mode 100644 index 00000000..5df92632 --- /dev/null +++ b/turbo/generators/libraries.ts @@ -0,0 +1,129 @@ +import * as fs from 'fs'; + +import {maybeReadFile} from './config'; + +const STARS_PATH = 'turbo/generators/stars.json'; +const CACHE_DURATION_IN_SEC = 7200; + +const LIBRARIES: Record< + string, + {adapterName?: string; github: string; url: string} +> = { + ajv: { + adapterName: 'json', + github: 'ajv-validator/ajv', + url: 'https://ajv.js.org', + }, + arktype: { + github: 'arktypeio/arktype', + url: 'https://arktype.io', + }, + 'class-validator': { + github: 'typestack/class-validator', + url: 'https://github.com/typestack/class-validator', + }, + deepkit: { + github: 'deepkit/deepkit-framework', + url: 'https://deepkit.io', + }, + effect: { + github: 'effect-ts/effect', + url: 'https://effect.website', + }, + 'fastest-validator': { + github: 'icebob/fastest-validator', + url: 'https://github.com/icebob/fastest-validator', + }, + 'io-ts': { + github: 'gcanti/io-ts', + url: 'https://gcanti.github.io/io-ts', + }, + joi: { + github: 'hapijs/joi', + url: 'https://joi.dev', + }, + ow: { + github: 'sindresorhus/ow', + url: 'https://sindresorhus.com/ow', + }, + runtypes: { + github: 'pelotom/runtypes', + url: 'https://github.com/pelotom/runtypes', + }, + superstruct: { + github: 'ianstormtaylor/superstruct', + url: 'https://docs.superstructjs.org', + }, + suretype: { + github: 'grantila/suretype', + url: 'https://github.com/grantila/suretype', + }, + typebox: { + github: 'sinclairzx81/typebox', + url: 'https://github.com/sinclairzx81/typebox', + }, + typia: { + adapterName: 'function', + github: 'samchon/typia', + url: 'https://typia.io', + }, + valibot: { + github: 'fabian-hiller/valibot', + url: 'https://valibot.dev', + }, + valita: { + github: 'badrap/valita', + url: 'https://github.com/badrap/valita', + }, + vine: { + github: 'vinejs/vine', + url: 'https://vinejs.dev', + }, + yup: { + github: 'jquense/yup', + url: 'https://github.com/jquense/yup', + }, + zod: { + github: 'colinhacks/zod', + url: 'https://zod.dev', + }, +}; + +async function genStars() { + const cachedResult: Record = JSON.parse( + maybeReadFile(STARS_PATH) ?? '{}', + ); + const updateTime = Math.round(Date.now() / 1000); + if ( + updateTime - (cachedResult.updateTime ?? 0) <= CACHE_DURATION_IN_SEC && + Object.values(LIBRARIES).every(({github}) => cachedResult[github] != null) + ) { + return cachedResult; + } + const result: Record = {updateTime}; + await Promise.all( + Object.values(LIBRARIES).map(async ({github}) => { + const response = await fetch(`https://api.github.com/repos/${github}`); + const data = await response.json(); + if (data.message != null) { + throw new Error(data.message); + } + result[github] = data.stargazers_count; + }), + ); + fs.writeFileSync(STARS_PATH, JSON.stringify(result, null, 2)); + return result; +} + +export async function genLibraries(adapters: Record) { + const stars = await genStars(); + const result = await Promise.all( + Object.keys(LIBRARIES).map(async name => ({ + ...LIBRARIES[name], + adapter: adapters[LIBRARIES[name]?.adapterName ?? name], + name, + stars: stars[LIBRARIES[name]?.github ?? ''] ?? 0, + })), + ); + return result.sort((a, b) => b.stars - a.stars); +} diff --git a/turbo/generators/templates/README.md.hbs b/turbo/generators/templates/README.md.hbs index 9b5f10f0..db6eea32 100644 --- a/turbo/generators/templates/README.md.hbs +++ b/turbo/generators/templates/README.md.hbs @@ -14,7 +14,7 @@

License - Bundle size + Bundle size npm downloads GitHub stars

@@ -27,7 +27,7 @@   •   GitHub   •   - npm + npm


@@ -38,7 +38,7 @@ > TypeSchema enables writing code that [works with any validation library](#coverage) out-of-the-box. It provides a universal adapter for interacting with any validation schema, decoupling from implementation specifics and increasing compatibility. ```ts -import {validate} from '@typeschema/{{adapter.name}}'; +import {validate} from '@typeschema/{{mainAdapter.name}}'; import {z} from 'zod'; import {string} from 'valibot'; @@ -91,7 +91,7 @@ We value flexibility, which is why there are multiple ways of using TypeSchema: > We welcome [PRs](https://github.com/decs/typeschema/pulls)! > Otherwise, please [file an issue](https://github.com/decs/typeschema/issues) to help us prioritize. 🙌 -{{> readmeAPI adapter}} +{{> readmeAPI mainAdapter}} ## Acknowledgements diff --git a/turbo/generators/templates/all/package.json.hbs b/turbo/generators/templates/all/package.json.hbs index 17eab555..01bb019a 100644 --- a/turbo/generators/templates/all/package.json.hbs +++ b/turbo/generators/templates/all/package.json.hbs @@ -1,14 +1,14 @@ { -{{> packageJson name="all" adapters=adapters}} +{{> packageJson name="all" adapters=singleAdapters}} "dependencies": { "@typeschema/core": "workspace:*", "@typeschema/main": "workspace:*", -{{#each adapters}} +{{#each singleAdapters}} "@typeschema/{{name}}": "workspace:*", {{/each}} }, "devDependencies": { -{{#each adapters}} +{{#each singleAdapters}} {{#each packageJson.devDependencies}} "{{@key}}": "{{this}}", {{/each}} diff --git a/turbo/generators/templates/main/package.json.hbs b/turbo/generators/templates/main/package.json.hbs index 3216bd1e..887350b8 100644 --- a/turbo/generators/templates/main/package.json.hbs +++ b/turbo/generators/templates/main/package.json.hbs @@ -1,10 +1,10 @@ { -{{> packageJson name="main" adapters=adapters}} +{{> packageJson name="main" adapters=singleAdapters}} "dependencies": { "@typeschema/core": "workspace:*" }, "devDependencies": { -{{#each adapters}} +{{#each singleAdapters}} "@typeschema/{{name}}": "workspace:*", {{#each packageJson.devDependencies}} "{{@key}}": "{{this}}", @@ -12,12 +12,12 @@ {{/each}} }, "peerDependencies": { -{{#each adapters}} +{{#each singleAdapters}} "@typeschema/{{name}}": "workspace:*", {{/each}} }, "peerDependenciesMeta": { -{{#each adapters}} +{{#each singleAdapters}} "@typeschema/{{name}}": { "optional": true }, diff --git a/turbo/generators/templates/main/src/adapters.ts.hbs b/turbo/generators/templates/main/src/adapters.ts.hbs index b07888f7..e8293e40 100644 --- a/turbo/generators/templates/main/src/adapters.ts.hbs +++ b/turbo/generators/templates/main/src/adapters.ts.hbs @@ -1,9 +1,9 @@ -{{#each adapters}} +{{#each singleAdapters}} import type {AdapterResolver as {{pascalCase name}}Resolver} from '@typeschema/{{name}}'; {{/each}} export type AdapterResolvers = { -{{#each adapters}} +{{#each singleAdapters}} {{camelCase name}}: {{pascalCase name}}Resolver; {{/each}} }; diff --git a/turbo/generators/templates/main/src/serialization.ts.hbs b/turbo/generators/templates/main/src/serialization.ts.hbs index 109e3692..f7daa8d7 100644 --- a/turbo/generators/templates/main/src/serialization.ts.hbs +++ b/turbo/generators/templates/main/src/serialization.ts.hbs @@ -8,7 +8,7 @@ import {memoize, unsupportedAdapter} from '@typeschema/core'; import {select} from './selector'; -{{#each adapters}} +{{#each singleAdapters}} {{#if hasModule.serialization}} const import{{pascalCase name}}SerializationAdapter = memoize(async () => { const {serializationAdapter} = await import('@typeschema/{{name}}'); @@ -18,7 +18,7 @@ const import{{pascalCase name}}SerializationAdapter = memoize(async () => { {{/if}} {{/each}} export const serializationAdapter: SerializationAdapter = select({ -{{#each adapters}} +{{#each singleAdapters}} {{#if hasModule.serialization}} {{camelCase name}}: async schema => (await import{{pascalCase name}}SerializationAdapter())(schema), {{else}} diff --git a/turbo/generators/templates/main/src/validation.ts.hbs b/turbo/generators/templates/main/src/validation.ts.hbs index f4037849..13e5ac0c 100644 --- a/turbo/generators/templates/main/src/validation.ts.hbs +++ b/turbo/generators/templates/main/src/validation.ts.hbs @@ -8,7 +8,7 @@ import {memoize, unsupportedAdapter} from '@typeschema/core'; import {select} from './selector'; -{{#each adapters}} +{{#each singleAdapters}} {{#if hasModule.validation}} const import{{pascalCase name}}ValidationAdapter = memoize(async () => { const {validationAdapter} = await import('@typeschema/{{name}}'); @@ -18,7 +18,7 @@ const import{{pascalCase name}}ValidationAdapter = memoize(async () => { {{/if}} {{/each}} export const validationAdapter: ValidationAdapter = select({ -{{#each adapters}} +{{#each singleAdapters}} {{#if hasModule.validation}} {{camelCase name}}: async schema => (await import{{pascalCase name}}ValidationAdapter())(schema), {{else}}