chore: migrate linting and formatting to Oxc
This commit is contained in:
@@ -4,6 +4,7 @@
|
||||
.env*
|
||||
!.env.example
|
||||
.output/
|
||||
.tanstack/
|
||||
dist/
|
||||
node_modules/
|
||||
playwright-report/
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"$schema": "./node_modules/oxfmt/configuration_schema.json",
|
||||
"ignorePatterns": [".output/**", ".tanstack/**", "src/routeTree.gen.ts"]
|
||||
}
|
||||
+200
-3
@@ -1,9 +1,206 @@
|
||||
{
|
||||
"$schema": "./node_modules/oxlint/configuration_schema.json",
|
||||
"categories": { "correctness": "off" },
|
||||
"categories": {
|
||||
"correctness": "error"
|
||||
},
|
||||
"options": {
|
||||
"typeAware": true,
|
||||
"typeCheck": true
|
||||
},
|
||||
"plugins": [
|
||||
"eslint",
|
||||
"typescript",
|
||||
"unicorn",
|
||||
"oxc",
|
||||
"import",
|
||||
"node",
|
||||
"promise",
|
||||
"react",
|
||||
"react-perf",
|
||||
"jsx-a11y"
|
||||
],
|
||||
"jsPlugins": ["@shadcn/lint"],
|
||||
"rules": {
|
||||
"shadcn/no-raw-colors": "error"
|
||||
"no-console": "error",
|
||||
"no-unused-vars": [
|
||||
"error",
|
||||
{
|
||||
"argsIgnorePattern": "^_"
|
||||
}
|
||||
],
|
||||
"typescript/consistent-type-exports": "error",
|
||||
"typescript/consistent-type-imports": [
|
||||
"error",
|
||||
{
|
||||
"disallowTypeAnnotations": false
|
||||
}
|
||||
],
|
||||
"typescript/no-non-null-assertion": "error",
|
||||
"typescript/no-explicit-any": "error",
|
||||
"unicorn/no-useless-spread": "error",
|
||||
"jsx-a11y/no-autofocus": "error",
|
||||
"jsx-a11y/no-static-element-interactions": "error",
|
||||
"jsx-a11y/prefer-tag-over-role": "off",
|
||||
"react/exhaustive-deps": "error",
|
||||
"react/purity": "error",
|
||||
"react/rules-of-hooks": "error",
|
||||
"react/set-state-in-effect": "error",
|
||||
"shadcn/no-arbitrary-values": [
|
||||
"error",
|
||||
{
|
||||
"allow": ["layout"]
|
||||
}
|
||||
],
|
||||
"shadcn/no-inline-styles": "error",
|
||||
"shadcn/no-raw-colors": "error",
|
||||
"shadcn/no-restyle": [
|
||||
"error",
|
||||
{
|
||||
"allow": ["layout"],
|
||||
"contracts": [
|
||||
{
|
||||
"pattern": "^Sidebar$",
|
||||
"allow": [
|
||||
"layout",
|
||||
"border-r",
|
||||
"transition-none",
|
||||
"inbox-sidebar",
|
||||
"inbox-list-panel",
|
||||
"support-sidebar",
|
||||
"support-list"
|
||||
]
|
||||
},
|
||||
{
|
||||
"pattern": "^SidebarHeader$",
|
||||
"allow": ["layout", "py-4", "gap-4", "border-b", "p-5"]
|
||||
},
|
||||
{
|
||||
"pattern": "^SidebarGroup$",
|
||||
"allow": ["layout", "px-3"]
|
||||
},
|
||||
{
|
||||
"pattern": "^SidebarFooter$",
|
||||
"allow": ["layout", "px-3", "pb-4"]
|
||||
},
|
||||
{
|
||||
"pattern": "^SidebarMenuButton$",
|
||||
"allow": ["layout", "p-0"]
|
||||
},
|
||||
{
|
||||
"pattern": "^Label$",
|
||||
"allow": ["layout", "deck-checkbox"]
|
||||
},
|
||||
{
|
||||
"pattern": "^Textarea$",
|
||||
"allow": ["layout", "text-sm", "leading-7", "journal-summary"]
|
||||
},
|
||||
{
|
||||
"pattern": "^MessageScroller$",
|
||||
"allow": ["layout", "research-conversation"]
|
||||
},
|
||||
{
|
||||
"pattern": "^MessageScrollerContent$",
|
||||
"allow": ["layout", "gap-5", "p-4"]
|
||||
},
|
||||
{
|
||||
"pattern": "^Marker$",
|
||||
"allow": ["layout", "research-tool-message"]
|
||||
},
|
||||
{
|
||||
"pattern": "^Message$",
|
||||
"allow": ["layout", "research-message"]
|
||||
},
|
||||
{
|
||||
"pattern": "^Collapsible$",
|
||||
"allow": ["layout", "research-account-picker"]
|
||||
},
|
||||
{
|
||||
"pattern": "^SidebarProvider$",
|
||||
"allow": [
|
||||
"layout",
|
||||
"workspace",
|
||||
"home-workspace",
|
||||
"inbox-workspace",
|
||||
"vital-workspace",
|
||||
"support-workspace",
|
||||
"journal-workspace"
|
||||
]
|
||||
},
|
||||
{
|
||||
"pattern": "^Button$",
|
||||
"allow": ["layout", "auth-submit", "inbox-back", "support-back"]
|
||||
},
|
||||
{
|
||||
"pattern": "^SidebarInset$",
|
||||
"allow": ["layout", "home-main", "inbox-reader", "support-main", "journal-main"]
|
||||
},
|
||||
{
|
||||
"pattern": "^Input$",
|
||||
"allow": ["layout", "pl-9"]
|
||||
},
|
||||
{
|
||||
"pattern": "^SidebarContent$",
|
||||
"allow": ["layout", "gap-0"]
|
||||
},
|
||||
{
|
||||
"pattern": "^Badge$",
|
||||
"allow": ["layout", "deck-source-label"]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"shadcn/no-unknown-classes": "error",
|
||||
"shadcn/require-static-classes": "error"
|
||||
},
|
||||
"ignorePatterns": ["src/routeTree.gen.ts"]
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["scripts/**"],
|
||||
"rules": {
|
||||
"no-console": "off"
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["src/components/ui/**"],
|
||||
"rules": {
|
||||
"shadcn/no-arbitrary-values": "off",
|
||||
"shadcn/no-restyle": "off",
|
||||
"shadcn/require-static-classes": "off"
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["src/**/*.{test,spec}.{ts,tsx}", "tests/**/*.{ts,tsx}"],
|
||||
"plugins": ["vitest"],
|
||||
"rules": {
|
||||
"typescript/unbound-method": "error",
|
||||
"vitest/expect-expect": "error",
|
||||
"vitest/no-conditional-expect": "error",
|
||||
"vitest/no-standalone-expect": "error",
|
||||
"vitest/require-mock-type-parameters": "error"
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["e2e/**/*.{ts,tsx}"],
|
||||
"rules": {
|
||||
"react/rules-of-hooks": "off"
|
||||
}
|
||||
}
|
||||
],
|
||||
"ignorePatterns": [
|
||||
"**/.pnpm-store/**",
|
||||
"**/.direnv/**",
|
||||
"**/node_modules/**",
|
||||
"**/dist/**",
|
||||
"**/build/**",
|
||||
"**/coverage/**",
|
||||
"**/playwright-report/**",
|
||||
"**/test-results/**",
|
||||
"**/.vercel/**",
|
||||
"**/drizzle/**",
|
||||
"**/*.tsbuildinfo",
|
||||
".output/**",
|
||||
".tanstack/**",
|
||||
"src/routeTree.gen.ts",
|
||||
"e2e/generated/**",
|
||||
"src/features/storage/migrations.generated.ts"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -10,14 +10,14 @@ original language.
|
||||
|
||||
## Workspaces and current status
|
||||
|
||||
| Workspace | Route | Current implementation |
|
||||
| --- | --- | --- |
|
||||
| Home | `/` | State-derived daily brief, flexible tasks and routines, and natural-language action proposals. Local prototype. |
|
||||
| Messages | `/support` | Sample conversations, editable replies, scheduling previews, and deferral. Sending is simulated. |
|
||||
| Journal | `/journal` | Notes, resume points, medication and weight entries, append-only corrections, and summary previews. Local prototype. |
|
||||
| Reader | `/inbox` | Searchable sample articles and a responsive article reader. Recommendation from activity history is not connected yet. |
|
||||
| Vitals | `/vitals` | Body and Environment dashboards with sample readings, charts, and simulated controls. No health or device connection. |
|
||||
| Research | `/deck` | Twitter and Mastodon columns, connected accounts, persistent decks, and optional Codex research chat. |
|
||||
| Workspace | Route | Current implementation |
|
||||
| --------- | ---------- | ---------------------------------------------------------------------------------------------------------------------- |
|
||||
| Home | `/` | State-derived daily brief, flexible tasks and routines, and natural-language action proposals. Local prototype. |
|
||||
| Messages | `/support` | Sample conversations, editable replies, scheduling previews, and deferral. Sending is simulated. |
|
||||
| Journal | `/journal` | Notes, resume points, medication and weight entries, append-only corrections, and summary previews. Local prototype. |
|
||||
| Reader | `/inbox` | Searchable sample articles and a responsive article reader. Recommendation from activity history is not connected yet. |
|
||||
| Vitals | `/vitals` | Body and Environment dashboards with sample readings, charts, and simulated controls. No health or device connection. |
|
||||
| Research | `/deck` | Twitter and Mastodon columns, connected accounts, persistent decks, and optional Codex research chat. |
|
||||
|
||||
Home, Messages, and Journal share in-memory state across navigation; reloading
|
||||
resets it. The home input uses a deterministic English/Japanese interpreter to
|
||||
@@ -27,6 +27,21 @@ connections, and chat history use server-side persistence.
|
||||
See [execution support](docs/execution-support.md) and the
|
||||
[vitals preview](docs/vitals-preview.md) for prototype scope.
|
||||
|
||||
## URL state
|
||||
|
||||
Reader search and article selection, Messages views and conversations, Home
|
||||
previews, account management, Journal review/correction views, and Vitals
|
||||
navigation are represented in search parameters. Research uses deck, run, and
|
||||
citation IDs. Reloading or browser Back/Forward restores the view while its
|
||||
underlying data remains available. Login preserves the requested workspace URL.
|
||||
|
||||
Explicit selections add browser history; search input replaces it after a
|
||||
300 ms debounce using TanStack Pacer. Navigation cancels pending search updates,
|
||||
and IME composition waits until confirmed. URL navigation never submits prompts,
|
||||
sends messages, or repeats device actions. Drafts, secrets, results, and task
|
||||
contents remain outside the URL. Unsaved mock records and manual temporary
|
||||
decks still disappear on reload; a URL does not persist their contents.
|
||||
|
||||
## Direction: personal context and execution
|
||||
|
||||
The intended connection with the separate private `life` / Rensheng repository
|
||||
@@ -52,7 +67,7 @@ or change direction.
|
||||
- Native X search syntax, Top/Latest ranking, and optional `filter:follows`
|
||||
- List discovery for the account selected in the column editor
|
||||
- Column editing, ordering, deletion/undo, refresh, and cursor pagination
|
||||
- SQLite-backed shared decks, revision conflicts, and device-local selection
|
||||
- SQLite-backed shared decks, revision conflicts, and URL-based selection
|
||||
- Temporary views for exploration, with explicit saving
|
||||
- Server-only encrypted Mastodon credentials and configurable Tailscale owner access
|
||||
- Read-only post cards with source links, text, media, and quotes
|
||||
@@ -106,7 +121,8 @@ nix develop -c pnpm dev
|
||||
nix develop -c pnpm dev:tailscale
|
||||
nix develop -c pnpm generate:e2e-openapi
|
||||
nix develop -c pnpm lint
|
||||
nix develop -c pnpm lint:ui
|
||||
nix develop -c pnpm format
|
||||
nix develop -c pnpm format:check
|
||||
nix develop -c pnpm typecheck
|
||||
nix develop -c pnpm knip
|
||||
nix develop -c pnpm test
|
||||
@@ -116,27 +132,37 @@ nix develop -c pnpm build
|
||||
nix develop -c pnpm start
|
||||
nix develop -c pnpm account:setup
|
||||
nix develop -c pnpm db:generate
|
||||
nix develop -c pnpm codex:serve
|
||||
```
|
||||
|
||||
## Home Codex research prototype
|
||||
|
||||
Run `codex login` as the host user, then keep `pnpm codex:serve` running separately
|
||||
from the web server. It starts a resident app-server at `ws://127.0.0.1:4500`
|
||||
using that user's existing Codex login. The launcher passes a small environment
|
||||
allowlist, without the app's database, relay or SNS secret settings. Configured
|
||||
MCP servers are disabled for this process without changing the user's settings.
|
||||
The prototype was developed against Codex CLI 0.156.1; its WebSocket and dynamic
|
||||
tool APIs are experimental.
|
||||
Run `codex login` as the host user. The backend uses Vercel AI SDK 7 and
|
||||
`ai-sdk-provider-codex-cli` 2.3 to manage a local Codex app-server over stdio.
|
||||
A separate WebSocket service is no longer required. Use Codex CLI 0.156.0 or
|
||||
newer; set an explicit executable path if it is not on PATH.
|
||||
The child receives an environment allowlist; inherited MCP servers, hooks,
|
||||
web search and multi-agent execution are disabled for these research turns.
|
||||
The user's Codex settings are not changed.
|
||||
|
||||
Configure the web app with these values in `.env.local` (absolute report path):
|
||||
Configure the web app in `.env.local`:
|
||||
|
||||
```dotenv
|
||||
TWITTER_LITE_CODEX_URL=ws://127.0.0.1:4500
|
||||
TWITTER_LITE_CODEX_MODEL=gpt-6-astra
|
||||
TWITTER_LITE_REPORT_ROOT=/absolute/path/to/twitter-lite/.data/research
|
||||
# Optional:
|
||||
# TWITTER_LITE_CODEX_PATH=/absolute/path/to/codex
|
||||
```
|
||||
|
||||
This provider does not register AI SDK `tools` as Codex dynamic tools.
|
||||
The existing validated, account-scoped research functions are exposed through
|
||||
its in-process `createSdkMcpServer` bridge instead. Streaming uses `streamText`
|
||||
and `smoothStream` with 15 ms pacing and Japanese-aware chunking:
|
||||
`/[\u3040-\u309F\u30A0-\u30FF]|\S+\s+/`.
|
||||
Streamdown renders the Markdown and animates new text only while the final
|
||||
assistant message is live; completed history is immediate and reduced-motion
|
||||
preferences are respected. `@shadcn/helpers/ai-sdk` supplies deterministic
|
||||
streaming fixtures for renderer tests.
|
||||
|
||||
Use the chat on the left while browsing horizontally scrollable deck columns on
|
||||
the right. Messages continue the same Codex thread and include the currently
|
||||
open deck as context. The account selector controls which connections Codex can
|
||||
@@ -152,15 +178,15 @@ Manual deck creation and column editing remain
|
||||
available. The backend handles tools even when the browser is closed.
|
||||
Use **New chat** to leave the current conversation and start a fresh Codex
|
||||
thread with your next message. The open deck and host reports remain available.
|
||||
If a turn is running, stop it first. The conversation reset is shared across
|
||||
open devices. **Chat history** reopens saved conversations with their account
|
||||
A running turn continues in the background when you open another chat.
|
||||
Selection belongs to the current URL; another browser does not change it. **Chat history** reopens saved conversations with their account
|
||||
selection, deck and citations; the next message resumes the same Codex thread.
|
||||
SQLite stores conversation snapshots and the active conversation selection.
|
||||
SQLite stores conversation snapshots. Active selection is not stored globally.
|
||||
Each turn is limited to 6 columns, 12 upstream requests shared between list
|
||||
discovery and post retrieval (at most 20 posts per fetch), and 15 minutes.
|
||||
The prototype accepts up to 100 messages per backend process.
|
||||
SSE pushes conversation messages, tool activity and generated deck updates to
|
||||
open browsers, without polling.
|
||||
SSE pushes messages, tool activity and deck updates for the requested conversation
|
||||
ID, without polling. Separate conversations have independent executions.
|
||||
Codex replies render as Markdown, including tables, lists and code. Clicking a
|
||||
cited post link scrolls to its card and briefly highlights it. Loaded cards take
|
||||
priority; otherwise the app shows the post captured during research in a column
|
||||
@@ -183,12 +209,11 @@ not independently verified. Report contents remain on the host filesystem;
|
||||
the app DB stores the conversation, thread ID, deck context and citation metadata.
|
||||
|
||||
Browser disconnection does not cancel research; reconnecting restores the latest
|
||||
state through `/api/research/events`, under the same owner access checks as the
|
||||
state through `/api/research/events?run=<id>`, under the same owner access checks as the
|
||||
rest of the app. After a backend restart, unfinished turns appear as interrupted;
|
||||
sending another message resumes the same thread. Before starting that message,
|
||||
the backend checks the resumed thread and interrupts any orphaned active turn.
|
||||
sending another message explicitly resumes the same thread through the provider.
|
||||
It does not automatically rerun interrupted requests. Files already written remain.
|
||||
Cancellation is explicit. The launcher and report directory must
|
||||
Cancellation is explicit. The backend and report directory must
|
||||
run under a user able to write those files. NixOS DynamicUser service integration
|
||||
and dedicated service credentials are outside this prototype.
|
||||
|
||||
@@ -216,22 +241,31 @@ editing, ordering, and deletion; refresh stays available directly in each header
|
||||
|
||||
Shared UI components live in `src/components/ui`; `components.json` configures
|
||||
the registry and theme entrypoint. Chat uses MessageScroller, Message, Bubble,
|
||||
Marker and InputGroup while retaining the existing Codex transport and saved
|
||||
conversation model. The icon rail switches between chat and deck management;
|
||||
Marker and InputGroup with Streamdown, reconnectable snapshot SSE, and saved
|
||||
conversations. The icon rail switches between chat and deck management;
|
||||
closing the mobile sheet or crossing the desktop breakpoint preserves chat
|
||||
drafts, selected accounts and live deck updates. Theme colors are declared in
|
||||
`src/ui.css`.
|
||||
`src/ui.css`. The shared entrypoint `src/styles.css` loads the theme, tokens,
|
||||
base styles and feature styles in that order, so the UI lint rules can resolve
|
||||
application classes through the same stylesheet used by the app.
|
||||
|
||||
`pnpm lint` runs Biome and the UI checks. `pnpm lint:ui` runs
|
||||
[@shadcn/lint](https://github.com/shadcn-ui/lint) through Oxlint. The
|
||||
`pnpm lint` runs Oxlint with type-aware linting and TypeScript checking through
|
||||
`oxlint-tsgolint`, including the
|
||||
[@shadcn/lint](https://github.com/shadcn-ui/lint) UI checks. The
|
||||
`shadcn/no-raw-colors` rule in `.oxlintrc.json` requires theme colors in JSX and
|
||||
class helpers, including shared UI components. Layout values remain available
|
||||
for the sidebar and deck. Biome continues to handle formatting and general
|
||||
code checks. Knip checks application usage while retaining the public exports
|
||||
for the sidebar and deck. Component contracts allow only the existing workspace
|
||||
styles; Next.js-specific rules are omitted because this app uses TanStack Start.
|
||||
`pnpm format` formats files with Oxfmt defaults, and
|
||||
`pnpm format:check` checks formatting without changing files. Knip checks application usage while retaining the public exports
|
||||
of registry components for composition. Additional design rules can be configured
|
||||
in `.oxlintrc.json`;
|
||||
see the [available rules](https://github.com/shadcn-ui/lint#rules).
|
||||
|
||||
The database and OpenAPI generators format their output with Oxfmt. TanStack
|
||||
Router owns the formatting of `src/routeTree.gen.ts`, which is excluded from
|
||||
formatting; `pnpm check:routes` verifies that regeneration leaves it unchanged.
|
||||
|
||||
Create a deck for an investigation, then add columns for each perspective.
|
||||
Choose a connected account and one of its supported sources. Account
|
||||
changes affect only the edited column. Matching source conditions and accounts
|
||||
|
||||
-32
@@ -1,32 +0,0 @@
|
||||
{
|
||||
"$schema": "https://biomejs.dev/schemas/2.5.3/schema.json",
|
||||
"files": {
|
||||
"includes": [
|
||||
"**",
|
||||
"!src/routeTree.gen.ts",
|
||||
"!!**/.output",
|
||||
"!!**/test-results"
|
||||
]
|
||||
},
|
||||
"formatter": {
|
||||
"enabled": true,
|
||||
"indentStyle": "space"
|
||||
},
|
||||
"linter": {
|
||||
"enabled": true,
|
||||
"rules": {
|
||||
"recommended": true
|
||||
}
|
||||
},
|
||||
"javascript": {
|
||||
"formatter": {
|
||||
"quoteStyle": "single",
|
||||
"semicolons": "asNeeded"
|
||||
}
|
||||
},
|
||||
"css": {
|
||||
"parser": {
|
||||
"tailwindDirectives": true
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -5,7 +5,7 @@
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "",
|
||||
"css": "src/ui.css",
|
||||
"css": "src/styles.css",
|
||||
"baseColor": "neutral",
|
||||
"cssVariables": true
|
||||
},
|
||||
|
||||
@@ -34,10 +34,10 @@ flowchart LR
|
||||
|
||||
SQLiteを採用する案。端末が増えても、各端末がSQLを実行するのではなく、同じアプリサーバーへアクセスする。デッキ設定とOAuth情報の小さな更新には単一サーバーのSQLiteで始められると判断する。DBファイルを端末間コピーしたり、NAS上のファイルを複数サーバーから直接開いたりしない。
|
||||
|
||||
| 候補 | 今回の評価 |
|
||||
| --- | --- |
|
||||
| SQLite | 推奨。別DBサービス不要。ローカルディスク、短いトランザクション、マイグレーション、復元試験を用意する |
|
||||
| PostgreSQL | 複数アプリサーバーや大量の並列収集へ進む時に再評価。現時点では運用対象が増える |
|
||||
| 候補 | 今回の評価 |
|
||||
| -------------------- | ------------------------------------------------------------------------------------------------------------ |
|
||||
| SQLite | 推奨。別DBサービス不要。ローカルディスク、短いトランザクション、マイグレーション、復元試験を用意する |
|
||||
| PostgreSQL | 複数アプリサーバーや大量の並列収集へ進む時に再評価。現時点では運用対象が増える |
|
||||
| ブラウザDB+同期基盤 | 採用しない。今回必要なのはオンラインで同じサーバー状態を読むこと。オフライン同期エンジンを導入する必要はない |
|
||||
|
||||
SQLiteは同時書き込みが1つという制約がある。複数サーバー・高い書き込み並列度ではclient/server DBを検討する。[SQLiteの用途](https://www.sqlite.org/whentouse.html)
|
||||
@@ -52,14 +52,14 @@ OAuth callbackは固定のHTTPS URLにする。認可後に戻る主体はブラ
|
||||
|
||||
## 保存モデル案
|
||||
|
||||
| 保存対象 | 主な内容 |
|
||||
| --- | --- |
|
||||
| `decks` | ID、名前、revision、更新日時 |
|
||||
| `deck_columns` | ID、deck ID、並び順、connection ID、名前、platform固有のsource JSON |
|
||||
| `connections` | ID、platform、接続先origin、外部アカウントIDまたはrelay profile参照、表示名、接続状態 |
|
||||
| `connection_credentials` | connection ID、暗号化したアクセストークン、鍵の識別子。通常の接続一覧とは分離 |
|
||||
| `oauth_apps` | インスタンスorigin、callback・scope構成、client ID、暗号化したclient secret |
|
||||
| `oauth_attempts` | 短時間有効なstate、開始ブラウザとの束縛、接続先、PKCE verifier、期限、一度限りの消費状態 |
|
||||
| 保存対象 | 主な内容 |
|
||||
| ------------------------ | ---------------------------------------------------------------------------------------- |
|
||||
| `decks` | ID、名前、revision、更新日時 |
|
||||
| `deck_columns` | ID、deck ID、並び順、connection ID、名前、platform固有のsource JSON |
|
||||
| `connections` | ID、platform、接続先origin、外部アカウントIDまたはrelay profile参照、表示名、接続状態 |
|
||||
| `connection_credentials` | connection ID、暗号化したアクセストークン、鍵の識別子。通常の接続一覧とは分離 |
|
||||
| `oauth_apps` | インスタンスorigin、callback・scope構成、client ID、暗号化したclient secret |
|
||||
| `oauth_attempts` | 短時間有効なstate、開始ブラウザとの束縛、接続先、PKCE verifier、期限、一度限りの消費状態 |
|
||||
|
||||
単一利用者なので、この段階ではusers/organizations/roles等のテーブルを作らない。
|
||||
|
||||
@@ -69,16 +69,16 @@ DBには秘密を暗号化して保存し、暗号鍵はDB外の実行時credent
|
||||
|
||||
## PR単位のタスク
|
||||
|
||||
| ID | タスク | 依存 | 完了条件 |
|
||||
| --- | --- | --- | --- |
|
||||
| T0 | 実行環境とアクセス経路を固定 | なし | 本番HTTPS origin/callback候補を決め、PC・スマホから同じ利用者として接続。Serve以外からのヘッダー偽装を許さない構成を確認。対象Mastodonのバージョン・OAuthメタデータも調べる |
|
||||
| T1 | SQLiteと永続ディレクトリを導入 | T0 | 現行Node/Nixで動くdriver・migration方式を実証。`StateDirectory`等でDBを永続化。再起動・アプリ更新後も残り、バックアップから復元できる。依存追加時はflakeのpnpm hash更新まで行う |
|
||||
| T2 | connectionモデルへTwitterを移行 | T1 | relay profile一覧から接続を作り、カラム・取得・キャッシュ・ページ送りがconnection IDを使う。2つのTwitter接続を混ぜずに並列表示。不明・削除済みの接続はエラーとして残す |
|
||||
| T3 | デッキをサーバー保存し端末間共有 | T1,T2 | PCで作ったデッキが別ブラウザコンテキストに表示。revision競合で上書きを拒否。WebMCPも同じ保存処理を使う。既存localStorageからの明示インポートを提供し、重複取り込みと既存DBの破壊を防ぐ |
|
||||
| T4 | Mastodon OAuthとトークン保管 | T0,T1,T2 | 接続先登録→認可→callback→本人確認→暗号化保存。同一インスタンス2アカウント・別インスタンス・再接続・解除が動く。拒否/state不一致・期限切れ・再利用を検証 |
|
||||
| T5 | Mastodon取得と投稿正規化 | T4 | ユーザー投稿・リスト・ハッシュタグのカラムを実装し、全文検索を別の能力として扱う。CW・sensitiveメディア・boost・HTML本文を安全に表示。元投稿URLと取得インスタンスのローカルIDを区別 |
|
||||
| T6 | 複数接続UIとWebMCPを統合 | T3,T5 | 接続管理から追加・再接続・解除。カラムは接続に応じたsourceを選べる。Twitter/Mastodonを同じデッキに並べ、AIも接続一覧を発見して作成・取得できる |
|
||||
| T7 | 2端末・障害・運用の通し検証 | T3,T4,T6 | 端末AのOAuth接続を端末Bで再認可せず利用。編集競合・接続失効・429・再起動・DB復元の検証。UI/API/WebMCP/ログにトークンが出ないことを確認 |
|
||||
| ID | タスク | 依存 | 完了条件 |
|
||||
| --- | -------------------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| T0 | 実行環境とアクセス経路を固定 | なし | 本番HTTPS origin/callback候補を決め、PC・スマホから同じ利用者として接続。Serve以外からのヘッダー偽装を許さない構成を確認。対象Mastodonのバージョン・OAuthメタデータも調べる |
|
||||
| T1 | SQLiteと永続ディレクトリを導入 | T0 | 現行Node/Nixで動くdriver・migration方式を実証。`StateDirectory`等でDBを永続化。再起動・アプリ更新後も残り、バックアップから復元できる。依存追加時はflakeのpnpm hash更新まで行う |
|
||||
| T2 | connectionモデルへTwitterを移行 | T1 | relay profile一覧から接続を作り、カラム・取得・キャッシュ・ページ送りがconnection IDを使う。2つのTwitter接続を混ぜずに並列表示。不明・削除済みの接続はエラーとして残す |
|
||||
| T3 | デッキをサーバー保存し端末間共有 | T1,T2 | PCで作ったデッキが別ブラウザコンテキストに表示。revision競合で上書きを拒否。WebMCPも同じ保存処理を使う。既存localStorageからの明示インポートを提供し、重複取り込みと既存DBの破壊を防ぐ |
|
||||
| T4 | Mastodon OAuthとトークン保管 | T0,T1,T2 | 接続先登録→認可→callback→本人確認→暗号化保存。同一インスタンス2アカウント・別インスタンス・再接続・解除が動く。拒否/state不一致・期限切れ・再利用を検証 |
|
||||
| T5 | Mastodon取得と投稿正規化 | T4 | ユーザー投稿・リスト・ハッシュタグのカラムを実装し、全文検索を別の能力として扱う。CW・sensitiveメディア・boost・HTML本文を安全に表示。元投稿URLと取得インスタンスのローカルIDを区別 |
|
||||
| T6 | 複数接続UIとWebMCPを統合 | T3,T5 | 接続管理から追加・再接続・解除。カラムは接続に応じたsourceを選べる。Twitter/Mastodonを同じデッキに並べ、AIも接続一覧を発見して作成・取得できる |
|
||||
| T7 | 2端末・障害・運用の通し検証 | T3,T4,T6 | 端末AのOAuth接続を端末Bで再認可せず利用。編集競合・接続失効・429・再起動・DB復元の検証。UI/API/WebMCP/ログにトークンが出ないことを確認 |
|
||||
|
||||
各PRに必要な単体・統合テストを含める。T7までテストを先送りしない。
|
||||
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
# shadcn workspace migration
|
||||
|
||||
> Tooling update: Biome has since been replaced by Oxlint for linting (including
|
||||
> the shadcn rules) and Oxfmt for formatting. The checks recorded below describe
|
||||
> the tooling used at the time of this migration; see README for current commands.
|
||||
|
||||
## Target
|
||||
|
||||
Replace the custom workspace UI with the Base UI version of shadcn sidebar-09
|
||||
|
||||
@@ -9,12 +9,12 @@ tailnet HTTPS address after Mastodon authorization.
|
||||
|
||||
## Runtime configuration
|
||||
|
||||
| Variable | Value |
|
||||
| --- | --- |
|
||||
| `TWITTER_LITE_ORIGIN` | Exact Serve HTTPS origin, without a trailing slash |
|
||||
| `TWITTER_LITE_ALLOWED_LOGIN` | Owner's Tailscale login |
|
||||
| `TWITTER_LITE_DB_PATH` | Absolute path to the SQLite database on local disk |
|
||||
| `TWITTER_LITE_MASTODON_ORIGINS` | Comma-separated approved HTTPS instance origins |
|
||||
| Variable | Value |
|
||||
| ---------------------------------- | --------------------------------------------------------- |
|
||||
| `TWITTER_LITE_ORIGIN` | Exact Serve HTTPS origin, without a trailing slash |
|
||||
| `TWITTER_LITE_ALLOWED_LOGIN` | Owner's Tailscale login |
|
||||
| `TWITTER_LITE_DB_PATH` | Absolute path to the SQLite database on local disk |
|
||||
| `TWITTER_LITE_MASTODON_ORIGINS` | Comma-separated approved HTTPS instance origins |
|
||||
| `TWITTER_LITE_CREDENTIAL_KEY_FILE` | Runtime file containing 32 random bytes encoded as base64 |
|
||||
|
||||
The credential key is required for Mastodon, but not for Twitter-only use.
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -89,14 +89,14 @@ The server input has two valid states:
|
||||
|
||||
```ts
|
||||
type InitialThreadInput = {
|
||||
tweetId: string
|
||||
}
|
||||
tweetId: string;
|
||||
};
|
||||
|
||||
type ContinuedThreadInput = {
|
||||
tweetId: string
|
||||
conversationId: string
|
||||
cursor: string
|
||||
}
|
||||
tweetId: string;
|
||||
conversationId: string;
|
||||
cursor: string;
|
||||
};
|
||||
```
|
||||
|
||||
All IDs must be decimal strings. Cursor values are opaque non-empty strings.
|
||||
@@ -106,11 +106,11 @@ The successful page shape is:
|
||||
|
||||
```ts
|
||||
type ThreadPage = {
|
||||
tweets: Post[]
|
||||
focalPost?: Post
|
||||
conversationId: string
|
||||
nextCursor?: string
|
||||
}
|
||||
tweets: Post[];
|
||||
focalPost?: Post;
|
||||
conversationId: string;
|
||||
nextCursor?: string;
|
||||
};
|
||||
```
|
||||
|
||||
`focalPost` is present only on the initial page. All returned posts pass through
|
||||
@@ -137,7 +137,7 @@ cannot rediscover the conversation root if the cursor page omits that reply.
|
||||
|
||||
1. Validate the supplied `tweetId`, `conversationId`, and cursor.
|
||||
2. Call `getThreadPaged(conversationId, {
|
||||
cursor, maxPages: 1, pageDelayMs: 0 })` once.
|
||||
cursor, maxPages: 1, pageDelayMs: 0 })` once.
|
||||
3. Return the sanitized tweets with the same conversation ID and new cursor.
|
||||
|
||||
The original `tweetId` remains part of the query identity and validation
|
||||
@@ -149,7 +149,10 @@ continuation requests.
|
||||
Extend the existing feed request union with:
|
||||
|
||||
```ts
|
||||
{ kind: 'thread'; tweetId: string }
|
||||
{
|
||||
kind: "thread";
|
||||
tweetId: string;
|
||||
}
|
||||
```
|
||||
|
||||
The thread query key includes the focal tweet ID. Its initial page parameter is
|
||||
|
||||
@@ -68,16 +68,16 @@ Work spans two repositories and the Gitea package registry:
|
||||
Bird currently hard-codes `product: 'Latest'` in SearchTimeline requests. Add the following public contract:
|
||||
|
||||
```ts
|
||||
export type SearchProduct = 'Top' | 'Latest'
|
||||
export type SearchProduct = "Top" | "Latest";
|
||||
|
||||
export interface SearchFetchOptions {
|
||||
includeRaw?: boolean
|
||||
product?: SearchProduct
|
||||
includeRaw?: boolean;
|
||||
product?: SearchProduct;
|
||||
}
|
||||
|
||||
export interface SearchPaginationOptions extends SearchFetchOptions {
|
||||
maxPages?: number
|
||||
cursor?: string
|
||||
maxPages?: number;
|
||||
cursor?: string;
|
||||
}
|
||||
```
|
||||
|
||||
@@ -126,7 +126,7 @@ new TwitterClient({
|
||||
relayBaseUrl: process.env.TWITTER_RELAY_BASE_URL,
|
||||
profileName: process.env.BIRD_PROFILE_NAME,
|
||||
timeoutMs: 20_000,
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
`TWITTER_RELAY_BASE_URL` is required. `BIRD_PROFILE_NAME` is optional but must be passed explicitly because the Bird library does not read that environment variable itself.
|
||||
|
||||
+5
-5
@@ -1,7 +1,7 @@
|
||||
import { defineConfig } from 'drizzle-kit'
|
||||
import { defineConfig } from "drizzle-kit";
|
||||
|
||||
export default defineConfig({
|
||||
dialect: 'sqlite',
|
||||
schema: './src/features/storage/schema.ts',
|
||||
out: './drizzle',
|
||||
})
|
||||
dialect: "sqlite",
|
||||
schema: "./src/features/storage/schema.ts",
|
||||
out: "./drizzle",
|
||||
});
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
inherit (finalAttrs) pname version src;
|
||||
pnpm = pkgs.pnpm_11;
|
||||
fetcherVersion = 4;
|
||||
hash = "sha256-+DqNt+58TrI0W8SU//JNdYYJtCdKNIG7gfWRxno36QQ=";
|
||||
hash = "sha256-60nQAoDWoO15P7TFLhABqGNYpJy/R+w+Zz3oo7qD8dM=";
|
||||
};
|
||||
|
||||
nativeBuildInputs = with pkgs; [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { KnipConfig } from 'knip'
|
||||
import type { KnipConfig } from "knip";
|
||||
|
||||
export default {
|
||||
entry: ['playwright.config.ts', 'e2e/**/*.test.ts', 'e2e/mock-relay.mjs'],
|
||||
entry: ["playwright.config.ts", "e2e/**/*.test.ts", "e2e/mock-relay.mjs"],
|
||||
playwright: false,
|
||||
} satisfies KnipConfig
|
||||
} satisfies KnipConfig;
|
||||
|
||||
+30
-33
@@ -1,64 +1,61 @@
|
||||
import { defineConfig, defineTransformer } from 'orval'
|
||||
import { defineConfig, defineTransformer } from "orval";
|
||||
|
||||
const selectedTag = 'twitter-lite-e2e'
|
||||
const selectedTag = "twitter-lite-e2e";
|
||||
const selectedOperationIds = new Set([
|
||||
'getUserByScreenName',
|
||||
'getUserTweets',
|
||||
'getTweetDetail',
|
||||
'getSearchTimeline',
|
||||
'getListLatestTweetsTimeline',
|
||||
])
|
||||
"getUserByScreenName",
|
||||
"getUserTweets",
|
||||
"getTweetDetail",
|
||||
"getSearchTimeline",
|
||||
"getListLatestTweetsTimeline",
|
||||
]);
|
||||
|
||||
const selectE2eOperations = defineTransformer((document) => {
|
||||
const foundOperationIds = new Set<string>()
|
||||
const foundOperationIds = new Set<string>();
|
||||
|
||||
for (const pathItem of Object.values(document.paths ?? {})) {
|
||||
if (!pathItem || typeof pathItem !== 'object') continue
|
||||
if (!pathItem || typeof pathItem !== "object") continue;
|
||||
|
||||
for (const candidate of Object.values(pathItem)) {
|
||||
if (
|
||||
!candidate ||
|
||||
typeof candidate !== 'object' ||
|
||||
typeof candidate !== "object" ||
|
||||
Array.isArray(candidate) ||
|
||||
!('operationId' in candidate)
|
||||
!("operationId" in candidate)
|
||||
) {
|
||||
continue
|
||||
continue;
|
||||
}
|
||||
|
||||
const operation = candidate as {
|
||||
operationId?: string
|
||||
tags?: string[]
|
||||
}
|
||||
operationId?: string;
|
||||
tags?: string[];
|
||||
};
|
||||
|
||||
if (
|
||||
operation.operationId &&
|
||||
selectedOperationIds.has(operation.operationId)
|
||||
) {
|
||||
foundOperationIds.add(operation.operationId)
|
||||
operation.tags = [...(operation.tags ?? []), selectedTag]
|
||||
if (operation.operationId && selectedOperationIds.has(operation.operationId)) {
|
||||
foundOperationIds.add(operation.operationId);
|
||||
operation.tags = [...(operation.tags ?? []), selectedTag];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const missingOperationIds = [...selectedOperationIds].filter(
|
||||
(operationId) => !foundOperationIds.has(operationId),
|
||||
)
|
||||
);
|
||||
if (missingOperationIds.length > 0) {
|
||||
throw new Error(
|
||||
`twitter-openapi is missing selected operations: ${missingOperationIds.join(', ')}`,
|
||||
)
|
||||
`twitter-openapi is missing selected operations: ${missingOperationIds.join(", ")}`,
|
||||
);
|
||||
}
|
||||
|
||||
return document
|
||||
})
|
||||
return document;
|
||||
});
|
||||
|
||||
export default defineConfig({
|
||||
twitterOpenApi: {
|
||||
input: {
|
||||
target:
|
||||
'https://raw.githubusercontent.com/fa0311/twitter-openapi/590dae5c9f8575abc91d3774946bfe6f23960aba/dist/docs/openapi-3.0.yaml',
|
||||
"https://raw.githubusercontent.com/fa0311/twitter-openapi/590dae5c9f8575abc91d3774946bfe6f23960aba/dist/docs/openapi-3.0.yaml",
|
||||
filters: {
|
||||
mode: 'include',
|
||||
mode: "include",
|
||||
tags: [selectedTag],
|
||||
},
|
||||
override: {
|
||||
@@ -67,9 +64,9 @@ export default defineConfig({
|
||||
},
|
||||
output: {
|
||||
clean: true,
|
||||
client: 'fetch',
|
||||
mode: 'single',
|
||||
target: './e2e/generated/twitter-openapi/client.ts',
|
||||
client: "fetch",
|
||||
mode: "single",
|
||||
target: "./e2e/generated/twitter-openapi/client.ts",
|
||||
},
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
import { getSetupToken, readAuthState } from '../src/features/auth/auth.server'
|
||||
import { getSetupToken, readAuthState } from "../src/features/auth/auth.server";
|
||||
|
||||
if (!readAuthState().needsSetup) {
|
||||
console.error(
|
||||
'This workspace already has an owner. Sign in with your existing account.',
|
||||
)
|
||||
process.exitCode = 1
|
||||
console.error("This workspace already has an owner. Sign in with your existing account.");
|
||||
process.exitCode = 1;
|
||||
} else {
|
||||
console.log(
|
||||
'Open /setup in your workspace and enter this one-time setup code:',
|
||||
)
|
||||
console.log(getSetupToken())
|
||||
console.log("Open /setup in your workspace and enter this one-time setup code:");
|
||||
console.log(getSetupToken());
|
||||
}
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import { backupDatabase } from '../src/features/storage/backup.server'
|
||||
import { backupDatabase } from "../src/features/storage/backup.server";
|
||||
|
||||
const source = process.env.TWITTER_LITE_DB_PATH
|
||||
const destination = process.argv[2]
|
||||
const source = process.env.TWITTER_LITE_DB_PATH;
|
||||
const destination = process.argv[2];
|
||||
if (!source || !destination || process.argv.length !== 3) {
|
||||
console.error(
|
||||
'Usage: TWITTER_LITE_DB_PATH=/absolute/app.db pnpm db:backup /absolute/backup.db',
|
||||
)
|
||||
process.exitCode = 1
|
||||
console.error("Usage: TWITTER_LITE_DB_PATH=/absolute/app.db pnpm db:backup /absolute/backup.db");
|
||||
process.exitCode = 1;
|
||||
} else {
|
||||
await backupDatabase(source, destination)
|
||||
console.log(`Backup verified: ${destination}`)
|
||||
await backupDatabase(source, destination);
|
||||
console.log(`Backup verified: ${destination}`);
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { build } from 'vite'
|
||||
import { build } from "vite";
|
||||
|
||||
for (const entry of ['backup-database', 'account-setup']) {
|
||||
for (const entry of ["backup-database", "account-setup"]) {
|
||||
await build({
|
||||
configFile: false,
|
||||
build: {
|
||||
ssr: `scripts/${entry}.ts`,
|
||||
outDir: '.output/server/tools',
|
||||
outDir: ".output/server/tools",
|
||||
emptyOutDir: false,
|
||||
rollupOptions: { external: ['better-sqlite3'] },
|
||||
rollupOptions: { external: ["better-sqlite3"] },
|
||||
},
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { writeFileSync } from 'node:fs'
|
||||
import { readMigrationFiles } from 'drizzle-orm/migrator'
|
||||
import { writeFileSync } from "node:fs";
|
||||
import { readMigrationFiles } from "drizzle-orm/migrator";
|
||||
|
||||
const migrations = readMigrationFiles({
|
||||
migrationsFolder: new URL('../drizzle', import.meta.url).pathname,
|
||||
})
|
||||
migrationsFolder: new URL("../drizzle", import.meta.url).pathname,
|
||||
});
|
||||
writeFileSync(
|
||||
new URL('../src/features/storage/migrations.generated.ts', import.meta.url),
|
||||
new URL("../src/features/storage/migrations.generated.ts", import.meta.url),
|
||||
`// Generated by pnpm db:generate. Do not edit.\nexport const migrations = ${JSON.stringify(migrations, null, 2)}\n`,
|
||||
)
|
||||
);
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { tanstackStart } from '@tanstack/react-start/plugin/vite'
|
||||
import { createServer } from 'vite'
|
||||
import { tanstackStart } from "@tanstack/react-start/plugin/vite";
|
||||
import { createServer } from "vite";
|
||||
|
||||
const server = await createServer({
|
||||
configFile: false,
|
||||
plugins: [tanstackStart()],
|
||||
server: { middlewareMode: true },
|
||||
})
|
||||
await server.close()
|
||||
});
|
||||
await server.close();
|
||||
|
||||
+18
-23
@@ -1,28 +1,23 @@
|
||||
// @vitest-environment node
|
||||
|
||||
import { TwitterClient } from '@yuta/bird'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { TwitterClient } from "@yuta/bird";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
describe.runIf(process.env.TWITTER_LITE_LIVE === '1')(
|
||||
'configured relay',
|
||||
() => {
|
||||
it('performs one read-only Top search', async () => {
|
||||
const relayBaseUrl = process.env.TWITTER_RELAY_BASE_URL
|
||||
if (!relayBaseUrl) {
|
||||
throw new Error(
|
||||
'TWITTER_RELAY_BASE_URL is required for the opt-in live smoke test',
|
||||
)
|
||||
}
|
||||
describe.runIf(process.env.TWITTER_LITE_LIVE === "1")("configured relay", () => {
|
||||
it("performs one read-only Top search", async () => {
|
||||
const relayBaseUrl = process.env.TWITTER_RELAY_BASE_URL;
|
||||
if (!relayBaseUrl) {
|
||||
throw new Error("TWITTER_RELAY_BASE_URL is required for the opt-in live smoke test");
|
||||
}
|
||||
|
||||
const client = new TwitterClient({
|
||||
relayBaseUrl,
|
||||
profileName: process.env.BIRD_PROFILE_NAME,
|
||||
timeoutMs: 20_000,
|
||||
})
|
||||
const query = process.env.TWITTER_LITE_LIVE_QUERY ?? 'OpenAI'
|
||||
const result = await client.search(query, 1, { product: 'Top' })
|
||||
const client = new TwitterClient({
|
||||
relayBaseUrl,
|
||||
profileName: process.env.BIRD_PROFILE_NAME,
|
||||
timeoutMs: 20_000,
|
||||
});
|
||||
const query = process.env.TWITTER_LITE_LIVE_QUERY ?? "OpenAI";
|
||||
const result = await client.search(query, 1, { product: "Top" });
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
}, 30_000)
|
||||
},
|
||||
)
|
||||
expect(result.success).toBe(true);
|
||||
}, 30_000);
|
||||
});
|
||||
|
||||
+4
-4
@@ -1,5 +1,5 @@
|
||||
import '@testing-library/jest-dom/vitest'
|
||||
import { cleanup } from '@testing-library/react'
|
||||
import { afterEach } from 'vitest'
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { cleanup } from "@testing-library/react";
|
||||
import { afterEach } from "vitest";
|
||||
|
||||
afterEach(cleanup)
|
||||
afterEach(cleanup);
|
||||
|
||||
+1
-2
@@ -19,8 +19,7 @@
|
||||
--color-shadow: oklch(10% 0.015 250 / 45%);
|
||||
--color-overlay: oklch(10% 0.015 250 / 70%);
|
||||
--font-body:
|
||||
-apple-system, BlinkMacSystemFont, "Segoe UI", "Hiragino Sans",
|
||||
"Noto Sans JP", sans-serif;
|
||||
-apple-system, BlinkMacSystemFont, "Segoe UI", "Hiragino Sans", "Noto Sans JP", sans-serif;
|
||||
--font-display: var(--font-body);
|
||||
--font-mono: ui-monospace, "SFMono-Regular", Consolas, monospace;
|
||||
--space-3xs: 0.125rem;
|
||||
|
||||
+6
-6
@@ -1,9 +1,9 @@
|
||||
import tailwindcss from '@tailwindcss/vite'
|
||||
import { tanstackStart } from '@tanstack/react-start/plugin/vite'
|
||||
import viteReact from '@vitejs/plugin-react'
|
||||
import { nitro } from 'nitro/vite'
|
||||
import { defineConfig } from 'vite'
|
||||
import tailwindcss from "@tailwindcss/vite";
|
||||
import { tanstackStart } from "@tanstack/react-start/plugin/vite";
|
||||
import viteReact from "@vitejs/plugin-react";
|
||||
import { nitro } from "nitro/vite";
|
||||
import { defineConfig } from "vite";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [tailwindcss(), nitro(), tanstackStart(), viteReact()],
|
||||
})
|
||||
});
|
||||
|
||||
+7
-7
@@ -1,11 +1,11 @@
|
||||
import path from 'node:path'
|
||||
import { configDefaults, defineConfig } from 'vitest/config'
|
||||
import path from "node:path";
|
||||
import { configDefaults, defineConfig } from "vitest/config";
|
||||
|
||||
export default defineConfig({
|
||||
resolve: { alias: { '#': path.resolve(import.meta.dirname, 'src') } },
|
||||
resolve: { alias: { "#": path.resolve(import.meta.dirname, "src") } },
|
||||
test: {
|
||||
environment: 'jsdom',
|
||||
setupFiles: ['./tests/setup.ts'],
|
||||
exclude: [...configDefaults.exclude, 'e2e/**'],
|
||||
environment: "jsdom",
|
||||
setupFiles: ["./tests/setup.ts"],
|
||||
exclude: [...configDefaults.exclude, "e2e/**"],
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user