openapi-ts icon indicating copy to clipboard operation
openapi-ts copied to clipboard

Allow node references as baseUrl values

Open jscarle opened this issue 10 months ago • 32 comments

The workflow has changed over the course of the versions, now there seems to be no way to stop the tooling for setting the baseUrl in the client.gen.ts.

I set it manually to this:

export const client = createClient(
  createConfig<ClientOptions>({
    baseUrl: import.meta.env.VITE_API_BASE_URL,
  })
);

But as soon as I run openapi-ts, it's overwritten. Setting import.meta.env in the openapi-ts.config.ts throws an error. So I'm not sure how to fix this, and this gets annoying pretty quickly as I run openapi-ts quite a lot.

jscarle avatar Mar 16 '25 03:03 jscarle

Why does setting it in the config throw an error? What's the error message?

mrlubos avatar Mar 16 '25 14:03 mrlubos

It's not that it throws an error, it's that it prevents the usage of an environment variable when you're deploying to multiple environments.

jscarle avatar Mar 21 '25 18:03 jscarle

Can you share the config that doesn't work? (your current config)

mrlubos avatar Mar 21 '25 18:03 mrlubos

This openapi-ts.config.mts:

import { defineConfig } from '@hey-api/openapi-ts';

export default defineConfig({
  input: 'https://api.dev.domain.com/openapi.json',
  output: 'src/api',
  plugins: ['@hey-api/client-fetch'],
});

Creates this client.gen.ts:

// This file is auto-generated by @hey-api/openapi-ts

import type { ClientOptions } from './types.gen';
import { type Config, type ClientOptions as DefaultClientOptions, createClient, createConfig } from '@hey-api/client-fetch';

/**
 * The `createClientConfig()` function will be called on client initialization
 * and the returned object will become the client's initial configuration.
 *
 * You may want to initialize your client this way instead of calling
 * `setConfig()`. This is useful for example if you're using Next.js
 * to ensure your client always has the correct values.
 */
export type CreateClientConfig<T extends DefaultClientOptions = ClientOptions> = (override?: Config<DefaultClientOptions & T>) => Config<Required<DefaultClientOptions> & T>;

export const client = createClient(createConfig<ClientOptions>({
    baseUrl: 'https://api.dev.domain.com'
}));

But I need this openapi-ts.config.mts:

import { defineConfig } from '@hey-api/openapi-ts';

export default defineConfig({
  input: import.meta.env.VITE_API_BASE_URL,
  output: 'src/api',
  plugins: ['@hey-api/client-fetch'],
});

To create this client.gen.ts:

// This file is auto-generated by @hey-api/openapi-ts

import type { ClientOptions } from './types.gen';
import { type Config, type ClientOptions as DefaultClientOptions, createClient, createConfig } from '@hey-api/client-fetch';

/**
 * The `createClientConfig()` function will be called on client initialization
 * and the returned object will become the client's initial configuration.
 *
 * You may want to initialize your client this way instead of calling
 * `setConfig()`. This is useful for example if you're using Next.js
 * to ensure your client always has the correct values.
 */
export type CreateClientConfig<T extends DefaultClientOptions = ClientOptions> = (override?: Config<DefaultClientOptions & T>) => Config<Required<DefaultClientOptions> & T>;

export const client = createClient(createConfig<ClientOptions>({
    baseUrl: import.meta.env.VITE_API_BASE_URL,
}));

Instead it throws this:

🚫 missing input - which OpenAPI specification should we use to generate your output?
Error: 🚫 missing input - which OpenAPI specification should we use to generate your output?
    at E:\project\node_modules\@hey-api\openapi-ts\dist\index.cjs:15:60793
    at Array.map (<anonymous>)
    at nc (E:\project\node_modules\@hey-api\openapi-ts\dist\index.cjs:15:60567)
    at async aj (E:\project\node_modules\@hey-api\openapi-ts\dist\index.cjs:1311:4415)
    at async start (E:\project\node_modules\@hey-api\openapi-ts\bin\index.cjs:124:21)

