aboutsummaryrefslogtreecommitdiff
path: root/PLAN.md
diff options
context:
space:
mode:
Diffstat (limited to 'PLAN.md')
-rw-r--r--PLAN.md138
1 files changed, 138 insertions, 0 deletions
diff --git a/PLAN.md b/PLAN.md
new file mode 100644
index 0000000..25f806e
--- /dev/null
+++ b/PLAN.md
@@ -0,0 +1,138 @@
+# PLAN.md — @eol/graphiql
+
+A Svelte 5 GraphiQL alternative. JSR-published under the EOL scope.
+
+## Current state (v0.2 complete)
+
+Shipped:
+
+- HTTP fetcher with injectable `fetch` impl
+- Schema introspection and SDL caching
+- Session store with tabs, persistence, and rename
+- TabBar with double-click inline rename (Enter commits, Escape cancels, blur commits)
+- Toolbar with Cmd/Ctrl+Enter run shortcut
+- Variables + Headers panes (switchable in bottom-left)
+- Debounced localStorage persistence (300ms) with `beforeunload` flush
+- Memory storage fallback for SSR
+- CodeMirror 6 editor wrapper via `cm6-graphql` with dynamic import (SSR-safe)
+- `readOnly` prop on Editor, used by ResultViewer
+- Reactive schema updates via `$effect` calling `updateSchema()`
+- Public API surface in `src/lib/index.ts`
+
+Stub files needing implementation: `src/lib/fetcher/sse.ts`, `src/lib/fetcher/websocket.ts`.
+
+## Coding conventions
+
+Non-negotiable. Match existing files exactly:
+
+- Double quotes, semicolons, two-space indent
+- No trailing spaces, no unnecessary whitespace
+- Svelte component structure: `<script lang="ts">`, then `<style lang="scss">`, then markup
+- Object literal keys alphabetical unless semantic ordering matters
+- TypeScript strict mode, no `any`
+- Svelte 5 runes only (`$state`, `$derived`, `$effect`, `$props`) — no stores, no `writable`
+- No direct `window` or `document` access — use `$app/environment`'s `browser`, or `typeof globalThis.X !== "undefined"` guards
+- No React, no Node-specific APIs (Deno/Bun first)
+- Fix mistakes in place; don't re-scaffold
+
+## Stages
+
+### v0.2.1 — Polish (small, worth doing before v0.3)
+
+**Tab auto-titling.** When typing in a brand-new tab, auto-generate a title from the first meaningful token of the query (operation name if present, else "query"/"mutation"/"subscription", else first 20 chars). Only auto-rename if the user hasn't manually renamed. Track with a `titleDirty: boolean` field on `Tab`. Set it to `true` in `renameTab()` and `renameActive()`.
+
+**Operation detection.** Parse the query with `graphql`'s `parse()` and set `tab.operationName` automatically when there's a single named operation. If multiple operations exist and none selected, surface a picker next to the Run button. Keep parse errors silent — the CM6 linter already shows them.
+
+**Theme prop.** Currently hardcoded to `oneDark`. Extract to an optional `theme?: Extension` prop on the root component, defaulting to `oneDark`. Consumers can pass any CM6 theme extension. A light theme preset (`import("@codemirror/theme")` basics or a small hand-rolled one) should ship alongside so people can toggle without adding a dependency.
+
+### v0.3 — Doc explorer and history
+
+**Doc explorer (`components/DocExplorer.svelte`).** Right-hand collapsible pane. Walks `schema.schema` (the built `GraphQLSchema` object). Breadcrumb navigation: Root → Type → Field → Arg. Clicking a field pushes onto a navigation stack. Clicking a breadcrumb crumb pops to that level.
+
+Structure:
+
+- `DocExplorer.svelte` — container, manages nav stack via `$state`
+- `DocExplorer/TypeView.svelte` — renders a `GraphQLObjectType`, `GraphQLInterfaceType`, `GraphQLUnionType`, `GraphQLEnumType`, `GraphQLInputObjectType`, or `GraphQLScalarType`
+- `DocExplorer/FieldView.svelte` — renders a single field with args and return type
+- `DocExplorer/TypeLink.svelte` — clickable type reference (unwraps `NonNull`/`List`)
+
+Use the `graphql` package's `isObjectType`, `isInterfaceType`, etc. guards — don't duck-type.
+
+Toggle visibility with a toolbar button. Width around 320px. Collapsed state persisted via the same storage adapter under a `"docExplorer"` key.
+
+**History panel (`components/HistoryPanel.svelte`).** Drawer from the left. Logs every successful query execution (query + variables + headers + timestamp + operationName). New history store: `state/history.svelte.ts` with `HistoryStore` class, methods `add(entry)`, `remove(id)`, `clear()`, `favorite(id)`. Cap at 100 entries; favorites never evict. Clicking an entry loads it into the active tab (overwriting). Shift-click opens in a new tab.
+
+Persist under `"history"` key. Same debounce pattern as session.
+
+### v0.4 — Subscriptions (SSE + WebSocket)
+
+Replace the stubs in `fetcher/sse.ts` and `fetcher/websocket.ts`.
+
+**SSE implementation.** Use `graphql-sse` protocol. The fetcher signature already allows `AsyncIterable<FetcherResult>` as a return type — use it. Stream each `next` event as an iteration. Close the `EventSource` on `complete` or consumer abort.
+
+**WebSocket implementation.** Use the `graphql-ws` library (JSR: `npm:graphql-ws`). Return an `AsyncIterable` that yields each `next` message. Handle `connection_init`, `ping`/`pong`, subscription IDs, and clean teardown.
+
+**Root component update.** `GraphiQL.svelte`'s `run()` needs to detect `AsyncIterable` return values and stream them into `tab.result`. Append each payload with a timestamp header, or replace on each — make it a prop choice (`subscriptionMode: "replace" | "append"`, default `"append"`).
+
+**Protocol detection.** Add a prop to pick transport per-operation: if the query contains a subscription, prefer WS; otherwise HTTP. Let consumers override by passing a custom fetcher that dispatches internally.
+
+### v0.5 — Theming and plugin slots
+
+**Theming.** Two layers: the editor theme (a CM6 `Extension` passed as a prop per v0.2.1) and the surrounding chrome theme (CSS custom properties like `--graphiql-bg`, `--graphiql-fg`). Extract the chrome variables into `src/lib/styles/theme.scss` with light/dark mode variables gated by `@media (prefers-color-scheme)` and an override class. Ship at least two CM6 theme presets (dark via `oneDark`, light via a small hand-rolled `EditorView.theme({...})`). Document the full variable list and theme prop API in README.
+
+**Plugin slots.** Use Svelte snippets (the `{@render}` / `Snippet` type from Svelte 5). Add optional props to `GraphiQL.svelte`:
+
+- `toolbarExtras?: Snippet` — rendered after the Run button
+- `resultFooter?: Snippet<[{ result: string }]>` — rendered below the result pane
+- `tabExtras?: Snippet<[{ tab: Tab }]>` — rendered inside each tab button
+
+This keeps the plugin story type-safe without a bespoke registry.
+
+### v0.6+ — Nice to have
+
+- Export/import session state as JSON (for sharing)
+- Query formatting button (uses `graphql`'s `print()`)
+- Keyboard shortcut for new tab (Cmd+T), close tab (Cmd+W), next/prev tab (Cmd+Opt+Right/Left)
+- Split-pane resize handles (use CSS `resize: horizontal` or a lightweight splitter)
+- Request/response timing display
+- Persisted query support (APQ)
+
+## Publishing
+
+The project publishes to JSR as `@eol/graphiql`. Current `jsr.json` and `deno.json` are set up for v0.2.0. Bump versions with ChronVer per NetOpWibby's convention. The `exports` points at `src/lib/index.ts`; JSR ships `.svelte` files as-is and the consumer's SvelteKit build compiles them. **Do not pre-compile Svelte components** — that breaks SSR and prevents consumers from using their own Svelte version.
+
+Run `deno publish --dry-run` before tagging.
+
+## Known gotchas
+
+**CM6 extension composition.** CodeMirror 6 is extension-based — every feature is an opt-in `Extension`. The current `Editor.svelte` bundles a reasonable default set (line numbers, history, bracket matching, close brackets, indent-on-input, default + history keymaps, syntax highlighting, oneDark theme). If consumers want a leaner editor (e.g., for the result viewer), the cleanest path is to make the extension array a prop. Not done yet — v0.2.1 `theme` prop is the entry point for this refactor.
+
+**Schema reactivity.** Schema changes are handled by an `$effect` that calls `cm6-graphql`'s `updateSchema(view, newSchema)`. This is the documented CM6 pattern — do not recreate the editor on schema change, as that loses cursor position, history, and undo stack. If `schema` is invalid SDL, the catch block silently skips and the editor keeps working without schema awareness.
+
+**`crypto.randomUUID()`** works in Deno, modern browsers, and Bun. It does not work in Node.js before 14.17. Since we're Deno/Bun first this is fine; flag in docs if someone reports Node issues.
+
+**Nested button in TabBar.** The close `×` button is nested inside the tab `<button>`. Technically invalid HTML but works everywhere. If a11y linters complain, refactor the outer `<button>` to `<div role="tab" tabindex="0">` with explicit keyboard handlers.
+
+**Schema introspection failures.** Some GraphQL servers disable introspection in production. Surface the error clearly (already done in `.status` bar) but make sure queries still run without schema — the CM6 editor falls back to plain GraphQL syntax highlighting when `sdl` is empty.
+
+## Testing
+
+No test harness yet. For v0.3 add Deno's built-in test runner targeting the state stores (pure logic, no DOM):
+
+- `tests/session.test.ts` — tab add/close/rename/persist round-trip
+- `tests/storage.test.ts` — memory storage + namespace isolation
+- `tests/history.test.ts` — eviction and favorite pinning
+
+Component tests can wait until there's demand; Svelte 5 + runes + Vitest is still settling.
+
+## Architecture notes for future me
+
+The separation between **state stores** (`*.svelte.ts` classes) and **components** is deliberate. Stores own all mutation logic and can be unit tested without a DOM. Components own rendering and side effects (persistence timers, keyboard handlers).
+
+The `$effect` in `GraphiQL.svelte` intentionally reads `session.tabs` and `session.activeId` via `void` to establish dependencies without using the values. This is idiomatic runes and cleaner than `$effect.root` with explicit subscriptions.
+
+The fetcher abstraction is intentionally minimal — one function type that returns a Promise or AsyncIterable. This covers HTTP, SSE, WS, and any future transport without an interface explosion. Transport-specific options live in the factory function, not the fetcher itself.
+
+**Why CodeMirror 6 over Monaco.** Bundle size, build simplicity, and philosophical fit. Monaco is multi-megabyte with required web worker setup in Vite — a real friction point for consumers. CM6 is tens of kilobytes, composed of opt-in extensions, and works with zero build config. `cm6-graphql` is maintained inside the graphql/graphiql monorepo itself and uses the same `graphql-language-service` that powers `monaco-graphql`, so we get schema-aware autocomplete and linting without the bloat. The tradeoff is slightly less polished hover docs and no signature-help popups — acceptable for v1, addressable later via custom CM6 extensions if needed.
+
+When in doubt about API shape, look at `@graphiql/toolkit` for reference — the GraphiQL maintainers have already factored out the transport concerns nicely, we're just giving it a Svelte + CM6 skin.