diff --git a/.vscode/settings.json b/.vscode/settings.json index 45fcd3d5a..ed6af8b26 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,4 @@ { - "cSpell.words": ["listitem", "openapi", "Orval", "Petstore", "tanstack"] + "cSpell.words": ["listitem", "openapi", "Orval", "Petstore", "tanstack"], + "typescript.tsdk": "node_modules/typescript/lib" } diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 4792e49bb..186351a54 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -64,6 +64,7 @@ export type NormalizedOutputOptions = { urlEncodeParameters: boolean; unionAddMissingProperties: boolean; optionsParamRequired: boolean; + onChanges: boolean; propertySortOrder: PropertySortOrder; }; @@ -214,6 +215,7 @@ export type OutputOptions = { urlEncodeParameters?: boolean; unionAddMissingProperties?: boolean; optionsParamRequired?: boolean; + onChanges?: boolean; propertySortOrder?: PropertySortOrder; }; @@ -634,6 +636,7 @@ export interface GlobalOptions { packageJson?: string; input?: string; output?: string; + onChanges?: boolean; } export interface Tsconfig { diff --git a/packages/core/src/utils/logger.ts b/packages/core/src/utils/logger.ts index 6181a64d9..1ca2cde22 100644 --- a/packages/core/src/utils/logger.ts +++ b/packages/core/src/utils/logger.ts @@ -46,6 +46,13 @@ export const createSuccessMessage = (backend?: string) => }Your OpenAPI spec has been converted into ready to use orval!`, ); +export const nothingChangedMessage = (backend?: string) => + log( + `🎉 ${ + backend ? `${chalk.green(backend)} - ` : '' + }No changes detected, skipping generation`, + ); + export const ibmOpenapiValidatorWarnings = ( warnings: { path: string[]; diff --git a/packages/orval/src/bin/orval.ts b/packages/orval/src/bin/orval.ts index 35100c765..dae7af9d0 100644 --- a/packages/orval/src/bin/orval.ts +++ b/packages/orval/src/bin/orval.ts @@ -1,10 +1,11 @@ #!/usr/bin/env node import { isString, logError, startMessage } from '@orval/core'; import { cac } from 'cac'; -import { generateConfig, generateSpec } from '../generate'; import pkg from '../../package.json'; +import { generateConfig, generateSpec } from '../generate'; import { normalizeOptions } from '../utils/options'; import { startWatcher } from '../utils/watcher'; +import { writeLastCommit } from '../utils/write-last-commit'; const cli = cac('orval'); @@ -40,6 +41,7 @@ cli .option('--tslint [path]', 'tslint generated files') .option('--biome [path]', 'biome generated files') .option('--tsconfig [path]', 'path to your tsconfig file') + .option('--onChanges', 'only generate when the input file changes') .action(async (paths, cmd) => { if (!cmd.config && isString(cmd.input) && isString(cmd.output)) { const normalizedOptions = await normalizeOptions({ @@ -54,6 +56,7 @@ cli client: cmd.client, mode: cmd.mode, tsconfig: cmd.tsconfig, + onChanges: cmd.onChanges, }, }); @@ -63,6 +66,7 @@ cli async () => { try { await generateSpec(process.cwd(), normalizedOptions); + await writeLastCommit(process.cwd()); } catch (e) { logError(e); } @@ -72,6 +76,7 @@ cli } else { try { await generateSpec(process.cwd(), normalizedOptions); + await writeLastCommit(process.cwd()); } catch (e) { logError(e); } @@ -90,6 +95,7 @@ cli tsconfig: cmd.tsconfig, input: cmd.input, output: cmd.output, + onChanges: cmd.onChanges, }); } }); diff --git a/packages/orval/src/generate.ts b/packages/orval/src/generate.ts index bca4cc758..4cfc38567 100644 --- a/packages/orval/src/generate.ts +++ b/packages/orval/src/generate.ts @@ -1,21 +1,23 @@ import { asyncReduce, ConfigExternal, - upath, - logError, getFileInfo, GlobalOptions, isFunction, isString, loadFile, log, - NormalizedOptions, + logError, NormalizedConfig, + NormalizedOptions, + nothingChangedMessage, removeFiles, + upath, } from '@orval/core'; import { importSpecs } from './import-specs'; import { normalizeOptions } from './utils/options'; import { startWatcher } from './utils/watcher'; +import { writeLastCommit } from './utils/write-last-commit'; import { writeSpecs } from './write-specs'; export const generateSpec = async ( @@ -44,6 +46,12 @@ export const generateSpec = async ( } const writeSpecBuilder = await importSpecs(workspace, options); + + if (!writeSpecBuilder) { + nothingChangedMessage(projectName); + return; + } + await writeSpecs(writeSpecBuilder, workspace, options, projectName); }; @@ -85,6 +93,7 @@ export const generateSpecs = async ( ); if (hasErrors) process.exit(1); + return accumulate; }; @@ -131,10 +140,14 @@ export const generateConfig = async ( if (options?.watch && fileToWatch.length) { startWatcher( options?.watch, - () => generateSpecs(normalizedConfig, workspace, options?.projectName), + async () => { + await generateSpecs(normalizedConfig, workspace, options?.projectName); + await writeLastCommit(workspace); + }, fileToWatch, ); } else { await generateSpecs(normalizedConfig, workspace, options?.projectName); + await writeLastCommit(workspace); } }; diff --git a/packages/orval/src/import-specs.ts b/packages/orval/src/import-specs.ts index 4152197b2..79f154241 100644 --- a/packages/orval/src/import-specs.ts +++ b/packages/orval/src/import-specs.ts @@ -4,15 +4,18 @@ import { isUrl, log, NormalizedOptions, - upath, SwaggerParserOptions, + upath, WriteSpecsBuilder, } from '@orval/core'; import chalk from 'chalk'; -import yaml from 'js-yaml'; +import { execSync } from 'child_process'; import fs from 'fs-extra'; +import yaml from 'js-yaml'; import { importOpenApi } from './import-open-api'; +import { getNodeModulesPath } from './utils/node-modules'; +import { readLastCommit } from './utils/read-last-commit'; const resolveSpecs = async ( path: string, @@ -59,7 +62,7 @@ const resolveSpecs = async ( export const importSpecs = async ( workspace: string, options: NormalizedOptions, -): Promise => { +): Promise => { const { input, output } = options; if (isObject(input.target)) { @@ -83,6 +86,49 @@ export const importSpecs = async ( !output.target, ); + if (options.output.onChanges) { + const nodeModulesPath = await getNodeModulesPath(workspace); + + if (nodeModulesPath) { + const lastCommit = await readLastCommit(workspace); + + if (lastCommit) { + const currentCommit = execSync('git rev-parse HEAD').toString(); + + if (lastCommit === currentCommit) { + return; + } + + const diff = execSync( + `git diff --name-only ${lastCommit} ${currentCommit}`, + ).toString(); + + const trackedFiles = Object.keys(data); + + const gitRoot = execSync('git rev-parse --show-toplevel') + .toString() + .replace('\n', ''); + + const changedFiles = diff + .split('\n') + .filter(Boolean) + .map((path) => { + return upath.joinSafe(gitRoot, path); + }); + + const hasChangedFiles = changedFiles.some((file) => + trackedFiles.includes(file), + ); + + console.log(changedFiles); + + if (!hasChangedFiles) { + return; + } + } + } + } + return importOpenApi({ data, input, diff --git a/packages/orval/src/index.ts b/packages/orval/src/index.ts index 807f006cc..32b885429 100644 --- a/packages/orval/src/index.ts +++ b/packages/orval/src/index.ts @@ -8,6 +8,7 @@ import { import { generateConfig, generateSpec } from './generate'; import { defineConfig, normalizeOptions } from './utils/options'; import { startWatcher } from './utils/watcher'; +import { writeLastCommit } from './utils/write-last-commit'; const generate = async ( optionsExport?: string | OptionsExport, @@ -30,6 +31,7 @@ const generate = async ( async () => { try { await generateSpec(workspace, normalizedOptions); + await writeLastCommit(workspace); } catch (e) { logError(e, options?.projectName); } @@ -38,7 +40,8 @@ const generate = async ( ); } else { try { - return await generateSpec(workspace, normalizedOptions); + await generateSpec(workspace, normalizedOptions); + await writeLastCommit(workspace); } catch (e) { logError(e, options?.projectName); } diff --git a/packages/orval/src/utils/node-modules.ts b/packages/orval/src/utils/node-modules.ts new file mode 100644 index 000000000..01950c00c --- /dev/null +++ b/packages/orval/src/utils/node-modules.ts @@ -0,0 +1,8 @@ +import findUp from 'find-up'; + +export const getNodeModulesPath = async (workspace: string) => { + return await findUp(['node_modules'], { + cwd: workspace, + type: 'directory', + }); +}; diff --git a/packages/orval/src/utils/options.ts b/packages/orval/src/utils/options.ts index e2f5c6aee..726857bb5 100644 --- a/packages/orval/src/utils/options.ts +++ b/packages/orval/src/utils/options.ts @@ -79,8 +79,16 @@ export const normalizeOptions = async ( workspace, ); - const { clean, prettier, client, httpClient, mode, tslint, biome } = - globalOptions; + const { + clean, + prettier, + client, + httpClient, + mode, + tslint, + biome, + onChanges, + } = globalOptions; const tsconfig = await loadTsconfig( outputOptions.tsconfig || globalOptions.tsconfig, @@ -161,6 +169,7 @@ export const normalizeOptions = async ( baseUrl: outputOptions.baseUrl, unionAddMissingProperties: outputOptions.unionAddMissingProperties ?? false, + onChanges: outputOptions.onChanges ?? onChanges ?? false, override: { ...outputOptions.override, mock: { diff --git a/packages/orval/src/utils/read-last-commit.ts b/packages/orval/src/utils/read-last-commit.ts new file mode 100644 index 000000000..97b6848da --- /dev/null +++ b/packages/orval/src/utils/read-last-commit.ts @@ -0,0 +1,14 @@ +import { upath } from '@orval/core'; +import fs from 'fs-extra'; +import { getNodeModulesPath } from './node-modules'; + +export const readLastCommit = async (workspace: string) => { + const nodeModulesPath = await getNodeModulesPath(workspace); + + if (!nodeModulesPath) return; + + return fs.readFileSync( + upath.join(nodeModulesPath, './.orval/last-commit'), + 'utf8', + ); +}; diff --git a/packages/orval/src/utils/write-last-commit.ts b/packages/orval/src/utils/write-last-commit.ts new file mode 100644 index 000000000..f048482c4 --- /dev/null +++ b/packages/orval/src/utils/write-last-commit.ts @@ -0,0 +1,15 @@ +import { upath } from '@orval/core'; +import { execSync } from 'child_process'; +import fs from 'fs-extra'; +import { getNodeModulesPath } from './node-modules'; + +export const writeLastCommit = async (workspace: string) => { + const nodeModulesPath = await getNodeModulesPath(workspace); + + if (!nodeModulesPath) return; + + fs.outputFileSync( + upath.join(nodeModulesPath, './.orval/last-commit'), + execSync('git rev-parse HEAD').toString(), + ); +}; diff --git a/samples/react-query/basic/package.json b/samples/react-query/basic/package.json index 20d930802..3f7fe7b02 100644 --- a/samples/react-query/basic/package.json +++ b/samples/react-query/basic/package.json @@ -24,7 +24,7 @@ "start": "react-scripts start", "build": "react-scripts build", "eject": "react-scripts eject", - "generate-api": "node ../../../packages/orval/dist/bin/orval.js", + "generate-api": "node ../../../packages/orval/dist/bin/orval.js --onChanges", "test": "tsc --noEmit" }, "eslintConfig": { diff --git a/yarn.lock b/yarn.lock index fea96d833..767babbbb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1253,23 +1253,23 @@ __metadata: languageName: node linkType: hard -"@orval/angular@npm:7.3.0, @orval/angular@workspace:packages/angular": +"@orval/angular@npm:7.4.0, @orval/angular@workspace:packages/angular": version: 0.0.0-use.local resolution: "@orval/angular@workspace:packages/angular" dependencies: - "@orval/core": "npm:7.3.0" + "@orval/core": "npm:7.4.0" languageName: unknown linkType: soft -"@orval/axios@npm:7.3.0, @orval/axios@workspace:packages/axios": +"@orval/axios@npm:7.4.0, @orval/axios@workspace:packages/axios": version: 0.0.0-use.local resolution: "@orval/axios@workspace:packages/axios" dependencies: - "@orval/core": "npm:7.3.0" + "@orval/core": "npm:7.4.0" languageName: unknown linkType: soft -"@orval/core@npm:7.3.0, @orval/core@workspace:packages/core": +"@orval/core@npm:7.4.0, @orval/core@workspace:packages/core": version: 0.0.0-use.local resolution: "@orval/core@workspace:packages/core" dependencies: @@ -1308,62 +1308,62 @@ __metadata: languageName: unknown linkType: soft -"@orval/fetch@npm:7.3.0, @orval/fetch@workspace:packages/fetch": +"@orval/fetch@npm:7.4.0, @orval/fetch@workspace:packages/fetch": version: 0.0.0-use.local resolution: "@orval/fetch@workspace:packages/fetch" dependencies: - "@orval/core": "npm:7.3.0" + "@orval/core": "npm:7.4.0" languageName: unknown linkType: soft -"@orval/hono@npm:7.3.0, @orval/hono@workspace:packages/hono": +"@orval/hono@npm:7.4.0, @orval/hono@workspace:packages/hono": version: 0.0.0-use.local resolution: "@orval/hono@workspace:packages/hono" dependencies: - "@orval/core": "npm:7.3.0" - "@orval/zod": "npm:7.3.0" + "@orval/core": "npm:7.4.0" + "@orval/zod": "npm:7.4.0" "@types/lodash.uniq": "npm:^4.5.7" lodash.uniq: "npm:^4.5.0" languageName: unknown linkType: soft -"@orval/mock@npm:7.3.0, @orval/mock@workspace:packages/mock": +"@orval/mock@npm:7.4.0, @orval/mock@workspace:packages/mock": version: 0.0.0-use.local resolution: "@orval/mock@workspace:packages/mock" dependencies: - "@orval/core": "npm:7.3.0" + "@orval/core": "npm:7.4.0" lodash.get: "npm:^4.4.2" lodash.omit: "npm:^4.5.0" openapi3-ts: "npm:^4.2.2" languageName: unknown linkType: soft -"@orval/query@npm:7.3.0, @orval/query@workspace:packages/query": +"@orval/query@npm:7.4.0, @orval/query@workspace:packages/query": version: 0.0.0-use.local resolution: "@orval/query@workspace:packages/query" dependencies: - "@orval/core": "npm:7.3.0" - "@orval/fetch": "npm:7.3.0" + "@orval/core": "npm:7.4.0" + "@orval/fetch": "npm:7.4.0" "@types/lodash.omitby": "npm:^4.6.7" lodash.omitby: "npm:^4.6.0" vitest: "npm:^0.34.6" languageName: unknown linkType: soft -"@orval/swr@npm:7.3.0, @orval/swr@workspace:packages/swr": +"@orval/swr@npm:7.4.0, @orval/swr@workspace:packages/swr": version: 0.0.0-use.local resolution: "@orval/swr@workspace:packages/swr" dependencies: - "@orval/core": "npm:7.3.0" - "@orval/fetch": "npm:7.3.0" + "@orval/core": "npm:7.4.0" + "@orval/fetch": "npm:7.4.0" languageName: unknown linkType: soft -"@orval/zod@npm:7.3.0, @orval/zod@workspace:packages/zod": +"@orval/zod@npm:7.4.0, @orval/zod@workspace:packages/zod": version: 0.0.0-use.local resolution: "@orval/zod@workspace:packages/zod" dependencies: - "@orval/core": "npm:7.3.0" + "@orval/core": "npm:7.4.0" "@types/lodash.uniq": "npm:^4.5.7" lodash.uniq: "npm:^4.5.0" languageName: unknown @@ -7578,15 +7578,15 @@ __metadata: resolution: "orval@workspace:packages/orval" dependencies: "@apidevtools/swagger-parser": "npm:^10.1.0" - "@orval/angular": "npm:7.3.0" - "@orval/axios": "npm:7.3.0" - "@orval/core": "npm:7.3.0" - "@orval/fetch": "npm:7.3.0" - "@orval/hono": "npm:7.3.0" - "@orval/mock": "npm:7.3.0" - "@orval/query": "npm:7.3.0" - "@orval/swr": "npm:7.3.0" - "@orval/zod": "npm:7.3.0" + "@orval/angular": "npm:7.4.0" + "@orval/axios": "npm:7.4.0" + "@orval/core": "npm:7.4.0" + "@orval/fetch": "npm:7.4.0" + "@orval/hono": "npm:7.4.0" + "@orval/mock": "npm:7.4.0" + "@orval/query": "npm:7.4.0" + "@orval/swr": "npm:7.4.0" + "@orval/zod": "npm:7.4.0" "@types/inquirer": "npm:^9.0.6" "@types/js-yaml": "npm:^4.0.8" "@types/lodash.uniq": "npm:^4.5.8"