jscarle avatar Mar 21 '25 23:03 jscarle

If you log your variable in the config file, is it set? You can also manually set baseUrl in the client plugin config

mrlubos avatar Mar 22 '25 00:03 mrlubos

It is not set. Come to think of it, makes sense why. The variable is site as part of Vite's build process, which obviously the tool wouldn't use. And even if it was set, it wouldn't really change the issue. As the tool generates the client.gen.ts and that's the one that needs to have the import.meta.env set.

jscarle avatar Mar 22 '25 01:03 jscarle

Is there still an ask?

mrlubos avatar Mar 22 '25 02:03 mrlubos

The only solution I can see would be to add a configuration option. Two ideas I have would be:

Allowing a flag to turn off overwriting:

export default {
  input: 'https://get.heyapi.dev/hey-api/backend',
  output: 'src/client',
  plugins: [
    {
      name: '@hey-api/client-fetch',
      overwrite: false, 
    },
  ],
};

or a flag to set the baseUrl:

export default {
  input: 'https://get.heyapi.dev/hey-api/backend',
  output: 'src/client',
  plugins: [
    {
      name: '@hey-api/client-fetch',
      baseUrl: 'import.meta.env.VITE_API_BASE_URL', 
    },
  ],
};

But I feel like that second option could quickly turn into a maintenance headache as you'd have to look at the contents of the string and if it was a URL then output as a string literal, otherwise dump the contents as typescript.

jscarle avatar Mar 22 '25 16:03 jscarle

The second one already exists. Also doesn't process.env.VITE_API_BASE_URL work?

mrlubos avatar Mar 22 '25 16:03 mrlubos

No, because in a Vite/Vue.js application, the variable is replaced at build time.

https://vite.dev/guide/env-and-mode.html

jscarle avatar Mar 23 '25 01:03 jscarle

Please add a reproducible example if you can't figure out a way to solve this

mrlubos avatar Mar 23 '25 02:03 mrlubos

https://stackblitz.com/edit/hey-api-client-fetch-example-tnzcbbvi?file=src%2Fclient%2Fclient.gen.ts

This setup is what the end result should be. As soon as you run, npx openapi-ts, the import.meta.env.VITE_API_BASE_URL reference will be overwritten with the Server URL found in the Open API definition and the value stored in the .env file will no longer be used.

jscarle avatar Mar 23 '25 17:03 jscarle

Does this not work? https://stackblitz.com/edit/hey-api-client-fetch-example-q9pnqjg7?file=src%2Fclient%2Fclient.gen.ts,package.json,openapi-ts.config.ts

You can also set the value there (see my commented out line above). We never supported passing the reference as you have in your workaround where the generated client would use import.meta.env.VITE_API_BASE_URL, did that use to work for you?

mrlubos avatar Mar 23 '25 18:03 mrlubos

To summarise, you can set the generated client base URL to any primitive value, infer from the document, or skip it entirely. But we never explicitly supported setting it to a variable reference such as import.meta.env.VITE_API_BASE_URL. If that's the ask, we can talk about it.

mrlubos avatar Mar 23 '25 18:03 mrlubos

In a perfect world, the ask would indeed be to allow explicitly setting baseUrl to a variable reference to support such scenarios as build variables like import.meta.env.VITE_API_BASE_URL, but I could see that being a development challenge. I see two alternatives as well. Either a flag to either ignore server URLs found in processed Open API definitions so that baseUrl is not set by generated code, or a flag to not overwritte the client definition that's generated.

jscarle avatar Mar 23 '25 19:03 jscarle

flag to either ignore server URLs found in processed Open API definitions

I showed you how to do that in my example!

mrlubos avatar Mar 23 '25 19:03 mrlubos

You also have that in the migration notes https://heyapi.dev/openapi-ts/migrating#added-client-baseurl-option

mrlubos avatar Mar 23 '25 19:03 mrlubos

flag to either ignore server URLs found in processed Open API definitions

I showed you how to do that in my example!

