diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index be09d18..ed9462c 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -74,7 +74,7 @@ jobs:
- name: Build
run: bun run build
- - uses: actions/upload-artifact@v2
+ - uses: actions/upload-artifact@v4
with:
name: build
path: dist
@@ -85,6 +85,7 @@ jobs:
needs: [lint, test, build]
steps:
- uses: actions/checkout@v4
+ - run: git --version
- uses: oven-sh/setup-bun@v1
with:
bun-version: latest
@@ -92,13 +93,13 @@ jobs:
- name: Install Packages
run: bun install --frozen-lockfile
- - uses: actions/download-artifact@v3
+ - uses: actions/download-artifact@v4
with:
name: build
path: dist
- name: Deploy
- run: bunx semantic-release
+ run: bunx semantic-release@^22
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
diff --git a/.swcrc b/.swcrc
new file mode 100644
index 0000000..32589e1
--- /dev/null
+++ b/.swcrc
@@ -0,0 +1,16 @@
+{
+ "$schema": "https://json.schemastore.org/swcrc",
+ "sourceMaps": "inline",
+ "jsc": {
+ "target": "es2022",
+ "parser": {
+ "syntax": "typescript",
+ "tsx": true
+ },
+ "transform": {
+ "react": {
+ "runtime": "automatic"
+ }
+ }
+ }
+}
diff --git a/README.md b/README.md
index 18204fb..64d0129 100644
--- a/README.md
+++ b/README.md
@@ -43,6 +43,22 @@ export function MySimpleInput({ className: externalClassName, value: externalVal
}
```
+## `useSubtleCrypto`
+
+```tsx
+import { useSubtleCrypto } from '@rain-cafe/react-utils';
+
+export type ProfileProps = {
+ email?: string;
+};
+
+export function Profile({ email }: ProfileProps) {
+ const hashedEmail = useSubtleCrypto('SHA-256', email);
+
+ return
;
+}
+```
+
[_**Want to Contribute?**_](/CONTRIBUTING.md)
[npm-version-image]: https://img.shields.io/npm/v/@rain-cafe/react-utils.svg
diff --git a/bun.lockb b/bun.lockb
index 67fe63d..923a35e 100755
Binary files a/bun.lockb and b/bun.lockb differ
diff --git a/jest.config.ts b/jest.config.ts
index b6fc5c0..27cb5f1 100644
--- a/jest.config.ts
+++ b/jest.config.ts
@@ -1,23 +1,19 @@
-import type { JestConfigWithTsJest } from 'ts-jest';
+import type { Config } from 'jest';
+import { readFileSync } from 'fs';
-const jestConfig: JestConfigWithTsJest = {
+// Changed sourcemaps to inline resolving issues with https://github.com/swc-project/swc/issues/3854
+const config = JSON.parse(readFileSync(`${__dirname}/.swcrc`, 'utf-8'));
+
+export default {
roots: ['/src'],
- testEnvironment: 'jsdom',
+ testEnvironment: '@happy-dom/jest-environment',
transform: {
- '^.+\\.tsx?$': [
- 'ts-jest',
- {
- tsconfig: 'tsconfig.json',
- },
- ],
+ '^.+\\.(t|j)sx?$': ['@swc/jest', config],
},
collectCoverageFrom: ['/src/**/*'],
-
coveragePathIgnorePatterns: ['__tests__'],
-
- setupFilesAfterEnv: ['@testing-library/jest-dom/extend-expect'],
-};
-
-export default jestConfig;
+ setupFilesAfterEnv: ['@inrupt/jest-jsdom-polyfills', '@testing-library/jest-dom/extend-expect'],
+ extensionsToTreatAsEsm: ['.ts', '.tsx'],
+} satisfies Config;
diff --git a/package.json b/package.json
index 44f3e7a..15a4786 100644
--- a/package.json
+++ b/package.json
@@ -25,6 +25,9 @@
"classnames": "^2.5.1"
},
"devDependencies": {
+ "@happy-dom/jest-environment": "^13.0.6",
+ "@inrupt/jest-jsdom-polyfills": "^3.0.2",
+ "@swc/jest": "^0.2.29",
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^14.0.0",
"@testing-library/user-event": "^14.4.3",
@@ -39,7 +42,6 @@
"eslint-plugin-react": "^7.33.1",
"eslint-plugin-unused-imports": "^3.0.0",
"jest": "^29.6.2",
- "jest-environment-jsdom": "^29.6.2",
"microbundle": "^0.15.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
diff --git a/src/hooks/__tests__/use-subtle-crypto.spec.tsx b/src/hooks/__tests__/use-subtle-crypto.spec.tsx
new file mode 100644
index 0000000..5a05abb
--- /dev/null
+++ b/src/hooks/__tests__/use-subtle-crypto.spec.tsx
@@ -0,0 +1,29 @@
+import { render, waitFor } from '@testing-library/react';
+import Chance from 'chance';
+import { useSubtleCrypto } from '../use-subtle-crypto';
+import { hash } from '../../utils/subtle-crypto';
+
+const chance = new Chance();
+
+describe('Crypto Hooks', () => {
+ describe('hook(useSubtleCrypto)', () => {
+ type ExampleComponentProps = {
+ value: string;
+ };
+
+ function ExampleComponent({ value }: ExampleComponentProps) {
+ const hashedValue = useSubtleCrypto('SHA-256', value);
+
+ return {hashedValue}
;
+ }
+
+ it('should cache the value', async () => {
+ const value = chance.string();
+ const expectedValue = await hash('SHA-256', value);
+
+ const component = render();
+
+ await waitFor(() => expect(component.getByText(expectedValue)).toBeTruthy());
+ });
+ });
+});
diff --git a/src/hooks/use-subtle-crypto.ts b/src/hooks/use-subtle-crypto.ts
new file mode 100644
index 0000000..1e38da8
--- /dev/null
+++ b/src/hooks/use-subtle-crypto.ts
@@ -0,0 +1,19 @@
+import { useEffect, useState } from 'react';
+import { Algorithms, hash } from '../utils/subtle-crypto';
+
+/**
+ * Returns a hashed version of the value provided
+ * @param algorithm the hashing algorithm to use
+ * @param value the value to hash
+ * @returns the hashed value
+ * @see https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/digest#browser_compatibility
+ */
+export function useSubtleCrypto(algorithm: Algorithms, value?: string | null): string | undefined {
+ const [hashedValue, setHashedValue] = useState();
+
+ useEffect(() => {
+ hash(algorithm, value).then(setHashedValue);
+ }, [algorithm, value]);
+
+ return hashedValue;
+}
diff --git a/src/utils/__tests__/subtle-crypto.spec.tsx b/src/utils/__tests__/subtle-crypto.spec.tsx
new file mode 100644
index 0000000..a0d9d4a
--- /dev/null
+++ b/src/utils/__tests__/subtle-crypto.spec.tsx
@@ -0,0 +1,25 @@
+import { Algorithms, hash } from '../../utils/subtle-crypto';
+
+describe('Crypto Utils', () => {
+ describe('fn(hash)', () => {
+ it.each([
+ ['SHA-1', 'a94a8fe5ccb19ba61c4c0873d391e987982fbbd3'],
+ ['SHA-256', '9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08'],
+ ['SHA-384', '768412320f7b0aa5812fce428dc4706b3cae50e02a64caa16a782249bfe8efc4b7ef1ccb126255d196047dfedf17a0a9'],
+ [
+ 'SHA-512',
+ 'ee26b0dd4af7e749aa1a8ee3c10ae9923f618980772e473f8819a5d4940e0db27ac185f8a0e1d5f84f88bc887fd67b143732c304cc5fa9ad8e6f57f50028a8ff',
+ ],
+ ])('should support %s', async (algorithm: Algorithms, expectedValue: string) => {
+ await expect(hash(algorithm, 'test')).resolves.toEqual(expectedValue);
+ });
+
+ it('should support being provided null', async () => {
+ await expect(hash('SHA-256', null)).resolves.toEqual(null);
+ });
+
+ it('should support being provided undefined', async () => {
+ await expect(hash('SHA-256', undefined)).resolves.toEqual(null);
+ });
+ });
+});
diff --git a/src/utils/subtle-crypto.ts b/src/utils/subtle-crypto.ts
new file mode 100644
index 0000000..5e5afc8
--- /dev/null
+++ b/src/utils/subtle-crypto.ts
@@ -0,0 +1,16 @@
+export type Algorithms = 'SHA-1' | 'SHA-256' | 'SHA-384' | 'SHA-512';
+
+export async function hash(algorithm: Algorithms, message?: string): Promise {
+ if (!message) return null;
+
+ const msgBuffer = new TextEncoder().encode(message);
+
+ // hash the message
+ const hashBuffer = await crypto.subtle.digest(algorithm, msgBuffer);
+
+ // convert ArrayBuffer to Array
+ const hashArray = Array.from(new Uint8Array(hashBuffer));
+
+ // convert bytes to hex string
+ return hashArray.map((b) => b.toString(16).padStart(2, '0')).join('');
+}