Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Lazy Parsing the Env #152

Open
arashi-dev opened this issue Dec 3, 2023 · 2 comments
Open

Lazy Parsing the Env #152

arashi-dev opened this issue Dec 3, 2023 · 2 comments

Comments

@arashi-dev
Copy link

I just recently started a monorepo project using create-t3-turbo which has many apps and many packages and also some of the packages have standalone scripts.
each app, package, and script requires some environment variables which have some common env variables (e.g. NODE_ENV, DATABASE_URL, FRONTEND_URL, etc.). For now, I made a util package that exports an env object created by createEnv that stores and parses the common env variables. but here is the problem: some of the common variables may not be used in a few of the scripts or packages. so, I have to still set the variables for the apps, packages, or scripts which even is not needed!

I thought maybe it would be great if this t3-env project could do something like this:

const lazyEnv = createLazyEnv({
  server: {
    DATABASE_URL: z.string().url(),
    OPEN_AI_API_KEY: z.string().min(1),
  },
  runtimeEnv: process.env,
})

// in different packages
const env = lazyEnv({
    import: ["DATABASE_URL"]
})

the advantages of this feature can be:

  1. DRY code; we don't need to repeat the zod validations in different packages and the createEnv boilerplate
  2. one source of truth
  3. easier to make a change to the project env variables such as renaming, adding, removing, and the changes to the validations

PS. I tried to make a wrapper for the createEnv to do this for me but I had to write it in typescript to keep the variable types. but as I am validating the variables inside .mjs configuration files such as next.config.mjs, I cannot write and import .ts files inside them.

@arashi-dev
Copy link
Author

arashi-dev commented Dec 3, 2023

I managed to make it work by adding this to the core code:

/core/index.ts
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function pick<T extends Record<string, any>, TKeys extends (keyof T)[]>(
  obj: T,
  keys: TKeys
): Pick<T, TKeys[number]> {
  const pickedItems: Pick<T, TKeys[number]> = {} as never;

  keys.forEach((key) => {
    if (obj.hasOwnProperty(key)) {
      pickedItems[key] = obj[key];
    }
  });

  return pickedItems;
}

export function createLazyEnv<
  TPrefix extends string | undefined,
  TServer extends Record<string, ZodType> = NonNullable<unknown>,
  TClient extends Record<string, ZodType> = NonNullable<unknown>,
  TShared extends Record<string, ZodType> = NonNullable<unknown>
>(opts: EnvOptions<TPrefix, TServer, TClient, TShared>) {
  const lazyEnv = <
    TIncludeKeys extends Extract<
      keyof TServer | keyof TClient | keyof TShared,
      string
    >[]
  >({
    include,
  }: {
    include: TIncludeKeys;
  }) => {
    const client =
      typeof opts.client === "object"
        ? pick(opts.client, include)
        : ({} as never);
    const server =
      typeof opts.server === "object"
        ? pick(opts.server, include)
        : ({} as never);
    const shared =
      typeof opts.shared === "object"
        ? pick(opts.shared, include)
        : opts.shared;

    return createEnv<
      TPrefix,
      Pick<TServer, TIncludeKeys[number]>,
      Pick<TClient, TIncludeKeys[number]>,
      Pick<TShared, TIncludeKeys[number]>
    >({
      ...opts,
      client,
      server,
      shared,
      clientPrefix: undefined as TPrefix,
    });
  };

  return lazyEnv;
}

const lazyEnv = createLazyEnv({
  server: {
    FOR_PROJECT_A: z.string(),
    FOR_PROJECT_B: z.coerce.number(),
    COMMON: z.coerce.boolean(),
  },
  runtimeEnv: process.env,
});

const env= lazyEnv({
  include: ["COMMON", "FOR_PROJECT_A"],
});

env.COMMON; // boolean - expected
env.FOR_PROJECT_A; // string - expected
env.FOR_PROJECT_B; // Error - expected

@g3-tin
Copy link

g3-tin commented Jan 30, 2024

Thanks for sharing @arashi-dev

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants