From d0f1062aad8f0350f048ef5309cdbf71ea851126 Mon Sep 17 00:00:00 2001 From: Alvin Date: Sun, 9 Jun 2024 14:39:50 +0800 Subject: [PATCH] feat(login): add login redirect (#5) * use alias * add LoginButton, cookie handling * get code verifier on query param * fix styles * add config, url found * minor change * moved * update to @lib * add test and fix * mock cookies and fixes * clean up * refactor and add test * fix test * update test env * linter fix --- .env.test.local | 3 + .gitignore | 1 + README.md | 30 ------ index.html | 2 +- lib/__tests__/cookie.test.js | 100 ++++++++++++++++++ {src/lib => lib}/__tests__/pkce.test.js | 0 lib/cookie.ts | 30 ++++++ {src/lib => lib}/pkce.ts | 0 src/App.css | 42 -------- src/App.test.tsx | 2 +- src/App.tsx | 55 +++++----- src/components/LoginButton.tsx | 21 ++++ src/config.ts | 13 +++ src/hooks/__tests__/useInitPKCE.test.ts | 39 +++++++ src/hooks/useInitPKCE.ts | 21 ++++ src/index.css | 22 ++++ src/lib/__tests__/cookie.test.js | 56 ---------- src/lib/cookie.ts | 18 ---- src/lib/stateCookie.ts | 15 --- src/types/index.ts | 5 + src/utils/__tests__/auth.test.ts | 54 ++++++++++ .../__tests__/stateCookie.test.ts | 23 +++- src/utils/auth.ts | 45 ++++++++ src/utils/stateCookie.ts | 20 ++++ tsconfig.json | 7 +- vite.config.ts | 18 ++-- 26 files changed, 446 insertions(+), 196 deletions(-) create mode 100644 .env.test.local create mode 100644 lib/__tests__/cookie.test.js rename {src/lib => lib}/__tests__/pkce.test.js (100%) create mode 100644 lib/cookie.ts rename {src/lib => lib}/pkce.ts (100%) delete mode 100644 src/App.css create mode 100644 src/components/LoginButton.tsx create mode 100644 src/config.ts create mode 100644 src/hooks/__tests__/useInitPKCE.test.ts create mode 100644 src/hooks/useInitPKCE.ts delete mode 100644 src/lib/__tests__/cookie.test.js delete mode 100644 src/lib/cookie.ts delete mode 100644 src/lib/stateCookie.ts create mode 100644 src/types/index.ts create mode 100644 src/utils/__tests__/auth.test.ts rename src/{lib => utils}/__tests__/stateCookie.test.ts (70%) create mode 100644 src/utils/auth.ts create mode 100644 src/utils/stateCookie.ts diff --git a/.env.test.local b/.env.test.local new file mode 100644 index 0000000..74f2f83 --- /dev/null +++ b/.env.test.local @@ -0,0 +1,3 @@ +VITE_AUTH_URL=http://test-auth-api/api/authorize +VITE_TOKEN_URL=http://test-token-api/api/oauth/token +VITE_BASE_URL=http://test-base-url diff --git a/.gitignore b/.gitignore index a547bf3..dbeb9a2 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ node_modules dist dist-ssr *.local +!.env.test.local # Editor directories and files .vscode/* diff --git a/README.md b/README.md index 0d6babe..e69de29 100644 --- a/README.md +++ b/README.md @@ -1,30 +0,0 @@ -# React + TypeScript + Vite - -This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. - -Currently, two official plugins are available: - -- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh -- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh - -## Expanding the ESLint configuration - -If you are developing a production application, we recommend updating the configuration to enable type aware lint rules: - -- Configure the top-level `parserOptions` property like this: - -```js -export default { - // other rules... - parserOptions: { - ecmaVersion: 'latest', - sourceType: 'module', - project: ['./tsconfig.json', './tsconfig.node.json'], - tsconfigRootDir: __dirname, - }, -} -``` - -- Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked` -- Optionally add `plugin:@typescript-eslint/stylistic-type-checked` -- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list diff --git a/index.html b/index.html index e4b78ea..770a8aa 100644 --- a/index.html +++ b/index.html @@ -4,7 +4,7 @@ - Vite + React + TS + Refresh Token Rotation Demo
diff --git a/lib/__tests__/cookie.test.js b/lib/__tests__/cookie.test.js new file mode 100644 index 0000000..afd8b6c --- /dev/null +++ b/lib/__tests__/cookie.test.js @@ -0,0 +1,100 @@ +import { describe, test, expect, beforeAll, afterAll, vi, afterEach } from 'vitest' +import { saveCookie, getCookie, deleteCookie, clearAllCookies } from '../cookie' + +describe('cookie', () => { + let mockCookie = [] + beforeAll(() => { + vi.stubGlobal('document', { + get cookie() { + return mockCookie.join(';') + }, + set cookie(value) { + mockCookie = mockCookie.filter(c => !c.startsWith(value.split('=')[0])) + mockCookie.push(value) + }, + }) + }) + + afterEach(() => { + mockCookie = [] + }) + + afterAll(() => { + vi.unstubAllGlobals() + }) + + describe('saveCookie', () => { + test('throws error on empty name', () => { + expect(() => saveCookie()).toThrowError('Cookie name is required') + }) + + test('saves empty value', () => { + saveCookie('a', '') + expect(document.cookie).toMatch('a=;') + }) + + test('saves cookie', () => { + saveCookie('a', '1234') + expect(document.cookie).toMatch('a=1234;') + }) + + test('saves cookie with latest value', () => { + saveCookie('a', '1234') + saveCookie('a', '124') + expect(document.cookie).toMatch('a=124;') + expect(document.cookie).not.toMatch('a=1234;') + }) + }) + + describe('getCookie', () => { + test('gets none', () => { + saveCookie('b', '54321') + expect(getCookie('c')).toBeUndefined() + }) + + test('gets cookie', () => { + saveCookie('b', '54321') + expect(getCookie('b')).toBe('54321') + }) + }) + + describe('deleteCookie', () => { + test('deletes cookie', () => { + saveCookie('a', '12345') + expect(document.cookie).toMatch('a=12345;') + + deleteCookie('a') + expect(document.cookie).toMatch('a=;') + }) + + test('deletes cookie without affecting others', () => { + saveCookie('a', '12345') + expect(document.cookie).toMatch('a=12345;') + + saveCookie('b', '54321') + expect(document.cookie).toMatch('b=54321;') + + deleteCookie('a') + expect(document.cookie).toMatch('a=;') + expect(document.cookie).toMatch('b=54321;') + }) + + test('deletes none', () => { + saveCookie('a', '12345') + saveCookie('b', '54321') + deleteCookie('c') + expect(document.cookie).toMatch('a=12345;') + expect(document.cookie).toMatch('b=54321;') + }) + }) + + describe('clearAllCookies', () => { + test('clears all cookies', () => { + saveCookie('a', '12345') + saveCookie('b', '54321') + clearAllCookies() + expect(document.cookie).toMatch('a=;') + expect(document.cookie).toMatch('b=;') + }) + }) +}) diff --git a/src/lib/__tests__/pkce.test.js b/lib/__tests__/pkce.test.js similarity index 100% rename from src/lib/__tests__/pkce.test.js rename to lib/__tests__/pkce.test.js diff --git a/lib/cookie.ts b/lib/cookie.ts new file mode 100644 index 0000000..091d26f --- /dev/null +++ b/lib/cookie.ts @@ -0,0 +1,30 @@ +export const saveCookie = (name: string, value: string, mins: number = 60) => { + if (!name) + throw new Error('Cookie name is required') + + const date = new Date() + date.setTime(date.getTime() + mins * 60 * 1000) + document.cookie = + `${name}=${value};Expires=${date.toUTCString()}; \ + path=/; Secure; SameSite=Strict` +} + +export const getCookie = (name: string) => { + const value = `; ${document.cookie}` + const parts = value.split(`; ${name}=`) + if (parts.length === 2) return parts.pop()?.split(';').shift() +} + +export const deleteCookie = (name: string) => { + document.cookie = `${name}=;expires=Thu, 01 Jan 1970 00:00:00 GMT` +} + +export const clearAllCookies = () => { + const cookies = document.cookie.split(";") + for (let i = 0; i < cookies.length; i++) { + const cookie = cookies[i] + const eqPos = cookie.indexOf("=") + const name = eqPos > -1 ? cookie.substring(0, eqPos) : cookie + document.cookie = `${name}=;expires=Thu, 01 Jan 1970 00:00:00 GMT` + } +} diff --git a/src/lib/pkce.ts b/lib/pkce.ts similarity index 100% rename from src/lib/pkce.ts rename to lib/pkce.ts diff --git a/src/App.css b/src/App.css deleted file mode 100644 index b9d355d..0000000 --- a/src/App.css +++ /dev/null @@ -1,42 +0,0 @@ -#root { - max-width: 1280px; - margin: 0 auto; - padding: 2rem; - text-align: center; -} - -.logo { - height: 6em; - padding: 1.5em; - will-change: filter; - transition: filter 300ms; -} -.logo:hover { - filter: drop-shadow(0 0 2em #646cffaa); -} -.logo.react:hover { - filter: drop-shadow(0 0 2em #61dafbaa); -} - -@keyframes logo-spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } -} - -@media (prefers-reduced-motion: no-preference) { - a:nth-of-type(2) .logo { - animation: logo-spin infinite 20s linear; - } -} - -.card { - padding: 2em; -} - -.read-the-docs { - color: #888; -} diff --git a/src/App.test.tsx b/src/App.test.tsx index ad60949..39c3d01 100644 --- a/src/App.test.tsx +++ b/src/App.test.tsx @@ -12,5 +12,5 @@ test('renders text', () => { expect(wrapper).toBeTruthy() const { getByText } = wrapper - expect(getByText('Vite + React')).toBeTruthy() + expect(getByText('Login')).toBeTruthy() }) diff --git a/src/App.tsx b/src/App.tsx index afe48ac..35845dc 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,33 +1,38 @@ -import { useState } from 'react' -import reactLogo from './assets/react.svg' -import viteLogo from '/vite.svg' -import './App.css' +import { useEffect, useState } from 'react' + +import { getPKCEStatus } from '@/utils/auth' +import LoginButton from '@/components/LoginButton' +import { clearAllCookies } from '@lib/cookie' function App() { - const [count, setCount] = useState(0) + const params = new URLSearchParams(window.location.search) + const state = params.get('state') + const code = params.get('code') + + const [isAuthReady, setIsAuthReady] = useState(false) + const [status, setStatus] = useState('') + + useEffect(() => { + if (state) { + const { isDone: isAuthReady, codeVerifier } = + getPKCEStatus(state) + + if (isAuthReady) { + setIsAuthReady(isAuthReady) + setStatus(`code: ${code}, codeVerifier: ${codeVerifier}`) + } + } + + return () => clearAllCookies() + // on mount only + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) return ( <> -
- - Vite logo - - - React logo - -
-

Vite + React

-
- -

- Edit src/App.tsx and save to test HMR -

-
-

- Click on the Vite and React logos to learn more -

+ {isAuthReady &&

Ready for Auth Request

} + {status &&

{status}

} + ) } diff --git a/src/components/LoginButton.tsx b/src/components/LoginButton.tsx new file mode 100644 index 0000000..aa24280 --- /dev/null +++ b/src/components/LoginButton.tsx @@ -0,0 +1,21 @@ +import useInitPKCE from '@/hooks/useInitPKCE' + +const LoginButton = () => { + const { error, onLogin } = useInitPKCE() + + return ( + <> + +
+        {error}
+      
+
+        {document.cookie}
+      
+ + ) +} + +export default LoginButton diff --git a/src/config.ts b/src/config.ts new file mode 100644 index 0000000..74ef65d --- /dev/null +++ b/src/config.ts @@ -0,0 +1,13 @@ +const BASE_URL = import.meta.env.VITE_BASE_URL +const LOGIN_URL = import.meta.env.VITE_AUTH_URL +const TOKEN_URL = import.meta.env.VITE_TOKEN_URL + +const STATE_COOKIE_PREFIX = "app.txs." + +export default { + BASE_URL, + LOGIN_URL, + TOKEN_URL, + + STATE_COOKIE_PREFIX +} diff --git a/src/hooks/__tests__/useInitPKCE.test.ts b/src/hooks/__tests__/useInitPKCE.test.ts new file mode 100644 index 0000000..df2e313 --- /dev/null +++ b/src/hooks/__tests__/useInitPKCE.test.ts @@ -0,0 +1,39 @@ +import { beforeAll, expect, describe, test, vi, afterAll } from 'vitest' +import { renderHook } from '@testing-library/react' + +import useInitPKCE from '../useInitPKCE' +import config from '@/config' + +describe('useInitPKCE', () => { + const redirect = vi.fn() + beforeAll(() => { + vi.stubGlobal('location', { replace: redirect }) + }) + + afterAll(() => { + vi.unstubAllGlobals() + }) + + test('returns empty error and onLogin', () => { + const { result } = renderHook(() => useInitPKCE()) + expect(result.current).toMatchObject({ + error: '', + onLogin: expect.any(Function), + }) + }) + + test('redirects to login url', async () => { + const { result } = renderHook(() => useInitPKCE()) + await result.current.onLogin() + + expect(redirect).toHaveBeenCalled() + const url = redirect.mock.calls[0][0] + const query = new URLSearchParams(url.split('?')[1]) + + expect(query.get('response_type')).toBe('code,id_token') + expect(query.get('redirect_uri')).toBe(config.BASE_URL) + expect(query.get('state')).toEqual(expect.any(String)) + expect(query.get('code_challenge')).toEqual(expect.any(String)) + }) +}) + diff --git a/src/hooks/useInitPKCE.ts b/src/hooks/useInitPKCE.ts new file mode 100644 index 0000000..8b3b542 --- /dev/null +++ b/src/hooks/useInitPKCE.ts @@ -0,0 +1,21 @@ +import { useCallback, useState } from 'react' +import { createPKCECodes, redirectToLogin } from '@/utils/auth' + +const useInitPKCE = () => { + const [error, setError] = useState('') + + const onLogin = useCallback(async () => { + try { + const codes = await createPKCECodes() + redirectToLogin(codes.state, codes.codeChallenge) + } catch (error) { + if (error instanceof Error) + setError(error.message) + else setError('An unknown error occurred') + } + }, []) + + return { error, onLogin } +} + +export default useInitPKCE diff --git a/src/index.css b/src/index.css index 6119ad9..58643dd 100644 --- a/src/index.css +++ b/src/index.css @@ -66,3 +66,25 @@ button:focus-visible { background-color: #f9f9f9; } } + +#root { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; +} + +pre { + max-width: 50vw; + text-align: left; + overflow: hidden; + white-space: wrap; + word-wrap: normal; + font-size: 0.9em; + line-height: 1.5; + border-radius: 8px; + padding: 1em; +} + +.error { + color: red; +} diff --git a/src/lib/__tests__/cookie.test.js b/src/lib/__tests__/cookie.test.js deleted file mode 100644 index 0f6bb74..0000000 --- a/src/lib/__tests__/cookie.test.js +++ /dev/null @@ -1,56 +0,0 @@ -import { describe, test, expect, beforeEach } from "vitest" -import { saveCookie, getCookie, deleteCookie } from "../cookie" - -describe("saveCookie", () => { - test("throws error on empty name", () => { - expect(() => saveCookie()) - .toThrowError("Cookie name is required") - }) - - test("saves empty value", () => { - saveCookie("a", "") - expect(document.cookie).toMatch("a=") - }) - - test("saves cookie", () => { - saveCookie("a", "1234") - expect(document.cookie).toMatch("a=1234") - }) - - test("saves cookie with latest value", () => { - saveCookie("a", "1234") - saveCookie("a", "124") - expect(document.cookie).toMatch("a=124") - expect(document.cookie).not.toMatch("a=1234") - }) -}) - -describe("getCookie", () => { - test("gets none", () => { - saveCookie("b", "54321") - expect(getCookie("c")).toBeUndefined() - }) - - test("gets cookie", () => { - saveCookie("b", "54321") - expect(getCookie("b")).toBe("54321") - }) -}) - -describe("deleteCookie", () => { - test("deletes cookie", () => { - saveCookie("c", "12345") - saveCookie("d", "54321") - deleteCookie("c") - expect(document.cookie).not.toMatch("c=12345") - expect(document.cookie).toMatch("d=54321") - }) - - test("deletes none", () => { - saveCookie("one", "12345") - saveCookie("two", "54321") - deleteCookie("e") - expect(document.cookie).toMatch("one=12345") - expect(document.cookie).toMatch("two=54321") - }) -}) diff --git a/src/lib/cookie.ts b/src/lib/cookie.ts deleted file mode 100644 index 47aed71..0000000 --- a/src/lib/cookie.ts +++ /dev/null @@ -1,18 +0,0 @@ -export const saveCookie = (name: string, value: string, mins: number = 60) => { - if (!name) - throw new Error('Cookie name is required') - - const date = new Date() - date.setTime(date.getTime() + mins * 60 * 1000) - document.cookie = `${name}=${value};Expires=${date.toUTCString()}; path=/; Secure; SameSite=None` -} - -export const getCookie = (name: string) => { - const value = `; ${document.cookie}` - const parts = value.split(`; ${name}=`) - if (parts.length === 2) return parts.pop()?.split(';').shift() -} - -export const deleteCookie = (name: string) => { - document.cookie = `${name}=; Max-Age=0; path=/; Secure; SameSite=None` -} diff --git a/src/lib/stateCookie.ts b/src/lib/stateCookie.ts deleted file mode 100644 index e6cd8c0..0000000 --- a/src/lib/stateCookie.ts +++ /dev/null @@ -1,15 +0,0 @@ - -import { saveCookie, getCookie } from './cookie' - -export const createStateCookie = (value: string) => { - if (!value) throw new Error('state value is required') - - const state = crypto.randomUUID() - saveCookie(`app.txs.${state}`, value, 30) - - return state -} - -export const getStateCookie = (state: string) => { - return getCookie(`app.txs.${state}`) -} diff --git a/src/types/index.ts b/src/types/index.ts new file mode 100644 index 0000000..5ad2e54 --- /dev/null +++ b/src/types/index.ts @@ -0,0 +1,5 @@ +export type PKCEState = { + isDone: boolean, + codeVerifier?: string, + code?: string +} diff --git a/src/utils/__tests__/auth.test.ts b/src/utils/__tests__/auth.test.ts new file mode 100644 index 0000000..a74d44d --- /dev/null +++ b/src/utils/__tests__/auth.test.ts @@ -0,0 +1,54 @@ +import { describe, test, expect, vi, afterAll } from "vitest" +import { getPKCEStatus, redirectToLogin, createPKCECodes } from "../auth" +import config from "@/config" + +describe("getPKCEStatus", () => { + test("returns false on empty state", () => { + expect(getPKCEStatus()).toEqual({ isDone: false }) + expect(getPKCEStatus('state')).toEqual({ isDone: false }) + }) + + test("returns true on state and code", () => { + document.cookie = 'app.txs.state-uuid=code_verifier' + expect(getPKCEStatus('state-uuid')).toEqual({ + isDone: true, + codeVerifier: 'code_verifier' + }) + }) +}) + +describe("createPKCECodes", () => { + test("returns codeVerifier, codeChallenge, state", async () => { + const codes = await createPKCECodes() + expect(codes).toMatchObject({ + codeVerifier: expect.any(String), + codeChallenge: expect.any(String), + state: expect.any(String) + }) + }) +}) + +describe("redirectToLogin", () => { + afterAll(() => { + vi.unstubAllGlobals() + }) + + test("redirects to login url", async () => { + const state = 'state' + const codeChallenge = 'code_challenge' + const redirect = vi.fn() + + const query = new URLSearchParams() + query.append('response_type', 'code,id_token') + query.append('redirect_uri', config.BASE_URL) + query.append('state', state) + query.append('code_challenge', codeChallenge) + + const url = `${config.LOGIN_URL}?${query.toString()}` + vi.stubGlobal('location', { replace: redirect }) + + redirectToLogin(state, codeChallenge) + + expect(redirect).toHaveBeenCalledWith(url) + }) +}) diff --git a/src/lib/__tests__/stateCookie.test.ts b/src/utils/__tests__/stateCookie.test.ts similarity index 70% rename from src/lib/__tests__/stateCookie.test.ts rename to src/utils/__tests__/stateCookie.test.ts index a15bc6d..cf009da 100644 --- a/src/lib/__tests__/stateCookie.test.ts +++ b/src/utils/__tests__/stateCookie.test.ts @@ -1,7 +1,28 @@ -import { describe, test, expect } from "vitest" +import { describe, test, expect, beforeAll, afterAll, vi } from "vitest" import { getStateCookie, createStateCookie } from "../stateCookie" describe("createStateCookie", () => { + let mockCookie = [] + beforeAll(() => { + vi.stubGlobal('document', { + get cookie() { + return mockCookie.join(';') + }, + set cookie(value) { + mockCookie = mockCookie.filter(c => !c.startsWith(value.split('=')[0])) + mockCookie.push(value) + }, + }) + }) + + afterEach(() => { + mockCookie = [] + }) + + afterAll(() => { + vi.unstubAllGlobals() + }) + test("throws errors on empty string", () => { expect(() => createStateCookie("")) .toThrow("state value is required") diff --git a/src/utils/auth.ts b/src/utils/auth.ts new file mode 100644 index 0000000..5a71219 --- /dev/null +++ b/src/utils/auth.ts @@ -0,0 +1,45 @@ +import { + getStateCookie, + createStateCookie +} from "@/utils/stateCookie" +import { + createPKCECodeChallenge, + createRandomString +} from "@lib/pkce" +import { PKCEState } from "@/types" +import config from "@/config" + +export const createPKCECodes = async () => { + const codeVerifier = createRandomString() + const codeChallenge = await createPKCECodeChallenge(codeVerifier) + const state = createStateCookie(codeVerifier) + + return { codeVerifier, codeChallenge, state } +} + +export const redirectToLogin = async ( + state: string, codeChallenge: string +) => { + const query = new URLSearchParams() + query.append('response_type', 'code,id_token') + query.append('redirect_uri', config.BASE_URL) + query.append('state', state) + query.append('code_challenge', codeChallenge) + + window.location.replace(`${config.LOGIN_URL}?${query.toString()}`) +} + +export const getPKCEStatus = (state?: string): PKCEState => { + // guard: pkce is not initialized yet + if (!state) { + return { isDone: false } + } + + const codeVerifier = getStateCookie(state) + if (!codeVerifier) return { isDone: false } + + return { + isDone: true, + codeVerifier, + } +} diff --git a/src/utils/stateCookie.ts b/src/utils/stateCookie.ts new file mode 100644 index 0000000..844b8ed --- /dev/null +++ b/src/utils/stateCookie.ts @@ -0,0 +1,20 @@ + +import { saveCookie, getCookie, deleteCookie } from '@lib/cookie' +import config from '@/config' + +export const createStateCookie = (value: string) => { + if (!value) throw new Error('state value is required') + + const state = crypto.randomUUID() + saveCookie(`${config.STATE_COOKIE_PREFIX}${state}`, value, 30) + + return state +} + +export const getStateCookie = (state: string) => { + return getCookie(`${config.STATE_COOKIE_PREFIX}${state}`) +} + +export const deleteStateCookie = (state: string) => { + deleteCookie(`${config.STATE_COOKIE_PREFIX}${state}`) +} diff --git a/tsconfig.json b/tsconfig.json index a7fc6fb..87e99c7 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,5 +1,10 @@ { "compilerOptions": { + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"], + "@lib/*": ["./lib/*"] + }, "target": "ES2020", "useDefineForClassFields": true, "lib": ["ES2020", "DOM", "DOM.Iterable"], @@ -20,6 +25,6 @@ "noUnusedParameters": true, "noFallthroughCasesInSwitch": true }, - "include": ["src"], + "include": ["src", "lib"], "references": [{ "path": "./tsconfig.node.json" }] } diff --git a/vite.config.ts b/vite.config.ts index 2b37192..ba76fb0 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,16 +1,22 @@ import { defineConfig } from 'vite' import react from '@vitejs/plugin-react-swc' - +import path from 'path' // https://vitejs.dev/config/ export default defineConfig({ plugins: [react()], + resolve: { + alias: [ + { find: '@', replacement: path.resolve(__dirname, 'src') }, + { find: '@lib', replacement: path.resolve(__dirname, 'lib') } + ] + }, test: { globals: true, environment: 'happy-dom', - browser: { - enabled: true, - name: 'chrome', - headless: true - } + // browser: { + // enabled: true, + // name: 'chrome', + // headless: true + // } } })