chore: migrate linting and formatting to Oxc
This commit is contained in:
@@ -4,6 +4,7 @@
|
|||||||
.env*
|
.env*
|
||||||
!.env.example
|
!.env.example
|
||||||
.output/
|
.output/
|
||||||
|
.tanstack/
|
||||||
dist/
|
dist/
|
||||||
node_modules/
|
node_modules/
|
||||||
playwright-report/
|
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",
|
"$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"],
|
"jsPlugins": ["@shadcn/lint"],
|
||||||
"rules": {
|
"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"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"ignorePatterns": ["src/routeTree.gen.ts"]
|
{
|
||||||
|
"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"
|
||||||
|
},
|
||||||
|
"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"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ original language.
|
|||||||
## Workspaces and current status
|
## Workspaces and current status
|
||||||
|
|
||||||
| Workspace | Route | Current implementation |
|
| Workspace | Route | Current implementation |
|
||||||
| --- | --- | --- |
|
| --------- | ---------- | ---------------------------------------------------------------------------------------------------------------------- |
|
||||||
| Home | `/` | State-derived daily brief, flexible tasks and routines, and natural-language action proposals. Local prototype. |
|
| 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. |
|
| 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. |
|
| Journal | `/journal` | Notes, resume points, medication and weight entries, append-only corrections, and summary previews. Local prototype. |
|
||||||
@@ -27,6 +27,21 @@ connections, and chat history use server-side persistence.
|
|||||||
See [execution support](docs/execution-support.md) and the
|
See [execution support](docs/execution-support.md) and the
|
||||||
[vitals preview](docs/vitals-preview.md) for prototype scope.
|
[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
|
## Direction: personal context and execution
|
||||||
|
|
||||||
The intended connection with the separate private `life` / Rensheng repository
|
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`
|
- Native X search syntax, Top/Latest ranking, and optional `filter:follows`
|
||||||
- List discovery for the account selected in the column editor
|
- List discovery for the account selected in the column editor
|
||||||
- Column editing, ordering, deletion/undo, refresh, and cursor pagination
|
- 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
|
- Temporary views for exploration, with explicit saving
|
||||||
- Server-only encrypted Mastodon credentials and configurable Tailscale owner access
|
- Server-only encrypted Mastodon credentials and configurable Tailscale owner access
|
||||||
- Read-only post cards with source links, text, media, and quotes
|
- 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 dev:tailscale
|
||||||
nix develop -c pnpm generate:e2e-openapi
|
nix develop -c pnpm generate:e2e-openapi
|
||||||
nix develop -c pnpm lint
|
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 typecheck
|
||||||
nix develop -c pnpm knip
|
nix develop -c pnpm knip
|
||||||
nix develop -c pnpm test
|
nix develop -c pnpm test
|
||||||
@@ -116,27 +132,37 @@ nix develop -c pnpm build
|
|||||||
nix develop -c pnpm start
|
nix develop -c pnpm start
|
||||||
nix develop -c pnpm account:setup
|
nix develop -c pnpm account:setup
|
||||||
nix develop -c pnpm db:generate
|
nix develop -c pnpm db:generate
|
||||||
nix develop -c pnpm codex:serve
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Home Codex research prototype
|
## Home Codex research prototype
|
||||||
|
|
||||||
Run `codex login` as the host user, then keep `pnpm codex:serve` running separately
|
Run `codex login` as the host user. The backend uses Vercel AI SDK 7 and
|
||||||
from the web server. It starts a resident app-server at `ws://127.0.0.1:4500`
|
`ai-sdk-provider-codex-cli` 2.3 to manage a local Codex app-server over stdio.
|
||||||
using that user's existing Codex login. The launcher passes a small environment
|
A separate WebSocket service is no longer required. Use Codex CLI 0.156.0 or
|
||||||
allowlist, without the app's database, relay or SNS secret settings. Configured
|
newer; set an explicit executable path if it is not on PATH.
|
||||||
MCP servers are disabled for this process without changing the user's settings.
|
The child receives an environment allowlist; inherited MCP servers, hooks,
|
||||||
The prototype was developed against Codex CLI 0.156.1; its WebSocket and dynamic
|
web search and multi-agent execution are disabled for these research turns.
|
||||||
tool APIs are experimental.
|
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
|
```dotenv
|
||||||
TWITTER_LITE_CODEX_URL=ws://127.0.0.1:4500
|
|
||||||
TWITTER_LITE_CODEX_MODEL=gpt-6-astra
|
TWITTER_LITE_CODEX_MODEL=gpt-6-astra
|
||||||
TWITTER_LITE_REPORT_ROOT=/absolute/path/to/twitter-lite/.data/research
|
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
|
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
|
the right. Messages continue the same Codex thread and include the currently
|
||||||
open deck as context. The account selector controls which connections Codex can
|
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.
|
available. The backend handles tools even when the browser is closed.
|
||||||
Use **New chat** to leave the current conversation and start a fresh Codex
|
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.
|
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
|
A running turn continues in the background when you open another chat.
|
||||||
open devices. **Chat history** reopens saved conversations with their account
|
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.
|
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
|
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.
|
discovery and post retrieval (at most 20 posts per fetch), and 15 minutes.
|
||||||
The prototype accepts up to 100 messages per backend process.
|
The prototype accepts up to 100 messages per backend process.
|
||||||
SSE pushes conversation messages, tool activity and generated deck updates to
|
SSE pushes messages, tool activity and deck updates for the requested conversation
|
||||||
open browsers, without polling.
|
ID, without polling. Separate conversations have independent executions.
|
||||||
Codex replies render as Markdown, including tables, lists and code. Clicking a
|
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
|
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
|
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.
|
the app DB stores the conversation, thread ID, deck context and citation metadata.
|
||||||
|
|
||||||
Browser disconnection does not cancel research; reconnecting restores the latest
|
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;
|
rest of the app. After a backend restart, unfinished turns appear as interrupted;
|
||||||
sending another message resumes the same thread. Before starting that message,
|
sending another message explicitly resumes the same thread through the provider.
|
||||||
the backend checks the resumed thread and interrupts any orphaned active turn.
|
|
||||||
It does not automatically rerun interrupted requests. Files already written remain.
|
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
|
run under a user able to write those files. NixOS DynamicUser service integration
|
||||||
and dedicated service credentials are outside this prototype.
|
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
|
Shared UI components live in `src/components/ui`; `components.json` configures
|
||||||
the registry and theme entrypoint. Chat uses MessageScroller, Message, Bubble,
|
the registry and theme entrypoint. Chat uses MessageScroller, Message, Bubble,
|
||||||
Marker and InputGroup while retaining the existing Codex transport and saved
|
Marker and InputGroup with Streamdown, reconnectable snapshot SSE, and saved
|
||||||
conversation model. The icon rail switches between chat and deck management;
|
conversations. The icon rail switches between chat and deck management;
|
||||||
closing the mobile sheet or crossing the desktop breakpoint preserves chat
|
closing the mobile sheet or crossing the desktop breakpoint preserves chat
|
||||||
drafts, selected accounts and live deck updates. Theme colors are declared in
|
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
|
`pnpm lint` runs Oxlint with type-aware linting and TypeScript checking through
|
||||||
[@shadcn/lint](https://github.com/shadcn-ui/lint) through Oxlint. The
|
`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
|
`shadcn/no-raw-colors` rule in `.oxlintrc.json` requires theme colors in JSX and
|
||||||
class helpers, including shared UI components. Layout values remain available
|
class helpers, including shared UI components. Layout values remain available
|
||||||
for the sidebar and deck. Biome continues to handle formatting and general
|
for the sidebar and deck. Component contracts allow only the existing workspace
|
||||||
code checks. Knip checks application usage while retaining the public exports
|
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
|
of registry components for composition. Additional design rules can be configured
|
||||||
in `.oxlintrc.json`;
|
in `.oxlintrc.json`;
|
||||||
see the [available rules](https://github.com/shadcn-ui/lint#rules).
|
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.
|
Create a deck for an investigation, then add columns for each perspective.
|
||||||
Choose a connected account and one of its supported sources. Account
|
Choose a connected account and one of its supported sources. Account
|
||||||
changes affect only the edited column. Matching source conditions and accounts
|
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,
|
"tsx": true,
|
||||||
"tailwind": {
|
"tailwind": {
|
||||||
"config": "",
|
"config": "",
|
||||||
"css": "src/ui.css",
|
"css": "src/styles.css",
|
||||||
"baseColor": "neutral",
|
"baseColor": "neutral",
|
||||||
"cssVariables": true
|
"cssVariables": true
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ flowchart LR
|
|||||||
SQLiteを採用する案。端末が増えても、各端末がSQLを実行するのではなく、同じアプリサーバーへアクセスする。デッキ設定とOAuth情報の小さな更新には単一サーバーのSQLiteで始められると判断する。DBファイルを端末間コピーしたり、NAS上のファイルを複数サーバーから直接開いたりしない。
|
SQLiteを採用する案。端末が増えても、各端末がSQLを実行するのではなく、同じアプリサーバーへアクセスする。デッキ設定とOAuth情報の小さな更新には単一サーバーのSQLiteで始められると判断する。DBファイルを端末間コピーしたり、NAS上のファイルを複数サーバーから直接開いたりしない。
|
||||||
|
|
||||||
| 候補 | 今回の評価 |
|
| 候補 | 今回の評価 |
|
||||||
| --- | --- |
|
| -------------------- | ------------------------------------------------------------------------------------------------------------ |
|
||||||
| SQLite | 推奨。別DBサービス不要。ローカルディスク、短いトランザクション、マイグレーション、復元試験を用意する |
|
| SQLite | 推奨。別DBサービス不要。ローカルディスク、短いトランザクション、マイグレーション、復元試験を用意する |
|
||||||
| PostgreSQL | 複数アプリサーバーや大量の並列収集へ進む時に再評価。現時点では運用対象が増える |
|
| PostgreSQL | 複数アプリサーバーや大量の並列収集へ進む時に再評価。現時点では運用対象が増える |
|
||||||
| ブラウザDB+同期基盤 | 採用しない。今回必要なのはオンラインで同じサーバー状態を読むこと。オフライン同期エンジンを導入する必要はない |
|
| ブラウザDB+同期基盤 | 採用しない。今回必要なのはオンラインで同じサーバー状態を読むこと。オフライン同期エンジンを導入する必要はない |
|
||||||
@@ -53,7 +53,7 @@ OAuth callbackは固定のHTTPS URLにする。認可後に戻る主体はブラ
|
|||||||
## 保存モデル案
|
## 保存モデル案
|
||||||
|
|
||||||
| 保存対象 | 主な内容 |
|
| 保存対象 | 主な内容 |
|
||||||
| --- | --- |
|
| ------------------------ | ---------------------------------------------------------------------------------------- |
|
||||||
| `decks` | ID、名前、revision、更新日時 |
|
| `decks` | ID、名前、revision、更新日時 |
|
||||||
| `deck_columns` | ID、deck ID、並び順、connection ID、名前、platform固有のsource JSON |
|
| `deck_columns` | ID、deck ID、並び順、connection ID、名前、platform固有のsource JSON |
|
||||||
| `connections` | ID、platform、接続先origin、外部アカウントIDまたはrelay profile参照、表示名、接続状態 |
|
| `connections` | ID、platform、接続先origin、外部アカウントIDまたはrelay profile参照、表示名、接続状態 |
|
||||||
@@ -70,7 +70,7 @@ DBには秘密を暗号化して保存し、暗号鍵はDB外の実行時credent
|
|||||||
## PR単位のタスク
|
## PR単位のタスク
|
||||||
|
|
||||||
| ID | タスク | 依存 | 完了条件 |
|
| ID | タスク | 依存 | 完了条件 |
|
||||||
| --- | --- | --- | --- |
|
| --- | -------------------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| T0 | 実行環境とアクセス経路を固定 | なし | 本番HTTPS origin/callback候補を決め、PC・スマホから同じ利用者として接続。Serve以外からのヘッダー偽装を許さない構成を確認。対象Mastodonのバージョン・OAuthメタデータも調べる |
|
| T0 | 実行環境とアクセス経路を固定 | なし | 本番HTTPS origin/callback候補を決め、PC・スマホから同じ利用者として接続。Serve以外からのヘッダー偽装を許さない構成を確認。対象Mastodonのバージョン・OAuthメタデータも調べる |
|
||||||
| T1 | SQLiteと永続ディレクトリを導入 | T0 | 現行Node/Nixで動くdriver・migration方式を実証。`StateDirectory`等でDBを永続化。再起動・アプリ更新後も残り、バックアップから復元できる。依存追加時はflakeのpnpm hash更新まで行う |
|
| T1 | SQLiteと永続ディレクトリを導入 | T0 | 現行Node/Nixで動くdriver・migration方式を実証。`StateDirectory`等でDBを永続化。再起動・アプリ更新後も残り、バックアップから復元できる。依存追加時はflakeのpnpm hash更新まで行う |
|
||||||
| T2 | connectionモデルへTwitterを移行 | T1 | relay profile一覧から接続を作り、カラム・取得・キャッシュ・ページ送りがconnection IDを使う。2つのTwitter接続を混ぜずに並列表示。不明・削除済みの接続はエラーとして残す |
|
| T2 | connectionモデルへTwitterを移行 | T1 | relay profile一覧から接続を作り、カラム・取得・キャッシュ・ページ送りがconnection IDを使う。2つのTwitter接続を混ぜずに並列表示。不明・削除済みの接続はエラーとして残す |
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
# shadcn workspace migration
|
# 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
|
## Target
|
||||||
|
|
||||||
Replace the custom workspace UI with the Base UI version of shadcn sidebar-09
|
Replace the custom workspace UI with the Base UI version of shadcn sidebar-09
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ tailnet HTTPS address after Mastodon authorization.
|
|||||||
## Runtime configuration
|
## Runtime configuration
|
||||||
|
|
||||||
| Variable | Value |
|
| Variable | Value |
|
||||||
| --- | --- |
|
| ---------------------------------- | --------------------------------------------------------- |
|
||||||
| `TWITTER_LITE_ORIGIN` | Exact Serve HTTPS origin, without a trailing slash |
|
| `TWITTER_LITE_ORIGIN` | Exact Serve HTTPS origin, without a trailing slash |
|
||||||
| `TWITTER_LITE_ALLOWED_LOGIN` | Owner's Tailscale login |
|
| `TWITTER_LITE_ALLOWED_LOGIN` | Owner's Tailscale login |
|
||||||
| `TWITTER_LITE_DB_PATH` | Absolute path to the SQLite database on local disk |
|
| `TWITTER_LITE_DB_PATH` | Absolute path to the SQLite database on local disk |
|
||||||
|
|||||||
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
|
```ts
|
||||||
type InitialThreadInput = {
|
type InitialThreadInput = {
|
||||||
tweetId: string
|
tweetId: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
type ContinuedThreadInput = {
|
type ContinuedThreadInput = {
|
||||||
tweetId: string
|
tweetId: string;
|
||||||
conversationId: string
|
conversationId: string;
|
||||||
cursor: string
|
cursor: string;
|
||||||
}
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
All IDs must be decimal strings. Cursor values are opaque non-empty strings.
|
All IDs must be decimal strings. Cursor values are opaque non-empty strings.
|
||||||
@@ -106,11 +106,11 @@ The successful page shape is:
|
|||||||
|
|
||||||
```ts
|
```ts
|
||||||
type ThreadPage = {
|
type ThreadPage = {
|
||||||
tweets: Post[]
|
tweets: Post[];
|
||||||
focalPost?: Post
|
focalPost?: Post;
|
||||||
conversationId: string
|
conversationId: string;
|
||||||
nextCursor?: string
|
nextCursor?: string;
|
||||||
}
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
`focalPost` is present only on the initial page. All returned posts pass through
|
`focalPost` is present only on the initial page. All returned posts pass through
|
||||||
@@ -149,7 +149,10 @@ continuation requests.
|
|||||||
Extend the existing feed request union with:
|
Extend the existing feed request union with:
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
{ kind: 'thread'; tweetId: string }
|
{
|
||||||
|
kind: "thread";
|
||||||
|
tweetId: string;
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
The thread query key includes the focal tweet ID. Its initial page parameter is
|
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:
|
Bird currently hard-codes `product: 'Latest'` in SearchTimeline requests. Add the following public contract:
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
export type SearchProduct = 'Top' | 'Latest'
|
export type SearchProduct = "Top" | "Latest";
|
||||||
|
|
||||||
export interface SearchFetchOptions {
|
export interface SearchFetchOptions {
|
||||||
includeRaw?: boolean
|
includeRaw?: boolean;
|
||||||
product?: SearchProduct
|
product?: SearchProduct;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SearchPaginationOptions extends SearchFetchOptions {
|
export interface SearchPaginationOptions extends SearchFetchOptions {
|
||||||
maxPages?: number
|
maxPages?: number;
|
||||||
cursor?: string
|
cursor?: string;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -126,7 +126,7 @@ new TwitterClient({
|
|||||||
relayBaseUrl: process.env.TWITTER_RELAY_BASE_URL,
|
relayBaseUrl: process.env.TWITTER_RELAY_BASE_URL,
|
||||||
profileName: process.env.BIRD_PROFILE_NAME,
|
profileName: process.env.BIRD_PROFILE_NAME,
|
||||||
timeoutMs: 20_000,
|
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.
|
`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({
|
export default defineConfig({
|
||||||
dialect: 'sqlite',
|
dialect: "sqlite",
|
||||||
schema: './src/features/storage/schema.ts',
|
schema: "./src/features/storage/schema.ts",
|
||||||
out: './drizzle',
|
out: "./drizzle",
|
||||||
})
|
});
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
inherit (finalAttrs) pname version src;
|
inherit (finalAttrs) pname version src;
|
||||||
pnpm = pkgs.pnpm_11;
|
pnpm = pkgs.pnpm_11;
|
||||||
fetcherVersion = 4;
|
fetcherVersion = 4;
|
||||||
hash = "sha256-+DqNt+58TrI0W8SU//JNdYYJtCdKNIG7gfWRxno36QQ=";
|
hash = "sha256-60nQAoDWoO15P7TFLhABqGNYpJy/R+w+Zz3oo7qD8dM=";
|
||||||
};
|
};
|
||||||
|
|
||||||
nativeBuildInputs = with pkgs; [
|
nativeBuildInputs = with pkgs; [
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { KnipConfig } from 'knip'
|
import type { KnipConfig } from "knip";
|
||||||
|
|
||||||
export default {
|
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,
|
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([
|
const selectedOperationIds = new Set([
|
||||||
'getUserByScreenName',
|
"getUserByScreenName",
|
||||||
'getUserTweets',
|
"getUserTweets",
|
||||||
'getTweetDetail',
|
"getTweetDetail",
|
||||||
'getSearchTimeline',
|
"getSearchTimeline",
|
||||||
'getListLatestTweetsTimeline',
|
"getListLatestTweetsTimeline",
|
||||||
])
|
]);
|
||||||
|
|
||||||
const selectE2eOperations = defineTransformer((document) => {
|
const selectE2eOperations = defineTransformer((document) => {
|
||||||
const foundOperationIds = new Set<string>()
|
const foundOperationIds = new Set<string>();
|
||||||
|
|
||||||
for (const pathItem of Object.values(document.paths ?? {})) {
|
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)) {
|
for (const candidate of Object.values(pathItem)) {
|
||||||
if (
|
if (
|
||||||
!candidate ||
|
!candidate ||
|
||||||
typeof candidate !== 'object' ||
|
typeof candidate !== "object" ||
|
||||||
Array.isArray(candidate) ||
|
Array.isArray(candidate) ||
|
||||||
!('operationId' in candidate)
|
!("operationId" in candidate)
|
||||||
) {
|
) {
|
||||||
continue
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const operation = candidate as {
|
const operation = candidate as {
|
||||||
operationId?: string
|
operationId?: string;
|
||||||
tags?: string[]
|
tags?: string[];
|
||||||
}
|
};
|
||||||
|
|
||||||
if (
|
if (operation.operationId && selectedOperationIds.has(operation.operationId)) {
|
||||||
operation.operationId &&
|
foundOperationIds.add(operation.operationId);
|
||||||
selectedOperationIds.has(operation.operationId)
|
operation.tags = [...(operation.tags ?? []), selectedTag];
|
||||||
) {
|
|
||||||
foundOperationIds.add(operation.operationId)
|
|
||||||
operation.tags = [...(operation.tags ?? []), selectedTag]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const missingOperationIds = [...selectedOperationIds].filter(
|
const missingOperationIds = [...selectedOperationIds].filter(
|
||||||
(operationId) => !foundOperationIds.has(operationId),
|
(operationId) => !foundOperationIds.has(operationId),
|
||||||
)
|
);
|
||||||
if (missingOperationIds.length > 0) {
|
if (missingOperationIds.length > 0) {
|
||||||
throw new Error(
|
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({
|
export default defineConfig({
|
||||||
twitterOpenApi: {
|
twitterOpenApi: {
|
||||||
input: {
|
input: {
|
||||||
target:
|
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: {
|
filters: {
|
||||||
mode: 'include',
|
mode: "include",
|
||||||
tags: [selectedTag],
|
tags: [selectedTag],
|
||||||
},
|
},
|
||||||
override: {
|
override: {
|
||||||
@@ -67,9 +64,9 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
output: {
|
output: {
|
||||||
clean: true,
|
clean: true,
|
||||||
client: 'fetch',
|
client: "fetch",
|
||||||
mode: 'single',
|
mode: "single",
|
||||||
target: './e2e/generated/twitter-openapi/client.ts',
|
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) {
|
if (!readAuthState().needsSetup) {
|
||||||
console.error(
|
console.error("This workspace already has an owner. Sign in with your existing account.");
|
||||||
'This workspace already has an owner. Sign in with your existing account.',
|
process.exitCode = 1;
|
||||||
)
|
|
||||||
process.exitCode = 1
|
|
||||||
} else {
|
} else {
|
||||||
console.log(
|
console.log("Open /setup in your workspace and enter this one-time setup code:");
|
||||||
'Open /setup in your workspace and enter this one-time setup code:',
|
console.log(getSetupToken());
|
||||||
)
|
|
||||||
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 source = process.env.TWITTER_LITE_DB_PATH;
|
||||||
const destination = process.argv[2]
|
const destination = process.argv[2];
|
||||||
if (!source || !destination || process.argv.length !== 3) {
|
if (!source || !destination || process.argv.length !== 3) {
|
||||||
console.error(
|
console.error("Usage: TWITTER_LITE_DB_PATH=/absolute/app.db pnpm db:backup /absolute/backup.db");
|
||||||
'Usage: TWITTER_LITE_DB_PATH=/absolute/app.db pnpm db:backup /absolute/backup.db',
|
process.exitCode = 1;
|
||||||
)
|
|
||||||
process.exitCode = 1
|
|
||||||
} else {
|
} else {
|
||||||
await backupDatabase(source, destination)
|
await backupDatabase(source, destination);
|
||||||
console.log(`Backup verified: ${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({
|
await build({
|
||||||
configFile: false,
|
configFile: false,
|
||||||
build: {
|
build: {
|
||||||
ssr: `scripts/${entry}.ts`,
|
ssr: `scripts/${entry}.ts`,
|
||||||
outDir: '.output/server/tools',
|
outDir: ".output/server/tools",
|
||||||
emptyOutDir: false,
|
emptyOutDir: false,
|
||||||
rollupOptions: { external: ['better-sqlite3'] },
|
rollupOptions: { external: ["better-sqlite3"] },
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { writeFileSync } from 'node:fs'
|
import { writeFileSync } from "node:fs";
|
||||||
import { readMigrationFiles } from 'drizzle-orm/migrator'
|
import { readMigrationFiles } from "drizzle-orm/migrator";
|
||||||
|
|
||||||
const migrations = readMigrationFiles({
|
const migrations = readMigrationFiles({
|
||||||
migrationsFolder: new URL('../drizzle', import.meta.url).pathname,
|
migrationsFolder: new URL("../drizzle", import.meta.url).pathname,
|
||||||
})
|
});
|
||||||
writeFileSync(
|
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`,
|
`// 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 { tanstackStart } from "@tanstack/react-start/plugin/vite";
|
||||||
import { createServer } from 'vite'
|
import { createServer } from "vite";
|
||||||
|
|
||||||
const server = await createServer({
|
const server = await createServer({
|
||||||
configFile: false,
|
configFile: false,
|
||||||
plugins: [tanstackStart()],
|
plugins: [tanstackStart()],
|
||||||
server: { middlewareMode: true },
|
server: { middlewareMode: true },
|
||||||
})
|
});
|
||||||
await server.close()
|
await server.close();
|
||||||
|
|||||||
+12
-17
@@ -1,28 +1,23 @@
|
|||||||
// @vitest-environment node
|
// @vitest-environment node
|
||||||
|
|
||||||
import { TwitterClient } from '@yuta/bird'
|
import { TwitterClient } from "@yuta/bird";
|
||||||
import { describe, expect, it } from 'vitest'
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
describe.runIf(process.env.TWITTER_LITE_LIVE === '1')(
|
describe.runIf(process.env.TWITTER_LITE_LIVE === "1")("configured relay", () => {
|
||||||
'configured relay',
|
it("performs one read-only Top search", async () => {
|
||||||
() => {
|
const relayBaseUrl = process.env.TWITTER_RELAY_BASE_URL;
|
||||||
it('performs one read-only Top search', async () => {
|
|
||||||
const relayBaseUrl = process.env.TWITTER_RELAY_BASE_URL
|
|
||||||
if (!relayBaseUrl) {
|
if (!relayBaseUrl) {
|
||||||
throw new Error(
|
throw new Error("TWITTER_RELAY_BASE_URL is required for the opt-in live smoke test");
|
||||||
'TWITTER_RELAY_BASE_URL is required for the opt-in live smoke test',
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const client = new TwitterClient({
|
const client = new TwitterClient({
|
||||||
relayBaseUrl,
|
relayBaseUrl,
|
||||||
profileName: process.env.BIRD_PROFILE_NAME,
|
profileName: process.env.BIRD_PROFILE_NAME,
|
||||||
timeoutMs: 20_000,
|
timeoutMs: 20_000,
|
||||||
})
|
});
|
||||||
const query = process.env.TWITTER_LITE_LIVE_QUERY ?? 'OpenAI'
|
const query = process.env.TWITTER_LITE_LIVE_QUERY ?? "OpenAI";
|
||||||
const result = await client.search(query, 1, { product: 'Top' })
|
const result = await client.search(query, 1, { product: "Top" });
|
||||||
|
|
||||||
expect(result.success).toBe(true)
|
expect(result.success).toBe(true);
|
||||||
}, 30_000)
|
}, 30_000);
|
||||||
},
|
});
|
||||||
)
|
|
||||||
|
|||||||
+4
-4
@@ -1,5 +1,5 @@
|
|||||||
import '@testing-library/jest-dom/vitest'
|
import "@testing-library/jest-dom/vitest";
|
||||||
import { cleanup } from '@testing-library/react'
|
import { cleanup } from "@testing-library/react";
|
||||||
import { afterEach } from 'vitest'
|
import { afterEach } from "vitest";
|
||||||
|
|
||||||
afterEach(cleanup)
|
afterEach(cleanup);
|
||||||
|
|||||||
+1
-2
@@ -19,8 +19,7 @@
|
|||||||
--color-shadow: oklch(10% 0.015 250 / 45%);
|
--color-shadow: oklch(10% 0.015 250 / 45%);
|
||||||
--color-overlay: oklch(10% 0.015 250 / 70%);
|
--color-overlay: oklch(10% 0.015 250 / 70%);
|
||||||
--font-body:
|
--font-body:
|
||||||
-apple-system, BlinkMacSystemFont, "Segoe UI", "Hiragino Sans",
|
-apple-system, BlinkMacSystemFont, "Segoe UI", "Hiragino Sans", "Noto Sans JP", sans-serif;
|
||||||
"Noto Sans JP", sans-serif;
|
|
||||||
--font-display: var(--font-body);
|
--font-display: var(--font-body);
|
||||||
--font-mono: ui-monospace, "SFMono-Regular", Consolas, monospace;
|
--font-mono: ui-monospace, "SFMono-Regular", Consolas, monospace;
|
||||||
--space-3xs: 0.125rem;
|
--space-3xs: 0.125rem;
|
||||||
|
|||||||
+6
-6
@@ -1,9 +1,9 @@
|
|||||||
import tailwindcss from '@tailwindcss/vite'
|
import tailwindcss from "@tailwindcss/vite";
|
||||||
import { tanstackStart } from '@tanstack/react-start/plugin/vite'
|
import { tanstackStart } from "@tanstack/react-start/plugin/vite";
|
||||||
import viteReact from '@vitejs/plugin-react'
|
import viteReact from "@vitejs/plugin-react";
|
||||||
import { nitro } from 'nitro/vite'
|
import { nitro } from "nitro/vite";
|
||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from "vite";
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [tailwindcss(), nitro(), tanstackStart(), viteReact()],
|
plugins: [tailwindcss(), nitro(), tanstackStart(), viteReact()],
|
||||||
})
|
});
|
||||||
|
|||||||
+7
-7
@@ -1,11 +1,11 @@
|
|||||||
import path from 'node:path'
|
import path from "node:path";
|
||||||
import { configDefaults, defineConfig } from 'vitest/config'
|
import { configDefaults, defineConfig } from "vitest/config";
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
resolve: { alias: { '#': path.resolve(import.meta.dirname, 'src') } },
|
resolve: { alias: { "#": path.resolve(import.meta.dirname, "src") } },
|
||||||
test: {
|
test: {
|
||||||
environment: 'jsdom',
|
environment: "jsdom",
|
||||||
setupFiles: ['./tests/setup.ts'],
|
setupFiles: ["./tests/setup.ts"],
|
||||||
exclude: [...configDefaults.exclude, 'e2e/**'],
|
exclude: [...configDefaults.exclude, "e2e/**"],
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user