diff --git a/.eslintrc.js b/.eslintrc.js
index c969b0179..67bd8ca82 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -1,5 +1,6 @@
module.exports = {
- extends: ['prettier'],
+ root: true,
+ extends: ['prettier', 'turbo'],
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaVersion: 6,
diff --git a/.gitignore b/.gitignore
index 1debcb4b6..1c0510ffd 100644
--- a/.gitignore
+++ b/.gitignore
@@ -100,4 +100,5 @@ typings/
/.idea
package-lock.json
-tests/generated/**
\ No newline at end of file
+tests/generated/**
+.turbo
\ No newline at end of file
diff --git a/.husky/pre-commit b/.husky/pre-commit
index c22f9d878..44c0219c0 100755
--- a/.husky/pre-commit
+++ b/.husky/pre-commit
@@ -2,5 +2,5 @@
. "$(dirname "$0")/_/husky.sh"
yarn lint
-yarn test --run
+yarn test:ci
yarn format:staged
diff --git a/package.json b/package.json
index b75a66704..bca6d7f40 100644
--- a/package.json
+++ b/package.json
@@ -1,85 +1,45 @@
{
- "name": "orval",
- "description": "A swagger client generator for typescript",
- "version": "6.10.3",
+ "name": "orval-workspaces",
+ "namespace": "@orval",
+ "version": "6.11.0-alpha.1",
"license": "MIT",
- "files": [
- "dist"
+ "workspaces": [
+ "packages/*"
],
- "bin": {
- "orval": "dist/bin/orval.js"
- },
- "main": "dist/index.js",
- "keywords": [
- "rest",
- "client",
- "swagger",
- "open-api",
- "fetch",
- "data fetching",
- "code-generation",
- "angular",
- "react",
- "react-query",
- "svelte",
- "svelte-query",
- "vue",
- "vue-query",
- "msw",
- "mock",
- "axios",
- "vue-query",
- "vue",
- "swr"
- ],
- "author": {
- "name": "Victor Bury",
- "email": "bury.victor@gmail.com"
- },
- "repository": {
- "type": "git",
- "url": "https://github.com/anymaniax/orval"
- },
+ "private": true,
"scripts": {
- "build": "tsup ./src/bin/orval.ts ./src/index.ts --target node12 --minify --clean --dts --splitting",
- "dev": "tsup ./src/bin/orval.ts ./src/index.ts --target node12 --clean --watch src --onSuccess 'yarn generate-api'",
- "lint": "eslint src/**/*.ts",
- "test": "vitest --global test.ts",
+ "old-build": "tsup ./src/bin/orval.ts ./src/index.ts --target node12 --minify --clean --dts --splitting",
+ "old-dev": "tsup ./src/bin/orval.ts ./src/index.ts --target node12 --clean --watch src --onSuccess 'yarn generate-api'",
+ "old-lint": "eslint src/**/*.ts",
+ "old-test": "vitest --global test.ts",
"format": "prettier --write .",
"format:staged": "pretty-quick --staged",
"prerelease": "yarn build && cd ./tests && yarn generate && yarn build",
"release": "dotenv release-it",
"postrelease": "yarn build && yarn update-samples",
- "generate-api": "node ./dist/bin/orval.js --config ./samples/react-query/basic/orval.config.ts --watch",
"prepare": "husky install && cd ./samples/react-query/basic && yarn",
"commitlint": "commitlint",
- "update-samples": "zx ./scripts/update-samples.mjs"
+ "update-samples": "zx ./scripts/update-samples.mjs",
+ "build": "turbo run build",
+ "test": "turbo run test",
+ "test:ci": "turbo run test -- --run",
+ "lint": "turbo run lint",
+ "dev": "turbo run dev"
},
"devDependencies": {
"@commitlint/cli": "^17.0.3",
"@commitlint/config-conventional": "^17.0.3",
"@faker-js/faker": "^7.4.0",
- "@release-it/conventional-changelog": "^5.0.0",
- "@types/chalk": "^2.2.0",
- "@types/commander": "^2.12.2",
- "@types/fs-extra": "^9.0.13",
- "@types/inquirer": "^8.2.2",
- "@types/lodash.get": "^4.4.7",
- "@types/lodash.omit": "^4.5.7",
- "@types/lodash.omitby": "^4.6.7",
- "@types/lodash.uniq": "^4.5.7",
- "@types/lodash.uniqby": "^4.7.7",
- "@types/lodash.uniqwith": "^4.5.7",
- "@types/micromatch": "^4.0.2",
"@types/node": "^18.7.3",
"@types/prettier": "^2.7.0",
- "@types/request": "^2.48.8",
- "@types/validator": "^13.7.5",
+ "@release-it/conventional-changelog": "^5.0.0",
"@typescript-eslint/eslint-plugin": "^5.33.0",
"@typescript-eslint/parser": "^5.33.0",
"dotenv-cli": "^6.0.0",
+ "esbuild-plugin-alias": "^0.2.1",
"eslint": "^8.22.0",
"eslint-config-prettier": "^8.5.0",
+ "eslint-config-turbo": "^0.0.4",
"eslint-plugin-prettier": "^4.2.1",
"husky": "^8.0.1",
"lint-staged": "^13.0.3",
@@ -89,40 +49,9 @@
"release-it": "^15.3.0",
"rimraf": "^3.0.2",
"tsup": "^6.2.2",
+ "turbo": "^1.6.3",
"typescript": "^4.7.4",
"vitest": "^0.6.3",
"zx": "^7.0.8"
- },
- "dependencies": {
- "@apidevtools/swagger-parser": "^10.1.0",
- "acorn": "^8.8.0",
- "cac": "^6.7.12",
- "chalk": "^4.1.2",
- "chokidar": "^3.5.3",
- "compare-versions": "^4.1.3",
- "cuid": "^2.1.8",
- "debug": "^4.3.4",
- "esbuild": "^0.15.3",
- "esutils": "2.0.3",
- "execa": "^5.1.1",
- "find-up": "5.0.0",
- "fs-extra": "^10.1.0",
- "globby": "11.1.0",
- "ibm-openapi-validator": "^0.88.0",
- "inquirer": "^8.2.4",
- "lodash.get": "^4.4.2",
- "lodash.omit": "^4.5.0",
- "lodash.omitby": "^4.6.0",
- "lodash.uniq": "^4.5.0",
- "lodash.uniqby": "^4.7.0",
- "lodash.uniqwith": "^4.5.0",
- "micromatch": "^4.0.5",
- "openapi3-ts": "^3.0.0",
- "string-argv": "^0.3.1",
- "swagger2openapi": "^7.0.8",
- "tsconfck": "^2.0.1",
- "upath": "^2.0.1",
- "url": "^0.11.0",
- "validator": "^13.7.0"
}
}
diff --git a/packages/angular/README.md b/packages/angular/README.md
new file mode 100644
index 000000000..8e1596ee8
--- /dev/null
+++ b/packages/angular/README.md
@@ -0,0 +1,28 @@
+[![npm version](https://badge.fury.io/js/orval.svg)](https://badge.fury.io/js/orval)
+[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
+[![tests](https://github.com/anymaniax/orval/actions/workflows/tests.yaml/badge.svg)](https://github.com/anymaniax/orval/actions/workflows/tests.yaml)
+
+
+
+
+
+ Visit orval.dev for docs, guides, API and beer!
+
+
+### Code Generation
+
+`orval` is able to generate client with appropriate type-signatures (TypeScript) from any valid OpenAPI v3 or Swagger v2 specification, either in `yaml` or `json` formats.
+
+`Generate`, `valid`, `cache` and `mock` in your React, Vue, Svelte and Angular applications all with your OpenAPI specification.
+
+### Samples
+
+You can find below some samples
+
+- [react app](https://github.com/anymaniax/orval/tree/master/samples/react-app)
+- [react query](https://github.com/anymaniax/orval/tree/master/samples/react-query)
+- [svelte query](https://github.com/anymaniax/orval/tree/master/samples/svelte-query)
+- [vue query](https://github.com/anymaniax/orval/tree/master/samples/vue-query)
+- [react app with swr](https://github.com/anymaniax/orval/tree/master/samples/react-app-with-swr)
+- [nx fastify react](https://github.com/anymaniax/orval/tree/master/samples/nx-fastify-react)
+- [angular app](https://github.com/anymaniax/orval/tree/master/samples/angular-app)
diff --git a/packages/angular/package.json b/packages/angular/package.json
new file mode 100644
index 000000000..5ac74c402
--- /dev/null
+++ b/packages/angular/package.json
@@ -0,0 +1,18 @@
+{
+ "name": "@orval/angular",
+ "version": "6.11.0-alpha.1",
+ "license": "MIT",
+ "main": "./dist/index.js",
+ "types": "./dist/index.d.ts",
+ "files": [
+ "dist"
+ ],
+ "scripts": {
+ "build": "tsup ./src/index.ts --target node12 --minify --clean --dts --splitting",
+ "dev": "tsup ./src/index.ts --target node12 --clean --watch src",
+ "lint": "eslint src/**/*.ts"
+ },
+ "dependencies": {
+ "@orval/core": "6.11.0-alpha.1"
+ }
+}
diff --git a/packages/angular/src/index.ts b/packages/angular/src/index.ts
new file mode 100644
index 000000000..5d9f5e1f8
--- /dev/null
+++ b/packages/angular/src/index.ts
@@ -0,0 +1,240 @@
+import {
+ ClientBuilder,
+ ClientDependenciesBuilder,
+ ClientFooterBuilder,
+ ClientGeneratorsBuilder,
+ ClientHeaderBuilder,
+ ClientTitleBuilder,
+ generateFormDataAndUrlEncodedFunction,
+ generateMutatorConfig,
+ generateMutatorRequestOptions,
+ generateOptions,
+ generateVerbImports,
+ GeneratorDependency,
+ GeneratorOptions,
+ GeneratorVerbOptions,
+ isBoolean,
+ pascal,
+ sanitize,
+ toObjectString,
+ VERBS_WITH_BODY,
+} from '@orval/core';
+
+const ANGULAR_DEPENDENCIES: GeneratorDependency[] = [
+ {
+ exports: [
+ { name: 'HttpClient', values: true },
+ { name: 'HttpHeaders' },
+ { name: 'HttpParams' },
+ { name: 'HttpContext' },
+ ],
+ dependency: '@angular/common/http',
+ },
+ {
+ exports: [{ name: 'Injectable', values: true }],
+ dependency: '@angular/core',
+ },
+ {
+ exports: [{ name: 'Observable', values: true }],
+ dependency: 'rxjs',
+ },
+];
+
+const returnTypesToWrite: Map = new Map();
+
+export const getAngularDependencies: ClientDependenciesBuilder = () =>
+ ANGULAR_DEPENDENCIES;
+
+export const generateAngularTitle: ClientTitleBuilder = (title) => {
+ const sanTitle = sanitize(title);
+ return `${pascal(sanTitle)}Service`;
+};
+
+export const generateAngularHeader: ClientHeaderBuilder = ({
+ title,
+ isRequestOptions,
+ isMutator,
+ isGlobalMutator,
+ provideIn,
+}) => `
+${
+ isRequestOptions && !isGlobalMutator
+ ? `type HttpClientOptions = {
+ headers?: HttpHeaders | {
+ [header: string]: string | string[];
+ };
+ context?: HttpContext;
+ observe?: any;
+ params?: HttpParams | {
+ [param: string]: string | number | boolean | ReadonlyArray;
+ };
+ reportProgress?: boolean;
+ responseType?: any;
+ withCredentials?: boolean;
+};`
+ : ''
+}
+
+${
+ isRequestOptions && isMutator
+ ? `// eslint-disable-next-line
+ type ThirdParameter any> = T extends (
+ config: any,
+ httpClient: any,
+ args: infer P,
+) => any
+ ? P
+ : never;`
+ : ''
+}
+
+@Injectable(${
+ provideIn
+ ? `{ providedIn: '${isBoolean(provideIn) ? 'root' : provideIn}' }`
+ : ''
+})
+export class ${title} {
+ constructor(
+ private http: HttpClient,
+ ) {}`;
+
+export const generateAngularFooter: ClientFooterBuilder = ({
+ operationNames,
+}) => {
+ let footer = '};\n\n';
+
+ operationNames.forEach((operationName) => {
+ if (returnTypesToWrite.has(operationName)) {
+ footer += returnTypesToWrite.get(operationName) + '\n';
+ }
+ });
+
+ return footer;
+};
+
+const generateImplementation = (
+ {
+ headers,
+ queryParams,
+ operationName,
+ response,
+ mutator,
+ body,
+ props,
+ verb,
+ override,
+ formData,
+ formUrlEncoded,
+ }: GeneratorVerbOptions,
+ { route, context }: GeneratorOptions,
+) => {
+ const isRequestOptions = override?.requestOptions !== false;
+ const isFormData = override?.formData !== false;
+ const isFormUrlEncoded = override?.formUrlEncoded !== false;
+ const isExactOptionalPropertyTypes =
+ !!context.tsconfig?.compilerOptions?.exactOptionalPropertyTypes;
+ const isBodyVerb = VERBS_WITH_BODY.includes(verb);
+ const bodyForm = generateFormDataAndUrlEncodedFunction({
+ formData,
+ formUrlEncoded,
+ body,
+ isFormData,
+ isFormUrlEncoded,
+ });
+
+ const dataType = response.definition.success || 'unknown';
+
+ returnTypesToWrite.set(
+ operationName,
+ `export type ${pascal(
+ operationName,
+ )}ClientResult = NonNullable<${dataType}>`,
+ );
+
+ if (mutator) {
+ const mutatorConfig = generateMutatorConfig({
+ route,
+ body,
+ headers,
+ queryParams,
+ response,
+ verb,
+ isFormData,
+ isFormUrlEncoded,
+ hasSignal: false,
+ isBodyVerb,
+ isExactOptionalPropertyTypes,
+ });
+
+ const requestOptions = isRequestOptions
+ ? generateMutatorRequestOptions(
+ override?.requestOptions,
+ mutator.hasThirdArg,
+ )
+ : '';
+
+ const propsImplementation =
+ mutator.bodyTypeName && body.definition
+ ? toObjectString(props, 'implementation').replace(
+ new RegExp(`(\\w*):\\s?${body.definition}`),
+ `$1: ${mutator.bodyTypeName}<${body.definition}>`,
+ )
+ : toObjectString(props, 'implementation');
+
+ return ` ${operationName}(\n ${propsImplementation}\n ${
+ isRequestOptions && mutator.hasThirdArg
+ ? `options?: ThirdParameter`
+ : ''
+ }) {${bodyForm}
+ return ${mutator.name}(
+ ${mutatorConfig},
+ this.http,
+ ${requestOptions});
+ }
+ `;
+ }
+
+ const options = generateOptions({
+ route,
+ body,
+ headers,
+ queryParams,
+ response,
+ verb,
+ requestOptions: override?.requestOptions,
+ isFormData,
+ isFormUrlEncoded,
+ isAngular: true,
+ isExactOptionalPropertyTypes,
+ hasSignal: false,
+ });
+
+ return ` ${operationName}(\n ${toObjectString(
+ props,
+ 'implementation',
+ )} ${
+ isRequestOptions ? `options?: HttpClientOptions\n` : ''
+ } ): Observable {${bodyForm}
+ return this.http.${verb}(${options});
+ }
+`;
+};
+
+export const generateAngular: ClientBuilder = (verbOptions, options) => {
+ const imports = generateVerbImports(verbOptions);
+ const implementation = generateImplementation(verbOptions, options);
+
+ return { implementation, imports };
+};
+
+const angularClientBuilder: ClientGeneratorsBuilder = {
+ client: generateAngular,
+ header: generateAngularHeader,
+ dependencies: getAngularDependencies,
+ footer: generateAngularFooter,
+ title: generateAngularTitle,
+};
+
+export const builder = () => angularClientBuilder;
+
+export default builder;
diff --git a/packages/angular/tsconfig.json b/packages/angular/tsconfig.json
new file mode 100644
index 000000000..9e25e6ece
--- /dev/null
+++ b/packages/angular/tsconfig.json
@@ -0,0 +1,4 @@
+{
+ "extends": "../../tsconfig.base.json",
+ "include": ["src/**/*.ts"]
+}
diff --git a/packages/axios/README.md b/packages/axios/README.md
new file mode 100644
index 000000000..8e1596ee8
--- /dev/null
+++ b/packages/axios/README.md
@@ -0,0 +1,28 @@
+[![npm version](https://badge.fury.io/js/orval.svg)](https://badge.fury.io/js/orval)
+[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
+[![tests](https://github.com/anymaniax/orval/actions/workflows/tests.yaml/badge.svg)](https://github.com/anymaniax/orval/actions/workflows/tests.yaml)
+
+
+
+
+
+ Visit orval.dev for docs, guides, API and beer!
+
+
+### Code Generation
+
+`orval` is able to generate client with appropriate type-signatures (TypeScript) from any valid OpenAPI v3 or Swagger v2 specification, either in `yaml` or `json` formats.
+
+`Generate`, `valid`, `cache` and `mock` in your React, Vue, Svelte and Angular applications all with your OpenAPI specification.
+
+### Samples
+
+You can find below some samples
+
+- [react app](https://github.com/anymaniax/orval/tree/master/samples/react-app)
+- [react query](https://github.com/anymaniax/orval/tree/master/samples/react-query)
+- [svelte query](https://github.com/anymaniax/orval/tree/master/samples/svelte-query)
+- [vue query](https://github.com/anymaniax/orval/tree/master/samples/vue-query)
+- [react app with swr](https://github.com/anymaniax/orval/tree/master/samples/react-app-with-swr)
+- [nx fastify react](https://github.com/anymaniax/orval/tree/master/samples/nx-fastify-react)
+- [angular app](https://github.com/anymaniax/orval/tree/master/samples/angular-app)
diff --git a/packages/axios/package.json b/packages/axios/package.json
new file mode 100644
index 000000000..c8a9cd5ae
--- /dev/null
+++ b/packages/axios/package.json
@@ -0,0 +1,18 @@
+{
+ "name": "@orval/axios",
+ "version": "6.11.0-alpha.1",
+ "license": "MIT",
+ "main": "./dist/index.js",
+ "types": "./dist/index.d.ts",
+ "files": [
+ "dist"
+ ],
+ "scripts": {
+ "build": "tsup ./src/index.ts --target node12 --minify --clean --dts --splitting",
+ "dev": "tsup ./src/index.ts --target node12 --clean --watch src",
+ "lint": "eslint src/**/*.ts"
+ },
+ "dependencies": {
+ "@orval/core": "6.11.0-alpha.1"
+ }
+}
diff --git a/packages/axios/src/index.ts b/packages/axios/src/index.ts
new file mode 100644
index 000000000..6e32d1c19
--- /dev/null
+++ b/packages/axios/src/index.ts
@@ -0,0 +1,273 @@
+import {
+ ClientBuilder,
+ ClientDependenciesBuilder,
+ ClientFooterBuilder,
+ ClientGeneratorsBuilder,
+ ClientHeaderBuilder,
+ ClientTitleBuilder,
+ generateFormDataAndUrlEncodedFunction,
+ generateMutatorConfig,
+ generateMutatorRequestOptions,
+ generateOptions,
+ generateVerbImports,
+ GeneratorDependency,
+ GeneratorOptions,
+ GeneratorVerbOptions,
+ isSyntheticDefaultImportsAllow,
+ pascal,
+ sanitize,
+ toObjectString,
+ VERBS_WITH_BODY,
+} from '@orval/core';
+
+const AXIOS_DEPENDENCIES: GeneratorDependency[] = [
+ {
+ exports: [
+ {
+ name: 'axios',
+ default: true,
+ values: true,
+ syntheticDefaultImport: true,
+ },
+ { name: 'AxiosRequestConfig' },
+ { name: 'AxiosResponse' },
+ ],
+ dependency: 'axios',
+ },
+];
+
+const returnTypesToWrite: Map string> = new Map();
+
+export const getAxiosDependencies: ClientDependenciesBuilder = (
+ hasGlobalMutator,
+) => [...(!hasGlobalMutator ? AXIOS_DEPENDENCIES : [])];
+
+const generateAxiosImplementation = (
+ {
+ headers,
+ queryParams,
+ operationName,
+ response,
+ mutator,
+ body,
+ props,
+ verb,
+ override,
+ formData,
+ formUrlEncoded,
+ }: GeneratorVerbOptions,
+ { route, context }: GeneratorOptions,
+) => {
+ const isRequestOptions = override?.requestOptions !== false;
+ const isFormData = override?.formData !== false;
+ const isFormUrlEncoded = override?.formUrlEncoded !== false;
+ const isExactOptionalPropertyTypes =
+ !!context.tsconfig?.compilerOptions?.exactOptionalPropertyTypes;
+
+ const isSyntheticDefaultImportsAllowed = isSyntheticDefaultImportsAllow(
+ context.tsconfig,
+ );
+
+ const bodyForm = generateFormDataAndUrlEncodedFunction({
+ formData,
+ formUrlEncoded,
+ body,
+ isFormData,
+ isFormUrlEncoded,
+ });
+ const isBodyVerb = VERBS_WITH_BODY.includes(verb);
+
+ if (mutator) {
+ const mutatorConfig = generateMutatorConfig({
+ route,
+ body,
+ headers,
+ queryParams,
+ response,
+ verb,
+ isFormData,
+ isFormUrlEncoded,
+ isBodyVerb,
+ hasSignal: false,
+ isExactOptionalPropertyTypes,
+ });
+
+ const requestOptions = isRequestOptions
+ ? generateMutatorRequestOptions(
+ override?.requestOptions,
+ mutator.hasSecondArg,
+ )
+ : '';
+
+ returnTypesToWrite.set(
+ operationName,
+ (title?: string) =>
+ `export type ${pascal(
+ operationName,
+ )}Result = NonNullable['${operationName}']`
+ : `typeof ${operationName}`
+ }>>>`,
+ );
+
+ const propsImplementation =
+ mutator.bodyTypeName && body.definition
+ ? toObjectString(props, 'implementation').replace(
+ new RegExp(`(\\w*):\\s?${body.definition}`),
+ `$1: ${mutator.bodyTypeName}<${body.definition}>`,
+ )
+ : toObjectString(props, 'implementation');
+
+ return `const ${operationName} = (\n ${propsImplementation}\n ${
+ isRequestOptions && mutator.hasSecondArg
+ ? `options?: SecondParameter,`
+ : ''
+ }) => {${bodyForm}
+ return ${mutator.name}<${response.definition.success || 'unknown'}>(
+ ${mutatorConfig},
+ ${requestOptions});
+ }
+ `;
+ }
+
+ const options = generateOptions({
+ route,
+ body,
+ headers,
+ queryParams,
+ response,
+ verb,
+ requestOptions: override?.requestOptions,
+ isFormData,
+ isFormUrlEncoded,
+ isExactOptionalPropertyTypes,
+ hasSignal: false,
+ });
+
+ returnTypesToWrite.set(
+ operationName,
+ () =>
+ `export type ${pascal(operationName)}Result = AxiosResponse<${
+ response.definition.success || 'unknown'
+ }>`,
+ );
+
+ return `const ${operationName} = >(\n ${toObjectString(props, 'implementation')} ${
+ isRequestOptions ? `options?: AxiosRequestConfig\n` : ''
+ } ): Promise => {${bodyForm}
+ return axios${
+ !isSyntheticDefaultImportsAllowed ? '.default' : ''
+ }.${verb}(${options});
+ }
+`;
+};
+
+export const generateAxiosTitle: ClientTitleBuilder = (title) => {
+ const sanTitle = sanitize(title);
+ return `get${pascal(sanTitle)}`;
+};
+
+export const generateAxiosHeader: ClientHeaderBuilder = ({
+ title,
+ isRequestOptions,
+ isMutator,
+ noFunction,
+}) => `
+${
+ isRequestOptions && isMutator
+ ? `// eslint-disable-next-line
+ type SecondParameter any> = T extends (
+ config: any,
+ args: infer P,
+) => any
+ ? P
+ : never;\n\n`
+ : ''
+}
+ ${!noFunction ? `export const ${title} = () => {\n` : ''}`;
+
+export const generateAxiosFooter: ClientFooterBuilder = ({
+ operationNames,
+ title,
+ noFunction,
+ hasMutator,
+ hasAwaitedType,
+}) => {
+ let footer = '';
+
+ if (!noFunction) {
+ footer += `return {${operationNames.join(',')}}};\n`;
+ }
+
+ if (hasMutator && !hasAwaitedType) {
+ footer += `\ntype AwaitedInput = PromiseLike | T;\n
+ type Awaited = O extends AwaitedInput ? T : never;
+\n`;
+ }
+
+ operationNames.forEach((operationName) => {
+ if (returnTypesToWrite.has(operationName)) {
+ const func = returnTypesToWrite.get(operationName)!;
+ footer += func(!noFunction ? title : undefined) + '\n';
+ }
+ });
+
+ return footer;
+};
+
+export const generateAxios: ClientBuilder = (
+ verbOptions: GeneratorVerbOptions,
+ options: GeneratorOptions,
+) => {
+ const imports = generateVerbImports(verbOptions);
+ const implementation = generateAxiosImplementation(verbOptions, options);
+
+ return { implementation, imports };
+};
+
+export const generateAxiosFunctions: ClientBuilder = (
+ verbOptions,
+ options,
+ outputClient,
+) => {
+ const { implementation, imports } = generateAxios(
+ verbOptions,
+ options,
+ outputClient,
+ );
+
+ return {
+ implementation: 'export ' + implementation,
+ imports,
+ };
+};
+
+const axiosClientBuilder: ClientGeneratorsBuilder = {
+ client: generateAxios,
+ header: generateAxiosHeader,
+ dependencies: getAxiosDependencies,
+ footer: generateAxiosFooter,
+ title: generateAxiosTitle,
+};
+
+const axiosFunctionsClientBuilder: ClientGeneratorsBuilder = {
+ client: generateAxiosFunctions,
+ header: (options) => generateAxiosHeader({ ...options, noFunction: true }),
+ dependencies: getAxiosDependencies,
+ footer: (options) => generateAxiosFooter({ ...options, noFunction: true }),
+ title: generateAxiosTitle,
+};
+
+export const builder = ({ type }: { type: 'axios' | 'axios-functions' }) => {
+ switch (type) {
+ case 'axios':
+ return axiosClientBuilder;
+ case 'axios-functions':
+ return axiosFunctionsClientBuilder;
+ }
+};
+
+export default builder;
diff --git a/packages/axios/tsconfig.json b/packages/axios/tsconfig.json
new file mode 100644
index 000000000..9e25e6ece
--- /dev/null
+++ b/packages/axios/tsconfig.json
@@ -0,0 +1,4 @@
+{
+ "extends": "../../tsconfig.base.json",
+ "include": ["src/**/*.ts"]
+}
diff --git a/packages/core/README.md b/packages/core/README.md
new file mode 100644
index 000000000..8e1596ee8
--- /dev/null
+++ b/packages/core/README.md
@@ -0,0 +1,28 @@
+[![npm version](https://badge.fury.io/js/orval.svg)](https://badge.fury.io/js/orval)
+[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
+[![tests](https://github.com/anymaniax/orval/actions/workflows/tests.yaml/badge.svg)](https://github.com/anymaniax/orval/actions/workflows/tests.yaml)
+
+
+
+
+
+ Visit orval.dev for docs, guides, API and beer!
+
+
+### Code Generation
+
+`orval` is able to generate client with appropriate type-signatures (TypeScript) from any valid OpenAPI v3 or Swagger v2 specification, either in `yaml` or `json` formats.
+
+`Generate`, `valid`, `cache` and `mock` in your React, Vue, Svelte and Angular applications all with your OpenAPI specification.
+
+### Samples
+
+You can find below some samples
+
+- [react app](https://github.com/anymaniax/orval/tree/master/samples/react-app)
+- [react query](https://github.com/anymaniax/orval/tree/master/samples/react-query)
+- [svelte query](https://github.com/anymaniax/orval/tree/master/samples/svelte-query)
+- [vue query](https://github.com/anymaniax/orval/tree/master/samples/vue-query)
+- [react app with swr](https://github.com/anymaniax/orval/tree/master/samples/react-app-with-swr)
+- [nx fastify react](https://github.com/anymaniax/orval/tree/master/samples/nx-fastify-react)
+- [angular app](https://github.com/anymaniax/orval/tree/master/samples/angular-app)
diff --git a/packages/core/package.json b/packages/core/package.json
new file mode 100644
index 000000000..71f113e24
--- /dev/null
+++ b/packages/core/package.json
@@ -0,0 +1,53 @@
+{
+ "name": "@orval/core",
+ "version": "6.11.0-alpha.1",
+ "license": "MIT",
+ "main": "./dist/index.js",
+ "types": "./dist/index.d.ts",
+ "files": [
+ "dist"
+ ],
+ "scripts": {
+ "build": "tsup ./src/index.ts --target node12 --minify --clean --dts --splitting",
+ "dev": "tsup ./src/index.ts --target node12 --clean --watch src",
+ "lint": "eslint src/**/*.ts",
+ "test": "vitest --global test.ts"
+ },
+ "devDependencies": {
+ "@types/chalk": "^2.2.0",
+ "@types/debug": "^4.1.7",
+ "@types/fs-extra": "^9.0.13",
+ "@types/inquirer": "^8.2.2",
+ "@types/lodash.get": "^4.4.7",
+ "@types/lodash.omit": "^4.5.7",
+ "@types/lodash.uniq": "^4.5.7",
+ "@types/lodash.uniqby": "^4.7.7",
+ "@types/lodash.uniqwith": "^4.5.7",
+ "@types/micromatch": "^4.0.2",
+ "@types/validator": "^13.7.5"
+ },
+ "dependencies": {
+ "@apidevtools/swagger-parser": "^10.1.0",
+ "acorn": "^8.8.0",
+ "ajv": "^8.11.0",
+ "chalk": "^4.1.2",
+ "compare-versions": "^4.1.3",
+ "debug": "^4.3.4",
+ "esbuild": "^0.15.3",
+ "esutils": "2.0.3",
+ "fs-extra": "^10.1.0",
+ "globby": "11.1.0",
+ "ibm-openapi-validator": "^0.88.0",
+ "lodash.get": "^4.4.2",
+ "lodash.omit": "^4.5.0",
+ "lodash.uniq": "^4.5.0",
+ "lodash.uniqby": "^4.7.0",
+ "lodash.uniqwith": "^4.5.0",
+ "micromatch": "^4.0.5",
+ "openapi3-ts": "^3.0.0",
+ "swagger2openapi": "^7.0.8",
+ "upath": "^2.0.1",
+ "url": "^0.11.0",
+ "validator": "^13.7.0"
+ }
+}
diff --git a/packages/core/src/constants.ts b/packages/core/src/constants.ts
new file mode 100644
index 000000000..ff65cf8a8
--- /dev/null
+++ b/packages/core/src/constants.ts
@@ -0,0 +1,30 @@
+import { Verbs } from './types';
+
+export const generalJSTypes = [
+ 'number',
+ 'string',
+ 'null',
+ 'unknown',
+ 'undefined',
+ 'object',
+ 'blob',
+];
+
+export const generalJSTypesWithArray = generalJSTypes.reduce(
+ (acc, type) => {
+ acc.push(type, `Array<${type}>`, `${type}[]`);
+
+ return acc;
+ },
+ [],
+);
+
+export const VERBS_WITH_BODY = [
+ Verbs.POST,
+ Verbs.PUT,
+ Verbs.PATCH,
+ Verbs.DELETE,
+];
+
+export const URL_REGEX =
+ /^(?:http(s)?:\/\/)?[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~:/?#[\]@!\$&'\(\)\*\+,;=.]+$/;
diff --git a/packages/core/src/generators/component-definition.ts b/packages/core/src/generators/component-definition.ts
new file mode 100644
index 000000000..dfa10a986
--- /dev/null
+++ b/packages/core/src/generators/component-definition.ts
@@ -0,0 +1,67 @@
+import isEmpty from 'lodash/isEmpty';
+import {
+ ComponentsObject,
+ ReferenceObject,
+ RequestBodyObject,
+ ResponseObject,
+} from 'openapi3-ts';
+import { getResReqTypes } from '../getters';
+import { ContextSpecs, GeneratorSchema } from '../types';
+import { jsDoc, pascal, sanitize } from '../utils';
+
+export const generateComponentDefinition = (
+ responses:
+ | ComponentsObject['responses']
+ | ComponentsObject['requestBodies'] = {},
+ context: ContextSpecs,
+ suffix: string,
+): GeneratorSchema[] => {
+ if (isEmpty(responses)) {
+ return [];
+ }
+
+ return Object.entries(responses).reduce(
+ (
+ acc,
+ [name, response]: [
+ string,
+ ReferenceObject | RequestBodyObject | ResponseObject,
+ ],
+ ) => {
+ const allResponseTypes = getResReqTypes(
+ [[suffix, response]],
+ name,
+ context,
+ 'void',
+ );
+
+ const imports = allResponseTypes.flatMap(({ imports }) => imports);
+ const schemas = allResponseTypes.flatMap(({ schemas }) => schemas);
+
+ const type = allResponseTypes.map(({ value }) => value).join(' | ');
+
+ const modelName = sanitize(`${pascal(name)}${suffix}`, {
+ underscore: '_',
+ whitespace: '_',
+ dash: true,
+ es5keyword: true,
+ es5IdentifierName: true,
+ });
+ const doc = jsDoc(response as ResponseObject | RequestBodyObject);
+ const model = `${doc}export type ${modelName} = ${type || 'unknown'};\n`;
+
+ acc.push(...schemas);
+
+ if (modelName !== type) {
+ acc.push({
+ name: modelName,
+ model,
+ imports,
+ });
+ }
+
+ return acc;
+ },
+ [] as GeneratorSchema[],
+ );
+};
diff --git a/packages/core/src/generators/imports.ts b/packages/core/src/generators/imports.ts
new file mode 100644
index 000000000..c54976ca7
--- /dev/null
+++ b/packages/core/src/generators/imports.ts
@@ -0,0 +1,283 @@
+import uniq from 'lodash.uniq';
+import uniqWith from 'lodash.uniqwith';
+import { join } from 'upath';
+import {
+ GeneratorImport,
+ GeneratorMutator,
+ GeneratorVerbOptions,
+} from '../types';
+import { camel } from '../utils';
+
+export const generateImports = ({
+ imports = [],
+ target,
+ isRootKey,
+ specsName,
+}: {
+ imports: GeneratorImport[];
+ target: string;
+ isRootKey: boolean;
+ specsName: Record;
+}) => {
+ if (!imports.length) {
+ return '';
+ }
+
+ return uniqWith(
+ imports,
+ (a, b) =>
+ a.name === b.name && a.default === b.default && a.specKey === b.specKey,
+ )
+ .sort()
+ .map(({ specKey, name, values, alias }) => {
+ if (specKey) {
+ const path = specKey !== target ? specsName[specKey] : '';
+
+ if (!isRootKey && specKey) {
+ return `import ${!values ? 'type ' : ''}{ ${name}${
+ alias ? ` as ${alias}` : ''
+ } } from \'../${join(path, camel(name))}\';`;
+ }
+
+ return `import ${!values ? 'type ' : ''}{ ${name}${
+ alias ? ` as ${alias}` : ''
+ } } from \'./${join(path, camel(name))}\';`;
+ }
+
+ return `import ${!values ? 'type ' : ''}{ ${name}${
+ alias ? ` as ${alias}` : ''
+ } } from \'./${camel(name)}\';`;
+ })
+ .join('\n');
+};
+
+export const generateMutatorImports = ({
+ mutators,
+ implementation,
+ oneMore,
+}: {
+ mutators: GeneratorMutator[];
+ implementation?: string;
+ oneMore?: boolean;
+}) => {
+ const imports = uniqWith(
+ mutators,
+ (a, b) => a.name === b.name && a.default === b.default,
+ )
+ .map((mutator) => {
+ const path = `${oneMore ? '../' : ''}${mutator.path}`;
+ const importDefault = mutator.default
+ ? mutator.name
+ : `{ ${mutator.name} }`;
+
+ let dep = `import ${importDefault} from '${path}'`;
+
+ if (implementation && (mutator.hasErrorType || mutator.bodyTypeName)) {
+ let errorImportName = '';
+ if (
+ mutator.hasErrorType &&
+ implementation.includes(mutator.errorTypeName)
+ ) {
+ errorImportName = mutator.default
+ ? `ErrorType as ${mutator.errorTypeName}`
+ : mutator.errorTypeName;
+ }
+
+ let bodyImportName = '';
+ if (
+ mutator.bodyTypeName &&
+ implementation.includes(mutator.bodyTypeName)
+ ) {
+ bodyImportName = mutator.default
+ ? `BodyType as ${mutator.bodyTypeName}`
+ : mutator.bodyTypeName;
+ }
+
+ if (bodyImportName || errorImportName) {
+ dep += '\n';
+ dep += `import type { ${errorImportName}${
+ errorImportName && bodyImportName ? ', ' : ''
+ }${bodyImportName} } from '${path}'`;
+ }
+ }
+
+ return dep;
+ })
+ .join('\n');
+
+ return imports ? imports + '\n' : '';
+};
+
+const generateDependency = ({
+ deps,
+ isAllowSyntheticDefaultImports,
+ dependency,
+ specsName,
+ key,
+ onlyTypes,
+}: {
+ key: string;
+ deps: GeneratorImport[];
+ dependency: string;
+ specsName: Record;
+ isAllowSyntheticDefaultImports: boolean;
+ onlyTypes: boolean;
+}) => {
+ const defaultDep = deps.find(
+ (e) =>
+ e.default &&
+ (isAllowSyntheticDefaultImports || !e.syntheticDefaultImport),
+ );
+ const syntheticDefaultImportDep = !isAllowSyntheticDefaultImports
+ ? deps.find((e) => e.syntheticDefaultImport)
+ : undefined;
+
+ const depsString = uniq(
+ deps
+ .filter((e) => !e.default && !e.syntheticDefaultImport)
+ .map(({ name, alias }) => (alias ? `${name} as ${alias}` : name)),
+ ).join(',\n ');
+
+ let importString = '';
+
+ const syntheticDefaultImport = syntheticDefaultImportDep
+ ? `import * as ${syntheticDefaultImportDep.name} from '${dependency}';`
+ : '';
+
+ if (syntheticDefaultImport) {
+ if (deps.length === 1) {
+ return syntheticDefaultImport;
+ }
+ importString += `${syntheticDefaultImport}\n`;
+ }
+
+ importString += `import ${onlyTypes ? 'type ' : ''}${
+ defaultDep ? `${defaultDep.name}${depsString ? ',' : ''}` : ''
+ }${depsString ? `{\n ${depsString}\n}` : ''} from '${dependency}${
+ key !== 'default' && specsName[key] ? `/${specsName[key]}` : ''
+ }'`;
+
+ return importString;
+};
+
+export const addDependency = ({
+ implementation,
+ exports,
+ dependency,
+ specsName,
+ hasSchemaDir,
+ isAllowSyntheticDefaultImports,
+}: {
+ implementation: string;
+ exports: GeneratorImport[];
+ dependency: string;
+ specsName: Record;
+ hasSchemaDir: boolean;
+ isAllowSyntheticDefaultImports: boolean;
+}) => {
+ const toAdds = exports.filter((e) =>
+ implementation.includes(e.alias || e.name),
+ );
+
+ if (!toAdds.length) {
+ return undefined;
+ }
+
+ const groupedBySpecKey = toAdds.reduce<
+ Record
+ >((acc, dep) => {
+ const key = hasSchemaDir && dep.specKey ? dep.specKey : 'default';
+
+ if (
+ dep.values &&
+ (isAllowSyntheticDefaultImports || !dep.syntheticDefaultImport)
+ ) {
+ acc[key] = {
+ ...acc[key],
+ values: [...(acc[key]?.values ?? []), dep],
+ };
+
+ return acc;
+ }
+
+ acc[key] = {
+ ...acc[key],
+ types: [...(acc[key]?.types ?? []), dep],
+ };
+
+ return acc;
+ }, {});
+
+ return Object.entries(groupedBySpecKey)
+ .map(([key, { values, types }]) => {
+ let dep = '';
+
+ if (values) {
+ dep += generateDependency({
+ deps: values,
+ isAllowSyntheticDefaultImports,
+ dependency,
+ specsName,
+ key,
+ onlyTypes: false,
+ });
+ }
+
+ if (types) {
+ if (values) {
+ dep += '\n';
+ }
+ dep += generateDependency({
+ deps: types,
+ isAllowSyntheticDefaultImports,
+ dependency,
+ specsName,
+ key,
+ onlyTypes: true,
+ });
+ }
+
+ return dep;
+ })
+ .join('\n');
+};
+
+export const generateDependencyImports = (
+ implementation: string,
+ imports: {
+ exports: GeneratorImport[];
+ dependency: string;
+ }[],
+ specsName: Record,
+ hasSchemaDir: boolean,
+ isAllowSyntheticDefaultImports: boolean,
+): string => {
+ const dependencies = imports
+ .map((dep) =>
+ addDependency({
+ ...dep,
+ implementation,
+ specsName,
+ hasSchemaDir,
+ isAllowSyntheticDefaultImports,
+ }),
+ )
+ .filter(Boolean)
+ .join('\n');
+
+ return dependencies ? dependencies + '\n' : '';
+};
+
+export const generateVerbImports = ({
+ response,
+ body,
+ queryParams,
+ headers,
+ params,
+}: GeneratorVerbOptions): GeneratorImport[] => [
+ ...response.imports,
+ ...body.imports,
+ ...(queryParams ? [{ name: queryParams.schema.name }] : []),
+ ...(headers ? [{ name: headers.schema.name }] : []),
+ ...params.flatMap(({ imports }) => imports),
+];
diff --git a/packages/core/src/generators/index.ts b/packages/core/src/generators/index.ts
new file mode 100644
index 000000000..28f312d75
--- /dev/null
+++ b/packages/core/src/generators/index.ts
@@ -0,0 +1,8 @@
+export * from './component-definition';
+export * from './imports';
+export * from './models-inline';
+export * from './mutator';
+export * from './options';
+export * from './parameter-definition';
+export * from './schema-definition';
+export * from './verbs-options';
diff --git a/packages/core/src/generators/interface.ts b/packages/core/src/generators/interface.ts
new file mode 100644
index 000000000..d6f110f2d
--- /dev/null
+++ b/packages/core/src/generators/interface.ts
@@ -0,0 +1,67 @@
+import { SchemaObject } from 'openapi3-ts';
+import { generalJSTypesWithArray } from '../constants';
+import { getScalar } from '../getters';
+import { ContextSpecs } from '../types';
+import { jsDoc } from '../utils';
+
+/**
+ * Generate the interface string
+ * A eslint|tslint comment is insert if the resulted object is empty
+ *
+ * @param name interface name
+ * @param schema
+ */
+export const generateInterface = ({
+ name,
+ schema,
+ context,
+ suffix,
+}: {
+ name: string;
+ schema: SchemaObject;
+ context: ContextSpecs;
+ suffix: string;
+}) => {
+ const scalar = getScalar({
+ item: schema,
+ name,
+ context,
+ });
+ const isEmptyObject = scalar.value === '{}';
+
+ let model = '';
+
+ model += jsDoc(schema);
+
+ if (isEmptyObject) {
+ if (context.tslint) {
+ model += '// tslint:disable-next-line:no-empty-interface\n';
+ } else {
+ model +=
+ '// eslint-disable-next-line @typescript-eslint/no-empty-interface\n';
+ }
+ }
+
+ if (
+ !generalJSTypesWithArray.includes(scalar.value) &&
+ !context?.override?.useTypeOverInterfaces
+ ) {
+ model += `export interface ${name} ${scalar.value}\n`;
+ } else {
+ model += `export type ${name} = ${scalar.value};\n`;
+ }
+
+ // Filter out imports that refer to the type defined in current file (OpenAPI recursive schema definitions)
+ const externalModulesImportsOnly = scalar.imports.filter(
+ (importName) => importName.name !== name,
+ );
+
+ return [
+ ...scalar.schemas,
+ {
+ name,
+ model,
+ imports: externalModulesImportsOnly,
+ },
+ ];
+};
diff --git a/packages/core/src/generators/models-inline.ts b/packages/core/src/generators/models-inline.ts
new file mode 100644
index 000000000..1ec05392b
--- /dev/null
+++ b/packages/core/src/generators/models-inline.ts
@@ -0,0 +1,17 @@
+import { GeneratorSchema } from '../types';
+
+export const generateModelInline = (acc: string, model: string): string =>
+ acc + `${model}\n`;
+
+export const generateModelsInline = (
+ obj: Record,
+): string => {
+ const schemas = Object.values(obj)
+ .flatMap((it) => it)
+ .sort((a, b) => (a.imports.some((i) => i.name === b.name) ? 1 : -1));
+
+ return schemas.reduce(
+ (acc, { model }) => generateModelInline(acc, model),
+ '',
+ );
+};
diff --git a/packages/core/src/generators/mutator.ts b/packages/core/src/generators/mutator.ts
new file mode 100644
index 000000000..3261863cc
--- /dev/null
+++ b/packages/core/src/generators/mutator.ts
@@ -0,0 +1,262 @@
+import { Parser } from 'acorn';
+import chalk from 'chalk';
+import { readFile } from 'fs-extra';
+import {
+ GeneratorMutator,
+ GeneratorMutatorParsingInfo,
+ NormalizedMutator,
+ Tsconfig,
+} from '../types';
+import {
+ createLogger,
+ getFileInfo,
+ loadFile,
+ pascal,
+ relativeSafe,
+} from '../utils';
+
+export const BODY_TYPE_NAME = 'BodyType';
+
+const getImport = (output: string, mutator: NormalizedMutator) => {
+ const outputFileInfo = getFileInfo(output);
+ const mutatorFileInfo = getFileInfo(mutator.path);
+ const { pathWithoutExtension } = getFileInfo(
+ relativeSafe(outputFileInfo.dirname, mutatorFileInfo.path),
+ );
+
+ return pathWithoutExtension;
+};
+
+export const generateMutator = async ({
+ output,
+ mutator,
+ name,
+ workspace,
+ tsconfig,
+}: {
+ output?: string;
+ mutator?: NormalizedMutator;
+ name: string;
+ workspace: string;
+ tsconfig?: Tsconfig;
+}): Promise => {
+ if (!mutator || !output) {
+ return;
+ }
+ const isDefault = mutator.default;
+ const importName = mutator.name ? mutator.name : `${name}Mutator`;
+ const importPath = mutator.path;
+
+ const rawFile = await readFile(importPath, 'utf8');
+
+ const hasErrorType =
+ rawFile.includes('export type ErrorType') ||
+ rawFile.includes('export interface ErrorType');
+
+ const hasBodyType =
+ rawFile.includes(`export type ${BODY_TYPE_NAME}`) ||
+ rawFile.includes(`export interface ${BODY_TYPE_NAME}`);
+
+ const errorTypeName = !mutator.default
+ ? 'ErrorType'
+ : `${pascal(name)}ErrorType`;
+
+ const bodyTypeName = !mutator.default
+ ? BODY_TYPE_NAME
+ : `${pascal(name)}${BODY_TYPE_NAME}`;
+
+ const { file, cached } = await loadFile(importPath, {
+ isDefault: false,
+ root: workspace,
+ alias: mutator.alias,
+ tsconfig,
+ load: false,
+ });
+
+ if (file) {
+ const mutatorInfoName = isDefault ? 'default' : mutator.name!;
+ const mutatorInfo = parseFile(file, mutatorInfoName);
+
+ if (!mutatorInfo) {
+ createLogger().error(
+ chalk.red(
+ `Your mutator file doesn't have the ${mutatorInfoName} exported function`,
+ ),
+ );
+ process.exit(1);
+ }
+
+ const path = getImport(output, mutator);
+
+ const isHook = mutator.name
+ ? !!mutator.name.startsWith('use') && !mutatorInfo.numberOfParams
+ : !mutatorInfo.numberOfParams;
+
+ return {
+ name: mutator.name || !isHook ? importName : `use${pascal(importName)}`,
+ path,
+ default: isDefault,
+ hasErrorType,
+ errorTypeName,
+ hasSecondArg: !isHook
+ ? mutatorInfo.numberOfParams > 1
+ : mutatorInfo.returnNumberOfParams! > 1,
+ hasThirdArg: mutatorInfo.numberOfParams > 2,
+ isHook,
+ ...(hasBodyType ? { bodyTypeName } : {}),
+ };
+ } else {
+ const path = getImport(output, mutator);
+
+ if (!cached) {
+ createLogger().warn(
+ chalk.yellow(`Failed to parse provided mutator function`),
+ );
+ }
+
+ return {
+ name: importName,
+ path,
+ default: isDefault,
+ hasSecondArg: false,
+ hasThirdArg: false,
+ isHook: false,
+ hasErrorType,
+ errorTypeName,
+ ...(hasBodyType ? { bodyTypeName } : {}),
+ };
+ }
+};
+
+const parseFile = (
+ file: string,
+ name: string,
+): GeneratorMutatorParsingInfo | undefined => {
+ try {
+ const ast = Parser.parse(file, { ecmaVersion: 6 }) as any;
+
+ const node = ast?.body?.find((childNode: any) => {
+ if (childNode.type === 'ExpressionStatement') {
+ if (
+ childNode.expression.arguments?.[1]?.properties?.some(
+ (p: any) => p.key?.name === name,
+ )
+ ) {
+ return true;
+ }
+
+ if (childNode.expression.left?.property?.name === name) {
+ return true;
+ }
+
+ return childNode.expression.right?.properties?.some(
+ (p: any) => p.key.name === name,
+ );
+ }
+ });
+
+ if (!node) {
+ return;
+ }
+
+ if (node.expression.type === 'AssignmentExpression') {
+ if (
+ node.expression.right.type === 'FunctionExpression' ||
+ node.expression.right.type === 'ArrowFunctionExpression'
+ ) {
+ return {
+ numberOfParams: node.expression.right.params.length,
+ };
+ }
+
+ if (node.expression.right.name) {
+ return parseFunction(ast, node.expression.right.name);
+ }
+
+ const property = node.expression.right?.properties.find(
+ (p: any) => p.key.name === name,
+ );
+
+ if (property.value.name) {
+ return parseFunction(ast, property.value.name);
+ }
+
+ if (
+ property.value.type === 'FunctionExpression' ||
+ property.value.type === 'ArrowFunctionExpression'
+ ) {
+ return {
+ numberOfParams: property.value.params.length,
+ };
+ }
+
+ return;
+ }
+
+ const property = node.expression.arguments[1].properties.find(
+ (p: any) => p.key?.name === name,
+ );
+
+ return parseFunction(ast, property.value.body.name);
+ } catch (e) {
+ return;
+ }
+};
+
+const parseFunction = (
+ ast: any,
+ name: string,
+): GeneratorMutatorParsingInfo | undefined => {
+ const node = ast?.body?.find((childNode: any) => {
+ if (childNode.type === 'VariableDeclaration') {
+ return childNode.declarations.find((d: any) => d.id.name === name);
+ }
+ if (
+ childNode.type === 'FunctionDeclaration' &&
+ childNode.id.name === name
+ ) {
+ return childNode;
+ }
+ });
+
+ if (!node) {
+ return;
+ }
+
+ if (node.type === 'FunctionDeclaration') {
+ const returnStatement = node.body?.body?.find(
+ (b: any) => b.type === 'ReturnStatement',
+ );
+
+ if (returnStatement?.argument?.params) {
+ return {
+ numberOfParams: node.params.length,
+ returnNumberOfParams: returnStatement.argument.params.length,
+ };
+ }
+ return {
+ numberOfParams: node.params.length,
+ };
+ }
+
+ const declaration = node.declarations.find((d: any) => d.id.name === name);
+
+ if (declaration.init.name) {
+ return parseFunction(ast, declaration.init.name);
+ }
+
+ const returnStatement = declaration.init.body?.body?.find(
+ (b: any) => b.type === 'ReturnStatement',
+ );
+
+ if (returnStatement?.argument?.params) {
+ return {
+ numberOfParams: declaration.init.params.length,
+ returnNumberOfParams: returnStatement.argument.params.length,
+ };
+ }
+
+ return {
+ numberOfParams: declaration.init.params.length,
+ };
+};
diff --git a/packages/core/src/generators/options.ts b/packages/core/src/generators/options.ts
new file mode 100644
index 000000000..85cf1a916
--- /dev/null
+++ b/packages/core/src/generators/options.ts
@@ -0,0 +1,308 @@
+import { VERBS_WITH_BODY } from '../constants';
+import {
+ GeneratorMutator,
+ GeneratorSchema,
+ GetterBody,
+ GetterQueryParam,
+ GetterResponse,
+ Verbs,
+} from '../types';
+import { isObject, stringify } from '../utils';
+
+export const generateBodyOptions = (
+ body: GetterBody,
+ isFormData: boolean,
+ isFormUrlEncoded: boolean,
+) => {
+ if (isFormData && body.formData) {
+ return '\n formData,';
+ }
+
+ if (isFormUrlEncoded && body.formUrlEncoded) {
+ return '\n formUrlEncoded,';
+ }
+
+ if (body.implementation) {
+ return `\n ${body.implementation},`;
+ }
+
+ return '';
+};
+
+export const generateAxiosOptions = ({
+ response,
+ isExactOptionalPropertyTypes,
+ queryParams,
+ headers,
+ requestOptions,
+ hasSignal,
+}: {
+ response: GetterResponse;
+ isExactOptionalPropertyTypes: boolean;
+ queryParams?: GeneratorSchema;
+ headers?: GeneratorSchema;
+ requestOptions?: object | boolean;
+ hasSignal: boolean;
+}) => {
+ const isRequestOptions = requestOptions !== false;
+ if (!queryParams && !headers && !response.isBlob) {
+ if (isRequestOptions) {
+ return 'options';
+ }
+ if (hasSignal) {
+ return !isExactOptionalPropertyTypes
+ ? 'signal'
+ : '...(signal ? { signal } : {})';
+ }
+ return '';
+ }
+
+ let value = '';
+
+ if (!isRequestOptions) {
+ if (queryParams) {
+ value += '\n params,';
+ }
+
+ if (headers) {
+ value += '\n headers,';
+ }
+
+ if (hasSignal) {
+ value += !isExactOptionalPropertyTypes
+ ? '\n signal,'
+ : '\n ...(signal ? { signal } : {}),';
+ }
+ }
+
+ if (
+ response.isBlob &&
+ (!isObject(requestOptions) ||
+ !requestOptions.hasOwnProperty('responseType'))
+ ) {
+ value += `\n responseType: 'blob',`;
+ }
+
+ if (isObject(requestOptions)) {
+ value += `\n ${stringify(requestOptions)?.slice(1, -1)}`;
+ }
+
+ if (isRequestOptions) {
+ value += '\n ...options,';
+
+ if (queryParams) {
+ value += '\n params: {...params, ...options?.params},';
+ }
+
+ if (headers) {
+ value += '\n headers: {...headers, ...options?.headers},';
+ }
+ }
+
+ return value;
+};
+
+export const generateOptions = ({
+ route,
+ body,
+ headers,
+ queryParams,
+ response,
+ verb,
+ requestOptions,
+ isFormData,
+ isFormUrlEncoded,
+ isAngular,
+ isExactOptionalPropertyTypes,
+ hasSignal,
+}: {
+ route: string;
+ body: GetterBody;
+ headers?: GetterQueryParam;
+ queryParams?: GetterQueryParam;
+ response: GetterResponse;
+ verb: Verbs;
+ requestOptions?: object | boolean;
+ isFormData: boolean;
+ isFormUrlEncoded: boolean;
+ isAngular?: boolean;
+ isExactOptionalPropertyTypes: boolean;
+ hasSignal: boolean;
+}) => {
+ const isBodyVerb = VERBS_WITH_BODY.includes(verb);
+ const bodyOptions = isBodyVerb
+ ? generateBodyOptions(body, isFormData, isFormUrlEncoded)
+ : '';
+
+ const axiosOptions = generateAxiosOptions({
+ response,
+ queryParams: queryParams?.schema,
+ headers: headers?.schema,
+ requestOptions,
+ isExactOptionalPropertyTypes,
+ hasSignal,
+ });
+
+ const options = axiosOptions ? `{${axiosOptions}}` : '';
+
+ if (verb === Verbs.DELETE) {
+ if (!bodyOptions) {
+ return `\n \`${route}\`,${
+ axiosOptions === 'options' ? axiosOptions : options
+ }\n `;
+ }
+
+ return `\n \`${route}\`,{${
+ isAngular ? 'body' : 'data'
+ }:${bodyOptions} ${
+ axiosOptions === 'options' ? `...${axiosOptions}` : axiosOptions
+ }}\n `;
+ }
+
+ return `\n \`${route}\`,${
+ isBodyVerb ? bodyOptions || 'undefined,' : ''
+ }${axiosOptions === 'options' ? axiosOptions : options}\n `;
+};
+
+export const generateBodyMutatorConfig = (
+ body: GetterBody,
+ isFormData: boolean,
+ isFormUrlEncoded: boolean,
+) => {
+ if (isFormData && body.formData) {
+ return ',\n data: formData';
+ }
+
+ if (isFormUrlEncoded && body.formUrlEncoded) {
+ return ',\n data: formUrlEncoded';
+ }
+
+ if (body.implementation) {
+ return `,\n data: ${body.implementation}`;
+ }
+
+ return '';
+};
+
+export const generateQueryParamsAxiosConfig = (
+ response: GetterResponse,
+ queryParams?: GetterQueryParam,
+) => {
+ if (!queryParams && !response.isBlob) {
+ return '';
+ }
+
+ let value = '';
+
+ if (queryParams) {
+ value += ',\n params';
+ }
+
+ if (response.isBlob) {
+ value += `,\n responseType: 'blob'`;
+ }
+
+ return value;
+};
+
+export const generateMutatorConfig = ({
+ route,
+ body,
+ headers,
+ queryParams,
+ response,
+ verb,
+ isFormData,
+ isFormUrlEncoded,
+ isBodyVerb,
+ hasSignal,
+ isExactOptionalPropertyTypes,
+}: {
+ route: string;
+ body: GetterBody;
+ headers?: GetterQueryParam;
+ queryParams?: GetterQueryParam;
+ response: GetterResponse;
+ verb: Verbs;
+ isFormData: boolean;
+ isFormUrlEncoded: boolean;
+ isBodyVerb: boolean;
+ hasSignal: boolean;
+ isExactOptionalPropertyTypes: boolean;
+}) => {
+ const bodyOptions = isBodyVerb
+ ? generateBodyMutatorConfig(body, isFormData, isFormUrlEncoded)
+ : '';
+
+ const queryParamsOptions = generateQueryParamsAxiosConfig(
+ response,
+ queryParams,
+ );
+
+ const headerOptions = body.contentType
+ ? `,\n headers: {'Content-Type': '${body.contentType}', ${
+ headers ? '...headers' : ''
+ }}`
+ : headers
+ ? ',\n headers'
+ : '';
+
+ return `{url: \`${route}\`, method: '${verb}'${headerOptions}${bodyOptions}${queryParamsOptions}${
+ !isBodyVerb && hasSignal
+ ? `, ${
+ isExactOptionalPropertyTypes
+ ? '...(signal ? { signal }: {})'
+ : 'signal'
+ }`
+ : ''
+ }\n }`;
+};
+
+export const generateMutatorRequestOptions = (
+ requestOptions: boolean | object | undefined,
+ hasSecondArgument: boolean,
+) => {
+ if (!hasSecondArgument) {
+ return isObject(requestOptions)
+ ? stringify(requestOptions)?.slice(1, -1)
+ : '';
+ }
+
+ if (isObject(requestOptions)) {
+ return `{${stringify(requestOptions)?.slice(1, -1)} ...options}`;
+ }
+
+ return 'options';
+};
+
+export const generateFormDataAndUrlEncodedFunction = ({
+ body,
+ formData,
+ formUrlEncoded,
+ isFormData,
+ isFormUrlEncoded,
+}: {
+ body: GetterBody;
+ formData?: GeneratorMutator;
+ formUrlEncoded?: GeneratorMutator;
+ isFormData: boolean;
+ isFormUrlEncoded: boolean;
+}) => {
+ if (isFormData && body.formData) {
+ if (formData) {
+ return `const formData = ${formData.name}(${body.implementation})`;
+ }
+
+ return body.formData;
+ }
+
+ if (isFormUrlEncoded && body.formUrlEncoded) {
+ if (formUrlEncoded) {
+ return `const formUrlEncoded = ${formUrlEncoded.name}(${body.implementation})`;
+ }
+
+ return body.formUrlEncoded;
+ }
+
+ return '';
+};
diff --git a/packages/core/src/generators/parameter-definition.ts b/packages/core/src/generators/parameter-definition.ts
new file mode 100644
index 000000000..90163797f
--- /dev/null
+++ b/packages/core/src/generators/parameter-definition.ts
@@ -0,0 +1,75 @@
+import { ComponentsObject, ParameterObject } from 'openapi3-ts';
+import { resolveObject, resolveRef } from '../resolvers';
+import { ContextSpecs, GeneratorSchema } from '../types';
+import { jsDoc, pascal, sanitize } from '../utils';
+
+export const generateParameterDefinition = (
+ parameters: ComponentsObject['parameters'] = {},
+ context: ContextSpecs,
+ suffix: string,
+): GeneratorSchema[] => {
+ return Object.entries(parameters).reduce(
+ (acc, [parameterName, parameter]) => {
+ const modelName = sanitize(`${pascal(parameterName)}${suffix}`, {
+ underscore: '_',
+ whitespace: '_',
+ dash: true,
+ es5keyword: true,
+ es5IdentifierName: true,
+ });
+ const { schema, imports } = resolveRef(
+ parameter,
+ context,
+ );
+
+ if (schema.in !== 'query') {
+ return acc;
+ }
+
+ if (!schema.schema || imports.length) {
+ acc.push({
+ name: modelName,
+ imports: imports.length
+ ? [
+ {
+ name: imports[0].name,
+ specKey: imports[0].specKey,
+ schemaName: imports[0].schemaName,
+ },
+ ]
+ : [],
+ model: `export type ${modelName} = ${
+ imports.length ? imports[0].name : 'unknown'
+ };\n`,
+ });
+
+ return acc;
+ }
+
+ const resolvedObject = resolveObject({
+ schema: schema.schema,
+ propName: modelName,
+ context,
+ });
+
+ const doc = jsDoc(parameter as ParameterObject);
+
+ const model = `${doc}export type ${modelName} = ${
+ resolvedObject.value || 'unknown'
+ };\n`;
+
+ acc.push(...resolvedObject.schemas);
+
+ if (modelName !== resolvedObject.value) {
+ acc.push({
+ name: modelName,
+ model,
+ imports: resolvedObject.imports,
+ });
+ }
+
+ return acc;
+ },
+ [] as GeneratorSchema[],
+ );
+};
diff --git a/packages/core/src/generators/schema-definition.ts b/packages/core/src/generators/schema-definition.ts
new file mode 100644
index 000000000..f200b87b7
--- /dev/null
+++ b/packages/core/src/generators/schema-definition.ts
@@ -0,0 +1,104 @@
+import isEmpty from 'lodash/isEmpty';
+import { SchemasObject } from 'openapi3-ts';
+import { getEnum, resolveDiscriminators } from '../getters';
+import { resolveValue } from '../resolvers';
+import { ContextSpecs, GeneratorSchema } from '../types';
+import { getSpecName, isReference, jsDoc, pascal, sanitize } from '../utils';
+import { generateInterface } from './interface';
+
+/**
+ * Extract all types from #/components/schemas
+ *
+ * @param schemas
+ */
+export const generateSchemasDefinition = (
+ schemas: SchemasObject = {},
+ context: ContextSpecs,
+ suffix: string,
+): GeneratorSchema[] => {
+ if (isEmpty(schemas)) {
+ return [];
+ }
+
+ const transformedSchemas = resolveDiscriminators(schemas, context);
+
+ const models = Object.entries(transformedSchemas).reduce(
+ (acc, [name, schema]) => {
+ const schemaName = sanitize(`${pascal(name)}${suffix}`, {
+ underscore: '_',
+ whitespace: '_',
+ dash: true,
+ es5keyword: true,
+ es5IdentifierName: true,
+ });
+ if (
+ (!schema.type || schema.type === 'object') &&
+ !schema.allOf &&
+ !schema.oneOf &&
+ !schema.anyOf &&
+ !isReference(schema) &&
+ !schema.nullable
+ ) {
+ acc.push(
+ ...generateInterface({
+ name: schemaName,
+ schema,
+ context,
+ suffix,
+ }),
+ );
+
+ return acc;
+ } else {
+ const resolvedValue = resolveValue({
+ schema,
+ name: schemaName,
+ context,
+ });
+
+ let output = '';
+
+ let imports = resolvedValue.imports;
+
+ output += jsDoc(schema);
+
+ if (resolvedValue.isEnum && !resolvedValue.isRef) {
+ output += getEnum(resolvedValue.value, schemaName);
+ } else if (schemaName === resolvedValue.value && resolvedValue.isRef) {
+ const imp = resolvedValue.imports.find(
+ (imp) => imp.name === schemaName,
+ );
+
+ if (!imp) {
+ output += `export type ${schemaName} = ${resolvedValue.value};\n`;
+ } else {
+ const alias = imp?.specKey
+ ? `${pascal(getSpecName(imp.specKey, context.specKey))}${
+ resolvedValue.value
+ }`
+ : `${resolvedValue.value}Bis`;
+
+ output += `export type ${schemaName} = ${alias};\n`;
+
+ imports = imports.map((imp) =>
+ imp.name === schemaName ? { ...imp, alias } : imp,
+ );
+ }
+ } else {
+ output += `export type ${schemaName} = ${resolvedValue.value};\n`;
+ }
+
+ acc.push(...resolvedValue.schemas, {
+ name: schemaName,
+ model: output,
+ imports,
+ });
+
+ return acc;
+ }
+ },
+ [] as GeneratorSchema[],
+ );
+
+ return models;
+};
diff --git a/packages/core/src/generators/verbs-options.ts b/packages/core/src/generators/verbs-options.ts
new file mode 100644
index 000000000..026c850dc
--- /dev/null
+++ b/packages/core/src/generators/verbs-options.ts
@@ -0,0 +1,219 @@
+import {
+ ComponentsObject,
+ OperationObject,
+ ParameterObject,
+ PathItemObject,
+ ReferenceObject,
+} from 'openapi3-ts';
+import {
+ getBody,
+ getOperationId,
+ getParameters,
+ getParams,
+ getProps,
+ getQueryParams,
+ getResponse,
+} from '../getters';
+import {
+ ContextSpecs,
+ GeneratorVerbOptions,
+ GeneratorVerbsOptions,
+ NormalizedOperationOptions,
+ NormalizedOutputOptions,
+ NormalizedOverrideOutput,
+ Verbs,
+} from '../types';
+import {
+ asyncReduce,
+ camel,
+ dynamicImport,
+ isObject,
+ isString,
+ isVerb,
+ jsDoc,
+ mergeDeep,
+ sanitize,
+} from '../utils';
+import { generateMutator } from './mutator';
+
+const generateVerbOptions = async ({
+ verb,
+ output,
+ operation,
+ route,
+ verbParameters = [],
+ context,
+}: {
+ verb: Verbs;
+ output: NormalizedOutputOptions;
+ operation: OperationObject;
+ route: string;
+ verbParameters?: Array;
+ components?: ComponentsObject;
+ context: ContextSpecs;
+}): Promise => {
+ const {
+ responses,
+ requestBody,
+ parameters: operationParameters,
+ tags = [],
+ deprecated,
+ description,
+ summary,
+ } = operation;
+
+ const operationId = getOperationId(operation, route, verb);
+ const overrideOperation = output.override.operations[operation.operationId!];
+ const overrideTag = Object.entries(output.override.tags).reduce(
+ (acc, [tag, options]) =>
+ tags.includes(tag) ? mergeDeep(acc, options) : acc,
+ {} as NormalizedOperationOptions,
+ );
+
+ const override: NormalizedOverrideOutput = {
+ ...output.override,
+ ...overrideTag,
+ ...overrideOperation,
+ };
+
+ const overrideOperationName =
+ overrideOperation?.operationName || output.override?.operationName;
+ const overriddenOperationName = overrideOperationName
+ ? overrideOperationName(operation, route, verb)
+ : camel(operationId);
+ const operationName = sanitize(overriddenOperationName, { es5keyword: true });
+
+ const response = getResponse({
+ responses,
+ operationName,
+ context,
+ contentType: override.contentType,
+ });
+
+ const body = getBody({
+ requestBody: requestBody!,
+ operationName,
+ context,
+ contentType: override.contentType,
+ });
+
+ const parameters = getParameters({
+ parameters: [...verbParameters, ...(operationParameters ?? [])],
+ context,
+ });
+
+ const queryParams = getQueryParams({
+ queryParams: parameters.query,
+ operationName,
+ context,
+ });
+
+ const headers = output.headers
+ ? await getQueryParams({
+ queryParams: parameters.header,
+ operationName,
+ context,
+ suffix: 'headers',
+ })
+ : undefined;
+
+ const params = getParams({
+ route,
+ pathParams: parameters.path,
+ operationId: operationId!,
+ context,
+ });
+
+ const props = getProps({ body, queryParams, params, headers });
+
+ const mutator = await generateMutator({
+ output: output.target,
+ name: operationName,
+ mutator: override?.mutator,
+ workspace: context.workspace,
+ tsconfig: context.tsconfig,
+ });
+
+ const formData =
+ isString(override?.formData) || isObject(override?.formData)
+ ? await generateMutator({
+ output: output.target,
+ name: operationName,
+ mutator: override.formData,
+ workspace: context.workspace,
+ tsconfig: context.tsconfig,
+ })
+ : undefined;
+
+ const formUrlEncoded =
+ isString(override?.formUrlEncoded) || isObject(override?.formUrlEncoded)
+ ? await generateMutator({
+ output: output.target,
+ name: operationName,
+ mutator: override.formUrlEncoded,
+ workspace: context.workspace,
+ tsconfig: context.tsconfig,
+ })
+ : undefined;
+
+ const doc = jsDoc({ description, deprecated, summary });
+
+ const verbOption: GeneratorVerbOptions = {
+ verb: verb as Verbs,
+ tags,
+ summary: operation.summary,
+ operationId: operationId!,
+ operationName,
+ response,
+ body,
+ headers,
+ queryParams,
+ params,
+ props,
+ mutator,
+ formData,
+ formUrlEncoded,
+ override,
+ doc,
+ deprecated,
+ };
+
+ const transformer = await dynamicImport(
+ override?.transformer,
+ context.workspace,
+ );
+
+ return transformer ? transformer(verbOption) : verbOption;
+};
+
+export const generateVerbsOptions = ({
+ verbs,
+ output,
+ route,
+ context,
+}: {
+ verbs: PathItemObject;
+ output: NormalizedOutputOptions;
+ route: string;
+ context: ContextSpecs;
+}): Promise =>
+ asyncReduce(
+ Object.entries(verbs),
+ async (acc, [verb, operation]: [string, OperationObject]) => {
+ if (isVerb(verb)) {
+ const verbOptions = await generateVerbOptions({
+ verb,
+ output,
+ verbParameters: verbs.parameters,
+ route,
+ operation,
+ context,
+ });
+
+ acc.push(verbOptions);
+ }
+
+ return acc;
+ },
+ [] as GeneratorVerbsOptions,
+ );
diff --git a/packages/core/src/getters/array.ts b/packages/core/src/getters/array.ts
new file mode 100644
index 000000000..011cc4ea0
--- /dev/null
+++ b/packages/core/src/getters/array.ts
@@ -0,0 +1,38 @@
+import { SchemaObject } from 'openapi3-ts';
+import { ContextSpecs, ResolverValue } from '../types';
+import { resolveObject } from '../resolvers/object';
+
+/**
+ * Return the output type from an array
+ *
+ * @param item item with type === "array"
+ */
+export const getArray = ({
+ schema,
+ name,
+ context,
+}: {
+ schema: SchemaObject;
+ name?: string;
+ context: ContextSpecs;
+}): ResolverValue => {
+ if (schema.items) {
+ const resolvedObject = resolveObject({
+ schema: schema.items,
+ propName: name + 'Item',
+ context,
+ });
+ return {
+ value: resolvedObject.value.includes('|')
+ ? `(${resolvedObject.value})[]`
+ : `${resolvedObject.value}[]`,
+ imports: resolvedObject.imports,
+ schemas: resolvedObject.schemas,
+ isEnum: false,
+ type: 'array',
+ isRef: false,
+ };
+ } else {
+ throw new Error('All arrays must have an `items` key define');
+ }
+};
diff --git a/packages/core/src/getters/body.ts b/packages/core/src/getters/body.ts
new file mode 100644
index 000000000..cbc029a0e
--- /dev/null
+++ b/packages/core/src/getters/body.ts
@@ -0,0 +1,70 @@
+import { ReferenceObject, RequestBodyObject } from 'openapi3-ts';
+import { generalJSTypesWithArray } from '../constants';
+import { ContextSpecs, OverrideOutputContentType } from '../types';
+import { GetterBody } from '../types';
+import { camel } from '../utils';
+import { getResReqTypes } from './res-req-types';
+
+export const getBody = ({
+ requestBody,
+ operationName,
+ context,
+ contentType,
+}: {
+ requestBody: ReferenceObject | RequestBodyObject;
+ operationName: string;
+ context: ContextSpecs;
+ contentType?: OverrideOutputContentType;
+}): GetterBody => {
+ const allBodyTypes = getResReqTypes(
+ [[context.override.components.requestBodies.suffix, requestBody]],
+ operationName,
+ context,
+ );
+
+ const filteredBodyTypes = contentType
+ ? allBodyTypes.filter((type) => {
+ let include = true;
+ let exclude = false;
+
+ if (contentType.include) {
+ include = contentType.include.includes(type.contentType);
+ }
+
+ if (contentType.exclude) {
+ exclude = contentType.exclude.includes(type.contentType);
+ }
+
+ return include && !exclude;
+ })
+ : allBodyTypes;
+
+ const imports = filteredBodyTypes.flatMap(({ imports }) => imports);
+ const schemas = filteredBodyTypes.flatMap(({ schemas }) => schemas);
+
+ const definition = filteredBodyTypes.map(({ value }) => value).join(' | ');
+
+ const implementation =
+ generalJSTypesWithArray.includes(definition.toLowerCase()) ||
+ filteredBodyTypes.length > 1
+ ? camel(operationName) + context.override.components.requestBodies.suffix
+ : camel(definition);
+
+ return {
+ definition,
+ implementation,
+ imports,
+ schemas,
+ ...(filteredBodyTypes.length === 1
+ ? {
+ formData: filteredBodyTypes[0].formData,
+ formUrlEncoded: filteredBodyTypes[0].formUrlEncoded,
+ contentType: filteredBodyTypes[0].contentType,
+ }
+ : {
+ formData: '',
+ formUrlEncoded: '',
+ contentType: '',
+ }),
+ };
+};
diff --git a/packages/core/src/getters/combine.ts b/packages/core/src/getters/combine.ts
new file mode 100644
index 000000000..247f09ce0
--- /dev/null
+++ b/packages/core/src/getters/combine.ts
@@ -0,0 +1,170 @@
+import omit from 'lodash.omit';
+import { SchemaObject } from 'openapi3-ts';
+import { resolveObject } from '../resolvers';
+import {
+ ContextSpecs,
+ GeneratorImport,
+ ResolverValue,
+ SchemaType,
+} from '../types';
+import { getNumberWord, pascal } from '../utils';
+import { getEnumImplementation } from './enum';
+import { getScalar } from './scalar';
+
+type CombinedData = Omit<
+ ResolverValue,
+ 'isRef' | 'isEnum' | 'value' | 'type'
+> & {
+ values: string[];
+ isRef: boolean[];
+ isEnum: boolean[];
+ types: string[];
+};
+
+type Separator = 'allOf' | 'anyOf' | 'oneOf';
+
+const combineValues = ({
+ resolvedData,
+ resolvedValue,
+ separator,
+}: {
+ resolvedData: CombinedData;
+ resolvedValue?: ResolverValue;
+ separator: Separator;
+}) => {
+ const isAllEnums = resolvedData.isEnum.every((v) => v);
+
+ if (isAllEnums) {
+ return `${resolvedData.values.join(` | `)}${
+ resolvedValue ? ` | ${resolvedValue.value}` : ''
+ }`;
+ }
+
+ if (separator === 'allOf') {
+ return `${resolvedData.values.join(` & `)}${
+ resolvedValue ? ` & ${resolvedValue.value}` : ''
+ }`;
+ }
+
+ if (resolvedValue) {
+ return `(${resolvedData.values.join(` & ${resolvedValue.value}) | (`)} & ${
+ resolvedValue.value
+ })`;
+ }
+
+ return resolvedData.values.join(' | ');
+};
+
+export const combineSchemas = ({
+ name,
+ schema,
+ separator,
+ context,
+ nullable,
+}: {
+ name?: string;
+ schema: SchemaObject;
+ separator: Separator;
+ context: ContextSpecs;
+ nullable: string;
+}) => {
+ const items = schema[separator] ?? [];
+
+ const resolvedData = items.reduce(
+ (acc, subSchema) => {
+ let propName = name ? name + pascal(separator) : undefined;
+ if (propName && acc.schemas.length) {
+ propName = propName + pascal(getNumberWord(acc.schemas.length + 1));
+ }
+
+ const resolvedValue = resolveObject({
+ schema: subSchema,
+ propName,
+ combined: true,
+ context,
+ });
+
+ acc.values.push(resolvedValue.value);
+ acc.imports.push(...resolvedValue.imports);
+ acc.schemas.push(...resolvedValue.schemas);
+ acc.isEnum.push(resolvedValue.isEnum);
+ acc.types.push(resolvedValue.type);
+ acc.isRef.push(resolvedValue.isRef);
+
+ return acc;
+ },
+ {
+ values: [],
+ imports: [],
+ schemas: [],
+ isEnum: [], // check if only enums
+ isRef: [],
+ types: [],
+ } as CombinedData,
+ );
+
+ const isAllEnums = resolvedData.isEnum.every((v) => v);
+
+ let resolvedValue;
+
+ if (schema.properties) {
+ resolvedValue = getScalar({ item: omit(schema, separator), name, context });
+ }
+
+ const value = combineValues({ resolvedData, separator, resolvedValue });
+
+ if (isAllEnums && name && items.length > 1) {
+ const newEnum = `\n\n// eslint-disable-next-line @typescript-eslint/no-redeclare\nexport const ${pascal(
+ name,
+ )} = ${getCombineEnumValue(resolvedData)}`;
+
+ return {
+ value:
+ `typeof ${pascal(name)}[keyof typeof ${pascal(name)}] ${nullable};` +
+ newEnum,
+ imports: resolvedData.imports.map((toImport) => ({
+ ...toImport,
+ values: true,
+ })),
+ schemas: resolvedData.schemas,
+ isEnum: false,
+ type: 'object' as SchemaType,
+ isRef: false,
+ };
+ }
+
+ return {
+ value: value + nullable,
+ imports: resolvedValue
+ ? [...resolvedData.imports, ...resolvedValue.imports]
+ : resolvedData.imports,
+ schemas: resolvedValue
+ ? [...resolvedData.schemas, ...resolvedValue.schemas]
+ : resolvedData.schemas,
+ isEnum: false,
+ type: 'object' as SchemaType,
+ isRef: false,
+ };
+};
+
+const getCombineEnumValue = ({ values, isRef, types }: CombinedData) => {
+ if (values.length === 1) {
+ if (isRef[0]) {
+ return values[0];
+ }
+
+ return `{${getEnumImplementation(values[0])}} as const`;
+ }
+
+ const enums = values
+ .map((e, i) => {
+ if (isRef[i]) {
+ return `...${e},`;
+ }
+
+ return getEnumImplementation(e);
+ })
+ .join('');
+
+ return `{${enums}} as const`;
+};
diff --git a/packages/core/src/getters/discriminators.ts b/packages/core/src/getters/discriminators.ts
new file mode 100644
index 000000000..3462eb6f8
--- /dev/null
+++ b/packages/core/src/getters/discriminators.ts
@@ -0,0 +1,45 @@
+import { SchemasObject } from 'openapi3-ts';
+import { ContextSpecs } from '../types';
+import { getRefInfo } from './ref';
+
+export const resolveDiscriminators = (
+ schemas: SchemasObject,
+ context: ContextSpecs,
+): SchemasObject => {
+ const transformedSchemas = { ...schemas };
+
+ for (const schema of Object.values(transformedSchemas)) {
+ if (schema.discriminator?.mapping) {
+ const { mapping, propertyName } = schema.discriminator;
+
+ for (const [mappingKey, mappingValue] of Object.entries(mapping)) {
+ let subTypeSchema;
+
+ try {
+ const { name } = getRefInfo(mappingValue, context);
+ subTypeSchema = transformedSchemas[name];
+ } catch (e) {
+ subTypeSchema = transformedSchemas[mappingValue];
+ }
+
+ if (!subTypeSchema) {
+ continue;
+ }
+
+ subTypeSchema.properties = {
+ ...subTypeSchema.properties,
+ [propertyName]: {
+ type: 'string',
+ enum: [mappingKey],
+ },
+ };
+ subTypeSchema.required = [
+ ...(subTypeSchema.required ?? []),
+ propertyName,
+ ];
+ }
+ }
+ }
+
+ return transformedSchemas;
+};
diff --git a/packages/core/src/getters/enum.ts b/packages/core/src/getters/enum.ts
new file mode 100644
index 000000000..9a4663905
--- /dev/null
+++ b/packages/core/src/getters/enum.ts
@@ -0,0 +1,55 @@
+import { keyword } from 'esutils';
+import { sanitize } from '../utils';
+
+export const getEnum = (value: string, enumName: string) => {
+ let enumValue = `export type ${enumName} = typeof ${enumName}[keyof typeof ${enumName}];\n`;
+
+ const implementation = getEnumImplementation(value);
+
+ enumValue += `\n\n`;
+
+ enumValue += '// eslint-disable-next-line @typescript-eslint/no-redeclare\n';
+
+ enumValue += `export const ${enumName} = {\n${implementation}} as const;\n`;
+
+ return enumValue;
+};
+
+export const getEnumImplementation = (value: string) => {
+ return [...new Set(value.split(' | '))].reduce((acc, val) => {
+ // nullable value shouldn't be in the enum implementation
+ if (val === 'null') return acc;
+
+ let key = val.startsWith("'") ? val.slice(1, -1) : val;
+
+ const isNumber = !Number.isNaN(Number(key));
+
+ if (isNumber) {
+ key = toNumberKey(key);
+ }
+
+ if (key.length > 1) {
+ key = sanitize(key, {
+ whitespace: '_',
+ underscore: true,
+ dash: true,
+ special: true,
+ });
+ }
+
+ return (
+ acc +
+ ` ${keyword.isIdentifierNameES5(key) ? key : `'${key}'`}: ${val},\n`
+ );
+ }, '');
+};
+
+const toNumberKey = (value: string) => {
+ if (value[0] === '-') {
+ return `NUMBER_MINUS_${value.slice(1)}`;
+ }
+ if (value[0] === '+') {
+ return `NUMBER_PLUS_${value.slice(1)}`;
+ }
+ return `NUMBER_${value}`;
+};
diff --git a/packages/core/src/getters/index.ts b/packages/core/src/getters/index.ts
new file mode 100644
index 000000000..411143437
--- /dev/null
+++ b/packages/core/src/getters/index.ts
@@ -0,0 +1,17 @@
+export * from './array';
+export * from './body';
+export * from './combine';
+export * from './discriminators';
+export * from './enum';
+export * from './keys';
+export * from './object';
+export * from './operation';
+export * from './parameters';
+export * from './params';
+export * from './props';
+export * from './query-params';
+export * from './ref';
+export * from './res-req-types';
+export * from './response';
+export * from './route';
+export * from './scalar';
diff --git a/packages/core/src/getters/keys.ts b/packages/core/src/getters/keys.ts
new file mode 100644
index 000000000..27734c37d
--- /dev/null
+++ b/packages/core/src/getters/keys.ts
@@ -0,0 +1,5 @@
+import { keyword } from 'esutils';
+
+export const getKey = (key: string) => {
+ return keyword.isIdentifierNameES5(key) ? key : `'${key}'`;
+};
diff --git a/packages/core/src/getters/object.ts b/packages/core/src/getters/object.ts
new file mode 100644
index 000000000..35910faf9
--- /dev/null
+++ b/packages/core/src/getters/object.ts
@@ -0,0 +1,168 @@
+import { ReferenceObject, SchemaObject } from 'openapi3-ts';
+import { resolveObject, resolveValue } from '../resolvers';
+import { ContextSpecs, ResolverValue, SchemaType } from '../types';
+import { isBoolean, isReference, jsDoc, pascal } from '../utils';
+import { combineSchemas } from './combine';
+import { getKey } from './keys';
+import { getRefInfo } from './ref';
+
+/**
+ * Return the output type from an object
+ *
+ * @param item item with type === "object"
+ */
+export const getObject = ({
+ item,
+ name,
+ context,
+ nullable,
+}: {
+ item: SchemaObject;
+ name?: string;
+ context: ContextSpecs;
+ nullable: string;
+}): ResolverValue => {
+ if (isReference(item)) {
+ const { name, specKey } = getRefInfo(item.$ref, context);
+ return {
+ value: name + nullable,
+ imports: [{ name, specKey }],
+ schemas: [],
+ isEnum: false,
+ type: 'object',
+ isRef: true,
+ };
+ }
+
+ if (item.allOf || item.oneOf || item.anyOf) {
+ const separator = item.allOf ? 'allOf' : item.oneOf ? 'oneOf' : 'anyOf';
+
+ return combineSchemas({
+ schema: item,
+ name,
+ separator,
+ context,
+ nullable,
+ });
+ }
+
+ if (item.type instanceof Array) {
+ return combineSchemas({
+ schema: { anyOf: item.type.map((type) => ({ type })) },
+ name,
+ separator: 'anyOf',
+ context,
+ nullable,
+ });
+ }
+
+ if (item.properties && Object.entries(item.properties).length > 0) {
+ return Object.entries(item.properties).reduce(
+ (
+ acc,
+ [key, schema]: [string, ReferenceObject | SchemaObject],
+ index,
+ arr,
+ ) => {
+ const isRequired = (
+ Array.isArray(item.required) ? item.required : []
+ ).includes(key);
+ let propName = name ? pascal(name) + pascal(key) : undefined;
+
+ const isNameAlreadyTaken =
+ !!context.specs[context.target]?.components?.schemas?.[
+ propName || ''
+ ];
+
+ if (isNameAlreadyTaken) {
+ propName = propName + 'Property';
+ }
+
+ const resolvedValue = resolveObject({
+ schema,
+ propName,
+ context,
+ });
+
+ const isReadOnly = item.readOnly || (schema as SchemaObject).readOnly;
+ if (!index) {
+ acc.value += '{';
+ }
+
+ const doc = jsDoc(schema as SchemaObject, true);
+
+ acc.imports.push(...resolvedValue.imports);
+ acc.value += `\n ${doc ? `${doc} ` : ''}${
+ isReadOnly ? 'readonly ' : ''
+ }${getKey(key)}${isRequired ? '' : '?'}: ${resolvedValue.value};`;
+ acc.schemas.push(...resolvedValue.schemas);
+
+ if (arr.length - 1 === index) {
+ if (item.additionalProperties) {
+ if (isBoolean(item.additionalProperties)) {
+ acc.value += `\n [key: string]: any;\n }`;
+ } else {
+ const resolvedValue = resolveValue({
+ schema: item.additionalProperties,
+ name,
+ context,
+ });
+ acc.value += `\n [key: string]: ${resolvedValue.value};\n}`;
+ }
+ } else {
+ acc.value += '\n}';
+ }
+
+ acc.value += nullable;
+ }
+
+ return acc;
+ },
+ {
+ imports: [],
+ schemas: [],
+ value: '',
+ isEnum: false,
+ type: 'object' as SchemaType,
+ isRef: false,
+ schema: {},
+ } as ResolverValue,
+ );
+ }
+
+ if (item.additionalProperties) {
+ if (isBoolean(item.additionalProperties)) {
+ return {
+ value: `{ [key: string]: any }` + nullable,
+ imports: [],
+ schemas: [],
+ isEnum: false,
+ type: 'object',
+ isRef: false,
+ };
+ }
+ const resolvedValue = resolveValue({
+ schema: item.additionalProperties,
+ name,
+ context,
+ });
+ return {
+ value: `{[key: string]: ${resolvedValue.value}}` + nullable,
+ imports: resolvedValue.imports ?? [],
+ schemas: resolvedValue.schemas ?? [],
+ isEnum: false,
+ type: 'object',
+ isRef: false,
+ };
+ }
+
+ return {
+ value:
+ item.type === 'object' ? '{ [key: string]: any }' : 'unknown' + nullable,
+ imports: [],
+ schemas: [],
+ isEnum: false,
+ type: 'object',
+ isRef: false,
+ };
+};
diff --git a/packages/core/src/getters/operation.test.ts b/packages/core/src/getters/operation.test.ts
new file mode 100644
index 000000000..181c8884a
--- /dev/null
+++ b/packages/core/src/getters/operation.test.ts
@@ -0,0 +1,17 @@
+import { OperationObject } from 'openapi3-ts';
+import { getOperationId } from './operation';
+
+describe('getOperationId getter', () => {
+ [
+ ['/api/test/{id}', 'ApiTestId'],
+ ['/api/test/{user_id}', 'ApiTestUserId'],
+ ['/api/test/{locale}.js', 'ApiTestLocaleJs'],
+ ['/api/test/i18n-{locale}.js', 'ApiTestI18nLocaleJs'],
+ ['/api/test/{param1}-{param2}.js', 'ApiTestParam1Param2Js'],
+ ['/api/test/user{param1}-{param2}.html', 'ApiTestUserparam1Param2Html'],
+ ].forEach(([input, expected]) => {
+ it(`should process ${input} to ${expected}`, () => {
+ expect(getOperationId({} as OperationObject, input, '')).toBe(expected);
+ });
+ });
+});
diff --git a/packages/core/src/getters/operation.ts b/packages/core/src/getters/operation.ts
new file mode 100644
index 000000000..9b0450c88
--- /dev/null
+++ b/packages/core/src/getters/operation.ts
@@ -0,0 +1,27 @@
+import { OperationObject } from 'openapi3-ts';
+import { Verbs } from '../types';
+import { pascal, sanitize } from '../utils';
+
+export const getOperationId = (
+ operation: OperationObject,
+ route: string,
+ verb: Verbs,
+): string => {
+ if (operation.operationId) {
+ return operation.operationId;
+ }
+
+ return pascal(
+ [
+ verb,
+ ...route.split('/').map((p) =>
+ sanitize(p, {
+ dash: true,
+ underscore: '-',
+ dot: '-',
+ whitespace: '-',
+ }),
+ ),
+ ].join('-'),
+ );
+};
diff --git a/packages/core/src/getters/parameters.ts b/packages/core/src/getters/parameters.ts
new file mode 100644
index 000000000..dd0a6cbb2
--- /dev/null
+++ b/packages/core/src/getters/parameters.ts
@@ -0,0 +1,42 @@
+import { ParameterObject, ReferenceObject } from 'openapi3-ts';
+import { resolveRef } from '../resolvers/ref';
+import { ContextSpecs, GetterParameters } from '../types';
+import { isReference } from '../utils';
+
+export const getParameters = ({
+ parameters = [],
+ context,
+}: {
+ parameters: (ReferenceObject | ParameterObject)[];
+ context: ContextSpecs;
+}): GetterParameters => {
+ return parameters.reduce(
+ (acc, p) => {
+ if (isReference(p)) {
+ const { schema: parameter, imports } = resolveRef(
+ p,
+ context,
+ );
+
+ if (
+ parameter.in === 'path' ||
+ parameter.in === 'query' ||
+ parameter.in === 'header'
+ ) {
+ acc[parameter.in].push({ parameter, imports });
+ }
+ } else {
+ if (p.in === 'query' || p.in === 'path' || p.in === 'header') {
+ acc[p.in].push({ parameter: p, imports: [] });
+ }
+ }
+
+ return acc;
+ },
+ {
+ path: [],
+ query: [],
+ header: [],
+ } as GetterParameters,
+ );
+};
diff --git a/packages/core/src/getters/params.ts b/packages/core/src/getters/params.ts
new file mode 100644
index 000000000..ebafe1887
--- /dev/null
+++ b/packages/core/src/getters/params.ts
@@ -0,0 +1,107 @@
+import { resolveValue } from '../resolvers';
+import { ContextSpecs, GetterParameters, GetterParams } from '../types';
+import { camel, sanitize, stringify } from '../utils';
+
+/**
+ * Return every params in a path
+ *
+ * @example
+ * ```
+ * getParamsInPath("/pet/{category}/{name}/");
+ * // => ["category", "name"]
+ * ```
+ * @param path
+ */
+export const getParamsInPath = (path: string) => {
+ let n;
+ const output = [];
+ const templatePathRegex = /\{(.*?)\}/g;
+ // tslint:disable-next-line:no-conditional-assignment
+ while ((n = templatePathRegex.exec(path)) !== null) {
+ output.push(n[1]);
+ }
+
+ return output;
+};
+
+export const getParams = ({
+ route,
+ pathParams = [],
+ operationId,
+ context,
+}: {
+ route: string;
+ pathParams?: GetterParameters['query'];
+ operationId: string;
+ context: ContextSpecs;
+}): GetterParams => {
+ const params = getParamsInPath(route);
+ return params.map((p) => {
+ const pathParam = pathParams.find(
+ ({ parameter }) =>
+ sanitize(camel(parameter.name), {
+ es5keyword: true,
+ underscore: true,
+ dash: true,
+ }) === p,
+ );
+
+ if (!pathParam) {
+ throw new Error(
+ `The path params ${p} can't be found in parameters (${operationId})`,
+ );
+ }
+
+ const {
+ name: nameWithoutSanitize,
+ required = false,
+ schema,
+ } = pathParam.parameter;
+
+ const name = sanitize(camel(nameWithoutSanitize), { es5keyword: true });
+
+ if (!schema) {
+ return {
+ name,
+ definition: `${name}${!required ? '?' : ''}: unknown`,
+ implementation: `${name}${!required ? '?' : ''}: unknown`,
+ default: false,
+ required,
+ imports: [],
+ };
+ }
+
+ const resolvedValue = resolveValue({
+ schema,
+ context: {
+ ...context,
+ ...(pathParam.imports.length
+ ? {
+ specKey: pathParam.imports[0].specKey,
+ }
+ : {}),
+ },
+ });
+
+ const definition = `${name}${
+ !required || resolvedValue.originalSchema!.default ? '?' : ''
+ }: ${resolvedValue.value}`;
+
+ const implementation = `${name}${
+ !required && !resolvedValue.originalSchema!.default ? '?' : ''
+ }${
+ !resolvedValue.originalSchema!.default
+ ? `: ${resolvedValue.value}`
+ : `= ${stringify(resolvedValue.originalSchema!.default)}`
+ }`;
+
+ return {
+ name,
+ definition,
+ implementation,
+ default: resolvedValue.originalSchema!.default,
+ required,
+ imports: resolvedValue.imports,
+ };
+ });
+};
diff --git a/packages/core/src/getters/props.ts b/packages/core/src/getters/props.ts
new file mode 100644
index 000000000..e89ad3ef8
--- /dev/null
+++ b/packages/core/src/getters/props.ts
@@ -0,0 +1,68 @@
+import {
+ GetterBody,
+ GetterParams,
+ GetterProps,
+ GetterPropType,
+ GetterQueryParam,
+} from '../types';
+import { isUndefined, sortByPriority } from '../utils';
+
+export const getProps = ({
+ body,
+ queryParams,
+ params,
+ headers,
+}: {
+ body: GetterBody;
+ queryParams?: GetterQueryParam;
+ params: GetterParams;
+ headers?: GetterQueryParam;
+}): GetterProps => {
+ const bodyProp = {
+ name: body.implementation,
+ definition: `${body.implementation}: ${body.definition}`,
+ implementation: `${body.implementation}: ${body.definition}`,
+ default: false,
+ required: true,
+ type: GetterPropType.BODY,
+ };
+
+ const queryParamsProp = {
+ name: 'params',
+ definition: `params${queryParams?.isOptional ? '?' : ''}: ${
+ queryParams?.schema.name
+ }`,
+ implementation: `params${queryParams?.isOptional ? '?' : ''}: ${
+ queryParams?.schema.name
+ }`,
+ default: false,
+ required: !isUndefined(queryParams?.isOptional)
+ ? !queryParams?.isOptional
+ : false,
+ type: GetterPropType.QUERY_PARAM,
+ };
+
+ const headersProp = {
+ name: 'headers',
+ definition: `headers${headers?.isOptional ? '?' : ''}: ${
+ headers?.schema.name
+ }`,
+ implementation: `headers${headers?.isOptional ? '?' : ''}: ${
+ headers?.schema.name
+ }`,
+ default: false,
+ required: !isUndefined(headers?.isOptional) ? !headers?.isOptional : false,
+ type: GetterPropType.HEADER,
+ };
+
+ const props = [
+ ...params.map((param) => ({ ...param, type: GetterPropType.PARAM })),
+ ...(body.definition ? [bodyProp] : []),
+ ...(queryParams ? [queryParamsProp] : []),
+ ...(headers ? [headersProp] : []),
+ ];
+
+ const sortedProps = sortByPriority(props);
+
+ return sortedProps;
+};
diff --git a/packages/core/src/getters/query-params.ts b/packages/core/src/getters/query-params.ts
new file mode 100644
index 000000000..f2e4df3aa
--- /dev/null
+++ b/packages/core/src/getters/query-params.ts
@@ -0,0 +1,117 @@
+import { ContentObject, SchemaObject } from 'openapi3-ts';
+import { resolveValue } from '../resolvers';
+import {
+ ContextSpecs,
+ GeneratorImport,
+ GeneratorSchema,
+ GetterParameters,
+ GetterQueryParam,
+} from '../types';
+import { pascal, sanitize } from '../utils';
+import { getEnum } from './enum';
+import { getKey } from './keys';
+
+type QueryParamsType = {
+ definition: string;
+ imports: GeneratorImport[];
+ schemas: GeneratorSchema[];
+};
+
+const getQueryParamsTypes = (
+ queryParams: GetterParameters['query'],
+ operationName: string,
+ context: ContextSpecs,
+): QueryParamsType[] => {
+ return queryParams.map(({ parameter, imports: parameterImports }) => {
+ const { name, required, schema, content } = parameter as {
+ name: string;
+ required: boolean;
+ schema: SchemaObject;
+ content: ContentObject;
+ };
+
+ const queryName = sanitize(`${pascal(operationName)}${pascal(name)}`, {
+ underscore: '_',
+ whitespace: '_',
+ dash: true,
+ es5keyword: true,
+ es5IdentifierName: true,
+ });
+
+ const { value, imports, isEnum, type, schemas, isRef } = resolveValue({
+ schema: (schema || content['application/json'].schema)!,
+ context,
+ name: queryName,
+ });
+
+ const key = getKey(name);
+
+ if (parameterImports.length) {
+ return {
+ definition: `${key}${!required || schema.default ? '?' : ''}: ${
+ parameterImports[0].name
+ }`,
+ imports: parameterImports,
+ schemas: [],
+ };
+ }
+
+ if (isEnum && !isRef) {
+ const enumName = queryName;
+ const enumValue = getEnum(value, enumName);
+
+ return {
+ definition: `${key}${
+ !required || schema.default ? '?' : ''
+ }: ${enumName}`,
+ imports: [{ name: enumName }],
+ schemas: [...schemas, { name: enumName, model: enumValue, imports }],
+ };
+ }
+
+ const definition = `${key}${
+ !required || schema.default ? '?' : ''
+ }: ${value}`;
+
+ return {
+ definition,
+ imports,
+ schemas,
+ };
+ });
+};
+
+export const getQueryParams = ({
+ queryParams = [],
+ operationName,
+ context,
+ suffix = 'params',
+}: {
+ queryParams: GetterParameters['query'];
+ operationName: string;
+ context: ContextSpecs;
+ suffix?: string;
+}): GetterQueryParam | undefined => {
+ if (!queryParams.length) {
+ return;
+ }
+ const types = getQueryParamsTypes(queryParams, operationName, context);
+ const imports = types.flatMap(({ imports }) => imports);
+ const schemas = types.flatMap(({ schemas }) => schemas);
+ const name = `${pascal(operationName)}${pascal(suffix)}`;
+
+ const type = types.map(({ definition }) => definition).join('; ');
+ const allOptional = queryParams.every(({ parameter }) => !parameter.required);
+
+ const schema = {
+ name,
+ model: `export type ${name} = { ${type} };\n`,
+ imports,
+ };
+
+ return {
+ schema,
+ deps: schemas,
+ isOptional: allOptional,
+ };
+};
diff --git a/packages/core/src/getters/ref.ts b/packages/core/src/getters/ref.ts
new file mode 100644
index 000000000..8c7cbaae5
--- /dev/null
+++ b/packages/core/src/getters/ref.ts
@@ -0,0 +1,75 @@
+import get from 'lodash.get';
+import { ReferenceObject } from 'openapi3-ts';
+import { resolve } from 'upath';
+import url from 'url';
+import { ContextSpecs } from '../types';
+import { getExtension, getFileInfo, isUrl, pascal } from '../utils';
+
+type RefComponent = 'schemas' | 'responses' | 'parameters' | 'requestBodies';
+
+const RefComponent = {
+ schemas: 'schemas' as RefComponent,
+ responses: 'responses' as RefComponent,
+ parameters: 'parameters' as RefComponent,
+ requestBodies: 'requestBodies' as RefComponent,
+};
+
+export const RefComponentSuffix: Record = {
+ schemas: '',
+ responses: 'Response',
+ parameters: 'Parameter',
+ requestBodies: 'Body',
+};
+
+const regex = new RegExp('~1', 'g');
+
+/**
+ * Return the output type from the $ref
+ *
+ * @param $ref
+ */
+export const getRefInfo = (
+ $ref: ReferenceObject['$ref'],
+ context: ContextSpecs,
+): {
+ name: string;
+ originalName: string;
+ refPaths?: string[];
+ specKey?: string;
+} => {
+ const [pathname, ref] = $ref.split('#');
+
+ const refPaths = ref
+ ?.slice(1)
+ .split('/')
+ .map((part) => part.replace(regex, '/'));
+
+ const suffix = refPaths
+ ? get(context.override, [...refPaths.slice(0, 2), 'suffix'], '')
+ : '';
+
+ const originalName = ref
+ ? refPaths[refPaths.length - 1]
+ : pathname
+ .replace(`.${getExtension(pathname)}`, '')
+ .slice(pathname.lastIndexOf('/') + 1);
+
+ if (!pathname) {
+ return {
+ name: pascal(originalName) + suffix,
+ originalName,
+ refPaths,
+ };
+ }
+
+ const path = isUrl(context.specKey)
+ ? url.resolve(context.specKey, pathname)
+ : resolve(getFileInfo(context.specKey).dirname, pathname);
+
+ return {
+ name: pascal(originalName) + suffix,
+ originalName,
+ specKey: path,
+ refPaths,
+ };
+};
diff --git a/packages/core/src/getters/res-req-types.ts b/packages/core/src/getters/res-req-types.ts
new file mode 100644
index 000000000..ec840ddcf
--- /dev/null
+++ b/packages/core/src/getters/res-req-types.ts
@@ -0,0 +1,288 @@
+import { keyword } from 'esutils';
+import uniqBy from 'lodash.uniqby';
+import {
+ MediaTypeObject,
+ ReferenceObject,
+ RequestBodyObject,
+ ResponseObject,
+ SchemaObject,
+} from 'openapi3-ts';
+import { resolveObject } from '../resolvers/object';
+import { resolveRef } from '../resolvers/ref';
+import { ContextSpecs, ResReqTypesValue } from '../types';
+import { camel } from '../utils';
+import { isReference } from '../utils/assertion';
+import { pascal } from '../utils/case';
+import { getNumberWord } from '../utils/string';
+
+const formDataContentTypes = ['multipart/form-data'];
+
+const formUrlEncodedContentTypes = ['application/x-www-form-urlencoded'];
+
+const getResReqContentTypes = ({
+ mediaType,
+ propName,
+ context,
+}: {
+ mediaType: MediaTypeObject;
+ propName?: string;
+ context: ContextSpecs;
+}) => {
+ if (!mediaType.schema) {
+ return undefined;
+ }
+
+ const resolvedObject = resolveObject({
+ schema: mediaType.schema,
+ propName,
+ context,
+ });
+
+ return resolvedObject;
+};
+
+export const getResReqTypes = (
+ responsesOrRequests: Array<
+ [string, ResponseObject | ReferenceObject | RequestBodyObject]
+ >,
+ name: string,
+ context: ContextSpecs,
+ defaultType = 'unknown',
+): ResReqTypesValue[] => {
+ const typesArray = responsesOrRequests
+ .filter(([_, res]) => Boolean(res))
+ .map(([key, res]) => {
+ if (isReference(res)) {
+ const {
+ schema: bodySchema,
+ imports: [{ name, specKey, schemaName }],
+ } = resolveRef(res, context);
+
+ const [contentType, mediaType] =
+ Object.entries(bodySchema.content ?? {})[0] ?? [];
+
+ const isFormData = formDataContentTypes.includes(contentType);
+ const isFormUrlEncoded =
+ formUrlEncodedContentTypes.includes(contentType);
+
+ if ((!isFormData && !isFormUrlEncoded) || !mediaType?.schema) {
+ return [
+ {
+ value: name,
+ imports: [{ name, specKey, schemaName }],
+ schemas: [],
+ type: 'unknown',
+ isEnum: false,
+ isRef: true,
+ originalSchema: mediaType?.schema,
+ key,
+ contentType,
+ },
+ ] as ResReqTypesValue[];
+ }
+
+ const formData = isFormData
+ ? getSchemaFormDataAndUrlEncoded({
+ name,
+ schemaObject: mediaType?.schema,
+ context: {
+ ...context,
+ specKey: specKey || context.specKey,
+ },
+ isRef: true,
+ })
+ : undefined;
+
+ const formUrlEncoded = isFormUrlEncoded
+ ? getSchemaFormDataAndUrlEncoded({
+ name,
+ schemaObject: mediaType?.schema,
+ context: {
+ ...context,
+ specKey: specKey || context.specKey,
+ },
+ isUrlEncoded: true,
+ isRef: true,
+ })
+ : undefined;
+
+ return [
+ {
+ value: name,
+ imports: [{ name, specKey, schemaName }],
+ schemas: [],
+ type: 'unknown',
+ isEnum: false,
+ formData,
+ formUrlEncoded,
+ isRef: true,
+ originalSchema: mediaType?.schema,
+ key,
+ contentType,
+ },
+ ] as ResReqTypesValue[];
+ }
+
+ if (res.content) {
+ const contents = Object.entries(res.content).map(
+ ([contentType, mediaType], index, arr) => {
+ let propName = key ? pascal(name) + pascal(key) : undefined;
+
+ if (propName && arr.length > 1) {
+ propName = propName + pascal(getNumberWord(index + 1));
+ }
+
+ const resolvedValue = getResReqContentTypes({
+ mediaType,
+ propName,
+ context,
+ });
+
+ if (!resolvedValue) {
+ return;
+ }
+
+ const isFormData = formDataContentTypes.includes(contentType);
+ const isFormUrlEncoded =
+ formUrlEncodedContentTypes.includes(contentType);
+
+ if ((!isFormData && !isFormUrlEncoded) || !propName) {
+ return { ...resolvedValue, contentType };
+ }
+
+ const formData = isFormData
+ ? getSchemaFormDataAndUrlEncoded({
+ name: propName,
+ schemaObject: mediaType.schema!,
+ context,
+ })
+ : undefined;
+
+ const formUrlEncoded = isFormUrlEncoded
+ ? getSchemaFormDataAndUrlEncoded({
+ name: propName,
+ schemaObject: mediaType.schema!,
+ context,
+ isUrlEncoded: true,
+ })
+ : undefined;
+
+ return {
+ ...resolvedValue,
+ formData,
+ formUrlEncoded,
+ contentType,
+ };
+ },
+ );
+
+ return contents
+ .filter((x) => x)
+ .map((x) => ({ ...x, key })) as ResReqTypesValue[];
+ }
+
+ return [
+ {
+ value: defaultType,
+ imports: [],
+ schemas: [],
+ type: defaultType,
+ isEnum: false,
+ key,
+ isRef: false,
+ contentType: 'application/json',
+ },
+ ] as ResReqTypesValue[];
+ });
+
+ return uniqBy(
+ typesArray.flatMap((it) => it),
+ 'value',
+ );
+};
+
+const getSchemaFormDataAndUrlEncoded = ({
+ name,
+ schemaObject,
+ context,
+ isUrlEncoded,
+ isRef,
+}: {
+ name: string;
+ schemaObject: SchemaObject | ReferenceObject;
+ context: ContextSpecs;
+ isUrlEncoded?: boolean;
+ isRef?: boolean;
+}) => {
+ const { schema, imports } = resolveRef(schemaObject, context);
+ const propName = !isRef && isReference(schemaObject) ? imports[0].name : name;
+
+ const variableName = isUrlEncoded ? 'formUrlEncoded' : 'formData';
+ const form = isUrlEncoded
+ ? `const ${variableName} = new URLSearchParams();\n`
+ : `const ${variableName} = new FormData();\n`;
+
+ if (schema.type === 'object' && schema.properties) {
+ const formDataValues = Object.entries(schema.properties).reduce(
+ (acc, [key, value]) => {
+ const { schema: property } = resolveRef(value, context);
+
+ let formDataValue = '';
+
+ const formatedKey = !keyword.isIdentifierNameES5(key)
+ ? `['${key}']`
+ : `.${key}`;
+
+ if (property.type === 'object') {
+ formDataValue = `${variableName}.append('${key}', JSON.stringify(${camel(
+ propName,
+ )}${formatedKey}));\n`;
+ } else if (property.type === 'array') {
+ formDataValue = `${camel(
+ propName,
+ )}${formatedKey}.forEach(value => ${variableName}.append('${key}', value));\n`;
+ } else if (
+ property.type === 'number' ||
+ property.type === 'integer' ||
+ property.type === 'boolean'
+ ) {
+ formDataValue = `${variableName}.append('${key}', ${camel(
+ propName,
+ )}${formatedKey}.toString())\n`;
+ } else {
+ formDataValue = `${variableName}.append('${key}', ${camel(
+ propName,
+ )}${formatedKey})\n`;
+ }
+
+ if (schema.required?.includes(key)) {
+ return acc + formDataValue;
+ }
+
+ return (
+ acc +
+ `if(${camel(
+ propName,
+ )}${formatedKey} !== undefined) {\n ${formDataValue} }\n`
+ );
+ },
+ '',
+ );
+
+ return `${form}${formDataValues}`;
+ }
+
+ if (schema.type === 'array') {
+ return `${form}${camel(
+ propName,
+ )}.forEach(value => ${variableName}.append('data', value))\n`;
+ }
+
+ if (schema.type === 'number' || schema.type === 'boolean') {
+ return `${form}${variableName}.append('data', ${camel(
+ propName,
+ )}.toString())\n`;
+ }
+
+ return `${form}${variableName}.append('data', ${camel(propName)})\n`;
+};
diff --git a/packages/core/src/getters/response.ts b/packages/core/src/getters/response.ts
new file mode 100644
index 000000000..aeb79f73f
--- /dev/null
+++ b/packages/core/src/getters/response.ts
@@ -0,0 +1,97 @@
+import { ResponsesObject } from 'openapi3-ts';
+import {
+ ContextSpecs,
+ GetterResponse,
+ OverrideOutputContentType,
+ ResReqTypesValue,
+} from '../types';
+import { getResReqTypes } from './res-req-types';
+
+export const getResponse = ({
+ responses,
+ operationName,
+ context,
+ contentType,
+}: {
+ responses: ResponsesObject;
+ operationName: string;
+ context: ContextSpecs;
+ contentType?: OverrideOutputContentType;
+}): GetterResponse => {
+ if (!responses) {
+ return {
+ imports: [],
+ definition: {
+ success: '',
+ errors: '',
+ },
+ isBlob: false,
+ types: { success: [], errors: [] },
+ schemas: [],
+ contentTypes: [],
+ };
+ }
+
+ const types = getResReqTypes(
+ Object.entries(responses),
+ operationName,
+ context,
+ 'void',
+ );
+
+ const filteredTypes = contentType
+ ? types.filter((type) => {
+ let include = true;
+ let exclude = false;
+
+ if (contentType.include) {
+ include = contentType.include.includes(type.contentType);
+ }
+
+ if (contentType.exclude) {
+ exclude = contentType.exclude.includes(type.contentType);
+ }
+
+ return include && !exclude;
+ })
+ : types;
+
+ const groupedByStatus = filteredTypes.reduce<{
+ success: ResReqTypesValue[];
+ errors: ResReqTypesValue[];
+ }>(
+ (acc, type) => {
+ if (type.key.startsWith('2')) {
+ acc.success.push(type);
+ } else {
+ acc.errors.push(type);
+ }
+ return acc;
+ },
+ { success: [], errors: [] },
+ );
+
+ const imports = filteredTypes.flatMap(({ imports }) => imports);
+ const schemas = filteredTypes.flatMap(({ schemas }) => schemas);
+
+ const contentTypes = [
+ ...new Set(filteredTypes.map(({ contentType }) => contentType)),
+ ];
+
+ const success = groupedByStatus.success
+ .map(({ value, formData }) => (formData ? 'Blob' : value))
+ .join(' | ');
+ const errors = groupedByStatus.errors.map(({ value }) => value).join(' | ');
+
+ return {
+ imports,
+ definition: {
+ success: success || 'unknown',
+ errors: errors || 'unknown',
+ },
+ isBlob: success === 'Blob',
+ types: groupedByStatus,
+ contentTypes,
+ schemas,
+ };
+};
diff --git a/packages/core/src/getters/route.test.ts b/packages/core/src/getters/route.test.ts
new file mode 100644
index 000000000..152419c22
--- /dev/null
+++ b/packages/core/src/getters/route.test.ts
@@ -0,0 +1,20 @@
+import { getRoute } from './route';
+
+describe('getRoute getter', () => {
+ [
+ ['/api/test/{id}', '/api/test/${id}'],
+ ['/api/test/{path*}', '/api/test/${path}'],
+ ['/api/test/{user_id}', '/api/test/${userId}'],
+ ['/api/test/{locale}.js', '/api/test/${locale}.js'],
+ ['/api/test/i18n-{locale}.js', '/api/test/i18n-${locale}.js'],
+ ['/api/test/{param1}-{param2}.js', '/api/test/${param1}-${param2}.js'],
+ [
+ '/api/test/user{param1}-{param2}.html',
+ '/api/test/user${param1}-${param2}.html',
+ ],
+ ].forEach(([input, expected]) => {
+ it(`should process ${input} to ${expected}`, () => {
+ expect(getRoute(input)).toBe(expected);
+ });
+ });
+});
diff --git a/packages/core/src/getters/route.ts b/packages/core/src/getters/route.ts
new file mode 100644
index 000000000..94c37b504
--- /dev/null
+++ b/packages/core/src/getters/route.ts
@@ -0,0 +1,39 @@
+import { camel, sanitize } from '../utils';
+
+const hasParam = (path: string): boolean => /[^{]*{[\w*_-]*}.*/.test(path);
+
+const getRoutePath = (path: string): string => {
+ const matches = path.match(/([^{]*){?([\w*_-]*)}?(.*)/);
+ if (!matches?.length) return path; // impossible due to regexp grouping here, but for TS
+
+ const prev = matches[1];
+ const param = sanitize(camel(matches[2]), {
+ es5keyword: true,
+ underscore: true,
+ dash: true,
+ dot: true,
+ });
+ const next = hasParam(matches[3]) ? getRoutePath(matches[3]) : matches[3];
+
+ if (hasParam(path)) {
+ return `${prev}\${${param}}${next}`;
+ } else {
+ return `${prev}${param}${next}`;
+ }
+};
+
+export const getRoute = (route: string) => {
+ const splittedRoute = route.split('/');
+
+ return splittedRoute.reduce((acc, path, i) => {
+ if (!path && !i) {
+ return acc;
+ }
+
+ if (!path.includes('{')) {
+ return `${acc}/${path}`;
+ }
+
+ return `${acc}/${getRoutePath(path)}`;
+ }, '');
+};
diff --git a/packages/core/src/getters/scalar.ts b/packages/core/src/getters/scalar.ts
new file mode 100644
index 000000000..9b4519944
--- /dev/null
+++ b/packages/core/src/getters/scalar.ts
@@ -0,0 +1,128 @@
+import { SchemaObject } from 'openapi3-ts';
+import { ContextSpecs } from '../types';
+import { ResolverValue } from '../types';
+import { isString } from '../utils';
+import { escape } from '../utils';
+import { getArray } from './array';
+import { getObject } from './object';
+
+/**
+ * Return the typescript equivalent of open-api data type
+ *
+ * @param item
+ * @ref https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.1.md#data-types
+ */
+export const getScalar = ({
+ item,
+ name,
+ context,
+}: {
+ item: SchemaObject;
+ name?: string;
+ context: ContextSpecs;
+}): ResolverValue => {
+ const nullable = item.nullable ? ' | null' : '';
+
+ if (!item.type && item.items) {
+ item.type = 'array';
+ }
+
+ switch (item.type) {
+ case 'number':
+ case 'integer': {
+ let value = 'number';
+ let isEnum = false;
+
+ if (item.enum) {
+ value = item.enum.join(' | ');
+ isEnum = true;
+ }
+
+ return {
+ value: value + nullable,
+ isEnum,
+ type: 'number',
+ schemas: [],
+ imports: [],
+ isRef: false,
+ };
+ }
+
+ case 'boolean':
+ return {
+ value: 'boolean' + nullable,
+ type: 'boolean',
+ isEnum: false,
+ schemas: [],
+ imports: [],
+ isRef: false,
+ };
+
+ case 'array': {
+ const { value, ...rest } = getArray({
+ schema: item,
+ name,
+ context,
+ });
+ return {
+ value: value + nullable,
+ ...rest,
+ };
+ }
+
+ case 'string': {
+ let value = 'string';
+ let isEnum = false;
+
+ if (item.enum) {
+ value = `'${item.enum
+ .map((enumItem: string) =>
+ isString(enumItem) ? escape(enumItem) : enumItem,
+ )
+ .filter(Boolean)
+ .join(`' | '`)}'`;
+ isEnum = true;
+ }
+
+ if (item.format === 'binary') {
+ value = 'Blob';
+ }
+
+ if (context.override.useDates) {
+ if (item.format === 'date' || item.format === 'date-time') {
+ value = 'Date';
+ }
+ }
+
+ return {
+ value: value + nullable,
+ isEnum,
+ type: 'string',
+ imports: [],
+ schemas: [],
+ isRef: false,
+ };
+ }
+
+ case 'null':
+ return {
+ value: 'null',
+ isEnum: false,
+ type: 'null',
+ imports: [],
+ schemas: [],
+ isRef: false,
+ };
+
+ case 'object':
+ default: {
+ const { value, ...rest } = getObject({
+ item,
+ name,
+ context,
+ nullable,
+ });
+ return { value: value, ...rest };
+ }
+ }
+};
diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts
new file mode 100644
index 000000000..ca1730bf9
--- /dev/null
+++ b/packages/core/src/index.ts
@@ -0,0 +1,7 @@
+export * from './constants';
+export * from './generators';
+export * from './getters';
+export * from './resolvers';
+export * from './types';
+export * from './utils';
+export * from './writers';
diff --git a/packages/core/src/resolvers/index.ts b/packages/core/src/resolvers/index.ts
new file mode 100644
index 000000000..ef9b94b63
--- /dev/null
+++ b/packages/core/src/resolvers/index.ts
@@ -0,0 +1,3 @@
+export * from './object';
+export * from './ref';
+export * from './value';
diff --git a/packages/core/src/resolvers/object.ts b/packages/core/src/resolvers/object.ts
new file mode 100644
index 000000000..69894e1ab
--- /dev/null
+++ b/packages/core/src/resolvers/object.ts
@@ -0,0 +1,71 @@
+import { ReferenceObject, SchemaObject } from 'openapi3-ts';
+import { getEnum } from '../getters/enum';
+import { ContextSpecs, ResolverValue } from '../types';
+import { jsDoc } from '../utils';
+import { resolveValue } from './value';
+
+export const resolveObject = ({
+ schema,
+ propName,
+ combined = false,
+ context,
+}: {
+ schema: SchemaObject | ReferenceObject;
+ propName?: string;
+ combined?: boolean;
+ context: ContextSpecs;
+}): ResolverValue => {
+ const resolvedValue = resolveValue({
+ schema,
+ name: propName,
+ context,
+ });
+ const doc = jsDoc(resolvedValue.originalSchema ?? {});
+
+ if (
+ propName &&
+ !resolvedValue.isEnum &&
+ resolvedValue?.type === 'object' &&
+ new RegExp(/{|&|\|/).test(resolvedValue.value)
+ ) {
+ return {
+ value: propName,
+ imports: [{ name: propName }],
+ schemas: [
+ ...resolvedValue.schemas,
+ {
+ name: propName,
+ model: `${doc}export type ${propName} = ${resolvedValue.value};\n`,
+ imports: resolvedValue.imports,
+ },
+ ],
+ isEnum: false,
+ type: 'object',
+ originalSchema: resolvedValue.originalSchema,
+ isRef: resolvedValue.isRef,
+ };
+ }
+
+ if (propName && resolvedValue.isEnum && !combined && !resolvedValue.isRef) {
+ const enumValue = getEnum(resolvedValue.value, propName);
+
+ return {
+ value: propName,
+ imports: [{ name: propName }],
+ schemas: [
+ ...resolvedValue.schemas,
+ {
+ name: propName,
+ model: doc + enumValue,
+ imports: resolvedValue.imports,
+ },
+ ],
+ isEnum: false,
+ type: 'enum',
+ originalSchema: resolvedValue.originalSchema,
+ isRef: resolvedValue.isRef,
+ };
+ }
+
+ return resolvedValue;
+};
diff --git a/packages/core/src/resolvers/ref.ts b/packages/core/src/resolvers/ref.ts
new file mode 100644
index 000000000..58b5f7653
--- /dev/null
+++ b/packages/core/src/resolvers/ref.ts
@@ -0,0 +1,67 @@
+import get from 'lodash.get';
+import {
+ ParameterObject,
+ ReferenceObject,
+ RequestBodyObject,
+ ResponseObject,
+ SchemaObject,
+} from 'openapi3-ts';
+import { getRefInfo } from '../getters/ref';
+import { ContextSpecs, GeneratorImport } from '../types';
+import { isReference } from '../utils';
+
+type ComponentObject =
+ | SchemaObject
+ | ResponseObject
+ | ParameterObject
+ | RequestBodyObject
+ | ReferenceObject;
+export const resolveRef = (
+ schema: ComponentObject,
+ context: ContextSpecs,
+ imports: GeneratorImport[] = [],
+): {
+ schema: Schema;
+ imports: GeneratorImport[];
+} => {
+ // the schema is refering to another object
+ if ((schema as any)?.schema?.$ref) {
+ const resolvedRef = resolveRef(
+ (schema as any)?.schema,
+ context,
+ imports,
+ );
+ return {
+ schema: {
+ ...schema,
+ schema: resolvedRef.schema,
+ } as Schema,
+ imports,
+ };
+ }
+
+ if (!isReference(schema)) {
+ return { schema: schema as Schema, imports };
+ }
+
+ const { name, originalName, specKey, refPaths } = getRefInfo(
+ schema.$ref,
+ context,
+ );
+
+ const currentSchema = (
+ refPaths
+ ? get(context.specs[specKey || context.specKey], refPaths)
+ : context.specs[specKey || context.specKey]
+ ) as Schema;
+
+ if (!currentSchema) {
+ throw `Oups... 🍻. Ref not found: ${schema.$ref}`;
+ }
+
+ return resolveRef(
+ currentSchema,
+ { ...context, specKey: specKey || context.specKey },
+ [...imports, { name, specKey, schemaName: originalName }],
+ );
+};
diff --git a/packages/core/src/resolvers/value.ts b/packages/core/src/resolvers/value.ts
new file mode 100644
index 000000000..731b040e9
--- /dev/null
+++ b/packages/core/src/resolvers/value.ts
@@ -0,0 +1,45 @@
+import { ReferenceObject, SchemaObject } from 'openapi3-ts';
+import { getScalar } from '../getters';
+import { ContextSpecs, ResolverValue, SchemaType } from '../types';
+import { isReference } from '../utils';
+import { resolveRef } from './ref';
+
+export const resolveValue = ({
+ schema,
+ name,
+ context,
+}: {
+ schema: SchemaObject | ReferenceObject;
+ name?: string;
+ context: ContextSpecs;
+}): ResolverValue => {
+ if (isReference(schema)) {
+ const { schema: schemaObject, imports } = resolveRef(
+ schema,
+ context,
+ );
+ const { name, specKey, schemaName } = imports[0];
+
+ const importSpecKey =
+ specKey ||
+ (context.specKey !== context.target ? context.specKey : undefined);
+
+ return {
+ value: name,
+ imports: [{ name, specKey: importSpecKey, schemaName }],
+ type: (schemaObject?.type as SchemaType) || 'object',
+ schemas: [],
+ isEnum: !!schemaObject?.enum,
+ originalSchema: schemaObject,
+ isRef: true,
+ };
+ }
+
+ const scalar = getScalar({ item: schema, name, context });
+
+ return {
+ ...scalar,
+ originalSchema: schema,
+ isRef: false,
+ };
+};
diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts
new file mode 100644
index 000000000..b97b08300
--- /dev/null
+++ b/packages/core/src/types.ts
@@ -0,0 +1,746 @@
+import SwaggerParser from '@apidevtools/swagger-parser';
+import {
+ InfoObject,
+ OpenAPIObject,
+ OperationObject,
+ ParameterObject,
+ SchemaObject,
+} from 'openapi3-ts';
+import swagger2openapi from 'swagger2openapi';
+
+export interface Options {
+ output?: string | OutputOptions;
+ input?: string | InputOptions;
+ hooks?: Partial;
+}
+
+export type OptionsFn = () => Options | Promise;
+export type OptionsExport = Options | Promise | OptionsFn;
+
+export type Config = {
+ [project: string]: OptionsExport;
+};
+export type ConfigFn = () => Config | Promise;
+
+export type ConfigExternal = Config | Promise | ConfigFn;
+
+export type NormizaledConfig = {
+ [project: string]: NormalizedOptions;
+};
+
+export interface NormalizedOptions {
+ output: NormalizedOutputOptions;
+ input: NormalizedInputOptions;
+ hooks: NormalizedHookOptions;
+}
+
+export type NormalizedOutputOptions = {
+ workspace?: string;
+ target?: string;
+ schemas?: string;
+ mode: OutputMode;
+ mock: boolean | ClientMSWBuilder;
+ override: NormalizedOverrideOutput;
+ client: OutputClient | OutputClientFunc;
+ clean: boolean | string[];
+ prettier: boolean;
+ tslint: boolean;
+ tsconfig?: Tsconfig;
+ packageJson?: PackageJson;
+ headers: boolean;
+};
+
+export type NormalizedOverrideOutput = {
+ title?: (title: string) => string;
+ transformer?: OutputTransformer;
+ mutator?: NormalizedMutator;
+ operations: { [key: string]: NormalizedOperationOptions };
+ tags: { [key: string]: NormalizedOperationOptions };
+ mock?: {
+ arrayMin?: number;
+ arrayMax?: number;
+ properties?: MockProperties;
+ format?: { [key: string]: unknown };
+ required?: boolean;
+ baseUrl?: string;
+ delay?: number;
+ };
+ contentType?: OverrideOutputContentType;
+ header: false | ((info: InfoObject) => string[] | string);
+ formData: boolean | NormalizedMutator;
+ formUrlEncoded: boolean | NormalizedMutator;
+ components: {
+ schemas: {
+ suffix: string;
+ };
+ responses: {
+ suffix: string;
+ };
+ parameters: {
+ suffix: string;
+ };
+ requestBodies: {
+ suffix: string;
+ };
+ };
+ query: QueryOptions;
+ angular: Required;
+ swr: {
+ options?: any;
+ };
+ operationName?: (
+ operation: OperationObject,
+ route: string,
+ verb: Verbs,
+ ) => string;
+ requestOptions: Record | boolean;
+ useDates?: boolean;
+ useTypeOverInterfaces?: boolean;
+ useDeprecatedOperations?: boolean;
+};
+
+export type NormalizedMutator = {
+ path: string;
+ name?: string;
+ default: boolean;
+ alias?: Record;
+};
+
+export type NormalizedOperationOptions = {
+ transformer?: OutputTransformer;
+ mutator?: NormalizedMutator;
+ mock?: {
+ data?: MockProperties;
+ properties?: MockProperties;
+ };
+ contentType?: OverrideOutputContentType;
+ query?: QueryOptions;
+ angular?: Required;
+ swr?: {
+ options?: any;
+ };
+ operationName?: (
+ operation: OperationObject,
+ route: string,
+ verb: Verbs,
+ ) => string;
+ formData?: boolean | NormalizedMutator;
+ formUrlEncoded?: boolean | NormalizedMutator;
+ requestOptions?: object | boolean;
+};
+export type NormalizedInputOptions = {
+ target: string | Record | OpenAPIObject;
+ validation: boolean;
+ override: OverrideInput;
+ converterOptions: swagger2openapi.Options;
+ parserOptions: SwaggerParserOptions;
+};
+
+export type OutputClientFunc = (
+ clients: GeneratorClients,
+) => ClientGeneratorsBuilder;
+
+export type OutputOptions = {
+ workspace?: string;
+ target?: string;
+ schemas?: string;
+ mode?: OutputMode;
+ mock?: boolean | ClientMSWBuilder;
+ override?: OverrideOutput;
+ client?: OutputClient | OutputClientFunc;
+ clean?: boolean | string[];
+ prettier?: boolean;
+ tslint?: boolean;
+ tsconfig?: string | Tsconfig;
+ packageJson?: string;
+ headers?: boolean;
+};
+
+export type SwaggerParserOptions = Omit & {
+ validate?: boolean;
+};
+
+export type InputOptions = {
+ target: string | Record | OpenAPIObject;
+ validation?: boolean;
+ override?: OverrideInput;
+ converterOptions?: swagger2openapi.Options;
+ parserOptions?: SwaggerParserOptions;
+};
+
+export type OutputClient =
+ | 'axios'
+ | 'axios-functions'
+ | 'angular'
+ | 'react-query'
+ | 'svelte-query'
+ | 'vue-query'
+ | 'swr';
+
+export const OutputClient = {
+ ANGULAR: 'angular' as OutputClient,
+ AXIOS: 'axios' as OutputClient,
+ AXIOS_FUNCTIONS: 'axios-functions' as OutputClient,
+ REACT_QUERY: 'react-query' as OutputClient,
+ SVELTE_QUERY: 'svelte-query' as OutputClient,
+ VUE_QUERY: 'vue-query' as OutputClient,
+};
+
+export type OutputMode = 'single' | 'split' | 'tags' | 'tags-split';
+export const OutputMode = {
+ SINGLE: 'single' as OutputMode,
+ SPLIT: 'split' as OutputMode,
+ TAGS: 'tags' as OutputMode,
+ TAGS_SPLIT: 'tags-split' as OutputMode,
+};
+
+export type MockOptions = {
+ arrayMin?: number;
+ arrayMax?: number;
+ required?: boolean;
+ properties?: Record;
+ operations?: Record }>;
+ format?: Record;
+ tags?: Record }>;
+};
+
+export type MockProperties =
+ | { [key: string]: unknown }
+ | ((specs: OpenAPIObject) => { [key: string]: unknown });
+
+type OutputTransformerFn = (verb: GeneratorVerbOptions) => GeneratorVerbOptions;
+
+type OutputTransformer = string | OutputTransformerFn;
+
+export type MutatorObject = {
+ path: string;
+ name?: string;
+ default?: boolean;
+ alias?: Record;
+};
+
+export type Mutator = string | MutatorObject;
+
+export type OverrideOutput = {
+ title?: (title: string) => string;
+ transformer?: OutputTransformer;
+ mutator?: Mutator;
+ operations?: { [key: string]: OperationOptions };
+ tags?: { [key: string]: OperationOptions };
+ mock?: {
+ arrayMin?: number;
+ arrayMax?: number;
+ properties?: MockProperties;
+ format?: { [key: string]: unknown };
+ required?: boolean;
+ baseUrl?: string;
+ delay?: number;
+ };
+ contentType?: OverrideOutputContentType;
+ header?: boolean | ((info: InfoObject) => string[] | string);
+ formData?: boolean | Mutator;
+ formUrlEncoded?: boolean | Mutator;
+ components?: {
+ schemas?: {
+ suffix?: string;
+ };
+ responses?: {
+ suffix?: string;
+ };
+ parameters?: {
+ suffix?: string;
+ };
+ requestBodies?: {
+ suffix?: string;
+ };
+ };
+ query?: QueryOptions;
+ swr?: {
+ options?: any;
+ };
+ angular?: AngularOptions;
+ operationName?: (
+ operation: OperationObject,
+ route: string,
+ verb: Verbs,
+ ) => string;
+ requestOptions?: Record | boolean;
+ useDates?: boolean;
+ useTypeOverInterfaces?: boolean;
+ useDeprecatedOperations?: boolean;
+};
+
+export type OverrideOutputContentType = {
+ include?: string[];
+ exclude?: string[];
+};
+
+type QueryOptions = {
+ useQuery?: boolean;
+ useInfinite?: boolean;
+ useInfiniteQueryParam?: string;
+ options?: any;
+ signal?: boolean;
+};
+
+export type AngularOptions = {
+ provideIn?: 'root' | 'any' | boolean;
+};
+
+export type InputTransformerFn = (spec: OpenAPIObject) => OpenAPIObject;
+
+type InputTransformer = string | InputTransformerFn;
+
+export type OverrideInput = {
+ transformer?: InputTransformer;
+};
+
+export type OperationOptions = {
+ transformer?: OutputTransformer;
+ mutator?: Mutator;
+ mock?: {
+ data?: MockProperties;
+ properties?: MockProperties;
+ };
+ query?: QueryOptions;
+ angular?: Required;
+ swr?: {
+ options?: any;
+ };
+ operationName?: (
+ operation: OperationObject,
+ route: string,
+ verb: Verbs,
+ ) => string;
+ formData?: boolean | Mutator;
+ formUrlEncoded?: boolean | Mutator;
+ requestOptions?: object | boolean;
+};
+
+export type Hook = 'afterAllFilesWrite';
+
+export type HookFunction = (...args: any[]) => void | Promise;
+
+export type HookCommand = string | HookFunction | (string | HookFunction)[];
+
+export type NormalizedHookCommand = HookCommand[];
+
+export type HooksOptions = Partial<
+ Record
+>;
+
+export type NormalizedHookOptions = HooksOptions;
+
+export type Verbs = 'post' | 'put' | 'get' | 'patch' | 'delete' | 'head';
+
+export const Verbs = {
+ POST: 'post' as Verbs,
+ PUT: 'put' as Verbs,
+ GET: 'get' as Verbs,
+ PATCH: 'patch' as Verbs,
+ DELETE: 'delete' as Verbs,
+ HEAD: 'head' as Verbs,
+};
+
+export type ImportOpenApi = {
+ data: Record;
+ input: InputOptions;
+ output: NormalizedOutputOptions;
+ target: string;
+ workspace: string;
+};
+
+export interface ContextSpecs {
+ specKey: string;
+ target: string;
+ workspace: string;
+ tslint: boolean;
+ specs: Record;
+ override: NormalizedOverrideOutput;
+ tsconfig?: Tsconfig;
+ packageJson?: PackageJson;
+}
+
+export interface GlobalOptions {
+ projectName?: string;
+ watch?: boolean | string | (string | boolean)[];
+ clean?: boolean | string[];
+ prettier?: boolean;
+ tslint?: boolean;
+ mock?: boolean;
+ client?: OutputClient;
+ mode?: OutputMode;
+ tsconfig?: string | Tsconfig;
+ packageJson?: string;
+ input?: string;
+ output?: string;
+}
+
+export interface Tsconfig {
+ baseUrl?: string;
+ compilerOptions?: {
+ esModuleInterop?: boolean;
+ allowSyntheticDefaultImports?: boolean;
+ exactOptionalPropertyTypes?: boolean;
+ paths?: Record;
+ };
+}
+
+export interface PackageJson {
+ dependencies?: Record;
+ devDependencies?: Record;
+}
+
+export type GeneratorSchema = {
+ name: string;
+ model: string;
+ imports: GeneratorImport[];
+};
+
+export type GeneratorImport = {
+ name: string;
+ schemaName?: string;
+ alias?: string;
+ specKey?: string;
+ default?: boolean;
+ values?: boolean;
+ syntheticDefaultImport?: boolean;
+};
+
+export type GeneratorDependency = {
+ exports: GeneratorImport[];
+ dependency: string;
+};
+
+export type GeneratorApiResponse = {
+ operations: GeneratorOperations;
+ schemas: GeneratorSchema[];
+};
+
+export type GeneratorOperations = {
+ [operationId: string]: GeneratorOperation;
+};
+
+export type GeneratorTarget = {
+ imports: GeneratorImport[];
+ implementation: string;
+ implementationMSW: string;
+ importsMSW: GeneratorImport[];
+ mutators?: GeneratorMutator[];
+ formData?: GeneratorMutator[];
+ formUrlEncoded?: GeneratorMutator[];
+};
+
+export type GeneratorTargetFull = {
+ imports: GeneratorImport[];
+ implementation: string;
+ implementationMSW: {
+ function: string;
+ handler: string;
+ };
+ importsMSW: GeneratorImport[];
+ mutators?: GeneratorMutator[];
+ formData?: GeneratorMutator[];
+ formUrlEncoded?: GeneratorMutator[];
+};
+
+export type GeneratorOperation = {
+ imports: GeneratorImport[];
+ implementation: string;
+ implementationMSW: { function: string; handler: string };
+ importsMSW: GeneratorImport[];
+ tags: string[];
+ mutator?: GeneratorMutator;
+ formData?: GeneratorMutator;
+ formUrlEncoded?: GeneratorMutator;
+ operationName: string;
+ types?: {
+ result: (title?: string) => string;
+ };
+};
+
+export type GeneratorVerbOptions = {
+ verb: Verbs;
+ summary?: string;
+ doc: string;
+ tags: string[];
+ operationId: string;
+ operationName: string;
+ response: GetterResponse;
+ body: GetterBody;
+ headers?: GetterQueryParam;
+ queryParams?: GetterQueryParam;
+ params: GetterParams;
+ props: GetterProps;
+ mutator?: GeneratorMutator;
+ formData?: GeneratorMutator;
+ formUrlEncoded?: GeneratorMutator;
+ override: NormalizedOverrideOutput;
+ deprecated?: boolean;
+};
+
+export type GeneratorVerbsOptions = GeneratorVerbOptions[];
+
+export type GeneratorOptions = {
+ route: string;
+ pathRoute: string;
+ override: NormalizedOverrideOutput;
+ context: ContextSpecs;
+ mock: boolean;
+};
+
+export type GeneratorClient = {
+ implementation: string;
+ imports: GeneratorImport[];
+ types?: {
+ result: (title?: string) => string;
+ };
+};
+
+export type GeneratorMutatorParsingInfo = {
+ numberOfParams: number;
+ returnNumberOfParams?: number;
+};
+export type GeneratorMutator = {
+ name: string;
+ path: string;
+ default: boolean;
+ hasErrorType: boolean;
+ errorTypeName: string;
+ hasSecondArg: boolean;
+ hasThirdArg: boolean;
+ isHook: boolean;
+ bodyTypeName?: string;
+};
+
+export type ClientBuilder = (
+ verbOptions: GeneratorVerbOptions,
+ options: GeneratorOptions,
+ outputClient: OutputClient | OutputClientFunc,
+) => GeneratorClient;
+
+export type ClientHeaderBuilder = (params: {
+ title: string;
+ isRequestOptions: boolean;
+ isMutator: boolean;
+ noFunction?: boolean;
+ isGlobalMutator: boolean;
+ provideIn: boolean | 'root' | 'any';
+ hasAwaitedType: boolean;
+}) => string;
+
+export type ClientFooterBuilder = (params: {
+ noFunction?: boolean | undefined;
+ operationNames: string[];
+ title?: string;
+ hasAwaitedType: boolean;
+ hasMutator: boolean;
+}) => string;
+
+export type ClientTitleBuilder = (title: string) => string;
+
+export type ClientDependenciesBuilder = (
+ hasGlobalMutator: boolean,
+ packageJson?: PackageJson,
+) => GeneratorDependency[];
+
+export type ClientMSWBuilder = (
+ verbOptions: GeneratorVerbOptions,
+ generatorOptions: GeneratorOptions,
+) => {
+ imports: string[];
+ implementation: string;
+};
+
+export interface ClientGeneratorsBuilder {
+ client: ClientBuilder;
+ header: ClientHeaderBuilder;
+ dependencies: ClientDependenciesBuilder;
+ footer: ClientFooterBuilder;
+ title: ClientTitleBuilder;
+}
+
+export type GeneratorClients = Record;
+
+export type GetterResponse = {
+ imports: GeneratorImport[];
+ definition: {
+ success: string;
+ errors: string;
+ };
+ isBlob: boolean;
+ types: {
+ success: ResReqTypesValue[];
+ errors: ResReqTypesValue[];
+ };
+ contentTypes: string[];
+ schemas: GeneratorSchema[];
+};
+
+export type GetterBody = {
+ imports: GeneratorImport[];
+ definition: string;
+ implementation: string;
+ schemas: GeneratorSchema[];
+ formData?: string;
+ formUrlEncoded?: string;
+ contentType: string;
+};
+
+export type GetterParameters = {
+ query: { parameter: ParameterObject; imports: GeneratorImport[] }[];
+ path: { parameter: ParameterObject; imports: GeneratorImport[] }[];
+ header: { parameter: ParameterObject; imports: GeneratorImport[] }[];
+};
+
+export type GetterParam = {
+ name: string;
+ definition: string;
+ implementation: string;
+ default: boolean;
+ required: boolean;
+ imports: GeneratorImport[];
+};
+
+export type GetterParams = GetterParam[];
+export type GetterQueryParam = {
+ schema: GeneratorSchema;
+ deps: GeneratorSchema[];
+ isOptional: boolean;
+};
+
+export type GetterPropType = 'param' | 'body' | 'queryParam' | 'header';
+
+export const GetterPropType = {
+ PARAM: 'param' as GetterPropType,
+ BODY: 'body' as GetterPropType,
+ QUERY_PARAM: 'queryParam' as GetterPropType,
+ HEADER: 'header' as GetterPropType,
+};
+
+export type GetterProp = {
+ name: string;
+ definition: string;
+ implementation: string;
+ default: boolean;
+ required: boolean;
+ type: GetterPropType;
+};
+
+export type GetterProps = GetterProp[];
+
+export type SchemaType =
+ | 'integer'
+ | 'number'
+ | 'string'
+ | 'boolean'
+ | 'object'
+ | 'null'
+ | 'array'
+ | 'enum'
+ | 'unknown';
+
+export type ResolverValue = {
+ value: string;
+ isEnum: boolean;
+ type: SchemaType;
+ imports: GeneratorImport[];
+ schemas: GeneratorSchema[];
+ originalSchema?: SchemaObject;
+ isRef: boolean;
+};
+
+export type ResReqTypesValue = ResolverValue & {
+ formData?: string;
+ formUrlEncoded?: string;
+ isRef?: boolean;
+ key: string;
+ contentType: string;
+};
+
+export type WriteSpecsBuilder = {
+ operations: GeneratorOperations;
+ schemas: Record;
+ title: GeneratorClientTitle;
+ header: GeneratorClientHeader;
+ footer: GeneratorClientFooter;
+ imports: GeneratorClientImports;
+ importsMock: GenerateMockImports;
+ info: InfoObject;
+ target: string;
+};
+
+export type WriteModeProps = {
+ builder: WriteSpecsBuilder;
+ output: NormalizedOutputOptions;
+ workspace: string;
+ specsName: Record;
+ header: string;
+};
+
+export type GeneratorApiOperations = {
+ operations: GeneratorOperations;
+ schemas: GeneratorSchema[];
+};
+
+export type GeneratorClientExtra = {
+ implementation: string;
+ implementationMSW: string;
+};
+
+export type GeneratorClientTitle = (data: {
+ outputClient?: OutputClient | OutputClientFunc;
+ title: string;
+ customTitleFunc?: (title: string) => string;
+}) => GeneratorClientExtra;
+
+export type GeneratorClientHeader = (data: {
+ outputClient?: OutputClient | OutputClientFunc;
+ isRequestOptions: boolean;
+ isMutator: boolean;
+ isGlobalMutator: boolean;
+ provideIn: boolean | 'root' | 'any';
+ hasAwaitedType: boolean;
+ titles: GeneratorClientExtra;
+}) => GeneratorClientExtra;
+
+export type GeneratorClientFooter = (data: {
+ outputClient: OutputClient | OutputClientFunc;
+ operationNames: string[];
+ hasMutator: boolean;
+ hasAwaitedType: boolean;
+ titles: GeneratorClientExtra;
+}) => GeneratorClientExtra;
+
+export type GeneratorClientImports = (data: {
+ client: OutputClient | OutputClientFunc;
+ implementation: string;
+ imports: {
+ exports: GeneratorImport[];
+ dependency: string;
+ }[];
+ specsName: Record;
+ hasSchemaDir: boolean;
+ isAllowSyntheticDefaultImports: boolean;
+ hasGlobalMutator: boolean;
+ packageJson?: PackageJson;
+}) => string;
+
+export type GenerateMockImports = (data: {
+ implementation: string;
+ imports: {
+ exports: GeneratorImport[];
+ dependency: string;
+ }[];
+ specsName: Record;
+ hasSchemaDir: boolean;
+ isAllowSyntheticDefaultImports: boolean;
+}) => string;
+
+export type GeneratorApiBuilder = GeneratorApiOperations & {
+ title: GeneratorClientTitle;
+ header: GeneratorClientHeader;
+ footer: GeneratorClientFooter;
+ imports: GeneratorClientImports;
+ importsMock: GenerateMockImports;
+};
diff --git a/packages/core/src/utils/assertion.ts b/packages/core/src/utils/assertion.ts
new file mode 100644
index 000000000..c0e89feb5
--- /dev/null
+++ b/packages/core/src/utils/assertion.ts
@@ -0,0 +1,58 @@
+import { ReferenceObject } from 'openapi3-ts';
+import { extname } from 'upath';
+import validatorIsUrl from 'validator/lib/isURL';
+import { Verbs } from '../types';
+
+/**
+ * Discriminator helper for `ReferenceObject`
+ *
+ * @param property
+ */
+export const isReference = (property: any): property is ReferenceObject => {
+ return Boolean(property.$ref);
+};
+
+export const isDirectory = (path: string) => {
+ return !extname(path);
+};
+
+export function isObject(x: any): x is Record {
+ return Object.prototype.toString.call(x) === '[object Object]';
+}
+
+export function isString(x: any): x is string {
+ return typeof x === 'string';
+}
+
+export function isNumber(x: any): x is number {
+ return typeof x === 'number';
+}
+
+export function isBoolean(x: any): x is boolean {
+ return typeof x === 'boolean';
+}
+
+export function isFunction(x: any): x is Function {
+ return typeof x === 'function';
+}
+
+export function isUndefined(x: any): x is undefined {
+ return typeof x === 'undefined';
+}
+
+export function isNull(x: any): x is null {
+ return typeof x === null;
+}
+
+export const isVerb = (verb: string): verb is Verbs =>
+ Object.values(Verbs).includes(verb as Verbs);
+
+export const isRootKey = (specKey: string, target: string) => {
+ return specKey === target;
+};
+
+const LOCALHOST_REGEX = /^https?:\/\/\w+(\.\w+)*(:[0-9]+)?(\/.*)?$/;
+
+export const isUrl = (str: string) => {
+ return validatorIsUrl(str) || LOCALHOST_REGEX.test(str);
+};
diff --git a/packages/core/src/utils/async-reduce.ts b/packages/core/src/utils/async-reduce.ts
new file mode 100644
index 000000000..92039fd26
--- /dev/null
+++ b/packages/core/src/utils/async-reduce.ts
@@ -0,0 +1,19 @@
+export async function asyncReduce(
+ array: IterationItem[],
+ reducer: (
+ accumulate: AccValue,
+ current: IterationItem,
+ ) => AccValue | Promise,
+ initValue: AccValue,
+): Promise {
+ let accumulate =
+ typeof initValue === 'object'
+ ? Object.create(initValue as unknown as object)
+ : initValue;
+
+ for (const item of array) {
+ accumulate = await reducer(accumulate, item);
+ }
+
+ return accumulate;
+}
diff --git a/packages/core/src/utils/case.ts b/packages/core/src/utils/case.ts
new file mode 100644
index 000000000..893d33427
--- /dev/null
+++ b/packages/core/src/utils/case.ts
@@ -0,0 +1,127 @@
+const unicodes = function (s: string, prefix: string) {
+ prefix = prefix || '';
+ return s.replace(/(^|-)/g, '$1\\u' + prefix).replace(/,/g, '\\u' + prefix);
+};
+
+const symbols = unicodes('20-26,28-2F,3A-40,5B-60,7B-7E,A0-BF,D7,F7', '00');
+const lowers = 'a-z' + unicodes('DF-F6,F8-FF', '00');
+const uppers = 'A-Z' + unicodes('C0-D6,D8-DE', '00');
+const impropers =
+ 'A|An|And|As|At|But|By|En|For|If|In|Of|On|Or|The|To|Vs?\\.?|Via';
+
+const regexps = {
+ capitalize: new RegExp('(^|[' + symbols + '])([' + lowers + '])', 'g'),
+ pascal: new RegExp('(^|[' + symbols + '])+([' + lowers + uppers + '])', 'g'),
+ fill: new RegExp('[' + symbols + ']+(.|$)', 'g'),
+ sentence: new RegExp(
+ '(^\\s*|[\\?\\!\\.]+"?\\s+"?|,\\s+")([' + lowers + '])',
+ 'g',
+ ),
+ improper: new RegExp('\\b(' + impropers + ')\\b', 'g'),
+ relax: new RegExp(
+ '([^' +
+ uppers +
+ '])([' +
+ uppers +
+ ']*)([' +
+ uppers +
+ '])(?=[^' +
+ uppers +
+ ']|$)',
+ 'g',
+ ),
+ upper: new RegExp('^[^' + lowers + ']+$'),
+ hole: /[^\s]\s[^\s]/,
+ apostrophe: /'/g,
+ room: new RegExp('[' + symbols + ']'),
+};
+
+const deapostrophe = (s: string) => {
+ return s.replace(regexps.apostrophe, '');
+};
+
+const up = String.prototype.toUpperCase;
+const low = String.prototype.toLowerCase;
+
+const fill = (s: string, fillWith?: string, isDeapostrophe = false) => {
+ if (fillWith != null) {
+ s = s.replace(regexps.fill, function (m, next) {
+ return next ? fillWith + next : '';
+ });
+ }
+ if (isDeapostrophe) {
+ s = deapostrophe(s);
+ }
+ return s;
+};
+
+const decap = (s: string) => {
+ return low.call(s.charAt(0)) + s.slice(1);
+};
+
+const relax = (
+ m: string,
+ before: string,
+ acronym: string | undefined,
+ caps: string,
+) => {
+ return before + ' ' + (acronym ? acronym + ' ' : '') + caps;
+};
+
+const prep = (s: string, isFill = false, isPascal = false, isUpper = false) => {
+ s = s == null ? '' : s + ''; // force to string
+ if (!isUpper && regexps.upper.test(s)) {
+ s = low.call(s);
+ }
+ if (!isFill && !regexps.hole.test(s)) {
+ var holey = fill(s, ' ');
+ if (regexps.hole.test(holey)) {
+ s = holey;
+ }
+ }
+ if (!isPascal && !regexps.room.test(s)) {
+ s = s.replace(regexps.relax, relax);
+ }
+ return s;
+};
+
+const lower = (s: string, fillWith: string, isDeapostrophe: boolean) => {
+ return fill(low.call(prep(s, !!fillWith)), fillWith, isDeapostrophe);
+};
+
+export const pascal = (s: string) => {
+ return fill(
+ prep(s, false, true).replace(
+ regexps.pascal,
+ (m: string, border: string, letter: string) => {
+ return up.call(letter);
+ },
+ ),
+ '',
+ true,
+ );
+};
+
+export const camel = (s: string) => {
+ return decap(pascal(s));
+};
+
+export const snake = (s: string) => {
+ return lower(s, '_', true);
+};
+
+export const kebab = (s: string) => {
+ return lower(s, '-', true);
+};
+
+export const upper = (
+ s: string,
+ fillWith?: string,
+ isDeapostrophe?: boolean,
+) => {
+ return fill(
+ up.call(prep(s, !!fillWith, false, true)),
+ fillWith,
+ isDeapostrophe,
+ );
+};
diff --git a/packages/core/src/utils/compare-version.ts b/packages/core/src/utils/compare-version.ts
new file mode 100644
index 000000000..4db4558a9
--- /dev/null
+++ b/packages/core/src/utils/compare-version.ts
@@ -0,0 +1,17 @@
+import { compare, CompareOperator } from 'compare-versions';
+
+export const compareVersions = (
+ firstVersion: string,
+ secondVersions: string,
+ operator: CompareOperator = '>=',
+) => {
+ if (firstVersion === 'latest') {
+ return true;
+ }
+
+ return compare(
+ firstVersion.replace(/(\s(.*))/, ''),
+ secondVersions,
+ operator,
+ );
+};
diff --git a/packages/core/src/utils/debug.ts b/packages/core/src/utils/debug.ts
new file mode 100644
index 000000000..c8828e618
--- /dev/null
+++ b/packages/core/src/utils/debug.ts
@@ -0,0 +1,26 @@
+import debug from 'debug';
+
+const filter = process.env.ORVAL_DEBUG_FILTER;
+const DEBUG = process.env.DEBUG;
+
+interface DebuggerOptions {
+ onlyWhenFocused?: boolean | string;
+}
+
+export function createDebugger(
+ ns: string,
+ options: DebuggerOptions = {},
+): debug.Debugger['log'] {
+ const log = debug(ns);
+ const { onlyWhenFocused } = options;
+ const focus = typeof onlyWhenFocused === 'string' ? onlyWhenFocused : ns;
+ return (msg: string, ...args: any[]) => {
+ if (filter && !msg.includes(filter)) {
+ return;
+ }
+ if (onlyWhenFocused && !DEBUG?.includes(focus)) {
+ return;
+ }
+ log(msg, ...args);
+ };
+}
diff --git a/packages/core/src/utils/doc.ts b/packages/core/src/utils/doc.ts
new file mode 100644
index 000000000..ffd88def9
--- /dev/null
+++ b/packages/core/src/utils/doc.ts
@@ -0,0 +1,68 @@
+const search = '\\*/'; // Find '*/'
+const replacement = '*\\/'; // Replace With '*\/'
+
+const regex = new RegExp(search, 'g');
+
+export function jsDoc(
+ {
+ description,
+ deprecated,
+ summary,
+ }: {
+ description?: string[] | string;
+ deprecated?: boolean;
+ summary?: string;
+ },
+ tryOneLine = false,
+): string {
+ // Ensure there aren't any comment terminations in doc
+ const lines = (
+ Array.isArray(description)
+ ? description.filter((d) => !d.includes('eslint-disable'))
+ : [description || '']
+ ).map((line) => line.replace(regex, replacement));
+
+ const count = [description, deprecated, summary].reduce(
+ (acc, it) => (it ? acc + 1 : acc),
+ 0,
+ );
+
+ if (!count) {
+ return '';
+ }
+
+ const oneLine = count === 1 && tryOneLine;
+ const eslintDisable = Array.isArray(description)
+ ? description
+ .find((d) => d.includes('eslint-disable'))
+ ?.replace(regex, replacement)
+ : undefined;
+ let doc = `${eslintDisable ? `/* ${eslintDisable} */\n` : ''}/**`;
+
+ if (description) {
+ if (!oneLine) {
+ doc += `\n${tryOneLine ? ' ' : ''} *`;
+ }
+ doc += ` ${lines.join('\n * ')}`;
+ }
+
+ if (deprecated) {
+ if (!oneLine) {
+ doc += `\n${tryOneLine ? ' ' : ''} *`;
+ }
+ doc += ' @deprecated';
+ }
+
+ if (summary) {
+ if (!oneLine) {
+ doc += `\n${tryOneLine ? ' ' : ''} *`;
+ }
+ doc += ` @summary ${summary.replace(regex, replacement)}`;
+ }
+
+ doc += !oneLine ? `\n ${tryOneLine ? ' ' : ''}` : ' ';
+
+ doc += '*/\n';
+
+ return doc;
+}
diff --git a/packages/core/src/utils/dynamic-import.ts b/packages/core/src/utils/dynamic-import.ts
new file mode 100644
index 000000000..118bb025c
--- /dev/null
+++ b/packages/core/src/utils/dynamic-import.ts
@@ -0,0 +1,28 @@
+import { resolve } from 'upath';
+import { isObject, isString } from './assertion';
+
+export const dynamicImport = async (
+ toImport: T | string,
+ from = process.cwd(),
+ takeDefault = true,
+): Promise => {
+ if (!toImport) {
+ return toImport as T;
+ }
+
+ try {
+ if (isString(toImport)) {
+ const path = resolve(from, toImport);
+ const data = await import(path);
+ if (takeDefault && isObject(data) && data.default) {
+ return (data as any).default as T;
+ }
+
+ return data;
+ }
+
+ return Promise.resolve(toImport);
+ } catch (error) {
+ throw `Oups... 🍻. Path: ${toImport} => ${error}`;
+ }
+};
diff --git a/packages/core/src/utils/extension.ts b/packages/core/src/utils/extension.ts
new file mode 100644
index 000000000..00c06f881
--- /dev/null
+++ b/packages/core/src/utils/extension.ts
@@ -0,0 +1,4 @@
+export const getExtension = (path: string) =>
+ path.toLowerCase().includes('.yaml') || path.toLowerCase().includes('.yml')
+ ? 'yaml'
+ : 'json';
diff --git a/packages/core/src/utils/file.ts b/packages/core/src/utils/file.ts
new file mode 100644
index 000000000..59f27415a
--- /dev/null
+++ b/packages/core/src/utils/file.ts
@@ -0,0 +1,374 @@
+import chalk from 'chalk';
+import { build, PluginBuild } from 'esbuild';
+import fs from 'fs';
+import glob from 'globby';
+import mm from 'micromatch';
+import path from 'path';
+import {
+ basename,
+ dirname,
+ extname,
+ join,
+ joinSafe,
+ normalizeSafe,
+ resolve,
+} from 'upath';
+import { Tsconfig } from '../types';
+import { isDirectory } from './assertion';
+import { createDebugger } from './debug';
+import { createLogger, LogLevel } from './logger';
+
+export const getFileInfo = (
+ target: string = '',
+ {
+ backupFilename = 'filename',
+ extension = '.ts',
+ }: { backupFilename?: string; extension?: string } = {},
+) => {
+ const isDir = isDirectory(target);
+ const path = isDir ? join(target, backupFilename + extension) : target;
+ const pathWithoutExtension = path.replace(/\.[^/.]+$/, '');
+ const dir = dirname(path);
+ const filename = basename(
+ path,
+ extension[0] !== '.' ? `.${extension}` : extension,
+ );
+
+ return {
+ path,
+ pathWithoutExtension,
+ extension,
+ isDirectory: isDir,
+ dirname: dir,
+ filename,
+ };
+};
+
+const debug = createDebugger('orval:file-load');
+
+const cache = new Map();
+
+export async function loadFile(
+ filePath?: string,
+ options?: {
+ root?: string;
+ defaultFileName?: string;
+ logLevel?: LogLevel;
+ isDefault?: boolean;
+ alias?: Record;
+ tsconfig?: Tsconfig;
+ load?: boolean;
+ },
+): Promise<{
+ path: string;
+ file?: File;
+ error?: any;
+ cached?: boolean;
+}> {
+ const {
+ root = process.cwd(),
+ isDefault = true,
+ defaultFileName,
+ logLevel,
+ alias,
+ tsconfig,
+ load = true,
+ } = options ?? {};
+ const start = Date.now();
+
+ let resolvedPath: string | undefined;
+ let isTS = false;
+ let isMjs = false;
+
+ if (filePath) {
+ // explicit path is always resolved from cwd
+ resolvedPath = path.resolve(filePath);
+ isTS = filePath.endsWith('.ts');
+ } else if (defaultFileName) {
+ // implicit file loaded from inline root (if present)
+ // otherwise from cwd
+ const jsFile = path.resolve(root, `${defaultFileName}.js`);
+ if (fs.existsSync(jsFile)) {
+ resolvedPath = jsFile;
+ }
+
+ if (!resolvedPath) {
+ const mjsFile = path.resolve(root, `${defaultFileName}.mjs`);
+ if (fs.existsSync(mjsFile)) {
+ resolvedPath = mjsFile;
+ isMjs = true;
+ }
+ }
+
+ if (!resolvedPath) {
+ const tsFile = path.resolve(root, `${defaultFileName}.ts`);
+ if (fs.existsSync(tsFile)) {
+ resolvedPath = tsFile;
+ isTS = true;
+ }
+ }
+ }
+
+ if (!resolvedPath) {
+ if (filePath) {
+ createLogger(logLevel).error(chalk.red(`File not found => ${filePath}`));
+ } else if (defaultFileName) {
+ createLogger(logLevel).error(
+ chalk.red(`File not found => ${defaultFileName}.{js,mjs,ts}`),
+ );
+ } else {
+ createLogger(logLevel).error(chalk.red(`File not found`));
+ }
+ process.exit(1);
+ }
+
+ const normalizeResolvedPath = normalizeSafe(resolvedPath);
+ const cachedData = cache.get(resolvedPath);
+
+ if (cachedData) {
+ return {
+ path: normalizeResolvedPath,
+ ...cachedData,
+ cached: true,
+ };
+ }
+
+ try {
+ let file: File | undefined;
+
+ if (!file && !isTS && !isMjs) {
+ // 1. try to directly require the module (assuming commonjs)
+ try {
+ // clear cache in case of server restart
+ delete require.cache[require.resolve(resolvedPath)];
+
+ file = require(resolvedPath);
+
+ debug(`cjs loaded in ${Date.now() - start}ms`);
+ } catch (e) {
+ const ignored = new RegExp(
+ [
+ `Cannot use import statement`,
+ `Must use import to load ES Module`,
+ // #1635, #2050 some Node 12.x versions don't have esm detection
+ // so it throws normal syntax errors when encountering esm syntax
+ `Unexpected token`,
+ `Unexpected identifier`,
+ ].join('|'),
+ );
+ //@ts-ignore
+ if (!ignored.test(e.message)) {
+ throw e;
+ }
+ }
+ }
+
+ if (!file) {
+ // 2. if we reach here, the file is ts or using es import syntax, or
+ // the user has type: "module" in their package.json (#917)
+ // transpile es import syntax to require syntax using rollup.
+ // lazy require rollup (it's actually in dependencies)
+ const { code } = await bundleFile(
+ resolvedPath,
+ isMjs,
+ root || dirname(normalizeResolvedPath),
+ alias,
+ tsconfig?.compilerOptions,
+ );
+
+ if (load) {
+ file = await loadFromBundledFile(resolvedPath, code, isDefault);
+ } else {
+ file = code as any;
+ }
+
+ debug(`bundled file loaded in ${Date.now() - start}ms`);
+ }
+
+ cache.set(resolvedPath, { file });
+
+ return {
+ path: normalizeResolvedPath,
+ file,
+ };
+ } catch (error: any) {
+ cache.set(resolvedPath, { error });
+
+ return {
+ path: normalizeResolvedPath,
+ error,
+ };
+ }
+}
+
+async function bundleFile(
+ fileName: string,
+ mjs = false,
+ workspace: string,
+ alias?: Record,
+ compilerOptions?: Tsconfig['compilerOptions'],
+): Promise<{ code: string; dependencies: string[] }> {
+ const result = await build({
+ absWorkingDir: process.cwd(),
+ entryPoints: [fileName],
+ outfile: 'out.js',
+ write: false,
+ platform: 'node',
+ bundle: true,
+ format: mjs ? 'esm' : 'cjs',
+ sourcemap: 'inline',
+ metafile: true,
+ target: 'es6',
+ minifyWhitespace: true,
+ plugins: [
+ ...(alias || compilerOptions?.paths
+ ? [
+ {
+ name: 'aliasing',
+ setup(build: PluginBuild) {
+ build.onResolve(
+ { filter: /^[\w@][^:]/ },
+ async ({ path: id }) => {
+ if (alias) {
+ const matchKeys = Object.keys(alias);
+ const match = matchKeys.find(
+ (key) =>
+ id.startsWith(key) || mm.isMatch(id, matchKeys),
+ );
+
+ if (match) {
+ const find = mm.scan(match);
+ const replacement = mm.scan(alias[match]);
+
+ const base = resolve(workspace, replacement.base);
+ const newPath = find.base
+ ? id.replace(find.base, base)
+ : joinSafe(base, id);
+
+ const ext = extname(newPath);
+
+ const aliased = ext ? newPath : `${newPath}.ts`;
+
+ if (!fs.existsSync(aliased)) {
+ return;
+ }
+
+ return {
+ path: aliased,
+ };
+ }
+ }
+
+ if (compilerOptions?.paths) {
+ const matchKeys = Object.keys(compilerOptions?.paths);
+ const match = matchKeys.find(
+ (key) =>
+ id.startsWith(key) || mm.isMatch(id, matchKeys),
+ );
+
+ if (match) {
+ const find = mm.scan(match);
+ const replacement = mm.scan(
+ compilerOptions?.paths[match][0],
+ );
+
+ const base = resolve(workspace, replacement.base);
+ const newPath = find.base
+ ? id.replace(find.base, base)
+ : joinSafe(base, id);
+
+ const ext = extname(newPath);
+
+ const aliased = ext ? newPath : `${newPath}.ts`;
+
+ if (!fs.existsSync(aliased)) {
+ return;
+ }
+
+ return {
+ path: aliased,
+ };
+ }
+ }
+ },
+ );
+ },
+ },
+ ]
+ : []),
+ {
+ name: 'externalize-deps',
+ setup(build) {
+ build.onResolve({ filter: /.*/ }, (args) => {
+ const id = args.path;
+ if (id[0] !== '.' && !path.isAbsolute(id)) {
+ return {
+ external: true,
+ };
+ }
+ });
+ },
+ },
+ {
+ name: 'replace-import-meta',
+ setup(build) {
+ build.onLoad({ filter: /\.[jt]s$/ }, async (args) => {
+ const contents = await fs.promises.readFile(args.path, 'utf8');
+ return {
+ loader: args.path.endsWith('.ts') ? 'ts' : 'js',
+ contents: contents
+ .replace(
+ /\bimport\.meta\.url\b/g,
+ JSON.stringify(`file://${args.path}`),
+ )
+ .replace(
+ /\b__dirname\b/g,
+ JSON.stringify(path.dirname(args.path)),
+ )
+ .replace(/\b__filename\b/g, JSON.stringify(args.path)),
+ };
+ });
+ },
+ },
+ ],
+ });
+ const { text } = result.outputFiles[0];
+ return {
+ code: text,
+ dependencies: result.metafile ? Object.keys(result.metafile.inputs) : [],
+ };
+}
+
+interface NodeModuleWithCompile extends NodeModule {
+ _compile(code: string, filename: string): any;
+}
+
+async function loadFromBundledFile(
+ fileName: string,
+ bundledCode: string,
+ isDefault: boolean,
+): Promise {
+ const extension = path.extname(fileName);
+ const defaultLoader = require.extensions[extension]!;
+ require.extensions[extension] = (module: NodeModule, filename: string) => {
+ if (filename === fileName) {
+ (module as NodeModuleWithCompile)._compile(bundledCode, filename);
+ } else {
+ defaultLoader(module, filename);
+ }
+ };
+ // clear cache in case of server restart
+ delete require.cache[require.resolve(fileName)];
+ const raw = require(fileName);
+ const file = isDefault && raw.__esModule ? raw.default : raw;
+ require.extensions[extension] = defaultLoader;
+ return file;
+}
+
+export async function removeFiles(patterns: string[], dir: string) {
+ const files = await glob(patterns, {
+ cwd: dir,
+ absolute: true,
+ });
+ await Promise.all(files.map((file) => fs.promises.unlink(file)));
+}
diff --git a/packages/core/src/utils/index.ts b/packages/core/src/utils/index.ts
new file mode 100644
index 000000000..c8d632037
--- /dev/null
+++ b/packages/core/src/utils/index.ts
@@ -0,0 +1,18 @@
+export * from './assertion';
+export * from './async-reduce';
+export * from './case';
+export * from './compare-version';
+export * from './debug';
+export * from './doc';
+export * from './dynamic-import';
+export * from './extension';
+export * from './file';
+export * from './logger';
+export * from './merge-deep';
+export * from './occurrence';
+export * from './open-api-converter';
+export * from './path';
+export * from './sort';
+export * from './string';
+export * from './tsconfig';
+export * from './validator';
diff --git a/packages/core/src/utils/logger.ts b/packages/core/src/utils/logger.ts
new file mode 100644
index 000000000..809ef413a
--- /dev/null
+++ b/packages/core/src/utils/logger.ts
@@ -0,0 +1,177 @@
+import chalk from 'chalk';
+import readline from 'readline';
+export const log = console.log; // tslint:disable-line:no-console
+
+export const startMessage = ({
+ name,
+ version,
+ description,
+}: {
+ name: string;
+ version: string;
+ description: string;
+}) =>
+ log(
+ `🍻 Start ${chalk.cyan.bold(name)} ${chalk.green(`v${version}`)}${
+ description ? ` - ${description}` : ''
+ }`,
+ );
+
+export const errorMessage = (err: string) => log(chalk.red(err));
+
+export const mismatchArgsMessage = (mismatchArgs: string[]) =>
+ log(
+ chalk.yellow(
+ `${mismatchArgs.join(', ')} ${
+ mismatchArgs.length === 1 ? 'is' : 'are'
+ } not defined in your configuration!`,
+ ),
+ );
+
+export const createSuccessMessage = (backend?: string) =>
+ log(
+ `🎉 ${
+ backend ? `${chalk.green(backend)} - ` : ''
+ }Your OpenAPI spec has been converted into ready to use orval!`,
+ );
+
+export const ibmOpenapiValidatorWarnings = (
+ warnings: {
+ path: string;
+ message: string;
+ }[],
+) => {
+ log(chalk.yellow('(!) Warnings'));
+
+ warnings.forEach((i) =>
+ log(chalk.yellow(`Message : ${i.message}\nPath : ${i.path}`)),
+ );
+};
+
+export const ibmOpenapiValidatorErrors = (
+ errors: {
+ path: string;
+ message: string;
+ }[],
+) => {
+ log(chalk.red('(!) Errors'));
+
+ errors.forEach((i) =>
+ log(chalk.red(`Message : ${i.message}\nPath : ${i.path}`)),
+ );
+};
+
+export type LogType = 'error' | 'warn' | 'info';
+export type LogLevel = LogType | 'silent';
+export interface Logger {
+ info(msg: string, options?: LogOptions): void;
+ warn(msg: string, options?: LogOptions): void;
+ warnOnce(msg: string, options?: LogOptions): void;
+ error(msg: string, options?: LogOptions): void;
+ clearScreen(type: LogType): void;
+ hasWarned: boolean;
+}
+
+export interface LogOptions {
+ clear?: boolean;
+ timestamp?: boolean;
+}
+
+export const LogLevels: Record = {
+ silent: 0,
+ error: 1,
+ warn: 2,
+ info: 3,
+};
+
+let lastType: LogType | undefined;
+let lastMsg: string | undefined;
+let sameCount = 0;
+
+function clearScreen() {
+ const repeatCount = process.stdout.rows - 2;
+ const blank = repeatCount > 0 ? '\n'.repeat(repeatCount) : '';
+ console.log(blank);
+ readline.cursorTo(process.stdout, 0, 0);
+ readline.clearScreenDown(process.stdout);
+}
+
+export interface LoggerOptions {
+ prefix?: string;
+ allowClearScreen?: boolean;
+}
+
+export function createLogger(
+ level: LogLevel = 'info',
+ options: LoggerOptions = {},
+): Logger {
+ const { prefix = '[vite]', allowClearScreen = true } = options;
+
+ const thresh = LogLevels[level];
+ const clear =
+ allowClearScreen && process.stdout.isTTY && !process.env.CI
+ ? clearScreen
+ : () => {};
+
+ function output(type: LogType, msg: string, options: LogOptions = {}) {
+ if (thresh >= LogLevels[type]) {
+ const method = type === 'info' ? 'log' : type;
+ const format = () => {
+ if (options.timestamp) {
+ const tag =
+ type === 'info'
+ ? chalk.cyan.bold(prefix)
+ : type === 'warn'
+ ? chalk.yellow.bold(prefix)
+ : chalk.red.bold(prefix);
+ return `${chalk.dim(new Date().toLocaleTimeString())} ${tag} ${msg}`;
+ } else {
+ return msg;
+ }
+ };
+ if (type === lastType && msg === lastMsg) {
+ sameCount++;
+ clear();
+ console[method](format(), chalk.yellow(`(x${sameCount + 1})`));
+ } else {
+ sameCount = 0;
+ lastMsg = msg;
+ lastType = type;
+ if (options.clear) {
+ clear();
+ }
+ console[method](format());
+ }
+ }
+ }
+
+ const warnedMessages = new Set();
+
+ const logger: Logger = {
+ hasWarned: false,
+ info(msg, opts) {
+ output('info', msg, opts);
+ },
+ warn(msg, opts) {
+ logger.hasWarned = true;
+ output('warn', msg, opts);
+ },
+ warnOnce(msg, opts) {
+ if (warnedMessages.has(msg)) return;
+ logger.hasWarned = true;
+ output('warn', msg, opts);
+ warnedMessages.add(msg);
+ },
+ error(msg, opts) {
+ logger.hasWarned = true;
+ output('error', msg, opts);
+ },
+ clearScreen(type) {
+ if (thresh >= LogLevels[type]) {
+ clear();
+ }
+ },
+ };
+
+ return logger;
+}
diff --git a/packages/core/src/utils/merge-deep.ts b/packages/core/src/utils/merge-deep.ts
new file mode 100644
index 000000000..d5150447f
--- /dev/null
+++ b/packages/core/src/utils/merge-deep.ts
@@ -0,0 +1,24 @@
+const isObject = (obj: unknown) => obj && typeof obj === 'object';
+
+export function mergeDeep>(
+ source: T,
+ target: T,
+): T {
+ if (!isObject(target) || !isObject(source)) {
+ return source;
+ }
+
+ return Object.entries(target).reduce((acc, [key, value]) => {
+ const sourceValue = acc[key];
+
+ if (Array.isArray(sourceValue) && Array.isArray(value)) {
+ (acc[key] as any) = [...sourceValue, ...value];
+ } else if (isObject(sourceValue) && isObject(value)) {
+ (acc[key] as any) = mergeDeep(sourceValue, value);
+ } else {
+ (acc[key] as any) = value;
+ }
+
+ return acc;
+ }, source);
+}
diff --git a/packages/core/src/utils/occurrence.ts b/packages/core/src/utils/occurrence.ts
new file mode 100644
index 000000000..1c79911f7
--- /dev/null
+++ b/packages/core/src/utils/occurrence.ts
@@ -0,0 +1,7 @@
+export const count = (str: string = '', key: string) => {
+ if (!str) {
+ return 0;
+ }
+
+ return (str.match(new RegExp(key, 'g')) ?? []).length;
+};
diff --git a/packages/core/src/utils/open-api-converter.ts b/packages/core/src/utils/open-api-converter.ts
new file mode 100644
index 000000000..4af82f289
--- /dev/null
+++ b/packages/core/src/utils/open-api-converter.ts
@@ -0,0 +1,29 @@
+import chalk from 'chalk';
+import { OpenAPIObject } from 'openapi3-ts';
+import swagger2openapi from 'swagger2openapi';
+import { log } from './logger';
+
+export const openApiConverter = async (
+ schema: any,
+ options: swagger2openapi.Options = {},
+ specKey: string,
+): Promise => {
+ try {
+ return new Promise((resolve) => {
+ if (!schema.openapi && schema.swagger === '2.0') {
+ swagger2openapi.convertObj(schema, options, (err, value) => {
+ if (err) {
+ log(chalk.yellow(`${specKey}\n=> ${err}`));
+ resolve(schema);
+ } else {
+ resolve(value.openapi);
+ }
+ });
+ } else {
+ resolve(schema);
+ }
+ });
+ } catch (e) {
+ throw `Oups... 🍻.\nPath: ${specKey}\nParsing Error: ${e}`;
+ }
+};
diff --git a/packages/core/src/utils/path.ts b/packages/core/src/utils/path.ts
new file mode 100644
index 000000000..0de019391
--- /dev/null
+++ b/packages/core/src/utils/path.ts
@@ -0,0 +1,40 @@
+import {
+ normalize,
+ normalizeSafe,
+ relative as normalizedRelative,
+ sep as seperator,
+} from 'upath';
+import { isUrl } from './assertion';
+import { getExtension } from './extension';
+import { getFileInfo } from './file';
+
+/**
+ * Behaves exactly like `path.relative(from, to)`, but keeps the first meaningful "./"
+ */
+export const relativeSafe = (from: string, to: string) => {
+ const normalizedRelativePath = normalizedRelative(from, to);
+ /**
+ * Prepend "./" to every path and then use normalizeSafe method to normalize it
+ * normalizeSafe doesn't remove meaningful leading "./"
+ */
+ const relativePath = normalizeSafe(`.${seperator}${normalizedRelativePath}`);
+ return relativePath;
+};
+
+export const getSpecName = (specKey: string, target: string) => {
+ if (isUrl(specKey)) {
+ const url = new URL(target);
+ return specKey
+ .replace(url.origin, '')
+ .replace(getFileInfo(url.pathname).dirname, '')
+ .replace(`.${getExtension(specKey)}`, '');
+ }
+
+ return (
+ '/' +
+ normalize(normalizedRelative(getFileInfo(target).dirname, specKey))
+ .split('../')
+ .join('')
+ .replace(`.${getExtension(specKey)}`, '')
+ );
+};
diff --git a/packages/core/src/utils/sort.ts b/packages/core/src/utils/sort.ts
new file mode 100644
index 000000000..e0b527616
--- /dev/null
+++ b/packages/core/src/utils/sort.ts
@@ -0,0 +1,25 @@
+export const sortByPriority = (
+ arr: (T & { default?: boolean; required?: boolean })[],
+) =>
+ arr.sort((a, b) => {
+ if (a.default) {
+ return 1;
+ }
+
+ if (b.default) {
+ return -1;
+ }
+
+ if (a.required && b.required) {
+ return 0;
+ }
+
+ if (a.required) {
+ return -1;
+ }
+
+ if (b.required) {
+ return 1;
+ }
+ return 0;
+ });
diff --git a/packages/core/src/utils/string.ts b/packages/core/src/utils/string.ts
new file mode 100644
index 000000000..8e3e38874
--- /dev/null
+++ b/packages/core/src/utils/string.ts
@@ -0,0 +1,141 @@
+import { keyword } from 'esutils';
+import get from 'lodash.get';
+import {
+ isBoolean,
+ isFunction,
+ isNull,
+ isNumber,
+ isString,
+ isUndefined,
+} from './assertion';
+
+export const stringify = (
+ data?: string | any[] | { [key: string]: any },
+): string | undefined => {
+ if (isUndefined(data) || isNull(data)) {
+ return;
+ }
+
+ if (isString(data)) {
+ return `'${data}'`;
+ }
+
+ if (isNumber(data) || isBoolean(data) || isFunction(data)) {
+ return `${data}`;
+ }
+
+ if (Array.isArray(data)) {
+ return `[${data.map(stringify).join(', ')}]`;
+ }
+
+ return Object.entries(data).reduce((acc, [key, value], index, arr) => {
+ const strValue = stringify(value);
+ if (arr.length === 1) {
+ return `{ ${key}: ${strValue}, }`;
+ }
+
+ if (!index) {
+ return `{ ${key}: ${strValue}, `;
+ }
+
+ if (arr.length - 1 === index) {
+ return acc + `${key}: ${strValue}, }`;
+ }
+
+ return acc + `${key}: ${strValue}, `;
+ }, '');
+};
+
+export const sanitize = (
+ value: string,
+ options?: {
+ whitespace?: string | true;
+ underscore?: string | true;
+ dot?: string | true;
+ dash?: string | true;
+ es5keyword?: boolean;
+ es5IdentifierName?: boolean;
+ special?: boolean;
+ },
+) => {
+ const {
+ whitespace = '',
+ underscore = '',
+ dot = '',
+ dash = '',
+ es5keyword = false,
+ es5IdentifierName = false,
+ special = false,
+ } = options ?? {};
+ let newValue = value;
+
+ if (special !== true) {
+ newValue = newValue.replace(
+ /[!"`'#%&,:;<>=@{}~\$\(\)\*\+\/\\\?\[\]\^\|]/g,
+ '',
+ );
+ }
+
+ if (whitespace !== true) {
+ newValue = newValue.replace(/[\s]/g, whitespace);
+ }
+
+ if (underscore !== true) {
+ newValue = newValue.replace(/['_']/g, underscore);
+ }
+
+ if (dot !== true) {
+ newValue = newValue.replace(/[.]/g, dot);
+ }
+
+ if (dash !== true) {
+ newValue = newValue.replace(/[-]/g, dash);
+ }
+
+ if (es5keyword) {
+ newValue = keyword.isKeywordES5(newValue, true) ? `_${newValue}` : newValue;
+ }
+
+ if (es5IdentifierName) {
+ if (newValue.match(/^[0-9]/)) {
+ newValue = `N${newValue}`;
+ } else {
+ newValue = keyword.isIdentifierNameES5(newValue)
+ ? newValue
+ : `_${newValue}`;
+ }
+ }
+
+ return newValue;
+};
+
+export const toObjectString = (props: T[], path?: keyof T) => {
+ if (!props.length) {
+ return '';
+ }
+
+ const arrayOfString = path ? props.map((prop) => get(prop, path)) : props;
+
+ return arrayOfString.join(',\n ') + ',';
+};
+
+const NUMBERS = {
+ '0': 'zero',
+ '1': 'one',
+ '2': 'two',
+ '3': 'three',
+ '4': 'four',
+ '5': 'five',
+ '6': 'six',
+ '7': 'seven',
+ '8': 'eight',
+ '9': 'nine',
+};
+
+export const getNumberWord = (num: number) => {
+ const arrayOfNumber = num.toString().split('') as (keyof typeof NUMBERS)[];
+ return arrayOfNumber.reduce((acc, n) => acc + NUMBERS[n], '');
+};
+
+export const escape = (str: string, char: string = "'") =>
+ str.replace(char, `\\${char}`);
diff --git a/packages/core/src/utils/tsconfig.ts b/packages/core/src/utils/tsconfig.ts
new file mode 100644
index 000000000..903853de6
--- /dev/null
+++ b/packages/core/src/utils/tsconfig.ts
@@ -0,0 +1,12 @@
+import { Tsconfig } from '../types';
+
+export const isSyntheticDefaultImportsAllow = (config?: Tsconfig) => {
+ if (!config) {
+ return true;
+ }
+
+ return !!(
+ config?.compilerOptions?.allowSyntheticDefaultImports ??
+ config?.compilerOptions?.esModuleInterop
+ );
+};
diff --git a/packages/core/src/utils/validator.ts b/packages/core/src/utils/validator.ts
new file mode 100644
index 000000000..74989ecf8
--- /dev/null
+++ b/packages/core/src/utils/validator.ts
@@ -0,0 +1,23 @@
+import openApiValidator from 'ibm-openapi-validator';
+import { OpenAPIObject } from 'openapi3-ts';
+import {
+ ibmOpenapiValidatorErrors,
+ ibmOpenapiValidatorWarnings,
+} from './logger';
+
+/**
+ * Validate the spec with ibm-openapi-validator (with a custom pretty logger).
+ * More information: https://github.com/IBM/openapi-validator/#configuration
+ * @param specs openAPI spec
+ */
+export const ibmOpenapiValidator = async (specs: OpenAPIObject) => {
+ const { errors, warnings } = await openApiValidator(specs);
+
+ if (warnings.length) {
+ ibmOpenapiValidatorWarnings(warnings);
+ }
+
+ if (errors.length) {
+ ibmOpenapiValidatorErrors(errors);
+ }
+};
diff --git a/packages/core/src/writers/index.ts b/packages/core/src/writers/index.ts
new file mode 100644
index 000000000..4ba8040e8
--- /dev/null
+++ b/packages/core/src/writers/index.ts
@@ -0,0 +1,7 @@
+export * from './schemas';
+export * from './single-mode';
+export * from './split-mode';
+export * from './split-tags-mode';
+export * from './tags-mode';
+export * from './target';
+export * from './target-tags';
diff --git a/packages/core/src/writers/schemas.ts b/packages/core/src/writers/schemas.ts
new file mode 100644
index 000000000..df2649766
--- /dev/null
+++ b/packages/core/src/writers/schemas.ts
@@ -0,0 +1,128 @@
+import { ensureFile, outputFile, readFile, writeFile } from 'fs-extra';
+import { join } from 'upath';
+import { generateImports } from '../generators';
+import { GeneratorSchema } from '../types';
+import { camel } from '../utils';
+
+const getSchema = ({
+ schema: { imports, model },
+ target,
+ isRootKey,
+ specsName,
+ header,
+}: {
+ schema: GeneratorSchema;
+ target: string;
+ isRootKey: boolean;
+ specsName: Record;
+ header: string;
+}): string => {
+ let file = header;
+ file += generateImports({
+ imports: imports.filter(
+ (imp) =>
+ !model.includes(`type ${imp.alias || imp.name} =`) &&
+ !model.includes(`interface ${imp.alias || imp.name} {`),
+ ),
+ target,
+ isRootKey,
+ specsName,
+ });
+ file += imports.length ? '\n\n' : '\n';
+ file += model;
+ return file;
+};
+
+const getPath = (path: string, name: string): string =>
+ join(path, `/${name}.ts`);
+
+export const writeModelInline = (acc: string, model: string): string =>
+ acc + `${model}\n`;
+
+export const writeModelsInline = (array: GeneratorSchema[]): string =>
+ array.reduce((acc, { model }) => writeModelInline(acc, model), '');
+
+export const writeSchema = async ({
+ path,
+ schema,
+ target,
+ isRootKey,
+ specsName,
+ header,
+}: {
+ path: string;
+ schema: GeneratorSchema;
+ target: string;
+ isRootKey: boolean;
+ specsName: Record;
+ header: string;
+}) => {
+ const name = camel(schema.name);
+ try {
+ await outputFile(
+ getPath(path, name),
+ getSchema({ schema, target, isRootKey, specsName, header }),
+ );
+ } catch (e) {
+ throw `Oups... 🍻. An Error occurred while writing schema ${name} => ${e}`;
+ }
+};
+
+export const writeSchemas = async ({
+ schemaPath,
+ schemas,
+ target,
+ isRootKey,
+ specsName,
+ header,
+}: {
+ schemaPath: string;
+ schemas: GeneratorSchema[];
+ target: string;
+ isRootKey: boolean;
+ specsName: Record;
+ header: string;
+}) => {
+ const schemaFilePath = join(schemaPath, '/index.ts');
+ await ensureFile(schemaFilePath);
+
+ await Promise.all(
+ schemas.map((schema) =>
+ writeSchema({
+ path: schemaPath,
+ schema,
+ target,
+ isRootKey,
+ specsName,
+ header,
+ }),
+ ),
+ );
+
+ try {
+ const data = await readFile(schemaFilePath);
+
+ const stringData = data.toString();
+
+ const importStatements = schemas
+ .filter((schema) => {
+ return (
+ !stringData.includes(`export * from './${camel(schema.name)}'`) &&
+ !stringData.includes(`export * from "./${camel(schema.name)}"`)
+ );
+ })
+ .map((schema) => `export * from './${camel(schema.name)}';`);
+
+ const currentFileExports = (stringData
+ .match(/export \* from(.*)('|")/g)
+ ?.map((s) => s + ';') ?? []) as string[];
+
+ const fileContent = [...currentFileExports, ...importStatements]
+ .sort()
+ .join('\n');
+
+ await writeFile(schemaFilePath, fileContent);
+ } catch (e) {
+ throw `Oups... 🍻. An Error occurred while writing schema index file ${schemaFilePath} => ${e}`;
+ }
+};
diff --git a/packages/core/src/writers/single-mode.ts b/packages/core/src/writers/single-mode.ts
new file mode 100644
index 000000000..7b4d4a775
--- /dev/null
+++ b/packages/core/src/writers/single-mode.ts
@@ -0,0 +1,104 @@
+import { outputFile } from 'fs-extra';
+import { generateModelsInline, generateMutatorImports } from '../generators';
+import { WriteModeProps } from '../types';
+import {
+ camel,
+ getFileInfo,
+ isSyntheticDefaultImportsAllow,
+ relativeSafe,
+} from '../utils';
+import { generateTarget } from './target';
+
+export const writeSingleMode = async ({
+ builder,
+ output,
+ specsName,
+ header,
+}: WriteModeProps): Promise => {
+ try {
+ const { path, dirname } = getFileInfo(output.target, {
+ backupFilename: camel(builder.info.title),
+ });
+
+ const {
+ imports,
+ importsMSW,
+ implementation,
+ implementationMSW,
+ mutators,
+ formData,
+ formUrlEncoded,
+ } = generateTarget(builder, output);
+
+ let data = header;
+
+ const schemasPath = output.schemas
+ ? relativeSafe(dirname, getFileInfo(output.schemas).dirname)
+ : undefined;
+
+ const isAllowSyntheticDefaultImports = isSyntheticDefaultImportsAllow(
+ output.tsconfig,
+ );
+
+ data += builder.imports({
+ client: output.client,
+ implementation,
+ imports: schemasPath
+ ? [
+ {
+ exports: imports.filter(
+ (imp) => !importsMSW.some((impMSW) => imp.name === impMSW.name),
+ ),
+ dependency: schemasPath,
+ },
+ ]
+ : [],
+ specsName,
+ hasSchemaDir: !!output.schemas,
+ isAllowSyntheticDefaultImports,
+ hasGlobalMutator: !!output.override.mutator,
+ packageJson: output.packageJson,
+ });
+
+ if (output.mock) {
+ data += builder.importsMock({
+ implementation: implementationMSW,
+ imports: schemasPath
+ ? [{ exports: importsMSW, dependency: schemasPath }]
+ : [],
+ specsName,
+ hasSchemaDir: !!output.schemas,
+ isAllowSyntheticDefaultImports,
+ });
+ }
+
+ if (mutators) {
+ data += generateMutatorImports({ mutators, implementation });
+ }
+
+ if (formData) {
+ data += generateMutatorImports({ mutators: formData });
+ }
+
+ if (formUrlEncoded) {
+ data += generateMutatorImports({ mutators: formUrlEncoded });
+ }
+
+ if (!output.schemas) {
+ data += generateModelsInline(builder.schemas);
+ }
+
+ data += `\n\n${implementation}`;
+
+ if (output.mock) {
+ data += '\n\n';
+ data += implementationMSW;
+ }
+
+ await outputFile(path, data);
+
+ return [path];
+ } catch (e) {
+ throw `Oups... 🍻. An Error occurred while writing file => ${e}`;
+ }
+};
diff --git a/packages/core/src/writers/split-mode.ts b/packages/core/src/writers/split-mode.ts
new file mode 100644
index 000000000..77a00f913
--- /dev/null
+++ b/packages/core/src/writers/split-mode.ts
@@ -0,0 +1,125 @@
+import { outputFile } from 'fs-extra';
+import { join } from 'upath';
+import { generateModelsInline, generateMutatorImports } from '../generators';
+import { OutputClient, WriteModeProps } from '../types';
+import {
+ camel,
+ getFileInfo,
+ isSyntheticDefaultImportsAllow,
+ relativeSafe,
+} from '../utils';
+import { generateTarget } from './target';
+
+export const writeSplitMode = async ({
+ builder,
+ output,
+ specsName,
+ header,
+}: WriteModeProps): Promise => {
+ try {
+ const { filename, dirname, extension } = getFileInfo(output.target, {
+ backupFilename: camel(builder.info.title),
+ });
+
+ const {
+ imports,
+ implementation,
+ implementationMSW,
+ importsMSW,
+ mutators,
+ formData,
+ formUrlEncoded,
+ } = generateTarget(builder, output);
+
+ let implementationData = header;
+ let mswData = header;
+
+ const relativeSchemasPath = output.schemas
+ ? relativeSafe(dirname, getFileInfo(output.schemas).dirname)
+ : './' + filename + '.schemas';
+
+ const isAllowSyntheticDefaultImports = isSyntheticDefaultImportsAllow(
+ output.tsconfig,
+ );
+
+ implementationData += builder.imports({
+ client: output.client,
+ implementation,
+ imports: [{ exports: imports, dependency: relativeSchemasPath }],
+ specsName,
+ hasSchemaDir: !!output.schemas,
+ isAllowSyntheticDefaultImports,
+ hasGlobalMutator: !!output.override.mutator,
+ packageJson: output.packageJson,
+ });
+ mswData += builder.importsMock({
+ implementation: implementationMSW,
+ imports: [
+ {
+ exports: importsMSW,
+ dependency: relativeSchemasPath,
+ },
+ ],
+ specsName,
+ hasSchemaDir: !!output.schemas,
+ isAllowSyntheticDefaultImports,
+ });
+
+ const schemasPath = !output.schemas
+ ? join(dirname, filename + '.schemas' + extension)
+ : undefined;
+
+ if (schemasPath) {
+ const schemasData = header + generateModelsInline(builder.schemas);
+
+ await outputFile(
+ join(dirname, filename + '.schemas' + extension),
+ schemasData,
+ );
+ }
+
+ if (mutators) {
+ implementationData += generateMutatorImports({
+ mutators,
+ implementation,
+ });
+ }
+
+ if (formData) {
+ implementationData += generateMutatorImports({ mutators: formData });
+ }
+
+ if (formUrlEncoded) {
+ implementationData += generateMutatorImports({
+ mutators: formUrlEncoded,
+ });
+ }
+
+ implementationData += `\n${implementation}`;
+ mswData += `\n${implementationMSW}`;
+
+ const implementationFilename =
+ filename +
+ (OutputClient.ANGULAR === output.client ? '.service' : '') +
+ extension;
+
+ const implementationPath = join(dirname, implementationFilename);
+ await outputFile(join(dirname, implementationFilename), implementationData);
+
+ const mockPath = output.mock
+ ? join(dirname, filename + '.msw' + extension)
+ : undefined;
+
+ if (mockPath) {
+ await outputFile(mockPath, mswData);
+ }
+
+ return [
+ implementationPath,
+ ...(schemasPath ? [schemasPath] : []),
+ ...(mockPath ? [mockPath] : []),
+ ];
+ } catch (e) {
+ throw `Oups... 🍻. An Error occurred while splitting => ${e}`;
+ }
+};
diff --git a/packages/core/src/writers/split-tags-mode.ts b/packages/core/src/writers/split-tags-mode.ts
new file mode 100644
index 000000000..8907907ff
--- /dev/null
+++ b/packages/core/src/writers/split-tags-mode.ts
@@ -0,0 +1,134 @@
+import { outputFile } from 'fs-extra';
+import { join } from 'upath';
+import { generateModelsInline, generateMutatorImports } from '../generators';
+import { OutputClient, WriteModeProps } from '../types';
+import {
+ camel,
+ getFileInfo,
+ isSyntheticDefaultImportsAllow,
+ relativeSafe,
+} from '../utils';
+import { generateTargetForTags } from './target-tags';
+
+export const writeSplitTagsMode = async ({
+ builder,
+ output,
+ specsName,
+ header,
+}: WriteModeProps): Promise => {
+ const { filename, dirname, extension } = getFileInfo(output.target, {
+ backupFilename: camel(builder.info.title),
+ });
+
+ const target = generateTargetForTags(builder, output);
+
+ const isAllowSyntheticDefaultImports = isSyntheticDefaultImportsAllow(
+ output.tsconfig,
+ );
+
+ const generatedFilePathsArray = await Promise.all(
+ Object.entries(target).map(async ([tag, target]) => {
+ try {
+ const {
+ imports,
+ implementation,
+ implementationMSW,
+ importsMSW,
+ mutators,
+ formData,
+ formUrlEncoded,
+ } = target;
+
+ let implementationData = header;
+ let mswData = header;
+
+ const relativeSchemasPath = output.schemas
+ ? '../' + relativeSafe(dirname, getFileInfo(output.schemas).dirname)
+ : '../' + filename + '.schemas';
+
+ implementationData += builder.imports({
+ client: output.client,
+ implementation,
+ imports: [{ exports: imports, dependency: relativeSchemasPath }],
+ specsName,
+ hasSchemaDir: !!output.schemas,
+ isAllowSyntheticDefaultImports,
+ hasGlobalMutator: !!output.override.mutator,
+ packageJson: output.packageJson,
+ });
+ mswData += builder.importsMock({
+ implementation: implementationMSW,
+ imports: [
+ {
+ exports: importsMSW,
+ dependency: relativeSchemasPath,
+ },
+ ],
+ specsName,
+ hasSchemaDir: !!output.schemas,
+ isAllowSyntheticDefaultImports,
+ });
+
+ const schemasPath = !output.schemas
+ ? join(dirname, filename + '.schemas' + extension)
+ : undefined;
+
+ if (schemasPath) {
+ const schemasData = header + generateModelsInline(builder.schemas);
+
+ await outputFile(schemasPath, schemasData);
+ }
+
+ if (mutators) {
+ implementationData += generateMutatorImports({
+ mutators,
+ implementation,
+ oneMore: true,
+ });
+ }
+
+ if (formData) {
+ implementationData += generateMutatorImports({
+ mutators: formData,
+ oneMore: true,
+ });
+ }
+ if (formUrlEncoded) {
+ implementationData += generateMutatorImports({
+ mutators: formUrlEncoded,
+ oneMore: true,
+ });
+ }
+
+ implementationData += `\n${implementation}`;
+ mswData += `\n${implementationMSW}`;
+
+ const implementationFilename =
+ tag +
+ (OutputClient.ANGULAR === output.client ? '.service' : '') +
+ extension;
+
+ const implementationPath = join(dirname, tag, implementationFilename);
+ await outputFile(implementationPath, implementationData);
+
+ const mockPath = output.mock
+ ? join(dirname, tag, tag + '.msw' + extension)
+ : undefined;
+
+ if (mockPath) {
+ await outputFile(mockPath, mswData);
+ }
+
+ return [
+ implementationPath,
+ ...(schemasPath ? [schemasPath] : []),
+ ...(mockPath ? [mockPath] : []),
+ ];
+ } catch (e) {
+ throw `Oups... 🍻. An Error occurred while splitting tag ${tag} => ${e}`;
+ }
+ }),
+ );
+
+ return generatedFilePathsArray.flatMap((it) => it);
+};
diff --git a/packages/core/src/writers/tags-mode.ts b/packages/core/src/writers/tags-mode.ts
new file mode 100644
index 000000000..1104d9345
--- /dev/null
+++ b/packages/core/src/writers/tags-mode.ts
@@ -0,0 +1,119 @@
+import { outputFile } from 'fs-extra';
+import { join } from 'upath';
+import { generateModelsInline, generateMutatorImports } from '../generators';
+import { WriteModeProps } from '../types';
+import {
+ camel,
+ getFileInfo,
+ isSyntheticDefaultImportsAllow,
+ kebab,
+ relativeSafe,
+} from '../utils';
+import { generateTargetForTags } from './target-tags';
+
+export const writeTagsMode = async ({
+ builder,
+ output,
+ specsName,
+ header,
+}: WriteModeProps): Promise => {
+ const { filename, dirname, extension } = getFileInfo(output.target, {
+ backupFilename: camel(builder.info.title),
+ });
+
+ const target = generateTargetForTags(builder, output);
+
+ const isAllowSyntheticDefaultImports = isSyntheticDefaultImportsAllow(
+ output.tsconfig,
+ );
+
+ const generatedFilePathsArray = await Promise.all(
+ Object.entries(target).map(async ([tag, target]) => {
+ try {
+ const {
+ imports,
+ implementation,
+ implementationMSW,
+ importsMSW,
+ mutators,
+ formData,
+ formUrlEncoded,
+ } = target;
+
+ let data = header;
+
+ const schemasPathRelative = output.schemas
+ ? relativeSafe(dirname, getFileInfo(output.schemas).dirname)
+ : './' + filename + '.schemas';
+
+ data += builder.imports({
+ client: output.client,
+ implementation,
+ imports: [
+ {
+ exports: imports.filter(
+ (imp) => !importsMSW.some((impMSW) => imp.name === impMSW.name),
+ ),
+ dependency: schemasPathRelative,
+ },
+ ],
+ specsName,
+ hasSchemaDir: !!output.schemas,
+ isAllowSyntheticDefaultImports,
+ hasGlobalMutator: !!output.override.mutator,
+ packageJson: output.packageJson,
+ });
+
+ if (output.mock) {
+ data += builder.importsMock({
+ implementation: implementationMSW,
+ imports: [{ exports: importsMSW, dependency: schemasPathRelative }],
+ specsName,
+ hasSchemaDir: !!output.schemas,
+ isAllowSyntheticDefaultImports,
+ });
+ }
+
+ const schemasPath = !output.schemas
+ ? join(dirname, filename + '.schemas' + extension)
+ : undefined;
+
+ if (schemasPath) {
+ const schemasData = header + generateModelsInline(builder.schemas);
+
+ await outputFile(schemasPath, schemasData);
+ }
+
+ if (mutators) {
+ data += generateMutatorImports({ mutators, implementation });
+ }
+
+ if (formData) {
+ data += generateMutatorImports({ mutators: formData });
+ }
+
+ if (formUrlEncoded) {
+ data += generateMutatorImports({ mutators: formUrlEncoded });
+ }
+
+ data += '\n\n';
+ data += implementation;
+
+ if (output.mock) {
+ data += '\n\n';
+
+ data += implementationMSW;
+ }
+
+ const implementationPath = join(dirname, `${kebab(tag)}${extension}`);
+ await outputFile(implementationPath, data);
+
+ return [implementationPath, ...(schemasPath ? [schemasPath] : [])];
+ } catch (e) {
+ throw `Oups... 🍻. An Error occurred while writing tag ${tag} => ${e}`;
+ }
+ }),
+ );
+
+ return generatedFilePathsArray.flatMap((it) => it);
+};
diff --git a/packages/core/src/writers/target-tags.ts b/packages/core/src/writers/target-tags.ts
new file mode 100644
index 000000000..ca281b79e
--- /dev/null
+++ b/packages/core/src/writers/target-tags.ts
@@ -0,0 +1,162 @@
+import {
+ GeneratorOperation,
+ GeneratorTarget,
+ GeneratorTargetFull,
+ NormalizedOutputOptions,
+ OutputClient,
+ WriteSpecsBuilder,
+} from '../types';
+import { compareVersions, kebab, pascal } from '../utils';
+
+const addDefaultTagIfEmpty = (operation: GeneratorOperation) => ({
+ ...operation,
+ tags: operation.tags.length ? operation.tags : ['default'],
+});
+
+const generateTargetTags = (
+ currentAcc: { [key: string]: GeneratorTargetFull },
+ operation: GeneratorOperation,
+): {
+ [key: string]: GeneratorTargetFull;
+} => {
+ return operation.tags.map(kebab).reduce((acc, tag) => {
+ const currentOperation = acc[tag];
+
+ acc[tag] = !currentOperation
+ ? {
+ imports: operation.imports,
+ importsMSW: operation.importsMSW,
+ mutators: operation.mutator ? [operation.mutator] : [],
+ formData: operation.formData ? [operation.formData] : [],
+ formUrlEncoded: operation.formUrlEncoded
+ ? [operation.formUrlEncoded]
+ : [],
+ implementation: operation.implementation,
+ implementationMSW: {
+ function: operation.implementationMSW.function,
+ handler: operation.implementationMSW.handler,
+ },
+ }
+ : {
+ implementation:
+ currentOperation.implementation + operation.implementation,
+ imports: [...currentOperation.imports, ...operation.imports],
+ importsMSW: [...currentOperation.importsMSW, ...operation.importsMSW],
+ implementationMSW: {
+ function:
+ currentOperation.implementationMSW.function +
+ operation.implementationMSW.function,
+ handler:
+ currentOperation.implementationMSW.handler +
+ operation.implementationMSW.handler,
+ },
+ mutators: operation.mutator
+ ? [...(currentOperation.mutators ?? []), operation.mutator]
+ : currentOperation.mutators,
+ formData: operation.formData
+ ? [...(currentOperation.formData ?? []), operation.formData]
+ : currentOperation.formData,
+ formUrlEncoded: operation.formUrlEncoded
+ ? [
+ ...(currentOperation.formUrlEncoded ?? []),
+ operation.formUrlEncoded,
+ ]
+ : currentOperation.formUrlEncoded,
+ };
+
+ return acc;
+ }, currentAcc);
+};
+
+export const generateTargetForTags = (
+ builder: WriteSpecsBuilder,
+ options: NormalizedOutputOptions,
+) => {
+ const isAngularClient = options.client === OutputClient.ANGULAR;
+
+ const allTargetTags = Object.values(builder.operations)
+ .map(addDefaultTagIfEmpty)
+ .reduce((acc, operation, index, arr) => {
+ const targetTags = generateTargetTags(acc, operation);
+
+ if (index === arr.length - 1) {
+ return Object.entries(targetTags).reduce<
+ Record
+ >((acc, [tag, target]) => {
+ const isMutator = !!target.mutators?.some((mutator) =>
+ isAngularClient ? mutator.hasThirdArg : mutator.hasSecondArg,
+ );
+ const operationNames = Object.values(builder.operations)
+ .filter(({ tags }) => tags.includes(tag))
+ .map(({ operationName }) => operationName);
+
+ const typescriptVersion =
+ options.packageJson?.dependencies?.['typescript'] ??
+ options.packageJson?.devDependencies?.['typescript'] ??
+ '4.4.0';
+
+ const hasAwaitedType = compareVersions(typescriptVersion, '4.5.0');
+
+ const titles = builder.title({
+ outputClient: options.client,
+ title: pascal(tag),
+ customTitleFunc: options.override.title,
+ });
+
+ const footer = builder.footer({
+ outputClient: options?.client,
+ operationNames,
+ hasMutator: !!target.mutators?.length,
+ hasAwaitedType,
+ titles,
+ });
+
+ const header = builder.header({
+ outputClient: options.client,
+ isRequestOptions: options.override.requestOptions !== false,
+ isMutator,
+ isGlobalMutator: !!options.override.mutator,
+ provideIn: options.override.angular.provideIn,
+ hasAwaitedType,
+ titles,
+ });
+
+ acc[tag] = {
+ implementation:
+ header.implementation +
+ target.implementation +
+ footer.implementation,
+ implementationMSW: {
+ function: target.implementationMSW.function,
+ handler:
+ header.implementationMSW +
+ target.implementationMSW.handler +
+ footer.implementationMSW,
+ },
+ imports: target.imports,
+ importsMSW: target.importsMSW,
+ mutators: target.mutators,
+ formData: target.formData,
+ formUrlEncoded: target.formUrlEncoded,
+ };
+
+ return acc;
+ }, {});
+ }
+
+ return targetTags;
+ }, {} as { [key: string]: GeneratorTargetFull });
+
+ return Object.entries(allTargetTags).reduce>(
+ (acc, [tag, target]) => {
+ acc[tag] = {
+ ...target,
+ implementationMSW:
+ target.implementationMSW.function + target.implementationMSW.handler,
+ };
+
+ return acc;
+ },
+ {},
+ );
+};
diff --git a/packages/core/src/writers/target.ts b/packages/core/src/writers/target.ts
new file mode 100644
index 000000000..293a25852
--- /dev/null
+++ b/packages/core/src/writers/target.ts
@@ -0,0 +1,99 @@
+import {
+ GeneratorTarget,
+ GeneratorTargetFull,
+ NormalizedOutputOptions,
+ OutputClient,
+ WriteSpecsBuilder,
+} from '../types';
+import { compareVersions, pascal } from '../utils';
+
+export const generateTarget = (
+ builder: WriteSpecsBuilder,
+ options: NormalizedOutputOptions,
+): GeneratorTarget => {
+ const operationNames = Object.values(builder.operations).map(
+ ({ operationName }) => operationName,
+ );
+ const isAngularClient = options?.client === OutputClient.ANGULAR;
+
+ const titles = builder.title({
+ outputClient: options.client,
+ title: pascal(builder.info.title),
+ customTitleFunc: options.override.title,
+ });
+
+ const target = Object.values(builder.operations).reduce(
+ (acc, operation, index, arr) => {
+ acc.imports.push(...operation.imports);
+ acc.importsMSW.push(...operation.importsMSW);
+ acc.implementation += operation.implementation + '\n';
+ acc.implementationMSW.function += operation.implementationMSW.function;
+ acc.implementationMSW.handler += operation.implementationMSW.handler;
+ if (operation.mutator) {
+ acc.mutators.push(operation.mutator);
+ }
+
+ if (operation.formData) {
+ acc.formData.push(operation.formData);
+ }
+ if (operation.formUrlEncoded) {
+ acc.formUrlEncoded.push(operation.formUrlEncoded);
+ }
+
+ if (index === arr.length - 1) {
+ const isMutator = acc.mutators.some((mutator) =>
+ isAngularClient ? mutator.hasThirdArg : mutator.hasSecondArg,
+ );
+
+ const typescriptVersion =
+ options.packageJson?.dependencies?.['typescript'] ??
+ options.packageJson?.devDependencies?.['typescript'] ??
+ '4.4.0';
+
+ const hasAwaitedType = compareVersions(typescriptVersion, '4.5.0');
+
+ const header = builder.header({
+ outputClient: options.client,
+ isRequestOptions: options.override.requestOptions !== false,
+ isMutator,
+ isGlobalMutator: !!options.override.mutator,
+ provideIn: options.override.angular.provideIn,
+ hasAwaitedType,
+ titles,
+ });
+ acc.implementation = header.implementation + acc.implementation;
+ acc.implementationMSW.handler =
+ header.implementationMSW + acc.implementationMSW.handler;
+
+ const footer = builder.footer({
+ outputClient: options?.client,
+ operationNames,
+ hasMutator: !!acc.mutators.length,
+ hasAwaitedType,
+ titles,
+ });
+ acc.implementation += footer.implementation;
+ acc.implementationMSW.handler += footer.implementationMSW;
+ }
+ return acc;
+ },
+ {
+ imports: [],
+ implementation: '',
+ implementationMSW: {
+ function: '',
+ handler: '',
+ },
+ importsMSW: [],
+ mutators: [],
+ formData: [],
+ formUrlEncoded: [],
+ } as Required,
+ );
+
+ return {
+ ...target,
+ implementationMSW:
+ target.implementationMSW.function + target.implementationMSW.handler,
+ };
+};
diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json
new file mode 100644
index 000000000..b299c4337
--- /dev/null
+++ b/packages/core/tsconfig.json
@@ -0,0 +1,4 @@
+{
+ "extends": "../../tsconfig.base.json",
+ "include": ["src/**/*.ts", "../../types/**.d.ts"]
+}
diff --git a/packages/msw/README.md b/packages/msw/README.md
new file mode 100644
index 000000000..8e1596ee8
--- /dev/null
+++ b/packages/msw/README.md
@@ -0,0 +1,28 @@
+[![npm version](https://badge.fury.io/js/orval.svg)](https://badge.fury.io/js/orval)
+[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
+[![tests](https://github.com/anymaniax/orval/actions/workflows/tests.yaml/badge.svg)](https://github.com/anymaniax/orval/actions/workflows/tests.yaml)
+
+
+
+
+
+ Visit orval.dev for docs, guides, API and beer!
+
+
+### Code Generation
+
+`orval` is able to generate client with appropriate type-signatures (TypeScript) from any valid OpenAPI v3 or Swagger v2 specification, either in `yaml` or `json` formats.
+
+`Generate`, `valid`, `cache` and `mock` in your React, Vue, Svelte and Angular applications all with your OpenAPI specification.
+
+### Samples
+
+You can find below some samples
+
+- [react app](https://github.com/anymaniax/orval/tree/master/samples/react-app)
+- [react query](https://github.com/anymaniax/orval/tree/master/samples/react-query)
+- [svelte query](https://github.com/anymaniax/orval/tree/master/samples/svelte-query)
+- [vue query](https://github.com/anymaniax/orval/tree/master/samples/vue-query)
+- [react app with swr](https://github.com/anymaniax/orval/tree/master/samples/react-app-with-swr)
+- [nx fastify react](https://github.com/anymaniax/orval/tree/master/samples/nx-fastify-react)
+- [angular app](https://github.com/anymaniax/orval/tree/master/samples/angular-app)
diff --git a/packages/msw/package.json b/packages/msw/package.json
new file mode 100644
index 000000000..bcc63e74b
--- /dev/null
+++ b/packages/msw/package.json
@@ -0,0 +1,22 @@
+{
+ "name": "@orval/msw",
+ "version": "6.11.0-alpha.1",
+ "license": "MIT",
+ "main": "./dist/index.js",
+ "types": "./dist/index.d.ts",
+ "files": [
+ "dist"
+ ],
+ "scripts": {
+ "build": "tsup ./src/index.ts --target node12 --minify --clean --dts --splitting",
+ "dev": "tsup ./src/index.ts --target node12 --clean --watch src",
+ "lint": "eslint src/**/*.ts"
+ },
+ "dependencies": {
+ "@orval/core": "6.11.0-alpha.1",
+ "cuid": "^2.1.8",
+ "openapi3-ts": "^3.0.0",
+ "lodash.get": "^4.4.2",
+ "lodash.omit": "^4.5.0"
+ }
+}
diff --git a/packages/msw/src/constants.ts b/packages/msw/src/constants.ts
new file mode 100644
index 000000000..f45021869
--- /dev/null
+++ b/packages/msw/src/constants.ts
@@ -0,0 +1,28 @@
+import { SchemaObject } from 'openapi3-ts';
+
+export const DEFAULT_FORMAT_MOCK: Record<
+ Required['format'],
+ string
+> = {
+ bic: 'faker.finance.bic()',
+ city: 'faker.address.city()',
+ country: 'faker.address.country()',
+ date: "faker.date.past().toISOString().split('T')[0]",
+ 'date-time': "`${faker.date.past().toISOString().split('.')[0]}Z`",
+ email: 'faker.internet.email()',
+ firstName: 'faker.name.firstName()',
+ gender: 'faker.name.gender()',
+ iban: 'faker.finance.iban()',
+ ipv4: 'faker.internet.ipv4()',
+ ipv6: 'faker.internet.ipv6()',
+ jobTitle: 'faker.name.jobTitle()',
+ lastName: 'faker.name.lastName()',
+ password: 'faker.internet.password()',
+ phoneNumber: 'faker.phone.phoneNumber()',
+ streetName: 'faker.address.streetName()',
+ uri: 'faker.internet.url()',
+ url: 'faker.internet.url()',
+ userName: 'faker.internet.userName()',
+ uuid: 'faker.datatype.uuid()',
+ zipCode: 'faker.address.zipCode()',
+};
diff --git a/packages/msw/src/getters/combine.ts b/packages/msw/src/getters/combine.ts
new file mode 100644
index 000000000..e44805d1d
--- /dev/null
+++ b/packages/msw/src/getters/combine.ts
@@ -0,0 +1,138 @@
+import {
+ ContextSpecs,
+ GeneratorImport,
+ isReference,
+ MockOptions,
+} from '@orval/core';
+import omit from 'lodash.omit';
+import { resolveMockValue } from '../resolvers';
+import { MockSchemaObject } from '../types';
+
+export const combineSchemasMock = ({
+ item,
+ separator,
+ mockOptions,
+ operationId,
+ tags,
+ combine,
+ context,
+ imports,
+}: {
+ item: MockSchemaObject;
+ separator: 'allOf' | 'oneOf' | 'anyOf';
+ operationId: string;
+ mockOptions?: MockOptions;
+ tags: string[];
+ combine?: {
+ separator: 'allOf' | 'oneOf' | 'anyOf';
+ includedProperties: string[];
+ };
+ context: ContextSpecs;
+ imports: GeneratorImport[];
+}) => {
+ let combineImports: GeneratorImport[] = [];
+ let includedProperties: string[] = (combine?.includedProperties ?? []).slice(
+ 0,
+ );
+ const itemResolvedValue =
+ isReference(item) || item.properties
+ ? resolveMockValue({
+ schema: omit(item, separator) as MockSchemaObject,
+ combine: {
+ separator: 'allOf',
+ includedProperties: [],
+ },
+ mockOptions,
+ operationId,
+ tags,
+ context,
+ imports,
+ })
+ : undefined;
+
+ includedProperties.push(...(itemResolvedValue?.includedProperties ?? []));
+ combineImports.push(...(itemResolvedValue?.imports ?? []));
+
+ const value = (item[separator] ?? []).reduce((acc, val, index, arr) => {
+ const resolvedValue = resolveMockValue({
+ schema: {
+ ...val,
+ name: item.name,
+ path: item.path ? item.path : '#',
+ },
+ combine: {
+ separator,
+ includedProperties:
+ separator !== 'oneOf'
+ ? includedProperties
+ : itemResolvedValue?.includedProperties ?? [],
+ },
+ mockOptions,
+ operationId,
+ tags,
+ context,
+ imports,
+ });
+
+ combineImports.push(...resolvedValue.imports);
+ includedProperties.push(...(resolvedValue.includedProperties ?? []));
+
+ const isLastElement = index === arr.length - 1;
+
+ let currentValue = resolvedValue.value;
+
+ if (itemResolvedValue?.value && separator === 'oneOf') {
+ currentValue = `${resolvedValue.value.slice(0, -1)},${
+ itemResolvedValue.value
+ }}`;
+ }
+
+ if (itemResolvedValue?.value && separator !== 'oneOf' && isLastElement) {
+ currentValue = `${currentValue}${
+ itemResolvedValue?.value ? `,${itemResolvedValue.value}` : ''
+ }`;
+ }
+
+ const isObjectBounds =
+ !combine || (combine.separator === 'oneOf' && separator === 'allOf');
+
+ if (!index && isObjectBounds) {
+ if (resolvedValue.enums || separator === 'oneOf') {
+ if (arr.length === 1) {
+ return `faker.helpers.arrayElement([${currentValue}])`;
+ }
+ return `faker.helpers.arrayElement([${currentValue},`;
+ }
+
+ if (arr.length === 1) {
+ if (resolvedValue.type !== 'object') {
+ return currentValue;
+ }
+ return `{${currentValue}}`;
+ }
+
+ return `{${currentValue},`;
+ }
+
+ if (isLastElement) {
+ if (resolvedValue.enums || separator === 'oneOf') {
+ return `${acc}${currentValue}${!combine ? '])' : ''}`;
+ }
+
+ return `${acc}${currentValue}${isObjectBounds ? '}' : ''}`;
+ }
+
+ if (!currentValue) {
+ return acc;
+ }
+
+ return `${acc}${currentValue},`;
+ }, '');
+
+ return {
+ value,
+ imports: combineImports,
+ name: item.name,
+ includedProperties,
+ };
+};
diff --git a/packages/msw/src/getters/index.ts b/packages/msw/src/getters/index.ts
new file mode 100644
index 000000000..b766499b4
--- /dev/null
+++ b/packages/msw/src/getters/index.ts
@@ -0,0 +1,4 @@
+export * from './combine';
+export * from './object';
+export * from './route';
+export * from './scalar';
diff --git a/packages/msw/src/getters/object.ts b/packages/msw/src/getters/object.ts
new file mode 100644
index 000000000..b6fb2ce72
--- /dev/null
+++ b/packages/msw/src/getters/object.ts
@@ -0,0 +1,144 @@
+import {
+ ContextSpecs,
+ count,
+ GeneratorImport,
+ getKey,
+ isBoolean,
+ isReference,
+ MockOptions,
+} from '@orval/core';
+import cuid from 'cuid';
+import { ReferenceObject, SchemaObject } from 'openapi3-ts';
+import { resolveMockValue } from '../resolvers/value';
+import { MockDefinition, MockSchemaObject } from '../types';
+import { combineSchemasMock } from './combine';
+
+export const getMockObject = ({
+ item,
+ mockOptions,
+ operationId,
+ tags,
+ combine,
+ context,
+ imports,
+}: {
+ item: MockSchemaObject;
+ operationId: string;
+ mockOptions?: MockOptions;
+ tags: string[];
+ combine?: {
+ separator: 'allOf' | 'oneOf' | 'anyOf';
+ includedProperties: string[];
+ };
+ context: ContextSpecs;
+ imports: GeneratorImport[];
+}): MockDefinition => {
+ if (isReference(item)) {
+ return resolveMockValue({
+ schema: {
+ ...item,
+ name: item.name,
+ path: item.path ? `${item.path}.${item.name}` : item.name,
+ },
+ mockOptions,
+ operationId,
+ tags,
+ context,
+ imports,
+ });
+ }
+
+ if (item.allOf || item.oneOf || item.anyOf) {
+ const separator = item.allOf ? 'allOf' : item.oneOf ? 'oneOf' : 'anyOf';
+ return combineSchemasMock({
+ item,
+ separator,
+ mockOptions,
+ operationId,
+ tags,
+ combine,
+ context,
+ imports,
+ });
+ }
+
+ if (item.properties) {
+ let value = !combine || combine?.separator === 'oneOf' ? '{' : '';
+ let imports: GeneratorImport[] = [];
+ let includedProperties: string[] = [];
+ value += Object.entries(item.properties)
+ .map(([key, prop]: [string, ReferenceObject | SchemaObject]) => {
+ if (combine?.includedProperties.includes(key)) {
+ return undefined;
+ }
+
+ const isRequired =
+ mockOptions?.required ||
+ (Array.isArray(item.required) ? item.required : []).includes(key);
+
+ if (count(item.path, `\\.${key}\\.`) >= 1) {
+ return undefined;
+ }
+
+ const resolvedValue = resolveMockValue({
+ schema: {
+ ...prop,
+ name: key,
+ path: item.path ? `${item.path}.${key}` : `#.${key}`,
+ },
+ mockOptions,
+ operationId,
+ tags,
+ context,
+ imports,
+ });
+
+ imports.push(...resolvedValue.imports);
+ includedProperties.push(key);
+
+ const keyDefinition = getKey(key);
+ if (!isRequired && !resolvedValue.overrided) {
+ return `${keyDefinition}: faker.helpers.arrayElement([${resolvedValue.value}, undefined])`;
+ }
+
+ return `${keyDefinition}: ${resolvedValue.value}`;
+ })
+ .filter(Boolean)
+ .join(', ');
+ value += !combine || combine?.separator === 'oneOf' ? '}' : '';
+ return {
+ value,
+ imports,
+ name: item.name,
+ includedProperties,
+ };
+ }
+
+ if (item.additionalProperties) {
+ if (isBoolean(item.additionalProperties)) {
+ return { value: `{}`, imports: [], name: item.name };
+ }
+
+ const resolvedValue = resolveMockValue({
+ schema: {
+ ...item.additionalProperties,
+ name: item.name,
+ path: item.path ? `${item.path}.#` : '#',
+ },
+ mockOptions,
+ operationId,
+ tags,
+ context,
+ imports,
+ });
+
+ return {
+ ...resolvedValue,
+ value: `{
+ '${cuid()}': ${resolvedValue.value}
+ }`,
+ };
+ }
+
+ return { value: '{}', imports: [], name: item.name };
+};
diff --git a/packages/msw/src/getters/route.test.ts b/packages/msw/src/getters/route.test.ts
new file mode 100644
index 000000000..1087d37ec
--- /dev/null
+++ b/packages/msw/src/getters/route.test.ts
@@ -0,0 +1,21 @@
+import { getRouteMSW } from './route';
+
+describe('getRoute getter', () => {
+ [
+ ['/', '*/'],
+ ['/api/test/{id}', '*/api/test/:id'],
+ ['/api/test/{path*}', '*/api/test/:path'],
+ ['/api/test/{user_id}', '*/api/test/:userId'],
+ ['/api/test/{locale}.js', '*/api/test/:locale.js'],
+ ['/api/test/i18n-{locale}.js', '*/api/test/i18n-:locale.js'],
+ ['/api/test/{param1}-{param2}.js', '*/api/test/:param1-:param2.js'],
+ [
+ '/api/test/user{param1}-{param2}.html',
+ '*/api/test/user:param1-:param2.html',
+ ],
+ ].forEach(([input, expected]) => {
+ it(`should process ${input} to ${expected}`, () => {
+ expect(getRouteMSW(input)).toBe(expected);
+ });
+ });
+});
diff --git a/packages/msw/src/getters/route.ts b/packages/msw/src/getters/route.ts
new file mode 100644
index 000000000..39d068fc3
--- /dev/null
+++ b/packages/msw/src/getters/route.ts
@@ -0,0 +1,39 @@
+import { camel, sanitize } from '@orval/core';
+
+const hasParam = (path: string): boolean => /[^{]*{[\w*_-]*}.*/.test(path);
+
+const getRoutePath = (path: string): string => {
+ const matches = path.match(/([^{]*){?([\w*_-]*)}?(.*)/);
+ if (!matches?.length) return path; // impossible due to regexp grouping here, but for TS
+
+ const prev = matches[1];
+ const param = sanitize(camel(matches[2]), {
+ es5keyword: true,
+ underscore: true,
+ dash: true,
+ dot: true,
+ });
+ const next = hasParam(matches[3]) ? getRoutePath(matches[3]) : matches[3];
+
+ if (hasParam(path)) {
+ return `${prev}:${param}${next}`;
+ } else {
+ return `${prev}${param}${next}`;
+ }
+};
+
+export const getRouteMSW = (route: string, baseUrl = '*') => {
+ const splittedRoute = route.split('/');
+
+ return splittedRoute.reduce((acc, path, i) => {
+ if (!path && !i) {
+ return acc;
+ }
+
+ if (!path.includes('{')) {
+ return `${acc}/${path}`;
+ }
+
+ return `${acc}/${getRoutePath(path)}`;
+ }, baseUrl);
+};
diff --git a/packages/msw/src/getters/scalar.ts b/packages/msw/src/getters/scalar.ts
new file mode 100644
index 000000000..38e8cdc28
--- /dev/null
+++ b/packages/msw/src/getters/scalar.ts
@@ -0,0 +1,215 @@
+import {
+ ContextSpecs,
+ escape,
+ GeneratorImport,
+ isReference,
+ isRootKey,
+ mergeDeep,
+ MockOptions,
+} from '@orval/core';
+import { DEFAULT_FORMAT_MOCK } from '../constants';
+import {
+ getNullable,
+ resolveMockOverride,
+ resolveMockValue,
+} from '../resolvers';
+import { MockDefinition, MockSchemaObject } from '../types';
+import { getMockObject } from './object';
+
+export const getMockScalar = ({
+ item,
+ imports,
+ mockOptions,
+ operationId,
+ tags,
+ combine,
+ context,
+}: {
+ item: MockSchemaObject;
+ imports: GeneratorImport[];
+ mockOptions?: MockOptions;
+ operationId: string;
+ isRef?: boolean;
+ tags: string[];
+ combine?: {
+ separator: 'allOf' | 'oneOf' | 'anyOf';
+ includedProperties: string[];
+ };
+ context: ContextSpecs;
+}): MockDefinition => {
+ const operationProperty = resolveMockOverride(
+ mockOptions?.operations?.[operationId]?.properties,
+ item,
+ );
+
+ if (operationProperty) {
+ return operationProperty;
+ }
+
+ const overrideTag = Object.entries(mockOptions?.tags ?? {}).reduce<{
+ properties: Record;
+ }>(
+ (acc, [tag, options]) =>
+ tags.includes(tag) ? mergeDeep(acc, options) : acc,
+ {} as { properties: Record },
+ );
+
+ const tagProperty = resolveMockOverride(overrideTag?.properties, item);
+
+ if (tagProperty) {
+ return tagProperty;
+ }
+
+ const property = resolveMockOverride(mockOptions?.properties, item);
+
+ if (property) {
+ return property;
+ }
+
+ const ALL_FORMAT: Record = {
+ ...DEFAULT_FORMAT_MOCK,
+ ...(mockOptions?.format ?? {}),
+ };
+
+ if (item.format && ALL_FORMAT[item.format]) {
+ return {
+ value: getNullable(ALL_FORMAT[item.format], item.nullable),
+ imports: [],
+ name: item.name,
+ overrided: false,
+ };
+ }
+
+ switch (item.type) {
+ case 'number':
+ case 'integer': {
+ return {
+ value: getNullable(
+ `faker.datatype.number({min: ${item.minimum}, max: ${item.maximum}})`,
+ item.nullable,
+ ),
+ imports: [],
+ name: item.name,
+ };
+ }
+
+ case 'boolean': {
+ return {
+ value: 'faker.datatype.boolean()',
+ imports: [],
+ name: item.name,
+ };
+ }
+
+ case 'array': {
+ if (!item.items) {
+ return { value: '[]', imports: [], name: item.name };
+ }
+
+ const {
+ value,
+ enums,
+ imports: resolvedImports,
+ name,
+ } = resolveMockValue({
+ schema: {
+ ...item.items,
+ name: item.name,
+ path: item.path ? `${item.path}.[]` : '#.[]',
+ },
+ combine,
+ mockOptions,
+ operationId,
+ tags,
+ context,
+ imports,
+ });
+
+ if (enums) {
+ if (!isReference(item.items)) {
+ return {
+ value,
+ imports: resolvedImports,
+ name: item.name,
+ };
+ }
+
+ const enumImp = imports.find(
+ (imp) => name.replace('[]', '') === imp.name,
+ );
+ const enumValue = enumImp?.name || name;
+ return {
+ value: `faker.helpers.arrayElements(Object.values(${enumValue}))`,
+ imports: enumImp
+ ? [
+ ...resolvedImports,
+ {
+ ...enumImp,
+ values: true,
+ ...(!isRootKey(context.specKey, context.target)
+ ? { specKey: context.specKey }
+ : {}),
+ },
+ ]
+ : resolvedImports,
+ name: item.name,
+ };
+ }
+
+ return {
+ value:
+ `Array.from({ length: faker.datatype.number({ ` +
+ `min: ${mockOptions?.arrayMin}, ` +
+ `max: ${mockOptions?.arrayMax} }) ` +
+ `}, (_, i) => i + 1).map(() => (${value}))`,
+ imports: resolvedImports,
+ name: item.name,
+ };
+ }
+
+ case 'string': {
+ let value = 'faker.random.word()';
+ let imports: GeneratorImport[] = [];
+
+ if (item.enum) {
+ let enumValue =
+ "['" + item.enum.map((e) => escape(e)).join("','") + "']";
+
+ if (item.isRef) {
+ enumValue = `Object.values(${item.name})`;
+ imports = [
+ {
+ name: item.name,
+ values: true,
+ ...(!isRootKey(context.specKey, context.target)
+ ? { specKey: context.specKey }
+ : {}),
+ },
+ ];
+ }
+
+ value = `faker.helpers.arrayElement(${enumValue})`;
+ }
+
+ return {
+ value: getNullable(value, item.nullable),
+ enums: item.enum,
+ name: item.name,
+ imports,
+ };
+ }
+
+ case 'object':
+ default: {
+ return getMockObject({
+ item,
+ mockOptions,
+ operationId,
+ tags,
+ combine,
+ context,
+ imports,
+ });
+ }
+ }
+};
diff --git a/packages/msw/src/index.ts b/packages/msw/src/index.ts
new file mode 100644
index 000000000..81e54e72f
--- /dev/null
+++ b/packages/msw/src/index.ts
@@ -0,0 +1,87 @@
+import {
+ generateDependencyImports,
+ GenerateMockImports,
+ GeneratorDependency,
+ GeneratorOptions,
+ GeneratorVerbOptions,
+ pascal,
+} from '@orval/core';
+import { getRouteMSW } from './getters';
+import { getMockDefinition, getMockOptionsDataOverride } from './mocks';
+
+const MSW_DEPENDENCIES: GeneratorDependency[] = [
+ {
+ exports: [{ name: 'rest', values: true }],
+ dependency: 'msw',
+ },
+ {
+ exports: [{ name: 'faker', values: true }],
+ dependency: '@faker-js/faker',
+ },
+];
+
+export const generateMSWImports: GenerateMockImports = ({
+ implementation,
+ imports,
+ specsName,
+ hasSchemaDir,
+ isAllowSyntheticDefaultImports,
+}) => {
+ return generateDependencyImports(
+ implementation,
+ [...MSW_DEPENDENCIES, ...imports],
+ specsName,
+ hasSchemaDir,
+ isAllowSyntheticDefaultImports,
+ );
+};
+
+export const generateMSW = (
+ { operationId, response, verb, tags }: GeneratorVerbOptions,
+ { pathRoute, override, context }: GeneratorOptions,
+) => {
+ const { definitions, definition, imports } = getMockDefinition({
+ operationId,
+ tags,
+ response,
+ override,
+ context,
+ });
+
+ const route = getRouteMSW(pathRoute, override?.mock?.baseUrl);
+ const mockData = getMockOptionsDataOverride(operationId, override);
+
+ let value = '';
+
+ if (mockData) {
+ value = mockData;
+ } else if (definitions.length > 1) {
+ value = `faker.helpers.arrayElement(${definition})`;
+ } else if (definitions[0]) {
+ value = definitions[0];
+ }
+
+ const responseType = response.contentTypes.includes('text/plain')
+ ? 'text'
+ : 'json';
+
+ return {
+ implementation: {
+ function:
+ value && value !== 'undefined'
+ ? `export const get${pascal(operationId)}Mock = () => (${value})\n\n`
+ : '',
+ handler: `rest.${verb}('${route}', (_req, res, ctx) => {
+ return res(
+ ctx.delay(${override?.mock?.delay ?? 1000}),
+ ctx.status(200, 'Mocked status'),${
+ value && value !== 'undefined'
+ ? `\nctx.${responseType}(get${pascal(operationId)}Mock()),`
+ : ''
+ }
+ )
+ }),`,
+ },
+ imports,
+ };
+};
diff --git a/packages/msw/src/mocks.ts b/packages/msw/src/mocks.ts
new file mode 100644
index 000000000..9a519b4eb
--- /dev/null
+++ b/packages/msw/src/mocks.ts
@@ -0,0 +1,230 @@
+import {
+ ContextSpecs,
+ generalJSTypesWithArray,
+ GeneratorImport,
+ GetterResponse,
+ isFunction,
+ MockOptions,
+ NormalizedOverrideOutput,
+ resolveRef,
+ stringify,
+} from '@orval/core';
+import { OpenAPIObject, SchemaObject } from 'openapi3-ts';
+import { getMockScalar } from './getters';
+
+const getMockPropertiesWithoutFunc = (properties: any, spec: OpenAPIObject) =>
+ Object.entries(isFunction(properties) ? properties(spec) : properties).reduce<
+ Record
+ >((acc, [key, value]) => {
+ const implementation = isFunction(value)
+ ? `(${value})()`
+ : stringify(value as string)!;
+
+ acc[key] = implementation?.replace(
+ /import_faker.defaults|import_faker.faker/g,
+ 'faker',
+ );
+ return acc;
+ }, {});
+
+const getMockWithoutFunc = (
+ spec: OpenAPIObject,
+ override?: NormalizedOverrideOutput,
+): MockOptions => ({
+ arrayMin: override?.mock?.arrayMin,
+ arrayMax: override?.mock?.arrayMax,
+ required: override?.mock?.required,
+ ...(override?.mock?.properties
+ ? {
+ properties: getMockPropertiesWithoutFunc(
+ override.mock.properties,
+ spec,
+ ),
+ }
+ : {}),
+ ...(override?.mock?.format
+ ? {
+ format: getMockPropertiesWithoutFunc(override.mock.format, spec),
+ }
+ : {}),
+ ...(override?.operations
+ ? {
+ operations: Object.entries(override.operations).reduce<
+ Exclude
+ >((acc, [key, value]) => {
+ if (value.mock?.properties) {
+ acc[key] = {
+ properties: getMockPropertiesWithoutFunc(
+ value.mock.properties,
+ spec,
+ ),
+ };
+ }
+
+ return acc;
+ }, {}),
+ }
+ : {}),
+ ...(override?.tags
+ ? {
+ tags: Object.entries(override.tags).reduce<
+ Exclude
+ >((acc, [key, value]) => {
+ if (value.mock?.properties) {
+ acc[key] = {
+ properties: getMockPropertiesWithoutFunc(
+ value.mock.properties,
+ spec,
+ ),
+ };
+ }
+
+ return acc;
+ }, {}),
+ }
+ : {}),
+});
+
+const getMockScalarJsTypes = (
+ definition: string,
+ mockOptionsWithoutFunc: { [key: string]: unknown },
+) => {
+ const isArray = definition.endsWith('[]');
+ const type = isArray ? definition.slice(0, -2) : definition;
+
+ switch (type) {
+ case 'number':
+ return isArray
+ ? `Array.from({length: faker.datatype.number({` +
+ `min: ${mockOptionsWithoutFunc.arrayMin}, ` +
+ `max: ${mockOptionsWithoutFunc.arrayMax}}` +
+ `)}, () => faker.datatype.number())`
+ : 'faker.datatype.number().toString()';
+ case 'string':
+ return isArray
+ ? `Array.from({length: faker.datatype.number({` +
+ `min: ${mockOptionsWithoutFunc?.arrayMin},` +
+ `max: ${mockOptionsWithoutFunc?.arrayMax}}` +
+ `)}, () => faker.random.word())`
+ : 'faker.random.word()';
+ default:
+ return 'undefined';
+ }
+};
+
+export const getResponsesMockDefinition = ({
+ operationId,
+ tags,
+ response,
+ mockOptionsWithoutFunc,
+ transformer,
+ context,
+}: {
+ operationId: string;
+ tags: string[];
+ response: GetterResponse;
+ mockOptionsWithoutFunc: { [key: string]: unknown };
+ transformer?: (value: unknown, definition: string) => string;
+ context: ContextSpecs;
+}) => {
+ return response.types.success.reduce(
+ (acc, { value: definition, originalSchema, imports, isRef }) => {
+ if (!definition || generalJSTypesWithArray.includes(definition)) {
+ const value = getMockScalarJsTypes(definition, mockOptionsWithoutFunc);
+
+ acc.definitions.push(
+ transformer ? transformer(value, response.definition.success) : value,
+ );
+
+ return acc;
+ }
+
+ if (!originalSchema) {
+ return acc;
+ }
+
+ const resolvedRef = resolveRef(originalSchema, context);
+
+ const scalar = getMockScalar({
+ item: {
+ name: definition,
+ ...resolvedRef.schema,
+ },
+ imports,
+ mockOptions: mockOptionsWithoutFunc,
+ operationId,
+ tags,
+ context: isRef
+ ? {
+ ...context,
+ specKey: response.imports[0]?.specKey ?? context.specKey,
+ }
+ : context,
+ });
+
+ acc.imports.push(...scalar.imports);
+ acc.definitions.push(
+ transformer
+ ? transformer(scalar.value, response.definition.success)
+ : scalar.value.toString(),
+ );
+
+ return acc;
+ },
+ {
+ definitions: [] as string[],
+ imports: [] as GeneratorImport[],
+ },
+ );
+};
+
+export const getMockDefinition = ({
+ operationId,
+ tags,
+ response,
+ override,
+ transformer,
+ context,
+}: {
+ operationId: string;
+ tags: string[];
+ response: GetterResponse;
+ override: NormalizedOverrideOutput;
+ transformer?: (value: unknown, definition: string) => string;
+ context: ContextSpecs;
+}) => {
+ const mockOptionsWithoutFunc = getMockWithoutFunc(
+ context.specs[context.specKey],
+ override,
+ );
+
+ const { definitions, imports } = getResponsesMockDefinition({
+ operationId,
+ tags,
+ response,
+ mockOptionsWithoutFunc,
+ transformer,
+ context,
+ });
+
+ return {
+ definition: '[' + definitions.join(', ') + ']',
+ definitions,
+ imports,
+ };
+};
+
+export const getMockOptionsDataOverride = (
+ operationId: string,
+ override: NormalizedOverrideOutput,
+) => {
+ const responseOverride = override?.operations?.[operationId]?.mock?.data;
+ const implementation = isFunction(responseOverride)
+ ? `(${responseOverride})()`
+ : stringify(responseOverride);
+
+ return implementation?.replace(
+ /import_faker.defaults|import_faker.faker/g,
+ 'faker',
+ );
+};
diff --git a/packages/msw/src/resolvers/index.ts b/packages/msw/src/resolvers/index.ts
new file mode 100644
index 000000000..937b73fd1
--- /dev/null
+++ b/packages/msw/src/resolvers/index.ts
@@ -0,0 +1 @@
+export * from './value';
diff --git a/packages/msw/src/resolvers/value.ts b/packages/msw/src/resolvers/value.ts
new file mode 100644
index 000000000..21e0c022d
--- /dev/null
+++ b/packages/msw/src/resolvers/value.ts
@@ -0,0 +1,118 @@
+import {
+ ContextSpecs,
+ GeneratorImport,
+ getRefInfo,
+ isReference,
+ MockOptions,
+} from '@orval/core';
+import get from 'lodash.get';
+import { SchemaObject } from 'openapi3-ts';
+import { getMockScalar } from '../getters/scalar';
+import { MockDefinition, MockSchemaObject } from '../types';
+
+const isRegex = (key: string) => key[0] === '/' && key[key.length - 1] === '/';
+
+export const resolveMockOverride = (
+ properties: Record | undefined = {},
+ item: SchemaObject & { name: string; path?: string },
+) => {
+ const property = Object.entries(properties).find(([key]) => {
+ if (isRegex(key)) {
+ const regex = new RegExp(key.slice(1, key.length - 1));
+ if (regex.test(item.name)) {
+ return true;
+ }
+ }
+
+ if (`#.${key}` === (item.path ? item.path : `#.${item.name}`)) {
+ return true;
+ }
+
+ return false;
+ });
+
+ if (!property) {
+ return;
+ }
+
+ return {
+ value: getNullable(property[1] as string, item.nullable),
+ imports: [],
+ name: item.name,
+ overrided: true,
+ };
+};
+
+export const getNullable = (value: string, nullable?: boolean) =>
+ nullable ? `faker.helpers.arrayElement([${value}, null])` : value;
+
+export const resolveMockValue = ({
+ schema,
+ mockOptions,
+ operationId,
+ tags,
+ combine,
+ context,
+ imports,
+}: {
+ schema: MockSchemaObject;
+ operationId: string;
+ mockOptions?: MockOptions;
+ tags: string[];
+ combine?: {
+ separator: 'allOf' | 'oneOf' | 'anyOf';
+ includedProperties: string[];
+ };
+ context: ContextSpecs;
+ imports: GeneratorImport[];
+}): MockDefinition & { type?: string } => {
+ if (isReference(schema)) {
+ const {
+ name,
+ specKey = context.specKey,
+ refPaths,
+ } = getRefInfo(schema.$ref, context);
+
+ const schemaRef = get(context.specs[specKey], refPaths);
+
+ const newSchema = {
+ ...schemaRef,
+ name,
+ path: schema.path,
+ isRef: true,
+ };
+
+ const scalar = getMockScalar({
+ item: newSchema,
+ mockOptions,
+ operationId,
+ tags,
+ combine,
+ context: {
+ ...context,
+ specKey,
+ },
+ imports,
+ });
+
+ return {
+ ...scalar,
+ type: newSchema.type,
+ };
+ }
+
+ const scalar = getMockScalar({
+ item: schema,
+ mockOptions,
+ operationId,
+ tags,
+ combine,
+ context,
+ imports,
+ });
+
+ return {
+ ...scalar,
+ type: schema.type,
+ };
+};
diff --git a/packages/msw/src/types.ts b/packages/msw/src/types.ts
new file mode 100644
index 000000000..58db4d31b
--- /dev/null
+++ b/packages/msw/src/types.ts
@@ -0,0 +1,17 @@
+import { GeneratorImport } from '@orval/core';
+import { SchemaObject } from 'openapi3-ts';
+
+export interface MockDefinition {
+ value: string;
+ enums?: string[];
+ imports: GeneratorImport[];
+ name: string;
+ overrided?: boolean;
+ includedProperties?: string[];
+}
+
+export type MockSchemaObject = SchemaObject & {
+ name: string;
+ path?: string;
+ isRef?: boolean;
+};
diff --git a/packages/msw/tsconfig.json b/packages/msw/tsconfig.json
new file mode 100644
index 000000000..9e25e6ece
--- /dev/null
+++ b/packages/msw/tsconfig.json
@@ -0,0 +1,4 @@
+{
+ "extends": "../../tsconfig.base.json",
+ "include": ["src/**/*.ts"]
+}
diff --git a/packages/orval/README.md b/packages/orval/README.md
new file mode 100644
index 000000000..8e1596ee8
--- /dev/null
+++ b/packages/orval/README.md
@@ -0,0 +1,28 @@
+[![npm version](https://badge.fury.io/js/orval.svg)](https://badge.fury.io/js/orval)
+[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
+[![tests](https://github.com/anymaniax/orval/actions/workflows/tests.yaml/badge.svg)](https://github.com/anymaniax/orval/actions/workflows/tests.yaml)
+
+
+
+
+
+ Visit orval.dev for docs, guides, API and beer!
+
+
+### Code Generation
+
+`orval` is able to generate client with appropriate type-signatures (TypeScript) from any valid OpenAPI v3 or Swagger v2 specification, either in `yaml` or `json` formats.
+
+`Generate`, `valid`, `cache` and `mock` in your React, Vue, Svelte and Angular applications all with your OpenAPI specification.
+
+### Samples
+
+You can find below some samples
+
+- [react app](https://github.com/anymaniax/orval/tree/master/samples/react-app)
+- [react query](https://github.com/anymaniax/orval/tree/master/samples/react-query)
+- [svelte query](https://github.com/anymaniax/orval/tree/master/samples/svelte-query)
+- [vue query](https://github.com/anymaniax/orval/tree/master/samples/vue-query)
+- [react app with swr](https://github.com/anymaniax/orval/tree/master/samples/react-app-with-swr)
+- [nx fastify react](https://github.com/anymaniax/orval/tree/master/samples/nx-fastify-react)
+- [angular app](https://github.com/anymaniax/orval/tree/master/samples/angular-app)
diff --git a/packages/orval/package.json b/packages/orval/package.json
new file mode 100644
index 000000000..fa4252f68
--- /dev/null
+++ b/packages/orval/package.json
@@ -0,0 +1,77 @@
+{
+ "name": "orval",
+ "description": "A swagger client generator for typescript",
+ "version": "6.11.0-alpha.1",
+ "license": "MIT",
+ "files": [
+ "dist"
+ ],
+ "bin": {
+ "orval": "dist/bin/orval.js"
+ },
+ "type": "commonjs",
+ "main": "dist/index.js",
+ "keywords": [
+ "rest",
+ "client",
+ "swagger",
+ "open-api",
+ "fetch",
+ "data fetching",
+ "code-generation",
+ "angular",
+ "react",
+ "react-query",
+ "svelte",
+ "svelte-query",
+ "vue",
+ "vue-query",
+ "msw",
+ "mock",
+ "axios",
+ "vue-query",
+ "vue",
+ "swr"
+ ],
+ "author": {
+ "name": "Victor Bury",
+ "email": "victor@anymaniax.com"
+ },
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/anymaniax/orval"
+ },
+ "scripts": {
+ "build": "tsup ./src/bin/orval.ts ./src/index.ts --target node12 --minify --clean --dts --splitting",
+ "dev": "tsup ./src/bin/orval.ts ./src/index.ts --target node12 --clean --watch src --onSuccess 'yarn generate-api'",
+ "lint": "eslint src/**/*.ts",
+ "generate-api": "node ./dist/bin/orval.js --config ../../samples/react-query/basic/orval.config.ts --watch"
+ },
+ "devDependencies": {
+ "@types/chalk": "^2.2.0",
+ "@types/inquirer": "^8.2.2",
+ "@types/lodash.uniq": "^4.5.7"
+ },
+ "dependencies": {
+ "@orval/core": "6.11.0-alpha.1",
+ "@orval/query": "6.11.0-alpha.1",
+ "@orval/angular": "6.11.0-alpha.1",
+ "@orval/axios": "6.11.0-alpha.1",
+ "@orval/swr": "6.11.0-alpha.1",
+ "@orval/msw": "6.11.0-alpha.1",
+ "@apidevtools/swagger-parser": "^10.1.0",
+ "openapi3-ts": "^3.0.0",
+ "chalk": "^4.1.2",
+ "upath": "^2.0.1",
+ "lodash.uniq": "^4.5.0",
+ "execa": "^5.1.1",
+ "string-argv": "^0.3.1",
+ "fs-extra": "^10.1.0",
+ "inquirer": "^8.2.4",
+ "find-up": "5.0.0",
+ "tsconfck": "^2.0.1",
+ "chokidar": "^3.5.3",
+ "ajv": "^8.11.0",
+ "cac": "^6.7.12"
+ }
+}
diff --git a/packages/orval/src/api.ts b/packages/orval/src/api.ts
new file mode 100644
index 000000000..7c47569f0
--- /dev/null
+++ b/packages/orval/src/api.ts
@@ -0,0 +1,112 @@
+import {
+ asyncReduce,
+ ContextSpecs,
+ generateVerbsOptions,
+ GeneratorApiBuilder,
+ GeneratorApiOperations,
+ GeneratorSchema,
+ getRoute,
+ isReference,
+ NormalizedOutputOptions,
+ resolveRef,
+} from '@orval/core';
+import { generateMSWImports } from '@orval/msw';
+import { PathItemObject } from 'openapi3-ts';
+import {
+ generateClientFooter,
+ generateClientHeader,
+ generateClientImports,
+ generateClientTitle,
+ generateOperations,
+} from './client';
+
+export const getApiBuilder = async ({
+ output,
+ context,
+}: {
+ output: NormalizedOutputOptions;
+ context: ContextSpecs;
+}): Promise => {
+ const api = await asyncReduce(
+ Object.entries(context.specs[context.specKey].paths),
+ async (acc, [pathRoute, verbs]: [string, PathItemObject]) => {
+ const route = getRoute(pathRoute);
+
+ let resolvedVerbs = verbs;
+ let resolvedContext = context;
+
+ if (isReference(verbs)) {
+ const { schema, imports } = resolveRef(verbs, context);
+
+ resolvedVerbs = schema;
+
+ resolvedContext = {
+ ...context,
+ ...(imports.length
+ ? {
+ specKey: imports[0].specKey,
+ }
+ : {}),
+ };
+ }
+
+ let verbsOptions = await generateVerbsOptions({
+ verbs: resolvedVerbs,
+ output,
+ route,
+ context: resolvedContext,
+ });
+
+ // GitHub #564 check if we want to exclude deprecated operations
+ if (output.override.useDeprecatedOperations === false) {
+ verbsOptions = verbsOptions.filter((verb) => {
+ return !verb.deprecated;
+ });
+ }
+
+ const schemas = verbsOptions.reduce(
+ (acc, { queryParams, headers, body, response }) => {
+ if (queryParams) {
+ acc.push(queryParams.schema, ...queryParams.deps);
+ }
+ if (headers) {
+ acc.push(headers.schema, ...headers.deps);
+ }
+
+ acc.push(...body.schemas);
+ acc.push(...response.schemas);
+
+ return acc;
+ },
+ [] as GeneratorSchema[],
+ );
+
+ const pathOperations = generateOperations(output.client, verbsOptions, {
+ route,
+ pathRoute,
+ override: output.override,
+ context: resolvedContext,
+ mock: !!output.mock,
+ });
+
+ acc.schemas.push(...schemas);
+ acc.operations = { ...acc.operations, ...pathOperations };
+
+ return acc;
+ },
+ {
+ operations: {},
+ schemas: [],
+ } as GeneratorApiOperations,
+ );
+
+ return {
+ operations: api.operations,
+ schemas: api.schemas,
+ title: generateClientTitle,
+ header: generateClientHeader,
+ footer: generateClientFooter,
+ imports: generateClientImports,
+ importsMock: generateMSWImports,
+ };
+};
diff --git a/packages/orval/src/bin/orval.ts b/packages/orval/src/bin/orval.ts
new file mode 100644
index 000000000..04a203898
--- /dev/null
+++ b/packages/orval/src/bin/orval.ts
@@ -0,0 +1,97 @@
+#!/usr/bin/env node
+import { isString, log, startMessage } from '@orval/core';
+import { cac } from 'cac';
+import chalk from 'chalk';
+import { generateConfig, generateSpec } from '../generate';
+import pkg from '../../package.json';
+import { normalizeOptions } from '../utils/options';
+import { startWatcher } from '../utils/watcher';
+
+const cli = cac('orval');
+
+startMessage({
+ name: pkg.name,
+ version: pkg.version,
+ description: pkg.description,
+});
+
+cli.version(pkg.version);
+
+cli
+ .command(
+ '[config]',
+ 'generate client with appropriate type-signatures from OpenAPI specs',
+ {
+ ignoreOptionDefaultValue: true,
+ },
+ )
+ .option('-o, --output ', 'output file destination')
+ .option('-i, --input ', 'input file (yaml or json openapi specs)')
+ .option('-c, --config ', 'override flags by a config file')
+ .option('-p, --project ', 'focus a project of the config')
+ .option('-m, --mode ', 'default mode that will be used')
+ .option('-c, --client ', 'default client that will be used')
+ .option('--mock', 'activate the mock')
+ .option(
+ '-w, --watch [path]',
+ 'Watch mode, if path is not specified, it watches the input target',
+ )
+ .option('--clean [path]', 'Clean output directory')
+ .option('--prettier [path]', 'Prettier generated files')
+ .option('--tslint [path]', 'tslint generated files')
+ .option('--tsconfig [path]', 'path to your tsconfig file')
+ .action(async (paths, cmd) => {
+ if (!cmd.config && isString(cmd.input) && isString(cmd.output)) {
+ const normalizedOptions = await normalizeOptions({
+ input: cmd.input,
+ output: {
+ target: cmd.output,
+ clean: cmd.clean,
+ prettier: cmd.prettier,
+ tslint: cmd.tslint,
+ mock: cmd.mock,
+ client: cmd.client,
+ mode: cmd.mode,
+ tsconfig: cmd.tsconfig,
+ },
+ });
+
+ if (cmd.watch) {
+ startWatcher(
+ cmd.watch,
+ async () => {
+ try {
+ await generateSpec(process.cwd(), normalizedOptions);
+ } catch (e) {
+ log(chalk.red(`🛑 ${e}`));
+ }
+ },
+ normalizedOptions.input.target as string,
+ );
+ } else {
+ try {
+ await generateSpec(process.cwd(), normalizedOptions);
+ } catch (e) {
+ log(chalk.red(`🛑 ${e}`));
+ }
+ }
+ } else {
+ await generateConfig(cmd.config, {
+ projectName: cmd.project,
+ watch: cmd.watch,
+ clean: cmd.clean,
+ prettier: cmd.prettier,
+ tslint: cmd.tslint,
+ mock: cmd.mock,
+ client: cmd.client,
+ mode: cmd.mode,
+ tsconfig: cmd.tsconfig,
+ input: cmd.input,
+ output: cmd.output,
+ });
+ }
+ });
+
+cli.help();
+
+cli.parse(process.argv);
diff --git a/packages/orval/src/client.ts b/packages/orval/src/client.ts
new file mode 100644
index 000000000..1d43968ed
--- /dev/null
+++ b/packages/orval/src/client.ts
@@ -0,0 +1,196 @@
+import angular from '@orval/angular';
+import axios from '@orval/axios';
+import {
+ generateDependencyImports,
+ GeneratorClientFooter,
+ GeneratorClientHeader,
+ GeneratorClientImports,
+ GeneratorClients,
+ GeneratorClientTitle,
+ GeneratorOperations,
+ GeneratorOptions,
+ GeneratorVerbOptions,
+ GeneratorVerbsOptions,
+ isFunction,
+ OutputClient,
+ OutputClientFunc,
+ pascal,
+} from '@orval/core';
+import { generateMSW } from '@orval/msw';
+import query from '@orval/query';
+import swr from '@orval/swr';
+
+const DEFAULT_CLIENT = OutputClient.AXIOS;
+
+export const GENERATOR_CLIENT: GeneratorClients = {
+ axios: axios({ type: 'axios' }),
+ 'axios-functions': axios({ type: 'axios-functions' }),
+ angular: angular(),
+ 'react-query': query({ type: 'react-query' }),
+ 'svelte-query': query({ type: 'svelte-query' }),
+ 'vue-query': query({ type: 'vue-query' }),
+ swr: swr(),
+};
+
+const getGeneratorClient = (outputClient: OutputClient | OutputClientFunc) => {
+ const generator = isFunction(outputClient)
+ ? outputClient(GENERATOR_CLIENT)
+ : GENERATOR_CLIENT[outputClient];
+
+ if (!generator) {
+ throw `Oups... 🍻. Client not found: ${outputClient}`;
+ }
+
+ return generator;
+};
+
+export const generateClientImports: GeneratorClientImports = ({
+ client = DEFAULT_CLIENT,
+ implementation,
+ imports,
+ specsName,
+ hasSchemaDir,
+ isAllowSyntheticDefaultImports,
+ hasGlobalMutator,
+ packageJson,
+}) => {
+ const { dependencies } = getGeneratorClient(client);
+ return generateDependencyImports(
+ implementation,
+ [...dependencies(hasGlobalMutator, packageJson), ...imports],
+ specsName,
+ hasSchemaDir,
+ isAllowSyntheticDefaultImports,
+ );
+};
+
+export const generateClientHeader: GeneratorClientHeader = ({
+ outputClient = DEFAULT_CLIENT,
+ isRequestOptions,
+ isGlobalMutator,
+ isMutator,
+ provideIn,
+ hasAwaitedType,
+ titles,
+}) => {
+ const { header } = getGeneratorClient(outputClient);
+ return {
+ implementation: header({
+ title: titles.implementation,
+ isRequestOptions,
+ isGlobalMutator,
+ isMutator,
+ provideIn,
+ hasAwaitedType,
+ }),
+ implementationMSW: `export const ${titles.implementationMSW} = () => [\n`,
+ };
+};
+
+export const generateClientFooter: GeneratorClientFooter = ({
+ outputClient = DEFAULT_CLIENT,
+ operationNames,
+ hasMutator,
+ hasAwaitedType,
+ titles,
+}) => {
+ const { footer } = getGeneratorClient(outputClient);
+ let implementation: string;
+ try {
+ if (isFunction(outputClient)) {
+ implementation = (footer as (operationNames: any) => string)(
+ operationNames,
+ );
+ // being here means that the previous call worked
+ console.warn(
+ '[WARN] Passing an array of strings for operations names to the footer function is deprecated and will be removed in a future major release. Please pass them in an object instead: { operationNames: string[] }.',
+ );
+ } else {
+ implementation = footer({
+ operationNames,
+ title: titles.implementation,
+ hasMutator,
+ hasAwaitedType,
+ });
+ }
+ } catch (e) {
+ implementation = footer({
+ operationNames,
+ title: titles.implementation,
+ hasMutator,
+ hasAwaitedType,
+ });
+ }
+
+ return {
+ implementation,
+ implementationMSW: `]\n`,
+ };
+};
+
+export const generateClientTitle: GeneratorClientTitle = ({
+ outputClient = DEFAULT_CLIENT,
+ title,
+ customTitleFunc,
+}) => {
+ const { title: generatorTitle } = getGeneratorClient(outputClient);
+ if (customTitleFunc) {
+ const customTitle = customTitleFunc(title);
+ return {
+ implementation: generatorTitle(customTitle),
+ implementationMSW: `get${pascal(customTitle)}MSW`,
+ };
+ }
+ return {
+ implementation: generatorTitle(title),
+ implementationMSW: `get${pascal(title)}MSW`,
+ };
+};
+
+const generateMock = (
+ verbOption: GeneratorVerbOptions,
+ options: GeneratorOptions,
+) => {
+ if (!options.mock) {
+ return {
+ implementation: {
+ function: '',
+ handler: '',
+ },
+ imports: [],
+ };
+ }
+
+ if (isFunction(options.mock)) {
+ return options.mock(verbOption, options);
+ }
+
+ return generateMSW(verbOption, options);
+};
+
+export const generateOperations = (
+ outputClient: OutputClient | OutputClientFunc = DEFAULT_CLIENT,
+ verbsOptions: GeneratorVerbsOptions,
+ options: GeneratorOptions,
+): GeneratorOperations => {
+ return verbsOptions.reduce((acc, verbOption) => {
+ const { client: generatorClient } = getGeneratorClient(outputClient);
+ const client = generatorClient(verbOption, options, outputClient);
+ const msw = generateMock(verbOption, options);
+
+ acc[verbOption.operationId] = {
+ implementation: verbOption.doc + client.implementation,
+ imports: client.imports,
+ implementationMSW: msw.implementation,
+ importsMSW: msw.imports,
+ tags: verbOption.tags,
+ mutator: verbOption.mutator,
+ formData: verbOption.formData,
+ formUrlEncoded: verbOption.formUrlEncoded,
+ operationName: verbOption.operationName,
+ types: client.types,
+ };
+
+ return acc;
+ }, {} as GeneratorOperations);
+};
diff --git a/packages/orval/src/generate.ts b/packages/orval/src/generate.ts
new file mode 100644
index 000000000..92c96b31f
--- /dev/null
+++ b/packages/orval/src/generate.ts
@@ -0,0 +1,136 @@
+import {
+ asyncReduce,
+ ConfigExternal,
+ errorMessage,
+ getFileInfo,
+ GlobalOptions,
+ isFunction,
+ isString,
+ loadFile,
+ log,
+ NormalizedOptions,
+ NormizaledConfig,
+ removeFiles,
+} from '@orval/core';
+import chalk from 'chalk';
+import { dirname } from 'upath';
+import { importSpecs } from './import-specs';
+import { writeSpecs } from './write-specs';
+
+import { normalizeOptions } from './utils/options';
+import { startWatcher } from './utils/watcher';
+
+export const generateSpec = async (
+ workspace: string,
+ options: NormalizedOptions,
+ projectName?: string,
+) => {
+ if (options.output.clean) {
+ const extraPatterns = Array.isArray(options.output.clean)
+ ? options.output.clean
+ : [];
+
+ if (options.output.target) {
+ await removeFiles(
+ ['**/*', '!**/*.d.ts', ...extraPatterns],
+ getFileInfo(options.output.target).dirname,
+ );
+ }
+ if (options.output.schemas) {
+ await removeFiles(
+ ['**/*', '!**/*.d.ts', ...extraPatterns],
+ getFileInfo(options.output.schemas).dirname,
+ );
+ }
+ log(`${projectName ? `${projectName}: ` : ''}Cleaning output folder`);
+ }
+
+ const writeSpecBuilder = await importSpecs(workspace, options);
+ await writeSpecs(writeSpecBuilder, workspace, options, projectName);
+};
+
+export const generateSpecs = async (
+ config: NormizaledConfig,
+ workspace: string,
+ projectName?: string,
+) => {
+ if (projectName) {
+ const options = config[projectName];
+
+ if (options) {
+ try {
+ await generateSpec(workspace, options, projectName);
+ } catch (e) {
+ log(chalk.red(`🛑 ${projectName ? `${projectName} - ` : ''}${e}`));
+ }
+ } else {
+ errorMessage('Project not found');
+ process.exit(1);
+ }
+ return;
+ }
+
+ return asyncReduce(
+ Object.entries(config),
+ async (acc, [projectName, options]) => {
+ try {
+ acc.push(await generateSpec(workspace, options, projectName));
+ } catch (e) {
+ log(chalk.red(`🛑 ${projectName ? `${projectName} - ` : ''}${e}`));
+ }
+ return acc;
+ },
+ [] as void[],
+ );
+};
+
+export const generateConfig = async (
+ configFile?: string,
+ options?: GlobalOptions,
+) => {
+ const {
+ path,
+ file: configExternal,
+ error,
+ } = await loadFile(configFile, {
+ defaultFileName: 'orval.config',
+ });
+
+ if (!configExternal) {
+ throw `failed to load from ${path} => ${error}`;
+ }
+
+ const workspace = dirname(path);
+
+ const config = await (isFunction(configExternal)
+ ? configExternal()
+ : configExternal);
+
+ const normalizedConfig = await asyncReduce(
+ Object.entries(config),
+ async (acc, [key, value]) => {
+ acc[key] = await normalizeOptions(value, workspace, options);
+
+ return acc;
+ },
+ {} as NormizaledConfig,
+ );
+
+ const fileToWatch = Object.entries(normalizedConfig)
+ .filter(
+ ([project]) =>
+ options?.projectName === undefined || project === options?.projectName,
+ )
+ .map(([, { input }]) => input.target)
+ .filter((target) => isString(target)) as string[];
+
+ if (options?.watch && fileToWatch.length) {
+ startWatcher(
+ options?.watch,
+ () => generateSpecs(normalizedConfig, workspace, options?.projectName),
+ fileToWatch,
+ );
+ } else {
+ await generateSpecs(normalizedConfig, workspace, options?.projectName);
+ }
+};
diff --git a/packages/orval/src/import-open-api.ts b/packages/orval/src/import-open-api.ts
new file mode 100644
index 000000000..237f3b5bb
--- /dev/null
+++ b/packages/orval/src/import-open-api.ts
@@ -0,0 +1,193 @@
+import {
+ asyncReduce,
+ ContextSpecs,
+ dynamicImport,
+ generateComponentDefinition,
+ generateParameterDefinition,
+ generateSchemasDefinition,
+ GeneratorSchema,
+ ibmOpenapiValidator,
+ ImportOpenApi,
+ InputOptions,
+ isObject,
+ isReference,
+ NormalizedOutputOptions,
+ openApiConverter,
+ WriteSpecsBuilder,
+} from '@orval/core';
+import omit from 'lodash.omit';
+import { OpenAPIObject, SchemasObject } from 'openapi3-ts';
+import { getApiBuilder } from './api';
+
+export const importOpenApi = async ({
+ data,
+ input,
+ output,
+ target,
+ workspace,
+}: ImportOpenApi): Promise => {
+ const specs = await generateInputSpecs({ specs: data, input, workspace });
+
+ const schemas = getApiSchemas({ output, target, workspace, specs });
+
+ const api = await getApiBuilder({
+ output,
+ context: {
+ specKey: target,
+ target,
+ workspace,
+ specs,
+ override: output.override,
+ tslint: output.tslint,
+ tsconfig: output.tsconfig,
+ packageJson: output.packageJson,
+ },
+ });
+
+ return {
+ ...api,
+ schemas: {
+ ...schemas,
+ [target]: [...(schemas[target] ?? []), ...api.schemas],
+ },
+ target,
+ info: specs[target].info,
+ };
+};
+
+const generateInputSpecs = async ({
+ specs,
+ input,
+ workspace,
+}: {
+ specs: Record;
+ input: InputOptions;
+ workspace: string;
+}): Promise> => {
+ const transformerFn = input.override?.transformer
+ ? await dynamicImport(input.override.transformer, workspace)
+ : undefined;
+
+ return asyncReduce(
+ Object.entries(specs),
+ async (acc, [specKey, value]) => {
+ const schema = await openApiConverter(
+ value,
+ input.converterOptions,
+ specKey,
+ );
+
+ const transfomedSchema = transformerFn ? transformerFn(schema) : schema;
+
+ if (input.validation) {
+ await ibmOpenapiValidator(transfomedSchema);
+ }
+
+ acc[specKey] = transfomedSchema;
+
+ return acc;
+ },
+ {} as Record,
+ );
+};
+
+const getApiSchemas = ({
+ output,
+ target,
+ workspace,
+ specs,
+}: {
+ output: NormalizedOutputOptions;
+ workspace: string;
+ target: string;
+ specs: Record;
+}) => {
+ return Object.entries(specs).reduce((acc, [specKey, spec]) => {
+ const context: ContextSpecs = {
+ specKey,
+ target,
+ workspace,
+ specs,
+ override: output.override,
+ tslint: output.tslint,
+ tsconfig: output.tsconfig,
+ packageJson: output.packageJson,
+ };
+
+ const schemaDefinition = generateSchemasDefinition(
+ !spec.openapi
+ ? getAllSchemas(spec)
+ : (spec.components?.schemas as SchemasObject),
+ context,
+ output.override.components.schemas.suffix,
+ );
+
+ const responseDefinition = generateComponentDefinition(
+ spec.components?.responses,
+ context,
+ output.override.components.responses.suffix,
+ );
+
+ const bodyDefinition = generateComponentDefinition(
+ spec.components?.requestBodies,
+ context,
+ output.override.components.requestBodies.suffix,
+ );
+
+ const parameters = generateParameterDefinition(
+ spec.components?.parameters,
+ context,
+ output.override.components.parameters.suffix,
+ );
+
+ const schemas = [
+ ...schemaDefinition,
+ ...responseDefinition,
+ ...bodyDefinition,
+ ...parameters,
+ ];
+
+ if (!schemas.length) {
+ return acc;
+ }
+
+ acc[specKey] = schemas;
+
+ return acc;
+ }, {} as Record);
+};
+
+const getAllSchemas = (spec: object): SchemasObject => {
+ const cleanedSpec = omit(spec, [
+ 'openapi',
+ 'info',
+ 'servers',
+ 'paths',
+ 'components',
+ 'security',
+ 'tags',
+ 'externalDocs',
+ ]);
+
+ const schemas = Object.entries(cleanedSpec).reduce(
+ (acc, [key, value]) => {
+ if (!isObject(value)) {
+ return acc;
+ }
+
+ if (!value.type && !isReference(value)) {
+ return { ...acc, ...getAllSchemas(value) };
+ }
+
+ acc[key] = value;
+
+ return acc;
+ },
+ {},
+ );
+
+ return {
+ ...schemas,
+ ...((spec as OpenAPIObject)?.components?.schemas as SchemasObject),
+ };
+};
diff --git a/packages/orval/src/import-specs.ts b/packages/orval/src/import-specs.ts
new file mode 100644
index 000000000..0bd76989f
--- /dev/null
+++ b/packages/orval/src/import-specs.ts
@@ -0,0 +1,69 @@
+import SwaggerParser from '@apidevtools/swagger-parser';
+import {
+ isObject,
+ isUrl,
+ log,
+ NormalizedOptions,
+ SwaggerParserOptions,
+ WriteSpecsBuilder,
+} from '@orval/core';
+import chalk from 'chalk';
+import { resolve } from 'upath';
+import { importOpenApi } from './import-open-api';
+
+const resolveSpecs = async (
+ path: string,
+ { validate, ...options }: SwaggerParserOptions,
+ isUrl: boolean,
+) => {
+ if (validate) {
+ try {
+ await SwaggerParser.validate(path);
+ } catch (e: any) {
+ if (e?.name === 'ParserError') {
+ throw e;
+ }
+ log(`⚠️ ${chalk.yellow(e)}`);
+ }
+ }
+
+ const data = (await SwaggerParser.resolve(path, options)).values();
+
+ if (isUrl) {
+ return data;
+ }
+
+ // normalizing slashes after SwaggerParser
+ return Object.fromEntries(
+ Object.entries(data).map(([key, value]) => [resolve(key), value]),
+ );
+};
+
+export const importSpecs = async (
+ workspace: string,
+ options: NormalizedOptions,
+): Promise => {
+ const { input, output } = options;
+
+ if (isObject(input.target)) {
+ return importOpenApi({
+ data: { [workspace]: input.target },
+ input,
+ output,
+ target: workspace,
+ workspace,
+ });
+ }
+
+ const isPathUrl = isUrl(input.target);
+
+ const data = await resolveSpecs(input.target, input.parserOptions, isPathUrl);
+
+ return importOpenApi({
+ data,
+ input,
+ output,
+ target: input.target,
+ workspace,
+ });
+};
diff --git a/packages/orval/src/index.ts b/packages/orval/src/index.ts
new file mode 100644
index 000000000..7241828b6
--- /dev/null
+++ b/packages/orval/src/index.ts
@@ -0,0 +1,63 @@
+import {
+ GlobalOptions,
+ isString,
+ log,
+ Options,
+ OptionsExport,
+} from '@orval/core';
+import chalk from 'chalk';
+import { generateConfig, generateSpec } from './generate';
+import { defineConfig, normalizeOptions } from './utils/options';
+import { startWatcher } from './utils/watcher';
+
+const generate = async (
+ optionsExport?: string | OptionsExport,
+ workspace = process.cwd(),
+ options?: GlobalOptions,
+) => {
+ if (!optionsExport || isString(optionsExport)) {
+ return generateConfig(optionsExport, options);
+ }
+
+ const normalizedOptions = await normalizeOptions(
+ optionsExport,
+ workspace,
+ options,
+ );
+
+ if (options?.watch) {
+ startWatcher(
+ options?.watch,
+ async () => {
+ try {
+ await generateSpec(workspace, normalizedOptions);
+ } catch (e) {
+ log(
+ chalk.red(
+ `🛑 ${
+ options?.projectName ? `${options?.projectName} - ` : ''
+ }${e}`,
+ ),
+ );
+ }
+ },
+ normalizedOptions.input.target as string,
+ );
+ } else {
+ try {
+ return await generateSpec(workspace, normalizedOptions);
+ } catch (e) {
+ log(
+ chalk.red(
+ `🛑 ${options?.projectName ? `${options?.projectName} - ` : ''}${e}`,
+ ),
+ );
+ }
+ }
+};
+
+export { defineConfig };
+export { Options };
+export { generate };
+
+export default generate;
diff --git a/packages/orval/src/utils/executeHook.ts b/packages/orval/src/utils/executeHook.ts
new file mode 100644
index 000000000..44594fe38
--- /dev/null
+++ b/packages/orval/src/utils/executeHook.ts
@@ -0,0 +1,32 @@
+import {
+ Hook,
+ isFunction,
+ isString,
+ log,
+ NormalizedHookCommand,
+} from '@orval/core';
+import chalk from 'chalk';
+import execa from 'execa';
+import { parseArgsStringToArgv } from 'string-argv';
+
+export const executeHook = async (
+ name: Hook,
+ commands: NormalizedHookCommand = [],
+ args: string[] = [],
+) => {
+ log(chalk.white(`Running ${name} hook...`));
+
+ for (const command of commands) {
+ if (isString(command)) {
+ const [cmd, ..._args] = [...parseArgsStringToArgv(command), ...args];
+
+ try {
+ await execa(cmd, _args);
+ } catch (e) {
+ log(chalk.red(`🛑 Failed to run ${name} hook: ${e}`));
+ }
+ } else if (isFunction(command)) {
+ await command(args);
+ }
+ }
+};
diff --git a/packages/orval/src/utils/github.ts b/packages/orval/src/utils/github.ts
new file mode 100644
index 000000000..417a8299c
--- /dev/null
+++ b/packages/orval/src/utils/github.ts
@@ -0,0 +1,141 @@
+import SwaggerParser from '@apidevtools/swagger-parser';
+import { outputFile, pathExists, readFile, unlink } from 'fs-extra';
+import https from 'https';
+import inquirer from 'inquirer';
+import { join } from 'upath';
+import { request } from './request';
+export const getGithubSpecReq = ({
+ accessToken,
+ repo,
+ owner,
+ branch,
+ path,
+}: {
+ accessToken: string;
+ repo: string;
+ owner: string;
+ branch: string;
+ path: string;
+}): [https.RequestOptions, string] => {
+ const payload = JSON.stringify({
+ query: `query {
+ repository(name: "${repo}", owner: "${owner}") {
+ object(expression: "${branch}:${path}") {
+ ... on Blob {
+ text
+ }
+ }
+ }
+ }`,
+ });
+
+ return [
+ {
+ method: 'POST',
+ hostname: 'api.github.com',
+ path: '/graphql',
+ headers: {
+ 'content-type': 'application/json',
+ 'user-agent': 'orval-importer',
+ authorization: `bearer ${accessToken}`,
+ 'Content-Length': payload.length,
+ },
+ },
+ payload,
+ ];
+};
+
+export const getGithubAcessToken = async (githubTokenPath: string) => {
+ if (await pathExists(githubTokenPath)) {
+ return readFile(githubTokenPath, 'utf-8');
+ } else {
+ const answers = await inquirer.prompt<{
+ githubToken: string;
+ saveToken: boolean;
+ }>([
+ {
+ type: 'input',
+ name: 'githubToken',
+ message:
+ 'Please provide a GitHub token with `repo` rules checked (https://help.github.com/articles/creating-a-personal-access-token-for-the-command-line/)',
+ },
+ {
+ type: 'confirm',
+ name: 'saveToken',
+ message:
+ 'Would you like to store your token for the next time? (stored in your node_modules)',
+ },
+ ]);
+ if (answers.saveToken) {
+ await outputFile(githubTokenPath, answers.githubToken);
+ }
+ return answers.githubToken;
+ }
+};
+
+export const getGithubOpenApi = async (url: string): Promise => {
+ const githubTokenPath = join(__dirname, '.githubToken');
+ const accessToken = await getGithubAcessToken(githubTokenPath);
+ const [info] = url.split('github.com/').slice(-1);
+
+ const [owner, repo, , branch, ...paths] = info.split('/');
+ const path = paths.join('/');
+
+ try {
+ const { body } = await request<{
+ data?: { repository: any };
+ errors?: { type: string }[];
+ }>(...getGithubSpecReq({ accessToken, repo, owner, branch, path }));
+ if (body.errors?.length) {
+ const isErrorRemoveLink = body.errors?.some(
+ (error) => error?.type === 'NOT_FOUND',
+ );
+
+ if (isErrorRemoveLink) {
+ const answers = await inquirer.prompt<{ removeToken: boolean }>([
+ {
+ type: 'confirm',
+ name: 'removeToken',
+ message:
+ "Your token doesn't have the correct permissions, should we remove it?",
+ },
+ ]);
+ if (answers.removeToken) {
+ await unlink(githubTokenPath);
+ }
+ }
+ }
+
+ return body.data?.repository?.object.text;
+ } catch (e) {
+ if (!e.body) {
+ throw `Oups... 🍻. ${e}`;
+ }
+
+ if (e.body.message === 'Bad credentials') {
+ const answers = await inquirer.prompt<{ removeToken: boolean }>([
+ {
+ type: 'confirm',
+ name: 'removeToken',
+ message:
+ "Your token doesn't have the correct permissions, should we remove it?",
+ },
+ ]);
+ if (answers.removeToken) {
+ await unlink(githubTokenPath);
+ }
+ }
+ throw e.body.message || `Oups... 🍻. ${e}`;
+ }
+};
+
+export const githubResolver = {
+ order: 199,
+ canRead(file: SwaggerParser.FileInfo) {
+ return file.url.includes('github.com');
+ },
+
+ read(file: SwaggerParser.FileInfo) {
+ return getGithubOpenApi(file.url);
+ },
+};
diff --git a/packages/orval/src/utils/index.ts b/packages/orval/src/utils/index.ts
new file mode 100644
index 000000000..cd8d73e13
--- /dev/null
+++ b/packages/orval/src/utils/index.ts
@@ -0,0 +1,7 @@
+export * from './executeHook';
+export * from './github';
+export * from './options';
+export * from './package-json';
+export * from './request';
+export * from './tsconfig';
+export * from './watcher';
diff --git a/packages/orval/src/utils/options.ts b/packages/orval/src/utils/options.ts
new file mode 100644
index 000000000..727f6efd7
--- /dev/null
+++ b/packages/orval/src/utils/options.ts
@@ -0,0 +1,358 @@
+import {
+ ConfigExternal,
+ createLogger,
+ GlobalOptions,
+ Hook,
+ HookFunction,
+ HooksOptions,
+ isBoolean,
+ isFunction,
+ isObject,
+ isString,
+ isUrl,
+ mergeDeep,
+ Mutator,
+ NormalizedHookOptions,
+ NormalizedMutator,
+ NormalizedOperationOptions,
+ NormalizedOptions,
+ OperationOptions,
+ OptionsExport,
+ OutputClient,
+ OutputMode,
+ RefComponentSuffix,
+ SwaggerParserOptions,
+} from '@orval/core';
+import chalk from 'chalk';
+import { InfoObject } from 'openapi3-ts';
+import { resolve } from 'upath';
+import pkg from '../../package.json';
+import { githubResolver } from './github';
+import { loadPackageJson } from './package-json';
+import { loadTsconfig } from './tsconfig';
+
+/**
+ * Type helper to make it easier to use orval.config.ts
+ * accepts a direct {@link ConfigExternal} object.
+ */
+export function defineConfig(options: ConfigExternal): ConfigExternal {
+ return options;
+}
+
+export const normalizeOptions = async (
+ optionsExport: OptionsExport,
+ workspace = process.cwd(),
+ globalOptions: GlobalOptions = {},
+) => {
+ const options = await (isFunction(optionsExport)
+ ? optionsExport()
+ : optionsExport);
+
+ if (!options.input) {
+ createLogger().error(chalk.red(`Config require an input`));
+ process.exit(1);
+ }
+
+ if (!options.output) {
+ createLogger().error(chalk.red(`Config require an output`));
+ process.exit(1);
+ }
+
+ const inputOptions = isString(options.input)
+ ? { target: options.input }
+ : options.input;
+
+ const outputOptions = isString(options.output)
+ ? { target: options.output }
+ : options.output;
+
+ const outputWorkspace = normalizePath(
+ outputOptions.workspace || '',
+ workspace,
+ );
+
+ const { clean, prettier, client, mode, mock, tslint } = globalOptions;
+
+ const tsconfig = await loadTsconfig(
+ outputOptions.tsconfig || globalOptions.tsconfig,
+ workspace,
+ );
+
+ const packageJson = await loadPackageJson(
+ outputOptions.packageJson || globalOptions.packageJson,
+ workspace,
+ );
+
+ const normalizedOptions: NormalizedOptions = {
+ input: {
+ target: globalOptions.input
+ ? normalizePathOrUrl(globalOptions.input, process.cwd())
+ : normalizePathOrUrl(inputOptions.target, workspace),
+ validation: inputOptions.validation || false,
+ override: {
+ transformer: normalizePath(
+ inputOptions.override?.transformer,
+ workspace,
+ ),
+ },
+ converterOptions: inputOptions.converterOptions ?? {},
+ parserOptions: mergeDeep(
+ parserDefaultOptions,
+ inputOptions.parserOptions ?? {},
+ ),
+ },
+ output: {
+ target: globalOptions.output
+ ? normalizePath(globalOptions.output, process.cwd())
+ : normalizePath(outputOptions.target, outputWorkspace),
+ schemas: normalizePath(outputOptions.schemas, outputWorkspace),
+ workspace: outputOptions.workspace ? outputWorkspace : undefined,
+ client: outputOptions.client ?? client ?? OutputClient.AXIOS_FUNCTIONS,
+ mode: normalizeOutputMode(outputOptions.mode ?? mode),
+ mock: outputOptions.mock ?? mock ?? false,
+ clean: outputOptions.clean ?? clean ?? false,
+ prettier: outputOptions.prettier ?? prettier ?? false,
+ tslint: outputOptions.tslint ?? tslint ?? false,
+ tsconfig,
+ packageJson,
+ headers: outputOptions.headers ?? false,
+ override: {
+ ...outputOptions.override,
+ mock: {
+ arrayMin: outputOptions.override?.mock?.arrayMin ?? 1,
+ arrayMax: outputOptions.override?.mock?.arrayMax ?? 10,
+ ...(outputOptions.override?.mock ?? {}),
+ },
+ operations: normalizeOperationsAndTags(
+ outputOptions.override?.operations ?? {},
+ outputWorkspace,
+ ),
+ tags: normalizeOperationsAndTags(
+ outputOptions.override?.tags ?? {},
+ outputWorkspace,
+ ),
+ mutator: normalizeMutator(
+ outputWorkspace,
+ outputOptions.override?.mutator,
+ ),
+ formData:
+ (!isBoolean(outputOptions.override?.formData)
+ ? normalizeMutator(
+ outputWorkspace,
+ outputOptions.override?.formData,
+ )
+ : outputOptions.override?.formData) ?? true,
+ formUrlEncoded:
+ (!isBoolean(outputOptions.override?.formUrlEncoded)
+ ? normalizeMutator(
+ outputWorkspace,
+ outputOptions.override?.formUrlEncoded,
+ )
+ : outputOptions.override?.formUrlEncoded) ?? true,
+ header:
+ outputOptions.override?.header === false
+ ? false
+ : isFunction(outputOptions.override?.header)
+ ? outputOptions.override?.header!
+ : getDefaultFilesHeader,
+ requestOptions: outputOptions.override?.requestOptions ?? true,
+ components: {
+ schemas: {
+ suffix: RefComponentSuffix.schemas,
+ ...(outputOptions.override?.components?.schemas ?? {}),
+ },
+ responses: {
+ suffix: RefComponentSuffix.responses,
+ ...(outputOptions.override?.components?.responses ?? {}),
+ },
+ parameters: {
+ suffix: RefComponentSuffix.parameters,
+ ...(outputOptions.override?.components?.parameters ?? {}),
+ },
+ requestBodies: {
+ suffix: RefComponentSuffix.requestBodies,
+ ...(outputOptions.override?.components?.requestBodies ?? {}),
+ },
+ },
+ query: {
+ useQuery: true,
+ signal: true,
+ ...(outputOptions.override?.query ?? {}),
+ },
+ swr: {
+ ...(outputOptions.override?.swr ?? {}),
+ },
+ angular: {
+ provideIn: outputOptions.override?.angular?.provideIn ?? 'root',
+ },
+ useDates: outputOptions.override?.useDates || false,
+ useDeprecatedOperations:
+ outputOptions.override?.useDeprecatedOperations ?? true,
+ },
+ },
+ hooks: options.hooks ? normalizeHooks(options.hooks) : {},
+ };
+
+ if (!normalizedOptions.input.target) {
+ createLogger().error(chalk.red(`Config require an input target`));
+ process.exit(1);
+ }
+
+ if (!normalizedOptions.output.target && !normalizedOptions.output.schemas) {
+ createLogger().error(
+ chalk.red(`Config require an output target or schemas`),
+ );
+ process.exit(1);
+ }
+
+ return normalizedOptions;
+};
+
+const parserDefaultOptions = {
+ validate: true,
+ resolve: { github: githubResolver },
+} as SwaggerParserOptions;
+
+const normalizeMutator = (
+ workspace: string,
+ mutator?: Mutator,
+): NormalizedMutator | undefined => {
+ if (isObject(mutator)) {
+ if (!mutator.path) {
+ createLogger().error(chalk.red(`Mutator need a path`));
+ process.exit(1);
+ }
+
+ return {
+ ...mutator,
+ path: resolve(workspace, mutator.path),
+ default: (mutator.default || !mutator.name) ?? false,
+ };
+ }
+
+ if (isString(mutator)) {
+ return {
+ path: resolve(workspace, mutator),
+ default: true,
+ };
+ }
+
+ return mutator;
+};
+
+const normalizePathOrUrl = (path: T, workspace: string) => {
+ if (isString(path) && !isUrl(path)) {
+ return normalizePath(path, workspace);
+ }
+
+ return path;
+};
+
+export const normalizePath = (path: T, workspace: string) => {
+ if (!isString(path)) {
+ return path;
+ }
+ return resolve(workspace, path);
+};
+
+const normalizeOperationsAndTags = (
+ operationsOrTags: {
+ [key: string]: OperationOptions;
+ },
+ workspace: string,
+): {
+ [key: string]: NormalizedOperationOptions;
+} => {
+ return Object.fromEntries(
+ Object.entries(operationsOrTags).map(
+ ([
+ key,
+ {
+ transformer,
+ mutator,
+ formData,
+ formUrlEncoded,
+ requestOptions,
+ ...rest
+ },
+ ]) => {
+ return [
+ key,
+ {
+ ...rest,
+ ...(transformer
+ ? { transformer: normalizePath(transformer, workspace) }
+ : {}),
+ ...(mutator
+ ? { mutator: normalizeMutator(workspace, mutator) }
+ : {}),
+ ...(formData
+ ? {
+ formData: !isBoolean(formData)
+ ? normalizeMutator(workspace, formData)
+ : formData,
+ }
+ : {}),
+ ...(formUrlEncoded
+ ? {
+ formUrlEncoded: !isBoolean(formUrlEncoded)
+ ? normalizeMutator(workspace, formUrlEncoded)
+ : formUrlEncoded,
+ }
+ : {}),
+ },
+ ];
+ },
+ ),
+ );
+};
+
+const normalizeOutputMode = (mode?: OutputMode): OutputMode => {
+ if (!mode) {
+ return OutputMode.SINGLE;
+ }
+
+ if (!Object.values(OutputMode).includes(mode)) {
+ createLogger().warn(chalk.yellow(`Unknown the provided mode => ${mode}`));
+ return OutputMode.SINGLE;
+ }
+
+ return mode;
+};
+
+const normalizeHooks = (hooks: HooksOptions): NormalizedHookOptions => {
+ const keys = Object.keys(hooks) as unknown as Hook[];
+
+ return keys.reduce((acc, key: Hook) => {
+ if (isString(hooks[key])) {
+ return {
+ ...acc,
+ [key]: [hooks[key]] as string[],
+ };
+ } else if (Array.isArray(hooks[key])) {
+ return {
+ ...acc,
+ [key]: hooks[key] as string[],
+ };
+ } else if (isFunction(hooks[key])) {
+ return {
+ ...acc,
+ [key]: [hooks[key]] as HookFunction[],
+ };
+ }
+
+ return acc;
+ }, {} as NormalizedHookOptions);
+};
+
+export const getDefaultFilesHeader = ({
+ title,
+ description,
+ version,
+}: InfoObject) => [
+ `Generated by ${pkg.name} v${pkg.version} 🍺`,
+ `Do not edit manually.`,
+ ...(title ? [title] : []),
+ ...(description ? [description] : []),
+ ...(version ? [`OpenAPI spec version: ${version}`] : []),
+];
diff --git a/packages/orval/src/utils/package-json.ts b/packages/orval/src/utils/package-json.ts
new file mode 100644
index 000000000..a2b3b1d47
--- /dev/null
+++ b/packages/orval/src/utils/package-json.ts
@@ -0,0 +1,28 @@
+import { PackageJson } from '@orval/core';
+import findUp from 'find-up';
+import { existsSync } from 'fs-extra';
+import { normalizePath } from './options';
+
+export const loadPackageJson = async (
+ packageJson?: string,
+ workspace = process.cwd(),
+): Promise => {
+ if (!packageJson) {
+ const pkgPath = await findUp(['package.json'], {
+ cwd: workspace,
+ });
+ if (pkgPath) {
+ const pkg = await import(pkgPath);
+ return pkg;
+ }
+ return;
+ }
+
+ const normalizedPath = normalizePath(packageJson, workspace);
+ if (existsSync(normalizedPath)) {
+ const pkg = await import(normalizedPath);
+
+ return pkg;
+ }
+ return;
+};
diff --git a/packages/orval/src/utils/request.ts b/packages/orval/src/utils/request.ts
new file mode 100644
index 000000000..1280427be
--- /dev/null
+++ b/packages/orval/src/utils/request.ts
@@ -0,0 +1,38 @@
+import http from 'http';
+import https from 'https';
+
+export type Response = {
+ status: http.IncomingMessage['statusCode'];
+ headers: http.IncomingMessage['headers'];
+ body: T;
+};
+
+export const request = (
+ urlOptions: string | https.RequestOptions | URL,
+ data?: string,
+): Promise> => {
+ return new Promise((resolve, reject) => {
+ const req = https.request(urlOptions, (res) => {
+ let body = '';
+ res.on('data', (chunk) => (body += chunk.toString()));
+ res.on('error', reject);
+ res.on('end', () => {
+ const response = {
+ status: res.statusCode,
+ headers: res.headers,
+ body: JSON.parse(body),
+ };
+ if (res.statusCode && res.statusCode >= 200 && res.statusCode <= 299) {
+ resolve(response);
+ } else {
+ reject(response);
+ }
+ });
+ });
+ req.on('error', reject);
+ if (data) {
+ req.write(data, 'binary');
+ }
+ req.end();
+ });
+};
diff --git a/packages/orval/src/utils/tsconfig.ts b/packages/orval/src/utils/tsconfig.ts
new file mode 100644
index 000000000..4cef226b4
--- /dev/null
+++ b/packages/orval/src/utils/tsconfig.ts
@@ -0,0 +1,41 @@
+import { isObject, isString, isUndefined, Tsconfig } from '@orval/core';
+import findUp from 'find-up';
+import { existsSync } from 'fs-extra';
+import { parse } from 'tsconfck';
+import { normalizePath } from './options';
+
+export const loadTsconfig = async (
+ tsconfig?: Tsconfig | string,
+ workspace = process.cwd(),
+): Promise => {
+ if (isUndefined(tsconfig)) {
+ const configPath = await findUp(['tsconfig.json', 'jsconfig.json'], {
+ cwd: workspace,
+ });
+ if (configPath) {
+ const config = await parse(configPath);
+ return config.tsconfig;
+ }
+ return;
+ }
+
+ if (isString(tsconfig)) {
+ const normalizedPath = normalizePath(tsconfig, workspace);
+ if (existsSync(normalizedPath)) {
+ const config = await parse(normalizedPath);
+
+ const tsconfig =
+ config.referenced?.find(
+ ({ tsconfigFile }) => tsconfigFile === normalizedPath,
+ )?.tsconfig || config.tsconfig;
+
+ return tsconfig;
+ }
+ return;
+ }
+
+ if (isObject(tsconfig)) {
+ return tsconfig;
+ }
+ return;
+};
diff --git a/packages/orval/src/utils/watcher.ts b/packages/orval/src/utils/watcher.ts
new file mode 100644
index 000000000..43e28ef55
--- /dev/null
+++ b/packages/orval/src/utils/watcher.ts
@@ -0,0 +1,42 @@
+import { log } from '@orval/core';
+import chalk from 'chalk';
+
+export const startWatcher = async (
+ watchOptions: boolean | string | (string | boolean)[],
+ watchFn: () => Promise,
+ defaultTarget: string | string[] = '.',
+) => {
+ if (!watchOptions) return;
+ const { watch } = await import('chokidar');
+
+ const ignored = ['**/{.git,node_modules}/**'];
+
+ const watchPaths =
+ typeof watchOptions === 'boolean'
+ ? defaultTarget
+ : Array.isArray(watchOptions)
+ ? watchOptions.filter((path): path is string => typeof path === 'string')
+ : watchOptions;
+
+ log(
+ `Watching for changes in ${
+ Array.isArray(watchPaths)
+ ? watchPaths.map((v) => '"' + v + '"').join(' | ')
+ : '"' + watchPaths + '"'
+ }`,
+ );
+
+ const watcher = watch(watchPaths, {
+ ignorePermissionErrors: true,
+ ignored,
+ });
+ watcher.on('all', async (type, file) => {
+ log(`Change detected: ${type} ${file}`);
+
+ try {
+ await watchFn();
+ } catch (e) {
+ log(chalk.red(e));
+ }
+ });
+};
diff --git a/packages/orval/src/write-specs.ts b/packages/orval/src/write-specs.ts
new file mode 100644
index 000000000..358092032
--- /dev/null
+++ b/packages/orval/src/write-specs.ts
@@ -0,0 +1,171 @@
+import {
+ createSuccessMessage,
+ getFileInfo,
+ getSpecName,
+ isRootKey,
+ jsDoc,
+ log,
+ NormalizedOptions,
+ OutputMode,
+ relativeSafe,
+ writeSchemas,
+ writeSingleMode,
+ WriteSpecsBuilder,
+ writeSplitMode,
+ writeSplitTagsMode,
+ writeTagsMode,
+} from '@orval/core';
+import chalk from 'chalk';
+import execa from 'execa';
+import { appendFile, outputFile, pathExists, readFile } from 'fs-extra';
+import uniq from 'lodash.uniq';
+import { InfoObject } from 'openapi3-ts';
+import { join } from 'upath';
+import { executeHook } from './utils';
+
+const getHeader = (
+ option: false | ((info: InfoObject) => string | string[]),
+ info: InfoObject,
+): string => {
+ if (!option) {
+ return '';
+ }
+
+ const header = option(info);
+
+ return Array.isArray(header) ? jsDoc({ description: header }) : header;
+};
+
+export const writeSpecs = async (
+ builder: WriteSpecsBuilder,
+ workspace: string,
+ options: NormalizedOptions,
+ projectName?: string,
+) => {
+ const { info, schemas, target } = builder;
+ const { output } = options;
+ const projectTitle = projectName || info.title;
+
+ const specsName = Object.keys(schemas).reduce((acc, specKey) => {
+ const basePath = getSpecName(specKey, target);
+ const name = basePath.slice(1).split('/').join('-');
+
+ acc[specKey] = name;
+
+ return acc;
+ }, {} as Record);
+
+ const header = getHeader(output.override.header, info);
+
+ if (output.schemas) {
+ const rootSchemaPath = output.schemas;
+
+ await Promise.all(
+ Object.entries(schemas).map(([specKey, schemas]) => {
+ const schemaPath = !isRootKey(specKey, target)
+ ? join(rootSchemaPath, specsName[specKey])
+ : rootSchemaPath;
+
+ return writeSchemas({
+ schemaPath,
+ schemas,
+ target,
+ specsName,
+ isRootKey: isRootKey(specKey, target),
+ header,
+ });
+ }),
+ );
+ }
+
+ let implementationPaths: string[] = [];
+
+ if (output.target) {
+ const writeMode = getWriteMode(output.mode);
+ implementationPaths = await writeMode({
+ builder,
+ workspace,
+ output,
+ specsName,
+ header,
+ });
+ }
+
+ if (output.workspace) {
+ const workspacePath = output.workspace;
+ let imports = implementationPaths
+ .filter((path) => !path.endsWith('.msw.ts'))
+ .map((path) =>
+ relativeSafe(workspacePath, getFileInfo(path).pathWithoutExtension),
+ );
+
+ if (output.schemas) {
+ imports.push(
+ relativeSafe(workspacePath, getFileInfo(output.schemas).dirname),
+ );
+ }
+
+ const indexFile = join(workspacePath, '/index.ts');
+
+ if (await pathExists(indexFile)) {
+ const data = await readFile(indexFile, 'utf8');
+ const importsNotDeclared = imports.filter((imp) => !data.includes(imp));
+ await appendFile(
+ indexFile,
+ uniq(importsNotDeclared)
+ .map((imp) => `export * from '${imp}';`)
+ .join('\n') + '\n',
+ );
+ } else {
+ await outputFile(
+ indexFile,
+ uniq(imports)
+ .map((imp) => `export * from '${imp}';`)
+ .join('\n') + '\n',
+ );
+ }
+
+ implementationPaths = [indexFile, ...implementationPaths];
+ }
+
+ const paths = [
+ ...(output.schemas ? [getFileInfo(output.schemas).dirname] : []),
+ ...implementationPaths,
+ ];
+
+ if (options.hooks.afterAllFilesWrite) {
+ await executeHook(
+ 'afterAllFilesWrite',
+ options.hooks.afterAllFilesWrite,
+ paths,
+ );
+ }
+
+ if (output.prettier) {
+ try {
+ await execa('prettier', ['--write', ...paths]);
+ } catch (e) {
+ log(
+ chalk.yellow(
+ `⚠️ ${projectTitle ? `${projectTitle} - ` : ''}Prettier not found`,
+ ),
+ );
+ }
+ }
+
+ createSuccessMessage(projectTitle);
+};
+
+const getWriteMode = (mode: OutputMode) => {
+ switch (mode) {
+ case OutputMode.SPLIT:
+ return writeSplitMode;
+ case OutputMode.TAGS:
+ return writeTagsMode;
+ case OutputMode.TAGS_SPLIT:
+ return writeSplitTagsMode;
+ case OutputMode.SINGLE:
+ default:
+ return writeSingleMode;
+ }
+};
diff --git a/packages/orval/tsconfig.json b/packages/orval/tsconfig.json
new file mode 100644
index 000000000..9e25e6ece
--- /dev/null
+++ b/packages/orval/tsconfig.json
@@ -0,0 +1,4 @@
+{
+ "extends": "../../tsconfig.base.json",
+ "include": ["src/**/*.ts"]
+}
diff --git a/packages/query/README.md b/packages/query/README.md
new file mode 100644
index 000000000..8e1596ee8
--- /dev/null
+++ b/packages/query/README.md
@@ -0,0 +1,28 @@
+[![npm version](https://badge.fury.io/js/orval.svg)](https://badge.fury.io/js/orval)
+[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
+[![tests](https://github.com/anymaniax/orval/actions/workflows/tests.yaml/badge.svg)](https://github.com/anymaniax/orval/actions/workflows/tests.yaml)
+
+
+
+
+
+ Visit orval.dev for docs, guides, API and beer!
+
+
+### Code Generation
+
+`orval` is able to generate client with appropriate type-signatures (TypeScript) from any valid OpenAPI v3 or Swagger v2 specification, either in `yaml` or `json` formats.
+
+`Generate`, `valid`, `cache` and `mock` in your React, Vue, Svelte and Angular applications all with your OpenAPI specification.
+
+### Samples
+
+You can find below some samples
+
+- [react app](https://github.com/anymaniax/orval/tree/master/samples/react-app)
+- [react query](https://github.com/anymaniax/orval/tree/master/samples/react-query)
+- [svelte query](https://github.com/anymaniax/orval/tree/master/samples/svelte-query)
+- [vue query](https://github.com/anymaniax/orval/tree/master/samples/vue-query)
+- [react app with swr](https://github.com/anymaniax/orval/tree/master/samples/react-app-with-swr)
+- [nx fastify react](https://github.com/anymaniax/orval/tree/master/samples/nx-fastify-react)
+- [angular app](https://github.com/anymaniax/orval/tree/master/samples/angular-app)
diff --git a/packages/query/package.json b/packages/query/package.json
new file mode 100644
index 000000000..b2bdc2cea
--- /dev/null
+++ b/packages/query/package.json
@@ -0,0 +1,22 @@
+{
+ "name": "@orval/query",
+ "version": "6.11.0-alpha.1",
+ "license": "MIT",
+ "main": "./dist/index.js",
+ "types": "./dist/index.d.ts",
+ "files": [
+ "dist"
+ ],
+ "scripts": {
+ "build": "tsup ./src/index.ts --target node12 --minify --clean --dts --splitting",
+ "dev": "tsup ./src/index.ts --target node12 --clean --watch src",
+ "lint": "eslint src/**/*.ts"
+ },
+ "dependencies": {
+ "@orval/core": "6.11.0-alpha.1",
+ "lodash.omitby": "^4.6.0"
+ },
+ "devDependencies": {
+ "@types/lodash.omitby": "^4.6.7"
+ }
+}
diff --git a/packages/query/src/index.ts b/packages/query/src/index.ts
new file mode 100644
index 000000000..c2a62a5ee
--- /dev/null
+++ b/packages/query/src/index.ts
@@ -0,0 +1,945 @@
+import {
+ camel,
+ ClientBuilder,
+ ClientDependenciesBuilder,
+ ClientFooterBuilder,
+ ClientGeneratorsBuilder,
+ ClientHeaderBuilder,
+ ClientTitleBuilder,
+ generateFormDataAndUrlEncodedFunction,
+ generateMutatorConfig,
+ generateMutatorRequestOptions,
+ generateOptions,
+ generateVerbImports,
+ GeneratorDependency,
+ GeneratorMutator,
+ GeneratorOptions,
+ GeneratorVerbOptions,
+ GetterParams,
+ GetterProps,
+ GetterPropType,
+ GetterResponse,
+ isObject,
+ isSyntheticDefaultImportsAllow,
+ OutputClient,
+ OutputClientFunc,
+ pascal,
+ stringify,
+ toObjectString,
+ Verbs,
+ VERBS_WITH_BODY,
+} from '@orval/core';
+import omitBy from 'lodash.omitby';
+
+const AXIOS_DEPENDENCIES: GeneratorDependency[] = [
+ {
+ exports: [
+ {
+ name: 'axios',
+ default: true,
+ values: true,
+ syntheticDefaultImport: true,
+ },
+ { name: 'AxiosRequestConfig' },
+ { name: 'AxiosResponse' },
+ { name: 'AxiosError' },
+ ],
+ dependency: 'axios',
+ },
+];
+
+const SVELTE_QUERY_DEPENDENCIES: GeneratorDependency[] = [
+ {
+ exports: [
+ { name: 'useQuery', values: true },
+ { name: 'useInfiniteQuery', values: true },
+ { name: 'useMutation', values: true },
+ { name: 'UseQueryOptions' },
+ {
+ name: 'UseInfiniteQueryOptions',
+ },
+ { name: 'UseMutationOptions' },
+ { name: 'QueryFunction' },
+ { name: 'MutationFunction' },
+ { name: 'UseQueryStoreResult' },
+ { name: 'UseInfiniteQueryStoreResult' },
+ { name: 'QueryKey' },
+ ],
+ dependency: '@sveltestack/svelte-query',
+ },
+];
+
+export const getSvelteQueryDependencies: ClientDependenciesBuilder = (
+ hasGlobalMutator: boolean,
+) => [
+ ...(!hasGlobalMutator ? AXIOS_DEPENDENCIES : []),
+ ...SVELTE_QUERY_DEPENDENCIES,
+];
+
+const REACT_QUERY_DEPENDENCIES: GeneratorDependency[] = [
+ {
+ exports: [
+ { name: 'useQuery', values: true },
+ { name: 'useInfiniteQuery', values: true },
+ { name: 'useMutation', values: true },
+ { name: 'UseQueryOptions' },
+ { name: 'UseInfiniteQueryOptions' },
+ { name: 'UseMutationOptions' },
+ { name: 'QueryFunction' },
+ { name: 'MutationFunction' },
+ { name: 'UseQueryResult' },
+ { name: 'UseInfiniteQueryResult' },
+ { name: 'QueryKey' },
+ ],
+ dependency: 'react-query',
+ },
+];
+const REACT_QUERY_V4_DEPENDENCIES: GeneratorDependency[] = [
+ {
+ exports: [
+ { name: 'useQuery', values: true },
+ { name: 'useInfiniteQuery', values: true },
+ { name: 'useMutation', values: true },
+ { name: 'UseQueryOptions' },
+ { name: 'UseInfiniteQueryOptions' },
+ { name: 'UseMutationOptions' },
+ { name: 'QueryFunction' },
+ { name: 'MutationFunction' },
+ { name: 'UseQueryResult' },
+ { name: 'UseInfiniteQueryResult' },
+ { name: 'QueryKey' },
+ ],
+ dependency: '@tanstack/react-query',
+ },
+];
+
+export const getReactQueryDependencies: ClientDependenciesBuilder = (
+ hasGlobalMutator,
+ packageJson,
+) => {
+ const hasReactQuery =
+ packageJson?.dependencies?.['react-query'] ??
+ packageJson?.devDependencies?.['react-query'];
+ const hasReactQueryV4 =
+ packageJson?.dependencies?.['@tanstack/react-query'] ??
+ packageJson?.devDependencies?.['@tanstack/react-query'];
+
+ return [
+ ...(!hasGlobalMutator ? AXIOS_DEPENDENCIES : []),
+ ...(hasReactQuery && !hasReactQueryV4
+ ? REACT_QUERY_DEPENDENCIES
+ : REACT_QUERY_V4_DEPENDENCIES),
+ ];
+};
+
+const VUE_QUERY_DEPENDENCIES: GeneratorDependency[] = [
+ {
+ exports: [
+ { name: 'useQuery', values: true },
+ { name: 'useInfiniteQuery', values: true },
+ { name: 'useMutation', values: true },
+ ],
+ dependency: 'vue-query',
+ },
+ {
+ exports: [
+ { name: 'UseQueryOptions' },
+ { name: 'UseInfiniteQueryOptions' },
+ { name: 'UseMutationOptions' },
+ { name: 'QueryFunction' },
+ { name: 'MutationFunction' },
+ { name: 'UseQueryResult' },
+ { name: 'UseInfiniteQueryResult' },
+ { name: 'QueryKey' },
+ ],
+ dependency: 'vue-query/types',
+ },
+ {
+ exports: [{ name: 'UseQueryReturnType' }],
+ dependency: 'vue-query/lib/vue/useBaseQuery',
+ },
+];
+
+export const getVueQueryDependencies: ClientDependenciesBuilder = (
+ hasGlobalMutator: boolean,
+) => [
+ ...(!hasGlobalMutator ? AXIOS_DEPENDENCIES : []),
+ ...VUE_QUERY_DEPENDENCIES,
+];
+
+const generateRequestOptionsArguments = ({
+ isRequestOptions,
+ hasSignal,
+}: {
+ isRequestOptions: boolean;
+ hasSignal: boolean;
+}) => {
+ if (isRequestOptions) {
+ return 'options?: AxiosRequestConfig\n';
+ }
+
+ return hasSignal ? 'signal?: AbortSignal\n' : '';
+};
+
+const generateQueryRequestFunction = (
+ {
+ headers,
+ queryParams,
+ operationName,
+ response,
+ mutator,
+ body,
+ props,
+ verb,
+ formData,
+ formUrlEncoded,
+ override,
+ }: GeneratorVerbOptions,
+ { route, context }: GeneratorOptions,
+) => {
+ const isRequestOptions = override.requestOptions !== false;
+ const isFormData = override.formData !== false;
+ const isFormUrlEncoded = override.formUrlEncoded !== false;
+ const hasSignal = !!override.query.signal;
+
+ const isSyntheticDefaultImportsAllowed = isSyntheticDefaultImportsAllow(
+ context.tsconfig,
+ );
+ const isExactOptionalPropertyTypes =
+ !!context.tsconfig?.compilerOptions?.exactOptionalPropertyTypes;
+ const isBodyVerb = VERBS_WITH_BODY.includes(verb);
+
+ const bodyForm = generateFormDataAndUrlEncodedFunction({
+ formData,
+ formUrlEncoded,
+ body,
+ isFormData,
+ isFormUrlEncoded,
+ });
+
+ if (mutator) {
+ const mutatorConfig = generateMutatorConfig({
+ route,
+ body,
+ headers,
+ queryParams,
+ response,
+ verb,
+ isFormData,
+ isFormUrlEncoded,
+ isBodyVerb,
+ hasSignal,
+ isExactOptionalPropertyTypes,
+ });
+
+ const propsImplementation =
+ mutator?.bodyTypeName && body.definition
+ ? toObjectString(props, 'implementation').replace(
+ new RegExp(`(\\w*):\\s?${body.definition}`),
+ `$1: ${mutator.bodyTypeName}<${body.definition}>`,
+ )
+ : toObjectString(props, 'implementation');
+
+ const requestOptions = isRequestOptions
+ ? generateMutatorRequestOptions(
+ override.requestOptions,
+ mutator.hasSecondArg,
+ )
+ : '';
+
+ if (mutator.isHook) {
+ return `export const use${pascal(operationName)}Hook = () => {
+ const ${operationName} = ${mutator.name}<${
+ response.definition.success || 'unknown'
+ }>();
+
+ return (\n ${propsImplementation}\n ${
+ isRequestOptions && mutator.hasSecondArg
+ ? `options?: SecondParameter>,`
+ : ''
+ }${
+ !isBodyVerb && hasSignal ? 'signal?: AbortSignal\n' : ''
+ }) => {${bodyForm}
+ return ${operationName}(
+ ${mutatorConfig},
+ ${requestOptions});
+ }
+ }
+ `;
+ }
+
+ return `export const ${operationName} = (\n ${propsImplementation}\n ${
+ isRequestOptions && mutator.hasSecondArg
+ ? `options?: SecondParameter,`
+ : ''
+ }${
+ !isBodyVerb && hasSignal ? 'signal?: AbortSignal\n' : ''
+ }) => {${bodyForm}
+ return ${mutator.name}<${response.definition.success || 'unknown'}>(
+ ${mutatorConfig},
+ ${requestOptions});
+ }
+ `;
+ }
+
+ const options = generateOptions({
+ route,
+ body,
+ headers,
+ queryParams,
+ response,
+ verb,
+ requestOptions: override?.requestOptions,
+ isFormData,
+ isFormUrlEncoded,
+ isExactOptionalPropertyTypes,
+ hasSignal,
+ });
+
+ const optionsArgs = generateRequestOptionsArguments({
+ isRequestOptions,
+ hasSignal,
+ });
+
+ return `export const ${operationName} = (\n ${toObjectString(
+ props,
+ 'implementation',
+ )} ${optionsArgs} ): Promise> => {${bodyForm}
+ return axios${
+ !isSyntheticDefaultImportsAllowed ? '.default' : ''
+ }.${verb}(${options});
+ }
+`;
+};
+
+type QueryType = 'infiniteQuery' | 'query';
+
+const QueryType = {
+ INFINITE: 'infiniteQuery' as QueryType,
+ QUERY: 'query' as QueryType,
+};
+
+const INFINITE_QUERY_PROPERTIES = ['getNextPageParam', 'getPreviousPageParam'];
+
+const generateQueryOptions = ({
+ params,
+ options,
+ type,
+}: {
+ params: GetterParams;
+ options?: object | boolean;
+ type: QueryType;
+}) => {
+ if (options === false) {
+ return '';
+ }
+
+ const queryConfig = isObject(options)
+ ? ` ${stringify(
+ omitBy(options, (_, key) => {
+ if (
+ type !== QueryType.INFINITE &&
+ INFINITE_QUERY_PROPERTIES.includes(key)
+ ) {
+ return true;
+ }
+ return false;
+ }),
+ )?.slice(1, -1)}`
+ : '';
+
+ if (!params.length) {
+ if (options) {
+ return `{${queryConfig} ...queryOptions}`;
+ }
+
+ return 'queryOptions';
+ }
+
+ return `{${
+ !isObject(options) || !options.hasOwnProperty('enabled')
+ ? `enabled: !!(${params.map(({ name }) => name).join(' && ')}),`
+ : ''
+ }${queryConfig} ...queryOptions}`;
+};
+
+const getQueryArgumentsRequestType = (mutator?: GeneratorMutator) => {
+ if (!mutator) {
+ return `axios?: AxiosRequestConfig`;
+ }
+
+ if (mutator.hasSecondArg && !mutator.isHook) {
+ return `request?: SecondParameter`;
+ }
+
+ if (mutator.hasSecondArg && mutator.isHook) {
+ return `request?: SecondParameter>`;
+ }
+
+ return '';
+};
+
+const generateQueryArguments = ({
+ operationName,
+ definitions,
+ mutator,
+ isRequestOptions,
+ type,
+}: {
+ operationName: string;
+ definitions: string;
+ mutator?: GeneratorMutator;
+ isRequestOptions: boolean;
+ type?: QueryType;
+}) => {
+ const isMutatorHook = mutator?.isHook;
+ const definition = type
+ ? `Use${pascal(type)}Options`
+ : `typeof ${operationName}`
+ }>>, TError, TData>`
+ : `UseMutationOptions`
+ : `typeof ${operationName}`
+ }>>, TError,${
+ definitions ? `{${definitions}}` : 'TVariables'
+ }, TContext>`;
+
+ if (!isRequestOptions) {
+ return `${type ? 'queryOptions' : 'mutationOptions'}?: ${definition}`;
+ }
+
+ const requestType = getQueryArgumentsRequestType(mutator);
+
+ return `options?: { ${
+ type ? 'query' : 'mutation'
+ }?:${definition}, ${requestType}}\n`;
+};
+
+const generateQueryReturnType = ({
+ outputClient,
+ type,
+ isMutatorHook,
+ operationName,
+}: {
+ outputClient: OutputClient | OutputClientFunc;
+ type: QueryType;
+ isMutatorHook?: boolean;
+ operationName: string;
+}) => {
+ switch (outputClient) {
+ case OutputClient.SVELTE_QUERY:
+ return `Use${pascal(type)}StoreResult`
+ : `typeof ${operationName}`
+ }>>, TError, TData, QueryKey>`;
+ case OutputClient.VUE_QUERY:
+ return ` UseQueryReturnType>`;
+ case OutputClient.REACT_QUERY:
+ default:
+ return ` Use${pascal(type)}Result`;
+ }
+};
+
+const getQueryOptions = ({
+ isRequestOptions,
+ mutator,
+ isExactOptionalPropertyTypes,
+ hasSignal,
+}: {
+ isRequestOptions: boolean;
+ mutator?: GeneratorMutator;
+ isExactOptionalPropertyTypes: boolean;
+ hasSignal: boolean;
+}) => {
+ if (!mutator && isRequestOptions) {
+ if (!hasSignal) {
+ return 'axiosOptions';
+ }
+ return `{ ${
+ isExactOptionalPropertyTypes ? '...(signal ? { signal } : {})' : 'signal'
+ }, ...axiosOptions }`;
+ }
+
+ if (mutator?.hasSecondArg && isRequestOptions) {
+ if (!hasSignal) {
+ return 'requestOptions';
+ }
+
+ return 'requestOptions, signal';
+ }
+
+ if (hasSignal) {
+ return 'signal';
+ }
+
+ return '';
+};
+
+const getHookOptions = ({
+ isRequestOptions,
+ mutator,
+}: {
+ isRequestOptions: boolean;
+ mutator?: GeneratorMutator;
+}) => {
+ if (!isRequestOptions) {
+ return '';
+ }
+
+ let value = 'const {query: queryOptions';
+
+ if (!mutator) {
+ value += ', axios: axiosOptions';
+ }
+
+ if (mutator?.hasSecondArg) {
+ value += ', request: requestOptions';
+ }
+
+ value += '} = options ?? {};';
+
+ return value;
+};
+
+const getQueryFnArguments = ({
+ hasQueryParam,
+ hasSignal,
+}: {
+ hasQueryParam: boolean;
+ hasSignal: boolean;
+}) => {
+ if (!hasQueryParam && !hasSignal) {
+ return '';
+ }
+
+ if (hasQueryParam) {
+ if (hasSignal) {
+ return '{ signal, pageParam }';
+ }
+
+ return '{ pageParam }';
+ }
+
+ return '{ signal }';
+};
+
+const generateQueryImplementation = ({
+ queryOption: { name, queryParam, options, type },
+ operationName,
+ queryKeyFnName,
+ queryProperties,
+ queryKeyProperties,
+ params,
+ props,
+ mutator,
+ isRequestOptions,
+ response,
+ outputClient,
+ isExactOptionalPropertyTypes,
+ hasSignal,
+}: {
+ queryOption: {
+ name: string;
+ options?: object | boolean;
+ type: QueryType;
+ queryParam?: string;
+ };
+ isRequestOptions: boolean;
+ operationName: string;
+ queryKeyFnName: string;
+ queryProperties: string;
+ queryKeyProperties: string;
+ params: GetterParams;
+ props: GetterProps;
+ response: GetterResponse;
+ mutator?: GeneratorMutator;
+ outputClient: OutputClient | OutputClientFunc;
+ isExactOptionalPropertyTypes: boolean;
+ hasSignal: boolean;
+}) => {
+ const queryProps = toObjectString(props, 'implementation');
+ const httpFunctionProps = queryParam
+ ? props
+ .map(({ name }) =>
+ name === 'params' ? `{ ${queryParam}: pageParam, ...params }` : name,
+ )
+ .join(',')
+ : queryProperties;
+
+ const returnType = generateQueryReturnType({
+ outputClient,
+ type,
+ isMutatorHook: mutator?.isHook,
+ operationName,
+ });
+
+ let errorType = `AxiosError<${response.definition.errors || 'unknown'}>`;
+
+ if (mutator) {
+ errorType = mutator.hasErrorType
+ ? `${mutator.default ? pascal(operationName) : ''}ErrorType<${
+ response.definition.errors || 'unknown'
+ }>`
+ : response.definition.errors || 'unknown';
+ }
+
+ const dataType = mutator?.isHook
+ ? `ReturnType`
+ : `typeof ${operationName}`;
+
+ const queryArguments = generateQueryArguments({
+ operationName,
+ definitions: '',
+ mutator,
+ isRequestOptions,
+ type,
+ });
+
+ const queryOptions = getQueryOptions({
+ isRequestOptions,
+ isExactOptionalPropertyTypes,
+ mutator,
+ hasSignal,
+ });
+
+ const hookOptions = getHookOptions({
+ isRequestOptions,
+ mutator,
+ });
+
+ const queryFnArguments = getQueryFnArguments({
+ hasQueryParam:
+ !!queryParam && props.some(({ type }) => type === 'queryParam'),
+ hasSignal,
+ });
+
+ return `
+export type ${pascal(
+ name,
+ )}QueryResult = NonNullable>>
+export type ${pascal(name)}QueryError = ${errorType}
+
+export const ${camel(
+ `use-${name}`,
+ )} = >, TError = ${errorType}>(\n ${queryProps} ${queryArguments}\n ): ${returnType} & { queryKey: QueryKey } => {
+
+ ${hookOptions}
+
+ const queryKey = queryOptions?.queryKey ?? ${queryKeyFnName}(${queryKeyProperties});
+
+ ${
+ mutator?.isHook
+ ? `const ${operationName} = use${pascal(operationName)}Hook();`
+ : ''
+ }
+
+ const queryFn: QueryFunction`
+ : `typeof ${operationName}`
+ }>>> = (${queryFnArguments}) => ${operationName}(${httpFunctionProps}${
+ httpFunctionProps ? ', ' : ''
+ }${queryOptions});
+
+ const query = ${camel(`use-${type}`)}`
+ : `typeof ${operationName}`
+ }>>, TError, TData>(queryKey, queryFn, ${generateQueryOptions({
+ params,
+ options,
+ type,
+ })}) as ${returnType} & { queryKey: QueryKey };
+
+ query.queryKey = queryKey;
+
+ return query;
+}\n`;
+};
+
+const generateQueryHook = (
+ {
+ queryParams,
+ operationName,
+ body,
+ props,
+ verb,
+ params,
+ override,
+ mutator,
+ response,
+ operationId,
+ }: GeneratorVerbOptions,
+ { route, override: { operations = {} }, context }: GeneratorOptions,
+ outputClient: OutputClient | OutputClientFunc,
+) => {
+ const query = override?.query;
+ const isRequestOptions = override?.requestOptions !== false;
+ const operationQueryOptions = operations[operationId]?.query;
+ const isExactOptionalPropertyTypes =
+ !!context.tsconfig?.compilerOptions?.exactOptionalPropertyTypes;
+
+ if (
+ verb === Verbs.GET ||
+ operationQueryOptions?.useInfinite ||
+ operationQueryOptions?.useQuery
+ ) {
+ const queryProperties = props
+ .map(({ name, type }) =>
+ type === GetterPropType.BODY ? body.implementation : name,
+ )
+ .join(',');
+
+ const queryKeyProperties = props
+ .filter((prop) => prop.type !== GetterPropType.HEADER)
+ .map(({ name, type }) =>
+ type === GetterPropType.BODY ? body.implementation : name,
+ )
+ .join(',');
+
+ const queries = [
+ ...(query?.useInfinite
+ ? [
+ {
+ name: camel(`${operationName}-infinite`),
+ options: query?.options,
+ type: QueryType.INFINITE,
+ queryParam: query?.useInfiniteQueryParam,
+ },
+ ]
+ : []),
+ ...((!query?.useQuery && !query?.useInfinite) || query?.useQuery
+ ? [
+ {
+ name: operationName,
+ options: query?.options,
+ type: QueryType.QUERY,
+ },
+ ]
+ : []),
+ ];
+
+ const queryKeyFnName = camel(`get-${operationName}-queryKey`);
+ const queryKeyProps = toObjectString(
+ props.filter((prop) => prop.type !== GetterPropType.HEADER),
+ 'implementation',
+ );
+
+ return `export const ${queryKeyFnName} = (${queryKeyProps}) => [\`${route}\`${
+ queryParams ? ', ...(params ? [params]: [])' : ''
+ }${body.implementation ? `, ${body.implementation}` : ''}];
+
+ ${queries.reduce(
+ (acc, queryOption) =>
+ acc +
+ generateQueryImplementation({
+ queryOption,
+ operationName,
+ queryKeyFnName,
+ queryProperties,
+ queryKeyProperties,
+ params,
+ props,
+ mutator,
+ isRequestOptions,
+ response,
+ outputClient,
+ isExactOptionalPropertyTypes,
+ hasSignal: !!query.signal,
+ }),
+ '',
+ )}
+`;
+ }
+
+ const definitions = props
+ .map(({ definition, type }) =>
+ type === GetterPropType.BODY
+ ? mutator?.bodyTypeName
+ ? `data: ${mutator.bodyTypeName}<${body.definition}>`
+ : `data: ${body.definition}`
+ : definition,
+ )
+ .join(';');
+
+ const properties = props
+ .map(({ name, type }) => (type === GetterPropType.BODY ? 'data' : name))
+ .join(',');
+
+ let errorType = `AxiosError<${response.definition.errors || 'unknown'}>`;
+
+ if (mutator) {
+ errorType = mutator.hasErrorType
+ ? `${mutator.default ? pascal(operationName) : ''}ErrorType<${
+ response.definition.errors || 'unknown'
+ }>`
+ : response.definition.errors || 'unknown';
+ }
+
+ const dataType = mutator?.isHook
+ ? `ReturnType`
+ : `typeof ${operationName}`;
+
+ return `
+ export type ${pascal(
+ operationName,
+ )}MutationResult = NonNullable>>
+ ${
+ body.definition
+ ? `export type ${pascal(operationName)}MutationBody = ${
+ mutator?.bodyTypeName
+ ? `${mutator.bodyTypeName}<${body.definition}>`
+ : body.definition
+ }`
+ : ''
+ }
+ export type ${pascal(operationName)}MutationError = ${errorType}
+
+ export const ${camel(`use-${operationName}`)} = (${generateQueryArguments({
+ operationName,
+ definitions,
+ mutator,
+ isRequestOptions,
+ })}) => {
+ ${
+ isRequestOptions
+ ? `const {mutation: mutationOptions${
+ !mutator
+ ? `, axios: axiosOptions`
+ : mutator?.hasSecondArg
+ ? ', request: requestOptions'
+ : ''
+ }} = options ?? {};`
+ : ''
+ }
+
+ ${
+ mutator?.isHook
+ ? `const ${operationName} = use${pascal(operationName)}Hook()`
+ : ''
+ }
+
+
+ const mutationFn: MutationFunction>, ${
+ definitions ? `{${definitions}}` : 'TVariables'
+ }> = (${properties ? 'props' : ''}) => {
+ ${properties ? `const {${properties}} = props ?? {};` : ''}
+
+ return ${operationName}(${properties}${properties ? ',' : ''}${
+ isRequestOptions
+ ? !mutator
+ ? `axiosOptions`
+ : mutator?.hasSecondArg
+ ? 'requestOptions'
+ : ''
+ : ''
+ })
+ }
+
+ return useMutation>, TError, ${
+ definitions ? `{${definitions}}` : 'TVariables'
+ }, TContext>(mutationFn, mutationOptions)
+ }
+ `;
+};
+
+export const generateQueryTitle: ClientTitleBuilder = () => '';
+
+export const generateQueryHeader: ClientHeaderBuilder = ({
+ isRequestOptions,
+ isMutator,
+ hasAwaitedType,
+}) => {
+ return `${
+ !hasAwaitedType
+ ? `type AwaitedInput = PromiseLike | T;\n
+ type Awaited = O extends AwaitedInput ? T : never;\n\n`
+ : ''
+ }
+${
+ isRequestOptions && isMutator
+ ? `// eslint-disable-next-line
+ type SecondParameter any> = T extends (
+ config: any,
+ args: infer P,
+) => any
+ ? P
+ : never;\n\n`
+ : ''
+}`;
+};
+
+export const generateQueryFooter: ClientFooterBuilder = () => '';
+
+export const generateQuery: ClientBuilder = (
+ verbOptions,
+ options,
+ outputClient,
+) => {
+ const imports = generateVerbImports(verbOptions);
+ const functionImplementation = generateQueryRequestFunction(
+ verbOptions,
+ options,
+ );
+ const hookImplementation = generateQueryHook(
+ verbOptions,
+ options,
+ outputClient,
+ );
+
+ return {
+ implementation: `${functionImplementation}\n\n${hookImplementation}`,
+ imports,
+ };
+};
+
+const reactQueryClientBuilder: ClientGeneratorsBuilder = {
+ client: generateQuery,
+ header: generateQueryHeader,
+ dependencies: getReactQueryDependencies,
+ footer: generateQueryFooter,
+ title: generateQueryTitle,
+};
+
+const svelteQueryClientBuilder: ClientGeneratorsBuilder = {
+ client: generateQuery,
+ header: generateQueryHeader,
+ dependencies: getSvelteQueryDependencies,
+ footer: generateQueryFooter,
+ title: generateQueryTitle,
+};
+
+const vueQueryClientBuilder: ClientGeneratorsBuilder = {
+ client: generateQuery,
+ header: generateQueryHeader,
+ dependencies: getVueQueryDependencies,
+ footer: generateQueryFooter,
+ title: generateQueryTitle,
+};
+
+export const builder = ({
+ type,
+}: {
+ type: 'react-query' | 'vue-query' | 'svelte-query';
+}) => {
+ switch (type) {
+ case 'react-query':
+ return reactQueryClientBuilder;
+ case 'vue-query':
+ return vueQueryClientBuilder;
+ case 'svelte-query':
+ return svelteQueryClientBuilder;
+ }
+};
+
+export default builder;
diff --git a/packages/query/src/types.ts b/packages/query/src/types.ts
new file mode 100644
index 000000000..24e8813b3
--- /dev/null
+++ b/packages/query/src/types.ts
@@ -0,0 +1,7 @@
+export type QueryOptions = {
+ useQuery?: boolean;
+ useInfinite?: boolean;
+ useInfiniteQueryParam?: string;
+ options?: any;
+ signal?: boolean;
+};
diff --git a/packages/query/tsconfig.json b/packages/query/tsconfig.json
new file mode 100644
index 000000000..9e25e6ece
--- /dev/null
+++ b/packages/query/tsconfig.json
@@ -0,0 +1,4 @@
+{
+ "extends": "../../tsconfig.base.json",
+ "include": ["src/**/*.ts"]
+}
diff --git a/packages/swr/README.md b/packages/swr/README.md
new file mode 100644
index 000000000..8e1596ee8
--- /dev/null
+++ b/packages/swr/README.md
@@ -0,0 +1,28 @@
+[![npm version](https://badge.fury.io/js/orval.svg)](https://badge.fury.io/js/orval)
+[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
+[![tests](https://github.com/anymaniax/orval/actions/workflows/tests.yaml/badge.svg)](https://github.com/anymaniax/orval/actions/workflows/tests.yaml)
+
+
+
+
+
+ Visit orval.dev for docs, guides, API and beer!
+
+
+### Code Generation
+
+`orval` is able to generate client with appropriate type-signatures (TypeScript) from any valid OpenAPI v3 or Swagger v2 specification, either in `yaml` or `json` formats.
+
+`Generate`, `valid`, `cache` and `mock` in your React, Vue, Svelte and Angular applications all with your OpenAPI specification.
+
+### Samples
+
+You can find below some samples
+
+- [react app](https://github.com/anymaniax/orval/tree/master/samples/react-app)
+- [react query](https://github.com/anymaniax/orval/tree/master/samples/react-query)
+- [svelte query](https://github.com/anymaniax/orval/tree/master/samples/svelte-query)
+- [vue query](https://github.com/anymaniax/orval/tree/master/samples/vue-query)
+- [react app with swr](https://github.com/anymaniax/orval/tree/master/samples/react-app-with-swr)
+- [nx fastify react](https://github.com/anymaniax/orval/tree/master/samples/nx-fastify-react)
+- [angular app](https://github.com/anymaniax/orval/tree/master/samples/angular-app)
diff --git a/packages/swr/package.json b/packages/swr/package.json
new file mode 100644
index 000000000..a7a5cc991
--- /dev/null
+++ b/packages/swr/package.json
@@ -0,0 +1,18 @@
+{
+ "name": "@orval/swr",
+ "version": "6.11.0-alpha.1",
+ "license": "MIT",
+ "main": "./dist/index.js",
+ "types": "./dist/index.d.ts",
+ "files": [
+ "dist"
+ ],
+ "scripts": {
+ "build": "tsup ./src/index.ts --target node12 --minify --clean --dts --splitting",
+ "dev": "tsup ./src/index.ts --target node12 --clean --watch src",
+ "lint": "eslint src/**/*.ts"
+ },
+ "dependencies": {
+ "@orval/core": "6.11.0-alpha.1"
+ }
+}
diff --git a/packages/swr/src/index.ts b/packages/swr/src/index.ts
new file mode 100644
index 000000000..40bfc7894
--- /dev/null
+++ b/packages/swr/src/index.ts
@@ -0,0 +1,397 @@
+import {
+ camel,
+ ClientBuilder,
+ ClientDependenciesBuilder,
+ ClientFooterBuilder,
+ ClientGeneratorsBuilder,
+ ClientHeaderBuilder,
+ ClientTitleBuilder,
+ generateFormDataAndUrlEncodedFunction,
+ generateMutatorConfig,
+ generateMutatorRequestOptions,
+ generateOptions,
+ generateVerbImports,
+ GeneratorDependency,
+ GeneratorMutator,
+ GeneratorOptions,
+ GeneratorVerbOptions,
+ GetterParams,
+ GetterProps,
+ GetterPropType,
+ GetterResponse,
+ isSyntheticDefaultImportsAllow,
+ pascal,
+ stringify,
+ toObjectString,
+ Verbs,
+ VERBS_WITH_BODY,
+} from '@orval/core';
+
+const AXIOS_DEPENDENCIES: GeneratorDependency[] = [
+ {
+ exports: [
+ {
+ name: 'axios',
+ default: true,
+ values: true,
+ syntheticDefaultImport: true,
+ },
+ { name: 'AxiosRequestConfig' },
+ { name: 'AxiosResponse' },
+ { name: 'AxiosError' },
+ ],
+ dependency: 'axios',
+ },
+];
+
+const SWR_DEPENDENCIES: GeneratorDependency[] = [
+ {
+ exports: [
+ { name: 'useSwr', values: true, default: true },
+ { name: 'SWRConfiguration' },
+ { name: 'Key' },
+ ],
+ dependency: 'swr',
+ },
+];
+
+export const getSwrDependencies: ClientDependenciesBuilder = (
+ hasGlobalMutator: boolean,
+) => [...(!hasGlobalMutator ? AXIOS_DEPENDENCIES : []), ...SWR_DEPENDENCIES];
+
+const generateSwrRequestFunction = (
+ {
+ headers,
+ queryParams,
+ operationName,
+ response,
+ mutator,
+ body,
+ props,
+ verb,
+ formData,
+ formUrlEncoded,
+ override,
+ }: GeneratorVerbOptions,
+ { route, context }: GeneratorOptions,
+) => {
+ const isRequestOptions = override?.requestOptions !== false;
+ const isFormData = override?.formData !== false;
+ const isFormUrlEncoded = override?.formUrlEncoded !== false;
+ const isExactOptionalPropertyTypes =
+ !!context.tsconfig?.compilerOptions?.exactOptionalPropertyTypes;
+ const isBodyVerb = VERBS_WITH_BODY.includes(verb);
+ const isSyntheticDefaultImportsAllowed = isSyntheticDefaultImportsAllow(
+ context.tsconfig,
+ );
+
+ const bodyForm = generateFormDataAndUrlEncodedFunction({
+ formData,
+ formUrlEncoded,
+ body,
+ isFormData,
+ isFormUrlEncoded,
+ });
+
+ if (mutator) {
+ const mutatorConfig = generateMutatorConfig({
+ route,
+ body,
+ headers,
+ queryParams,
+ response,
+ verb,
+ isFormData,
+ isFormUrlEncoded,
+ hasSignal: false,
+ isBodyVerb,
+ isExactOptionalPropertyTypes,
+ });
+
+ const propsImplementation =
+ mutator?.bodyTypeName && body.definition
+ ? toObjectString(props, 'implementation').replace(
+ new RegExp(`(\\w*):\\s?${body.definition}`),
+ `$1: ${mutator.bodyTypeName}<${body.definition}>`,
+ )
+ : toObjectString(props, 'implementation');
+
+ const requestOptions = isRequestOptions
+ ? generateMutatorRequestOptions(
+ override?.requestOptions,
+ mutator.hasSecondArg,
+ )
+ : '';
+
+ return `export const ${operationName} = (\n ${propsImplementation}\n ${
+ isRequestOptions && mutator.hasSecondArg
+ ? `options?: SecondParameter`
+ : ''
+ }) => {${bodyForm}
+ return ${mutator.name}<${response.definition.success || 'unknown'}>(
+ ${mutatorConfig},
+ ${requestOptions});
+ }
+ `;
+ }
+
+ const options = generateOptions({
+ route,
+ body,
+ headers,
+ queryParams,
+ response,
+ verb,
+ requestOptions: override?.requestOptions,
+ isFormData,
+ isFormUrlEncoded,
+ isExactOptionalPropertyTypes,
+ hasSignal: false,
+ });
+
+ return `export const ${operationName} = (\n ${toObjectString(
+ props,
+ 'implementation',
+ )} ${
+ isRequestOptions ? `options?: AxiosRequestConfig\n` : ''
+ } ): Promise> => {${bodyForm}
+ return axios${
+ !isSyntheticDefaultImportsAllowed ? '.default' : ''
+ }.${verb}(${options});
+ }
+`;
+};
+
+const generateSwrArguments = ({
+ operationName,
+ mutator,
+ isRequestOptions,
+}: {
+ operationName: string;
+ mutator?: GeneratorMutator;
+ isRequestOptions: boolean;
+}) => {
+ const definition = `SWRConfiguration>, TError> & { swrKey?: Key, enabled?: boolean }`;
+
+ if (!isRequestOptions) {
+ return `swrOptions?: ${definition}`;
+ }
+
+ return `options?: { swr?:${definition}, ${
+ !mutator
+ ? `axios?: AxiosRequestConfig`
+ : mutator?.hasSecondArg
+ ? `request?: SecondParameter`
+ : ''
+ } }\n`;
+};
+
+const generateSwrImplementation = ({
+ operationName,
+ swrKeyFnName,
+ swrProperties,
+ swrKeyProperties,
+ params,
+ mutator,
+ isRequestOptions,
+ response,
+ swrOptions,
+ props,
+}: {
+ isRequestOptions: boolean;
+ operationName: string;
+ swrKeyFnName: string;
+ swrProperties: string;
+ swrKeyProperties: string;
+ params: GetterParams;
+ props: GetterProps;
+ response: GetterResponse;
+ mutator?: GeneratorMutator;
+ swrOptions: { options?: any };
+}) => {
+ const swrProps = toObjectString(props, 'implementation');
+ const httpFunctionProps = swrProperties;
+
+ const swrKeyImplementation = `const isEnabled = swrOptions?.enabled !== false${
+ params.length
+ ? ` && !!(${params.map(({ name }) => name).join(' && ')})`
+ : ''
+ }
+ const swrKey = swrOptions?.swrKey ?? (() => isEnabled ? ${swrKeyFnName}(${swrKeyProperties}) : null);`;
+
+ let errorType = `AxiosError<${response.definition.errors || 'unknown'}>`;
+
+ if (mutator) {
+ errorType = mutator.hasErrorType
+ ? `ErrorType<${response.definition.errors || 'unknown'}>`
+ : response.definition.errors || 'unknown';
+ }
+
+ return `
+export type ${pascal(
+ operationName,
+ )}QueryResult = NonNullable>>
+export type ${pascal(operationName)}QueryError = ${errorType}
+
+export const ${camel(
+ `use-${operationName}`,
+ )} = (\n ${swrProps} ${generateSwrArguments({
+ operationName,
+ mutator,
+ isRequestOptions,
+ })}\n ) => {
+
+ ${
+ isRequestOptions
+ ? `const {swr: swrOptions${
+ !mutator
+ ? `, axios: axiosOptions`
+ : mutator?.hasSecondArg
+ ? ', request: requestOptions'
+ : ''
+ }} = options ?? {}`
+ : ''
+ }
+
+ ${swrKeyImplementation}
+ const swrFn = () => ${operationName}(${httpFunctionProps}${
+ httpFunctionProps ? ', ' : ''
+ }${
+ isRequestOptions
+ ? !mutator
+ ? `axiosOptions`
+ : mutator?.hasSecondArg
+ ? 'requestOptions'
+ : ''
+ : ''
+ });
+
+ const query = useSwr>, TError>(swrKey, swrFn, ${
+ swrOptions.options
+ ? `{
+ ${stringify(swrOptions.options)?.slice(1, -1)}
+ ...swrOptions
+ }`
+ : 'swrOptions'
+ })
+
+ return {
+ swrKey,
+ ...query
+ }
+}\n`;
+};
+
+const generateSwrHook = (
+ {
+ queryParams,
+ operationName,
+ body,
+ props,
+ verb,
+ params,
+ override,
+ mutator,
+ response,
+ }: GeneratorVerbOptions,
+ { route }: GeneratorOptions,
+) => {
+ const isRequestOptions = override?.requestOptions !== false;
+
+ if (verb !== Verbs.GET) {
+ return '';
+ }
+
+ const swrProperties = props
+ .map(({ name, type }) =>
+ type === GetterPropType.BODY ? body.implementation : name,
+ )
+ .join(',');
+
+ const swrKeyProperties = props
+ .filter((prop) => prop.type !== GetterPropType.HEADER)
+ .map(({ name, type }) =>
+ type === GetterPropType.BODY ? body.implementation : name,
+ )
+ .join(',');
+
+ const swrKeyFnName = camel(`get-${operationName}-key`);
+ const queryKeyProps = toObjectString(
+ props.filter((prop) => prop.type !== GetterPropType.HEADER),
+ 'implementation',
+ );
+
+ return `export const ${swrKeyFnName} = (${queryKeyProps}) => [\`${route}\`${
+ queryParams ? ', ...(params ? [params]: [])' : ''
+ }${body.implementation ? `, ${body.implementation}` : ''}];
+
+ ${generateSwrImplementation({
+ operationName,
+ swrKeyFnName,
+ swrProperties,
+ swrKeyProperties,
+ params,
+ props,
+ mutator,
+ isRequestOptions,
+ response,
+ swrOptions: override.swr,
+ })}
+`;
+};
+
+export const generateSwrTitle: ClientTitleBuilder = () => '';
+
+export const generateSwrHeader: ClientHeaderBuilder = ({
+ isRequestOptions,
+ isMutator,
+ hasAwaitedType,
+}) =>
+ `
+ ${
+ !hasAwaitedType
+ ? `type AwaitedInput = PromiseLike | T;\n
+ type Awaited = O extends AwaitedInput ? T : never;\n\n`
+ : ''
+ }
+ ${
+ isRequestOptions && isMutator
+ ? `// eslint-disable-next-line
+ type SecondParameter any> = T extends (
+ config: any,
+ args: infer P,
+) => any
+ ? P
+ : never;\n\n`
+ : ''
+ }`;
+
+export const generateSwrFooter: ClientFooterBuilder = () => '';
+
+export const generateSwr: ClientBuilder = (verbOptions, options) => {
+ const imports = generateVerbImports(verbOptions);
+ const functionImplementation = generateSwrRequestFunction(
+ verbOptions,
+ options,
+ );
+ const hookImplementation = generateSwrHook(verbOptions, options);
+
+ return {
+ implementation: `${functionImplementation}\n\n${hookImplementation}`,
+ imports,
+ };
+};
+
+const swrClientBuilder: ClientGeneratorsBuilder = {
+ client: generateSwr,
+ header: generateSwrHeader,
+ dependencies: getSwrDependencies,
+ footer: generateSwrFooter,
+ title: generateSwrTitle,
+};
+
+export const builder = () => swrClientBuilder;
+
+export default builder;
diff --git a/packages/swr/tsconfig.json b/packages/swr/tsconfig.json
new file mode 100644
index 000000000..9e25e6ece
--- /dev/null
+++ b/packages/swr/tsconfig.json
@@ -0,0 +1,4 @@
+{
+ "extends": "../../tsconfig.base.json",
+ "include": ["src/**/*.ts"]
+}
diff --git a/samples/angular-app/package.json b/samples/angular-app/package.json
index 21b6fb860..ddff0f056 100644
--- a/samples/angular-app/package.json
+++ b/samples/angular-app/package.json
@@ -6,7 +6,7 @@
"start": "ng serve",
"build": "ng build",
"lint": "ng lint",
- "generate-api": "node ../../dist/bin/orval.js"
+ "generate-api": "node ../../packages/orval/dist/bin/orval.js"
},
"private": true,
"dependencies": {
@@ -30,7 +30,7 @@
"@types/node": "^12.11.1",
"codelyzer": "^6.0.0",
"msw": "^0.21.2",
- "orval": "link:../../dist",
+ "orval": "link:../../../packages/orval/dist",
"ts-node": "~8.3.0",
"tslint": "~6.1.0",
"typescript": "~4.3.5"
diff --git a/samples/basic/package.json b/samples/basic/package.json
index 6002869aa..b6c0d3eb9 100644
--- a/samples/basic/package.json
+++ b/samples/basic/package.json
@@ -12,7 +12,7 @@
"license": "ISC",
"devDependencies": {
"npm-run-all": "^4.1.5",
- "orval": "link:../../dist"
+ "orval": "link:../../../packages/orval/dist"
},
"dependencies": {
"@faker-js/faker": "^7.3.0",
diff --git a/samples/nx-fastify-react/package.json b/samples/nx-fastify-react/package.json
index 94a5fe3fd..35ab9eb0f 100644
--- a/samples/nx-fastify-react/package.json
+++ b/samples/nx-fastify-react/package.json
@@ -18,7 +18,7 @@
"fastify": "^3.21.0",
"fastify-cors": "^6.0.2",
"fastify-swagger": "^4.11.0",
- "orval": "link:../../dist",
+ "orval": "link:../../../packages/orval/dist",
"react": "17.0.2",
"react-dom": "17.0.2",
"react-query": "^3.34.19",
diff --git a/samples/react-app-with-swr/package.json b/samples/react-app-with-swr/package.json
index 0297cda8f..8a8ec6094 100644
--- a/samples/react-app-with-swr/package.json
+++ b/samples/react-app-with-swr/package.json
@@ -1,5 +1,5 @@
{
- "name": "react-app",
+ "name": "swr",
"version": "0.1.0",
"private": true,
"dependencies": {
@@ -16,13 +16,13 @@
"typescript": "^4.1.3"
},
"devDependencies": {
- "orval": "link:../../dist"
+ "orval": "link:../../../packages/orval/dist"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"eject": "react-scripts eject",
- "generate-api": "node ../../dist/bin/orval.js"
+ "generate-api": "node ../../packages/orval/dist/bin/orval.js"
},
"eslintConfig": {
"extends": "react-app"
diff --git a/samples/react-app/package.json b/samples/react-app/package.json
index d52f4ed7e..aab64ca33 100644
--- a/samples/react-app/package.json
+++ b/samples/react-app/package.json
@@ -18,7 +18,7 @@
"start": "react-scripts start",
"build": "react-scripts build",
"eject": "react-scripts eject",
- "generate-api": "node ../../dist/bin/orval.js"
+ "generate-api": "node ../../packages/orval/dist/bin/orval.js"
},
"eslintConfig": {
"extends": "react-app"
@@ -37,7 +37,7 @@
},
"devDependencies": {
"msw": "^0.30.0",
- "orval": "link:../../dist"
+ "orval": "link:../../../packages/orval/dist"
},
"msw": {
"workerDirectory": "public"
diff --git a/samples/react-query/basic/package.json b/samples/react-query/basic/package.json
index eb9117a4c..ac9d83cea 100644
--- a/samples/react-query/basic/package.json
+++ b/samples/react-query/basic/package.json
@@ -1,5 +1,5 @@
{
- "name": "react-app",
+ "name": "react-query-basic",
"version": "0.1.0",
"private": true,
"dependencies": {
@@ -8,6 +8,7 @@
"@types/node": "^14.14.13",
"@types/react": "^17.0.0",
"@types/react-dom": "^17.0.0",
+ "ajv": "^8.11.0",
"axios": "^0.26.1",
"msw": "^0.24.2",
"react": "^17.0.1",
@@ -16,13 +17,13 @@
"typescript": "^4.1.3"
},
"devDependencies": {
- "orval": "link:../../../dist"
+ "orval": "link:../../../packages/orval/dist"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"eject": "react-scripts eject",
- "generate-api": "node ../../../dist/bin/orval.js"
+ "generate-api": "node ../../../packages/orval/dist/bin/orval.js"
},
"eslintConfig": {
"extends": "react-app"
diff --git a/samples/react-query/basic/src/api/endpoints/petstoreFromFileSpecWithTransformer.msw.ts b/samples/react-query/basic/src/api/endpoints/petstoreFromFileSpecWithTransformer.msw.ts
index ee8c18165..5d7c5c683 100644
--- a/samples/react-query/basic/src/api/endpoints/petstoreFromFileSpecWithTransformer.msw.ts
+++ b/samples/react-query/basic/src/api/endpoints/petstoreFromFileSpecWithTransformer.msw.ts
@@ -1,5 +1,5 @@
/**
- * Generated by orval v6.10.3 🍺
+ * Generated by orval v6.11.0-alpha.1 🍺
* Do not edit manually.
* Swagger Petstore
* OpenAPI spec version: 1.0.0
diff --git a/samples/react-query/basic/src/api/endpoints/petstoreFromFileSpecWithTransformer.ts b/samples/react-query/basic/src/api/endpoints/petstoreFromFileSpecWithTransformer.ts
index b7faf55ec..73ca92ba6 100644
--- a/samples/react-query/basic/src/api/endpoints/petstoreFromFileSpecWithTransformer.ts
+++ b/samples/react-query/basic/src/api/endpoints/petstoreFromFileSpecWithTransformer.ts
@@ -1,10 +1,10 @@
/**
- * Generated by orval v6.10.3 🍺
+ * Generated by orval v6.11.0-alpha.1 🍺
* Do not edit manually.
* Swagger Petstore
* OpenAPI spec version: 1.0.0
*/
-import { useQuery, useInfiniteQuery, useMutation } from '@tanstack/react-query';
+import { useQuery, useInfiniteQuery, useMutation } from '@tanstack/react-quer';
import type {
UseQueryOptions,
UseInfiniteQueryOptions,
@@ -14,7 +14,7 @@ import type {
UseQueryResult,
UseInfiniteQueryResult,
QueryKey,
-} from '@tanstack/react-query';
+} from '@tanstack/react-quer';
import type {
Pets,
Error,
diff --git a/samples/react-query/basic/src/api/model/cat.ts b/samples/react-query/basic/src/api/model/cat.ts
index b82d00f62..5752d551a 100644
--- a/samples/react-query/basic/src/api/model/cat.ts
+++ b/samples/react-query/basic/src/api/model/cat.ts
@@ -1,5 +1,5 @@
/**
- * Generated by orval v6.10.3 🍺
+ * Generated by orval v6.11.0-alpha.1 🍺
* Do not edit manually.
* Swagger Petstore
* OpenAPI spec version: 1.0.0
diff --git a/samples/react-query/basic/src/api/model/catType.ts b/samples/react-query/basic/src/api/model/catType.ts
index 5d858194c..e3b3eff34 100644
--- a/samples/react-query/basic/src/api/model/catType.ts
+++ b/samples/react-query/basic/src/api/model/catType.ts
@@ -1,5 +1,5 @@
/**
- * Generated by orval v6.10.3 🍺
+ * Generated by orval v6.11.0-alpha.1 🍺
* Do not edit manually.
* Swagger Petstore
* OpenAPI spec version: 1.0.0
diff --git a/samples/react-query/basic/src/api/model/createPetsBody.ts b/samples/react-query/basic/src/api/model/createPetsBody.ts
index 0753f994d..0288f6403 100644
--- a/samples/react-query/basic/src/api/model/createPetsBody.ts
+++ b/samples/react-query/basic/src/api/model/createPetsBody.ts
@@ -1,5 +1,5 @@
/**
- * Generated by orval v6.10.3 🍺
+ * Generated by orval v6.11.0-alpha.1 🍺
* Do not edit manually.
* Swagger Petstore
* OpenAPI spec version: 1.0.0
diff --git a/samples/react-query/basic/src/api/model/dachshund.ts b/samples/react-query/basic/src/api/model/dachshund.ts
index 475dd1571..3353876df 100644
--- a/samples/react-query/basic/src/api/model/dachshund.ts
+++ b/samples/react-query/basic/src/api/model/dachshund.ts
@@ -1,5 +1,5 @@
/**
- * Generated by orval v6.10.3 🍺
+ * Generated by orval v6.11.0-alpha.1 🍺
* Do not edit manually.
* Swagger Petstore
* OpenAPI spec version: 1.0.0
diff --git a/samples/react-query/basic/src/api/model/dachshundBreed.ts b/samples/react-query/basic/src/api/model/dachshundBreed.ts
index 36c56c56f..fd30936bc 100644
--- a/samples/react-query/basic/src/api/model/dachshundBreed.ts
+++ b/samples/react-query/basic/src/api/model/dachshundBreed.ts
@@ -1,5 +1,5 @@
/**
- * Generated by orval v6.10.3 🍺
+ * Generated by orval v6.11.0-alpha.1 🍺
* Do not edit manually.
* Swagger Petstore
* OpenAPI spec version: 1.0.0
diff --git a/samples/react-query/basic/src/api/model/dog.ts b/samples/react-query/basic/src/api/model/dog.ts
index c02a9f035..b920553f5 100644
--- a/samples/react-query/basic/src/api/model/dog.ts
+++ b/samples/react-query/basic/src/api/model/dog.ts
@@ -1,5 +1,5 @@
/**
- * Generated by orval v6.10.3 🍺
+ * Generated by orval v6.11.0-alpha.1 🍺
* Do not edit manually.
* Swagger Petstore
* OpenAPI spec version: 1.0.0
diff --git a/samples/react-query/basic/src/api/model/dogType.ts b/samples/react-query/basic/src/api/model/dogType.ts
index 011dc9dbb..b99c7d625 100644
--- a/samples/react-query/basic/src/api/model/dogType.ts
+++ b/samples/react-query/basic/src/api/model/dogType.ts
@@ -1,5 +1,5 @@
/**
- * Generated by orval v6.10.3 🍺
+ * Generated by orval v6.11.0-alpha.1 🍺
* Do not edit manually.
* Swagger Petstore
* OpenAPI spec version: 1.0.0
diff --git a/samples/react-query/basic/src/api/model/error.ts b/samples/react-query/basic/src/api/model/error.ts
index 426c8d95d..181ab726f 100644
--- a/samples/react-query/basic/src/api/model/error.ts
+++ b/samples/react-query/basic/src/api/model/error.ts
@@ -1,5 +1,5 @@
/**
- * Generated by orval v6.10.3 🍺
+ * Generated by orval v6.11.0-alpha.1 🍺
* Do not edit manually.
* Swagger Petstore
* OpenAPI spec version: 1.0.0
diff --git a/samples/react-query/basic/src/api/model/labradoodle.ts b/samples/react-query/basic/src/api/model/labradoodle.ts
index f0528b5a3..0326014d5 100644
--- a/samples/react-query/basic/src/api/model/labradoodle.ts
+++ b/samples/react-query/basic/src/api/model/labradoodle.ts
@@ -1,5 +1,5 @@
/**
- * Generated by orval v6.10.3 🍺
+ * Generated by orval v6.11.0-alpha.1 🍺
* Do not edit manually.
* Swagger Petstore
* OpenAPI spec version: 1.0.0
diff --git a/samples/react-query/basic/src/api/model/labradoodleBreed.ts b/samples/react-query/basic/src/api/model/labradoodleBreed.ts
index 3f3f91e24..6a7ef1ec8 100644
--- a/samples/react-query/basic/src/api/model/labradoodleBreed.ts
+++ b/samples/react-query/basic/src/api/model/labradoodleBreed.ts
@@ -1,5 +1,5 @@
/**
- * Generated by orval v6.10.3 🍺
+ * Generated by orval v6.11.0-alpha.1 🍺
* Do not edit manually.
* Swagger Petstore
* OpenAPI spec version: 1.0.0
diff --git a/samples/react-query/basic/src/api/model/listPetsParams.ts b/samples/react-query/basic/src/api/model/listPetsParams.ts
index 3d9da9eee..a2cf35f3e 100644
--- a/samples/react-query/basic/src/api/model/listPetsParams.ts
+++ b/samples/react-query/basic/src/api/model/listPetsParams.ts
@@ -1,5 +1,5 @@
/**
- * Generated by orval v6.10.3 🍺
+ * Generated by orval v6.11.0-alpha.1 🍺
* Do not edit manually.
* Swagger Petstore
* OpenAPI spec version: 1.0.0
diff --git a/samples/react-query/basic/src/api/model/pet.ts b/samples/react-query/basic/src/api/model/pet.ts
index 0ed7a409e..9aee7aa3a 100644
--- a/samples/react-query/basic/src/api/model/pet.ts
+++ b/samples/react-query/basic/src/api/model/pet.ts
@@ -1,5 +1,5 @@
/**
- * Generated by orval v6.10.3 🍺
+ * Generated by orval v6.11.0-alpha.1 🍺
* Do not edit manually.
* Swagger Petstore
* OpenAPI spec version: 1.0.0
diff --git a/samples/react-query/basic/src/api/model/petCallingCode.ts b/samples/react-query/basic/src/api/model/petCallingCode.ts
index f9de506c2..be9765cb4 100644
--- a/samples/react-query/basic/src/api/model/petCallingCode.ts
+++ b/samples/react-query/basic/src/api/model/petCallingCode.ts
@@ -1,5 +1,5 @@
/**
- * Generated by orval v6.10.3 🍺
+ * Generated by orval v6.11.0-alpha.1 🍺
* Do not edit manually.
* Swagger Petstore
* OpenAPI spec version: 1.0.0
@@ -9,6 +9,6 @@ export type PetCallingCode = typeof PetCallingCode[keyof typeof PetCallingCode];
// eslint-disable-next-line @typescript-eslint/no-redeclare
export const PetCallingCode = {
- '33': '+33',
- '420': '+420',
+ NUMBER_PLUS_33: '+33',
+ NUMBER_PLUS_420: '+420',
} as const;
diff --git a/samples/react-query/basic/src/api/model/petCountry.ts b/samples/react-query/basic/src/api/model/petCountry.ts
index 3a06e600b..435c053c0 100644
--- a/samples/react-query/basic/src/api/model/petCountry.ts
+++ b/samples/react-query/basic/src/api/model/petCountry.ts
@@ -1,5 +1,5 @@
/**
- * Generated by orval v6.10.3 🍺
+ * Generated by orval v6.11.0-alpha.1 🍺
* Do not edit manually.
* Swagger Petstore
* OpenAPI spec version: 1.0.0
@@ -9,6 +9,6 @@ export type PetCountry = typeof PetCountry[keyof typeof PetCountry];
// eslint-disable-next-line @typescript-eslint/no-redeclare
export const PetCountry = {
- Peoples_Republic_of_China: "People's Republic of China",
+ "People's_Republic_of_China": "People's Republic of China",
Uruguay: 'Uruguay',
} as const;
diff --git a/samples/react-query/basic/src/api/model/pets.ts b/samples/react-query/basic/src/api/model/pets.ts
index f4c32651c..a29b176ce 100644
--- a/samples/react-query/basic/src/api/model/pets.ts
+++ b/samples/react-query/basic/src/api/model/pets.ts
@@ -1,5 +1,5 @@
/**
- * Generated by orval v6.10.3 🍺
+ * Generated by orval v6.11.0-alpha.1 🍺
* Do not edit manually.
* Swagger Petstore
* OpenAPI spec version: 1.0.0
diff --git a/samples/react-query/basic/yarn.lock b/samples/react-query/basic/yarn.lock
index 6f09414de..8bd322c2a 100644
--- a/samples/react-query/basic/yarn.lock
+++ b/samples/react-query/basic/yarn.lock
@@ -2374,6 +2374,16 @@ ajv@^6.12.5:
json-schema-traverse "^0.4.1"
uri-js "^4.2.2"
+ajv@^8.11.0:
+ version "8.11.0"
+ resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.11.0.tgz#977e91dd96ca669f54a11e23e378e33b884a565f"
+ integrity sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==
+ dependencies:
+ fast-deep-equal "^3.1.1"
+ json-schema-traverse "^1.0.0"
+ require-from-string "^2.0.2"
+ uri-js "^4.2.2"
+
alphanum-sort@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/alphanum-sort/-/alphanum-sort-1.0.2.tgz#97a1119649b211ad33691d9f9f486a8ec9fbe0a3"
@@ -6905,6 +6915,11 @@ json-schema-traverse@^0.4.1:
resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660"
integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==
+json-schema-traverse@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2"
+ integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==
+
json-schema@0.2.3:
version "0.2.3"
resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13"
@@ -7944,7 +7959,7 @@ original@^1.0.0:
dependencies:
url-parse "^1.4.3"
-"orval@link:../../../dist":
+"orval@link:../../../packages/orval/dist":
version "0.0.0"
uid ""
@@ -9572,6 +9587,11 @@ require-directory@^2.1.1:
resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I=
+require-from-string@^2.0.2:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909"
+ integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==
+
require-main-filename@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b"
diff --git a/samples/react-query/custom-client/package.json b/samples/react-query/custom-client/package.json
index b49bf6d89..c9aeea77f 100644
--- a/samples/react-query/custom-client/package.json
+++ b/samples/react-query/custom-client/package.json
@@ -1,5 +1,5 @@
{
- "name": "react-app",
+ "name": "react-query-custom-client",
"version": "0.1.0",
"private": true,
"dependencies": {
@@ -18,13 +18,13 @@
"typescript": "^4.1.3"
},
"devDependencies": {
- "orval": "link:../../../dist"
+ "orval": "link:../../../packages/orval/dist"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"eject": "react-scripts eject",
- "generate-api": "node ../../../dist/bin/orval.js"
+ "generate-api": "node ../../../packages/orval/dist/bin/orval.js"
},
"eslintConfig": {
"extends": "react-app"
diff --git a/samples/react-query/form-data-mutator/package.json b/samples/react-query/form-data-mutator/package.json
index 69529e518..8f5a8d5cb 100644
--- a/samples/react-query/form-data-mutator/package.json
+++ b/samples/react-query/form-data-mutator/package.json
@@ -3,10 +3,10 @@
"version": "0.1.0",
"private": true,
"devDependencies": {
- "orval": "link:../../../dist"
+ "orval": "link:../../../packages/orval/dist"
},
"scripts": {
- "generate-api": "node ../../../dist/bin/orval.js"
+ "generate-api": "node ../../../packages/orval/dist/bin/orval.js"
},
"dependencies": {
"@faker-js/faker": "^7.3.0",
diff --git a/samples/react-query/form-data/package.json b/samples/react-query/form-data/package.json
index b35e6cce6..9cc906b9e 100644
--- a/samples/react-query/form-data/package.json
+++ b/samples/react-query/form-data/package.json
@@ -3,10 +3,10 @@
"version": "0.1.0",
"private": true,
"devDependencies": {
- "orval": "link:../../../dist"
+ "orval": "link:../../../packages/orval/dist"
},
"scripts": {
- "generate-api": "node ../../../dist/bin/orval.js"
+ "generate-api": "node ../../../packages/orval/dist/bin/orval.js"
},
"dependencies": {
"@faker-js/faker": "^7.3.0",
diff --git a/samples/react-query/form-url-encoded-mutator/package.json b/samples/react-query/form-url-encoded-mutator/package.json
index e746dae36..fbd42d8f9 100644
--- a/samples/react-query/form-url-encoded-mutator/package.json
+++ b/samples/react-query/form-url-encoded-mutator/package.json
@@ -3,10 +3,10 @@
"version": "0.1.0",
"private": true,
"devDependencies": {
- "orval": "link:../../../dist"
+ "orval": "link:../../../packages/orval/dist"
},
"scripts": {
- "generate-api": "node ../../../dist/bin/orval.js"
+ "generate-api": "node ../../../packages/orval/dist/bin/orval.js"
},
"dependencies": {
"@faker-js/faker": "^7.3.0",
diff --git a/samples/react-query/form-url-encoded/package.json b/samples/react-query/form-url-encoded/package.json
index 195b998df..f7822e81b 100644
--- a/samples/react-query/form-url-encoded/package.json
+++ b/samples/react-query/form-url-encoded/package.json
@@ -3,10 +3,10 @@
"version": "0.1.0",
"private": true,
"devDependencies": {
- "orval": "link:../../../dist"
+ "orval": "link:../../../packages/orval/dist"
},
"scripts": {
- "generate-api": "node ../../../dist/bin/orval.js"
+ "generate-api": "node ../../../packages/orval/dist/bin/orval.js"
},
"dependencies": {
"@faker-js/faker": "^7.3.0",
diff --git a/samples/react-query/hook-mutator/package.json b/samples/react-query/hook-mutator/package.json
index 89edc98ef..c11cdf9fc 100644
--- a/samples/react-query/hook-mutator/package.json
+++ b/samples/react-query/hook-mutator/package.json
@@ -3,10 +3,10 @@
"version": "0.1.0",
"private": true,
"devDependencies": {
- "orval": "link:../../../dist"
+ "orval": "link:../../../packages/orval/dist"
},
"scripts": {
- "generate-api": "node ../../../dist/bin/orval.js"
+ "generate-api": "node ../../../packages/orval/dist/bin/orval.js"
},
"dependencies": {
"@faker-js/faker": "^7.3.0",
diff --git a/samples/svelte-query/package.json b/samples/svelte-query/package.json
index d5dc0d3e5..290862650 100644
--- a/samples/svelte-query/package.json
+++ b/samples/svelte-query/package.json
@@ -7,7 +7,7 @@
"start": "svelte-kit start",
"lint": "prettier --check . && eslint --ignore-path .gitignore .",
"format": "prettier --write .",
- "generate-api": "node ../../dist/bin/orval.js"
+ "generate-api": "node ../../packages/orval/dist/bin/orval.js"
},
"devDependencies": {
"@faker-js/faker": "^7.3.0",
@@ -30,7 +30,7 @@
"dependencies": {
"@sveltestack/svelte-query": "^1.6.0",
"axios": "^0.26.1",
- "orval": "link:../../dist",
+ "orval": "link:../../../packages/orval/dist",
"ya": "^0.2.2"
},
"type": "module",
diff --git a/samples/vue-query/package.json b/samples/vue-query/package.json
index af6d81c72..781c07983 100644
--- a/samples/vue-query/package.json
+++ b/samples/vue-query/package.json
@@ -1,10 +1,11 @@
{
+ "name": "vue-query",
"version": "0.0.0",
"scripts": {
"dev": "vite",
"build": "vue-tsc --noEmit && vite build",
"serve": "vite preview",
- "generate-api": "node ../../dist/bin/orval.js"
+ "generate-api": "node ../../packages/orval/dist/bin/orval.js"
},
"dependencies": {
"@faker-js/faker": "^7.3.0",
@@ -16,7 +17,7 @@
"@vitejs/plugin-vue": "^1.2.5",
"@vue/compiler-sfc": "^3.0.5",
"msw": "^0.32.0",
- "orval": "link:../../dist",
+ "orval": "link:../../../packages/orval/dist",
"typescript": "^4.3.2",
"vite": "^2.4.2",
"vue-tsc": "^0.0.24"
diff --git a/src/core/getters/enum.ts b/src/core/getters/enum.ts
index e584a8c08..017669159 100644
--- a/src/core/getters/enum.ts
+++ b/src/core/getters/enum.ts
@@ -29,10 +29,11 @@ export const getEnumImplementation = (value: string, type: string) => {
}
if (key.length > 1) {
- key = sanitize(val, {
- underscore: '_',
+ key = sanitize(key, {
whitespace: '_',
+ underscore: true,
dash: true,
+ special: true,
});
}
diff --git a/src/utils/file.ts b/src/utils/file.ts
index f00fd6fd7..5e1cb1853 100644
--- a/src/utils/file.ts
+++ b/src/utils/file.ts
@@ -156,6 +156,7 @@ export async function loadFile(
`Unexpected identifier`,
].join('|'),
);
+ // @ts-ignore
if (!ignored.test(e.message)) {
throw e;
}
diff --git a/tsconfig.base.json b/tsconfig.base.json
new file mode 100644
index 000000000..1ffe38974
--- /dev/null
+++ b/tsconfig.base.json
@@ -0,0 +1,23 @@
+{
+ "exclude": ["node_modules", "dist", "__tests__", "**/*.test*", "**/*.spec*"],
+ "$schema": "https://json.schemastore.org/tsconfig",
+ "compilerOptions": {
+ "target": "es2015",
+ "module": "esnext",
+ "lib": ["es2017", "dom"],
+ "composite": false,
+ "declaration": true,
+ "declarationMap": true,
+ "esModuleInterop": true,
+ "forceConsistentCasingInFileNames": true,
+ "inlineSources": false,
+ "isolatedModules": false,
+ "moduleResolution": "node",
+ "noUnusedLocals": false,
+ "noUnusedParameters": false,
+ "preserveWatchOutput": true,
+ "skipLibCheck": true,
+ "strict": true,
+ "resolveJsonModule": true
+ }
+}
diff --git a/tsconfig.json b/tsconfig.json
deleted file mode 100644
index 0890ddb55..000000000
--- a/tsconfig.json
+++ /dev/null
@@ -1,23 +0,0 @@
-{
- "exclude": ["node_modules", "dist", "__tests__", "**/*.test*", "**/*.spec*"],
- "include": ["src/**/*"],
- "compilerOptions": {
- "skipLibCheck": true,
- "target": "es5",
- "module": "commonjs",
- "lib": ["es2019", "dom", "es2016.array.include", "es2017.object"],
- "declaration": true,
- "declarationMap": true,
- "downlevelIteration": true,
- "strict": true,
- "noUnusedLocals": true,
- "noUnusedParameters": false,
- "noImplicitReturns": true,
- "noFallthroughCasesInSwitch": true,
- "moduleResolution": "node",
- "allowSyntheticDefaultImports": true,
- "esModuleInterop": true,
- "inlineSourceMap": true,
- "resolveJsonModule": true
- }
-}
diff --git a/turbo.json b/turbo.json
new file mode 100644
index 000000000..a4c018707
--- /dev/null
+++ b/turbo.json
@@ -0,0 +1,19 @@
+{
+ "$schema": "https://turbo.build/schema.json",
+ "pipeline": {
+ "build": {
+ "dependsOn": ["^build"],
+ "outputs": ["dist/**"]
+ },
+ "dev": {
+ "cache": false
+ },
+ "lint": {
+ "outputs": []
+ },
+ "test": {
+ "outputs": []
+ }
+ },
+ "globalEnv": ["ORVAL_DEBUG_FILTER", "CI", "DEBUG"]
+}
diff --git a/types/esutils.d.ts b/types/esutils.d.ts
new file mode 100644
index 000000000..ef2e551a1
--- /dev/null
+++ b/types/esutils.d.ts
@@ -0,0 +1,6 @@
+declare module 'esutils' {
+ export namespace keyword {
+ export function isIdentifierNameES5(str: string): boolean;
+ export function isKeywordES5(id: string, strict: boolean): boolean;
+ }
+}
diff --git a/types/ibm-openapi-validator.d.ts b/types/ibm-openapi-validator.d.ts
new file mode 100644
index 000000000..7e0ee1115
--- /dev/null
+++ b/types/ibm-openapi-validator.d.ts
@@ -0,0 +1,24 @@
+declare module 'ibm-openapi-validator' {
+ interface OpenAPIError {
+ path: string;
+ message: string;
+ }
+
+ interface ValidatorResults {
+ errors: OpenAPIError[];
+ warnings: OpenAPIError[];
+ }
+
+ /**
+ * Returns a Promise with the validation results.
+ *
+ * @param openApiDoc An object that represents an OpenAPI document.
+ * @param defaultMode If set to true, the validator will ignore the .validaterc file and will use the configuration defaults.
+ */
+ function validator(
+ openApiDoc: any,
+ defaultMode = false,
+ ): Promise;
+
+ export default validator;
+}
diff --git a/types/swagger2openapi.d.ts b/types/swagger2openapi.d.ts
new file mode 100644
index 000000000..d624fbfd4
--- /dev/null
+++ b/types/swagger2openapi.d.ts
@@ -0,0 +1,50 @@
+declare module 'swagger2openapi' {
+ import { OpenAPIObject } from 'openapi3-ts';
+ interface ConverObjCallbackData {
+ openapi: OpenAPIObject;
+ }
+
+ /**
+ * Source: https://github.com/Mermade/oas-kit/tree/master/packages/swagger2openapi#a-command-line
+ */
+ interface Options {
+ /** mode to handle $ref's with sibling properties */
+ refSiblings?: 'remove' | 'preserve' | 'allOf';
+ /** resolve internal references also */
+ resolveInternal?: boolean;
+ /** Property name to use for warning extensions [default: "x-s2o-warning"] */
+ warnProperty?: string;
+ /** output information to unresolve a definition */
+ components?: boolean;
+ /** enable debug mode, adds specification-extensions */
+ debug?: boolean;
+ /** encoding for input/output files [default: "utf8"] */
+ encoding?: string;
+ /** make resolution errors fatal */
+ fatal?: boolean;
+ /** JSON indent to use, defaults to 4 spaces */
+ indent?: string;
+ /** the output file to write to */
+ outfile?: string;
+ /** fix up small errors in the source definition */
+ patch?: boolean;
+ /** resolve external references */
+ resolve?: boolean;
+ /** override default target version of 3.0.0 */
+ targetVersion?: string;
+ /** url of original spec, creates x-origin entry */
+ url?: string;
+ /** Do not throw on non-patchable errors, add warning extensions */
+ warnOnly?: boolean;
+ /** write YAML, default JSON (overridden by outfile filepath extension */
+ yaml?: boolean;
+ /** Extension to use to preserve body parameter names in converted operations ("" == disabled) [default: ""] */
+ rbname?: string;
+ }
+
+ function convertObj(
+ schema: unknown,
+ options: Options,
+ callback: (err: Error, data: ConverObjCallbackData) => void,
+ ): void;
+}
diff --git a/yarn.lock b/yarn.lock
index f75152a48..5cee4426b 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -889,11 +889,6 @@
"@types/node" "*"
"@types/responselike" "*"
-"@types/caseless@*":
- version "0.12.2"
- resolved "https://registry.yarnpkg.com/@types/caseless/-/caseless-0.12.2.tgz#f65d3d6389e01eeb458bd54dc8f52b95a9463bc8"
- integrity sha512-6ckxMjBBD8URvjB6J3NcnuAn5Pkl7t3TizAg+xdlzzQGSPSmBcXf8KoIH0ua/i+tio+ZRUHEXp0HEmvaR4kt0w==
-
"@types/chai-subset@^1.3.3":
version "1.3.3"
resolved "https://registry.yarnpkg.com/@types/chai-subset/-/chai-subset-1.3.3.tgz#97893814e92abd2c534de422cb377e0e0bdaac94"
@@ -913,12 +908,12 @@
dependencies:
chalk "*"
-"@types/commander@^2.12.2":
- version "2.12.2"
- resolved "https://registry.yarnpkg.com/@types/commander/-/commander-2.12.2.tgz#183041a23842d4281478fa5d23c5ca78e6fd08ae"
- integrity sha512-0QEFiR8ljcHp9bAbWxecjVRuAMr16ivPiGOw6KFQBVrVd0RQIcM3xKdRisH2EDWgVWujiYtHwhSkSUoAAGzH7Q==
+"@types/debug@^4.1.7":
+ version "4.1.7"
+ resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.7.tgz#7cc0ea761509124709b8b2d1090d8f6c17aadb82"
+ integrity sha512-9AonUzyTjXXhEOa0DnqpzZi6VHlqKMswga9EXjpXnnqxwLtdvPPtlO8evrI5D9S6asFRCQ6v+wpiUKbw+vKqyg==
dependencies:
- commander "*"
+ "@types/ms" "*"
"@types/es-aggregate-error@^1.0.2":
version "1.0.2"
@@ -1037,6 +1032,11 @@
resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.2.tgz#ee771e2ba4b3dc5b372935d549fd9617bf345b8c"
integrity sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ==
+"@types/ms@*":
+ version "0.7.31"
+ resolved "https://registry.yarnpkg.com/@types/ms/-/ms-0.7.31.tgz#31b7ca6407128a3d2bbc27fe2d21b345397f6197"
+ integrity sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==
+
"@types/node@*", "@types/node@>=12", "@types/node@^18.6.3", "@types/node@^18.7.3":
version "18.7.3"
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.7.3.tgz#432c89796eab539b7a30b7b8801a727b585238a4"
@@ -1062,16 +1062,6 @@
resolved "https://registry.yarnpkg.com/@types/ps-tree/-/ps-tree-1.1.2.tgz#5c60773a38ffb1402e049902a7b7a8d3c67cd59a"
integrity sha512-ZREFYlpUmPQJ0esjxoG1fMvB2HNaD3z+mjqdSosZvd3RalncI9NEur73P8ZJz4YQdL64CmV1w0RuqoRUlhQRBw==
-"@types/request@^2.48.8":
- version "2.48.8"
- resolved "https://registry.yarnpkg.com/@types/request/-/request-2.48.8.tgz#0b90fde3b655ab50976cb8c5ac00faca22f5a82c"
- integrity sha512-whjk1EDJPcAR2kYHRbFl/lKeeKYTi05A15K9bnLInCVroNDCtXce57xKdI0/rQaA3K+6q0eFyUBPmqfSndUZdQ==
- dependencies:
- "@types/caseless" "*"
- "@types/node" "*"
- "@types/tough-cookie" "*"
- form-data "^2.5.0"
-
"@types/responselike@*", "@types/responselike@^1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@types/responselike/-/responselike-1.0.0.tgz#251f4fe7d154d2bad125abe1b429b23afd262e29"
@@ -1086,11 +1076,6 @@
dependencies:
"@types/node" "*"
-"@types/tough-cookie@*":
- version "4.0.2"
- resolved "https://registry.yarnpkg.com/@types/tough-cookie/-/tough-cookie-4.0.2.tgz#6286b4c7228d58ab7866d19716f3696e03a09397"
- integrity sha512-Q5vtl1W5ue16D+nIaW8JWebSSraJVlK+EthKn7e7UcD4KWsaSJ8BqGPXNaPghgtcn/fhvrN17Tv8ksUsQpiplw==
-
"@types/urijs@^1.19.16":
version "1.19.19"
resolved "https://registry.yarnpkg.com/@types/urijs/-/urijs-1.19.19.tgz#2789369799907fc11e2bc6e3a00f6478c2281b95"
@@ -1790,18 +1775,13 @@ colorette@^2.0.16, colorette@^2.0.17:
resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.19.tgz#cdf044f47ad41a0f4b56b3a0d5b4e6e1a2d5a798"
integrity sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==
-combined-stream@^1.0.6, combined-stream@^1.0.8:
+combined-stream@^1.0.8:
version "1.0.8"
resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==
dependencies:
delayed-stream "~1.0.0"
-commander@*, commander@^9.3.0:
- version "9.4.0"
- resolved "https://registry.yarnpkg.com/commander/-/commander-9.4.0.tgz#bc4a40918fefe52e22450c111ecd6b7acce6f11c"
- integrity sha512-sRPT+umqkz90UA8M1yqYfnHlZA7fF6nSphDtxeywPZ49ysjxDQybzk13CL+mXekDRG92skbcqCLVovuCusNmFw==
-
commander@^2.20.3:
version "2.20.3"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
@@ -1812,6 +1792,11 @@ commander@^4.0.0:
resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068"
integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==
+commander@^9.3.0:
+ version "9.4.0"
+ resolved "https://registry.yarnpkg.com/commander/-/commander-9.4.0.tgz#bc4a40918fefe52e22450c111ecd6b7acce6f11c"
+ integrity sha512-sRPT+umqkz90UA8M1yqYfnHlZA7fF6nSphDtxeywPZ49ysjxDQybzk13CL+mXekDRG92skbcqCLVovuCusNmFw==
+
commondir@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b"
@@ -2563,6 +2548,11 @@ esbuild-openbsd-64@0.15.3:
resolved "https://registry.yarnpkg.com/esbuild-openbsd-64/-/esbuild-openbsd-64-0.15.3.tgz#fa2e84480fe043480401c73fdfb944ba2d21826d"
integrity sha512-QL7xYQ4noukuqh8UGnsrk1m+ShPMYIXjOnAQl3siA7VV6cjuUoCxx6cThgcUDzih8iL5u2xgsGRhsviQIMsUuA==
+esbuild-plugin-alias@^0.2.1:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/esbuild-plugin-alias/-/esbuild-plugin-alias-0.2.1.tgz#45a86cb941e20e7c2bc68a2bea53562172494fcb"
+ integrity sha512-jyfL/pwPqaFXyKnj8lP8iLk6Z0m099uXR45aSN8Av1XD4vhvQutxxPzgA2bTcAwQpa1zCXDcWOlhFgyP3GKqhQ==
+
esbuild-sunos-64@0.14.54:
version "0.14.54"
resolved "https://registry.yarnpkg.com/esbuild-sunos-64/-/esbuild-sunos-64-0.14.54.tgz#54287ee3da73d3844b721c21bc80c1dc7e1bf7da"
@@ -2699,6 +2689,13 @@ eslint-config-prettier@^8.5.0:
resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-8.5.0.tgz#5a81680ec934beca02c7b1a61cf8ca34b66feab1"
integrity sha512-obmWKLUNCnhtQRKc+tmnYuQl0pFU1ibYJQ5BGhTVB08bHe9wC8qUeG7c08dj9XX+AuPj1YSGSQIHl1pnDHZR0Q==
+eslint-config-turbo@^0.0.4:
+ version "0.0.4"
+ resolved "https://registry.yarnpkg.com/eslint-config-turbo/-/eslint-config-turbo-0.0.4.tgz#4850255f8c858131843aa38854d4aed0ff09bb6e"
+ integrity sha512-HErPS/wfWkSdV9Yd2dDkhZt3W2B78Ih/aWPFfaHmCMjzPalh+5KxRRGTf8MOBQLCebcWJX0lP1Zvc1rZIHlXGg==
+ dependencies:
+ eslint-plugin-turbo "0.0.4"
+
eslint-plugin-prettier@^4.2.1:
version "4.2.1"
resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-4.2.1.tgz#651cbb88b1dab98bfd42f017a12fa6b2d993f94b"
@@ -2706,6 +2703,11 @@ eslint-plugin-prettier@^4.2.1:
dependencies:
prettier-linter-helpers "^1.0.0"
+eslint-plugin-turbo@0.0.4:
+ version "0.0.4"
+ resolved "https://registry.yarnpkg.com/eslint-plugin-turbo/-/eslint-plugin-turbo-0.0.4.tgz#966f3dd6202143d0c28dc9cdbb48b0f779d06172"
+ integrity sha512-dfmYE/iPvoJInQq+5E/0mj140y/rYwKtzZkn3uVK8+nvwC5zmWKQ6ehMWrL4bYBkGzSgpOndZM+jOXhPQ2m8Cg==
+
eslint-scope@^5.1.1:
version "5.1.1"
resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c"
@@ -3067,15 +3069,6 @@ form-data@4.0.0:
combined-stream "^1.0.8"
mime-types "^2.1.12"
-form-data@^2.5.0:
- version "2.5.1"
- resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.5.1.tgz#f2cbec57b5e59e23716e128fe44d4e5dd23895f4"
- integrity sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==
- dependencies:
- asynckit "^0.4.0"
- combined-stream "^1.0.6"
- mime-types "^2.1.12"
-
format-util@^1.0.3:
version "1.0.5"
resolved "https://registry.yarnpkg.com/format-util/-/format-util-1.0.5.tgz#1ffb450c8a03e7bccffe40643180918cc297d271"
@@ -6219,6 +6212,48 @@ tsutils@^3.21.0:
dependencies:
tslib "^1.8.1"
+turbo-darwin-64@1.6.3:
+ version "1.6.3"
+ resolved "https://registry.yarnpkg.com/turbo-darwin-64/-/turbo-darwin-64-1.6.3.tgz#fad7e078784b0fafc0b1f75ce9378828918595f5"
+ integrity sha512-QmDIX0Yh1wYQl0bUS0gGWwNxpJwrzZU2GIAYt3aOKoirWA2ecnyb3R6ludcS1znfNV2MfunP+l8E3ncxUHwtjA==
+
+turbo-darwin-arm64@1.6.3:
+ version "1.6.3"
+ resolved "https://registry.yarnpkg.com/turbo-darwin-arm64/-/turbo-darwin-arm64-1.6.3.tgz#f0a32cae39e3fcd3da5e3129a94c18bb2e3ed6aa"
+ integrity sha512-75DXhFpwE7CinBbtxTxH08EcWrxYSPFow3NaeFwsG8aymkWXF+U2aukYHJA6I12n9/dGqf7yRXzkF0S/9UtdyQ==
+
+turbo-linux-64@1.6.3:
+ version "1.6.3"
+ resolved "https://registry.yarnpkg.com/turbo-linux-64/-/turbo-linux-64-1.6.3.tgz#8ddc6ac55ef84641182fe5ff50647f1b355826b0"
+ integrity sha512-O9uc6J0yoRPWdPg9THRQi69K6E2iZ98cRHNvus05lZbcPzZTxJYkYGb5iagCmCW/pq6fL4T4oLWAd6evg2LGQA==
+
+turbo-linux-arm64@1.6.3:
+ version "1.6.3"
+ resolved "https://registry.yarnpkg.com/turbo-linux-arm64/-/turbo-linux-arm64-1.6.3.tgz#846c1dc84d8dc741651906613c16acccba30428c"
+ integrity sha512-dCy667qqEtZIhulsRTe8hhWQNCJO0i20uHXv7KjLHuFZGCeMbWxB8rsneRoY+blf8+QNqGuXQJxak7ayjHLxiA==
+
+turbo-windows-64@1.6.3:
+ version "1.6.3"
+ resolved "https://registry.yarnpkg.com/turbo-windows-64/-/turbo-windows-64-1.6.3.tgz#89ac819fa76ad31d12fbfdeb3045bcebd0d308eb"
+ integrity sha512-lKRqwL3mrVF09b9KySSaOwetehmGknV9EcQTF7d2dxngGYYX1WXoQLjFP9YYH8ZV07oPm+RUOAKSCQuDuMNhiA==
+
+turbo-windows-arm64@1.6.3:
+ version "1.6.3"
+ resolved "https://registry.yarnpkg.com/turbo-windows-arm64/-/turbo-windows-arm64-1.6.3.tgz#977607c9a51f0b76076c8b158bafce06ce813070"
+ integrity sha512-BXY1sDPEA1DgPwuENvDCD8B7Hb0toscjus941WpL8CVd10hg9pk/MWn9CNgwDO5Q9ks0mw+liDv2EMnleEjeNA==
+
+turbo@^1.6.3:
+ version "1.6.3"
+ resolved "https://registry.yarnpkg.com/turbo/-/turbo-1.6.3.tgz#ec26cc8907c38a9fd6eb072fb10dad254733543e"
+ integrity sha512-FtfhJLmEEtHveGxW4Ye/QuY85AnZ2ZNVgkTBswoap7UMHB1+oI4diHPNyqrQLG4K1UFtCkjOlVoLsllUh/9QRw==
+ optionalDependencies:
+ turbo-darwin-64 "1.6.3"
+ turbo-darwin-arm64 "1.6.3"
+ turbo-linux-64 "1.6.3"
+ turbo-linux-arm64 "1.6.3"
+ turbo-windows-64 "1.6.3"
+ turbo-windows-arm64 "1.6.3"
+
type-check@^0.4.0, type-check@~0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1"