diff --git a/web/README.md b/web/README.md index d8b03e7..3775b25 100644 --- a/web/README.md +++ b/web/README.md @@ -17,6 +17,10 @@ extracted into its own repository later without rewrites. - `/` — landing (hero with animated REPL, features, architecture, roadmap, SDK switcher, SQL surface, desktop showcase, blog series, footer) - `/docs` — Getting Started page (sticky sidebar nav + on-page TOC) +- `/blog` — index of long-form posts pulled from `content/blog/*.mdx` +- `/blog/[slug]` — per-post detail page (MDX rendered server-side, `Article` JSON-LD, breadcrumb JSON-LD, dynamic OG image, prev/next navigation) +- `/blog/tags/[tag]` — tag pages (one per unique frontmatter tag) +- `/blog/rss.xml` — RSS 2.0 feed ## SEO surface @@ -43,8 +47,9 @@ Each public route ships full search/social metadata. The pieces: [`src/app/robots.ts`](src/app/robots.ts)). Add a route to the `ROUTES` list when shipping a new page. - **JSON-LD structured data** — `SoftwareApplication` schema on the landing - page, `BreadcrumbList` on `/docs`. Validate via Google's - [Rich Results Test](https://search.google.com/test/rich-results). + page, `BreadcrumbList` on `/docs`, `Blog` on `/blog`, and + `BlogPosting` + `BreadcrumbList` on each `/blog/`. Validate via + Google's [Rich Results Test](https://search.google.com/test/rich-results). - **Search Console verification** — fill in the placeholder tokens in `metadata.verification` ([`src/app/layout.tsx`](src/app/layout.tsx)) once Google Search Console + Bing Webmaster Tools issue them. @@ -73,19 +78,74 @@ npm run lint # next lint (ESLint) ``` web/ +├── content/ +│ └── blog/ # MDX posts (one .mdx file per post; frontmatter at top) ├── src/ │ ├── app/ │ │ ├── globals.css # design tokens + utility CSS (ports the original design's styles.css) │ │ ├── layout.tsx # root layout, fonts (Inter + JetBrains Mono via next/font) │ │ ├── page.tsx # landing -│ │ └── docs/page.tsx # /docs +│ │ ├── docs/page.tsx # /docs +│ │ ├── blog/ # /blog index, [slug] detail, tags/[tag], rss.xml +│ │ ├── sitemap.ts # /sitemap.xml — enumerates static + per-post + per-tag URLs +│ │ └── robots.ts # /robots.txt │ ├── components/ # one .tsx per landing section (hero, features, roadmap, …) │ └── lib/ +│ ├── blog.ts # MDX loader: frontmatter parsing, post enumeration, tag helpers +│ ├── og.tsx # shared OpenGraph frame │ ├── site.ts # SITE constants (version, repo URL, social links) │ └── utils.ts # shadcn cn() helper └── components.json # shadcn/ui config ``` +## Blog + +The blog is content-driven. Posts live as `.mdx` files in +[`content/blog/`](content/blog) and are rendered server-side via +[`next-mdx-remote`](https://github.com/hashicorp/next-mdx-remote). +Frontmatter is parsed by `gray-matter`. + +### Adding a post + +Create `content/blog/.mdx`: + +```mdx +--- +title: "Your post title" +description: "One-sentence description used in , OG, RSS." +publishedAt: "2026-05-10" # ISO date, sorts the index +updatedAt: "2026-05-12" # optional +author: "Joao Henrique Machado Silva" +tags: ["sqlrite", "rust"] # also drives /blog/tags/[tag] +primaryKeyword: "rust sql engine" # optional, for SEO bookkeeping +--- + +Body text in Markdown / MDX. +``` + +Then: + +- The post is automatically picked up by `/blog`, `/blog/`, + every relevant `/blog/tags/`, the RSS feed, and the sitemap. +- An OG image is generated dynamically from the title + + description at `/blog//opengraph-image`. +- `BlogPosting` JSON-LD and `BreadcrumbList` JSON-LD are injected + on the detail page. + +### Required frontmatter validity + +`src/lib/blog.ts` validates frontmatter at load time and throws if +`title`, `description`, `publishedAt`, `author`, or `tags` is +missing / wrong-typed. The build will fail fast in CI rather than +shipping a half-broken post. + +### MDX caveats + +`<` and `{` in prose can confuse the MDX parser. Wrap them in +backticks or escape (`<`, `\{`). The MDX renderer auto-routes +internal `[link](/foo)` markdown links through `next/link`; external +links open in a new tab via `rel="noreferrer"`. + The design tokens (colors, typography, spacing) live in `globals.css`'s `@theme` block. The page-level CSS (sections, terminal, feature grid, roadmap timeline, etc.) is intentionally hand-rolled — it ports the diff --git a/web/content/blog/adding-vector-search-with-hnsw.mdx b/web/content/blog/adding-vector-search-with-hnsw.mdx new file mode 100644 index 0000000..8035986 --- /dev/null +++ b/web/content/blog/adding-vector-search-with-hnsw.mdx @@ -0,0 +1,235 @@ +--- +title: "Adding vector search to a SQLite-style database with HNSW" +description: "How SQLRite added a VECTOR(N) column type and an HNSW index to an embedded SQL engine — distance functions, the executor shortcut, and the design tradeoffs of putting an ANN graph next to a B-tree." +publishedAt: "2026-04-22" +author: "Joao Henrique Machado Silva" +tags: ["sqlrite", "vector-search", "hnsw", "rust", "rag", "ai"] +primaryKeyword: "embedded vector search HNSW Rust" +--- + +Vector search inside an embedded SQL engine sounds like it should be +weird, and it sort of is. SQL was built for sets and predicates. ANN +search is geometry. The two languages don't quite agree on what a +"row" is, what an "index" does, or what `ORDER BY` should mean when +the order is "closer to this 384-dimensional point." + +But for the AI-shaped year we're in, this is the integration users +actually want. RAG over local notes. Agent memory. Hybrid retrieval +that combines a BM25 score with a cosine distance. Today those use +cases get stitched together with `sqlite-vec` plus FTS5 plus glue +code; it works, but it's "you, the integrator, are responsible." For +[SQLRite](https://github.com/joaoh82/rust_sqlite) Phase 7d, I wanted +the integration to be the engine's job. (For the philosophy behind +that decision, see [the origin-story post](/blog/why-im-building-sqlrite). +For how rows actually get to disk underneath all this, see +[the storage deep-dive](/blog/how-sqlrite-stores-rows-on-disk).) + +This post is a retrospective on how that landed: what the API looks +like, what the index does under the hood, and which tradeoffs were +not obvious until I'd shipped the wrong version of them. + +## The user-facing API + +Here is the whole thing: + +```sql +CREATE TABLE docs ( + id INTEGER PRIMARY KEY, + body TEXT, + embedding VECTOR(384) +); + +CREATE INDEX docs_emb ON docs(embedding) USING HNSW; + +INSERT INTO docs (body, embedding) VALUES + ('rust embedded database', [0.13, -0.02, ...]); + +SELECT id, vec_distance_cosine(embedding, ?) AS dist +FROM docs +ORDER BY dist ASC +LIMIT 10; +``` + +`VECTOR(N)` is a column type. The dimension is fixed per column; rows +that don't conform get rejected with a typed error. Three distance +functions ship: `vec_distance_cosine`, `vec_distance_dot`, +`vec_distance_l2`. There's a single `USING HNSW` index type. That's +the surface. + +Bracket array literals are first-class — `[0.13, -0.02, ...]` is a +vector value, not a string. From Rust: + +```rust +use sqlrite::{Connection, Value}; + +let mut conn = Connection::open("memory.sqlrite")?; +let mut stmt = conn.prepare_cached( + "SELECT id FROM docs ORDER BY vec_distance_cosine(embedding, ?) ASC LIMIT 10", +)?; +let rows = stmt.query_with_params(&[Value::Vector(query_vec)])?.collect_all()?; +``` + +A `Value::Vector(Vec)` binds where a literal would otherwise go, +which means *prepared* k-NN queries still take the index shortcut. +That last bit was its own little design problem; more on it below. + +## The HNSW index + +[HNSW](https://arxiv.org/abs/1603.09320) (Hierarchical Navigable +Small World) is the standard graph-based ANN index. The summary, in +two sentences: every vector is a node in a multi-layer graph where +higher layers are sparser; you start a query at the top, greedily +hop toward the query, and descend layers as you get closer. It's not +the only ANN index in the world — IVF-PQ, ScaNN, DiskANN, and +FAISS-style flat-with-quantization all have their use cases — but +HNSW has two properties that matter for embedded: + +- **Sub-linear search time** without giving up much recall. +- **No training step.** You insert, you search. There is no "first + fit a centroid model on a representative batch." For an embedded + database that doesn't know what the workload looks like before the + user installs it, this is exactly the right tradeoff. + +The downside is build cost. Inserting a vector into HNSW does a +short-radius search to find its neighbors, then wires them together. +That's a O(log n) walk per insert with a fan-out of `M` (the +neighbor cap, default 16). On a million-row dataset, that adds up. +SQLRite's first version of insert was naïve and bench-bound; the +fixed version batches insertions and reuses search state. + +## Where the graph lives + +Two options: in the SQLite-style file format, or in memory rebuilt +on open. + +I tried both. The persistent option is, eventually, the right answer — +but it's a much bigger change than it sounds. HNSW graphs aren't +B-trees; their access pattern is "follow many small pointers across +the graph," which is what virtual memory and `mmap` are good at and +what 4 KiB pages are not. Persisting the graph in 4 KiB pages either +spreads neighbors across many pages (fine for spinning disks, awful +for cache locality) or packs neighbors next to nodes (which means +node insertions can reshape the layout, which means the diff-based +pager writes a lot more pages per commit, which is exactly what we +spent the last phase removing). + +So Phase 7d ships **HNSW in memory, rebuilt on open**. The vectors +themselves still live in the table's B-tree — they're just typed +column values — so durability and crash safety are unchanged. On +`Connection::open`, the engine walks the table once and re-inserts +each vector into a fresh HNSW. For a million 384-dimensional rows, +this takes a few seconds. For the embedded use cases I care about +right now (notes, chat history, codebases at human scale), that's +fine. For bigger tables, it isn't, and persistence is a Phase 9 +problem. + +The thing I want to defend here: **the wrong persistence is worse +than no persistence.** A reader who opens a million-vector database, +waits 800 ms for the graph rebuild, and runs a query that returns in +1.5 ms is having a fine time. A reader who opens the same database, +loads a 200 MB graph file from disk that doesn't quite match the +table any more, and gets a stale answer is *not*. Embedded +databases live or die on the user's trust that what they query is +what they wrote. + +## Wiring it into the executor + +This is where SQL stops being convenient. The query + +```sql +SELECT id, vec_distance_cosine(embedding, ?) AS dist +FROM docs +ORDER BY dist ASC +LIMIT 10; +``` + +…parses as a normal scan-then-sort plan. A naïve executor would +compute `vec_distance_cosine` for every row in `docs`, sort, and +take the top 10. That's O(n) per query. We have an HNSW index sitting +right there. + +The executor recognizes a specific shape — `ORDER BY` a +`vec_distance_*` over the indexed column, `LIMIT k`, no other +side effects — and rewrites it into an HNSW probe with a bounded +top-k heap. If the shape doesn't match (because of a `WHERE` clause +that filters on a non-vector column, say), the query falls back to a +full scan and the optimizer flags it. We also keep the `WHERE` +predicate path correct under the rewrite — the heap accepts a row +only if it satisfies the surviving predicates. This is the +"post-filter" pattern; it's the same shape Pinecone and pgvector +use. + +There's a third path the executor takes: when `LIMIT k` is large +enough relative to `n`, the rewrite isn't worth it (HNSW with +`ef_search` tuned to recall everything stops being sub-linear), and +we just do the full scan. The threshold is currently a constant; it +should eventually live in a per-index PRAGMA. + +## The thing nobody tells you about prepared statements + +A prepared statement looks like a tiny optimization — parse the SQL +once, run the plan many times. For vector search, it is also a +correctness requirement: if `?` binds to a different vector each +time, the plan must still take the HNSW shortcut. + +The naïve implementation parses the placeholder as "an opaque +parameter," and the optimizer can't tell at plan time whether the +parameter is a vector or a string or an integer. So it can't safely +rewrite the plan. So you fall back to the full scan, every time. + +The fix is small but not obvious: the **prepared cache stores plans +keyed by the AST shape with parameter slots typed.** When the AST is +of the form `ORDER BY vec_distance_*(col, ?) LIMIT k` and `col` has +an HNSW index, we install the rewritten plan and remember that the +parameter must bind to a `Value::Vector(_)`. At bind time we +type-check; if the user binds a `Value::Text(_)` instead, we error. +This is the same trick a real query optimizer uses for parameter +sniffing, dressed up in 30 lines of code. + +`prepare_cached` keeps a per-connection LRU plan cache (default cap +16). On a hot RAG path, the cache hit means the SQL never parses +twice and the plan never gets rewritten twice. The cost is exactly +the lookup. + +## A short list of things that surprised me + +- **Distance choice matters.** Cosine, dot product, and L2 are not + interchangeable, and embeddings shipped by different model families + expect different ones. We support all three, but the README needed + a paragraph telling users to pick the one their model recommends. +- **Vectors are huge.** A `VECTOR(1024)` is 4 KiB by itself, which + means it always overflows. That's fine, but it changes the cost + curve of inserts in a table that has *only* vector columns versus + one that mixes vectors with small typed columns. +- **`LIMIT 1` is the canary.** ANN indexes shine when `k` is small. + If your benchmarks all use `LIMIT 100`, you'll think HNSW is + modestly faster than scan. If your benchmarks use `LIMIT 10`, + HNSW dunks on it. RAG retrieval is in the second regime. +- **Recall is a knob, not a constant.** HNSW has an `ef_search` + parameter that trades recall for latency. Phase 7d picked a + conservative default; an upcoming PRAGMA exposes it. + +## Where this leaves us + +Vector search as a first-class feature of the embedded SQL engine, +with prepared statements that take the shortcut, with a graph that +rebuilds on open and otherwise feels invisible, with three distance +functions and one index type. It is the integration I wanted the +engine to provide, and it's the bedrock for the next phase: BM25 +full-text search and **hybrid retrieval**, where `bm25_score` and +`vec_distance_cosine` get to compose in the same query. That's +[Phase 8](https://github.com/joaoh82/rust_sqlite/blob/main/docs/roadmap.md), +in flight now. + +If you want to play with it, there's a runnable example in +[`examples/vector-search/`](https://github.com/joaoh82/rust_sqlite/tree/main/examples) +of the repo, and the [docs page](/docs) covers the +[query patterns](/docs#vector). The MCP server's `vector_search` +tool exposes the same API to AI clients with two lines of config — +the smallest "agent memory" demo I've seen, with no extra moving +parts. + +If SQLRite is useful to you, ⭐ the +[GitHub repo](https://github.com/joaoh82/rust_sqlite) — visibility +is the bottleneck for projects like this, and a single star helps +more than you'd think. diff --git a/web/content/blog/how-sqlrite-stores-rows-on-disk.mdx b/web/content/blog/how-sqlrite-stores-rows-on-disk.mdx new file mode 100644 index 0000000..1b3545f --- /dev/null +++ b/web/content/blog/how-sqlrite-stores-rows-on-disk.mdx @@ -0,0 +1,275 @@ +--- +title: "How SQLRite stores rows on disk: pages, B-trees, and a diff-based pager" +description: "A walkthrough of SQLRite's on-disk format — 4 KiB pages, cell-encoded B-trees per table and index, and a write-ahead log that only persists the pages that actually changed." +publishedAt: "2026-04-15" +author: "Joao Henrique Machado Silva" +tags: ["sqlrite", "rust", "databases", "storage", "b-tree", "wal"] +primaryKeyword: "embedded database B-tree storage in Rust" +--- + +The first time you write a database from scratch, the part you don't +expect to spend the most time on is the file. Not the parser, not the +optimizer, not joins — the *file*. The shape of the bytes on disk +constrains every other decision you make for the rest of the +project's life. Get it wrong early and every later feature is a tax. + +This post is a walkthrough of how +[SQLRite](https://github.com/joaoh82/rust_sqlite) currently stores +rows on disk. It covers the four things that, together, are the whole +storage subsystem: + +1. The **page** — a fixed-size unit on disk. +2. The **cell** — how a single row is encoded into a page. +3. The **B-tree** — how cells across many pages are organized. +4. The **pager and the WAL** — how changes get from RAM to disk + without corrupting anything when the lights go out. + +If you are coming from "I read about B-trees once" and you want to +see what the implementation actually looks like in a working embedded +database, this is the post for you. The user-level surface — the SQL +this engine accepts, the SDKs that wrap it — lives over in the +[docs](/docs); we'll only refer to it where it matters. + +## Pages + +A SQLRite database is a single file. The file is divided into +contiguous **4 KiB pages**, numbered starting at 1. Page 0 is a +header and Page 1 is the schema page; everything else is allocated +on demand to tables and indexes. + +Why 4 KiB? + +- It matches the page size of every common filesystem (ext4, APFS, + NTFS), so a single page write is almost always a single physical + write. +- It matches what `pread`/`pwrite` syscalls cost least to issue. +- It matches the sweet spot for memory-mapped page caches. +- It is exactly the size SQLite uses by default, which means + comparing performance numbers later is easier. + +A page in SQLRite has a header (page type, free byte count, slot +directory size, and parent/right-sibling pointers if it's a B-tree +node) and a body. The body grows from the bottom of the page upward +as cells; a slot directory at the top of the body grows downward. +This is the classic +[**slotted page**](https://en.wikipedia.org/wiki/Slotted_page) layout +that most disk-resident databases settle on, for the same reason +everyone settles on it: it makes variable-length rows easy to insert, +delete, and reorder without rewriting the whole page. + +## Cells + +A "cell" is what SQLRite calls one row encoded into bytes. The +encoding is very simple by design: + +- A small per-cell header carrying the row's encoded payload size and + a key (the integer primary key for tables, or the indexed value for + index pages). +- A sequence of typed values: `INTEGER`, `TEXT`, `REAL`, `BOOLEAN`, + `NULL`, `VECTOR(N)`, `JSON`. Each is length-prefixed when it + needs to be. + +What we deliberately do *not* do (yet) is the SQLite varint trick +where each integer is encoded with the smallest number of bytes that +will hold it. SQLite gets a real space win out of that — most +integers in a real database are small. SQLRite uses fixed-width +encoding for now because the win is modest at our scale and the +debugging cost of a malformed varint is not modest. We will likely +add it once the page format hits v5. + +A row that's larger than will fit in a page — for example, a long +`TEXT` blob or a 1024-dimensional `VECTOR` — spills into an +**overflow chain**. The first part stays on the page, with a pointer +to a continuation page. Continuations chain until the value ends. +This is also the SQLite playbook; it is roughly the only known good +way to keep a fixed page size while supporting arbitrarily large +values. + +## B-trees + +Every SQLRite table is a B-tree keyed on its `INTEGER PRIMARY KEY` +(or an implicit row ID if the schema doesn't declare one). Every +SQLRite index is a B-tree keyed on the indexed columns. The B-trees +share the same on-disk node layout — leaf vs. internal — so the +pager doesn't need to know whether it's looking at a table page or +an index page. That's a small implementation win that pays off every +time you go to write a new feature. + +Two implementation decisions are worth calling out, because they are +not the obvious ones: + +### Bottom-up rebuild on commit + +Most database tutorials describe B-tree mutations as in-place edits: +insert a key, find the right leaf, split the leaf if it overflows, +propagate the split up the parent chain. SQLRite does this in +*memory* during a transaction, against a snapshot of the +in-memory state — but at commit time, instead of writing back the +edited tree page-by-page, **the entire B-tree is rebuilt +bottom-up**. + +That sounds expensive. It mostly isn't. The leaf pages already exist +in memory; rebuilding is a serialization pass and a couple of slot +directory tweaks. The reason to do it this way is that **bottom-up +rebuild is correct by construction.** Every page that gets persisted +is a fresh, valid page, with no "did we update the parent before the +crash?" failure mode. It also gives us a much simpler diffing pass +for the WAL (more on that next). + +The cost we are paying for this simplicity is O(N) rather than +O(log N) per commit on a hot table. That ceiling will eventually need +to come down — probably with an in-place split path — but it is a +ceiling I'd rather raise after I have the rest of the system, not +before. + +### Slot directory, not a sorted array + +A leaf page's cells are *not* stored in key order in the page body. +They're stored in arrival order. A separate **slot directory** lists +cell offsets in key order. This means an insert never has to memmove +the page body — only update the slot directory. It also means lookups +are still O(log n_cells) on the slot directory. + +Index pages use the same trick. Same wins. + +## The pager + +The pager is the thing in the middle: it sits between the in-memory +B-trees and the file. Its job is to: + +- Read pages from disk on demand into a small in-memory page cache. +- Track which pages are dirty (changed since the last commit). +- On commit, write only the dirty pages, in a way that survives a + crash. + +That last clause is what the WAL is for. + +### The diff is the trick + +A naïve pager writes every page that changed during the transaction, +in order, and then `fsync`s. That works. It is also a *lot* of +writes. SQLRite's pager only writes pages whose **bytes actually +changed.** During a write transaction, the pager hashes each +candidate dirty page against its on-disk copy. If the hash matches, +the page isn't dirty after all — somebody touched the in-memory copy +but didn't change anything that affects the persisted bytes. + +This is the diff-based pager. In practice, on workloads with +read-modify-write cycles or `UPDATE … WHERE` queries that match +nothing, it cuts disk writes dramatically. On a clean +`CREATE TABLE` + `INSERT` path, it's a no-op — every page is +genuinely new. + +## The WAL + +The write-ahead log is a sidecar file: +`.sqlrite-wal`. It exists only while writes are in flight; it is +truncated on a clean checkpoint. + +Each commit appends one **frame** per dirty page. A frame is the +page's full 4 KiB body, plus a small header carrying the page number +and a per-frame checksum. The very last frame in a commit carries the +new page-0 contents — the database header — and is marked as a +**commit frame.** + +On crash recovery, the rules are: + +- Walk the WAL forward. +- Validate each frame's checksum. +- If a frame is torn (incomplete trailing bytes), or if its checksum + doesn't match, **truncate the WAL at the last good frame and stop.** +- For every commit frame found, replay all frames up through the + commit into the page cache. +- Treat the most recent commit's page-0 as the live database header, + even if the main file's page 0 is older. + +The contract is: a commit either landed in the WAL completely, or it +didn't. There is no half-committed transaction. + +### Auto-checkpointing + +A WAL that grows forever is a problem. SQLRite **auto-checkpoints** +once the WAL crosses 100 frames. A checkpoint is straightforward: + +1. Acquire the writer lock (shared with the rest of the engine via + [`fs2`](https://github.com/danburkert/fs2-rs) advisory locks). +2. For each dirty page in the WAL, in WAL order, write it to its + home offset in the main file. +3. `fsync` the main file. +4. Truncate the WAL to zero bytes. +5. Drop the lock. + +Readers using shared locks can keep reading from the WAL while the +checkpoint runs; they'll see the WAL's view of any pages that haven't +been pushed to the main file yet. + +### Crash safety, in three sentences + +Every page write is preceded by a WAL frame. Every commit terminates +with a special frame that validates the entire batch. On reopen, the +WAL is the source of truth until checkpointed. + +That's it. That's the whole crash-safety story. + +## Putting it together + +Here is what the path looks like when you do a single `INSERT`: + +1. Parse the SQL into an AST. Bind it to a typed `InsertQuery`. +2. The executor takes the in-memory snapshot of the table (a + `BTree`), inserts the row into a leaf, splits if needed, walks + parents up. +3. On `COMMIT` (or auto-commit, for a bare `INSERT`): + - The pager finds the dirty pages of the rebuilt B-tree. + - It diffs each one against its on-disk copy and discards + no-op writes. + - It encodes the survivors as WAL frames, computing checksums. + - It writes all frames, then a final commit frame carrying the + new page-0. + - It `fsync`s the WAL. +4. The auto-checkpointer fires later if the WAL is long enough. + +The contract that sits over all of this: **a `COMMIT` returns +success only after the commit frame is durably on disk.** That's the +hard part. + +## What I'd do differently + +A few decisions I'd revisit: + +- **Fixed-width integer encoding.** The space win from varints is + real on most workloads. Worth doing in v5. +- **No per-table page chains.** Tables and indexes share a page + pool but don't pre-allocate contiguous runs. This makes seek + patterns slightly worse than they need to be on rotational media, + which doesn't matter today and might matter later. +- **The B-tree rebuild ceiling.** Eventually this needs an in-place + split path. The bottom-up rebuild is great until your hot table is + several gigabytes. + +But I'd keep the file format. The shape of bytes on disk is the +hardest thing to change later, and the shape SQLRite settled on is +boring in all the right ways. + +If you want to read the actual code, the page and B-tree types live +in [`src/sql/pager/`](https://github.com/joaoh82/rust_sqlite/tree/main/src/sql/pager) +and the executor's commit path lives in +[`src/sql/db/database.rs`](https://github.com/joaoh82/rust_sqlite/blob/main/src/sql/db/database.rs). +The full file format spec is at +[`docs/file-format.md`](https://github.com/joaoh82/rust_sqlite/blob/main/docs/file-format.md). + +The next post in the series is the most fun one to write so far: +[adding vector search to a SQLite-style database with HNSW](/blog/adding-vector-search-with-hnsw). +That's where embedded SQL stops looking like 1992 and starts looking +like the next decade. + +More on the philosophy behind these choices in the +[origin-story post](/blog/why-im-building-sqlrite); a head-to-head +against rusqlite-bundled SQLite on storage-heavy workloads is in +[the benchmarks post](/blog/sqlrite-vs-sqlite-benchmarks). The +[transactions and persistence section of the docs](/docs#persistence) +covers the same ground from the user side. + +If SQLRite has been useful to you, ⭐ the +[repo](https://github.com/joaoh82/rust_sqlite) — visibility is the +bottleneck. diff --git a/web/content/blog/shipping-sqlrite-tauri-mcp-sdks.mdx b/web/content/blog/shipping-sqlrite-tauri-mcp-sdks.mdx new file mode 100644 index 0000000..5d23b57 --- /dev/null +++ b/web/content/blog/shipping-sqlrite-tauri-mcp-sdks.mdx @@ -0,0 +1,351 @@ +--- +title: "From SQL to Tauri: shipping SQLRite as a desktop app, MCP server, and Python/Node/Go SDKs" +description: "The distribution story behind SQLRite — one engine, six surfaces. How a Rust crate becomes a Svelte/Tauri desktop app, a Model Context Protocol stdio server, and language SDKs for Python, Node, Go, C, and the browser." +publishedAt: "2026-05-06" +author: "Joao Henrique Machado Silva" +tags: ["sqlrite", "tauri", "mcp", "ffi", "wasm", "rust", "distribution"] +primaryKeyword: "Rust embedded database SDK packaging Tauri MCP" +--- + +The least glamorous part of shipping a database is the part that +matters most for whether anyone uses it. The engine is a Rust crate. +Beautiful. Now you need a CLI binary, a desktop app, an MCP server +for AI clients, a Python package, a Node module, a Go module that +plays nicely with `database/sql`, a C header, a WASM bundle, and — +crucially — a release process that builds all of those for every +target on every push without involving a human. + +I underestimated this project. I'd guess a third of the engineering +time on +[SQLRite](https://github.com/joaoh82/rust_sqlite) so far has been +distribution. Half of that was figuring it out the first time; +half was the honest cost of *keeping six surfaces in sync.* + +This post is a tour of those six surfaces, what each one wraps, and +which problems showed up only at the distribution boundary. If +you're building a Rust library and considering "should I make this +embeddable from Python / Node / a browser tab," the short answer is +yes. The longer answer is what's below. + +## The engine, then everything else + +Everything starts at one file: +[`src/connection.rs`](https://github.com/joaoh82/rust_sqlite/blob/main/src/connection.rs). +That's the public Rust API. `Connection`, `Statement`, `Rows`, +`Row`, `Value`. Five types. Every other surface — every SDK, every +binary, every server — binds against those five types. There is +no second API. + +That sounds obvious. It is the single most useful decision in this +project. The temptation when you ship to multiple ecosystems is to +let each ecosystem add its own ergonomic shortcut to the engine — +a Python-flavored `executemany`, a Node `prepare().all()`, a Go +`Rows.Scan` — and to do that by exposing some private API "just for +the binding." Each shortcut is fine in isolation; together they +turn into six engines pretending to be one. + +SQLRite's rule is **the binding can wrap, but it cannot reach +inside.** If a binding wants `executemany`, it loops `execute` and +catches errors. If it wants `Rows.Scan`, it calls `Row::get`. If +that's awkward — and it is sometimes — the fix goes into the +*Rust* API, where every other surface gets it for free. + +For the user-facing reference of every API mentioned below — the +SQL accepted, the meta commands, the SDK call shapes — head to +[the getting-started docs](/docs). + +## The CLI + +The simplest surface and probably the most-used one. `sqlrite` is a +[`[[bin]]`](https://github.com/joaoh82/rust_sqlite/blob/main/Cargo.toml) +target in the engine crate, gated on the `cli` and `ask` features. +It runs a [`rustyline`](https://github.com/kkawakam/rustyline)-driven +prompt with history, syntax highlighting, bracket matching, and a +small set of meta commands (`.open`, `.tables`, `.ask`, `.help`, +`.exit`). + +The thing that surprised me here: the REPL is the **best testing +surface for the engine.** Every weird SQL the parser doesn't +handle, every error message that's confusing, every long-running +query that should be interruptible — all of those show up first in +the REPL, before any binding sees them. Treating the REPL as a +"first-class diagnostic tool, not just a demo" was a mid-Phase-2 +attitude shift that's paid for itself a hundred times over. + +## The Tauri desktop app + +[Tauri 2](https://tauri.app/) plus [Svelte 5](https://svelte.dev/). +A three-pane layout: file pickers in the header, tables and schema +in the sidebar, a query editor with line numbers and a +selection-aware Run. Prebuilt installers for macOS (`.dmg`), +Windows (`.msi`), and Linux (`.AppImage` / `.deb` / `.rpm`) ship +with every release. The `desktop/` workspace member is its own +Cargo crate plus its own `package.json`; both speak to the same +engine. + +Two things were genuinely hard: + +### Embedding, not FFI + +The first version of the desktop app talked to the engine through +the C FFI. That worked, and it was fast, and it was wrong. The C +FFI exists for non-Rust callers; using it from a Rust app inside a +Tauri shell is a layer of unnecessary translation. The Tauri +backend is itself Rust. It can `use sqlrite::Connection;` and +that's the end of the integration. We deleted ~400 lines of FFI +glue and got rid of the threading surprise that came with it. + +### Filesystem access without panic + +A desktop database app does things a server-side library can +ignore: open a database from a file picker, save under a different +name, refuse to open a file the user can't write to, recover +gracefully from "the user opened a JPEG instead of a database." +SQLRite's typed errors made this dramatically simpler than I +expected — every "this isn't a database" path returns a +`SQLRiteError::Io(_)` or `Format(_)`, and the GUI lights up the +right toast. + +The biggest single-day improvement was the day I stopped making the +GUI thread call into the engine directly and put a small +`Arc>` between them. SQLRite is single-writer, +many-reader; the GUI uses one writer (the editor) and many readers +(the table list, the schema panel). Three lines of code; a category +of races gone. + +## The MCP server + +This one I almost didn't build. I'd seen +[Model Context Protocol](https://modelcontextprotocol.io/) demos +and assumed the integration would be cute but shallow. Then a +collaborator mentioned that they'd been pasting SQL into Claude +Code by hand for an hour to debug a schema problem, and I thought: +of course this is the integration. The whole point of an embedded +database is that it doesn't need a server. An MCP stdio server +*is* the AI-native interface to that. + +`sqlrite-mcp` is a separate crate that links the engine and exposes +eight tools: + +- `list_tables` +- `describe_table` +- `query` (read-only) +- `execute` (DDL/DML — hidden in `--read-only` mode) +- `schema_dump` +- `vector_search` +- `bm25_search` +- `ask` (LLM-powered natural-language → SQL) + +Stdio transport, JSON-RPC frames. The whole thing is a few hundred +lines because the engine already does the work; the server is a +shim. `--read-only` opens with a shared lock and hides `execute`, +which is the right default for "let the LLM look at my database +without nuking it." + +The thing I want to emphasize: **the MCP server is the engine.** +There is no caching layer, no protocol-specific adapter, no +re-implementation of `SELECT`. A query received over stdio takes +exactly the same path as a query from the REPL. + +## The C FFI + +`sqlrite-ffi` is a C ABI cdylib plus a +[`cbindgen`](https://github.com/mozilla/cbindgen)-generated +`sqlrite.h`. Opaque pointer types, thread-local last-error, +split `sqlrite_execute` (DDL/DML) vs. +`sqlrite_query` / `sqlrite_step` (SELECT iteration). + +The C FFI exists to back two callers: + +- The Go SDK (cgo). +- "Some C consumer we haven't met yet." + +We considered exposing the FFI surface to Python and Node too. We +didn't, for one reason: PyO3 and napi-rs are *much* better Rust +bindings than they are C bindings. Every minute spent generating +ctypes wrappers is a minute not spent making the Python API feel +like Python. + +## The Python and Node SDKs + +[PyO3](https://pyo3.rs/) for Python, [napi-rs](https://napi.rs/) +for Node. Both build cdylibs that wrap the engine, both ship as +`pip install sqlrite` and `npm install @joaoh82/sqlrite`, both +expose APIs that look native to their host ecosystem. + +The Python API is `sqlite3`-flavored on purpose: + +```python +import sqlrite + +with sqlrite.connect("app.sqlrite") as conn: + cur = conn.cursor() + cur.execute("CREATE TABLE notes (id INTEGER PRIMARY KEY, body TEXT)") + cur.execute("INSERT INTO notes (body) VALUES (?)", ("first",)) + rows = cur.execute("SELECT id, body FROM notes").fetchall() +``` + +Anyone who has ever used `sqlite3` in Python can read this. That +familiarity is the entire point. We are not selling a new mental +model; we are selling the same mental model with a different +storage engine. + +The Node API is `better-sqlite3`-flavored for the same reason: + +```js +import { Database } from "@joaoh82/sqlrite"; + +const db = new Database("app.sqlrite"); +db.exec("CREATE TABLE notes (id INTEGER PRIMARY KEY, body TEXT)"); +db.prepare("INSERT INTO notes (body) VALUES (?)").run("first"); +console.log(db.prepare("SELECT id, body FROM notes").all()); +``` + +Two things you don't see but that took real work: + +- **Cross-platform prebuilt wheels / binaries.** Both crates + publish prebuilds for every common platform so users don't need a + Rust toolchain. The CI matrix for this is large and the linker + options are platform-specific. `napi-rs` and `maturin` both + earn their keep here; rolling this from scratch would be weeks of + work per ecosystem. +- **`Value::Vector` round-tripping.** A Python `list[float]` becomes + a `Value::Vector(Vec)` becomes a JS `Float32Array` becomes a + Go `[]float32`. The conversion code is small but the test matrix + isn't. Every binding has a "round-trip a vector through every + type" test; one of those tests fired about a month into Phase 7d + and saved a real bug. + +## The Go SDK + +Go is a different shape than Python or Node. The expected API is +`database/sql`. The expected import looks like: + +```go +import ( + "database/sql" + _ "github.com/joaoh82/rust_sqlite/sdk/go" +) + +db, _ := sql.Open("sqlrite", "app.sqlrite") +``` + +Implementing a `database/sql` driver is straightforward but +opinionated; the things that matter are types (Go's +`sql.NullString` and friends map cleanly to SQLRite's typed +values), prepared statements (the trait-level prepared-statement +support already exists; the driver just exposes it), and **cgo +boundaries.** + +Cgo is the cost of admission. The Go SDK is *not* part of the +Cargo workspace, by design — a workspace member targeting cgo +spreads its build constraints across the whole workspace, and that +hurts cargo's incremental builds. A separate workspace member, a +separate Makefile target, a release process that knows about both. + +If you're starting a multi-language SDK story today, my advice +would be: **commit to a workspace shape early.** Decide which +crates are workspace members and which are siblings, and be +prepared to defend the choice. Refactoring this halfway through is +miserable. + +## The WASM bundle + +`@joaoh82/sqlrite-wasm` is the engine compiled to WebAssembly via +[`wasm-bindgen`](https://rustwasm.github.io/wasm-bindgen/). The +output is roughly 1.8 MB raw, ~500 KB gzipped, with three +`wasm-pack` targets (`web`, `bundler`, `nodejs`). + +The whole database can live in a browser tab. That sentence feels +absurd to type, but it's a flavor of "embedded" SQLite never +quite delivered for browsers — sql.js is a Emscripten port of the +C engine and it's wonderful, but it's also enormous and the build +toolchain is a different planet from the rest of the SQLRite +binaries. Building once, in Rust, and emitting a WASM target alongside +every other surface is a meaningfully better story. + +The WASM target is also where the engine's feature gates earn their +keep. WASM builds with `default-features = false` to drop the +`cli`, `ask`, and `file-locks` features (rustyline and `fs2` don't +make sense in a browser). The conditional compilation has been +fiddly to maintain — every new feature has to declare which +surfaces it belongs to — but it's also forced a useful discipline: +the engine has a clear "always-on" core and a clear "shell-y" outer +ring, and that mental model has paid off when reasoning about what +goes into a security review. + +## The release process + +`scripts/bump-version.sh 0.10.0` updates the version across 11 +manifests in one shot. That number used to be lower; every binding +adds a manifest. The cost of keeping them in sync is real, and the +script paid for itself the first time I forgot one of them. + +Releases are GitHub-Actions-driven matrix builds. Every push to a +release tag produces: + +- `crates.io` publish for `sqlrite-engine`, `sqlrite-mcp`, + `sqlrite-ffi`, `sqlrite-ask`. +- Python wheels via `maturin` for macOS / Linux / Windows × + CPython 3.10–3.13. +- Node prebuilds via `napi-rs` for the same platform matrix. +- Go module is a tag — Go pulls source. +- WASM target published to npm separately as + `@joaoh82/sqlrite-wasm`. +- Tauri installers as GitHub release assets. + +This is, for what it's worth, the most fragile part of the project. +A new binding adds rows to the matrix; a platform regression in any +of the upstream toolchains breaks a column. The release pipeline is +where I spend the most time reading other people's CI logs. + +## What I'd tell a smaller version of this project + +Three things I'd do the same and one I'd do differently. + +**Same.** One Rust API. The discipline of making every surface +wrap-not-extend has saved more time than it's cost. + +**Same.** Tauri instead of Electron. The bundle size, the security +posture, and the fact that the backend is *Rust* mean the desktop +app doesn't have to learn a new mental model. + +**Same.** Build the MCP server early. I didn't, and the people who +got the most value out of SQLRite first were the ones with +AI-flavored use cases. + +**Differently.** Pin the cdylib's symbol surface from day one. +Both PyO3 and napi-rs let you regenerate ABIs every release, and +they will, and you will spend a Saturday afternoon figuring out why +your Python wheel for macOS arm64 doesn't load. A `crate-type` +discipline plus a pinned `cdylib-link-lines` is the prevention. + +If you want to read the actual code for any of these surfaces: + +- Engine: [`src/`](https://github.com/joaoh82/rust_sqlite/tree/main/src) +- Desktop: [`desktop/`](https://github.com/joaoh82/rust_sqlite/tree/main/desktop) +- MCP server: [`sqlrite-mcp/`](https://github.com/joaoh82/rust_sqlite/tree/main/sqlrite-mcp) +- C FFI: [`sqlrite-ffi/`](https://github.com/joaoh82/rust_sqlite/tree/main/sqlrite-ffi) +- Python / Node / WASM / Go: [`sdk/`](https://github.com/joaoh82/rust_sqlite/tree/main/sdk) + +The point of the whole exercise is one thing: a user picks the +language they already know, and `pip install sqlrite` (or +`npm install`, or `cargo add`, or `go get`) is the only command +they have to learn. One database, six surfaces, no thought +required. + +If you want to dig in further, the +[origin-story post](/blog/why-im-building-sqlrite) covers the +"why," [the storage deep-dive](/blog/how-sqlrite-stores-rows-on-disk) +covers the file format that every surface above shares, and +[the benchmarks post](/blog/sqlrite-vs-sqlite-benchmarks) covers how +we actually measure whether any of this is fast enough yet. The +[/docs page](/docs) is the user-facing reference for the whole +surface. + +The next milestone is full-text search and hybrid retrieval — and +then we start moving the benchmarks. If SQLRite has been useful to +you, ⭐ the +[repo](https://github.com/joaoh82/rust_sqlite) — visibility +matters, especially for a project that wants to live in six +ecosystems at once. diff --git a/web/content/blog/sqlrite-vs-sqlite-benchmarks.mdx b/web/content/blog/sqlrite-vs-sqlite-benchmarks.mdx new file mode 100644 index 0000000..96daf23 --- /dev/null +++ b/web/content/blog/sqlrite-vs-sqlite-benchmarks.mdx @@ -0,0 +1,248 @@ +--- +title: "SQLRite vs SQLite, side by side: a benchmark and a fair-fight setup" +description: "How SQLRite's benchmark harness compares an in-development Rust embedded database against rusqlite-bundled SQLite — the workloads, what's already close, and where the gap is wide." +publishedAt: "2026-04-29" +author: "Joao Henrique Machado Silva" +tags: ["sqlrite", "benchmarks", "sqlite", "rust", "performance"] +primaryKeyword: "Rust embedded database benchmarks vs SQLite" +--- + +Comparing a one-year-old database against SQLite is, on paper, a +silly thing to do. SQLite has 25 years of micro-optimization, an +army of contributors, and a test suite the size of some small +operating systems. Whatever the result, "we're slower than SQLite" +is the boring one and "we're faster than SQLite" is almost certainly +the result of measuring the wrong thing. + +I built a benchmark harness for [SQLRite](https://github.com/joaoh82/rust_sqlite) +anyway, because I needed to know two things you cannot guess: + +1. **Where exactly is SQLRite slow today?** Slow on what shapes of + query? Slow because of which subsystem? Slow because we picked the + wrong algorithm or because we haven't optimized the right path? +2. **Which corners of the engine are *already* competitive?** Not + because we won, but because the gap is small enough that further + work is throwing rocks at boulders. + +The harness — `benchmarks/` in the repo — is what those answers +come from. This post walks through how it's built, what a "fair +fight" actually means here, and what the numbers look like today. + +## Why a separate harness + +Criterion has a great Rust benchmark API, but for a comparison +between databases I wanted three things that a plain Criterion bench +makes hard: + +- **A `Driver` trait** so that adding "SQLite via rusqlite" or + "DuckDB via duckdb-rs" or, eventually, "Postgres via libpq" is one + file with no special-cased code. +- **Workloads that look like real embedded usage**, not just + microbenchmarks. Insert a thousand rows in a transaction. Run a + prepared `SELECT` ten thousand times with different bind values. + Re-open a 100k-row database from disk and run the first query. +- **Group A / Group B separation.** Group A is "anything any + embedded SQL engine should handle" — single-row inserts, predicate + scans, simple aggregates. Group B is "things SQLRite supports that + classic embedded SQL doesn't" — vector k-NN, hybrid retrieval. The + separation matters because including a DuckDB driver in Group A is + an honest comparison; including DuckDB in Group B is testing + something else entirely. + +The harness lives outside the default cargo test/build commands. +Criterion's stable noise floor on a shared CI runner is too high to +be useful, and the `rusqlite` build with `bundled` enabled is heavy +enough to slow CI for a benchmark that nobody runs in CI anyway. You +run it locally with `make bench` (or `make bench-duckdb` to add +the DuckDB driver to Group A). + +## The drivers + +`Driver` is a small Rust trait. The interesting part of the contract: + +```rust +pub trait Driver { + fn name(&self) -> &'static str; + fn open(path: &Path) -> Result where Self: Sized; + fn execute(&mut self, sql: &str) -> Result; + fn execute_with_params(&mut self, sql: &str, params: &[Param]) -> Result; + fn query_count(&mut self, sql: &str) -> Result; + // ... and a couple more for SELECT iteration and prepared statements +} +``` + +There are three implementations: + +- **`SqliteDriver`** — wraps `rusqlite` with `bundled` so we + control the SQLite version under test (currently 3.45.x). +- **`SqlriteDriver`** — wraps the SQLRite engine directly, with the + default features. +- **`DuckdbDriver`** — wraps `duckdb-rs`, opt-in via a feature + flag, used only for Group A reads. + +Every workload is parameterized by row count and accepts any +`Driver`. The harness times each combination with Criterion and +emits a JSON-shaped summary that the website turns into bar charts +on the [benchmarks section of the landing page](/#benchmarks). + +The SQLite driver is the relevant baseline. SQLite is the +*incumbent embedded SQL database*. If we are doing the work right, +the gap between SQLRite and rusqlite-bundled SQLite is the gap that +matters; no synthetic baseline gets to substitute for that. + +## What "fair fight" actually means + +A benchmark is only as honest as its setup. Three rules I've held +to: + +### 1. Same on-disk state + +For workloads that touch disk — and basically all of them do — +SQLRite and SQLite each open a fresh database file in a fresh +tmpdir before the run starts. No warm OS cache from a previous +iteration. The harness `fsync`s the parent directory after creation +so we're not measuring a delayed dirent. + +### 2. Same durability guarantees + +SQLite has knobs that change durability: +`PRAGMA synchronous = OFF` is famously fast and famously +*will eat your data on a power loss*. By default, the harness runs +SQLite in `synchronous = NORMAL` and `journal_mode = WAL`, the same +durability stance SQLRite uses. There's a separate variant that +unlocks `synchronous = OFF` for both engines, but the headline +numbers are the matched-durability ones. + +### 3. Same workload shape + +If a workload is "insert 1000 rows in one transaction," both +drivers run that as one transaction. If the SQLite driver wraps it +in `BEGIN; … COMMIT;` automatically because of how rusqlite handles +implicit transactions, the SQLRite driver does the same. The +harness assertion at the end is "both drivers got to the same +SELECT result" — not just "both finished without erroring." + +What this rules out is the classic benchmark trick where the +"contender" runs in some unsafe-but-fast mode and the +"baseline" runs in safe mode. Easy way to win a benchmark; not a +useful number. + +## What the numbers look like + +The numbers move with every release; the charts on the landing page +are pulled from the most recent run. As of the harness's first +public commit (Phase 7d → Phase 8 transition), the rough story is: + +- **Single-row INSERT, no transaction.** SQLRite is in the + ballpark of SQLite. The diff-based pager helps here — most + inserts are one dirty page and one WAL frame in both engines. +- **Bulk INSERT in one transaction.** SQLite is meaningfully + faster. SQLite has had decades of tuning around the bulk-insert + fast path; the SQLRite cost is dominated by the bottom-up B-tree + rebuild, which is O(N) and we will eventually replace. +- **Indexed point lookup (`SELECT WHERE id = ?` on PK).** Close + enough that the noise dominates. +- **Range scan with predicate.** SQLite is faster, mainly because + its predicate evaluator is cleverer than ours. This is also where + the prepared-statement plan cache gives back the most ground. +- **Prepared statement vs. parsed-each-time.** Both engines win + big from preparation. SQLRite's per-connection LRU plan cache + defaults to 16 slots; tuning it up helps a lot for hot loops. +- **Vector k-NN with HNSW (Group B).** SQLite-vec or DuckDB + flat-scan in Group A is the wrong comparison; for the right + comparison (HNSW vs. brute force on the same data) the win is + large at small `k`, as expected. + +I am being deliberately vague about *exact* numbers in a blog post. +The right place for those is the live charts, which update every +release. What I'd like you to take away is the **shape**: SQLRite +is not catastrophically slow on the workloads I care about, it is +clearly slower on a couple of paths, and the slow paths have +identifiable causes. + +## What the slow paths tell us + +The benchmark harness is, in effect, a roadmap generator. Every +workload that lags has a one-line cause: + +- **Bulk insert** → bottom-up B-tree rebuild. Replace with an + in-place split path eventually. +- **Range scan with complex predicate** → naive predicate + evaluator. The fix is a small interpreter loop, not a full + rewrite. +- **Reopen-then-first-query** → HNSW rebuild on open. Persisting + the graph removes this entirely. +- **Multi-join queries** → nested-loop driver. Hash and merge join + are both Phase-9 candidates. + +The thing that's nice about a written-down benchmark is that you +can argue with it. "Why is the bulk-insert workload 1000 rows +instead of 100,000?" is a real question with a real answer (because +nobody on the embedded side actually does single-transaction bulk +loads of 100k rows in their hot path; if they do, they should). I +got several good roadmap suggestions from people complaining about +the harness. That's a feature, not a bug. + +## What we're *not* benchmarking + +Worth being explicit: + +- **Latency under crash recovery.** This is a real metric — how + long does it take to reopen a database with a 50 MB unflushed WAL + — but it requires fault injection to measure properly. On the + list. +- **Memory.** Embedded databases live or die on memory footprint. + The harness doesn't track RSS yet; it should. +- **Multi-connection contention.** SQLite is *the* gold standard + for read-mostly multi-connection workloads. SQLRite is single + writer + many readers via advisory locks; the harness measures + this for both engines but I haven't yet written a workload that + stresses it. +- **The non-SQL surfaces.** Python via PyO3, Node via napi-rs, Go + via cgo, WASM. These all add an FFI hop on top of the engine, and + some of them are slower than the Rust path by a measurable + amount. The harness has the hooks but no published numbers yet. + +## The point of the exercise + +If you take only one thing from this post, take this: **a database +without a benchmark harness is a database that is silently lying to +you about which features are slow.** SQLRite was probably lying +about three or four things until the harness landed; some of them I +was wrong about, and one of them ("we should be roughly competitive +on point lookups") I was right about and didn't realize. Either +outcome is useful. + +For the broader context behind this engine's design — why we have +HNSW and a diff-based pager in the same file format — see the +[origin-story post](/blog/why-im-building-sqlrite) and +[the storage deep-dive](/blog/how-sqlrite-stores-rows-on-disk). The +SQL surface that everything in this post exercises is documented in +[the getting-started docs](/docs). + +If you want to run the harness yourself: + +```sh +git clone https://github.com/joaoh82/rust_sqlite +cd rust_sqlite +make bench # SQLRite + SQLite +make bench-duckdb # add the DuckDB driver to Group A +``` + +The full plan, including which workloads we want to add next, is in +[`docs/benchmarks-plan.md`](https://github.com/joaoh82/rust_sqlite/blob/main/docs/benchmarks-plan.md). +And if you build a workload SQLRite handles surprisingly well or +surprisingly badly, please file an +[issue](https://github.com/joaoh82/rust_sqlite/issues) — that's the +cheapest contribution you can make to a database project, and it's +the one with the best leverage. + +The next post in the series is the distribution one: +[shipping SQLRite as a desktop app, an MCP server, and Python / +Node / Go SDKs](/blog/shipping-sqlrite-tauri-mcp-sdks). One engine, +six surfaces, a story about packaging that took longer than the +file format did. + +If SQLRite is useful to you, ⭐ the +[repo](https://github.com/joaoh82/rust_sqlite) — visibility +matters more than I'd like to admit. diff --git a/web/content/blog/why-im-building-sqlrite.mdx b/web/content/blog/why-im-building-sqlrite.mdx new file mode 100644 index 0000000..78a704c --- /dev/null +++ b/web/content/blog/why-im-building-sqlrite.mdx @@ -0,0 +1,222 @@ +--- +title: "Why I'm building SQLRite — an embedded SQL and vector database in Rust" +description: "An origin story for SQLRite: the design tenets behind a SQLite-style engine rebuilt from scratch in Rust, what's shipped, and what comes next." +publishedAt: "2026-04-08" +author: "Joao Henrique Machado Silva" +tags: ["sqlrite", "rust", "databases", "design"] +primaryKeyword: "embedded SQL database in Rust" +--- + +There is a particular kind of software that you can use for a decade +without ever really seeing. SQLite is one of those. It is everywhere — +on your phone, in your browser, inside Photoshop, behind your favorite +editor — and yet most of the people who depend on it have never opened +the file format spec, never read the page cache, never traced what +actually happens between `INSERT` and the green light on your SSD. + +I started [SQLRite](https://github.com/joaoh82/rust_sqlite) because I +wanted to *see* it. Not just use it: own it. Build the thing, type the +B-tree split, watch the WAL frames roll past. SQLRite is what you get +when you take the constraints that made SQLite great — single file, +embedded, zero configuration, ACID — and try to rediscover them from +first principles, in Rust, with one set of eyes on the AI-shaped +present. + +This post is the manifesto. The why before the what. + +## What SQLRite is + +SQLRite is a from-scratch SQLite-style embedded database written in +Rust. It ships as a Rust crate +([`sqlrite-engine`](https://crates.io/crates/sqlrite-engine)), a REPL +binary, a Tauri desktop app, an MCP stdio server, a C ABI shim, and +SDKs for Python, Node, Go, and the browser via WASM. One engine, one +file format, six surfaces. + +The core is small enough to read in a weekend: + +- A 4 KiB-page on-disk format with cell-encoded rows. +- A B-tree per table and per index, rebuilt bottom-up on commit. +- A write-ahead log with crash-safe checkpointing. +- A SQL surface — `CREATE` / `INSERT` / `SELECT` with predicates, + `JOIN`s in all four flavors, aggregates and `GROUP BY`, prepared + statements, transactions, `ALTER` / `DROP` / `VACUUM`, `PRAGMA`. +- A vector type with HNSW indexing for sub-linear k-NN. +- A BM25 full-text index that composes with the vector path for + hybrid retrieval. + +Here is what one session looks like: + +```sh +$ cargo install sqlrite-engine +$ sqlrite app.sqlrite +sqlrite> CREATE TABLE notes (id INTEGER PRIMARY KEY, body TEXT); +sqlrite> INSERT INTO notes (body) VALUES ('the only embedded sql in rust'); +sqlrite> SELECT * FROM notes; ++----+-------------------------------+ +| id | body | ++----+-------------------------------+ +| 1 | the only embedded sql in rust | ++----+-------------------------------+ +``` + +The same engine runs underneath every other surface. You can open the +same `.sqlrite` file from the REPL, from Python, from Node, from a +desktop GUI, or from an MCP server — and the bytes on disk don't care. + +## Why rebuild SQLite + +The honest answer is: to learn the things that you cannot learn by +reading. There is a whole class of database concepts — page splits, +WAL replay, free lists, write amplification, cache eviction +heuristics — that read like trivia until you've shipped a buggy +version of them. Once you have, you don't forget. + +But there is a more interesting answer too, and it has to do with +*now*. + +Embedded databases have a moment again. The reasons are different +from the ones SQLite was born into. The wave this time is local, +private, model-shaped: agents that need a working memory; desktop +apps that ship a vector store and a knowledge graph by default; mobile +RAG; offline-first sync; LLM tools that want to run a SQL query +against your project without phoning a server. The classic SQLite +recipe — single file, embedded, zero config — fits all of that +beautifully. What does *not* fit is "wait for an extension." + +SQLite supports vectors today only through extensions +([`sqlite-vss`](https://github.com/asg017/sqlite-vss), +[`sqlite-vec`](https://github.com/asg017/sqlite-vec)) — fine when you +control the binary, awkward when you ship to users. Full-text search +is FTS5, an opt-in module. Both are excellent in their domain, but +the integration story for embedded apps that want both, plus +hybrid retrieval, plus a desktop installer, plus six SDKs, plus an +MCP server, is "good luck." + +SQLRite's bet is that those things should be in the engine. Vectors +are a column type. FTS is `CREATE INDEX … USING FTS`. The desktop +GUI, the MCP server, and the SDKs all link the same Rust crate. +There's no "extension story" because there's no extension. + +## Design tenets + +A few principles fall out of that bet, and they have shaped almost +every implementation choice so far: + +1. **One file, end of story.** A SQLRite database is one + `.sqlrite` file plus a sidecar WAL during writes. No directories, + no config, no daemon. You can `cp` it to back it up and `rsync` it + to a peer. +2. **Crash safety is a feature, not an afterthought.** Every + release has a torn-write test, a partial-WAL test, and a header + mismatch test. The pager refuses to commit unless the WAL frame + landed. +3. **The lib is the engine.** No `unsafe` you can avoid. Single + `Connection` API. Tauri can embed it directly. WASM gets the same + surface stripped of POSIX locks. +4. **Phase-by-phase, public.** SQLRite is built in numbered phases, + each with a written plan in `docs/phase-*.md`. The roadmap is open; + the design discussions are open; this blog is open. You can read + exactly why a decision was made. +5. **Don't reinvent the parser.** SQLRite uses + [`sqlparser`](https://github.com/sqlparser-rs/sqlparser-rs) (SQLite + dialect) and only narrows the AST. Inventing grammar is rarely the + useful part of building a database. + +There is also a tenet that's mostly aesthetic but I think matters: +**every error returns a typed `SQLRiteError`, no panics ever**. It is +shocking how much effort that takes, and how much trust it buys. + +## What's shipped + +As of mid-2026, SQLRite is in version `0.9.x`. The roadmap is broken +into phases; phases 1–7 are shipped, phase 8 is in flight. The short +version: + +- **Phase 1** — REPL + parser scaffolding. +- **Phase 2** — typed errors, meta commands. +- **Phase 3** — B-tree storage. +- **Phase 4** — pager, WAL, transactions, persistence. +- **Phase 5** — JOINs (all four flavors), aggregates, `GROUP BY`, + prepared statements, `ALTER` / `DROP` / `VACUUM`, `PRAGMA`, + free-list reuse with auto-VACUUM. +- **Phase 6** — desktop GUI (Svelte 5 + Tauri 2), prebuilt + installers for macOS / Windows / Linux. +- **Phase 7** — multi-language story: C FFI, Python (PyO3), Node + (napi-rs), Go (cgo), WASM (wasm-bindgen). Plus the **vector + column type and HNSW index** in 7d. +- **Phase 8** — full-text search (FTS5-style inverted index with + BM25) and hybrid retrieval. In progress. + +The thing I'm proudest of is not any one feature. It's that all six +surfaces exist at once. You can talk to the same database from a Rust +unit test, a Python notebook, a `node` REPL, a Go binary, a browser +tab, and a Claude Code session — and they all see the same B-tree. + +## What's coming next + +The roadmap continues past Phase 8: + +- **Subqueries, then `HAVING`, then CTEs.** The executor is + ready for them; the AST narrowing isn't. +- **Hash and merge joins** for equi-join shapes. The current driver + is a plain nested-loop. Adequate for small embedded workloads; + silly for any join above a few thousand rows on each side. +- **Better persistence for HNSW.** The graph is rebuilt on open + today. It needs to live in the file format alongside everything + else. +- **More pragmas.** `journal_mode`, `synchronous`, `cache_size`, + `page_size` should all be reachable. +- **Online migrations.** `ALTER` is single-op per statement; the + long-running case (rewrite a column under load) deserves better. + +Beyond features, the bigger goal is **performance.** SQLRite's +benchmark suite (which I'll write about +[in a later post](/blog/sqlrite-vs-sqlite-benchmarks)) compares +against rusqlite-backed SQLite head to head. The point isn't to beat +SQLite; SQLite has 25 years of micro-optimization behind it. The +point is to know exactly where SQLRite is slow, so the curve bends in +the right direction over time. + +## Why open-source it + +There is a version of this project that lives on my laptop and never +ships. I would have learned roughly the same things from it. But that +version doesn't have a roadmap, doesn't have to defend a feature +gate, doesn't have to write down why the B-tree commits bottom-up +instead of in place. I am writing SQLRite in public partly because it +is more useful to me that way: the act of explaining a decision is +the act of testing it. + +It is also, frankly, more fun. The most rewarding bug reports I have +ever read came in on a database side project. The internet has a +small but excellent population of people who care about WAL frames at +3 a.m., and I would like to keep finding them. + +## How to follow along + +If you want to play with what's there: + +```sh +cargo install sqlrite-engine # CLI / REPL +cargo add sqlrite-engine # Rust crate +pip install sqlrite # Python +npm install @joaoh82/sqlrite # Node +``` + +The desktop installer, MCP server, and the +[docs](/docs) all live at [sqlritedb.com](/). The next post in this +series digs into how SQLRite actually stores rows on disk — +[pages, B-trees, and the diff-based pager](/blog/how-sqlrite-stores-rows-on-disk). + +If you build something on it, even something small, I would love to +hear about it. The repo is at +[github.com/joaoh82/rust_sqlite](https://github.com/joaoh82/rust_sqlite), +and if SQLRite is useful to you, the most helpful thing you can do is +⭐ it — visibility is the bottleneck for almost every dev tool. + +The whole project is the result of a simple bet: that the embedded +database deserves a fresh take, that Rust is a good language to make +that take in, and that the AI-shaped era we're entering will value +"local, private, single-file, vectors and FTS in the box" more, not +less. We will see. Either way, the journey is worth writing about. diff --git a/web/package-lock.json b/web/package-lock.json index 64b40f8..bce38d4 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -10,8 +10,10 @@ "dependencies": { "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "gray-matter": "^4.0.3", "lucide-react": "^0.469.0", "next": "^15.5.18", + "next-mdx-remote": "^6.0.0", "react": "19.0.0", "react-dom": "19.0.0", "tailwind-merge": "^2.6.0" @@ -41,6 +43,29 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@emnapi/core": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", @@ -800,6 +825,60 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@mdx-js/mdx": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@mdx-js/mdx/-/mdx-3.1.1.tgz", + "integrity": "sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdx": "^2.0.0", + "acorn": "^8.0.0", + "collapse-white-space": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "estree-util-scope": "^1.0.0", + "estree-walker": "^3.0.0", + "hast-util-to-jsx-runtime": "^2.0.0", + "markdown-extensions": "^2.0.0", + "recma-build-jsx": "^1.0.0", + "recma-jsx": "^1.0.0", + "recma-stringify": "^1.0.0", + "rehype-recma": "^1.0.0", + "remark-mdx": "^3.0.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.0.0", + "source-map": "^0.7.0", + "unified": "^11.0.0", + "unist-util-position-from-estree": "^2.0.0", + "unist-util-stringify-position": "^4.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/@mdx-js/react": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@mdx-js/react/-/react-3.1.1.tgz", + "integrity": "sha512-f++rKLQgUVYDAtECQ6fn/is15GkEH9+nZPM3MS0RcxVqoTfawHvDlSCH7JbMhAM6uJ32v3eXLvLmLvjGu7PTQw==", + "license": "MIT", + "dependencies": { + "@types/mdx": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + }, + "peerDependencies": { + "@types/react": ">=16", + "react": ">=16" + } + }, "node_modules/@napi-rs/wasm-runtime": { "version": "0.2.12", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", @@ -1310,13 +1389,39 @@ "tslib": "^2.4.0" } }, + "node_modules/@types/debug": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", + "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==", + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, "node_modules/@types/estree": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", - "dev": true, "license": "MIT" }, + "node_modules/@types/estree-jsx": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz", + "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==", + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -1331,6 +1436,27 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/mdx": { + "version": "2.0.13", + "resolved": "https://registry.npmjs.org/@types/mdx/-/mdx-2.0.13.tgz", + "integrity": "sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw==", + "license": "MIT" + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "license": "MIT" + }, "node_modules/@types/node": { "version": "22.19.18", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.18.tgz", @@ -1345,7 +1471,6 @@ "version": "19.2.14", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", - "dev": true, "license": "MIT", "peer": true, "dependencies": { @@ -1362,6 +1487,12 @@ "@types/react": "^19.2.0" } }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.59.2", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.2.tgz", @@ -1645,6 +1776,12 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.1.tgz", + "integrity": "sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ==", + "license": "ISC" + }, "node_modules/@unrs/resolver-binding-android-arm-eabi": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.11.1.tgz", @@ -1918,7 +2055,6 @@ "version": "8.16.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", - "dev": true, "license": "MIT", "peer": true, "bin": { @@ -1932,7 +2068,6 @@ "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, "license": "MIT", "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" @@ -2155,6 +2290,15 @@ "dev": true, "license": "MIT" }, + "node_modules/astring": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/astring/-/astring-1.9.0.tgz", + "integrity": "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==", + "license": "MIT", + "bin": { + "astring": "bin/astring" + } + }, "node_modules/async-function": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", @@ -2201,6 +2345,16 @@ "node": ">= 0.4" } }, + "node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -2312,6 +2466,16 @@ ], "license": "CC-BY-4.0" }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -2329,6 +2493,46 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-reference-invalid": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", + "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/class-variance-authority": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", @@ -2356,6 +2560,16 @@ "node": ">=6" } }, + "node_modules/collapse-white-space": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/collapse-white-space/-/collapse-white-space-2.1.0.tgz", + "integrity": "sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -2376,6 +2590,16 @@ "dev": true, "license": "MIT" }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -2402,7 +2626,6 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "dev": true, "license": "MIT" }, "node_modules/damerau-levenshtein": { @@ -2470,7 +2693,6 @@ "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -2484,6 +2706,19 @@ } } }, + "node_modules/decode-named-character-reference": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz", + "integrity": "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==", + "license": "MIT", + "dependencies": { + "character-entities": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -2527,6 +2762,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -2537,6 +2781,19 @@ "node": ">=8" } }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/doctrine": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", @@ -2763,6 +3020,38 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/esast-util-from-estree": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/esast-util-from-estree/-/esast-util-from-estree-2.0.0.tgz", + "integrity": "sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "devlop": "^1.0.0", + "estree-util-visit": "^2.0.0", + "unist-util-position-from-estree": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/esast-util-from-js": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/esast-util-from-js/-/esast-util-from-js-2.0.1.tgz", + "integrity": "sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "acorn": "^8.0.0", + "esast-util-from-estree": "^2.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -3139,6 +3428,19 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/esquery": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", @@ -3175,6 +3477,97 @@ "node": ">=4.0" } }, + "node_modules/estree-util-attach-comments": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/estree-util-attach-comments/-/estree-util-attach-comments-3.0.0.tgz", + "integrity": "sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-util-build-jsx": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/estree-util-build-jsx/-/estree-util-build-jsx-3.0.1.tgz", + "integrity": "sha512-8U5eiL6BTrPxp/CHbs2yMgP8ftMhR5ww1eIKoWRMlqvltHF8fZn5LRDvTKuxD3DUn+shRbLGqXemcP51oFCsGQ==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "estree-walker": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-util-is-identifier-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", + "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-util-scope": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/estree-util-scope/-/estree-util-scope-1.0.0.tgz", + "integrity": "sha512-2CAASclonf+JFWBNJPndcOpA8EMJwa0Q8LUFJEKqXLW6+qBvbFZuF5gItbQOs/umBUkjviCSDCbBwU2cXbmrhQ==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-util-to-js": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/estree-util-to-js/-/estree-util-to-js-2.0.0.tgz", + "integrity": "sha512-WDF+xj5rRWmD5tj6bIqRi6CkLIXbbNQUcxQHzGysQzvHmdYG2G7p/Tf0J0gpxGgkeMZNTIjT/AoSvC9Xehcgdg==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "astring": "^1.8.0", + "source-map": "^0.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-util-visit": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/estree-util-visit/-/estree-util-visit-2.0.0.tgz", + "integrity": "sha512-m5KgiH85xAhhW8Wta0vShLcUvOsh3LLPI2YVwcbio1l7E09NTLL1EyMZFM1OyWowoH0skScNbhOPl4kcBgzTww==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -3185,6 +3578,24 @@ "node": ">=0.10.0" } }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, + "node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "license": "MIT", + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -3510,6 +3921,43 @@ "dev": true, "license": "ISC" }, + "node_modules/gray-matter": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.3.tgz", + "integrity": "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==", + "license": "MIT", + "dependencies": { + "js-yaml": "^3.13.1", + "kind-of": "^6.0.2", + "section-matter": "^1.0.0", + "strip-bom-string": "^1.0.0" + }, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/gray-matter/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/gray-matter/node_modules/js-yaml": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, "node_modules/has-bigints": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", @@ -3604,24 +4052,92 @@ "node": ">= 0.4" } }, - "node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" + "node_modules/hast-util-to-estree": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/hast-util-to-estree/-/hast-util-to-estree-3.1.3.tgz", + "integrity": "sha512-48+B/rJWAp0jamNbAAf9M7Uf//UVqAoMmgXhBdxTDJLGKY+LRnZ99qcG+Qjl5HfMpYNzS5v4EAwVEF34LeAj7w==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-attach-comments": "^3.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-js": "^1.0.0", + "unist-util-position": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/import-fresh": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", - "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", - "dev": true, + "node_modules/hast-util-to-jsx-runtime": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz", + "integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==", "license": "MIT", "dependencies": { - "parent-module": "^1.0.0", + "@types/estree": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-js": "^1.0.0", + "unist-util-position": "^5.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", "resolve-from": "^4.0.0" }, "engines": { @@ -3641,6 +4157,12 @@ "node": ">=0.8.19" } }, + "node_modules/inline-style-parser": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz", + "integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==", + "license": "MIT" + }, "node_modules/internal-slot": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", @@ -3656,6 +4178,30 @@ "node": ">= 0.4" } }, + "node_modules/is-alphabetical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", + "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-alphanumerical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", + "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", + "license": "MIT", + "dependencies": { + "is-alphabetical": "^2.0.0", + "is-decimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/is-array-buffer": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", @@ -3801,6 +4347,25 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-decimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", + "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -3860,6 +4425,16 @@ "node": ">=0.10.0" } }, + "node_modules/is-hexadecimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", + "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/is-map": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", @@ -3913,6 +4488,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-regex": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", @@ -4104,7 +4691,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true, "license": "MIT" }, "node_modules/js-yaml": { @@ -4180,6 +4766,15 @@ "json-buffer": "3.0.1" } }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/language-subtag-registry": { "version": "0.3.23", "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz", @@ -4498,6 +5093,16 @@ "dev": true, "license": "MIT" }, + "node_modules/longest-streak": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -4530,6 +5135,18 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/markdown-extensions": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/markdown-extensions/-/markdown-extensions-2.0.0.tgz", + "integrity": "sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q==", + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -4540,6 +5157,176 @@ "node": ">= 0.4" } }, + "node_modules/mdast-util-from-markdown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.3.tgz", + "integrity": "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-mdx/-/mdast-util-mdx-3.0.0.tgz", + "integrity": "sha512-JfbYLAW7XnYTTbUsmpu0kdBUVe+yKVJZBItEjwyYJiDJuZ9w4eeaqks4HQO+R7objWgS2ymV60GYpI14Ug554w==", + "license": "MIT", + "dependencies": { + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-expression": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz", + "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-jsx": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz", + "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "parse-entities": "^4.0.0", + "stringify-entities": "^4.0.0", + "unist-util-stringify-position": "^4.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdxjs-esm": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz", + "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-phrasing": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", + "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.1", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", + "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-markdown": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", + "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -4550,6 +5337,602 @@ "node": ">= 8" } }, + "node_modules/micromark": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-core-commonmark": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", + "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-destination": "^2.0.0", + "micromark-factory-label": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-title": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-mdx-expression": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/micromark-extension-mdx-expression/-/micromark-extension-mdx-expression-3.0.1.tgz", + "integrity": "sha512-dD/ADLJ1AeMvSAKBwO22zG22N4ybhe7kFIZ3LsDI0GlsNr2A3KYxb0LdC1u5rj4Nw+CHKY0RVdnHX8vj8ejm4Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-mdx-expression": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-events-to-acorn": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-mdx-jsx": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/micromark-extension-mdx-jsx/-/micromark-extension-mdx-jsx-3.0.2.tgz", + "integrity": "sha512-e5+q1DjMh62LZAJOnDraSSbDMvGJ8x3cbjygy2qFEi7HCeUT4BDKCvMozPozcD6WmOt6sVvYDNBKhFSz3kjOVQ==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "micromark-factory-mdx-expression": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-events-to-acorn": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-mdx-md": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-mdx-md/-/micromark-extension-mdx-md-2.0.0.tgz", + "integrity": "sha512-EpAiszsB3blw4Rpba7xTOUptcFeBFi+6PY8VnJ2hhimH+vCQDirWgsMpz7w1XcZE7LVrSAUGb9VJpG9ghlYvYQ==", + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-mdxjs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-mdxjs/-/micromark-extension-mdxjs-3.0.0.tgz", + "integrity": "sha512-A873fJfhnJ2siZyUrJ31l34Uqwy4xIFmvPY1oj+Ean5PHcPBYzEsvqvWGaWcfEIr11O5Dlw3p2y0tZWpKHDejQ==", + "license": "MIT", + "dependencies": { + "acorn": "^8.0.0", + "acorn-jsx": "^5.0.0", + "micromark-extension-mdx-expression": "^3.0.0", + "micromark-extension-mdx-jsx": "^3.0.0", + "micromark-extension-mdx-md": "^2.0.0", + "micromark-extension-mdxjs-esm": "^3.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-mdxjs-esm": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-mdxjs-esm/-/micromark-extension-mdxjs-esm-3.0.0.tgz", + "integrity": "sha512-DJFl4ZqkErRpq/dAPyeWp15tGrcrrJho1hKK5uBS70BCtfrIFg81sqcTVu3Ta+KD1Tk5vAtBNElWxtAa+m8K9A==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-events-to-acorn": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-position-from-estree": "^2.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-factory-destination": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", + "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-label": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", + "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-mdx-expression": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-factory-mdx-expression/-/micromark-factory-mdx-expression-2.0.3.tgz", + "integrity": "sha512-kQnEtA3vzucU2BkrIa8/VaSAsP+EJ3CKOvhMuJgOEGg9KDC6OAY6nSnNDVRiVNRqj7Y4SlSzcStaH/5jge8JdQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-events-to-acorn": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-position-from-estree": "^2.0.0", + "vfile-message": "^4.0.0" + } + }, + "node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-title": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", + "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-whitespace": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", + "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-chunked": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", + "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-classify-character": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", + "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-combine-extensions": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", + "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", + "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-string": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", + "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-events-to-acorn": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-util-events-to-acorn/-/micromark-util-events-to-acorn-2.0.3.tgz", + "integrity": "sha512-jmsiEIiZ1n7X1Rr5k8wVExBQCg5jy4UXVADItHmNk1zkwEVhBuIUKRu3fqv+hs4nxLISi2DQGlqIOGiFxgbfHg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/unist": "^3.0.0", + "devlop": "^1.0.0", + "estree-util-visit": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "vfile-message": "^4.0.0" + } + }, + "node_modules/micromark-util-html-tag-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", + "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-normalize-identifier": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", + "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-resolve-all": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", + "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-subtokenize": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", + "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, "node_modules/micromatch": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", @@ -4591,7 +5974,6 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, "license": "MIT" }, "node_modules/nanoid": { @@ -4687,6 +6069,28 @@ } } }, + "node_modules/next-mdx-remote": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/next-mdx-remote/-/next-mdx-remote-6.0.0.tgz", + "integrity": "sha512-cJEpEZlgD6xGjB4jL8BnI8FaYdN9BzZM4NwadPe1YQr7pqoWjg9EBCMv3nXBkuHqMRfv2y33SzUsuyNh9LFAQQ==", + "license": "MPL-2.0", + "dependencies": { + "@babel/code-frame": "^7.23.5", + "@mdx-js/mdx": "^3.0.1", + "@mdx-js/react": "^3.0.1", + "unist-util-remove": "^4.0.0", + "unist-util-visit": "^5.1.0", + "vfile": "^6.0.1", + "vfile-matter": "^5.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=7" + }, + "peerDependencies": { + "react": ">=16" + } + }, "node_modules/next/node_modules/postcss": { "version": "8.4.31", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", @@ -4948,6 +6352,31 @@ "node": ">=6" } }, + "node_modules/parse-entities": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", + "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "character-entities-legacy": "^3.0.0", + "character-reference-invalid": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "is-alphanumerical": "^2.0.0", + "is-decimal": "^2.0.0", + "is-hexadecimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/parse-entities/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -5055,6 +6484,16 @@ "react-is": "^16.13.1" } }, + "node_modules/property-information": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", + "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -5116,6 +6555,73 @@ "dev": true, "license": "MIT" }, + "node_modules/recma-build-jsx": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/recma-build-jsx/-/recma-build-jsx-1.0.0.tgz", + "integrity": "sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-util-build-jsx": "^3.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/recma-jsx": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/recma-jsx/-/recma-jsx-1.0.1.tgz", + "integrity": "sha512-huSIy7VU2Z5OLv6oFLosQGGDqPqdO1iq6bWNAdhzMxSJP7RAso4fCZ1cKu8j9YHCZf3TPrq4dw3okhrylgcd7w==", + "license": "MIT", + "dependencies": { + "acorn-jsx": "^5.0.0", + "estree-util-to-js": "^2.0.0", + "recma-parse": "^1.0.0", + "recma-stringify": "^1.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + }, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/recma-parse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/recma-parse/-/recma-parse-1.0.0.tgz", + "integrity": "sha512-OYLsIGBB5Y5wjnSnQW6t3Xg7q3fQ7FWbw/vcXtORTnyaSFscOtABg+7Pnz6YZ6c27fG1/aN8CjfwoUEUIdwqWQ==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "esast-util-from-js": "^2.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/recma-stringify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/recma-stringify/-/recma-stringify-1.0.0.tgz", + "integrity": "sha512-cjwII1MdIIVloKvC9ErQ+OgAtwHBmcZ0Bg4ciz78FtbT8In39aAYbaA7zvxQ61xVMSPE8WxhLwLbhif4Js2C+g==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-util-to-js": "^2.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -5160,6 +6666,68 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/rehype-recma": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/rehype-recma/-/rehype-recma-1.0.0.tgz", + "integrity": "sha512-lqA4rGUf1JmacCNWWZx0Wv1dHqMwxzsDWYMTowuplHF3xH0N/MmrZ/G3BDZnzAkRmxDadujCjaKM2hqYdCBOGw==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/hast": "^3.0.0", + "hast-util-to-estree": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-mdx": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/remark-mdx/-/remark-mdx-3.1.1.tgz", + "integrity": "sha512-Pjj2IYlUY3+D8x00UJsIOg5BEvfMyeI+2uLPn9VO9Wg4MEtN/VTIq2NEJQfde9PnX15KgtHyl9S0BcTnWrIuWg==", + "license": "MIT", + "dependencies": { + "mdast-util-mdx": "^3.0.0", + "micromark-extension-mdxjs": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-parse": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", + "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-rehype": { + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz", + "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "mdast-util-to-hast": "^13.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/resolve": { "version": "2.0.0-next.6", "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.6.tgz", @@ -5300,6 +6868,19 @@ "integrity": "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==", "license": "MIT" }, + "node_modules/section-matter": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz", + "integrity": "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==", + "license": "MIT", + "dependencies": { + "extend-shallow": "^2.0.1", + "kind-of": "^6.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/semver": { "version": "7.8.0", "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", @@ -5506,6 +7087,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -5515,6 +7105,22 @@ "node": ">=0.10.0" } }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "license": "BSD-3-Clause" + }, "node_modules/stable-hash": { "version": "0.0.5", "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz", @@ -5649,6 +7255,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "license": "MIT", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/strip-bom": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", @@ -5659,6 +7279,15 @@ "node": ">=4" } }, + "node_modules/strip-bom-string": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz", + "integrity": "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -5672,6 +7301,24 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/style-to-js": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz", + "integrity": "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==", + "license": "MIT", + "dependencies": { + "style-to-object": "1.0.14" + } + }, + "node_modules/style-to-object": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.14.tgz", + "integrity": "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==", + "license": "MIT", + "dependencies": { + "inline-style-parser": "0.2.7" + } + }, "node_modules/styled-jsx": { "version": "5.1.6", "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", @@ -5814,6 +7461,26 @@ "node": ">=8.0" } }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/trough": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", + "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/ts-api-utils": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", @@ -5978,6 +7645,121 @@ "dev": true, "license": "MIT" }, + "node_modules/unified": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", + "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-is": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position-from-estree": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position-from-estree/-/unist-util-position-from-estree-2.0.0.tgz", + "integrity": "sha512-KaFVRjoqLyF6YXCbVLNad/eS4+OfPQQn2yOd7zF/h5T/CSL2v8NpN6a5TPvtbXthAGw5nG+PuTtq+DdIZr+cRQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-remove": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-remove/-/unist-util-remove-4.0.0.tgz", + "integrity": "sha512-b4gokeGId57UVRX/eVKej5gXqGlc9+trkORhFJpu9raqZkZhU0zm8Doi05+HaiBsMEIJowL+2WtQ5ItjsngPXg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz", + "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", + "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/unrs-resolver": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.11.1.tgz", @@ -6023,6 +7805,48 @@ "punycode": "^2.1.0" } }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-matter": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/vfile-matter/-/vfile-matter-5.0.1.tgz", + "integrity": "sha512-o6roP82AiX0XfkyTHyRCMXgHfltUNlXSEqCIS80f+mbAyiQBE2fxtDVMtseyytGx75sihiJFo/zR6r/4LTs2Cw==", + "license": "MIT", + "dependencies": { + "vfile": "^6.0.0", + "yaml": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -6138,6 +7962,21 @@ "node": ">=0.10.0" } }, + "node_modules/yaml": { + "version": "2.8.4", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.4.tgz", + "integrity": "sha512-ml/JPOj9fOQK8RNnWojA67GbZ0ApXAUlN2UQclwv2eVgTgn7O9gg9o7paZWKMp4g0H3nTLtS9LVzhkpOFIKzog==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", @@ -6150,6 +7989,16 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } } } } diff --git a/web/package.json b/web/package.json index d589c07..3657fd4 100644 --- a/web/package.json +++ b/web/package.json @@ -12,8 +12,10 @@ "dependencies": { "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "gray-matter": "^4.0.3", "lucide-react": "^0.469.0", "next": "^15.5.18", + "next-mdx-remote": "^6.0.0", "react": "19.0.0", "react-dom": "19.0.0", "tailwind-merge": "^2.6.0" diff --git a/web/src/app/blog/[slug]/opengraph-image.tsx b/web/src/app/blog/[slug]/opengraph-image.tsx new file mode 100644 index 0000000..3dbd374 --- /dev/null +++ b/web/src/app/blog/[slug]/opengraph-image.tsx @@ -0,0 +1,32 @@ +import { ImageResponse } from "next/og"; +import { OG_CONTENT_TYPE, OG_SIZE, OgFrame } from "@/lib/og"; +import { getAllPostSlugs, getPostBySlug } from "@/lib/blog"; + +// nodejs runtime is required because getPostBySlug reads the MDX +// from disk via fs. The route is statically prerendered at build via +// generateStaticParams, so the runtime choice doesn't affect cost +// at request time. +export const runtime = "nodejs"; +export const alt = "SQLRite blog post"; +export const size = OG_SIZE; +export const contentType = OG_CONTENT_TYPE; + +export function generateStaticParams() { + return getAllPostSlugs().map((slug) => ({ slug })); +} + +export default async function OgImage({ + params, +}: { + params: Promise<{ slug: string }>; +}) { + const { slug } = await params; + const post = getPostBySlug(slug); + const title = post?.frontmatter.title ?? "SQLRite blog"; + const subtitle = post?.frontmatter.description; + + return new ImageResponse( + , + { ...size }, + ); +} diff --git a/web/src/app/blog/[slug]/page.tsx b/web/src/app/blog/[slug]/page.tsx new file mode 100644 index 0000000..8e5c0c3 --- /dev/null +++ b/web/src/app/blog/[slug]/page.tsx @@ -0,0 +1,232 @@ +import Link from "next/link"; +import type { Metadata } from "next"; +import { notFound } from "next/navigation"; +import { Footer } from "@/components/footer"; +import { Nav } from "@/components/nav"; +import { BlogMDX } from "@/components/blog-mdx"; +import { SITE } from "@/lib/site"; +import { + formatDate, + getAllPostSlugs, + getAllPosts, + getPostBySlug, + tagToSlug, +} from "@/lib/blog"; + +type RouteParams = { params: Promise<{ slug: string }> }; + +export function generateStaticParams() { + return getAllPostSlugs().map((slug) => ({ slug })); +} + +export async function generateMetadata({ + params, +}: RouteParams): Promise { + const { slug } = await params; + const post = getPostBySlug(slug); + if (!post) return {}; + + const url = `${SITE.url}/blog/${post.slug}`; + return { + title: post.frontmatter.title, + description: post.frontmatter.description, + authors: [{ name: post.frontmatter.author }], + keywords: post.frontmatter.tags, + alternates: { canonical: `/blog/${post.slug}` }, + openGraph: { + type: "article", + siteName: "SQLRite", + locale: "en_US", + url, + title: post.frontmatter.title, + description: post.frontmatter.description, + publishedTime: post.frontmatter.publishedAt, + modifiedTime: post.frontmatter.updatedAt, + authors: [post.frontmatter.author], + tags: post.frontmatter.tags, + }, + twitter: { + card: "summary_large_image", + site: SITE.twitterHandle, + creator: SITE.twitterHandle, + title: post.frontmatter.title, + description: post.frontmatter.description, + }, + }; +} + +export default async function BlogPostPage({ params }: RouteParams) { + const { slug } = await params; + const post = getPostBySlug(slug); + if (!post) notFound(); + + const url = `${SITE.url}/blog/${post.slug}`; + const allPosts = getAllPosts(); + const idx = allPosts.findIndex((p) => p.slug === post.slug); + // Posts sort newest-first, so idx-1 is newer and idx+1 is older. + const newer = idx > 0 ? allPosts[idx - 1] : null; + const older = + idx >= 0 && idx < allPosts.length - 1 ? allPosts[idx + 1] : null; + + const articleJsonLd = { + "@context": "https://schema.org", + "@type": "BlogPosting", + headline: post.frontmatter.title, + description: post.frontmatter.description, + datePublished: post.frontmatter.publishedAt, + dateModified: post.frontmatter.updatedAt ?? post.frontmatter.publishedAt, + author: { + "@type": "Person", + name: post.frontmatter.author, + url: SITE.socials.github, + }, + publisher: { + "@type": "Organization", + name: "SQLRite", + url: SITE.url, + }, + mainEntityOfPage: { + "@type": "WebPage", + "@id": url, + }, + url, + image: `${url}/opengraph-image`, + keywords: post.frontmatter.tags.join(", "), + }; + + const breadcrumbJsonLd = { + "@context": "https://schema.org", + "@type": "BreadcrumbList", + itemListElement: [ + { "@type": "ListItem", position: 1, name: "Home", item: SITE.url }, + { + "@type": "ListItem", + position: 2, + name: "Blog", + item: `${SITE.url}/blog`, + }, + { + "@type": "ListItem", + position: 3, + name: post.frontmatter.title, + item: url, + }, + ], + }; + + return ( + <> +