Skip to content
This repository has been archived by the owner on Dec 7, 2024. It is now read-only.

chore: update kit #57

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ concurrency:

env:
COMMON_INSTALL_COMMAND: pnpm install --frozen-lockfile --prefer-offline
COMMON_NODE_VERSION: 18.15.0
COMMON_NODE_VERSION: 20.9.0

jobs:
##################################################################################################
Expand Down
2 changes: 0 additions & 2 deletions packages/eslint-config-nerve/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,6 @@
"eslint-import-resolver-typescript": "^3.5.5",
"eslint-plugin-html": "^7.1.0",
"eslint-plugin-import": "2.27.5",
"eslint-plugin-jest": "^27.2.2",
"eslint-plugin-jest-dom": "^5.0.1",
"eslint-plugin-jsx-a11y": "^6.7.1",
"eslint-plugin-prettier": "^5.0.0",
"eslint-plugin-react": "^7.32.2",
Expand Down
25 changes: 21 additions & 4 deletions packages/kit/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@
"fix:lint": "eslint . --fix",
"test": "echo \"Bypassing: no test specified\" && exit 0"
},
"dependencies": {
"date-fns": "^3.3.1",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@nerve/tsconfig": "workspace:*",
"@types/node": "20.4.2",
Expand All @@ -24,29 +29,41 @@
"src"
],
"exports": {
"./env": {
"import": "./src/env/index.ts",
"require": "./dist/env.js"
},
"./node": {
"import": "./src/node/index.ts",
"require": "./dist/node.js"
},
"./react": {
"import": "./src/react/index.ts",
"require": "./dist/react.js"
},
"./utils": {
"import": "./src/utils/index.ts",
"require": "./dist/utils.js"
}
},
"typesVersions": {
"*": {
"env": [
"src/env/index.ts",
"dist/env.d.ts"
],
"node": [
"src/node/index.ts",
"dist/node.d.ts"
],
"react": [
"src/react/index.ts",
"dist/react.d.ts"
],
"utils": [
"src/utils/index.ts",
"dist/utils.d.ts"
]
}
},
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0"
}
}
66 changes: 66 additions & 0 deletions packages/kit/src/env/env-vars/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/**
* Resolves an environment variable with an optional default value.
*
* @param value The value of the environment variable.
* @param defaultValue The default value to use if the environment variable is falsy.
*/
export function resolveEnvVar(value: string | undefined, defaultValue: string): string;
export function resolveEnvVar(value?: string, defaultValue?: string): string | undefined;
export function resolveEnvVar(value?: string, defaultValue?: string): string | undefined {
return value || defaultValue;
}

/**
* Resolves an environment variable that is required. Throws an error if the value is falsy.
*
* * We don't allow for a default value here because we want to ensure that the environment variable is set.
* * If we want to allow a default value, we should use `resolveEnvVar` instead, as the env var isn't truly required.
*
* @param name The name of the required environment variable (only used in the error message)
* @param value The value of the required environment variable.
*/
export const resolveRequiredEnvVar = (name: string, value?: string): string => {
if (!value) {
// Allow missing env vars in CI since otherwise the BundleMon audit would fail
if (process.env.CI === 'true') {
return `missing-${name}-in-ci`;
}
throw new Error(`Missing required environment variable: '${name}'.`);
}
return value;
};

/**
* Converts a given string to a boolean based on commonly recognized boolean string representations.
* * If the value is falsy, we will return false.
*
* @param value The value to convert to a boolean.
*/
export const boolEnv = (value?: string): boolean => {
if (!value) {
return false;
}
return ['true', 'yes', 'on', '1'].includes(value.toLowerCase());
};

/**
* Converts a given string to a number.
* * If the value is falsy, we will return 0.
*
* @param value The value to convert to a number.
*/
export const numberEnv = (value?: string): number => {
return Number(value) || 0;
};

/**
* Converts a comma-separated string to a list.
*
* @param value The value to convert to a string array.
*/
export const listEnv = (value: string | undefined): string[] => {
if (!value) {
return [];
}
return value.split(',').map((s) => s.trim());
};
2 changes: 2 additions & 0 deletions packages/kit/src/env/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './server';
export * from './env-vars';
6 changes: 6 additions & 0 deletions packages/kit/src/env/server/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/**
* A simple utility function to check if we are on the server
*/
export const isServer = () => {
return typeof window === 'undefined';
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
'use client';

import { useCallback, useEffect, useRef } from 'react';

/**
* Debounce a callback function, preventing it from being called until a certain amount of time has passed since the last invocation.
* This is useful for preventing a function from being called too frequently.
*
* Note: This hook is not meant to be used as a one-time delay. If you need a one-time delay, use `useTimeout` instead.
*
* Example Scenario: Prevent an API request from firing every time an input changes as a user types
*
* Usage Example:
* ```tsx
* const debouncedSearch = useDebouncedCallback(search, 300);
*
* <input onChange={(e) => debouncedSearch(e.target.value)} />
* ```
*
* @param callback - The callback to debounce
* @param delay - The amount of time to wait after the latest invocation before calling the function
* @returns A debounced version of the callback function that can be called safely
*/
export const useDebouncedCallback = <Callback extends (...args: Parameters<Callback>) => void>(
callback: Callback,
delay: number
): ((...args: Parameters<Callback>) => void) => {
const callbackRef = useRef(callback);
const timeoutRef = useRef<ReturnType<typeof setTimeout>>();

useEffect(() => {
callbackRef.current = callback;
}, [callback]);

/**
* Debounced version of original callback for the consumer to safely call as many times as they please!
* Note: Props will match and be passed through the original callback.
*/
const debouncedCallback = useCallback(
(...args: Parameters<Callback>) => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}

timeoutRef.current = setTimeout(() => {
callbackRef.current(...args);
}, delay);
},
[delay]
);

