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

feat(shared): Support passing env source into runtime environment helpers [SDK-773] #1881

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/wicked-squids-attend.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@clerk/shared': patch
---

Support passing the env as argument in runtime environment helpers (eg isDevelopmentEnvironment())
to allow using those helpers with per-request environment runtimes (eg Cloudflare workers).
219 changes: 219 additions & 0 deletions packages/shared/src/utils/runtimeEnvironment.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
import { isDevelopmentEnvironment, isProductionEnvironment, isTestEnvironment } from './runtimeEnvironment';

async function withEnv(name: string, value: any, cb: () => any | Promise<any>) {
const currentValue = process.env[name];

process.env[name] = value;
const res = await cb();

process.env[name] = currentValue;
return res;
}

describe('isDevelopmentEnvironment(env)', () => {
test('is false for undefined process.env.NODE_ENV', async () => {
const isDev = await withEnv('NODE_ENV', undefined, () => {
return isDevelopmentEnvironment();
});

expect(isDev).toBe(false);
});

test('is false for non-development process.env.NODE_ENV', async () => {
const isDev = await withEnv('NODE_ENV', 'whatever', () => {
return isDevelopmentEnvironment();
});

expect(isDev).toBe(false);
});

test('is true for development process.env.NODE_ENV', async () => {
const isDev = await withEnv('NODE_ENV', 'development', () => {
return isDevelopmentEnvironment();
});

expect(isDev).toBe(true);
});

describe('when env is passed as parameter', () => {
test('is false for non-development process.env.NODE_ENV', async () => {
const isDev = await withEnv('NODE_ENV', 'whatever', () => {
return isDevelopmentEnvironment(process.env);
});

expect(isDev).toBe(false);
});

test('is true for development process.env.NODE_ENV', async () => {
const isDev = await withEnv('NODE_ENV', 'development', () => {
return isDevelopmentEnvironment(process.env);
});

expect(isDev).toBe(true);
});
});

// This case is related to per-request env on Cloudflare fetch handler
describe('when env-like object is passed as parameter', () => {
test('is false for non-development process.env.NODE_ENV', async () => {
const isDev = isDevelopmentEnvironment({ NODE_ENV: 'whatever' });
expect(isDev).toBe(false);
});

test('is true for development process.env.NODE_ENV', async () => {
const isDev = isDevelopmentEnvironment({ NODE_ENV: 'development' });
expect(isDev).toBe(true);
});
});

describe('when publishableKey is passed as parameter', () => {
test('is false for non-development process.env.NODE_ENV', async () => {
const isDev = isDevelopmentEnvironment('pk_live_ZXhhbXBsZS5jbGVyay5hY2NvdW50cy5kZXYk');
expect(isDev).toBe(false);
});

test('is true for development process.env.NODE_ENV', async () => {
const isDev = isDevelopmentEnvironment('pk_test_Zm9vLWJhci0xMy5jbGVyay5hY2NvdW50cy5kZXYk');
expect(isDev).toBe(true);
});
});
});

