From 5a7d689b29a1950ee4de4420dbd04b7697b21658 Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Thu, 4 Jun 2026 10:06:28 +0100 Subject: [PATCH] Add JS spec generator for codama-nodes link nodes This PR introduces a JS-driven spec generator under `spec-generators/` that regenerates parts of `codama-nodes` from `@codama/spec`, mirroring the JS monorepo's `@codama/fragments`/RenderMap architecture. It covers the link-node category: each leaf node is emitted to `codama-nodes/src/generated/link_nodes/`, and the `LinkNode` union enum (plus its `HasName` dispatch) is generated from the flattened spec union. Hand-written link-node files shrink to test-only modules. The generator is a standalone pnpm project (tsup build, vitest, oxlint/oxfmt) wired into CI via a new job that runs lint, tests, and a `generate` + `git diff --exit-code` round-trip check. --- .github/workflows/main.yml | 48 + .../generated/link_nodes/account_link_node.rs | 24 + .../link_nodes/defined_type_link_node.rs | 24 + .../instruction_account_link_node.rs | 24 + .../instruction_argument_link_node.rs | 24 + .../link_nodes/instruction_link_node.rs | 24 + .../src/generated/link_nodes/link_node.rs | 30 + codama-nodes/src/generated/link_nodes/mod.rs | 17 + .../src/generated/link_nodes/pda_link_node.rs | 24 + .../generated/link_nodes/program_link_node.rs | 20 + codama-nodes/src/generated/mod.rs | 3 + codama-nodes/src/lib.rs | 3 +- .../src/link_nodes/account_link_node.rs | 25 +- .../src/link_nodes/defined_type_link_node.rs | 25 +- .../instruction_account_link_node.rs | 25 +- .../instruction_argument_link_node.rs | 25 +- .../src/link_nodes/instruction_link_node.rs | 25 +- codama-nodes/src/link_nodes/link_node.rs | 34 +- codama-nodes/src/link_nodes/mod.rs | 9 - codama-nodes/src/link_nodes/pda_link_node.rs | 25 +- .../src/link_nodes/program_link_node.rs | 21 +- spec-generators/.gitignore | 3 + spec-generators/README.md | 80 + spec-generators/bin/generate.ts | 18 + spec-generators/oxfmt.config.ts | 7 + spec-generators/oxlint.config.ts | 11 + spec-generators/package.json | 34 + spec-generators/pnpm-lock.yaml | 2484 +++++++++++++++++ spec-generators/src/defaults.ts | 77 + .../src/fragments/attributeBodyLine.ts | 66 + spec-generators/src/fragments/fromImpl.ts | 18 + spec-generators/src/fragments/hasNameImpl.ts | 54 + spec-generators/src/fragments/helpers.ts | 28 + spec-generators/src/fragments/index.ts | 10 + spec-generators/src/fragments/modPage.ts | 70 + spec-generators/src/fragments/nodePage.ts | 24 + .../src/fragments/nodeStructFragment.ts | 50 + spec-generators/src/fragments/page.ts | 41 + spec-generators/src/fragments/typeExpr.ts | 100 + spec-generators/src/fragments/unionPage.ts | 64 + spec-generators/src/index.ts | 114 + spec-generators/src/options.ts | 58 + spec-generators/src/repoDirectory.ts | 14 + spec-generators/src/unions.ts | 55 + .../test/fragments/attributeBodyLine.test.ts | 37 + .../test/fragments/fromImpl.test.ts | 33 + .../test/fragments/hasNameImpl.test.ts | 95 + .../test/fragments/modPage.test.ts | 57 + .../test/fragments/nodePage.test.ts | 38 + .../test/fragments/nodeStructFragment.test.ts | 60 + spec-generators/test/fragments/page.test.ts | 42 + .../test/fragments/typeExpr.test.ts | 120 + .../test/fragments/unionPage.test.ts | 50 + spec-generators/test/generate.test.ts | 92 + spec-generators/test/scope.test.ts | 29 + spec-generators/test/unions.test.ts | 51 + spec-generators/tsconfig.json | 24 + spec-generators/tsup.config.ts | 30 + spec-generators/vitest.config.ts | 7 + 59 files changed, 4517 insertions(+), 207 deletions(-) create mode 100644 codama-nodes/src/generated/link_nodes/account_link_node.rs create mode 100644 codama-nodes/src/generated/link_nodes/defined_type_link_node.rs create mode 100644 codama-nodes/src/generated/link_nodes/instruction_account_link_node.rs create mode 100644 codama-nodes/src/generated/link_nodes/instruction_argument_link_node.rs create mode 100644 codama-nodes/src/generated/link_nodes/instruction_link_node.rs create mode 100644 codama-nodes/src/generated/link_nodes/link_node.rs create mode 100644 codama-nodes/src/generated/link_nodes/mod.rs create mode 100644 codama-nodes/src/generated/link_nodes/pda_link_node.rs create mode 100644 codama-nodes/src/generated/link_nodes/program_link_node.rs create mode 100644 codama-nodes/src/generated/mod.rs create mode 100644 spec-generators/.gitignore create mode 100644 spec-generators/README.md create mode 100644 spec-generators/bin/generate.ts create mode 100644 spec-generators/oxfmt.config.ts create mode 100644 spec-generators/oxlint.config.ts create mode 100644 spec-generators/package.json create mode 100644 spec-generators/pnpm-lock.yaml create mode 100644 spec-generators/src/defaults.ts create mode 100644 spec-generators/src/fragments/attributeBodyLine.ts create mode 100644 spec-generators/src/fragments/fromImpl.ts create mode 100644 spec-generators/src/fragments/hasNameImpl.ts create mode 100644 spec-generators/src/fragments/helpers.ts create mode 100644 spec-generators/src/fragments/index.ts create mode 100644 spec-generators/src/fragments/modPage.ts create mode 100644 spec-generators/src/fragments/nodePage.ts create mode 100644 spec-generators/src/fragments/nodeStructFragment.ts create mode 100644 spec-generators/src/fragments/page.ts create mode 100644 spec-generators/src/fragments/typeExpr.ts create mode 100644 spec-generators/src/fragments/unionPage.ts create mode 100644 spec-generators/src/index.ts create mode 100644 spec-generators/src/options.ts create mode 100644 spec-generators/src/repoDirectory.ts create mode 100644 spec-generators/src/unions.ts create mode 100644 spec-generators/test/fragments/attributeBodyLine.test.ts create mode 100644 spec-generators/test/fragments/fromImpl.test.ts create mode 100644 spec-generators/test/fragments/hasNameImpl.test.ts create mode 100644 spec-generators/test/fragments/modPage.test.ts create mode 100644 spec-generators/test/fragments/nodePage.test.ts create mode 100644 spec-generators/test/fragments/nodeStructFragment.test.ts create mode 100644 spec-generators/test/fragments/page.test.ts create mode 100644 spec-generators/test/fragments/typeExpr.test.ts create mode 100644 spec-generators/test/fragments/unionPage.test.ts create mode 100644 spec-generators/test/generate.test.ts create mode 100644 spec-generators/test/scope.test.ts create mode 100644 spec-generators/test/unions.test.ts create mode 100644 spec-generators/tsconfig.json create mode 100644 spec-generators/tsup.config.ts create mode 100644 spec-generators/vitest.config.ts diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 79b5e56d..8b78100e 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -44,3 +44,51 @@ jobs: - name: Run Cargo test run: cargo +${{ env.RUST_TOOLCHAIN }} test --all-features + + spec-generators: + name: Verify Generated Code Is Up To Date + runs-on: ubuntu-latest + steps: + - name: Git Checkout + uses: actions/checkout@v4 + + - name: Install pnpm + uses: pnpm/action-setup@v4 + with: + version: 10 + + - name: Install Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: pnpm + cache-dependency-path: spec-generators/pnpm-lock.yaml + + - name: Install Rust Toolchain + uses: dtolnay/rust-toolchain@master + with: + toolchain: ${{ env.RUST_TOOLCHAIN }} + components: rustfmt + + - name: Install JS dependencies + working-directory: spec-generators + run: pnpm install --frozen-lockfile + + - name: Lint JS generator + working-directory: spec-generators + run: pnpm lint + + - name: Type-check + unit-test JS generator + working-directory: spec-generators + run: pnpm test + + - name: Regenerate Rust sources from spec + working-directory: spec-generators + run: pnpm generate + + - name: Fail if generated sources are out of date + run: | + if ! git diff --exit-code; then + echo "::error::Generated sources are out of date. Run \`pnpm generate\` inside spec-generators/ and commit the diff." + exit 1 + fi diff --git a/codama-nodes/src/generated/link_nodes/account_link_node.rs b/codama-nodes/src/generated/link_nodes/account_link_node.rs new file mode 100644 index 00000000..9a807aae --- /dev/null +++ b/codama-nodes/src/generated/link_nodes/account_link_node.rs @@ -0,0 +1,24 @@ +use crate::{CamelCaseString, HasName, ProgramLinkNode}; +use codama_nodes_derive::node; + +#[node] +pub struct AccountLinkNode { + // Data. + pub name: CamelCaseString, + + // Children. + #[serde(skip_serializing_if = "crate::is_default")] + pub program: Option, +} + +impl From for crate::Node { + fn from(val: AccountLinkNode) -> Self { + crate::Node::Link(val.into()) + } +} + +impl HasName for AccountLinkNode { + fn name(&self) -> &CamelCaseString { + &self.name + } +} diff --git a/codama-nodes/src/generated/link_nodes/defined_type_link_node.rs b/codama-nodes/src/generated/link_nodes/defined_type_link_node.rs new file mode 100644 index 00000000..e3173160 --- /dev/null +++ b/codama-nodes/src/generated/link_nodes/defined_type_link_node.rs @@ -0,0 +1,24 @@ +use crate::{CamelCaseString, HasName, ProgramLinkNode}; +use codama_nodes_derive::node; + +#[node] +pub struct DefinedTypeLinkNode { + // Data. + pub name: CamelCaseString, + + // Children. + #[serde(skip_serializing_if = "crate::is_default")] + pub program: Option, +} + +impl From for crate::Node { + fn from(val: DefinedTypeLinkNode) -> Self { + crate::Node::Link(val.into()) + } +} + +impl HasName for DefinedTypeLinkNode { + fn name(&self) -> &CamelCaseString { + &self.name + } +} diff --git a/codama-nodes/src/generated/link_nodes/instruction_account_link_node.rs b/codama-nodes/src/generated/link_nodes/instruction_account_link_node.rs new file mode 100644 index 00000000..c147953c --- /dev/null +++ b/codama-nodes/src/generated/link_nodes/instruction_account_link_node.rs @@ -0,0 +1,24 @@ +use crate::{CamelCaseString, HasName, InstructionLinkNode}; +use codama_nodes_derive::node; + +#[node] +pub struct InstructionAccountLinkNode { + // Data. + pub name: CamelCaseString, + + // Children. + #[serde(skip_serializing_if = "crate::is_default")] + pub instruction: Option, +} + +impl From for crate::Node { + fn from(val: InstructionAccountLinkNode) -> Self { + crate::Node::Link(val.into()) + } +} + +impl HasName for InstructionAccountLinkNode { + fn name(&self) -> &CamelCaseString { + &self.name + } +} diff --git a/codama-nodes/src/generated/link_nodes/instruction_argument_link_node.rs b/codama-nodes/src/generated/link_nodes/instruction_argument_link_node.rs new file mode 100644 index 00000000..e704e818 --- /dev/null +++ b/codama-nodes/src/generated/link_nodes/instruction_argument_link_node.rs @@ -0,0 +1,24 @@ +use crate::{CamelCaseString, HasName, InstructionLinkNode}; +use codama_nodes_derive::node; + +#[node] +pub struct InstructionArgumentLinkNode { + // Data. + pub name: CamelCaseString, + + // Children. + #[serde(skip_serializing_if = "crate::is_default")] + pub instruction: Option, +} + +impl From for crate::Node { + fn from(val: InstructionArgumentLinkNode) -> Self { + crate::Node::Link(val.into()) + } +} + +impl HasName for InstructionArgumentLinkNode { + fn name(&self) -> &CamelCaseString { + &self.name + } +} diff --git a/codama-nodes/src/generated/link_nodes/instruction_link_node.rs b/codama-nodes/src/generated/link_nodes/instruction_link_node.rs new file mode 100644 index 00000000..39fdb0cb --- /dev/null +++ b/codama-nodes/src/generated/link_nodes/instruction_link_node.rs @@ -0,0 +1,24 @@ +use crate::{CamelCaseString, HasName, ProgramLinkNode}; +use codama_nodes_derive::node; + +#[node] +pub struct InstructionLinkNode { + // Data. + pub name: CamelCaseString, + + // Children. + #[serde(skip_serializing_if = "crate::is_default")] + pub program: Option, +} + +impl From for crate::Node { + fn from(val: InstructionLinkNode) -> Self { + crate::Node::Link(val.into()) + } +} + +impl HasName for InstructionLinkNode { + fn name(&self) -> &CamelCaseString { + &self.name + } +} diff --git a/codama-nodes/src/generated/link_nodes/link_node.rs b/codama-nodes/src/generated/link_nodes/link_node.rs new file mode 100644 index 00000000..d00afd7d --- /dev/null +++ b/codama-nodes/src/generated/link_nodes/link_node.rs @@ -0,0 +1,30 @@ +use crate::{ + AccountLinkNode, CamelCaseString, DefinedTypeLinkNode, HasName, InstructionAccountLinkNode, + InstructionArgumentLinkNode, InstructionLinkNode, PdaLinkNode, ProgramLinkNode, +}; +use codama_nodes_derive::node_union; + +#[node_union] +pub enum LinkNode { + Account(AccountLinkNode), + DefinedType(DefinedTypeLinkNode), + Instruction(InstructionLinkNode), + InstructionAccount(InstructionAccountLinkNode), + InstructionArgument(InstructionArgumentLinkNode), + Pda(PdaLinkNode), + Program(ProgramLinkNode), +} + +impl HasName for LinkNode { + fn name(&self) -> &CamelCaseString { + match self { + LinkNode::Account(node) => node.name(), + LinkNode::DefinedType(node) => node.name(), + LinkNode::Instruction(node) => node.name(), + LinkNode::InstructionAccount(node) => node.name(), + LinkNode::InstructionArgument(node) => node.name(), + LinkNode::Pda(node) => node.name(), + LinkNode::Program(node) => node.name(), + } + } +} diff --git a/codama-nodes/src/generated/link_nodes/mod.rs b/codama-nodes/src/generated/link_nodes/mod.rs new file mode 100644 index 00000000..58122759 --- /dev/null +++ b/codama-nodes/src/generated/link_nodes/mod.rs @@ -0,0 +1,17 @@ +mod account_link_node; +mod defined_type_link_node; +mod instruction_account_link_node; +mod instruction_argument_link_node; +mod instruction_link_node; +mod link_node; +mod pda_link_node; +mod program_link_node; + +pub use account_link_node::*; +pub use defined_type_link_node::*; +pub use instruction_account_link_node::*; +pub use instruction_argument_link_node::*; +pub use instruction_link_node::*; +pub use link_node::*; +pub use pda_link_node::*; +pub use program_link_node::*; diff --git a/codama-nodes/src/generated/link_nodes/pda_link_node.rs b/codama-nodes/src/generated/link_nodes/pda_link_node.rs new file mode 100644 index 00000000..2167c452 --- /dev/null +++ b/codama-nodes/src/generated/link_nodes/pda_link_node.rs @@ -0,0 +1,24 @@ +use crate::{CamelCaseString, HasName, ProgramLinkNode}; +use codama_nodes_derive::node; + +#[node] +pub struct PdaLinkNode { + // Data. + pub name: CamelCaseString, + + // Children. + #[serde(skip_serializing_if = "crate::is_default")] + pub program: Option, +} + +impl From for crate::Node { + fn from(val: PdaLinkNode) -> Self { + crate::Node::Link(val.into()) + } +} + +impl HasName for PdaLinkNode { + fn name(&self) -> &CamelCaseString { + &self.name + } +} diff --git a/codama-nodes/src/generated/link_nodes/program_link_node.rs b/codama-nodes/src/generated/link_nodes/program_link_node.rs new file mode 100644 index 00000000..df495b49 --- /dev/null +++ b/codama-nodes/src/generated/link_nodes/program_link_node.rs @@ -0,0 +1,20 @@ +use crate::{CamelCaseString, HasName}; +use codama_nodes_derive::node; + +#[node] +pub struct ProgramLinkNode { + // Data. + pub name: CamelCaseString, +} + +impl From for crate::Node { + fn from(val: ProgramLinkNode) -> Self { + crate::Node::Link(val.into()) + } +} + +impl HasName for ProgramLinkNode { + fn name(&self) -> &CamelCaseString { + &self.name + } +} diff --git a/codama-nodes/src/generated/mod.rs b/codama-nodes/src/generated/mod.rs new file mode 100644 index 00000000..a9cba954 --- /dev/null +++ b/codama-nodes/src/generated/mod.rs @@ -0,0 +1,3 @@ +mod link_nodes; + +pub use link_nodes::*; diff --git a/codama-nodes/src/lib.rs b/codama-nodes/src/lib.rs index 566c2eaa..a28f4d60 100644 --- a/codama-nodes/src/lib.rs +++ b/codama-nodes/src/lib.rs @@ -8,6 +8,7 @@ mod defined_type_node; mod discriminator_nodes; mod error_node; mod event_node; +mod generated; mod instruction_account_node; mod instruction_argument_node; mod instruction_byte_delta_node; @@ -33,13 +34,13 @@ pub use defined_type_node::*; pub use discriminator_nodes::*; pub use error_node::*; pub use event_node::*; +pub use generated::*; pub use instruction_account_node::*; pub use instruction_argument_node::*; pub use instruction_byte_delta_node::*; pub use instruction_node::*; pub use instruction_remaining_accounts_node::*; pub use instruction_status_node::*; -pub use link_nodes::*; pub use node::*; pub use pda_node::*; pub use pda_seed_nodes::*; diff --git a/codama-nodes/src/link_nodes/account_link_node.rs b/codama-nodes/src/link_nodes/account_link_node.rs index 4ae248a7..a83eed2b 100644 --- a/codama-nodes/src/link_nodes/account_link_node.rs +++ b/codama-nodes/src/link_nodes/account_link_node.rs @@ -1,21 +1,4 @@ -use crate::{CamelCaseString, HasName, ProgramLinkNode}; -use codama_nodes_derive::node; - -#[node] -pub struct AccountLinkNode { - // Data. - pub name: CamelCaseString, - - // Children. - #[serde(skip_serializing_if = "crate::is_default")] - pub program: Option, -} - -impl From for crate::Node { - fn from(val: AccountLinkNode) -> Self { - crate::Node::Link(val.into()) - } -} +use crate::{AccountLinkNode, CamelCaseString, ProgramLinkNode}; impl AccountLinkNode { pub fn new(name: T) -> Self @@ -51,12 +34,6 @@ impl From<&str> for AccountLinkNode { } } -impl HasName for AccountLinkNode { - fn name(&self) -> &CamelCaseString { - &self.name - } -} - #[cfg(test)] mod tests { use super::*; diff --git a/codama-nodes/src/link_nodes/defined_type_link_node.rs b/codama-nodes/src/link_nodes/defined_type_link_node.rs index 48ff80ef..f6d8f581 100644 --- a/codama-nodes/src/link_nodes/defined_type_link_node.rs +++ b/codama-nodes/src/link_nodes/defined_type_link_node.rs @@ -1,21 +1,4 @@ -use crate::{CamelCaseString, HasName, ProgramLinkNode}; -use codama_nodes_derive::node; - -#[node] -pub struct DefinedTypeLinkNode { - // Data. - pub name: CamelCaseString, - - // Children. - #[serde(skip_serializing_if = "crate::is_default")] - pub program: Option, -} - -impl From for crate::Node { - fn from(val: DefinedTypeLinkNode) -> Self { - crate::Node::Link(val.into()) - } -} +use crate::{CamelCaseString, DefinedTypeLinkNode, ProgramLinkNode}; impl DefinedTypeLinkNode { pub fn new(name: T) -> Self @@ -51,12 +34,6 @@ impl From<&str> for DefinedTypeLinkNode { } } -impl HasName for DefinedTypeLinkNode { - fn name(&self) -> &CamelCaseString { - &self.name - } -} - #[cfg(test)] mod tests { use super::*; diff --git a/codama-nodes/src/link_nodes/instruction_account_link_node.rs b/codama-nodes/src/link_nodes/instruction_account_link_node.rs index e22c0c39..be06bd2e 100644 --- a/codama-nodes/src/link_nodes/instruction_account_link_node.rs +++ b/codama-nodes/src/link_nodes/instruction_account_link_node.rs @@ -1,21 +1,4 @@ -use crate::{CamelCaseString, HasName, InstructionLinkNode}; -use codama_nodes_derive::node; - -#[node] -pub struct InstructionAccountLinkNode { - // Data. - pub name: CamelCaseString, - - // Children. - #[serde(skip_serializing_if = "crate::is_default")] - pub instruction: Option, -} - -impl From for crate::Node { - fn from(val: InstructionAccountLinkNode) -> Self { - crate::Node::Link(val.into()) - } -} +use crate::{CamelCaseString, InstructionAccountLinkNode, InstructionLinkNode}; impl InstructionAccountLinkNode { pub fn new(name: T) -> Self @@ -51,12 +34,6 @@ impl From<&str> for InstructionAccountLinkNode { } } -impl HasName for InstructionAccountLinkNode { - fn name(&self) -> &CamelCaseString { - &self.name - } -} - #[cfg(test)] mod tests { use crate::ProgramLinkNode; diff --git a/codama-nodes/src/link_nodes/instruction_argument_link_node.rs b/codama-nodes/src/link_nodes/instruction_argument_link_node.rs index 1636568d..e8c5651c 100644 --- a/codama-nodes/src/link_nodes/instruction_argument_link_node.rs +++ b/codama-nodes/src/link_nodes/instruction_argument_link_node.rs @@ -1,21 +1,4 @@ -use crate::{CamelCaseString, HasName, InstructionLinkNode}; -use codama_nodes_derive::node; - -#[node] -pub struct InstructionArgumentLinkNode { - // Data. - pub name: CamelCaseString, - - // Children. - #[serde(skip_serializing_if = "crate::is_default")] - pub instruction: Option, -} - -impl From for crate::Node { - fn from(val: InstructionArgumentLinkNode) -> Self { - crate::Node::Link(val.into()) - } -} +use crate::{CamelCaseString, InstructionArgumentLinkNode, InstructionLinkNode}; impl InstructionArgumentLinkNode { pub fn new(name: T) -> Self @@ -51,12 +34,6 @@ impl From<&str> for InstructionArgumentLinkNode { } } -impl HasName for InstructionArgumentLinkNode { - fn name(&self) -> &CamelCaseString { - &self.name - } -} - #[cfg(test)] mod tests { use crate::ProgramLinkNode; diff --git a/codama-nodes/src/link_nodes/instruction_link_node.rs b/codama-nodes/src/link_nodes/instruction_link_node.rs index da06048a..d29f50ad 100644 --- a/codama-nodes/src/link_nodes/instruction_link_node.rs +++ b/codama-nodes/src/link_nodes/instruction_link_node.rs @@ -1,21 +1,4 @@ -use crate::{CamelCaseString, HasName, ProgramLinkNode}; -use codama_nodes_derive::node; - -#[node] -pub struct InstructionLinkNode { - // Data. - pub name: CamelCaseString, - - // Children. - #[serde(skip_serializing_if = "crate::is_default")] - pub program: Option, -} - -impl From for crate::Node { - fn from(val: InstructionLinkNode) -> Self { - crate::Node::Link(val.into()) - } -} +use crate::{CamelCaseString, InstructionLinkNode, ProgramLinkNode}; impl InstructionLinkNode { pub fn new(name: T) -> Self @@ -51,12 +34,6 @@ impl From<&str> for InstructionLinkNode { } } -impl HasName for InstructionLinkNode { - fn name(&self) -> &CamelCaseString { - &self.name - } -} - #[cfg(test)] mod tests { use super::*; diff --git a/codama-nodes/src/link_nodes/link_node.rs b/codama-nodes/src/link_nodes/link_node.rs index 9541523d..d55fef47 100644 --- a/codama-nodes/src/link_nodes/link_node.rs +++ b/codama-nodes/src/link_nodes/link_node.rs @@ -1,38 +1,6 @@ -use crate::{ - AccountLinkNode, CamelCaseString, DefinedTypeLinkNode, HasName, InstructionAccountLinkNode, - InstructionArgumentLinkNode, InstructionLinkNode, PdaLinkNode, ProgramLinkNode, -}; -use codama_nodes_derive::node_union; - -#[node_union] -pub enum LinkNode { - Account(AccountLinkNode), - DefinedType(DefinedTypeLinkNode), - Instruction(InstructionLinkNode), - InstructionAccount(InstructionAccountLinkNode), - InstructionArgument(InstructionArgumentLinkNode), - Pda(PdaLinkNode), - Program(ProgramLinkNode), -} - -impl HasName for LinkNode { - fn name(&self) -> &CamelCaseString { - match self { - LinkNode::Account(node) => node.name(), - LinkNode::DefinedType(node) => node.name(), - LinkNode::Instruction(node) => node.name(), - LinkNode::InstructionAccount(node) => node.name(), - LinkNode::InstructionArgument(node) => node.name(), - LinkNode::Pda(node) => node.name(), - LinkNode::Program(node) => node.name(), - } - } -} - #[cfg(test)] mod tests { - use super::*; - use crate::HasKind; + use crate::{HasKind, LinkNode, ProgramLinkNode}; #[test] fn kind() { diff --git a/codama-nodes/src/link_nodes/mod.rs b/codama-nodes/src/link_nodes/mod.rs index 58122759..20f2c404 100644 --- a/codama-nodes/src/link_nodes/mod.rs +++ b/codama-nodes/src/link_nodes/mod.rs @@ -6,12 +6,3 @@ mod instruction_link_node; mod link_node; mod pda_link_node; mod program_link_node; - -pub use account_link_node::*; -pub use defined_type_link_node::*; -pub use instruction_account_link_node::*; -pub use instruction_argument_link_node::*; -pub use instruction_link_node::*; -pub use link_node::*; -pub use pda_link_node::*; -pub use program_link_node::*; diff --git a/codama-nodes/src/link_nodes/pda_link_node.rs b/codama-nodes/src/link_nodes/pda_link_node.rs index d8c8c191..7533411a 100644 --- a/codama-nodes/src/link_nodes/pda_link_node.rs +++ b/codama-nodes/src/link_nodes/pda_link_node.rs @@ -1,21 +1,4 @@ -use crate::{CamelCaseString, HasName, ProgramLinkNode}; -use codama_nodes_derive::node; - -#[node] -pub struct PdaLinkNode { - // Data. - pub name: CamelCaseString, - - // Children. - #[serde(skip_serializing_if = "crate::is_default")] - pub program: Option, -} - -impl From for crate::Node { - fn from(val: PdaLinkNode) -> Self { - crate::Node::Link(val.into()) - } -} +use crate::{CamelCaseString, PdaLinkNode, ProgramLinkNode}; impl PdaLinkNode { pub fn new(name: T) -> Self @@ -51,12 +34,6 @@ impl From<&str> for PdaLinkNode { } } -impl HasName for PdaLinkNode { - fn name(&self) -> &CamelCaseString { - &self.name - } -} - #[cfg(test)] mod tests { use super::*; diff --git a/codama-nodes/src/link_nodes/program_link_node.rs b/codama-nodes/src/link_nodes/program_link_node.rs index 187c64af..5be8b1fb 100644 --- a/codama-nodes/src/link_nodes/program_link_node.rs +++ b/codama-nodes/src/link_nodes/program_link_node.rs @@ -1,17 +1,4 @@ -use crate::{CamelCaseString, HasName}; -use codama_nodes_derive::node; - -#[node] -pub struct ProgramLinkNode { - // Data. - pub name: CamelCaseString, -} - -impl From for crate::Node { - fn from(val: ProgramLinkNode) -> Self { - crate::Node::Link(val.into()) - } -} +use crate::{CamelCaseString, ProgramLinkNode}; impl ProgramLinkNode { pub fn new(name: T) -> Self @@ -34,12 +21,6 @@ impl From<&str> for ProgramLinkNode { } } -impl HasName for ProgramLinkNode { - fn name(&self) -> &CamelCaseString { - &self.name - } -} - #[cfg(test)] mod tests { use super::*; diff --git a/spec-generators/.gitignore b/spec-generators/.gitignore new file mode 100644 index 00000000..83631f81 --- /dev/null +++ b/spec-generators/.gitignore @@ -0,0 +1,3 @@ +dist/ +node_modules/ +*.tsbuildinfo diff --git a/spec-generators/README.md b/spec-generators/README.md new file mode 100644 index 00000000..9cc77371 --- /dev/null +++ b/spec-generators/README.md @@ -0,0 +1,80 @@ +# `@codama-internal/rust-spec-generators` + +Private project. Houses the code generator that turns the `@codama/spec` encoded specification into the source code of the Codama Rust monorepo crates. + +This project is **never published**. It is only invoked at development time, via `pnpm generate` from this directory, to regenerate specific subtrees of the Rust monorepo (today: `codama-nodes/src/generated/`). + +## Architecture + +The generator owns its full pipeline. It knows which spec major it targets, which output directory it writes to, and which compatibility knobs it needs. It does not take a spec as a parameter — it imports it directly from `@codama/spec`. + +The bin script (`bin/generate.ts`) wraps the top-level `generate()` function for `pnpm generate`. + +## Layout + +``` +src/ +├── index.ts # generate() + getRenderMap() (pure) +├── options.ts # RenderOptions, buildRenderScope, validateRenderOptions +├── defaults.ts # CATEGORY_DIRECTORIES, name overrides, CATEGORY_ROUTING +├── repoDirectory.ts # locates the Rust workspace root +├── unions.ts # flattenNodeUnion + getEmittableUnions +└── fragments/ + ├── helpers.ts # `use(name)` — Rust analogue of the JS `use(…)` helper + ├── typeExpr.ts # spec TypeExpr → Rust Fragment + ├── attributeBodyLine.ts + ├── structFragment.ts + ├── fromImpl.ts + ├── hasNameImpl.ts + ├── nodePage.ts + ├── unionPage.ts + ├── page.ts # use-block renderer (groups crate::*) + └── modPage.ts # Rust mod.rs renderer + +bin/generate.ts # pnpm generate entry; wraps generate() + +test/ +├── fragments/ # one .test.ts per fragment renderer +├── generate.test.ts # validateRenderOptions + render-map assertions +└── scope.test.ts # buildRenderScope tests +``` + +## RenderMap pipeline + +The generator builds an in-memory `RenderMap` — a map from output path to a fragment carrying content + an `ImportMap` — before touching the filesystem. + +Per spec entity, the generator emits a page fragment composed from focused sub-fragments (`structFragment`, `fromImpl`, `hasNameImpl`, `unionPage`, …). Each sub-fragment attaches its referenced identifiers as concrete `crate::Foo` imports via the shared `use(name)` helper in `fragments/helpers.ts`. + +The page renderer (`fragments/page.ts`) groups every `use crate::Foo;` line into a single `use crate::{…};` block, keeps non-`crate::` paths (e.g. `codama_nodes_derive::node`) on their own lines, and prepends the import block to the page body. + +The mod-page renderer (`fragments/modPage.ts`) walks every folder in the spec pages, emits a `mod.rs` listing the per-node files, and a root `mod.rs` listing the subdirectories. + +Finally, `generate()` calls `writeRenderMap` (from `@codama/fragments`) to flush the final map to disk, wiping the target directory first so stale files cannot survive. + +## Running + +```sh +pnpm install +pnpm generate +``` + +`pnpm generate` builds the orchestrator with `tsup`, runs the bundled script via `node ./dist/generate.mjs`, and then runs `cargo fmt -p codama-nodes` to keep the output rustfmt-clean. A CI job (`spec-generators` in `.github/workflows/main.yml`) runs the same pipeline and fails on `git diff --exit-code`, catching anyone who forgets to regenerate after editing the spec dep. + +## Scope (v1) + +This first iteration generates source files for nodes and unions in spec category `link`. Each generated per-node file contains the struct definition wrapped in `#[node]`, a `From for crate::Node` impl routing through the category's union variant, and a `HasName` impl when the node has a `name: stringIdentifier()` attribute. Each generated per-union file contains the union enum wrapped in `#[node_union]` plus a `HasName` impl when every member node has a `name` attribute. + +Other categories (`count`, `discriminator`, `pdaSeed`, `value`, `topLevel`, `contextualValue`, `type`) stay hand-written for now and will land in future PRs. Per-node constructors (`AccountLinkNode::new`, etc.), bespoke `TryFrom` impls, and `#[cfg(test)] mod tests` blocks also stay hand-written, in `codama-nodes/src/.rs` files that `use` the generated struct. + +Nested-union type aliases, enumerations, and the top-level `Node` registry stay hand-written. Expanding the generator's scope to cover them is a future PR. + +## Bumping `@codama/spec` + +1. Update the version pin in `package.json`. +2. `pnpm install`. +3. `pnpm generate`. +4. Review the diff under `codama-nodes/src/generated/`, run `cargo test --workspace`, fix any consumer fallout in the hand-written `codama-nodes/src/.rs` files. + +## Tests + +Tests mirror the source layout: one `test/fragments/.test.ts` per fragment renderer, plus a `test/scope.test.ts` for `buildRenderScope` and a `test/generate.test.ts` for option validation + render-map assertions via `getFromRenderMap` (no filesystem access). diff --git a/spec-generators/bin/generate.ts b/spec-generators/bin/generate.ts new file mode 100644 index 00000000..1e90ca51 --- /dev/null +++ b/spec-generators/bin/generate.ts @@ -0,0 +1,18 @@ +import { relative } from 'node:path'; +import process from 'node:process'; + +import { generate } from '../src/index'; +import { getRepoDirectory } from '../src/repoDirectory'; + +function main(): void { + const { outputDir } = generate(); + process.stdout.write(`generated → ${relative(getRepoDirectory(), outputDir)}\n`); +} + +try { + main(); +} catch (err: unknown) { + process.stderr.write(`spec-generators failed: ${err instanceof Error ? err.message : String(err)}\n`); + if (err instanceof Error && err.stack) process.stderr.write(`${err.stack}\n`); + process.exit(1); +} diff --git a/spec-generators/oxfmt.config.ts b/spec-generators/oxfmt.config.ts new file mode 100644 index 00000000..6e438a60 --- /dev/null +++ b/spec-generators/oxfmt.config.ts @@ -0,0 +1,7 @@ +import solanaFmt from '@solana-config/oxc/oxfmt'; +import { defineConfig } from 'oxfmt'; + +export default defineConfig({ + ...solanaFmt, + ignorePatterns: ['**/node_modules/', 'pnpm-lock.yaml'], +}); diff --git a/spec-generators/oxlint.config.ts b/spec-generators/oxlint.config.ts new file mode 100644 index 00000000..f01be47e --- /dev/null +++ b/spec-generators/oxlint.config.ts @@ -0,0 +1,11 @@ +import solanaConfig from '@solana-config/oxc/oxlint'; +import { defineConfig } from 'oxlint'; + +export default defineConfig({ + extends: [solanaConfig], + ignorePatterns: ['**/node_modules/', 'pnpm-lock.yaml'], + options: { typeAware: true }, + rules: { + 'sort-keys': 'off', + }, +}); diff --git a/spec-generators/package.json b/spec-generators/package.json new file mode 100644 index 00000000..20cc7257 --- /dev/null +++ b/spec-generators/package.json @@ -0,0 +1,34 @@ +{ + "name": "@codama-internal/rust-spec-generators", + "version": "0.0.0", + "private": true, + "description": "Private code generators that produce parts of the Codama Rust monorepo from the encoded `@codama/spec`", + "type": "module", + "scripts": { + "build": "rimraf dist && tsup", + "generate": "pnpm build && node ./dist/generate.mjs && (cd .. && cargo fmt -p codama-nodes)", + "lint": "oxlint && oxfmt --check .", + "lint:fix": "oxlint --fix && oxfmt .", + "test": "pnpm test:types && pnpm test:unit", + "test:types": "tsc --noEmit", + "test:unit": "vitest run" + }, + "dependencies": { + "@codama/fragments": "^0.1.0", + "@codama/spec": "^1.6.0-rc.6" + }, + "devDependencies": { + "@solana-config/oxc": "^0.1.1", + "@types/node": "^25", + "oxfmt": "^0.51.0", + "oxlint": "^1.66.0", + "oxlint-tsgolint": "^0.23.0", + "rimraf": "^6.1.3", + "tsup": "^8.5.1", + "typescript": "^5.9.3", + "vitest": "^4.0.16" + }, + "engines": { + "node": ">=22.12.0" + } +} diff --git a/spec-generators/pnpm-lock.yaml b/spec-generators/pnpm-lock.yaml new file mode 100644 index 00000000..62e508c6 --- /dev/null +++ b/spec-generators/pnpm-lock.yaml @@ -0,0 +1,2484 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@codama/fragments': + specifier: ^0.1.0 + version: 0.1.0 + '@codama/spec': + specifier: ^1.6.0-rc.6 + version: 1.6.0-rc.6 + devDependencies: + '@solana-config/oxc': + specifier: ^0.1.1 + version: 0.1.1(oxfmt@0.51.0)(oxlint@1.66.0(oxlint-tsgolint@0.23.0)) + '@types/node': + specifier: ^25 + version: 25.9.1 + oxfmt: + specifier: ^0.51.0 + version: 0.51.0 + oxlint: + specifier: ^1.66.0 + version: 1.66.0(oxlint-tsgolint@0.23.0) + oxlint-tsgolint: + specifier: ^0.23.0 + version: 0.23.0 + rimraf: + specifier: ^6.1.3 + version: 6.1.3 + tsup: + specifier: ^8.5.1 + version: 8.5.1(postcss@8.5.15)(tsx@4.22.3)(typescript@5.9.3) + typescript: + specifier: ^5.9.3 + version: 5.9.3 + vitest: + specifier: ^4.0.16 + version: 4.1.7(@types/node@25.9.1)(vite@8.0.14(@types/node@25.9.1)(esbuild@0.27.7)(tsx@4.22.3)) + +packages: + + '@codama/errors@1.7.0': + resolution: {integrity: sha512-N1E4LT3XRYqHHJAnL+eVQ5V3Pc0uSPjsn4Xt6QEO6Fz1p0slF6hbD+/axkFUc7+lNCAfgnkKzhk+6SpFLU/WrQ==} + hasBin: true + + '@codama/fragments@0.1.0': + resolution: {integrity: sha512-rWnSKw4UA9LS7mMQyzKnR1woibVEYrYgp66+i+JB+O1lnvU/i/KCH4TmoV+S6Y3ADL2WyznrwfDA3rBET6U5Cg==} + + '@codama/node-types@1.7.0': + resolution: {integrity: sha512-VDytcSgN6jOGEh4aJ1LgSnqCe1drSEUSdAeKOV92aU0MOYiDi2s2B18+Gx7dx40mex2GfVc3zamu7KGzTlxegA==} + + '@codama/spec@1.6.0-rc.6': + resolution: {integrity: sha512-5MmIkIBkYFBawkTHqY2NbGV0jOS4uDLeIbfsAP5laXMlEsPXjoI2Uzyp6OdxRN9cfthE+GEXN64ObaY7FrPQ5A==} + engines: {node: '>=22.12.0'} + + '@emnapi/core@1.10.0': + resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} + + '@emnapi/runtime@1.10.0': + resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} + + '@emnapi/wasi-threads@1.2.1': + resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} + + '@esbuild/aix-ppc64@0.27.7': + resolution: {integrity: sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/aix-ppc64@0.28.0': + resolution: {integrity: sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.27.7': + resolution: {integrity: sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm64@0.28.0': + resolution: {integrity: sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.27.7': + resolution: {integrity: sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-arm@0.28.0': + resolution: {integrity: sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.27.7': + resolution: {integrity: sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/android-x64@0.28.0': + resolution: {integrity: sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.27.7': + resolution: {integrity: sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-arm64@0.28.0': + resolution: {integrity: sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.27.7': + resolution: {integrity: sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/darwin-x64@0.28.0': + resolution: {integrity: sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.27.7': + resolution: {integrity: sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-arm64@0.28.0': + resolution: {integrity: sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.27.7': + resolution: {integrity: sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.28.0': + resolution: {integrity: sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.27.7': + resolution: {integrity: sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm64@0.28.0': + resolution: {integrity: sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.27.7': + resolution: {integrity: sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-arm@0.28.0': + resolution: {integrity: sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.27.7': + resolution: {integrity: sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-ia32@0.28.0': + resolution: {integrity: sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.27.7': + resolution: {integrity: sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-loong64@0.28.0': + resolution: {integrity: sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.27.7': + resolution: {integrity: sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-mips64el@0.28.0': + resolution: {integrity: sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.27.7': + resolution: {integrity: sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-ppc64@0.28.0': + resolution: {integrity: sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.27.7': + resolution: {integrity: sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-riscv64@0.28.0': + resolution: {integrity: sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.27.7': + resolution: {integrity: sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-s390x@0.28.0': + resolution: {integrity: sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.27.7': + resolution: {integrity: sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/linux-x64@0.28.0': + resolution: {integrity: sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.27.7': + resolution: {integrity: sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-arm64@0.28.0': + resolution: {integrity: sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.27.7': + resolution: {integrity: sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.28.0': + resolution: {integrity: sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.27.7': + resolution: {integrity: sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-arm64@0.28.0': + resolution: {integrity: sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.27.7': + resolution: {integrity: sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.28.0': + resolution: {integrity: sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.27.7': + resolution: {integrity: sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/openharmony-arm64@0.28.0': + resolution: {integrity: sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.27.7': + resolution: {integrity: sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/sunos-x64@0.28.0': + resolution: {integrity: sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.27.7': + resolution: {integrity: sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-arm64@0.28.0': + resolution: {integrity: sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.27.7': + resolution: {integrity: sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-ia32@0.28.0': + resolution: {integrity: sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.27.7': + resolution: {integrity: sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@esbuild/win32-x64@0.28.0': + resolution: {integrity: sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@napi-rs/wasm-runtime@1.1.4': + resolution: {integrity: sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==} + peerDependencies: + '@emnapi/core': ^1.7.1 + '@emnapi/runtime': ^1.7.1 + + '@oxc-project/types@0.132.0': + resolution: {integrity: sha512-FESMOxil5Se014ui/Eq8fT5uHJo6nIRwH0PfJrZJXs6Gek3ZVFOrpUv3YIZT20m+extU98Hg1Ym72U58rlsxUQ==} + + '@oxfmt/binding-android-arm-eabi@0.51.0': + resolution: {integrity: sha512-Ni0sCqg5CIHaLIYFGj+ncbcumylvNC6FE4rfD0KfdmnWHbPJ+zev0qZCXKxy2hFVa0fYRK0yPzf5nzPbkZou7g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + + '@oxfmt/binding-android-arm64@0.51.0': + resolution: {integrity: sha512-eu5lAZjuo0KAkp+M24EhDqfOwA8owQ8d7wyBlOUUGRbDLHpU3IRlDHp8Dif+YqGlxs6jra7yS6WQu/NkPhAxeg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@oxfmt/binding-darwin-arm64@0.51.0': + resolution: {integrity: sha512-6LsUNIdURhhcIfIn8+xsOb61mSTa9msAHTeSGx9Jf4rsP/gN8PGCF+SKWPAQZbND2w/WBkqQ6303jqEEIXzMdQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@oxfmt/binding-darwin-x64@0.51.0': + resolution: {integrity: sha512-9aUMGmVxdHjYMsEAW1tNRoieTJXlVNDFkRvIR1J7LttJXWjVYCu2ekclLij2KJtxBxSQOYSHd12ME/adVGVbZg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@oxfmt/binding-freebsd-x64@0.51.0': + resolution: {integrity: sha512-mkY1nhZTqYb+NHaAWxOCKISN6FwdrwMNsu17vTUA3wzUV2VJ+Paq15ZokRcsMU/2PUdHO73prxyeJpjXQ3MPpQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@oxfmt/binding-linux-arm-gnueabihf@0.51.0': + resolution: {integrity: sha512-wtFwNwE4+YCNuPaWoGDZeGsKvD6D1YSUNBJNn/rJBh7CrDBThFE+TBI5kY7vRW9rIOQRsbW2IpyyL3Du4Zqwiw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxfmt/binding-linux-arm-musleabihf@0.51.0': + resolution: {integrity: sha512-rnOaNx86G7iRKM6lsCIQMux0SMGNC/TEbFR+r7lpruJ12bnrIWgxd5w1PLqOvgR9r8ZJbpK/zfRKctJnh8/Jfg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxfmt/binding-linux-arm64-gnu@0.51.0': + resolution: {integrity: sha512-jOgDzSqWcICGRjsp4mc08FxKMN8vzP2Kgs4E0d2HUP99F+nJDQKklRV4Zuj+0gcBgjrzx2CbpqaIdUVPepCojA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + + '@oxfmt/binding-linux-arm64-musl@0.51.0': + resolution: {integrity: sha512-KBUCdrH5bwVrAvI9gU/1S55oH6fzXjr++J/oVocdu7bYTks1l7DNNT+rLd/1TDdAEjObGwmfWamn7LC1m8A0DQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + + '@oxfmt/binding-linux-ppc64-gnu@0.51.0': + resolution: {integrity: sha512-NapfjYsABFqTJ1Dn9Efq6sN5esaHconVKwVLbDGNQLrwpOx/g17mkwErHzU72PutL67nf3wNAkbq122H+zLxag==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + + '@oxfmt/binding-linux-riscv64-gnu@0.51.0': + resolution: {integrity: sha512-5dlDt1dUZCVi6elIhiK1PWg9wpTzTcIuj0IZnSurvIoMrhOWqqTcc1dSTxcSkNaBZhfsNqRZdINI1zAgbKkJNQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + + '@oxfmt/binding-linux-riscv64-musl@0.51.0': + resolution: {integrity: sha512-pgdWUJn0S5nulyiVdlFV8DzCUnGXkU99W5PSkkmbaZW+LrZBPxpezun4G0DDHbQaVYuJeCuKsXsGKGo77CkUTQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + + '@oxfmt/binding-linux-s390x-gnu@0.51.0': + resolution: {integrity: sha512-2XTFUe97CbDGAI8vjwDfZ1HdakO0XIADyJ24idEg64SC4/K4in/OisXVnrW4NMK7I6TgC7EqRhC0Ln/nKhAemA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + + '@oxfmt/binding-linux-x64-gnu@0.51.0': + resolution: {integrity: sha512-kQ1OuCqqt/yyf0ZN9VFxW1/JnlgJgii3Dr7pWf9vNBvrX1hv6g39/+mc5oGRHRGJFZtl3zsGDWR9c5N2B/gwBw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + + '@oxfmt/binding-linux-x64-musl@0.51.0': + resolution: {integrity: sha512-ARTYqxHF475o96Gbn41hvSWSSRygPlRDXZZgZ9I2scU1y0qiWpCQyZCoefaQa0mwv+wwtZ+luS4YOzsRzM/izg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + + '@oxfmt/binding-openharmony-arm64@0.51.0': + resolution: {integrity: sha512-QiC1XrCl6a6BmqMzduO8hdIRMf1m44hCkt2Q68KWkTvUB/E7fd2iomyNh6KnnRca5w6eBrRAAtLFqTh+xjsjJA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@oxfmt/binding-win32-arm64-msvc@0.51.0': + resolution: {integrity: sha512-NC/hJb9dtU23Zf8L7IVK95xnFjiQ7AfcLO2l5pb69TDEr958qxrtnB2CveeeNSCBFNIkgaTCfd/vHNSoG78l9g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@oxfmt/binding-win32-ia32-msvc@0.51.0': + resolution: {integrity: sha512-2C45za4Rj36n8YIbhRL1PQbxmXJYf81WEcAgvj5I4ptRROG+A+81hREEN5bmCHADE1UfYaN312U6tkILoZZy6w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ia32] + os: [win32] + + '@oxfmt/binding-win32-x64-msvc@0.51.0': + resolution: {integrity: sha512-73RqdAuVKQTkjZIDw08JaDHUM4lav5Qu+CaPwg4QbbA7k8o7LEW0p3UsfZ/F8dsO/pwVYh3RzFcanwLRTTahbQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@oxlint-tsgolint/darwin-arm64@0.23.0': + resolution: {integrity: sha512-gOs9PVr2wEg4ox9z0aJo+RKhhImW86YL5N6yav8BK/rgPsIrwN/igSZ+pbRr723NFvUNKde9fgMhRA6JrXAOZw==} + cpu: [arm64] + os: [darwin] + + '@oxlint-tsgolint/darwin-x64@0.23.0': + resolution: {integrity: sha512-kjJ8B+7n4tB9VJdxS5A9GdJt6/bYpzbu4lXp2uO1S3sRmCB5gDEABlGoiePNApRWaW+xqL4b4xgiE727jSLhuA==} + cpu: [x64] + os: [darwin] + + '@oxlint-tsgolint/linux-arm64@0.23.0': + resolution: {integrity: sha512-6dCZuKNu135seMXilkRk9SpCx6i1XgmiipYGalLij5WVRX6ZYS8c4xI7preN/zv9fCXhsQclTIMDu2Y/cytTjw==} + cpu: [arm64] + os: [linux] + + '@oxlint-tsgolint/linux-x64@0.23.0': + resolution: {integrity: sha512-3bdilnyA7kmSTjK27rvjIjSxL5SIg3wt7vwNiRkouWB83ytssyKnuGvxSYJxgMEmFpSutzaBzcCUM2jDtPGcgA==} + cpu: [x64] + os: [linux] + + '@oxlint-tsgolint/win32-arm64@0.23.0': + resolution: {integrity: sha512-j+OEp44SVYiQ+ZD+uttsX7u6L9SvmbbQ77SO1pSFCcJlsVMeCk8qZsjhKfGKuT/jIA+ipOJMVs/+pqUfObBWNw==} + cpu: [arm64] + os: [win32] + + '@oxlint-tsgolint/win32-x64@0.23.0': + resolution: {integrity: sha512-5MyjFuqf+g8OUPJBSGWHJtmoWnzFJYyOg4To9WMQshZYEWig/vtu7JtJ03VWnzHv9LJkAUeApY0gVCOywFR/iQ==} + cpu: [x64] + os: [win32] + + '@oxlint/binding-android-arm-eabi@1.66.0': + resolution: {integrity: sha512-f7kq8N51T4phpzqfBpA2qaVTI/KrkCmNwaj3t/97I/WLTDI+UhlP5GL9eER+zVxBhtlx5rKXWByJU1/zDAvyaw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + + '@oxlint/binding-android-arm64@1.66.0': + resolution: {integrity: sha512-xu6QO71tdDS9mjmLZ3AqhtaVHBvdmsOKkYnReNNDgh+XiwnsipeQOIxbiYOOO0iAXycJ+GK0wdMSZP/2j/AmSg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@oxlint/binding-darwin-arm64@1.66.0': + resolution: {integrity: sha512-HZ24VimSOC7mxuEA99e0H2FS0C1yO3+iW13jPRAk+e2njsUs3QeAXsafCDyaIrV/MirdOVez+etQNQsJE43zNQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@oxlint/binding-darwin-x64@1.66.0': + resolution: {integrity: sha512-awhj8ZvJrrRSnXj7V++rpZvTmnl99L6mi0B7gg7Cp7BN6cKpzuI481bHNLvXGA9GB1/oEgA3ponuyoAc6Md12A==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@oxlint/binding-freebsd-x64@1.66.0': + resolution: {integrity: sha512-KQF0oVV21/FjIqkRuL8Q1vh8ECsE5+ocdH5tcqTQ4ZnYuDVoYibQUNfqBjQaUsP6UIIda5Y75Wpm5p4RgQWiWw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@oxlint/binding-linux-arm-gnueabihf@1.66.0': + resolution: {integrity: sha512-9u1rgwZSEXWb30vbFZzQ78HVXBo0WCKNwJ3a2InRUTNMRng+PUDIoSFmA+m4HdUfBaIqftShq8J8qHc+eE/Vig==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxlint/binding-linux-arm-musleabihf@1.66.0': + resolution: {integrity: sha512-Ynot2HR1bHxUaNWoC280MVTDfZuaWuP3XfSMRDhyuZrVjhzoaBCVFlw8h8qeZjWKVUBhPWFIxB7AQTlK8Z2WWg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxlint/binding-linux-arm64-gnu@1.66.0': + resolution: {integrity: sha512-xCbgzciGgo+A4aQZEknsNrNiIwY7sU5SfRuMmRjPIvZAgdF34cIHiKvwOsS5XRLjlTVSFwitmq6YclTtHTfU+g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + + '@oxlint/binding-linux-arm64-musl@1.66.0': + resolution: {integrity: sha512-hmo+ZB/lHkR1HdDmnziNpzSLmulnUSu10VEqX2Yex7OwvoBAbjJQLvy4gIBRV3AAwWnCvAxKp5Nv1GE6LU1QMg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + + '@oxlint/binding-linux-ppc64-gnu@1.66.0': + resolution: {integrity: sha512-2Invd4Uyy81mVooQC5FBtfxSNrvcX1OxbMlVQ6M2erRrNI2awFYF26YNW2yFxdVFZ4ffNOWKghtMjhnUPsXsVA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + + '@oxlint/binding-linux-riscv64-gnu@1.66.0': + resolution: {integrity: sha512-s0iXPDQVdgayE3RGa/N2DZF7tjgg0TwEtD1sGoDxqPDGrIXgo45H0yHknT0f9A0yteASsweYZtDyTuVlM4aSag==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + + '@oxlint/binding-linux-riscv64-musl@1.66.0': + resolution: {integrity: sha512-OekL4XFiu7RPK0JIZi8VeHgtIXPREf42t8Cy/rKEsC+P3gcqDgNAAGiyuUOpdbG4wwbfue1q4CHcCO7spSve6w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + + '@oxlint/binding-linux-s390x-gnu@1.66.0': + resolution: {integrity: sha512-Ga1D0kj1SFslm34ThA/BdkUlyAYEnTsXyRC4pF0C5agZSwtGdHYWMTQWemUfBGp4RCG4QWXgdO+HmmmKqOtlBg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + + '@oxlint/binding-linux-x64-gnu@1.66.0': + resolution: {integrity: sha512-p5jfP1wUZe/IC3qpQO84n9DRnf9g3lKRtLBlQq23ykyrDglHcVx7sWmVTlPuU6SBw8mNnPzyOn022G3XZHnlww==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + + '@oxlint/binding-linux-x64-musl@1.66.0': + resolution: {integrity: sha512-vUB/sYlYZorDL1ZD+o9mRv7zbsykrrFRtmgS6R8musZqLtrPRQn1gc1eGpuX+sfdccz42STl/AqldY6XRb2upQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + + '@oxlint/binding-openharmony-arm64@1.66.0': + resolution: {integrity: sha512-yde+6p/F59xRkGR9H1HfngWRif1QRJjynZK349l+UI0H6w9hL3G8/AVaTHFyTtLVQ56qtNbX2/5Dc77n1ovnOg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@oxlint/binding-win32-arm64-msvc@1.66.0': + resolution: {integrity: sha512-O9GLucgoTdmOrbBX+EjzNe7o/Ze5TFOvXcib6bzUOtBOmj6cV+zw18NgB+cGKAkDw1Pdqs8vGkfHbbsLuDtXWg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@oxlint/binding-win32-ia32-msvc@1.66.0': + resolution: {integrity: sha512-m3Pjwc2MfTcom4E4gOv7DyuGyt7OfGNCbmqDHd+N7EzXmP+ppHuudm2NjcA3AjV5TSeGxaguVF4SbTKHe1USYA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ia32] + os: [win32] + + '@oxlint/binding-win32-x64-msvc@1.66.0': + resolution: {integrity: sha512-/DbBvw8UFBhja6PqudUjV4UtfsJr0Oa7jUjWVKB0g86lj/VwnPrkngn0sFql3c9RDA0O16dh7ozsXb6GjNAzBQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/binding-android-arm64@1.0.2': + resolution: {integrity: sha512-ZS4D1JPGn/MYQN/SYDWftIE/nVsM8j/AFOYEzAoOE2O3NktQOZru+/vYXGbR/qtdLdIfGCP0lcoJiYVzsEz+iQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.0.2': + resolution: {integrity: sha512-vdFA9+C/rekyGce7WqHs/xoT0ioZEWaOFyZLIV1mEeNFaFDUQrPIo8Vs2GvJ6eetb3rzDUtUBgzto3ExpXJB3w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.0.2': + resolution: {integrity: sha512-BewSOwTHazv77DTYiAZXSqqKZ4KP/KonFisDMVU7PImxoWfB2aepnPhd2E4SWz3zDzYgDNbs6jBmTdgNnF02GA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.0.2': + resolution: {integrity: sha512-m41o7M0YWtUdqk61Tb+jnKb2rN++iRdIASlExkUoKfIAH30DOHCB8fVLzSUpbWHHU8esmEioY62PxzexE8MBuA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.0.2': + resolution: {integrity: sha512-jcojB9H7W/jS29pMKWAK1N+fU99vXodHDTatS3b3y/XSOCiHo0kkA74pL3jJmkoQtYpOCxDvaKs1fo2Ij/1X5w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.0.2': + resolution: {integrity: sha512-1jn6qDU5iiOgFgygDzKUuKP0maTi0/f1+sBLgvij/76C77Nm3ts6ufz9Bjg5q5dduxiUIxtq86JIoBvo1xQ4Ig==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + + '@rolldown/binding-linux-arm64-musl@1.0.2': + resolution: {integrity: sha512-QVLO/czFMdoMFSqlX3bcswcJNm/23r+qoa/jgtmFc/qEp6/jXmIkDjF/XIo8dPfGaiwy1xfQn8o77L79GeXFgw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + + '@rolldown/binding-linux-ppc64-gnu@1.0.2': + resolution: {integrity: sha512-hgO5Abm0w5UL6FEa2iFnZqo2KlK7TQ5QhV5x09hujBf7t5KzHQ1VmfPuTpqRy/rNlSxua3eWH374xxiVrP+lcA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + + '@rolldown/binding-linux-s390x-gnu@1.0.2': + resolution: {integrity: sha512-fy8rXxuYEu602abC8MUNaPjYLIFzReOaEIEMKMUa0rFEUxNpVXhs15KSSQ4qlqSaM7B6rcj9rDZgADh/IGDzLQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + + '@rolldown/binding-linux-x64-gnu@1.0.2': + resolution: {integrity: sha512-0+bOkiQ779+r1WpoHOWHqncvyySci0vKph+myNDYb+im6meJAzHQXay6oEgnkHuUGouM1LKTZwqKpBow6Kj7CQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + + '@rolldown/binding-linux-x64-musl@1.0.2': + resolution: {integrity: sha512-mjSkrzZK5Qsl0a9d1JgILOiuZOSDTVdKENcSXBoqbzSrspLR/4/IRVDo5wd2GgZjNss/viBFJdeq+j7qH2nypw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + + '@rolldown/binding-openharmony-arm64@1.0.2': + resolution: {integrity: sha512-1v5vHasdfQAZoEHakBV72LIFAC9JjnymsiKxp+GEr/ma3+NJCPSaYK+qavInOovJkgwFrs7GccX2d6IgDA3Z5w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-wasm32-wasi@1.0.2': + resolution: {integrity: sha512-mb1VobWn6NheziTk5/WEaR6AKVbrwT5sOi6C7zk3gy/pD1qtJfU1j4PgTo2NJnOtbL9Dl3Aeei8w9jJ7qC2jZQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [wasm32] + + '@rolldown/binding-win32-arm64-msvc@1.0.2': + resolution: {integrity: sha512-SqKonF56vA/L2yHwHYcEp2P34URpOZ7d1fS635cTkpDnUtEGdUbhI6NzsPdqeSWvAAeGDrxjWjNmibDIdFf9/A==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.0.2': + resolution: {integrity: sha512-v7qRI7gXLRINcOGXt+7YmAZ6iFuyZVMIoXAxhd8oP+DR9dLfL9GfNIx7PLMxmhZdvq8waUJBQiWN9EKNy+TRBQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/pluginutils@1.0.1': + resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} + + '@rollup/rollup-android-arm-eabi@4.61.0': + resolution: {integrity: sha512-dnxczajOqt0gesZlN5pGQ1s1imQVrsmCw5G2Ci4oM+0WvNz3pyRnlWrT7McoZIb8VlFwCawdmbWRmxRn7HI+VQ==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.61.0': + resolution: {integrity: sha512-Bp3JpGP00Vu3f238ivRrjf7z3xSzVPXqCmaJYA9t2c+c8vKYvOzmXF7LkkeUalTEGd6cZcSWe+PFIP3Vy48fRg==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.61.0': + resolution: {integrity: sha512-zaYIpr670mUmmZ1tVzUFplbQbG7h3Gugx3L5FoqhsC2m/YnLlR1a7zVLmXNPy+iY1tFPEbNG+HHBXZGyId0G5w==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.61.0': + resolution: {integrity: sha512-+P49fvkv2dSoeevUW+lgZ/I2JHSsJCK1Lyjj7Cu6E4UHG4tS9XIefzIjo5qhgELjAclnen1rLzK2PMKJdo+Dyg==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.61.0': + resolution: {integrity: sha512-l3FAAOyKJXH2ea6KNFN+MMgC/rnE94YGLXs2ehYqDcCoHt1DpvgWX75BhUJxN38XojP7Ul+4H8PRn7EdyqSDrw==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.61.0': + resolution: {integrity: sha512-VokPN3TSctKj65cyCNPaUh4vMFA8awxOot/0sp+4J7ZlNRKQEhXhawqPwajoi8H5ZFt61i0ugZJuTKXBjGJ17Q==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.61.0': + resolution: {integrity: sha512-DxH0P3wxm+Yzs/p3zrk9dw1rURu8p0Nv5+MRK/L7OtnLNg5rLZraSBFZ8iUXOd9f2BlhJyEpIZUH/emjq4UJ4g==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.61.0': + resolution: {integrity: sha512-T6ZvMNe84kAz6TBWHC7hGAoEtzP1LWYw/AqayGWEF6uISt3Abk/st06LqRD9THd7Xz3NxzurUpzAuEAUbZf+nw==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.61.0': + resolution: {integrity: sha512-q/4hzvQkDs8b4jIBab1pnLiiM0ayTZsN2amBFPDzuyZxjEd4wDwx0UJFYM3cOZzSf5Kw8fnWSprJzIBMkcR44Q==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.61.0': + resolution: {integrity: sha512-vvYWX3akdEAY6km+9wAqFDnk6pQsbJKVnj7xawcvs/+fdlYBGp+U+Qq/lLfpIxYIZvZLHMAKD9HLdacSx/r3dw==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loong64-gnu@4.61.0': + resolution: {integrity: sha512-DePa5cqOxDP/Zp0VOXpeWaGew5iIv5DXp9NYbzkX5PFQyWVX9184WCTh3hvr/7lhXo8ZVlbFLkz8+o/q1dU6gA==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-loong64-musl@4.61.0': + resolution: {integrity: sha512-LV8aWMB8UChglMCEzs7RkN0GsH29RJaLLqwm9fCIjlqwxQTiWAqNcc7wjBkH31hV0PU/yVxGYvrYsgfea2qw6g==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-ppc64-gnu@4.61.0': + resolution: {integrity: sha512-QoNSnwQtaeNu5grdBbsL0tt1uyl5EnS8DA8Mr3nluMXbhdQNyhN+G4tBax7VCdxLKj8YJ0/4OO9Ho84jMnJtKA==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-ppc64-musl@4.61.0': + resolution: {integrity: sha512-/zZp5MKapIIApE8trN8qLGNSiRN9TUoaUZ1cmVu4XnVdd5LQLOXTtyi+vtfUbNnT3iyjzpPqYeKXmvJ+gJGYWw==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.61.0': + resolution: {integrity: sha512-RbrzcD3aJ1k3UbtMRRBNwojdVVyXjuVAFTfn/xPa6EEl6GE9Sm/akPgFTb9aAC9pMKGJ6CtWxaGrqWcabH+ySg==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.61.0': + resolution: {integrity: sha512-ZF+onDsBso8PJf1XaG9lB+O9RnBpKGnY6OrzC4CSHrtC1jb6jWLTKK4bRqdoCXHd22gyr2hiYmEAm8Wns/BOCw==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.61.0': + resolution: {integrity: sha512-Atk0aSIk5Zx2Wuh9dgRQgLP0Koc8hOeYpbWryMXyk8G8/HmPkwPPkMqIIDhrXHHYqfUzSJA/I7IWSBv8xSmRBA==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.61.0': + resolution: {integrity: sha512-0uMOcf3eZ5K+K4cYHkdxShFMPlPXCOdfDFEFn9dNYAEEd2cVvmOfH7zFgRVoDgmtQ1m9k5q7qfrHzyMAubKYUA==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.61.0': + resolution: {integrity: sha512-mvFtE4A/t/7hRJ7X8Ozmu8FsIkAUat2nzl12pgU337BRmq87AQUJztwHz2Zv5/tjo9/C95E66CK03SI/ToEDJw==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-openbsd-x64@4.61.0': + resolution: {integrity: sha512-z9b9+aTxvt8n2rNltMPvyaUfB8NJ+CVyOrGK/MdIKHx7B+lXmZpm/XbRsU7Rpf3fRqJ2uS6mBJiJveCtq8LHDg==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.61.0': + resolution: {integrity: sha512-jXaXFqKMehsOc+g8R6oo33RRC6w07G9jDBxAE5eAKX7mOcCbZloYIPNhfG9Wl+P9O9IWHFO4OJgPi1Ml2qkt7w==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.61.0': + resolution: {integrity: sha512-OXNWVFocS2IA4+QplhTZZ2a+8hPZR7T8KuozsNmJKK8y7cp83StHvGksfHzPG3wczWTczyWHVQuqeiTUbjiyBg==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.61.0': + resolution: {integrity: sha512-AlAbNtBO637LxSldqV43z0FfXoGfl2TW1DgAg/bs7aQswFbDewz2SJm3BUhiGfbOVtW571xbc9p+REdxhyN/Eg==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.61.0': + resolution: {integrity: sha512-QRSrQXyJ1M4tjNXdR0/G/IgV6lzfQQJYBjlWIEYkY2Xs86DRl/iEpQ4blMDjJxSl7n19eDKKXMg0AmuBVYy8pQ==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.61.0': + resolution: {integrity: sha512-tkuFxhvKO/HlGd0VsINF6vHSYH8AF8W0TcNxKDK6JZmrehngFj78pToc8iemtnvwilDjs2G/qSzYFhe9U8q+fw==} + cpu: [x64] + os: [win32] + + '@solana-config/oxc@0.1.1': + resolution: {integrity: sha512-U71jzRSTmVQmGbKNwuEjDZDjrYlLxpjnSzxAR2ebrg6Vmagc9MbE8pAMQl1qnJzO9J0g0qexzw+9lh9X0HLPvA==} + peerDependencies: + oxfmt: '>=0.48.0' + oxlint: '>=1.63.0' + peerDependenciesMeta: + oxfmt: + optional: true + oxlint: + optional: true + + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + + '@tybys/wasm-util@0.10.2': + resolution: {integrity: sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==} + + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + + '@types/node@25.9.1': + resolution: {integrity: sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==} + + '@vitest/expect@4.1.7': + resolution: {integrity: sha512-1R+tw0ortHEbZDGMymm+pN7/AFQ/RkFFdtd7EN+VBpynKmLbP8A3rpEXdshBJ7+8hQ9zBJh/i1s0yKNtxAnU7w==} + + '@vitest/mocker@4.1.7': + resolution: {integrity: sha512-vY7nuamKgfvpA1Koa3oYIw/k7D6kZnpGyNMZW8loow2bsBYla1TFdqTaXncWdRn4pgwNs+90RhnXhJScDwQeJA==} + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@4.1.7': + resolution: {integrity: sha512-umgCarTOYQWIaDMvGDRZij+6b9oVeLIyJzfN+AS88e0ZOU3QTgNNSTtjQOpcvWr3np1N0j4WgZj+sb3oYBDscw==} + + '@vitest/runner@4.1.7': + resolution: {integrity: sha512-BapjmAQ2aI78WdMEfeUWivnfVzB+VPGwWRQcJE0OUq7qEeEcBsCSf+0T5iREBNE5nBb4wA5Ya0W6IA+sghdEFw==} + + '@vitest/snapshot@4.1.7': + resolution: {integrity: sha512-ZacLzja+TmJeZ1h14xW2FB/WpeimUD3haBXQPyJqxvo8jQTmfeA8zv58mtjN2C7EHXZDYVcVYdYmAxjkWVvKCw==} + + '@vitest/spy@4.1.7': + resolution: {integrity: sha512-kbkI5LMWakyuTIvs6fUJ5qdIVb1XVKsYJAT4OJ938cHMROYMSfmoQdZy0aaAnjbbc8F61vkoTqz/Az+/HiIu5Q==} + + '@vitest/utils@4.1.7': + resolution: {integrity: sha512-T532WBu791cBxJlCl6SO+J14l81DQx6uQHm1bQbmCDY7nqlEIgkza/UFnSBNaUtSf41unldDFjdOBYEQC4b5Hw==} + + acorn@8.16.0: + resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} + engines: {node: '>=0.4.0'} + hasBin: true + + any-promise@1.3.0: + resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + + brace-expansion@5.0.6: + resolution: {integrity: sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==} + engines: {node: 18 || 20 || >=22} + + bundle-require@5.1.0: + resolution: {integrity: sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + peerDependencies: + esbuild: '>=0.18' + + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + + chai@6.2.2: + resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} + engines: {node: '>=18'} + + chokidar@4.0.3: + resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} + engines: {node: '>= 14.16.0'} + + commander@14.0.3: + resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==} + engines: {node: '>=20'} + + commander@4.1.1: + resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} + engines: {node: '>= 6'} + + confbox@0.1.8: + resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} + + consola@3.4.2: + resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} + engines: {node: ^14.18.0 || >=16.10.0} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + es-module-lexer@2.1.0: + resolution: {integrity: sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==} + + esbuild@0.27.7: + resolution: {integrity: sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==} + engines: {node: '>=18'} + hasBin: true + + esbuild@0.28.0: + resolution: {integrity: sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==} + engines: {node: '>=18'} + hasBin: true + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + expect-type@1.3.0: + resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} + engines: {node: '>=12.0.0'} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fix-dts-default-cjs-exports@1.0.1: + resolution: {integrity: sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + glob@13.0.6: + resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==} + engines: {node: 18 || 20 || >=22} + + joycon@3.1.1: + resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} + engines: {node: '>=10'} + + lightningcss-android-arm64@1.32.0: + resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.32.0: + resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.32.0: + resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.32.0: + resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.32.0: + resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.32.0: + resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-arm64-musl@1.32.0: + resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-x64-gnu@1.32.0: + resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-linux-x64-musl@1.32.0: + resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-win32-arm64-msvc@1.32.0: + resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.32.0: + resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.32.0: + resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} + engines: {node: '>= 12.0.0'} + + lilconfig@3.1.3: + resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} + engines: {node: '>=14'} + + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + load-tsconfig@0.2.5: + resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + lru-cache@11.5.1: + resolution: {integrity: sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==} + engines: {node: 20 || >=22} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + minimatch@10.2.5: + resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} + engines: {node: 18 || 20 || >=22} + + minipass@7.1.3: + resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} + engines: {node: '>=16 || 14 >=14.17'} + + mlly@1.8.2: + resolution: {integrity: sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + mz@2.7.0: + resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + + nanoid@3.3.12: + resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + obug@2.1.1: + resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} + + oxfmt@0.51.0: + resolution: {integrity: sha512-l/AoAnaEOV7Q5/Z9kHOMDehVJnCgYN7wRoooWCTUMBMi16BJhLZqd9cmCnwcVFfVlzkt53zK2KLPFNp8vSsoDg==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + svelte: ^5.0.0 + peerDependenciesMeta: + svelte: + optional: true + + oxlint-tsgolint@0.23.0: + resolution: {integrity: sha512-3mBv3CoPbh8dFbzfDGIWa2ytZjn2v+3EX4aKRXjIhsoGFzG8GCjfRirz3rwZf1wYbZzsNLTSgpw8VjQuWdp/jA==} + hasBin: true + + oxlint@1.66.0: + resolution: {integrity: sha512-N4LLxYLd94KEBqXDMDM5f+2PUpItTjDLreXe2Gn5KhjhCK4Qp2YUXaBi8Yu325ryOgKwt22m45fpD7nPOn69Yw==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + oxlint-tsgolint: '>=0.22.1' + peerDependenciesMeta: + oxlint-tsgolint: + optional: true + + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + + path-scurry@2.0.2: + resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==} + engines: {node: 18 || 20 || >=22} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} + engines: {node: '>=12'} + + pirates@4.0.7: + resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} + engines: {node: '>= 6'} + + pkg-types@1.3.1: + resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} + + postcss-load-config@6.0.1: + resolution: {integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==} + engines: {node: '>= 18'} + peerDependencies: + jiti: '>=1.21.0' + postcss: '>=8.0.9' + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + jiti: + optional: true + postcss: + optional: true + tsx: + optional: true + yaml: + optional: true + + postcss@8.5.15: + resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==} + engines: {node: ^10 || ^12 || >=14} + + readdirp@4.1.2: + resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} + engines: {node: '>= 14.18.0'} + + resolve-from@5.0.0: + resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} + engines: {node: '>=8'} + + rimraf@6.1.3: + resolution: {integrity: sha512-LKg+Cr2ZF61fkcaK1UdkH2yEBBKnYjTyWzTJT6KNPcSPaiT7HSdhtMXQuN5wkTX0Xu72KQ1l8S42rlmexS2hSA==} + engines: {node: 20 || >=22} + hasBin: true + + rolldown@1.0.2: + resolution: {integrity: sha512-oZx5zVDtVB44AW3eaifgDml1gWRDZGvjcfdxonE4swNPG98PrrXjaO/KrnUjzlMnztCCRVlUueA1kCXhARGk6g==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + + rollup@4.61.0: + resolution: {integrity: sha512-T9mWdbWfQtp0B5lv/HX+wrhYsmXRlcWnXXmJbXqKJhlRaoS6KMhq0gpyzW4UJfclcxrEdLnTgjT2NjruLONu0g==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + source-map@0.7.6: + resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==} + engines: {node: '>= 12'} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + std-env@4.1.0: + resolution: {integrity: sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==} + + sucrase@3.35.1: + resolution: {integrity: sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==} + engines: {node: '>=16 || 14 >=14.17'} + hasBin: true + + thenify-all@1.6.0: + resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} + engines: {node: '>=0.8'} + + thenify@3.3.1: + resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + + tinyexec@1.1.2: + resolution: {integrity: sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA==} + engines: {node: '>=18'} + + tinyglobby@0.2.16: + resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} + engines: {node: '>=12.0.0'} + + tinypool@2.1.0: + resolution: {integrity: sha512-Pugqs6M0m7Lv1I7FtxN4aoyToKg1C4tu+/381vH35y8oENM/Ai7f7C4StcoK4/+BSw9ebcS8jRiVrORFKCALLw==} + engines: {node: ^20.0.0 || >=22.0.0} + + tinyrainbow@3.1.0: + resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} + engines: {node: '>=14.0.0'} + + tree-kill@1.2.2: + resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} + hasBin: true + + ts-interface-checker@0.1.13: + resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + tsup@8.5.1: + resolution: {integrity: sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing==} + engines: {node: '>=18'} + hasBin: true + peerDependencies: + '@microsoft/api-extractor': ^7.36.0 + '@swc/core': ^1 + postcss: ^8.4.12 + typescript: '>=4.5.0' + peerDependenciesMeta: + '@microsoft/api-extractor': + optional: true + '@swc/core': + optional: true + postcss: + optional: true + typescript: + optional: true + + tsx@4.22.3: + resolution: {integrity: sha512-mdoNxBC/cSQObGGVQ5Bpn5i+yv7j68gk3Nfm3wFjcJg3Z0Mix9jzAFfP12prmm5eVGmDKtp0yyArrs0Q+8gZHg==} + engines: {node: '>=18.0.0'} + hasBin: true + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + ufo@1.6.4: + resolution: {integrity: sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==} + + undici-types@7.24.6: + resolution: {integrity: sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==} + + vite@8.0.14: + resolution: {integrity: sha512-s4BJJ+5y1pYL6Otw51FHhVJQhPnuRinKig64g/1+EUNaJsd3gCKdD31IPFvswUgW9/60QT9oFHbZHbQK5imcxw==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.1.18 + esbuild: ^0.27.0 || ^0.28.0 + jiti: '>=1.21.0' + less: ^4.0.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + '@vitejs/devtools': + optional: true + esbuild: + optional: true + jiti: + optional: true + less: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitest@4.1.7: + resolution: {integrity: sha512-flYyaFd2CgoCoU+0UKt3pxksgC+S02iTDN0n3LtqaMeXsI9SBcdNujc2k0DeFLzUn/0k538yNjOSdwgCqcrwJA==} + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@opentelemetry/api': ^1.9.0 + '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 4.1.7 + '@vitest/browser-preview': 4.1.7 + '@vitest/browser-webdriverio': 4.1.7 + '@vitest/coverage-istanbul': 4.1.7 + '@vitest/coverage-v8': 4.1.7 + '@vitest/ui': 4.1.7 + happy-dom: '*' + jsdom: '*' + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@opentelemetry/api': + optional: true + '@types/node': + optional: true + '@vitest/browser-playwright': + optional: true + '@vitest/browser-preview': + optional: true + '@vitest/browser-webdriverio': + optional: true + '@vitest/coverage-istanbul': + optional: true + '@vitest/coverage-v8': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + +snapshots: + + '@codama/errors@1.7.0': + dependencies: + '@codama/node-types': 1.7.0 + commander: 14.0.3 + picocolors: 1.1.1 + + '@codama/fragments@0.1.0': + dependencies: + '@codama/errors': 1.7.0 + + '@codama/node-types@1.7.0': {} + + '@codama/spec@1.6.0-rc.6': {} + + '@emnapi/core@1.10.0': + dependencies: + '@emnapi/wasi-threads': 1.2.1 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.10.0': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.2.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@esbuild/aix-ppc64@0.27.7': + optional: true + + '@esbuild/aix-ppc64@0.28.0': + optional: true + + '@esbuild/android-arm64@0.27.7': + optional: true + + '@esbuild/android-arm64@0.28.0': + optional: true + + '@esbuild/android-arm@0.27.7': + optional: true + + '@esbuild/android-arm@0.28.0': + optional: true + + '@esbuild/android-x64@0.27.7': + optional: true + + '@esbuild/android-x64@0.28.0': + optional: true + + '@esbuild/darwin-arm64@0.27.7': + optional: true + + '@esbuild/darwin-arm64@0.28.0': + optional: true + + '@esbuild/darwin-x64@0.27.7': + optional: true + + '@esbuild/darwin-x64@0.28.0': + optional: true + + '@esbuild/freebsd-arm64@0.27.7': + optional: true + + '@esbuild/freebsd-arm64@0.28.0': + optional: true + + '@esbuild/freebsd-x64@0.27.7': + optional: true + + '@esbuild/freebsd-x64@0.28.0': + optional: true + + '@esbuild/linux-arm64@0.27.7': + optional: true + + '@esbuild/linux-arm64@0.28.0': + optional: true + + '@esbuild/linux-arm@0.27.7': + optional: true + + '@esbuild/linux-arm@0.28.0': + optional: true + + '@esbuild/linux-ia32@0.27.7': + optional: true + + '@esbuild/linux-ia32@0.28.0': + optional: true + + '@esbuild/linux-loong64@0.27.7': + optional: true + + '@esbuild/linux-loong64@0.28.0': + optional: true + + '@esbuild/linux-mips64el@0.27.7': + optional: true + + '@esbuild/linux-mips64el@0.28.0': + optional: true + + '@esbuild/linux-ppc64@0.27.7': + optional: true + + '@esbuild/linux-ppc64@0.28.0': + optional: true + + '@esbuild/linux-riscv64@0.27.7': + optional: true + + '@esbuild/linux-riscv64@0.28.0': + optional: true + + '@esbuild/linux-s390x@0.27.7': + optional: true + + '@esbuild/linux-s390x@0.28.0': + optional: true + + '@esbuild/linux-x64@0.27.7': + optional: true + + '@esbuild/linux-x64@0.28.0': + optional: true + + '@esbuild/netbsd-arm64@0.27.7': + optional: true + + '@esbuild/netbsd-arm64@0.28.0': + optional: true + + '@esbuild/netbsd-x64@0.27.7': + optional: true + + '@esbuild/netbsd-x64@0.28.0': + optional: true + + '@esbuild/openbsd-arm64@0.27.7': + optional: true + + '@esbuild/openbsd-arm64@0.28.0': + optional: true + + '@esbuild/openbsd-x64@0.27.7': + optional: true + + '@esbuild/openbsd-x64@0.28.0': + optional: true + + '@esbuild/openharmony-arm64@0.27.7': + optional: true + + '@esbuild/openharmony-arm64@0.28.0': + optional: true + + '@esbuild/sunos-x64@0.27.7': + optional: true + + '@esbuild/sunos-x64@0.28.0': + optional: true + + '@esbuild/win32-arm64@0.27.7': + optional: true + + '@esbuild/win32-arm64@0.28.0': + optional: true + + '@esbuild/win32-ia32@0.27.7': + optional: true + + '@esbuild/win32-ia32@0.28.0': + optional: true + + '@esbuild/win32-x64@0.27.7': + optional: true + + '@esbuild/win32-x64@0.28.0': + optional: true + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@napi-rs/wasm-runtime@1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@tybys/wasm-util': 0.10.2 + optional: true + + '@oxc-project/types@0.132.0': {} + + '@oxfmt/binding-android-arm-eabi@0.51.0': + optional: true + + '@oxfmt/binding-android-arm64@0.51.0': + optional: true + + '@oxfmt/binding-darwin-arm64@0.51.0': + optional: true + + '@oxfmt/binding-darwin-x64@0.51.0': + optional: true + + '@oxfmt/binding-freebsd-x64@0.51.0': + optional: true + + '@oxfmt/binding-linux-arm-gnueabihf@0.51.0': + optional: true + + '@oxfmt/binding-linux-arm-musleabihf@0.51.0': + optional: true + + '@oxfmt/binding-linux-arm64-gnu@0.51.0': + optional: true + + '@oxfmt/binding-linux-arm64-musl@0.51.0': + optional: true + + '@oxfmt/binding-linux-ppc64-gnu@0.51.0': + optional: true + + '@oxfmt/binding-linux-riscv64-gnu@0.51.0': + optional: true + + '@oxfmt/binding-linux-riscv64-musl@0.51.0': + optional: true + + '@oxfmt/binding-linux-s390x-gnu@0.51.0': + optional: true + + '@oxfmt/binding-linux-x64-gnu@0.51.0': + optional: true + + '@oxfmt/binding-linux-x64-musl@0.51.0': + optional: true + + '@oxfmt/binding-openharmony-arm64@0.51.0': + optional: true + + '@oxfmt/binding-win32-arm64-msvc@0.51.0': + optional: true + + '@oxfmt/binding-win32-ia32-msvc@0.51.0': + optional: true + + '@oxfmt/binding-win32-x64-msvc@0.51.0': + optional: true + + '@oxlint-tsgolint/darwin-arm64@0.23.0': + optional: true + + '@oxlint-tsgolint/darwin-x64@0.23.0': + optional: true + + '@oxlint-tsgolint/linux-arm64@0.23.0': + optional: true + + '@oxlint-tsgolint/linux-x64@0.23.0': + optional: true + + '@oxlint-tsgolint/win32-arm64@0.23.0': + optional: true + + '@oxlint-tsgolint/win32-x64@0.23.0': + optional: true + + '@oxlint/binding-android-arm-eabi@1.66.0': + optional: true + + '@oxlint/binding-android-arm64@1.66.0': + optional: true + + '@oxlint/binding-darwin-arm64@1.66.0': + optional: true + + '@oxlint/binding-darwin-x64@1.66.0': + optional: true + + '@oxlint/binding-freebsd-x64@1.66.0': + optional: true + + '@oxlint/binding-linux-arm-gnueabihf@1.66.0': + optional: true + + '@oxlint/binding-linux-arm-musleabihf@1.66.0': + optional: true + + '@oxlint/binding-linux-arm64-gnu@1.66.0': + optional: true + + '@oxlint/binding-linux-arm64-musl@1.66.0': + optional: true + + '@oxlint/binding-linux-ppc64-gnu@1.66.0': + optional: true + + '@oxlint/binding-linux-riscv64-gnu@1.66.0': + optional: true + + '@oxlint/binding-linux-riscv64-musl@1.66.0': + optional: true + + '@oxlint/binding-linux-s390x-gnu@1.66.0': + optional: true + + '@oxlint/binding-linux-x64-gnu@1.66.0': + optional: true + + '@oxlint/binding-linux-x64-musl@1.66.0': + optional: true + + '@oxlint/binding-openharmony-arm64@1.66.0': + optional: true + + '@oxlint/binding-win32-arm64-msvc@1.66.0': + optional: true + + '@oxlint/binding-win32-ia32-msvc@1.66.0': + optional: true + + '@oxlint/binding-win32-x64-msvc@1.66.0': + optional: true + + '@rolldown/binding-android-arm64@1.0.2': + optional: true + + '@rolldown/binding-darwin-arm64@1.0.2': + optional: true + + '@rolldown/binding-darwin-x64@1.0.2': + optional: true + + '@rolldown/binding-freebsd-x64@1.0.2': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.0.2': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.0.2': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.0.2': + optional: true + + '@rolldown/binding-linux-ppc64-gnu@1.0.2': + optional: true + + '@rolldown/binding-linux-s390x-gnu@1.0.2': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.0.2': + optional: true + + '@rolldown/binding-linux-x64-musl@1.0.2': + optional: true + + '@rolldown/binding-openharmony-arm64@1.0.2': + optional: true + + '@rolldown/binding-wasm32-wasi@1.0.2': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.0.2': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.0.2': + optional: true + + '@rolldown/pluginutils@1.0.1': {} + + '@rollup/rollup-android-arm-eabi@4.61.0': + optional: true + + '@rollup/rollup-android-arm64@4.61.0': + optional: true + + '@rollup/rollup-darwin-arm64@4.61.0': + optional: true + + '@rollup/rollup-darwin-x64@4.61.0': + optional: true + + '@rollup/rollup-freebsd-arm64@4.61.0': + optional: true + + '@rollup/rollup-freebsd-x64@4.61.0': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.61.0': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.61.0': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.61.0': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.61.0': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.61.0': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.61.0': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.61.0': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.61.0': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.61.0': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.61.0': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.61.0': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.61.0': + optional: true + + '@rollup/rollup-linux-x64-musl@4.61.0': + optional: true + + '@rollup/rollup-openbsd-x64@4.61.0': + optional: true + + '@rollup/rollup-openharmony-arm64@4.61.0': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.61.0': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.61.0': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.61.0': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.61.0': + optional: true + + '@solana-config/oxc@0.1.1(oxfmt@0.51.0)(oxlint@1.66.0(oxlint-tsgolint@0.23.0))': + optionalDependencies: + oxfmt: 0.51.0 + oxlint: 1.66.0(oxlint-tsgolint@0.23.0) + + '@standard-schema/spec@1.1.0': {} + + '@tybys/wasm-util@0.10.2': + dependencies: + tslib: 2.8.1 + optional: true + + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + + '@types/deep-eql@4.0.2': {} + + '@types/estree@1.0.9': {} + + '@types/node@25.9.1': + dependencies: + undici-types: 7.24.6 + + '@vitest/expect@4.1.7': + dependencies: + '@standard-schema/spec': 1.1.0 + '@types/chai': 5.2.3 + '@vitest/spy': 4.1.7 + '@vitest/utils': 4.1.7 + chai: 6.2.2 + tinyrainbow: 3.1.0 + + '@vitest/mocker@4.1.7(vite@8.0.14(@types/node@25.9.1)(esbuild@0.27.7)(tsx@4.22.3))': + dependencies: + '@vitest/spy': 4.1.7 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 8.0.14(@types/node@25.9.1)(esbuild@0.27.7)(tsx@4.22.3) + + '@vitest/pretty-format@4.1.7': + dependencies: + tinyrainbow: 3.1.0 + + '@vitest/runner@4.1.7': + dependencies: + '@vitest/utils': 4.1.7 + pathe: 2.0.3 + + '@vitest/snapshot@4.1.7': + dependencies: + '@vitest/pretty-format': 4.1.7 + '@vitest/utils': 4.1.7 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@4.1.7': {} + + '@vitest/utils@4.1.7': + dependencies: + '@vitest/pretty-format': 4.1.7 + convert-source-map: 2.0.0 + tinyrainbow: 3.1.0 + + acorn@8.16.0: {} + + any-promise@1.3.0: {} + + assertion-error@2.0.1: {} + + balanced-match@4.0.4: {} + + brace-expansion@5.0.6: + dependencies: + balanced-match: 4.0.4 + + bundle-require@5.1.0(esbuild@0.27.7): + dependencies: + esbuild: 0.27.7 + load-tsconfig: 0.2.5 + + cac@6.7.14: {} + + chai@6.2.2: {} + + chokidar@4.0.3: + dependencies: + readdirp: 4.1.2 + + commander@14.0.3: {} + + commander@4.1.1: {} + + confbox@0.1.8: {} + + consola@3.4.2: {} + + convert-source-map@2.0.0: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + detect-libc@2.1.2: {} + + es-module-lexer@2.1.0: {} + + esbuild@0.27.7: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.7 + '@esbuild/android-arm': 0.27.7 + '@esbuild/android-arm64': 0.27.7 + '@esbuild/android-x64': 0.27.7 + '@esbuild/darwin-arm64': 0.27.7 + '@esbuild/darwin-x64': 0.27.7 + '@esbuild/freebsd-arm64': 0.27.7 + '@esbuild/freebsd-x64': 0.27.7 + '@esbuild/linux-arm': 0.27.7 + '@esbuild/linux-arm64': 0.27.7 + '@esbuild/linux-ia32': 0.27.7 + '@esbuild/linux-loong64': 0.27.7 + '@esbuild/linux-mips64el': 0.27.7 + '@esbuild/linux-ppc64': 0.27.7 + '@esbuild/linux-riscv64': 0.27.7 + '@esbuild/linux-s390x': 0.27.7 + '@esbuild/linux-x64': 0.27.7 + '@esbuild/netbsd-arm64': 0.27.7 + '@esbuild/netbsd-x64': 0.27.7 + '@esbuild/openbsd-arm64': 0.27.7 + '@esbuild/openbsd-x64': 0.27.7 + '@esbuild/openharmony-arm64': 0.27.7 + '@esbuild/sunos-x64': 0.27.7 + '@esbuild/win32-arm64': 0.27.7 + '@esbuild/win32-ia32': 0.27.7 + '@esbuild/win32-x64': 0.27.7 + + esbuild@0.28.0: + optionalDependencies: + '@esbuild/aix-ppc64': 0.28.0 + '@esbuild/android-arm': 0.28.0 + '@esbuild/android-arm64': 0.28.0 + '@esbuild/android-x64': 0.28.0 + '@esbuild/darwin-arm64': 0.28.0 + '@esbuild/darwin-x64': 0.28.0 + '@esbuild/freebsd-arm64': 0.28.0 + '@esbuild/freebsd-x64': 0.28.0 + '@esbuild/linux-arm': 0.28.0 + '@esbuild/linux-arm64': 0.28.0 + '@esbuild/linux-ia32': 0.28.0 + '@esbuild/linux-loong64': 0.28.0 + '@esbuild/linux-mips64el': 0.28.0 + '@esbuild/linux-ppc64': 0.28.0 + '@esbuild/linux-riscv64': 0.28.0 + '@esbuild/linux-s390x': 0.28.0 + '@esbuild/linux-x64': 0.28.0 + '@esbuild/netbsd-arm64': 0.28.0 + '@esbuild/netbsd-x64': 0.28.0 + '@esbuild/openbsd-arm64': 0.28.0 + '@esbuild/openbsd-x64': 0.28.0 + '@esbuild/openharmony-arm64': 0.28.0 + '@esbuild/sunos-x64': 0.28.0 + '@esbuild/win32-arm64': 0.28.0 + '@esbuild/win32-ia32': 0.28.0 + '@esbuild/win32-x64': 0.28.0 + optional: true + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.9 + + expect-type@1.3.0: {} + + fdir@6.5.0(picomatch@4.0.4): + optionalDependencies: + picomatch: 4.0.4 + + fix-dts-default-cjs-exports@1.0.1: + dependencies: + magic-string: 0.30.21 + mlly: 1.8.2 + rollup: 4.61.0 + + fsevents@2.3.3: + optional: true + + glob@13.0.6: + dependencies: + minimatch: 10.2.5 + minipass: 7.1.3 + path-scurry: 2.0.2 + + joycon@3.1.1: {} + + lightningcss-android-arm64@1.32.0: + optional: true + + lightningcss-darwin-arm64@1.32.0: + optional: true + + lightningcss-darwin-x64@1.32.0: + optional: true + + lightningcss-freebsd-x64@1.32.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.32.0: + optional: true + + lightningcss-linux-arm64-gnu@1.32.0: + optional: true + + lightningcss-linux-arm64-musl@1.32.0: + optional: true + + lightningcss-linux-x64-gnu@1.32.0: + optional: true + + lightningcss-linux-x64-musl@1.32.0: + optional: true + + lightningcss-win32-arm64-msvc@1.32.0: + optional: true + + lightningcss-win32-x64-msvc@1.32.0: + optional: true + + lightningcss@1.32.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.32.0 + lightningcss-darwin-arm64: 1.32.0 + lightningcss-darwin-x64: 1.32.0 + lightningcss-freebsd-x64: 1.32.0 + lightningcss-linux-arm-gnueabihf: 1.32.0 + lightningcss-linux-arm64-gnu: 1.32.0 + lightningcss-linux-arm64-musl: 1.32.0 + lightningcss-linux-x64-gnu: 1.32.0 + lightningcss-linux-x64-musl: 1.32.0 + lightningcss-win32-arm64-msvc: 1.32.0 + lightningcss-win32-x64-msvc: 1.32.0 + + lilconfig@3.1.3: {} + + lines-and-columns@1.2.4: {} + + load-tsconfig@0.2.5: {} + + lru-cache@11.5.1: {} + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + minimatch@10.2.5: + dependencies: + brace-expansion: 5.0.6 + + minipass@7.1.3: {} + + mlly@1.8.2: + dependencies: + acorn: 8.16.0 + pathe: 2.0.3 + pkg-types: 1.3.1 + ufo: 1.6.4 + + ms@2.1.3: {} + + mz@2.7.0: + dependencies: + any-promise: 1.3.0 + object-assign: 4.1.1 + thenify-all: 1.6.0 + + nanoid@3.3.12: {} + + object-assign@4.1.1: {} + + obug@2.1.1: {} + + oxfmt@0.51.0: + dependencies: + tinypool: 2.1.0 + optionalDependencies: + '@oxfmt/binding-android-arm-eabi': 0.51.0 + '@oxfmt/binding-android-arm64': 0.51.0 + '@oxfmt/binding-darwin-arm64': 0.51.0 + '@oxfmt/binding-darwin-x64': 0.51.0 + '@oxfmt/binding-freebsd-x64': 0.51.0 + '@oxfmt/binding-linux-arm-gnueabihf': 0.51.0 + '@oxfmt/binding-linux-arm-musleabihf': 0.51.0 + '@oxfmt/binding-linux-arm64-gnu': 0.51.0 + '@oxfmt/binding-linux-arm64-musl': 0.51.0 + '@oxfmt/binding-linux-ppc64-gnu': 0.51.0 + '@oxfmt/binding-linux-riscv64-gnu': 0.51.0 + '@oxfmt/binding-linux-riscv64-musl': 0.51.0 + '@oxfmt/binding-linux-s390x-gnu': 0.51.0 + '@oxfmt/binding-linux-x64-gnu': 0.51.0 + '@oxfmt/binding-linux-x64-musl': 0.51.0 + '@oxfmt/binding-openharmony-arm64': 0.51.0 + '@oxfmt/binding-win32-arm64-msvc': 0.51.0 + '@oxfmt/binding-win32-ia32-msvc': 0.51.0 + '@oxfmt/binding-win32-x64-msvc': 0.51.0 + + oxlint-tsgolint@0.23.0: + optionalDependencies: + '@oxlint-tsgolint/darwin-arm64': 0.23.0 + '@oxlint-tsgolint/darwin-x64': 0.23.0 + '@oxlint-tsgolint/linux-arm64': 0.23.0 + '@oxlint-tsgolint/linux-x64': 0.23.0 + '@oxlint-tsgolint/win32-arm64': 0.23.0 + '@oxlint-tsgolint/win32-x64': 0.23.0 + + oxlint@1.66.0(oxlint-tsgolint@0.23.0): + optionalDependencies: + '@oxlint/binding-android-arm-eabi': 1.66.0 + '@oxlint/binding-android-arm64': 1.66.0 + '@oxlint/binding-darwin-arm64': 1.66.0 + '@oxlint/binding-darwin-x64': 1.66.0 + '@oxlint/binding-freebsd-x64': 1.66.0 + '@oxlint/binding-linux-arm-gnueabihf': 1.66.0 + '@oxlint/binding-linux-arm-musleabihf': 1.66.0 + '@oxlint/binding-linux-arm64-gnu': 1.66.0 + '@oxlint/binding-linux-arm64-musl': 1.66.0 + '@oxlint/binding-linux-ppc64-gnu': 1.66.0 + '@oxlint/binding-linux-riscv64-gnu': 1.66.0 + '@oxlint/binding-linux-riscv64-musl': 1.66.0 + '@oxlint/binding-linux-s390x-gnu': 1.66.0 + '@oxlint/binding-linux-x64-gnu': 1.66.0 + '@oxlint/binding-linux-x64-musl': 1.66.0 + '@oxlint/binding-openharmony-arm64': 1.66.0 + '@oxlint/binding-win32-arm64-msvc': 1.66.0 + '@oxlint/binding-win32-ia32-msvc': 1.66.0 + '@oxlint/binding-win32-x64-msvc': 1.66.0 + oxlint-tsgolint: 0.23.0 + + package-json-from-dist@1.0.1: {} + + path-scurry@2.0.2: + dependencies: + lru-cache: 11.5.1 + minipass: 7.1.3 + + pathe@2.0.3: {} + + picocolors@1.1.1: {} + + picomatch@4.0.4: {} + + pirates@4.0.7: {} + + pkg-types@1.3.1: + dependencies: + confbox: 0.1.8 + mlly: 1.8.2 + pathe: 2.0.3 + + postcss-load-config@6.0.1(postcss@8.5.15)(tsx@4.22.3): + dependencies: + lilconfig: 3.1.3 + optionalDependencies: + postcss: 8.5.15 + tsx: 4.22.3 + + postcss@8.5.15: + dependencies: + nanoid: 3.3.12 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + readdirp@4.1.2: {} + + resolve-from@5.0.0: {} + + rimraf@6.1.3: + dependencies: + glob: 13.0.6 + package-json-from-dist: 1.0.1 + + rolldown@1.0.2: + dependencies: + '@oxc-project/types': 0.132.0 + '@rolldown/pluginutils': 1.0.1 + optionalDependencies: + '@rolldown/binding-android-arm64': 1.0.2 + '@rolldown/binding-darwin-arm64': 1.0.2 + '@rolldown/binding-darwin-x64': 1.0.2 + '@rolldown/binding-freebsd-x64': 1.0.2 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.2 + '@rolldown/binding-linux-arm64-gnu': 1.0.2 + '@rolldown/binding-linux-arm64-musl': 1.0.2 + '@rolldown/binding-linux-ppc64-gnu': 1.0.2 + '@rolldown/binding-linux-s390x-gnu': 1.0.2 + '@rolldown/binding-linux-x64-gnu': 1.0.2 + '@rolldown/binding-linux-x64-musl': 1.0.2 + '@rolldown/binding-openharmony-arm64': 1.0.2 + '@rolldown/binding-wasm32-wasi': 1.0.2 + '@rolldown/binding-win32-arm64-msvc': 1.0.2 + '@rolldown/binding-win32-x64-msvc': 1.0.2 + + rollup@4.61.0: + dependencies: + '@types/estree': 1.0.9 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.61.0 + '@rollup/rollup-android-arm64': 4.61.0 + '@rollup/rollup-darwin-arm64': 4.61.0 + '@rollup/rollup-darwin-x64': 4.61.0 + '@rollup/rollup-freebsd-arm64': 4.61.0 + '@rollup/rollup-freebsd-x64': 4.61.0 + '@rollup/rollup-linux-arm-gnueabihf': 4.61.0 + '@rollup/rollup-linux-arm-musleabihf': 4.61.0 + '@rollup/rollup-linux-arm64-gnu': 4.61.0 + '@rollup/rollup-linux-arm64-musl': 4.61.0 + '@rollup/rollup-linux-loong64-gnu': 4.61.0 + '@rollup/rollup-linux-loong64-musl': 4.61.0 + '@rollup/rollup-linux-ppc64-gnu': 4.61.0 + '@rollup/rollup-linux-ppc64-musl': 4.61.0 + '@rollup/rollup-linux-riscv64-gnu': 4.61.0 + '@rollup/rollup-linux-riscv64-musl': 4.61.0 + '@rollup/rollup-linux-s390x-gnu': 4.61.0 + '@rollup/rollup-linux-x64-gnu': 4.61.0 + '@rollup/rollup-linux-x64-musl': 4.61.0 + '@rollup/rollup-openbsd-x64': 4.61.0 + '@rollup/rollup-openharmony-arm64': 4.61.0 + '@rollup/rollup-win32-arm64-msvc': 4.61.0 + '@rollup/rollup-win32-ia32-msvc': 4.61.0 + '@rollup/rollup-win32-x64-gnu': 4.61.0 + '@rollup/rollup-win32-x64-msvc': 4.61.0 + fsevents: 2.3.3 + + siginfo@2.0.0: {} + + source-map-js@1.2.1: {} + + source-map@0.7.6: {} + + stackback@0.0.2: {} + + std-env@4.1.0: {} + + sucrase@3.35.1: + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + commander: 4.1.1 + lines-and-columns: 1.2.4 + mz: 2.7.0 + pirates: 4.0.7 + tinyglobby: 0.2.16 + ts-interface-checker: 0.1.13 + + thenify-all@1.6.0: + dependencies: + thenify: 3.3.1 + + thenify@3.3.1: + dependencies: + any-promise: 1.3.0 + + tinybench@2.9.0: {} + + tinyexec@0.3.2: {} + + tinyexec@1.1.2: {} + + tinyglobby@0.2.16: + dependencies: + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + + tinypool@2.1.0: {} + + tinyrainbow@3.1.0: {} + + tree-kill@1.2.2: {} + + ts-interface-checker@0.1.13: {} + + tslib@2.8.1: + optional: true + + tsup@8.5.1(postcss@8.5.15)(tsx@4.22.3)(typescript@5.9.3): + dependencies: + bundle-require: 5.1.0(esbuild@0.27.7) + cac: 6.7.14 + chokidar: 4.0.3 + consola: 3.4.2 + debug: 4.4.3 + esbuild: 0.27.7 + fix-dts-default-cjs-exports: 1.0.1 + joycon: 3.1.1 + picocolors: 1.1.1 + postcss-load-config: 6.0.1(postcss@8.5.15)(tsx@4.22.3) + resolve-from: 5.0.0 + rollup: 4.61.0 + source-map: 0.7.6 + sucrase: 3.35.1 + tinyexec: 0.3.2 + tinyglobby: 0.2.16 + tree-kill: 1.2.2 + optionalDependencies: + postcss: 8.5.15 + typescript: 5.9.3 + transitivePeerDependencies: + - jiti + - supports-color + - tsx + - yaml + + tsx@4.22.3: + dependencies: + esbuild: 0.28.0 + optionalDependencies: + fsevents: 2.3.3 + optional: true + + typescript@5.9.3: {} + + ufo@1.6.4: {} + + undici-types@7.24.6: {} + + vite@8.0.14(@types/node@25.9.1)(esbuild@0.27.7)(tsx@4.22.3): + dependencies: + lightningcss: 1.32.0 + picomatch: 4.0.4 + postcss: 8.5.15 + rolldown: 1.0.2 + tinyglobby: 0.2.16 + optionalDependencies: + '@types/node': 25.9.1 + esbuild: 0.27.7 + fsevents: 2.3.3 + tsx: 4.22.3 + + vitest@4.1.7(@types/node@25.9.1)(vite@8.0.14(@types/node@25.9.1)(esbuild@0.27.7)(tsx@4.22.3)): + dependencies: + '@vitest/expect': 4.1.7 + '@vitest/mocker': 4.1.7(vite@8.0.14(@types/node@25.9.1)(esbuild@0.27.7)(tsx@4.22.3)) + '@vitest/pretty-format': 4.1.7 + '@vitest/runner': 4.1.7 + '@vitest/snapshot': 4.1.7 + '@vitest/spy': 4.1.7 + '@vitest/utils': 4.1.7 + es-module-lexer: 2.1.0 + expect-type: 1.3.0 + magic-string: 0.30.21 + obug: 2.1.1 + pathe: 2.0.3 + picomatch: 4.0.4 + std-env: 4.1.0 + tinybench: 2.9.0 + tinyexec: 1.1.2 + tinyglobby: 0.2.16 + tinyrainbow: 3.1.0 + vite: 8.0.14(@types/node@25.9.1)(esbuild@0.27.7)(tsx@4.22.3) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 25.9.1 + transitivePeerDependencies: + - msw + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 diff --git a/spec-generators/src/defaults.ts b/spec-generators/src/defaults.ts new file mode 100644 index 00000000..189687e9 --- /dev/null +++ b/spec-generators/src/defaults.ts @@ -0,0 +1,77 @@ +/** + * Default options for the v1 spec. Future spec versions can ship + * their own defaults alongside these without breaking v1 callers. + */ + +/** + * Per-spec-category routing: tells the generator which + * `crate::Node::` to wrap a node in for its + * `From for crate::Node` impl. For categories reached through a + * union enum (e.g. `link` → `LinkNode`), the impl does + * `crate::Node::Link(val.into())` — `.into()` lands in the category + * union first, then the variant wraps it in `Node`. `topLevel` skips + * the union step; that distinction lands when `topLevel` does. + */ +export interface CategoryRouting { + /** The `crate::Node::` constructor the node routes through. */ + readonly nodeVariant: string; +} + +/** + * Routing table for spec categories whose nodes the generator emits in + * v1. Categories absent from this map are not generated yet (today + * that's every category except `link`). + */ +export const CATEGORY_ROUTING: ReadonlyMap = new Map([['link', { nodeVariant: 'Link' }]]); + +/** + * Mapping from spec category name to the output subdirectory the + * generator emits its entities into (relative to `generated/`). The + * empty string places `topLevel` entities at the root. + * + * Only categories with a corresponding routing entry in + * {@link CATEGORY_ROUTING} are emitted in v1; the rest stay + * hand-written for now and will land in future PRs. + */ +export const CATEGORY_DIRECTORIES: ReadonlyMap = new Map([ + ['contextualValue', 'contextual_value_nodes'], + ['count', 'count_nodes'], + ['discriminator', 'discriminator_nodes'], + ['link', 'link_nodes'], + ['pdaSeed', 'pda_seed_nodes'], + ['shared', 'shared'], + ['topLevel', ''], + ['type', 'type_nodes'], + ['value', 'value_nodes'], +]); + +/** + * Per-spec-union Rust-side name overrides. + * + * Most spec union names map directly to their PascalCase Rust enum: + * `linkNode` → `LinkNode`, `typeNode` → `TypeNode`. A handful of + * unions are exposed under a different name in the Rust crate for + * historic API-stability reasons; those overrides live here. + * + * Keys are spec union names (camelCase); values are Rust enum names + * (PascalCase). Unions absent from this map use the default + * `pascalCase(specName)` mapping. + */ +export const UNION_NAME_OVERRIDES: ReadonlyMap = new Map([ + ['conditionalValueCondition', 'ConditionNode'], + ['instructionByteDeltaValue', 'InstructionByteDeltaNodeValue'], + ['instructionRemainingAccountsValue', 'InstructionRemainingAccountsNodeValue'], + ['pdaSeedValueValue', 'PdaSeedValueValueNode'], + ['pdaValuePda', 'PdaValue'], + ['pdaValueProgramId', 'PdaProgramIdValueNode'], +]); + +/** + * Per-spec-enumeration Rust-side name overrides. Same rule as + * {@link UNION_NAME_OVERRIDES}: present only when the Rust enum name + * differs from `pascalCase(specName)`. + */ +export const ENUMERATION_NAME_OVERRIDES: ReadonlyMap = new Map([ + ['endianness', 'Endian'], + ['optionalAccountStrategy', 'InstructionOptionalAccountStrategy'], +]); diff --git a/spec-generators/src/fragments/attributeBodyLine.ts b/spec-generators/src/fragments/attributeBodyLine.ts new file mode 100644 index 00000000..c18bd994 --- /dev/null +++ b/spec-generators/src/fragments/attributeBodyLine.ts @@ -0,0 +1,66 @@ +import { snakeCase } from '@codama/fragments'; +import { type Fragment, fragment, mergeFragments } from '@codama/fragments/rust'; +import type { AttributeSpec, TypeExpr } from '@codama/spec'; + +import { getTypeExprFragment } from './typeExpr'; + +/** Spec attribute names that collide with Rust keywords and need `r#` escaping. */ +const RUST_KEYWORDS: ReadonlySet = new Set(['type']); + +/** + * Render one spec attribute as a Rust struct field plus any preceding + * `#[serde(…)]` attribute. The fragment carries the crate-rooted + * imports referenced by the field type. Leading indentation is left + * to `rustfmt`. + * + * Serde rules: + * + * - Required, non-Vec field → no `#[serde]` attr. + * - Optional single → `Option` + `#[serde(skip_serializing_if = "crate::is_default")]`. + * - Optional Vec → bare `Vec` + `#[serde(default, skip_serializing_if = "crate::is_default")]`. + * - `docs` field → `Docs` + `#[serde(default, skip_serializing_if = "crate::is_default")]`. + */ +export function getAttributeBodyLineFragment(attr: AttributeSpec): Fragment { + const inner = getTypeExprFragment(attr.type); + const isOptional = attr.optional === true; + const isVecLike = isVecLikeType(attr.type); + + let typeFragment: Fragment; + let serdeAttr: string; + if (isOptional) { + if (isVecLike) { + typeFragment = inner; + serdeAttr = '#[serde(default, skip_serializing_if = "crate::is_default")]'; + } else { + typeFragment = fragment`Option<${inner}>`; + serdeAttr = '#[serde(skip_serializing_if = "crate::is_default")]'; + } + } else if (isDocsType(attr.type)) { + // `docs` attributes are technically required by the spec but + // their absence-in-JSON case needs explicit defaulting. Match + // the hand-written convention. + typeFragment = inner; + serdeAttr = '#[serde(default, skip_serializing_if = "crate::is_default")]'; + } else { + typeFragment = inner; + serdeAttr = ''; + } + + const fieldName = rustFieldName(attr.name); + const fieldLine = fragment`pub ${fieldName}: ${typeFragment},`; + if (serdeAttr === '') return fieldLine; + return mergeFragments([fragment`${serdeAttr}`, fieldLine], parts => parts.join('\n')); +} + +function isVecLikeType(type: TypeExpr): boolean { + return type.kind === 'array'; +} + +function isDocsType(type: TypeExpr): boolean { + return type.kind === 'docs'; +} + +function rustFieldName(specName: string): string { + const snake = snakeCase(specName); + return RUST_KEYWORDS.has(snake) ? `r#${snake}` : snake; +} diff --git a/spec-generators/src/fragments/fromImpl.ts b/spec-generators/src/fragments/fromImpl.ts new file mode 100644 index 00000000..3ee82440 --- /dev/null +++ b/spec-generators/src/fragments/fromImpl.ts @@ -0,0 +1,18 @@ +import { pascalCase } from '@codama/fragments'; +import { type Fragment, fragment } from '@codama/fragments/rust'; +import type { NodeSpec } from '@codama/spec'; + +import type { CategoryRouting } from '../defaults'; + +/** + * Render the `impl From for crate::Node` block that routes + * the node through its category's union variant (see + * {@link CategoryRouting}). + * + * `crate::Node` is written with its absolute path, so no import is + * added to the fragment's import map. + */ +export function getFromImplFragment(node: NodeSpec, routing: CategoryRouting): Fragment { + const structName = pascalCase(node.kind); + return fragment`impl From<${structName}> for crate::Node {\nfn from(val: ${structName}) -> Self {\ncrate::Node::${routing.nodeVariant}(val.into())\n}\n}`; +} diff --git a/spec-generators/src/fragments/hasNameImpl.ts b/spec-generators/src/fragments/hasNameImpl.ts new file mode 100644 index 00000000..21cfdb9a --- /dev/null +++ b/spec-generators/src/fragments/hasNameImpl.ts @@ -0,0 +1,54 @@ +import { pascalCase } from '@codama/fragments'; +import { type Fragment, fragment, mergeFragments } from '@codama/fragments/rust'; +import type { NodeSpec } from '@codama/spec'; + +import { use } from './helpers'; + +/** + * `true` when the node has a `name: stringIdentifier()` attribute — + * the only condition under which the generator emits a `HasName` + * impl. Matches the spec attribute shape (not the field type) so a + * future spec rename surfaces as a build failure rather than a silent + * skip. + */ +export function nodeHasName(node: NodeSpec): boolean { + return node.attributes.some( + a => a.name === 'name' && a.type.kind === 'string' && a.type.constraint === 'identifier', + ); +} + +/** + * Render the `impl HasName for XxxNode` block for a struct, or + * `undefined` when the node doesn't satisfy {@link nodeHasName}. + * + * The struct's own name is interpolated (not `use`d) because the + * struct lives in the same file; importing it would emit an invalid + * `use crate::Self;` line. + */ +export function getStructHasNameImplFragment(node: NodeSpec): Fragment | undefined { + if (!nodeHasName(node)) return undefined; + const structName = pascalCase(node.kind); + return fragment`impl ${use('crate::HasName')} for ${structName} {\nfn name(&self) -> &${use( + 'crate::CamelCaseString', + )} {\n&self.name\n}\n}`; +} + +/** + * Render the `impl HasName for XxxUnion` block for a union enum + * whose every member node has a `name: stringIdentifier()` attribute, + * dispatching each variant to the underlying node's `name()` via a + * `match` arm. Returns `undefined` when any member lacks a name. + */ +export function getUnionHasNameImplFragment( + unionName: string, + variants: readonly { readonly name: string; readonly node: NodeSpec }[], +): Fragment | undefined { + if (!variants.every(v => nodeHasName(v.node))) return undefined; + const arms = mergeFragments( + variants.map(v => fragment`${unionName}::${v.name}(node) => node.name(),`), + parts => parts.join('\n'), + ); + return fragment`impl ${use('crate::HasName')} for ${unionName} {\nfn name(&self) -> &${use( + 'crate::CamelCaseString', + )} {\nmatch self {\n${arms}\n}\n}\n}`; +} diff --git a/spec-generators/src/fragments/helpers.ts b/spec-generators/src/fragments/helpers.ts new file mode 100644 index 00000000..6d5b2049 --- /dev/null +++ b/spec-generators/src/fragments/helpers.ts @@ -0,0 +1,28 @@ +import { addFragmentImports, type Fragment, fragment } from '@codama/fragments/rust'; + +/** + * Build a fragment that references a Rust identifier reachable via + * the given fully-qualified path. The fragment's content is the + * trailing segment of the path; its import map carries a single + * matching `use ;` entry. + * + * Rust analogue of the JS subpath's `use(identifier, module)` helper. + * Unlike the JS version, `use` takes the full path in one string + * because Rust paths are absolute and self-describing. + * + * @example + * ```ts + * use('crate::CamelCaseString'); + * // content: `CamelCaseString` + * // imports: { 'crate::CamelCaseString' } + * + * use('codama_nodes_derive::node'); + * // content: `node` + * // imports: { 'codama_nodes_derive::node' } + * ``` + */ +export function use(path: string): Fragment { + const name = path.split('::').at(-1); + if (!name) throw new Error(`use(): empty Rust path "${path}"`); + return addFragmentImports(fragment`${name}`, [path]); +} diff --git a/spec-generators/src/fragments/index.ts b/spec-generators/src/fragments/index.ts new file mode 100644 index 00000000..2fa0788e --- /dev/null +++ b/spec-generators/src/fragments/index.ts @@ -0,0 +1,10 @@ +export * from './attributeBodyLine'; +export * from './fromImpl'; +export * from './hasNameImpl'; +export * from './helpers'; +export * from './modPage'; +export * from './nodePage'; +export * from './nodeStructFragment'; +export * from './page'; +export * from './typeExpr'; +export * from './unionPage'; diff --git a/spec-generators/src/fragments/modPage.ts b/spec-generators/src/fragments/modPage.ts new file mode 100644 index 00000000..96a0032b --- /dev/null +++ b/spec-generators/src/fragments/modPage.ts @@ -0,0 +1,70 @@ +import { createRenderMap, type Path, pathBasename, pathDirectory, type RenderMap } from '@codama/fragments'; +import { type Fragment, fragment, mergeFragments } from '@codama/fragments/rust'; + +/** + * Build the per-folder and root `mod.rs` re-export pages from a set of + * already-emitted spec pages. Each `mod.rs` lists subdirectory and + * file siblings via `mod xxx;` + `pub use xxx::*;` blocks. + * + * Top-level files (directly under `generated/`) plus subdirectories + * (one per spec category that the generator emits) flow into the root + * `mod.rs`. Each subdirectory gets its own `mod.rs` listing its + * per-node and per-union files. + */ +export function getModPagesRenderMap(specPages: RenderMap): RenderMap { + const filesByFolder = groupPathsByFolder([...specPages.keys()]); + const entries: Record = {}; + + const topLevelFiles = filesByFolder.get('') ?? []; + const subdirs: string[] = []; + for (const [folder, names] of filesByFolder) { + if (folder === '') continue; + entries[`${folder}/mod.rs`] = getModPageFragment(names); + subdirs.push(folder); + } + + const topLevelMod = topLevelFiles.length > 0 ? getModPageFragment(topLevelFiles) : undefined; + const subdirsMod = subdirs.length > 0 ? getModPageFragment(subdirs) : undefined; + entries['mod.rs'] = mergeFragments([topLevelMod, subdirsMod], parts => `${parts.join('\n\n')}\n`); + + return createRenderMap(entries); +} + +/** + * Render a `mod.rs` body that alphabetically declares + re-exports + * every supplied module name: + * + * mod a; + * mod b; + * + * pub use a::*; + * pub use b::*; + */ +export function getModPageFragment(names: readonly string[]): Fragment { + const sorted = [...names].toSorted((a, b) => a.localeCompare(b)); + const modLines = sorted.map(n => `mod ${n};`).join('\n'); + const useLines = sorted.map(n => `pub use ${n}::*;`).join('\n'); + return fragment`${modLines}\n\n${useLines}\n`; +} + +/** + * Group `.rs`-suffixed paths by their parent folder. Top-level files + * (no slash) land under the `''` key. The `.rs` extension is stripped + * from each basename so the result feeds directly into + * {@link getModPageFragment}. + */ +export function groupPathsByFolder(paths: readonly Path[]): Map { + const byFolder = new Map(); + for (const path of paths) { + const withoutExtension = path.endsWith('.rs') ? path.slice(0, -3) : path; + // `pathDirectory('AccountNode')` returns `'.'` on Node; normalise + // to `''` so the top-level sentinel stays consistent. + const directory = pathDirectory(withoutExtension); + const folder = directory === '.' ? '' : directory; + const basename = pathBasename(withoutExtension); + const names = byFolder.get(folder) ?? []; + names.push(basename); + byFolder.set(folder, names); + } + return byFolder; +} diff --git a/spec-generators/src/fragments/nodePage.ts b/spec-generators/src/fragments/nodePage.ts new file mode 100644 index 00000000..3d3df00d --- /dev/null +++ b/spec-generators/src/fragments/nodePage.ts @@ -0,0 +1,24 @@ +import { type Fragment, mergeFragments } from '@codama/fragments/rust'; +import type { NodeSpec } from '@codama/spec'; + +import type { CategoryRouting } from '../defaults'; +import { getFromImplFragment } from './fromImpl'; +import { getStructHasNameImplFragment } from './hasNameImpl'; +import { getNodeStructFragment } from './nodeStructFragment'; + +/** + * The body for one node's generated source file: + * + * 1. `#[node] pub struct XxxNode { … }` ({@link getNodeStructFragment}). + * 2. `impl From for crate::Node { … }` ({@link getFromImplFragment}). + * 3. `impl HasName for XxxNode { … }` ({@link getStructHasNameImplFragment}), + * when applicable. + */ +export function getNodePageFragment(node: NodeSpec, routing: CategoryRouting): Fragment { + const blocks: (Fragment | undefined)[] = [ + getNodeStructFragment(node), + getFromImplFragment(node, routing), + getStructHasNameImplFragment(node), + ]; + return mergeFragments(blocks, parts => parts.join('\n\n')); +} diff --git a/spec-generators/src/fragments/nodeStructFragment.ts b/spec-generators/src/fragments/nodeStructFragment.ts new file mode 100644 index 00000000..c00a0e33 --- /dev/null +++ b/spec-generators/src/fragments/nodeStructFragment.ts @@ -0,0 +1,50 @@ +import { pascalCase } from '@codama/fragments'; +import { type Fragment, fragment, mergeFragments } from '@codama/fragments/rust'; +import { isChildAttribute, type AttributeSpec, type NodeSpec } from '@codama/spec'; + +import { getAttributeBodyLineFragment } from './attributeBodyLine'; +import { use } from './helpers'; + +/** + * Render the `#[node] pub struct XxxNode { … }` declaration for one + * spec node. Attributes are partitioned into "Data." (primitives, + * enumerations) and "Children." (node / union / nestedUnion refs, + * recursively through `array` / `tuple`) sections; within each + * section, fields appear in spec declaration order. Empty sections + * are omitted. + */ +export function getNodeStructFragment(node: NodeSpec): Fragment { + const structName = pascalCase(node.kind); + const { data, children } = partitionAttributes(node); + + const body = buildBody(data, children); + return fragment`#[${use('codama_nodes_derive::node')}]\npub struct ${structName} {\n${body}\n}`; +} + +interface PartitionedAttributes { + readonly data: readonly AttributeSpec[]; + readonly children: readonly AttributeSpec[]; +} + +function partitionAttributes(node: NodeSpec): PartitionedAttributes { + const data: AttributeSpec[] = []; + const children: AttributeSpec[] = []; + for (const attr of node.attributes) { + if (isChildAttribute(attr.type)) children.push(attr); + else data.push(attr); + } + return { data, children }; +} + +function buildBody(data: readonly AttributeSpec[], children: readonly AttributeSpec[]): Fragment { + const sections: Fragment[] = []; + if (data.length > 0) sections.push(buildSection('// Data.', data)); + if (children.length > 0) sections.push(buildSection('// Children.', children)); + return mergeFragments(sections, parts => parts.join('\n\n')); +} + +function buildSection(header: string, attrs: readonly AttributeSpec[]): Fragment { + const lines = attrs.map(getAttributeBodyLineFragment); + const body = mergeFragments(lines, parts => parts.join('\n')); + return fragment`${header}\n${body}`; +} diff --git a/spec-generators/src/fragments/page.ts b/spec-generators/src/fragments/page.ts new file mode 100644 index 00000000..d0565f43 --- /dev/null +++ b/spec-generators/src/fragments/page.ts @@ -0,0 +1,41 @@ +import { type Fragment, fragment, type ImportMap, importMapToString, mergeFragments } from '@codama/fragments/rust'; + +/** + * Render a Rust source page from a body fragment: prepend a `use` + * block built from the fragment's import map, then the body content. + * + * `use crate::Foo;` lines are collapsed into a single + * `use crate::{Foo, Bar};` to match the hand-written convention; + * non-`crate::` paths stay on their own lines, sorted alphabetically. + * + * This grouping has to happen here because stable `rustfmt` (the + * codama-rs toolchain) sorts `use` lines but won't merge them — + * `imports_granularity` is a nightly-only knob. + */ +export function getPageFragment(body: Fragment): Fragment { + if (body.imports.size === 0) return body; + const importBlock = formatImports(body.imports); + return mergeFragments([fragment`${importBlock}`, body], parts => parts.join('\n\n').trimEnd() + '\n'); +} + +function formatImports(importMap: ImportMap): string { + const lines = importMapToString(importMap) + .split('\n') + .filter(line => line !== ''); + + const crateRefs: string[] = []; + const other: string[] = []; + for (const line of lines) { + const match = /^use crate::([A-Za-z0-9_]+);$/.exec(line); + if (match) crateRefs.push(match[1]); + else other.push(line); + } + + const output: string[] = []; + if (crateRefs.length > 0) { + const sorted = [...crateRefs].toSorted((a, b) => a.localeCompare(b)); + output.push(sorted.length === 1 ? `use crate::${sorted[0]};` : `use crate::{${sorted.join(', ')}};`); + } + output.push(...other.toSorted((a, b) => a.localeCompare(b))); + return output.join('\n'); +} diff --git a/spec-generators/src/fragments/typeExpr.ts b/spec-generators/src/fragments/typeExpr.ts new file mode 100644 index 00000000..dfab078a --- /dev/null +++ b/spec-generators/src/fragments/typeExpr.ts @@ -0,0 +1,100 @@ +import { pascalCase } from '@codama/fragments'; +import { type Fragment, fragment, mergeFragments } from '@codama/fragments/rust'; +import type { TypeExpr } from '@codama/spec'; + +import { ENUMERATION_NAME_OVERRIDES, UNION_NAME_OVERRIDES } from '../defaults'; +import { use } from './helpers'; + +const NUMBER_FORMAT_TO_RUST: ReadonlyMap = new Map([ + ['u8', 'u8'], + ['u16', 'u16'], + ['u32', 'u32'], + ['u64', 'u64'], + ['u128', 'u128'], + ['i8', 'i8'], + ['i16', 'i16'], + ['i32', 'i32'], + ['i64', 'i64'], + ['i128', 'i128'], +]); + +const FLOAT_WIDTH_TO_RUST: ReadonlyMap = new Map([ + ['f32', 'f32'], + ['f64', 'f64'], +]); + +/** + * Translate a spec {@link TypeExpr} to a Rust type expression. The + * fragment's content is the rendered type text (e.g. + * `Option`) and its import map carries the + * `crate::` paths the surrounding file needs. + * + * v1 mapping: + * + * - `address` → `String` + * - `string` (no constraint) → `String` + * - `string` (`identifier`) → `CamelCaseString` + * - `string` (`version`) → `String` + * - `boolean` → `bool` + * - `integer`/`float` → `uN` / `iN` / `fN` by width + * - `docs` → `Docs` + * - `enumeration('foo')` → `Foo` (subject to overrides) + * - `node('fooBar')` → `FooBar` + * - `union('fooBar')` → `FooBar` (subject to overrides) + * - `nestedUnion('alias','k')` → `Alias` (only `nestedTypeNode` in v1) + * - `array(of)` → `Vec` + * - `tuple(items)` → `(A, B, …)` + */ +export function getTypeExprFragment(expr: TypeExpr): Fragment { + switch (expr.kind) { + case 'address': + return fragment`String`; + case 'string': + if (expr.constraint === 'identifier') return use('crate::CamelCaseString'); + return fragment`String`; + case 'boolean': + return fragment`bool`; + case 'integer': { + const rust = NUMBER_FORMAT_TO_RUST.get(expr.width); + if (!rust) throw new Error(`unknown integer width "${expr.width}"`); + return fragment`${rust}`; + } + case 'float': { + const rust = FLOAT_WIDTH_TO_RUST.get(expr.width); + if (!rust) throw new Error(`unknown float width "${expr.width}"`); + return fragment`${rust}`; + } + case 'literal': + // Not used directly in any v1 node attribute; render best-effort. + return fragment`${JSON.stringify(expr.value)}`; + case 'literalUnion': + // Only `shared` enumerations use this directly, and we don't generate + // enumerations here. Fail loudly so a regression surfaces. + throw new Error('literalUnion TypeExpr is not supported at the node-attribute level in v1'); + case 'codamaVersion': + // No Rust analogue in v1; render as `String`. + return fragment`String`; + case 'docs': + return use('crate::Docs'); + case 'enumeration': + return use(`crate::${ENUMERATION_NAME_OVERRIDES.get(expr.name) ?? pascalCase(expr.name)}`); + case 'node': + return use(`crate::${pascalCase(expr.name)}`); + case 'union': + return use(`crate::${UNION_NAME_OVERRIDES.get(expr.name) ?? pascalCase(expr.name)}`); + case 'nestedUnion': { + // v1 only has one nested-union alias: `nestedTypeNode`. The + // Rust type alias is `NestedTypeNode`. + return fragment`${use(`crate::${pascalCase(expr.alias)}`)}<${use(`crate::${pascalCase(expr.name)}`)}>`; + } + case 'array': { + const inner = getTypeExprFragment(expr.of); + return fragment`Vec<${inner}>`; + } + case 'tuple': { + const items = expr.items.map(getTypeExprFragment); + const joined = mergeFragments(items, contents => contents.join(', ')); + return fragment`(${joined})`; + } + } +} diff --git a/spec-generators/src/fragments/unionPage.ts b/spec-generators/src/fragments/unionPage.ts new file mode 100644 index 00000000..defc6140 --- /dev/null +++ b/spec-generators/src/fragments/unionPage.ts @@ -0,0 +1,64 @@ +import { pascalCase } from '@codama/fragments'; +import { type Fragment, fragment, mergeFragments } from '@codama/fragments/rust'; +import type { NodeSpec, Spec, UnionSpec } from '@codama/spec'; + +import { UNION_NAME_OVERRIDES } from '../defaults'; +import { flattenNodeUnion } from '../unions'; +import { getUnionHasNameImplFragment } from './hasNameImpl'; +import { use } from './helpers'; + +/** + * The body for one spec union's generated source file: + * + * 1. `#[node_union] pub enum XxxUnion { Variant(MemberNode), … }` — + * variants are the union's flattened leaf nodes, sorted + * alphabetically. Variant name = pascalCase(kind) minus the + * union's implied node suffix (e.g. `accountLinkNode` in + * `linkNode` → `Account`). + * 2. `impl HasName for XxxUnion { … }` when every member node has a + * `name: stringIdentifier()` attribute. + */ +export function getUnionPageFragment(union: UnionSpec, spec: Spec): Fragment { + const unionName = UNION_NAME_OVERRIDES.get(union.name) ?? pascalCase(union.name); + const variants = buildVariants(union, spec); + + const enumFragment = buildEnumFragment(unionName, variants); + const hasNameFragment = getUnionHasNameImplFragment(unionName, variants); + + const blocks: (Fragment | undefined)[] = [enumFragment, hasNameFragment]; + return mergeFragments(blocks, parts => parts.join('\n\n')); +} + +interface UnionVariant { + /** The Rust variant name (PascalCase, stripped suffix). */ + readonly name: string; + /** The wrapped node spec (for `HasName` dispatch). */ + readonly node: NodeSpec; +} + +function buildVariants(union: UnionSpec, spec: Spec): readonly UnionVariant[] { + const suffix = pascalCase(union.name); + return [...flattenNodeUnion(union, spec)] + .map(node => ({ name: variantNameForNode(node.kind, suffix), node })) + .toSorted((a, b) => a.name.localeCompare(b.name)); +} + +/** + * Variant name = pascalCase(kind) minus the union's implied suffix. + * + * - `accountLinkNode` in union `linkNode` → `Account` + * - `fixedCountNode` in union `countNode` → `Fixed` + * - `variablePdaSeedNode` in union `pdaSeedNode` → `Variable` + */ +function variantNameForNode(nodeKind: string, suffix: string): string { + const pascal = pascalCase(nodeKind); + return pascal.endsWith(suffix) ? pascal.slice(0, pascal.length - suffix.length) : pascal; +} + +function buildEnumFragment(unionName: string, variants: readonly UnionVariant[]): Fragment { + const lines = mergeFragments( + variants.map(v => fragment`${v.name}(${use(`crate::${pascalCase(v.node.kind)}`)}),`), + parts => parts.join('\n'), + ); + return fragment`#[${use('codama_nodes_derive::node_union')}]\npub enum ${unionName} {\n${lines}\n}`; +} diff --git a/spec-generators/src/index.ts b/spec-generators/src/index.ts new file mode 100644 index 00000000..4e888404 --- /dev/null +++ b/spec-generators/src/index.ts @@ -0,0 +1,114 @@ +import { + createRenderMap, + deleteDirectory, + joinPath, + mergeRenderMaps, + type Path, + type RenderMap, + snakeCase, + writeRenderMap, +} from '@codama/fragments'; +import { type Fragment } from '@codama/fragments/rust'; +import { getSpec, type Spec } from '@codama/spec'; + +import { CATEGORY_ROUTING } from './defaults'; +import { getModPagesRenderMap, getNodePageFragment, getPageFragment, getUnionPageFragment } from './fragments'; +import { + buildRenderScope, + type GenerateOptions, + type RenderOptions, + type RenderScope, + validateRenderOptions, +} from './options'; +import { getRepoDirectory } from './repoDirectory'; +import { getEmittableUnions } from './unions'; + +export { + CATEGORY_DIRECTORIES, + type CategoryRouting, + CATEGORY_ROUTING, + ENUMERATION_NAME_OVERRIDES, + UNION_NAME_OVERRIDES, +} from './defaults'; +export { + buildRenderScope, + type GenerateOptions, + type RenderOptions, + type RenderScope, + type ResolvedRenderOptions, + validateRenderOptions, +} from './options'; + +export interface GenerateResult { + /** The output directory the generator wrote to. */ + readonly outputDir: Path; +} + +/** + * Run the generator against the embedded `@codama/spec` and write the + * full `codama-nodes/src/generated/` tree. The output directory is + * wiped before each run so stale files cannot survive. + */ +export function generate(): GenerateResult { + const spec = getSpec(); + const outputDir = joinPath(getRepoDirectory(), 'codama-nodes', 'src', 'generated'); + generateInto(spec, { outputDir, targetSpecMajor: 1 }); + return { outputDir }; +} + +/** + * Build the render map and write it to disk under `options.outputDir`. + * The target directory is wiped before each run so stale files cannot + * survive. No formatter is applied — chain `cargo fmt` afterwards. + */ +export function generateInto(spec: Spec, options: GenerateOptions): void { + const renderMap = getRenderMap(spec, options); + deleteDirectory(options.outputDir); + writeRenderMap(renderMap, options.outputDir); +} + +/** + * Pure-and-sync render-map entry point. Tests can call this directly + * without touching the filesystem and assert against individual + * entries via `getFromRenderMap`. + */ +export function getRenderMap(spec: Spec, options: RenderOptions): RenderMap { + validateRenderOptions(spec, options); + const scope = buildRenderScope(options); + const specPages = getSpecPagesRenderMap(spec, scope); + const modPages = getModPagesRenderMap(specPages); + return mergeRenderMaps([specPages, modPages]); +} + +/** + * Walk every spec category covered by {@link CATEGORY_ROUTING} and + * emit one page per node and one page per emittable union. Returns a + * render map keyed by output path (relative to `generated/`) with the + * resolved page fragment as the value. + */ +function getSpecPagesRenderMap(spec: Spec, scope: RenderScope): RenderMap { + const entries: Record = {}; + + for (const category of spec.categories) { + const routing = CATEGORY_ROUTING.get(category.name); + if (!routing) continue; + const folder = scope.categoryDirectories.get(category.name); + if (folder === undefined) { + throw new Error(`categoryDirectories has no entry for category "${category.name}".`); + } + + for (const node of category.nodes) { + const path = joinPath(folder, `${snakeCase(node.kind)}.rs`); + entries[path] = getPageFragment(getNodePageFragment(node, routing)); + } + for (const union of getEmittableUnions(category)) { + // The on-disk file name follows the *spec* union name in + // snake_case (`linkNode` → `link_node.rs`), independent of + // any Rust-side name override. + const path = joinPath(folder, `${snakeCase(union.name)}.rs`); + entries[path] = getPageFragment(getUnionPageFragment(union, spec)); + } + } + + return createRenderMap(entries); +} diff --git a/spec-generators/src/options.ts b/spec-generators/src/options.ts new file mode 100644 index 00000000..1baca108 --- /dev/null +++ b/spec-generators/src/options.ts @@ -0,0 +1,58 @@ +import { type Path } from '@codama/fragments'; +import type { Spec } from '@codama/spec'; + +import { CATEGORY_DIRECTORIES } from './defaults'; + +/** User-facing options for the spec generator. */ +export interface RenderOptions { + /** + * Map from each spec `category.name` to the output subdirectory + * its entities are emitted into (relative to `generated/`). Use an + * empty string for the top-level (no subdirectory). Omitted means + * "use the v1 defaults" ({@link CATEGORY_DIRECTORIES}). + */ + readonly categoryDirectories?: ReadonlyMap; + /** The spec major version this invocation targets. */ + readonly targetSpecMajor: number; +} + +/** Options consumed by {@link generate}, the disk-writing entry point. */ +export interface GenerateOptions extends RenderOptions { + readonly outputDir: Path; +} + +/** {@link RenderOptions} with every defaultable field resolved. */ +export type ResolvedRenderOptions = Required; + +/** Runtime context threaded through every fragment renderer. */ +export type RenderScope = ResolvedRenderOptions; + +export function resolveRenderOptions(options: RenderOptions): ResolvedRenderOptions { + return { + categoryDirectories: options.categoryDirectories ?? CATEGORY_DIRECTORIES, + targetSpecMajor: options.targetSpecMajor, + }; +} + +export function buildRenderScope(options: RenderOptions): RenderScope { + return Object.freeze(resolveRenderOptions(options)); +} + +/** + * Cross-check the caller-supplied options against the spec at + * generation time. Catches a mismatched major version. + */ +export function validateRenderOptions(spec: Spec, options: RenderOptions): void { + const actualMajor = parseSpecMajor(spec.version); + if (actualMajor !== options.targetSpecMajor) { + throw new Error( + `targetSpecMajor=${options.targetSpecMajor} but the supplied spec is at version "${spec.version}" (major ${actualMajor}).`, + ); + } +} + +function parseSpecMajor(version: string): number { + const match = /^(\d+)\./.exec(version); + if (!match) throw new Error(`unable to parse spec version "${version}".`); + return Number(match[1]); +} diff --git a/spec-generators/src/repoDirectory.ts b/spec-generators/src/repoDirectory.ts new file mode 100644 index 00000000..26753fad --- /dev/null +++ b/spec-generators/src/repoDirectory.ts @@ -0,0 +1,14 @@ +import { fileURLToPath } from 'node:url'; + +import { joinPath, pathDirectory } from '@codama/fragments'; + +/** + * Resolve the absolute path to the Rust monorepo root. + * + * The compiled bin lives at `/spec-generators/dist/.mjs`; + * resolving two levels up lands in the workspace root. + */ +export function getRepoDirectory(): string { + const here = pathDirectory(fileURLToPath(import.meta.url)); + return joinPath(here, '..', '..'); +} diff --git a/spec-generators/src/unions.ts b/spec-generators/src/unions.ts new file mode 100644 index 00000000..0ab4843b --- /dev/null +++ b/spec-generators/src/unions.ts @@ -0,0 +1,55 @@ +import type { NodeSpec, Spec, UnionSpec } from '@codama/spec'; + +/** + * Spec unions starting with `registered` are category-registry unions + * (e.g. `registeredLinkNode`); the Rust crate exposes one flattened + * enum per category instead, so we skip them and recurse through them + * via {@link flattenNodeUnion} when they appear as nested members. + */ +const REGISTERED_UNION_PREFIX = 'registered'; + +/** + * The spec unions in a category that the generator emits Rust enums + * for, sorted alphabetically by name for stable output. + */ +export function getEmittableUnions(category: Spec['categories'][number]): readonly UnionSpec[] { + return category.unions + .filter(u => !u.name.startsWith(REGISTERED_UNION_PREFIX)) + .toSorted((a, b) => a.name.localeCompare(b.name)); +} + +/** + * Walk a union's members, recursively expanding nested `union(...)` + * references down to their leaf nodes. Returns the flat list of + * concrete node specs in spec declaration order. + * + * `nestedUnion` members are not followed (they're name-aliased and + * break the cycle Rust-side). Unknown member references are skipped + * silently — the spec validator catches those upstream. + * + * `@codama/spec` doesn't expose a flatten helper of its own, so the + * traversal is implemented here against the explicit `spec` arg. + */ +export function flattenNodeUnion(union: UnionSpec, spec: Spec): readonly NodeSpec[] { + const unionByName = new Map(spec.categories.flatMap(c => c.unions).map(u => [u.name, u])); + const nodeByKind = new Map(spec.categories.flatMap(c => c.nodes).map(n => [n.kind, n])); + const out: NodeSpec[] = []; + const visited = new Set(); + const stack: string[] = [union.name]; + while (stack.length > 0) { + const name = stack.pop(); + if (name === undefined || visited.has(name)) continue; + visited.add(name); + const u = unionByName.get(name); + if (!u) continue; + for (const m of u.members) { + if (m.kind === 'node') { + const node = nodeByKind.get(m.name); + if (node) out.push(node); + } else if (m.kind === 'union') { + stack.push(m.name); + } + } + } + return out; +} diff --git a/spec-generators/test/fragments/attributeBodyLine.test.ts b/spec-generators/test/fragments/attributeBodyLine.test.ts new file mode 100644 index 00000000..ba54e1c1 --- /dev/null +++ b/spec-generators/test/fragments/attributeBodyLine.test.ts @@ -0,0 +1,37 @@ +import { attribute, boolean, docs, node, optionalAttribute, stringIdentifier } from '@codama/spec/api'; +import { describe, expect, it } from 'vitest'; + +import { getAttributeBodyLineFragment } from '../../src/fragments/attributeBodyLine'; + +describe('getAttributeBodyLineFragment', () => { + it('renders a required primitive field with no serde attribute and no leading indentation', () => { + const result = getAttributeBodyLineFragment(attribute('isWritable', boolean())); + // No leading whitespace — rustfmt restores indentation on the + // generated file. + expect(result.content).toBe('pub is_writable: bool,'); + }); + + it('renders a required stringIdentifier field as CamelCaseString with no serde attribute', () => { + const result = getAttributeBodyLineFragment(attribute('name', stringIdentifier())); + expect(result.content).toBe('pub name: CamelCaseString,'); + }); + + it('renders an optional single-valued node attribute with Option<…> + skip_serializing_if', () => { + const result = getAttributeBodyLineFragment(optionalAttribute('program', node('programLinkNode'))); + expect(result.content).toBe( + ['#[serde(skip_serializing_if = "crate::is_default")]', 'pub program: Option,'].join('\n'), + ); + }); + + it('renders a docs attribute with default + skip_serializing_if and no Option wrap', () => { + const result = getAttributeBodyLineFragment(attribute('docs', docs())); + expect(result.content).toBe( + ['#[serde(default, skip_serializing_if = "crate::is_default")]', 'pub docs: Docs,'].join('\n'), + ); + }); + + it('escapes the Rust keyword `type` as `r#type` in field position', () => { + const result = getAttributeBodyLineFragment(attribute('type', stringIdentifier())); + expect(result.content).toBe('pub r#type: CamelCaseString,'); + }); +}); diff --git a/spec-generators/test/fragments/fromImpl.test.ts b/spec-generators/test/fragments/fromImpl.test.ts new file mode 100644 index 00000000..a5f54402 --- /dev/null +++ b/spec-generators/test/fragments/fromImpl.test.ts @@ -0,0 +1,33 @@ +import { defineNode } from '@codama/spec/api'; +import { describe, expect, it } from 'vitest'; + +import { getFromImplFragment } from '../../src/fragments/fromImpl'; + +describe('getFromImplFragment', () => { + it('emits the From for crate::Node impl routing through the routing variant', () => { + const spec = defineNode('accountLinkNode', { attributes: [] }); + const result = getFromImplFragment(spec, { nodeVariant: 'Link' }); + // No inner indentation — rustfmt restores it. + expect(result.content).toBe( + [ + 'impl From for crate::Node {', + 'fn from(val: AccountLinkNode) -> Self {', + 'crate::Node::Link(val.into())', + '}', + '}', + ].join('\n'), + ); + }); + + it('carries no imports — crate::Node is referenced absolutely and the struct name is in scope', () => { + const spec = defineNode('accountLinkNode', { attributes: [] }); + const result = getFromImplFragment(spec, { nodeVariant: 'Link' }); + expect(result.imports.size).toBe(0); + }); + + it('uses the supplied routing variant verbatim (no normalisation)', () => { + const spec = defineNode('someNode', { attributes: [] }); + const result = getFromImplFragment(spec, { nodeVariant: 'Account' }); + expect(result.content).toContain('crate::Node::Account(val.into())'); + }); +}); diff --git a/spec-generators/test/fragments/hasNameImpl.test.ts b/spec-generators/test/fragments/hasNameImpl.test.ts new file mode 100644 index 00000000..cc5d6790 --- /dev/null +++ b/spec-generators/test/fragments/hasNameImpl.test.ts @@ -0,0 +1,95 @@ +import { attribute, defineNode, node, stringIdentifier } from '@codama/spec/api'; +import { describe, expect, it } from 'vitest'; + +import { + getStructHasNameImplFragment, + getUnionHasNameImplFragment, + nodeHasName, +} from '../../src/fragments/hasNameImpl'; + +describe('nodeHasName', () => { + it('is true when the node has a `name: stringIdentifier()` attribute', () => { + const spec = defineNode('programLinkNode', { + attributes: [attribute('name', stringIdentifier())], + }); + expect(nodeHasName(spec)).toBe(true); + }); + + it('is false when the node has no attributes at all', () => { + const spec = defineNode('emptyNode', { attributes: [] }); + expect(nodeHasName(spec)).toBe(false); + }); + + it('is false when the node has a `name` attribute but it is not a stringIdentifier', () => { + const spec = defineNode('exampleNode', { attributes: [attribute('name', node('programLinkNode'))] }); + expect(nodeHasName(spec)).toBe(false); + }); +}); + +describe('getStructHasNameImplFragment', () => { + it('returns the HasName impl for a node with a name: stringIdentifier() attribute', () => { + const spec = defineNode('programLinkNode', { + attributes: [attribute('name', stringIdentifier())], + }); + const result = getStructHasNameImplFragment(spec); + // No inner indentation — rustfmt restores it. + expect(result?.content).toBe( + ['impl HasName for ProgramLinkNode {', 'fn name(&self) -> &CamelCaseString {', '&self.name', '}', '}'].join( + '\n', + ), + ); + }); + + it('carries the crate imports for HasName and CamelCaseString — and NOT the struct name (defined in the same file)', () => { + const spec = defineNode('programLinkNode', { + attributes: [attribute('name', stringIdentifier())], + }); + const result = getStructHasNameImplFragment(spec); + const imports = [...(result?.imports.keys() ?? [])].toSorted(); + expect(imports).toEqual(['crate::CamelCaseString', 'crate::HasName']); + }); + + it('returns undefined for a node without a name: stringIdentifier() attribute', () => { + const spec = defineNode('emptyNode', { attributes: [] }); + expect(getStructHasNameImplFragment(spec)).toBeUndefined(); + }); +}); + +describe('getUnionHasNameImplFragment', () => { + const namedNode = (kind: string) => defineNode(kind, { attributes: [attribute('name', stringIdentifier())] }); + + it('returns the union HasName impl when every member has a name attribute', () => { + const variants = [ + { name: 'Account', node: namedNode('accountLinkNode') }, + { name: 'Program', node: namedNode('programLinkNode') }, + ]; + const result = getUnionHasNameImplFragment('LinkNode', variants); + expect(result?.content).toBe( + [ + 'impl HasName for LinkNode {', + 'fn name(&self) -> &CamelCaseString {', + 'match self {', + 'LinkNode::Account(node) => node.name(),', + 'LinkNode::Program(node) => node.name(),', + '}', + '}', + '}', + ].join('\n'), + ); + }); + + it('returns undefined when any member node lacks a name attribute', () => { + const variants = [ + { name: 'Account', node: namedNode('accountLinkNode') }, + { name: 'Empty', node: defineNode('emptyNode', { attributes: [] }) }, + ]; + expect(getUnionHasNameImplFragment('LinkNode', variants)).toBeUndefined(); + }); + + it('carries the crate imports for HasName and CamelCaseString', () => { + const variants = [{ name: 'Account', node: namedNode('accountLinkNode') }]; + const result = getUnionHasNameImplFragment('LinkNode', variants); + const imports = [...(result?.imports.keys() ?? [])].toSorted(); + expect(imports).toEqual(['crate::CamelCaseString', 'crate::HasName']); + }); +}); diff --git a/spec-generators/test/fragments/modPage.test.ts b/spec-generators/test/fragments/modPage.test.ts new file mode 100644 index 00000000..2670855b --- /dev/null +++ b/spec-generators/test/fragments/modPage.test.ts @@ -0,0 +1,57 @@ +import { createRenderMap, type Path } from '@codama/fragments'; +import { type Fragment, fragment } from '@codama/fragments/rust'; +import { describe, expect, it } from 'vitest'; + +import { getModPageFragment, getModPagesRenderMap, groupPathsByFolder } from '../../src/fragments/modPage'; + +function specPagesWithKeys(keys: readonly Path[]): ReadonlyMap { + const entries: Record = {}; + for (const k of keys) entries[k] = fragment`// stub`; + return createRenderMap(entries); +} + +describe('getModPageFragment', () => { + it('renders mod and pub use lines sorted alphabetically with a blank line between the two blocks', () => { + const result = getModPageFragment(['b', 'a', 'c']); + expect(result.content).toBe( + ['mod a;', 'mod b;', 'mod c;', '', 'pub use a::*;', 'pub use b::*;', 'pub use c::*;\n'].join('\n'), + ); + }); +}); + +describe('groupPathsByFolder', () => { + it('strips the .rs extension and groups by parent folder', () => { + const grouped = groupPathsByFolder(['a.rs', 'sub/b.rs', 'sub/c.rs']); + expect(grouped.get('')).toEqual(['a']); + expect(grouped.get('sub')).toEqual(['b', 'c']); + }); + + it('places extension-less paths and top-level paths under the empty-string sentinel folder', () => { + const grouped = groupPathsByFolder(['top']); + expect(grouped.get('')).toEqual(['top']); + }); +}); + +describe('getModPagesRenderMap', () => { + it('emits one mod.rs per subdirectory plus a root mod.rs that re-exports every subdirectory', () => { + const specPages = specPagesWithKeys(['link_nodes/account_link_node.rs', 'link_nodes/program_link_node.rs']); + const map = getModPagesRenderMap(specPages); + expect(map.has('link_nodes/mod.rs')).toBe(true); + expect(map.has('mod.rs')).toBe(true); + const linkMod = map.get('link_nodes/mod.rs')!.content; + expect(linkMod).toContain('mod account_link_node;'); + expect(linkMod).toContain('pub use program_link_node::*;'); + const rootMod = map.get('mod.rs')!.content; + expect(rootMod).toContain('mod link_nodes;'); + expect(rootMod).toContain('pub use link_nodes::*;'); + }); + + it('only emits a root mod.rs (no subdirectory mod.rs) when every spec page is top-level', () => { + const specPages = specPagesWithKeys(['account_node.rs', 'program_node.rs']); + const map = getModPagesRenderMap(specPages); + expect([...map.keys()].toSorted()).toEqual(['mod.rs']); + const rootMod = map.get('mod.rs')!.content; + expect(rootMod).toContain('mod account_node;'); + expect(rootMod).toContain('pub use program_node::*;'); + }); +}); diff --git a/spec-generators/test/fragments/nodePage.test.ts b/spec-generators/test/fragments/nodePage.test.ts new file mode 100644 index 00000000..a5f28529 --- /dev/null +++ b/spec-generators/test/fragments/nodePage.test.ts @@ -0,0 +1,38 @@ +import { attribute, defineNode, stringIdentifier } from '@codama/spec/api'; +import { describe, expect, it } from 'vitest'; + +import { getNodePageFragment } from '../../src/fragments/nodePage'; + +const linkRouting = { nodeVariant: 'Link' }; + +describe('getNodePageFragment', () => { + it('composes the struct, From impl, and HasName impl in order, separated by blank lines', () => { + const spec = defineNode('programLinkNode', { + attributes: [attribute('name', stringIdentifier())], + }); + const result = getNodePageFragment(spec, linkRouting); + const sections = result.content.split('\n\n'); + // [0] struct block, [1] From impl block, [2] HasName impl block. + expect(sections[0]).toMatch(/#\[node\][\s\S]*pub struct ProgramLinkNode/); + expect(sections[1]).toMatch(/^impl From/); + expect(sections[2]).toMatch(/^impl HasName for ProgramLinkNode/); + }); + + it('omits the HasName impl when the node has no name: stringIdentifier() attribute', () => { + const spec = defineNode('emptyNode', { attributes: [] }); + const out = getNodePageFragment(spec, linkRouting).content; + expect(out).not.toContain('HasName'); + }); + + it('propagates every fragment’s crate-rooted imports through to the page fragment', () => { + const spec = defineNode('programLinkNode', { + attributes: [attribute('name', stringIdentifier())], + }); + const result = getNodePageFragment(spec, linkRouting); + const imports = [...result.imports.keys()].toSorted(); + // `crate::Node` is written absolutely inside the From impl, so + // no import is emitted for it. The struct's own name is not + // imported either — it's defined in the same file. + expect(imports).toEqual(['codama_nodes_derive::node', 'crate::CamelCaseString', 'crate::HasName']); + }); +}); diff --git a/spec-generators/test/fragments/nodeStructFragment.test.ts b/spec-generators/test/fragments/nodeStructFragment.test.ts new file mode 100644 index 00000000..12803994 --- /dev/null +++ b/spec-generators/test/fragments/nodeStructFragment.test.ts @@ -0,0 +1,60 @@ +import { attribute, defineNode, node, optionalAttribute, stringIdentifier } from '@codama/spec/api'; +import { describe, expect, it } from 'vitest'; + +import { getNodeStructFragment } from '../../src/fragments/nodeStructFragment'; + +describe('getNodeStructFragment', () => { + it('emits the #[node] macro attribute and uses PascalCase struct name', () => { + const spec = defineNode('programLinkNode', { + attributes: [attribute('name', stringIdentifier())], + }); + const result = getNodeStructFragment(spec); + expect(result.content).toContain('#[node]'); + expect(result.content).toContain('pub struct ProgramLinkNode {'); + }); + + it('carries the codama_nodes_derive::node import so the page renderer brings the procedural macro into scope', () => { + const spec = defineNode('programLinkNode', { attributes: [] }); + const result = getNodeStructFragment(spec); + expect([...result.imports.keys()]).toContain('codama_nodes_derive::node'); + }); + + it('partitions data attributes before child attributes with section comments', () => { + const spec = defineNode('accountLinkNode', { + attributes: [optionalAttribute('program', node('programLinkNode')), attribute('name', stringIdentifier())], + }); + const out = getNodeStructFragment(spec).content; + const dataIdx = out.indexOf('// Data.'); + const childrenIdx = out.indexOf('// Children.'); + expect(dataIdx).toBeGreaterThan(-1); + expect(childrenIdx).toBeGreaterThan(-1); + expect(dataIdx).toBeLessThan(childrenIdx); + // No leading indentation on field lines — rustfmt restores it. + expect(out).toContain('// Data.\npub name: CamelCaseString,'); + expect(out).toContain( + [ + '// Children.', + '#[serde(skip_serializing_if = "crate::is_default")]', + 'pub program: Option,', + ].join('\n'), + ); + }); + + it('omits the Children. section when the node has no child attributes', () => { + const spec = defineNode('programLinkNode', { + attributes: [attribute('name', stringIdentifier())], + }); + const out = getNodeStructFragment(spec).content; + expect(out).toContain('// Data.'); + expect(out).not.toContain('// Children.'); + }); + + it('omits the Data. section when the node has no data attributes', () => { + const spec = defineNode('exampleNode', { + attributes: [attribute('program', node('programLinkNode'))], + }); + const out = getNodeStructFragment(spec).content; + expect(out).not.toContain('// Data.'); + expect(out).toContain('// Children.'); + }); +}); diff --git a/spec-generators/test/fragments/page.test.ts b/spec-generators/test/fragments/page.test.ts new file mode 100644 index 00000000..0943fa7c --- /dev/null +++ b/spec-generators/test/fragments/page.test.ts @@ -0,0 +1,42 @@ +import { addFragmentImports, fragment } from '@codama/fragments/rust'; +import { describe, expect, it } from 'vitest'; + +import { getPageFragment } from '../../src/fragments/page'; + +describe('getPageFragment', () => { + it('returns the input fragment unchanged when its imports map is empty', () => { + const body = fragment`pub struct Foo;`; + const result = getPageFragment(body); + expect(result.imports.size).toBe(0); + expect(result.content).toBe('pub struct Foo;'); + }); + + it('emits a single `use crate::Foo;` line when only one crate import is present', () => { + const body = addFragmentImports(fragment`pub type X = ProgramLinkNode;`, ['crate::ProgramLinkNode']); + const result = getPageFragment(body); + expect(result.content).toMatch(/^use crate::ProgramLinkNode;\n\npub type X/); + }); + + it('groups multiple crate:: imports into a single `use crate::{…}` line, sorted alphabetically', () => { + const body = addFragmentImports(fragment`pub type X = Foo;`, [ + 'crate::CamelCaseString', + 'crate::HasName', + 'crate::ProgramLinkNode', + ]); + const result = getPageFragment(body); + expect(result.content).toMatch(/^use crate::\{CamelCaseString, HasName, ProgramLinkNode\};/); + }); + + it('keeps non-crate paths on their own use lines, sorted after the crate group', () => { + const body = addFragmentImports(fragment`#[node]\npub struct Foo;`, ['codama_nodes_derive::node']); + const result = getPageFragment(body); + expect(result.content).toBe(['use codama_nodes_derive::node;', '', '#[node]', 'pub struct Foo;\n'].join('\n')); + }); + + it('ensures a single trailing newline on the rendered page', () => { + const body = addFragmentImports(fragment`pub type X = Foo;`, ['crate::ProgramLinkNode']); + const result = getPageFragment(body); + expect(result.content.endsWith('\n')).toBe(true); + expect(result.content.endsWith('\n\n')).toBe(false); + }); +}); diff --git a/spec-generators/test/fragments/typeExpr.test.ts b/spec-generators/test/fragments/typeExpr.test.ts new file mode 100644 index 00000000..95841f1f --- /dev/null +++ b/spec-generators/test/fragments/typeExpr.test.ts @@ -0,0 +1,120 @@ +import { + address, + array, + boolean, + docs, + enumeration, + literal, + literalUnion, + nestedUnion, + node, + string, + stringIdentifier, + stringVersion, + tuple, + u32, + union, +} from '@codama/spec/api'; +import { describe, expect, it } from 'vitest'; + +import { getTypeExprFragment } from '../../src/fragments/typeExpr'; + +describe('getTypeExprFragment', () => { + it('renders plain string as String', () => { + expect(getTypeExprFragment(string()).content).toBe('String'); + }); + + it('renders address as plain String for v1 (no brand, no import)', () => { + const result = getTypeExprFragment(address()); + expect(result.content).toBe('String'); + expect(result.imports.size).toBe(0); + }); + + it('renders boolean as bool with no import', () => { + const result = getTypeExprFragment(boolean()); + expect(result.content).toBe('bool'); + expect(result.imports.size).toBe(0); + }); + + it('renders integer widths to the matching Rust primitive', () => { + expect(getTypeExprFragment(u32()).content).toBe('u32'); + }); + + it('renders a string literal as a JSON-quoted source-form literal', () => { + expect(getTypeExprFragment(literal('codama')).content).toBe('"codama"'); + }); + + it('renders a boolean literal', () => { + expect(getTypeExprFragment(literal(true)).content).toBe('true'); + }); + + it('throws on literalUnion (only used inside spec enumerations, not as a node attribute type)', () => { + expect(() => getTypeExprFragment(literalUnion(1, 2))).toThrow(/literalUnion/); + }); + + it('routes stringIdentifier to CamelCaseString via a crate-rooted import', () => { + const result = getTypeExprFragment(stringIdentifier()); + expect(result.content).toBe('CamelCaseString'); + expect([...result.imports.keys()]).toEqual(['crate::CamelCaseString']); + }); + + it('routes stringVersion to plain String (no Version brand on the Rust side in v1)', () => { + const result = getTypeExprFragment(stringVersion()); + expect(result.content).toBe('String'); + expect(result.imports.size).toBe(0); + }); + + it('routes docs to Docs via a crate-rooted import', () => { + const result = getTypeExprFragment(docs()); + expect(result.content).toBe('Docs'); + expect([...result.imports.keys()]).toEqual(['crate::Docs']); + }); + + it('routes enumeration references through the override table when present', () => { + const result = getTypeExprFragment(enumeration('endianness')); + // The Rust name `Endian` comes from ENUMERATION_NAME_OVERRIDES. + expect(result.content).toBe('Endian'); + expect([...result.imports.keys()]).toEqual(['crate::Endian']); + }); + + it('routes node references via PascalCase content + crate import', () => { + const result = getTypeExprFragment(node('programLinkNode')); + expect(result.content).toBe('ProgramLinkNode'); + expect([...result.imports.keys()]).toEqual(['crate::ProgramLinkNode']); + }); + + it('routes union references through UNION_NAME_OVERRIDES when present', () => { + const result = getTypeExprFragment(union('pdaValuePda')); + expect(result.content).toBe('PdaValue'); + expect([...result.imports.keys()]).toEqual(['crate::PdaValue']); + }); + + it('routes union references via PascalCase content when no override', () => { + const result = getTypeExprFragment(union('typeNode')); + expect(result.content).toBe('TypeNode'); + expect([...result.imports.keys()]).toEqual(['crate::TypeNode']); + }); + + it('renders nestedUnion(alias, kind) as Alias with both crate imports', () => { + const result = getTypeExprFragment(nestedUnion('nestedTypeNode', 'numberTypeNode')); + expect(result.content).toBe('NestedTypeNode'); + const imports = [...result.imports.keys()].toSorted(); + expect(imports).toEqual(['crate::NestedTypeNode', 'crate::NumberTypeNode']); + }); + + it('renders array(T) as Vec and propagates the inner imports', () => { + const result = getTypeExprFragment(array(node('pdaSeedValueNode'))); + expect(result.content).toBe('Vec'); + expect([...result.imports.keys()]).toEqual(['crate::PdaSeedValueNode']); + }); + + it('handles nested array types', () => { + expect(getTypeExprFragment(array(array(boolean()))).content).toBe('Vec>'); + }); + + it('renders tuple(items) as (A, B, …) and merges the inner imports', () => { + const result = getTypeExprFragment(tuple(node('programLinkNode'), boolean())); + expect(result.content).toBe('(ProgramLinkNode, bool)'); + expect([...result.imports.keys()]).toEqual(['crate::ProgramLinkNode']); + }); +}); diff --git a/spec-generators/test/fragments/unionPage.test.ts b/spec-generators/test/fragments/unionPage.test.ts new file mode 100644 index 00000000..1662d709 --- /dev/null +++ b/spec-generators/test/fragments/unionPage.test.ts @@ -0,0 +1,50 @@ +import { getSpec } from '@codama/spec'; +import { describe, expect, it } from 'vitest'; + +import { getUnionPageFragment } from '../../src/fragments/unionPage'; + +const spec = getSpec(); +const linkCategory = spec.categories.find(c => c.name === 'link')!; +const linkUnion = linkCategory.unions.find(u => u.name === 'linkNode')!; + +describe('getUnionPageFragment', () => { + it('emits #[node_union] and a PascalCase enum name', () => { + const result = getUnionPageFragment(linkUnion, spec); + expect(result.content).toContain('#[node_union]'); + expect(result.content).toContain('pub enum LinkNode {'); + }); + + it('lists every member of the flattened union as a variant, sorted alphabetically', () => { + const result = getUnionPageFragment(linkUnion, spec); + // The spec union `linkNode` references `union(\'registeredLinkNode\')` + // which expands to 7 leaf nodes; the generator strips the + // `LinkNode` suffix and pascalCases the rest. + const variants = result.content.match(/^\s*(\w+)\(/gm)?.map(s => s.trim().replace('(', '')) ?? []; + expect(variants).toEqual([ + 'Account', + 'DefinedType', + 'Instruction', + 'InstructionAccount', + 'InstructionArgument', + 'Pda', + 'Program', + ]); + }); + + it('emits an `impl HasName for LinkNode` block dispatching to each variant', () => { + const result = getUnionPageFragment(linkUnion, spec); + expect(result.content).toContain('impl HasName for LinkNode {'); + expect(result.content).toContain('LinkNode::Account(node) => node.name(),'); + expect(result.content).toContain('LinkNode::Program(node) => node.name(),'); + }); + + it('carries the codama_nodes_derive::node_union import plus the crate-rooted imports for every member type and the HasName helpers', () => { + const result = getUnionPageFragment(linkUnion, spec); + const imports = [...result.imports.keys()].toSorted(); + expect(imports).toContain('codama_nodes_derive::node_union'); + expect(imports).toContain('crate::AccountLinkNode'); + expect(imports).toContain('crate::ProgramLinkNode'); + expect(imports).toContain('crate::HasName'); + expect(imports).toContain('crate::CamelCaseString'); + }); +}); diff --git a/spec-generators/test/generate.test.ts b/spec-generators/test/generate.test.ts new file mode 100644 index 00000000..496cb364 --- /dev/null +++ b/spec-generators/test/generate.test.ts @@ -0,0 +1,92 @@ +import { getFromRenderMap } from '@codama/fragments'; +import { getSpec } from '@codama/spec'; +import { describe, expect, it } from 'vitest'; + +import { type GenerateOptions, getRenderMap, validateRenderOptions } from '../src/index'; + +function options(overrides: Partial = {}): GenerateOptions { + return { + outputDir: '/tmp/unused', + targetSpecMajor: 1, + ...overrides, + }; +} + +describe('validateRenderOptions', () => { + const spec = getSpec(); + + it('accepts the active v1 spec with defaulted options', () => { + expect(() => validateRenderOptions(spec, options())).not.toThrow(); + }); + + it('throws when targetSpecMajor does not match the spec version', () => { + expect(() => validateRenderOptions(spec, options({ targetSpecMajor: 9 }))).toThrow( + /targetSpecMajor=9.*major 1/, + ); + }); + + it('throws on a malformed spec version', () => { + const broken = { ...spec, version: 'not-a-version' }; + expect(() => validateRenderOptions(broken, options())).toThrow(/unable to parse spec version "not-a-version"/); + }); +}); + +describe('getRenderMap', () => { + const map = getRenderMap(getSpec(), options()); + + it('emits one .rs file per link node, the link_node.rs union, link_nodes/mod.rs, and a root mod.rs', () => { + const keys = [...map.keys()].toSorted(); + expect(keys).toEqual([ + 'link_nodes/account_link_node.rs', + 'link_nodes/defined_type_link_node.rs', + 'link_nodes/instruction_account_link_node.rs', + 'link_nodes/instruction_argument_link_node.rs', + 'link_nodes/instruction_link_node.rs', + 'link_nodes/link_node.rs', + 'link_nodes/mod.rs', + 'link_nodes/pda_link_node.rs', + 'link_nodes/program_link_node.rs', + 'mod.rs', + ]); + }); + + it('per-node pages resolve their crate imports to a grouped `use crate::{…}` block plus the macro line', () => { + const entry = getFromRenderMap(map, 'link_nodes/account_link_node.rs'); + expect(entry.content).toContain('use crate::{CamelCaseString, HasName, ProgramLinkNode};'); + expect(entry.content).toContain('use codama_nodes_derive::node;'); + }); + + it('per-node pages declare the struct, the From impl, and the HasName impl', () => { + const entry = getFromRenderMap(map, 'link_nodes/account_link_node.rs'); + expect(entry.content).toContain('#[node]'); + expect(entry.content).toContain('pub struct AccountLinkNode {'); + expect(entry.content).toContain('impl From for crate::Node {'); + expect(entry.content).toContain('impl HasName for AccountLinkNode {'); + }); + + it('emits the LinkNode union with the node_union macro, every variant, and a HasName impl', () => { + const entry = getFromRenderMap(map, 'link_nodes/link_node.rs'); + expect(entry.content).toContain('use codama_nodes_derive::node_union;'); + expect(entry.content).toContain('#[node_union]'); + expect(entry.content).toContain('pub enum LinkNode {'); + expect(entry.content).toContain('Account(AccountLinkNode),'); + expect(entry.content).toContain('Program(ProgramLinkNode),'); + expect(entry.content).toContain('impl HasName for LinkNode {'); + expect(entry.content).toContain('LinkNode::Account(node) => node.name(),'); + }); + + it('emits link_nodes/mod.rs listing every per-file module alphabetically with mod + pub use lines', () => { + const entry = getFromRenderMap(map, 'link_nodes/mod.rs'); + expect(entry.content).toContain('mod account_link_node;'); + expect(entry.content).toContain('mod link_node;'); + expect(entry.content).toContain('mod program_link_node;'); + expect(entry.content).toContain('pub use account_link_node::*;'); + expect(entry.content).toContain('pub use link_node::*;'); + }); + + it('emits a root mod.rs that re-exports the link_nodes subdirectory', () => { + const entry = getFromRenderMap(map, 'mod.rs'); + expect(entry.content).toContain('mod link_nodes;'); + expect(entry.content).toContain('pub use link_nodes::*;'); + }); +}); diff --git a/spec-generators/test/scope.test.ts b/spec-generators/test/scope.test.ts new file mode 100644 index 00000000..3eff1c32 --- /dev/null +++ b/spec-generators/test/scope.test.ts @@ -0,0 +1,29 @@ +import { describe, expect, it } from 'vitest'; + +import { CATEGORY_DIRECTORIES } from '../src/defaults'; +import { buildRenderScope, type RenderOptions } from '../src/options'; + +const options: RenderOptions = { targetSpecMajor: 1 }; + +describe('buildRenderScope', () => { + it('defaults categoryDirectories to the v1 table', () => { + const scope = buildRenderScope(options); + expect(scope.categoryDirectories).toBe(CATEGORY_DIRECTORIES); + }); + + it('threads the caller-supplied targetSpecMajor through', () => { + const scope = buildRenderScope({ targetSpecMajor: 2 }); + expect(scope.targetSpecMajor).toBe(2); + }); + + it('returns a frozen scope', () => { + const scope = buildRenderScope(options); + expect(Object.isFrozen(scope)).toBe(true); + }); + + it('honours a caller-supplied categoryDirectories override', () => { + const override = new Map([['link', 'custom_link_dir']]); + const scope = buildRenderScope({ ...options, categoryDirectories: override }); + expect(scope.categoryDirectories).toBe(override); + }); +}); diff --git a/spec-generators/test/unions.test.ts b/spec-generators/test/unions.test.ts new file mode 100644 index 00000000..0ec2f339 --- /dev/null +++ b/spec-generators/test/unions.test.ts @@ -0,0 +1,51 @@ +import { getSpec } from '@codama/spec'; +import { describe, expect, it } from 'vitest'; + +import { flattenNodeUnion, getEmittableUnions } from '../src/unions'; + +const spec = getSpec(); +const linkCategory = spec.categories.find(c => c.name === 'link')!; + +describe('getEmittableUnions', () => { + it('returns spec unions whose name does NOT start with `registered`, sorted alphabetically', () => { + const names = getEmittableUnions(linkCategory).map(u => u.name); + expect(names).toEqual(['linkNode']); + }); + + it('skips category-registry unions (`registered*`)', () => { + const names = getEmittableUnions(linkCategory).map(u => u.name); + expect(names).not.toContain('registeredLinkNode'); + }); +}); + +describe('flattenNodeUnion', () => { + it('walks nested union members to leaf nodes', () => { + // `linkNode` references `union(\'registeredLinkNode\')` which + // lists 7 concrete link nodes. + const linkNode = linkCategory.unions.find(u => u.name === 'linkNode')!; + const kinds = flattenNodeUnion(linkNode, spec).map(n => n.kind); + expect(kinds.toSorted()).toEqual([ + 'accountLinkNode', + 'definedTypeLinkNode', + 'instructionAccountLinkNode', + 'instructionArgumentLinkNode', + 'instructionLinkNode', + 'pdaLinkNode', + 'programLinkNode', + ]); + }); + + it('returns direct node members unchanged when the union has no nested unions', () => { + const registered = linkCategory.unions.find(u => u.name === 'registeredLinkNode')!; + const kinds = flattenNodeUnion(registered, spec).map(n => n.kind); + expect(kinds.toSorted()).toEqual([ + 'accountLinkNode', + 'definedTypeLinkNode', + 'instructionAccountLinkNode', + 'instructionArgumentLinkNode', + 'instructionLinkNode', + 'pdaLinkNode', + 'programLinkNode', + ]); + }); +}); diff --git a/spec-generators/tsconfig.json b/spec-generators/tsconfig.json new file mode 100644 index 00000000..b352fa96 --- /dev/null +++ b/spec-generators/tsconfig.json @@ -0,0 +1,24 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "compilerOptions": { + "composite": false, + "declaration": false, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "inlineSources": false, + "isolatedModules": true, + "module": "esnext", + "moduleResolution": "bundler", + "noFallthroughCasesInSwitch": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "preserveWatchOutput": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "strict": true, + "target": "ESNext", + "types": ["node"] + }, + "include": ["bin", "src", "test"], + "exclude": ["node_modules", "dist"] +} diff --git a/spec-generators/tsup.config.ts b/spec-generators/tsup.config.ts new file mode 100644 index 00000000..2b5e1c3c --- /dev/null +++ b/spec-generators/tsup.config.ts @@ -0,0 +1,30 @@ +import { defineConfig } from 'tsup'; + +/** + * `@codama-internal/rust-spec-generators` is a private build-time tool, + * never published to npm and never imported by other workspace crates + * at runtime. We only need a single Node ESM build that the `generate` + * script can invoke directly. + * + * Two entries are emitted: the orchestrator surface (`src/index.ts`) and + * the bin script (`bin/generate.ts`). Both inline their dependencies + * (`splitting: false`) so each entry stands on its own and the dist + * layout remains predictable from the script that runs it. + */ +export default defineConfig({ + clean: false, + dts: false, + entry: { + generate: './bin/generate.ts', + index: './src/index.ts', + }, + format: 'esm', + outExtension() { + return { js: '.mjs' }; + }, + platform: 'node', + sourcemap: true, + splitting: false, + target: 'node22', + treeshake: true, +}); diff --git a/spec-generators/vitest.config.ts b/spec-generators/vitest.config.ts new file mode 100644 index 00000000..5ab9cb52 --- /dev/null +++ b/spec-generators/vitest.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + include: ['test/**/*.test.ts'], + }, +});