From 55adf5ec2f34a83178c0452884fd5a977a9c9c18 Mon Sep 17 00:00:00 2001 From: Matthew L McPeak Date: Wed, 17 Jun 2026 16:21:54 -0400 Subject: [PATCH] fix: portfolio --- .env | 1 + .forgejo/workflows/ci.yml | 19 +-- bun.lock | 25 +++- bunfig.toml | 1 + ...14-nychthemeron-library-refactor-design.md | 112 ++++++++++++++++++ package.json | 3 +- sample.env | 1 + src/__tests__/composables/useAPI.spec.ts | 52 ++++++++ src/composables/useAPI.ts | 20 ++++ src/composables/useUtils.ts | 11 ++ tsconfig.app.json | 28 ++--- tsconfig.json | 24 ++-- tsconfig.vitest.json | 32 ++--- vite-env.d.ts | 13 ++ vite.config.ts | 34 +++--- vitest.config.ts | 28 +++-- 16 files changed, 323 insertions(+), 81 deletions(-) create mode 100644 .env create mode 100644 bunfig.toml create mode 100644 docs/superpowers/specs/2026-06-14-nychthemeron-library-refactor-design.md create mode 100644 sample.env create mode 100644 src/__tests__/composables/useAPI.spec.ts create mode 100644 src/composables/useAPI.ts create mode 100644 src/composables/useUtils.ts create mode 100644 vite-env.d.ts diff --git a/.env b/.env new file mode 100644 index 0000000..11c9252 --- /dev/null +++ b/.env @@ -0,0 +1 @@ +VITE_API_URL=http://localhost:5173/ diff --git a/.forgejo/workflows/ci.yml b/.forgejo/workflows/ci.yml index f04801d..2d9dd41 100644 --- a/.forgejo/workflows/ci.yml +++ b/.forgejo/workflows/ci.yml @@ -8,14 +8,11 @@ on: jobs: build: - runs-on: ubuntu-latest - container: oven/bun:1 + runs-on: self-hosted + container: git.mcpeakdev.com/mcpeakdev/bun-ci:latest outputs: artifact_name: ${{ steps.meta.outputs.name }} steps: - - name: Install git and node - run: apt-get update && apt-get install -y git nodejs - - name: Checkout uses: actions/checkout@v4 @@ -30,7 +27,13 @@ jobs: echo "name=portfolio_${tag}" >> "$GITHUB_OUTPUT" - name: Install dependencies - run: bun install --frozen-lockfile + run: | + printf '@nychthemeron:registry=https://git.mcpeakdev.com/api/packages/McPeakDev/npm/\n//git.mcpeakdev.com/api/packages/McPeakDev/npm/:_authToken=%s\n' "$BUN_AUTH_TOKEN" > .npmrc + bun install --frozen-lockfile + rm .npmrc + working-directory: ui + env: + BUN_AUTH_TOKEN: ${{ secrets.BUN_AUTH_TOKEN }} - name: Audit dependencies run: bun audit || true @@ -53,8 +56,8 @@ jobs: publish: needs: build if: startsWith(github.ref, 'refs/tags/v') - runs-on: ubuntu-latest - container: catthehacker/ubuntu:act-latest + runs-on: self-hosted + container: git.mcpeakdev.com/mcpeakdev/docker-pub:latest steps: - name: Checkout uses: actions/checkout@v4 diff --git a/bun.lock b/bun.lock index 4dc03e0..5b42324 100644 --- a/bun.lock +++ b/bun.lock @@ -5,13 +5,14 @@ "": { "name": "portfolio", "dependencies": { + "@nychthemeron/library": "^0.0.1", "vue": "^3.5.32", }, "devDependencies": { "@playwright/test": "^1.59.1", "@tsconfig/node24": "^24.0.4", "@types/jsdom": "^28.0.1", - "@types/node": "^24.12.2", + "@types/node": "^25.9.3", "@vitejs/plugin-vue": "^6.0.6", "@vitest/eslint-plugin": "^1.6.16", "@vue/eslint-config-typescript": "^14.7.0", @@ -170,6 +171,8 @@ "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], + "@nychthemeron/library": ["@nychthemeron/library@0.0.1", "https://git.mcpeakdev.com/api/packages/McPeakDev/npm/%40nychthemeron%2Flibrary/-/0.0.1/library-0.0.1.tgz", { "peerDependencies": { "primevue": "^4.5.0", "vue": "^3.5.0" } }, "sha512-84pcTnF8Ead8D7TSLhKTS8ACpaOCTS0mylzkWqgGrNs4fnrH9tEJg874nFHegiFy5P1fuKCk4B7Yt/Pjc4NK/Q=="], + "@one-ini/wasm": ["@one-ini/wasm@0.1.1", "", {}, "sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw=="], "@oxc-project/types": ["@oxc-project/types@0.133.0", "", {}, "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA=="], @@ -256,6 +259,16 @@ "@polka/url": ["@polka/url@1.0.0-next.29", "", {}, "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww=="], + "@primeuix/styled": ["@primeuix/styled@0.7.4", "", { "dependencies": { "@primeuix/utils": "^0.6.1" } }, "sha512-QSO/NpOQg8e9BONWRBx9y8VGMCMYz0J/uKfNJEya/RGEu7ARx0oYW0ugI1N3/KB1AAvyGxzKBzGImbwg0KUiOQ=="], + + "@primeuix/styles": ["@primeuix/styles@2.0.3", "", { "dependencies": { "@primeuix/styled": "^0.7.4" } }, "sha512-2ykAB6BaHzR/6TwF8ShpJTsZrid6cVIEBVlookSdvOdmlWuevGu5vWOScgIwqWwlZcvkFYAGR/SUV3OHCTBMdw=="], + + "@primeuix/utils": ["@primeuix/utils@0.6.4", "", {}, "sha512-pZ5f+vj7wSzRhC7KoEQRU5fvYAe+RP9+m39CTscZ3UywCD1Y2o6Fe1rRgklMPSkzUcty2jzkA0zMYkiJBD1hgg=="], + + "@primevue/core": ["@primevue/core@4.5.5", "", { "dependencies": { "@primeuix/styled": "^0.7.4", "@primeuix/utils": "^0.6.2" }, "peerDependencies": { "vue": "^3.5.0" } }, "sha512-JpkXhq1ddc70JdsC3CC4dM+UbeeWuCW/8DpS9dNBfrOk824TLSlRlMEGFyVKqRMn5WPQvYLiy3xXfLQeNdSqhQ=="], + + "@primevue/icons": ["@primevue/icons@4.5.5", "", { "dependencies": { "@primeuix/utils": "^0.6.2", "@primevue/core": "4.5.5" } }, "sha512-eteOhTdAOXEYE9qW1AOrBBgDxQ2szHJxSkEK1XVdV2TKxGM5FQf03Ovms0VDyZTc16XBIgvwYjXJQS0BPbhPaA=="], + "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.3", "", { "os": "android", "cpu": "arm64" }, "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw=="], "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA=="], @@ -306,7 +319,7 @@ "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], - "@types/node": ["@types/node@24.13.1", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-RSpUJGmvsJ1ZeBehQZFhIdpsz+bIpES0nIQXko4Ybq+N+kX6XvOq3Jo+iJ82FWLdblFq85AsMikd3m35jgezYg=="], + "@types/node": ["@types/node@25.9.3", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-603BddQMv3pUcr4U2dhujk83N2tTDVr/34wII2B6bJy6g+8WD6yUb11jszNs0gdi4PesVWl7ABt8nYMVpnLUcg=="], "@types/tough-cookie": ["@types/tough-cookie@4.0.5", "", {}, "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA=="], @@ -720,6 +733,8 @@ "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], + "primevue": ["primevue@4.5.5", "", { "dependencies": { "@primeuix/styled": "^0.7.4", "@primeuix/styles": "^2.0.3", "@primeuix/utils": "^0.6.2", "@primevue/core": "4.5.5", "@primevue/icons": "4.5.5" } }, "sha512-Kv5REIewCdP806QaoU+4nBXfmpzOGFKkZ9qH4KsL6MjiAQVc4PUzypt8erl4r3Vzh3nr3aWZIxkxYRRsLGiX2A=="], + "proto-list": ["proto-list@1.2.4", "", {}, "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA=="], "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], @@ -876,7 +891,9 @@ "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], - "@types/node/undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], + "@types/jsdom/@types/node": ["@types/node@24.13.1", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-RSpUJGmvsJ1ZeBehQZFhIdpsz+bIpES0nIQXko4Ybq+N+kX6XvOq3Jo+iJ82FWLdblFq85AsMikd3m35jgezYg=="], + + "@types/node/undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="], "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], @@ -918,6 +935,8 @@ "wrap-ansi-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "@types/jsdom/@types/node/undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], + "cross-spawn/which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], "editorconfig/minimatch/brace-expansion": ["brace-expansion@2.1.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA=="], diff --git a/bunfig.toml b/bunfig.toml new file mode 100644 index 0000000..dc6756b --- /dev/null +++ b/bunfig.toml @@ -0,0 +1 @@ +env = false diff --git a/docs/superpowers/specs/2026-06-14-nychthemeron-library-refactor-design.md b/docs/superpowers/specs/2026-06-14-nychthemeron-library-refactor-design.md new file mode 100644 index 0000000..17442ac --- /dev/null +++ b/docs/superpowers/specs/2026-06-14-nychthemeron-library-refactor-design.md @@ -0,0 +1,112 @@ +# Refactor portfolio to use `@nychthemeron/library` + +## Context + +The portfolio currently ships a hand-rolled "Nychthemeron" design system: +local components in `src/components/ds/` (`NychButton`, `NychCard`, +`NychInputText`, `NychTextarea`, `NychMessage`, `NychTag`, `LaurelSpinner`, +`LaurelDivider`, `ImageSlot`) backed by ~1200 lines of token CSS in +`src/assets/styles/`. + +`@nychthemeron/library` (now installed from the private registry at +`git.mcpeakdev.com`) is the extracted, PrimeVue-backed version of this same +design system: unstyled PrimeVue components (`Button`, `Card`, `InputText`, +`Textarea`, `Message`, `Tag`, `LoadingIcon`, plus others not yet used here) +wired up with the same `nych-*` passthrough classes, and ships the same +theme as CSS via `@nychthemeron/library/style` and `@nychthemeron/library/theme`. + +Goal: replace the local duplicated implementation with the library, keeping +only what the library doesn't provide (`LaurelDivider`, `ImageSlot`, and the +580px static hero spinner). + +## Styling / setup + +- `src/main.ts`: + - Add `import '@nychthemeron/library/style'` and + `import '@nychthemeron/library/theme'`. + - Register PrimeVue: `app.use(PrimeVue)` (the library's components are + PrimeVue components under the hood). + - Keep the existing `import './assets/styles/styles.css'`, but trim its + `@import` manifest (see below) so it only covers portfolio-specific + CSS; the library CSS imports are added separately in `main.ts`. +- Delete `src/assets/styles/tokens/fonts.css`, `tokens/colors.css`, + `tokens/base.css` — fully superseded by `@nychthemeron/library/style` + (same theme CSS variables for both `hades`/`apollo` themes, font imports, + base reset, focus-ring, and the `nych-spin` keyframes). +- `src/assets/styles/tokens/components.css`: delete every section except + **LaurelDivider** (currently the last section, ~line 738 onward). Rename + the file to `tokens/laurel-divider.css` containing just that section. + Everything else (Button, Input/Textarea, Checkbox/Radio, ToggleSwitch, + Select, Card, Message, Tag, Dialog) is covered by + `@nychthemeron/library/theme`. +- Keep as-is: `tokens/spacing.css`, `tokens/typography.css`, + `portfolio.css`, brand SVGs in `src/assets/brand/`. +- Update `src/assets/styles/styles.css`'s `@import` manifest to drop the + removed files and add `tokens/laurel-divider.css` in place of + `tokens/components.css`. + +## Component mapping + +| Local component | Replacement | Notes | +|---|---|---| +| `NychTag` | `Tag` from `@nychthemeron/library` | `severity` / `rounded` props pass straight through | +| `NychMessage` | `Message` | `severity` / `closable` pass straight through | +| `NychInputText` | `InputText` | `v-model`, `placeholder`, `type`, `invalid` pass through; drop the local `size="normal"` prop (PrimeVue `size` only accepts `small`/`large`/default — `normal` maps to "don't pass `size`") | +| `NychTextarea` | `Textarea` | `v-model`, `rows`, `placeholder`, `invalid` — direct match | +| `NychButton` | `Button` | `severity`, `disabled`, `loading`, default + `icon` slots — direct match. The library's `Button` has a built-in loading spinner (laurel wreath), replacing the local `LaurelSpinner` usage inside `NychButton` | +| `NychCard` | `Card` | slot rename: local `image` slot → PrimeVue `header` slot; local default slot → PrimeVue `content` slot; `title`/`subtitle` props match | +| `LaurelSpinner` (small, used inside `NychButton`) | dropped — covered by `Button`'s built-in loader | | +| `LaurelSpinner` (Hero, 580px static, `:spinning="false"`) | kept as a local component, rewritten as a thin wrapper around `NychLoadingIcon` imported from `@nychthemeron/library/components`, with scoped CSS to set `width`/`height` and override/disable the `nych-spin` animation when `spinning` is false | per approved "wrap with CSS overrides" approach | +| `LaurelDivider`, `ImageSlot` | unchanged, no library equivalent | | + +## Files affected + +**Deleted:** +- `src/components/ds/NychTag.vue` +- `src/components/ds/NychMessage.vue` +- `src/components/ds/NychInputText.vue` +- `src/components/ds/NychTextarea.vue` +- `src/components/ds/NychButton.vue` +- `src/components/ds/NychCard.vue` +- `src/assets/styles/tokens/fonts.css` +- `src/assets/styles/tokens/colors.css` +- `src/assets/styles/tokens/base.css` + +**Rewritten:** +- `src/components/ds/LaurelSpinner.vue` — becomes a thin wrapper around the + library's `NychLoadingIcon`, preserving its existing `size` / `color` / + `spinning` prop API so `HeroSection.vue` doesn't need to change its usage. +- `src/assets/styles/tokens/components.css` → `tokens/laurel-divider.css` + (LaurelDivider section only). +- `src/assets/styles/styles.css` (updated `@import` manifest). +- `src/main.ts` (add PrimeVue + library CSS imports, `app.use(PrimeVue)`). + +**Updated imports/usages:** +- `src/components/portfolio/SkillsSection.vue` — `NychTag` from library. +- `src/components/portfolio/WorkSection.vue` — `NychCard`, `NychTag` from + library; update `NychCard` slot usage (`image` → `header`, default → + `content`). +- `src/components/portfolio/ContactSection.vue` — `NychInputText`, + `NychTextarea`, `NychButton`, `NychMessage` from library. +- `src/components/portfolio/HeroSection.vue` — `NychButton` from library; + `LaurelSpinner` continues to be imported from `./ds/LaurelSpinner.vue` + (rewritten). + +## Verification + +- `bun run build` and `bun run type-check` must pass. +- Visually verify via dev server (`bun run dev`): + - Hero section renders the large static laurel wreath at the same size. + - Skills/Work section tags render with correct severity colors. + - Work section cards render header image, title, content, footer tags. + - Contact form inputs/textarea/button/message render and behave + (loading spinner shows in button while "sending"). + - Both `hades` and `apollo` themes (toggle `data-theme` on ``) still + render correctly. + +## Out of scope + +- No changes to `LaurelDivider.vue` or `ImageSlot.vue` internals. +- No changes to other library components not currently used + (`Checkbox`, `Select`, `Dialog`, `ToggleSwitch`, `RadioButton`, etc.). +- No changes to the CI workflow or registry configuration. diff --git a/package.json b/package.json index e2a6397..ced0fb2 100644 --- a/package.json +++ b/package.json @@ -17,13 +17,14 @@ "format": "oxfmt src/" }, "dependencies": { + "@nychthemeron/library": "^0.0.1", "vue": "^3.5.32" }, "devDependencies": { "@playwright/test": "^1.59.1", "@tsconfig/node24": "^24.0.4", "@types/jsdom": "^28.0.1", - "@types/node": "^24.12.2", + "@types/node": "^25.9.3", "@vitejs/plugin-vue": "^6.0.6", "@vitest/eslint-plugin": "^1.6.16", "@vue/eslint-config-typescript": "^14.7.0", diff --git a/sample.env b/sample.env new file mode 100644 index 0000000..11c9252 --- /dev/null +++ b/sample.env @@ -0,0 +1 @@ +VITE_API_URL=http://localhost:5173/ diff --git a/src/__tests__/composables/useAPI.spec.ts b/src/__tests__/composables/useAPI.spec.ts new file mode 100644 index 0000000..69aaed4 --- /dev/null +++ b/src/__tests__/composables/useAPI.spec.ts @@ -0,0 +1,52 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; + +import { useAPI } from "../../composables/useAPI"; + +describe("useAPI", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("creates properly", () => { + const api = useAPI(); + expect(api).not.toBeUndefined(); + }); + + it("returns data", async () => { + const mockData = { + something: "test", + }; + + globalThis.fetch = vi.fn(() => + Promise.resolve({ + ok: true, + json: () => Promise.resolve(mockData), + } as Response), + ); + + const api = useAPI(); + + const data = await api.getPortfolioData(); + + expect(data).toStrictEqual(mockData); + }); + + it("returns undefined on failure", async () => { + const mockData = { + something: "test", + }; + + globalThis.fetch = vi.fn(() => + Promise.reject({ + ok: true, + json: () => Promise.resolve(mockData), + } as Response), + ); + + const api = useAPI(); + + const data = await api.getPortfolioData(); + + expect(data).toStrictEqual(undefined); + }); +}); diff --git a/src/composables/useAPI.ts b/src/composables/useAPI.ts new file mode 100644 index 0000000..4844636 --- /dev/null +++ b/src/composables/useAPI.ts @@ -0,0 +1,20 @@ +import { useUtils } from "./useUtils"; + +export const useAPI = (baseURL = API_URL) => { + const { url } = useUtils(); + + const normalizedURL = url.normalizePath(baseURL.trim()); + + const generateURL = (path: string) => { + return normalizedURL + path; + }; + + const getPortfolioData = async () => + fetch(generateURL("/portfolio/")) + .then((resp) => resp.json()) + .catch(() => undefined); + + return { + getPortfolioData, + }; +}; diff --git a/src/composables/useUtils.ts b/src/composables/useUtils.ts new file mode 100644 index 0000000..52be440 --- /dev/null +++ b/src/composables/useUtils.ts @@ -0,0 +1,11 @@ +export const useUtils = () => { + const normalizePath = (path: string) => { + path.endsWith("/") ? path.slice(0, -1) : path; + }; + + return { + url: { + normalizePath, + }, + }; +}; diff --git a/tsconfig.app.json b/tsconfig.app.json index c0f2d86..bb89c10 100644 --- a/tsconfig.app.json +++ b/tsconfig.app.json @@ -1,18 +1,18 @@ { - "extends": "@vue/tsconfig/tsconfig.dom.json", - "include": ["env.d.ts", "src/**/*", "src/**/*.vue"], - "exclude": ["src/**/__tests__/*"], - "compilerOptions": { - // Extra safety for array and object lookups, but may have false positives. - "noUncheckedIndexedAccess": true, + "extends": "@vue/tsconfig/tsconfig.dom.json", + "include": ["vite-env.d.ts", "env.d.ts", "src/**/*", "src/**/*.vue"], + "exclude": ["src/**/__tests__/*"], + "compilerOptions": { + // Extra safety for array and object lookups, but may have false positives. + "noUncheckedIndexedAccess": true, - // Path mapping for cleaner imports. - "paths": { - "@/*": ["./src/*"] - }, + // Path mapping for cleaner imports. + "paths": { + "@/*": ["./src/*"] + }, - // `vue-tsc --build` produces a .tsbuildinfo file for incremental type-checking. - // Specified here to keep it out of the root directory. - "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo" - } + // `vue-tsc --build` produces a .tsbuildinfo file for incremental type-checking. + // Specified here to keep it out of the root directory. + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo" + } } diff --git a/tsconfig.json b/tsconfig.json index 100cf6a..fb7fbd2 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,14 +1,14 @@ { - "files": [], - "references": [ - { - "path": "./tsconfig.node.json" - }, - { - "path": "./tsconfig.app.json" - }, - { - "path": "./tsconfig.vitest.json" - } - ] + "files": [], + "references": [ + { + "path": "./tsconfig.node.json" + }, + { + "path": "./tsconfig.app.json" + }, + { + "path": "./tsconfig.vitest.json" + } + ] } diff --git a/tsconfig.vitest.json b/tsconfig.vitest.json index 06dd490..4640333 100644 --- a/tsconfig.vitest.json +++ b/tsconfig.vitest.json @@ -1,21 +1,21 @@ { - "extends": "./tsconfig.app.json", + "extends": "./tsconfig.app.json", - // Override to include only test files and clear exclusions. - // .vue files must be explicitly included so vue-tsc enables .vue module - // resolution for this project (relying on transitive module resolution - // from test imports is unreliable in `vue-tsc --build`). - "include": ["src/**/__tests__/*", "env.d.ts", "src/**/*.vue"], - "exclude": [], + // Override to include only test files and clear exclusions. + // .vue files must be explicitly included so vue-tsc enables .vue module + // resolution for this project (relying on transitive module resolution + // from test imports is unreliable in `vue-tsc --build`). + "include": ["src/**/__tests__/*", "env.d.ts", "src/**/*.vue"], + "exclude": [], - "compilerOptions": { - // Vitest runs in a different environment than the application code. - // Adjust lib and types accordingly. - "lib": [], - "types": ["node", "jsdom"], + "compilerOptions": { + // Vitest runs in a different environment than the application code. + // Adjust lib and types accordingly. + "lib": ["dom", "es2015", "esnext"], + "types": ["node", "jsdom"], - // `vue-tsc --build` produces a .tsbuildinfo file for incremental type-checking. - // Specified here to keep it out of the root directory. - "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.vitest.tsbuildinfo" - } + // `vue-tsc --build` produces a .tsbuildinfo file for incremental type-checking. + // Specified here to keep it out of the root directory. + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.vitest.tsbuildinfo" + } } diff --git a/vite-env.d.ts b/vite-env.d.ts new file mode 100644 index 0000000..8bff7d7 --- /dev/null +++ b/vite-env.d.ts @@ -0,0 +1,13 @@ +interface ViteTypeOptions { + strictImportMetaEnv: unknown; +} + +interface ImportMetaEnv { + readonly VITE_API_URL: string; +} + +interface ImportMeta { + readonly env: ImportMetaEnv; +} + +declare const API_URL: string; diff --git a/vite.config.ts b/vite.config.ts index 4217010..c1eeead 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,18 +1,24 @@ -import { fileURLToPath, URL } from 'node:url' +import { fileURLToPath, URL } from "node:url"; +import { defineConfig, loadEnv } from "vite"; -import { defineConfig } from 'vite' -import vue from '@vitejs/plugin-vue' -import vueDevTools from 'vite-plugin-vue-devtools' +import vue from "@vitejs/plugin-vue"; +import vueDevTools from "vite-plugin-vue-devtools"; // https://vite.dev/config/ -export default defineConfig({ - plugins: [ - vue(), - vueDevTools(), - ], - resolve: { - alias: { - '@': fileURLToPath(new URL('./src', import.meta.url)) +export default defineConfig(({ mode }) => { + const env = loadEnv(mode, process.cwd(), "VITE"); + const baseURL = fileURLToPath(new URL("./src", import.meta.url)); + + return { + define: { + API_URL: JSON.stringify(env.VITE_API_URL), }, - }, -}) + plugins: [vue(), vueDevTools()], + resolve: { + alias: { + "@": baseURL, + "~": baseURL, + }, + }, + }; +}); diff --git a/vitest.config.ts b/vitest.config.ts index c328717..7d1de9f 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -1,14 +1,16 @@ -import { fileURLToPath } from 'node:url' -import { mergeConfig, defineConfig, configDefaults } from 'vitest/config' -import viteConfig from './vite.config' +import { fileURLToPath } from "node:url"; +import { mergeConfig, defineConfig, configDefaults } from "vitest/config"; +import viteConfig from "./vite.config"; -export default mergeConfig( - viteConfig, - defineConfig({ - test: { - environment: 'jsdom', - exclude: [...configDefaults.exclude, 'e2e/**'], - root: fileURLToPath(new URL('./', import.meta.url)), - }, - }), -) +export default defineConfig((configEnv) => + mergeConfig( + viteConfig(configEnv), + defineConfig({ + test: { + environment: "jsdom", + exclude: [...configDefaults.exclude, "e2e/**"], + root: fileURLToPath(new URL("./", import.meta.url)), + }, + }), + ), +);