describe('isProductionEnvironment(env)', () => {
test('is false for undefined process.env.NODE_ENV', async () => {
const isDev = await withEnv('NODE_ENV', undefined, () => {
return isProductionEnvironment();
});

expect(isDev).toBe(false);
});

test('is false for non-production process.env.NODE_ENV', async () => {
const isDev = await withEnv('NODE_ENV', 'whatever', () => {
return isProductionEnvironment();
});

expect(isDev).toBe(false);
});

test('is true for production process.env.NODE_ENV', async () => {
const isDev = await withEnv('NODE_ENV', 'production', () => {
return isProductionEnvironment();
});

expect(isDev).toBe(true);
});

describe('when env is passed as parameter', () => {
test('is false for non-production process.env.NODE_ENV', async () => {
const isDev = await withEnv('NODE_ENV', 'whatever', () => {
return isProductionEnvironment(process.env);
});

expect(isDev).toBe(false);
});

test('is true for production process.env.NODE_ENV', async () => {
const isDev = await withEnv('NODE_ENV', 'production', () => {
return isProductionEnvironment(process.env);
});

expect(isDev).toBe(true);
});
});

// This case is related to per-request env on Cloudflare fetch handler
describe('when env-like object is passed as parameter', () => {
test('is false for non-production process.env.NODE_ENV', async () => {
const isDev = isProductionEnvironment({ NODE_ENV: 'whatever' });
expect(isDev).toBe(false);
});

test('is true for production process.env.NODE_ENV', async () => {
const isDev = isProductionEnvironment({ NODE_ENV: 'production' });
expect(isDev).toBe(true);
});
});

describe('when publishableKey is passed as parameter', () => {
test('is false for non-production process.env.NODE_ENV', async () => {
const isDev = isProductionEnvironment('pk_test_Zm9vLWJhci0xMy5jbGVyay5hY2NvdW50cy5kZXYk');
expect(isDev).toBe(false);
});

test('is true for production process.env.NODE_ENV', async () => {
const isDev = isProductionEnvironment('pk_live_ZXhhbXBsZS5jbGVyay5hY2NvdW50cy5kZXYk');
expect(isDev).toBe(true);
});
});
});