The API URL still gets added to the generated definition

jscarle avatar Mar 23 '25 20:03 jscarle

Ah wait, hadn't seen that

jscarle avatar Mar 23 '25 20:03 jscarle

Ok, so setting baseUrl to false:

export default defineConfig({
  input: 'https://api.dev.domain.com/openapi.json',
  output: 'src/api',
  plugins: [
    {
      baseUrl: false,
      name: '@hey-api/client-fetch',
    },
  ],
});

Does stop including the server URL to the client definition, but then this requires me to set the baseUrl in the application startup:

client.setConfig({
  baseUrl: import.meta.env.VITE_API_BASE_URL,
});

However this now changes the problem from a configuration problem into a tree shaking problem as now that will add hey-api to the initial chunk that's loaded with the application.

jscarle avatar Mar 23 '25 21:03 jscarle

That tracks but it's not a regression, right? Are you able to show an example using an older version where it worked as you're describing?

mrlubos avatar Mar 23 '25 22:03 mrlubos

That's correct, it's not a regression.

jscarle avatar Mar 23 '25 23:03 jscarle

What's the practical difference between using the variable in config vs runtime? Aren't you ultimately setting it to the same value? Or does the variable change between codegen and actual runtime?

mrlubos avatar Mar 24 '25 01:03 mrlubos

The code gen is run locally, the final value is detemined by the CI/CD pipeline.

jscarle avatar Mar 24 '25 02:03 jscarle

I see. I'll try to come up with a clean way to achieve this. Side note, why not run codegen in CI too? I'm meant to add docs around this, I'm currently setting it up at @Banff2020 too

mrlubos avatar Mar 24 '25 03:03 mrlubos

I honestly do not see any way that code gen could be done in a CI/CD pipeline without the whole thing turning into an continuously exploding fireball.

Most projects that have a separate front end and back end usually have different technologies with different repositories, as such, it's unlikely that either one is built at the same time. Running code gen for the front end to consume the backend API during a CI/CD pipeline would only result in broken builds as any changes found would need a developer to adjust the code manually.

jscarle avatar Mar 24 '25 03:03 jscarle

Yes. That might be desirable though, it would prevent you from knowingly shipping broken clients. Without that, they'll be still broken, but you won't know until an error reporting tool catches it. But I get it's all a matter of preference. I built the platform so that codegen can be pinned to a static build too through commit SHA or version. I'm open to ideas what else to build there btw, this is just the initial feature set because people complained about API drift

mrlubos avatar Mar 24 '25 04:03 mrlubos

Yea, I could see the value of having codegen in a test pipeline as it'll prevent you from shipping a broken front end. I'll add that as a pre-build step to our current tests.

jscarle avatar Mar 24 '25 15:03 jscarle

As a workaround, I've started using this:

// devops/patch-client-gen.mjs
import { readFile, writeFile } from 'fs/promises';
import { fileURLToPath } from 'url';
import { dirname, resolve } from 'path';

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

const filePath = resolve(__dirname, '../src/api/client.gen.ts');

let content = await readFile(filePath, 'utf-8');

content = content.replace(/^\s*baseUrl:.*;?$/m, '    baseUrl: import.meta.env.VITE_API_BASE_URI');

await writeFile(filePath, content, 'utf-8');

console.log('✅ Patched client.gen.ts');

With the following commands in my package.json:

{
  "scripts": {
    "build": "run-p type-check \"build-only {@}\" --",
    "build-only": "node devops/patch-client-gen.mjs && vite build",
    "build-api": "openapi-ts",
  }
}

jscarle avatar Mar 29 '25 13:03 jscarle

How about the following, or something similar?

import { defineConfig } from '@hey-api/openapi-ts';
import { loadEnv } from 'vite';

process.env = {...process.env, ...loadEnv(process.env.NODE_ENV || 'development', process.cwd(), '')};

export default defineConfig({
  input:
    process.env.VITE_API_OPENAPI_URL,
  output: {
...

Infro avatar Jul 06 '25 03:07 Infro