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 |
|
+
+ effect |
+ |
+ ✅ |
+ ✅ |
+ ✅ |
+ ✅ |
+ @typeschema/effect |
+ |
+
typia |
|
@@ -184,16 +194,6 @@ We value flexibility, which is why there are multiple ways of using TypeSchema:
@typeschema/ow |
|
-
- effect |
- |
- ✅ |
- ✅ |
- ✅ |
- ✅ |
- @typeschema/effect |
- |
-
arktype |
|
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 @@
-
+
@@ -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}}