describe('isTestEnvironment(env)', () => {
test('is false for undefined process.env.NODE_ENV', async () => {
const isDev = await withEnv('NODE_ENV', undefined, () => {
return isTestEnvironment();
});

expect(isDev).toBe(false);
});

test('is false for non-test process.env.NODE_ENV', async () => {
const isDev = await withEnv('NODE_ENV', 'whatever', () => {
return isTestEnvironment();
});

expect(isDev).toBe(false);
});

test('is true for test process.env.NODE_ENV', async () => {
const isDev = await withEnv('NODE_ENV', 'test', () => {
return isTestEnvironment();
});

expect(isDev).toBe(true);
});

describe('when env is passed as parameter', () => {
test('is false non-test process.env.NODE_ENV', async () => {
const isDev = await withEnv('NODE_ENV', 'whatever', () => {
return isTestEnvironment(process.env);
});

expect(isDev).toBe(false);
});

test('is true for test process.env.NODE_ENV', async () => {
const isDev = await withEnv('NODE_ENV', 'test', () => {
return isTestEnvironment(process.env);
});

expect(isDev).toBe(true);
});
});

// This case is related to per-request env on Cloudflare fetch handler
describe('when env-like object is passed as parameter', () => {
test('is false for non-test process.env.NODE_ENV', async () => {
const isDev = isTestEnvironment({ NODE_ENV: 'whatever' });
expect(isDev).toBe(false);
});

test('is false for test process.env.NODE_ENV', async () => {
const isDev = isTestEnvironment({ NODE_ENV: 'test' });
expect(isDev).toBe(true);
});
});

// There is no publishableKey for test env (only prod /dev)
describe('when publishableKey is passed as parameter', () => {
test('is false for non-test process.env.NODE_ENV', async () => {
const isDev = isTestEnvironment('pk_test_Zm9vLWJhci0xMy5jbGVyay5hY2NvdW50cy5kZXYk');
expect(isDev).toBe(false);
});

test('is false for test process.env.NODE_ENV', async () => {
const isDev = isTestEnvironment('pk_live_ZXhhbXBsZS5jbGVyay5hY2NvdW50cy5kZXYk');
expect(isDev).toBe(false);
});
});
});
89 changes: 71 additions & 18 deletions packages/shared/src/utils/runtimeEnvironment.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,83 @@
export const isDevelopmentEnvironment = (): boolean => {
try {
return process.env.NODE_ENV === 'development';
// eslint-disable-next-line no-empty
} catch (err) {}
import { parsePublishableKey } from './keys';

// TODO: add support for import.meta.env.DEV that is being used by vite
/**
* Check for development runtime environment using the environment variables of the Clerk publishableKey.
*
* Examples:
*
* 1. using process.env: `isDevelopmentEnvironment()`
* 2. passing custom process.env source: `isDevelopmentEnvironment(import.meta.env)`
* 3. using publishableKey: `isDevelopmentEnvironment('pk_********')`
*
* @param envOrPublishableKey
* @returns boolean
*/
export const isDevelopmentEnvironment = (envOrPublishableKey?: EnvironmentOrPublishableKey): boolean => {
return isEnvironment('development', envOrPublishableKey);
};

return false;
/**
* Check for testing runtime environment using the environment variables of the Clerk publishableKey.
*
* Examples:
*
* 1. using process.env: `isTestEnvironment()`
* 2. passing custom process.env source: `isTestEnvironment(import.meta.env)`
* 3. using publishableKey: `isTestEnvironment('pk_test_********')`
*
* The `isTestEnvironment()` will always be false for any publishableKey since development and production
* publishable keys are only supported.
*
* @param envOrPublishableKey
* @returns boolean
*/
export const isTestEnvironment = (envOrPublishableKey?: EnvironmentOrPublishableKey): boolean => {
return isEnvironment('test', envOrPublishableKey);
};

export const isTestEnvironment = (): boolean => {
try {
return process.env.NODE_ENV === 'test';
// eslint-disable-next-line no-empty
} catch (err) {}
/**
* Check for production runtime environment using the environment variables of the Clerk publishableKey.
*
* Examples:
*
* 1. using process.env: `isProductionEnvironment()`
* 2. passing custom process.env source: `isProductionEnvironment(import.meta.env)`
* 3. using publishableKey: `isProductionEnvironment('pk_live_********')`
*
* @param envOrPublishableKey
* @returns boolean
*/
export const isProductionEnvironment = (envOrPublishableKey?: EnvironmentOrPublishableKey): boolean => {
return isEnvironment('production', envOrPublishableKey);
};

// TODO: add support for import.meta.env.DEV that is being used by vite
return false;
const getRuntimeFromProcess = (env?: typeof process.env) => {
return env?.NODE_ENV;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dimkl this one applies only on node environments, right?
Or, if you pass an env object, it should include a NODE_ENV property for this to work, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, it applies for all runtimes (Node, Cloudflare Workers, ...).
If you pass an env object with the NODE_ENV property, it should work.
I probably need to test if the process.env type works for Vite and Cloudflare workers.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dimkl That's what I meant. You need to have NODE_ENV in your passed object for this to work. NODE_ENV usually refers to node environments.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that this name is a bit misleading. At the moment the function assumes that NODE_ENV exists and that it's Node.js

You can do a check for the runtime like this: https://github.com/unjs/std-env/blob/main/src/runtimes.ts

Probably the whole "runtime" name (also for the file name) is a bit misleading as we only check the NODE_ENV environment 🤔

};

const getRuntimeFromPublishableKey = (publishableKey: string) => {
return parsePublishableKey(publishableKey)?.instanceType;
};

const isPublishableKey = (envOrPublishableKey?: string | typeof process.env): envOrPublishableKey is string => {
return typeof envOrPublishableKey === 'string';
};

export const isProductionEnvironment = (): boolean => {
type Environment = 'development' | 'production' | 'test';
type EnvironmentOrPublishableKey = typeof process.env | string;

// Allow passing env as arg to support Cloudflare workers case
function isEnvironment(environment: Environment, envOrPublishableKey?: EnvironmentOrPublishableKey) {
try {
return process.env.NODE_ENV === 'production';
envOrPublishableKey ||= process?.env;
// eslint-disable-next-line no-empty
} catch (err) {}

if (isPublishableKey(envOrPublishableKey)) {
return getRuntimeFromPublishableKey(envOrPublishableKey) === environment;
}

// TODO: add support for import.meta.env.DEV that is being used by vite
return false;
};

return getRuntimeFromProcess(envOrPublishableKey) === environment;
}