// Clear timeout on unmount
useEffect(() => {
return () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
};
}, []);

return debouncedCallback;
};
29 changes: 29 additions & 0 deletions packages/kit/src/react/useInterval/useInterval.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { useEffect, useRef } from 'react';

/**
* useInterval repeatedly calls the callback with a period of delay (aka: throttle)
*
* @param callback
* @param delay
*/
export const useInterval = (callback: () => void, delay: number | null) => {
const savedCallback = useRef(callback);
const intervalRef = useRef<ReturnType<typeof setInterval>>();

// Remember the latest callback.
useEffect(() => {
savedCallback.current = callback;
}, [callback]);

// Set up the interval.
useEffect(() => {
function tick() {
savedCallback.current();
}

if (delay !== null) {
intervalRef.current = setInterval(tick, delay);
return clearInterval(intervalRef.current);
}
}, [delay]);
};
40 changes: 40 additions & 0 deletions packages/kit/src/react/useSingletonEffect/useSingletonEffect.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { useEffect, useRef } from 'react';

import { isServer } from '../../env/server';

/**
* A hook that is guaranteed to execute a callback only once per render (dependent on provided condition).
* ! The callback will not be executed on the server.
*
* @param callback - The callback to execute
* @param condition - The boolean or function condition under which to execute the callback
*/
export const useSingletonEffect = (callback: Callback, condition: Condition = true) => {
const isCalledOnce = useRef(false);

useEffect(() => {
if (isServer() || isCalledOnce.current) {
return;
}

if (shouldExecuteCallback(condition)) {
callback();

isCalledOnce.current = true;
}
}, [callback, condition]);
};

/**
* Determine if conditions are met to execute a callback
*/
const shouldExecuteCallback = (condition: Condition) => {
if (typeof condition === 'boolean') {
return condition;
}

return condition();
};

type Callback = () => void;
type Condition = boolean | (() => boolean);
30 changes: 30 additions & 0 deletions packages/kit/src/react/useTimeout/useTimeout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { useEffect, useRef } from 'react';

/**
* Delay, and then call callback.
*
* Note: Not meant to be used as a debounced callback, but rather as a one-time delay.
* If you need a debounced callback, use `useDebouncedCallback` instead.
*
* @param callback
* @param delay delay in milliseconds
*/
export const useTimeout = (callback: () => void, delay?: number | null) => {
const callbackRef = useRef(callback);
const timeoutRef = useRef<ReturnType<typeof setTimeout>>();

useEffect(() => {
callbackRef.current = callback;
}, [callback]);

useEffect(() => {
if (delay !== null) {
timeoutRef.current = setTimeout(() => callbackRef.current(), delay);
}
return () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
};
}, [delay]);
};
30 changes: 30 additions & 0 deletions packages/kit/src/react/useUpdateEffect/useUpdateEffect.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { type DependencyList, type EffectCallback, useEffect, useRef } from 'react';

import { isServer } from '../../env/server';

/**
* A hook that runs an callback only after the first render has ocurred. This is useful for when you want to effectively
* "skip" the first render, and only have an effect run on subsequent "update" renders.
*
* ! The callback will not be executed on the server.
*
* @param callback The effect to run after the first render
* @param deps The dependencies to watch for changes
*/
export const useUpdateEffect = (callback: EffectCallback, deps?: DependencyList) => {
const hasMounted = useRef(false);

useEffect(() => {
if (isServer()) {
return;
}

if (hasMounted.current) {
callback();
} else {
hasMounted.current = true;
}
// ! The eslint rule override below is intentional. We don't want to include `hasMounted` in the dependency list
// eslint-disable-next-line react-hooks/exhaustive-deps
}, deps);
};
12 changes: 12 additions & 0 deletions packages/kit/src/utils/arrays.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/**
* Removes all 'invalid' values from an array (undefined, null, NaN)
*
* @param array The array to remove all 'invalid' values from.
* @returns A new array with all 'invalid' values removed.
*/
export const removeInvalidValues = <T>(array: (T | undefined | null)[]): T[] => {
const isT = (t: T | undefined | null): t is T => {
return t !== undefined && t !== null && !Number.isNaN(t);
};
return array.filter(isT);
};
17 changes: 17 additions & 0 deletions packages/kit/src/utils/console.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/**
* A wrapper around console.debug that only logs if the env log level is set to DEBUG.
*
* This is a useful utility to use when we want to do a debug log but we are outside of the scope of any kind
* of internal logger service. In such cases, this util prevents unintended logging.
*
* Note: This is not a replacement for a true logger service. This is only meant to be used in cases where we are
* outside of the scope of the logger service. ALWAYS use a logger service when possible.
*/
export const consoleDebug = (message: unknown, ...args: unknown[]) => {
const logLevel = process.env.NEXT_PUBLIC_LOG_LEVEL || process.env.LOG_LEVEL;

if (logLevel !== 'DEBUG') return;

// eslint-disable-next-line no-console
console.debug(message, ...args);
};
Loading
Loading