prototypey.org - atproto lexicon typescript toolkit - mirror https://github.com/tylersayshi/prototypey

Compare changes

Choose any two refs to compare.

+10788 -7352
+8
.changeset/README.md
··· 1 + # Changesets 2 + 3 + Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 + with multi-package repos, or single-package repos to help you version and publish your code. You can 5 + find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 + 7 + We have a quick list of common questions to get you started engaging with this project in 8 + [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)
+11
.changeset/config.json
··· 1 + { 2 + "$schema": "https://unpkg.com/@changesets/config@3.1.1/schema.json", 3 + "changelog": "@changesets/cli/changelog", 4 + "commit": false, 5 + "fixed": [], 6 + "linked": [], 7 + "access": "public", 8 + "baseBranch": "main", 9 + "updateInternalDependencies": "patch", 10 + "ignore": ["@prototypey/site"] 11 + }
+7 -3
.github/workflows/ci.yml
··· 6 6 - uses: actions/checkout@v4 7 7 - uses: ./.github/actions/prepare 8 8 - run: pnpm build 9 - - run: node packages/prototypey/lib/index.js 10 9 lint: 11 10 name: Lint 12 11 runs-on: ubuntu-latest ··· 21 20 - uses: actions/checkout@v4 22 21 - uses: ./.github/actions/prepare 23 22 - run: pnpm format 23 + knip: 24 + name: Knip 25 + runs-on: ubuntu-latest 26 + steps: 27 + - uses: actions/checkout@v4 28 + - uses: ./.github/actions/prepare 29 + - run: pnpm knip 24 30 type_check: 25 31 name: Type Check 26 32 runs-on: ubuntu-latest 27 33 steps: 28 34 - uses: actions/checkout@v4 29 35 - uses: ./.github/actions/prepare 30 - - run: pnpm build 31 36 - run: pnpm tsc 32 37 test: 33 38 name: Test ··· 36 41 - uses: actions/checkout@v4 37 42 - uses: ./.github/actions/prepare 38 43 - run: pnpm build 39 - - run: pnpm codegen:samples 40 44 - run: pnpm test 41 45 benchmark_types: 42 46 name: Benchmark Types
+45
.github/workflows/release.yml
··· 1 + name: Release 2 + 3 + on: 4 + push: 5 + branches: 6 + - main 7 + 8 + concurrency: ${{ github.workflow }}-${{ github.ref }} 9 + 10 + jobs: 11 + release: 12 + name: Release 13 + runs-on: ubuntu-latest 14 + permissions: 15 + contents: write 16 + pull-requests: write 17 + id-token: write 18 + steps: 19 + - name: Checkout Repo 20 + uses: actions/checkout@v4 21 + 22 + - name: Setup pnpm 23 + uses: pnpm/action-setup@v4 24 + 25 + - name: Setup Node.js 26 + uses: actions/setup-node@v4 27 + with: 28 + node-version: 24 29 + cache: "pnpm" 30 + registry-url: "https://registry.npmjs.org" 31 + 32 + - name: Install Dependencies 33 + run: pnpm install --frozen-lockfile 34 + 35 + - name: Build packages 36 + run: pnpm build 37 + 38 + - name: Create Release Pull Request or Publish to npm 39 + id: changesets 40 + uses: changesets/action@v1 41 + with: 42 + publish: pnpm exec changeset publish --provenance 43 + version: pnpm exec changeset version 44 + env: 45 + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+44
.github/workflows/sync-tangled.yml
··· 1 + name: sync-tangled 2 + 3 + on: 4 + push: 5 + branches: 6 + - main 7 + 8 + jobs: 9 + sync: 10 + runs-on: ubuntu-latest 11 + environment: tangled-sync 12 + steps: 13 + - uses: actions/checkout@v4.1.7 14 + with: 15 + fetch-depth: 0 16 + ref: ${{ github.event.pull_request.head.sha }} 17 + - name: sync tangled 18 + env: 19 + TANGLED_SSH_PRIVATE_KEY: ${{ secrets.TANGLED_SSH_PRIVATE_KEY }} 20 + run: | 21 + set -euo pipefail 22 + # Turn off strict SSH key checking 23 + mkdir -p ~/.ssh 24 + echo "Host * 25 + StrictHostKeyChecking no 26 + UserKnownHostsFile=/dev/null" > ~/.ssh/config 27 + 28 + # Write SSH key to disk 29 + echo "$TANGLED_SSH_PRIVATE_KEY" > ~/.ssh/tangled_key 30 + chmod 600 ~/.ssh/tangled_key 31 + 32 + # Configure SSH to use the key for tangled.sh 33 + echo "Host tangled.sh 34 + IdentityFile ~/.ssh/tangled_key" >> ~/.ssh/config 35 + 36 + chmod 600 ~/.ssh/config 37 + 38 + # Set git user 39 + git config --global user.name "Prototypey Bot" 40 + git config --global user.email "bot@prototypey.org" 41 + 42 + git remote add tangled git@tangled.sh:tylur.dev/prototypey 43 + git push -f --all tangled 44 + git push -f --tags tangled
+28
.github/workflows/update-dependencies.yml
··· 1 + name: Update Dependencies 2 + 3 + on: 4 + schedule: 5 + # Run every Monday at 9:00 AM UTC 6 + - cron: "0 9 * * 1" 7 + workflow_dispatch: # Allow manual trigger 8 + 9 + jobs: 10 + update_dependencies: 11 + name: Update Dependencies 12 + runs-on: ubuntu-latest 13 + environment: update 14 + permissions: 15 + contents: write 16 + pull-requests: write 17 + steps: 18 + - uses: actions/checkout@v4 19 + 20 + - uses: ./.github/actions/prepare 21 + 22 + - uses: tylersayshi/taze-update-action@v1 23 + with: 24 + taze-input: -rw --concurrency 1 25 + branch-prefix: update-deps 26 + pr-title: "update dependencies" 27 + pr-labels: dependencies 28 + github-token: ${{ secrets.PAT_TOKEN }}
+1 -1
.node-version
··· 1 - 24.10.0 1 + 25
+5 -1
.prettierrc.json
··· 1 - { "$schema": "http://json.schemastore.org/prettierrc", "useTabs": true } 1 + { 2 + "$schema": "http://json.schemastore.org/prettierrc", 3 + "useTabs": true, 4 + "overrides": [{ "files": ["README.md"], "options": { "useTabs": false } }] 5 + }
+1 -94
README.md
··· 1 - # typed-lexicon 2 - 3 - > [!WARNING] 4 - > this project is in the middle of active initial development and not ready for 5 - > use. there will be updates posted [here](https://bsky.app/profile/tylur.dev) 6 - > if you'd like to follow along! 7 - 8 - ![demo of jsdoc with typed-lexicon](https://github.com/user-attachments/assets/1dbc0901-a950-4779-bf20-2e818456fd3c) 9 - 10 - this will be a toolkit for writing lexicon json schema's in typescript and 11 - providing types for lexicon data shape. it will: 12 - 13 - - remove boilerplate and improve ergonomics 14 - - type hint for 15 - [atproto type parameters](https://atproto.com/specs/lexicon#overview-of-types) 16 - - infer the typescript type definitions for the data shape to avoid duplication 17 - and skew 18 - - methods and a cli for generating json 19 - 20 - With each of the above finished, i'll plan to write a `validate` method that 21 - will be published alongside this that takes any lexicon json definition and 22 - validates payloads off that. 23 - 24 - My working hypothesis: it will be easier to write lexicons in typescript with a 25 - single api, then validate based off the json definition, than it would be to 26 - start with validation library types (standard-schema style) and attempt to use 27 - those as the authoring and validation tools. 28 - 29 - **what you'd write:** 30 - 31 - ```typescript 32 - const profileNamespace = lx.lexicon("app.bsky.actor.profile", { 33 - main: lx.record({ 34 - key: "self", 35 - record: lx.object({ 36 - displayName: lx.string({ maxLength: 64, maxGraphemes: 64 }), 37 - description: lx.string({ maxLength: 256, maxGraphemes: 256 }), 38 - }), 39 - }), 40 - }); 41 - ``` 42 - 43 - **generates to:** 44 - 45 - ```json 46 - { 47 - "lexicon": 1, 48 - "id": "app.bsky.actor.profile", 49 - "defs": { 50 - "main": { 51 - "type": "record", 52 - "key": "self", 53 - "record": { 54 - "type": "object", 55 - "properties": { 56 - "displayName": { 57 - "type": "string", 58 - "maxLength": 64, 59 - "maxGraphemes": 64 60 - }, 61 - "description": { 62 - "type": "string", 63 - "maxLength": 256, 64 - "maxGraphemes": 256 65 - } 66 - } 67 - } 68 - } 69 - } 70 - } 71 - ``` 72 - 73 - --- 74 - 75 - <p align="center"> 76 - <a href="https://github.com/tylersayshi/prototypey/blob/main/.github/CODE_OF_CONDUCT.md" target="_blank"><img alt="๐Ÿค Code of Conduct: Kept" src="https://img.shields.io/badge/%F0%9F%A4%9D_code_of_conduct-kept-21bb42" /></a> 77 - <a href="https://github.com/tylersayshi/prototypey/blob/main/LICENSE.md" target="_blank"><img alt="๐Ÿ“ License: MIT" src="https://img.shields.io/badge/%F0%9F%93%9D_license-MIT-21bb42.svg" /></a> 78 - <img alt="๐Ÿ’ช TypeScript: Strict" src="https://img.shields.io/badge/%F0%9F%92%AA_typescript-strict-21bb42.svg" /> 79 - </p> 80 - 81 - ## Usage 82 - 83 - tbd 84 - 85 - ## Development 86 - 87 - See [`.github/CONTRIBUTING.md`](./.github/CONTRIBUTING.md), then 88 - [`.github/DEVELOPMENT.md`](./.github/DEVELOPMENT.md). Thanks! ๐Ÿ’– 89 - 90 - <!-- You can remove this notice if you don't want it ๐Ÿ™‚ no worries! --> 91 - 92 - > ๐Ÿ’ This package was templated with 93 - > [`create-typescript-app`](https://github.com/JoshuaKGoldberg/create-typescript-app) 94 - > using the [Bingo framework](https://create.bingo). 1 + ./packages/prototypey/README.md
+15 -11
eslint.config.js
··· 1 1 import eslint from "@eslint/js"; 2 2 import tseslint from "typescript-eslint"; 3 - import reactCompiler from "eslint-plugin-react-compiler"; 4 3 5 4 export default tseslint.config( 6 - { ignores: ["lib", "node_modules", "pnpm-lock.yaml", "setup-vitest.ts"] }, 5 + { 6 + ignores: [ 7 + "**/lib/**", 8 + "**/dist/**", 9 + "node_modules", 10 + "pnpm-lock.yaml", 11 + "**/setup-vitest.ts", 12 + "**/tests/**", 13 + ], 14 + }, 7 15 { linterOptions: { reportUnusedDisableDirectives: "error" } }, 8 16 eslint.configs.recommended, 9 17 { ··· 19 27 }, 20 28 rules: { 21 29 "@typescript-eslint/consistent-type-definitions": "off", 22 - }, 23 - }, 24 - { 25 - files: ["**/*.{jsx,tsx}"], 26 - plugins: { 27 - "react-compiler": reactCompiler, 28 - }, 29 - rules: { 30 - "react-compiler/react-compiler": "error", 30 + "@typescript-eslint/no-unsafe-assignment": "off", 31 + "@typescript-eslint/no-unsafe-argument": "off", 32 + "@typescript-eslint/no-unsafe-member-access": "off", 33 + "@typescript-eslint/no-unsafe-call": "off", 34 + "@typescript-eslint/restrict-plus-operands": "off", 31 35 }, 32 36 }, 33 37 {
+20
knip.json
··· 1 + { 2 + "$schema": "https://unpkg.com/knip@5/schema.json", 3 + "workspaces": { 4 + ".": { 5 + "entry": [], 6 + "project": [] 7 + }, 8 + "packages/prototypey": { 9 + "entry": ["**/*.test.ts", "**/*.bench.ts"], 10 + "project": ["**/*.ts"] 11 + }, 12 + "packages/site": { 13 + "entry": ["src/main.tsx!", "tests/**/*.test.{ts,tsx}"], 14 + "project": ["src/**/*.{ts,tsx}"] 15 + } 16 + }, 17 + "ignore": ["**/*.d.ts", "**/dist/**", "**/lib/**", "**/node_modules/**"], 18 + "ignoreDependencies": ["tailwindcss"], 19 + "ignoreExportsUsedInFile": true 20 + }
+6 -7
package.json
··· 2 2 "name": "prototypey-monorepo", 3 3 "version": "0.0.0", 4 4 "private": true, 5 - "description": "Type-safe lexicon inference for ATProto schemas", 5 + "type": "module", 6 + "description": "atproto lexicon typescript toolkit", 6 7 "repository": { 7 8 "type": "git", 8 9 "url": "git+https://github.com/tylersayshi/prototypey.git" 9 10 }, 10 11 "license": "MIT", 11 - "author": { 12 - "name": "tylersayshi", 13 - "email": "hi@tylur.dev" 14 - }, 15 12 "scripts": { 16 13 "build": "pnpm -r build", 17 - "codegen:samples": "node packages/cli/src/index.ts gen-inferred ./generated/inferred './samples/*.json'", 18 14 "format": "prettier . --list-different", 19 15 "format:fix": "prettier . --write", 16 + "knip": "knip", 20 17 "lint": "pnpm -r lint", 21 18 "test": "pnpm -r test", 22 19 "tsc": "pnpm -r tsc" 23 20 }, 24 21 "devDependencies": { 22 + "@changesets/cli": "^2.29.8", 25 23 "@eslint/js": "9.29.0", 26 24 "eslint": "9.29.0", 25 + "knip": "^5.83.1", 27 26 "prettier": "3.6.1", 28 27 "typescript-eslint": "8.35.0" 29 28 }, 30 - "packageManager": "pnpm@10.4.0", 29 + "packageManager": "pnpm@10.29.2", 31 30 "engines": { 32 31 "node": ">=20.19.0" 33 32 },
-100
packages/cli/README.md
··· 1 - # @prototypey/cli 2 - 3 - CLI tool for generating types from ATProto lexicon schemas. 4 - 5 - ## Installation 6 - 7 - ```bash 8 - npm install -g @prototypey/cli 9 - ``` 10 - 11 - Or use directly with npx: 12 - 13 - ```bash 14 - npx @prototypey/cli 15 - ``` 16 - 17 - ## Commands 18 - 19 - ### `gen-inferred` 20 - 21 - Generate type-inferred TypeScript code from JSON lexicon schemas. 22 - 23 - **Usage:** 24 - 25 - ```bash 26 - prototypey gen-inferred <outdir> <schemas...> 27 - ``` 28 - 29 - **Arguments:** 30 - 31 - - `outdir` - Output directory for generated TypeScript files 32 - - `schemas...` - One or more glob patterns matching lexicon JSON schema files 33 - 34 - **Example:** 35 - 36 - ```bash 37 - prototypey gen-inferred ./generated/inferred ./lexicons/**/*.json 38 - ``` 39 - 40 - **What it does:** 41 - 42 - - Reads ATProto lexicon JSON schemas 43 - - Generates TypeScript types that match the schema structure 44 - - Organizes output files by namespace (e.g., `app.bsky.feed.post` โ†’ `app/bsky/feed/post.ts`) 45 - - Provides type-safe interfaces for working with lexicon data 46 - 47 - ### `gen-emit` 48 - 49 - Emit JSON lexicon schemas from authored TypeScript files. 50 - 51 - **Usage:** 52 - 53 - ```bash 54 - prototypey gen-emit <outdir> <sources...> 55 - ``` 56 - 57 - **Arguments:** 58 - 59 - - `outdir` - Output directory for emitted JSON schema files 60 - - `sources...` - One or more glob patterns matching TypeScript source files 61 - 62 - **Example:** 63 - 64 - ```bash 65 - prototypey gen-emit ./lexicons ./src/lexicons/**/*.ts 66 - ``` 67 - 68 - **What it does:** 69 - 70 - - Scans TypeScript files for exported lexicon definitions 71 - - Extracts the `.json` property from each lexicon 72 - - Emits properly formatted JSON lexicon schema files 73 - - Names output files by lexicon ID (e.g., `app.bsky.feed.post.json`) 74 - 75 - ## Workflow 76 - 77 - The typical workflow combines both commands for bidirectional type safety: 78 - 79 - 1. **Author lexicons in TypeScript** using the `prototypey` library 80 - 2. **Emit to JSON** with `gen-emit` for runtime validation and API contracts 81 - 3. **Generate inferred types** with `gen-inferred` for consuming code 82 - 83 - ```bash 84 - # Write your lexicons in TypeScript 85 - # src/lexicons/app.bsky.actor.profile.ts 86 - 87 - # Emit JSON schemas 88 - prototypey gen-emit ./schemas ./src/lexicons/**/*.ts 89 - 90 - # Generate TypeScript types from schemas 91 - prototypey gen-inferred ./generated ./schemas/**/*.json 92 - ``` 93 - 94 - ## Requirements 95 - 96 - - Node.js >= 20.19.0 97 - 98 - ## License 99 - 100 - MIT
-42
packages/cli/package.json
··· 1 - { 2 - "name": "@prototypey/cli", 3 - "version": "0.0.0", 4 - "description": "CLI tool for generating types from ATProto lexicon schemas", 5 - "repository": { 6 - "type": "git", 7 - "url": "git+https://github.com/tylersayshi/prototypey.git", 8 - "directory": "packages/cli" 9 - }, 10 - "license": "MIT", 11 - "author": { 12 - "name": "tylersayshi", 13 - "email": "hi@tylur.dev" 14 - }, 15 - "type": "module", 16 - "bin": { 17 - "prototypey": "./lib/index.js" 18 - }, 19 - "files": [ 20 - "lib/", 21 - "README.md" 22 - ], 23 - "scripts": { 24 - "build": "tsdown --entry src/index.ts --format esm --dts false", 25 - "test": "vitest run", 26 - "tsc": "tsc" 27 - }, 28 - "dependencies": { 29 - "prototypey": "workspace:*", 30 - "sade": "^1.8.1", 31 - "tinyglobby": "^0.2.15" 32 - }, 33 - "devDependencies": { 34 - "@types/node": "24.0.4", 35 - "tsdown": "0.12.7", 36 - "typescript": "5.8.3", 37 - "vitest": "^3.2.4" 38 - }, 39 - "engines": { 40 - "node": ">=20.19.0" 41 - } 42 - }
-102
packages/cli/src/commands/gen-emit.ts
··· 1 - import { glob } from "tinyglobby"; 2 - import { mkdir, writeFile } from "node:fs/promises"; 3 - import { join } from "node:path"; 4 - import { pathToFileURL } from "node:url"; 5 - 6 - interface LexiconNamespace { 7 - json: { 8 - lexicon: number; 9 - id: string; 10 - defs: Record<string, unknown>; 11 - }; 12 - } 13 - 14 - export async function genEmit( 15 - outdir: string, 16 - sources: string | string[], 17 - ): Promise<void> { 18 - try { 19 - const sourcePatterns = Array.isArray(sources) ? sources : [sources]; 20 - 21 - // Find all source files matching the patterns 22 - const sourceFiles = await glob(sourcePatterns, { 23 - absolute: true, 24 - onlyFiles: true, 25 - }); 26 - 27 - if (sourceFiles.length === 0) { 28 - console.log("No source files found matching patterns:", sourcePatterns); 29 - return; 30 - } 31 - 32 - console.log(`Found ${sourceFiles.length} source file(s)`); 33 - 34 - // Ensure output directory exists 35 - await mkdir(outdir, { recursive: true }); 36 - 37 - // Process each source file 38 - for (const sourcePath of sourceFiles) { 39 - await processSourceFile(sourcePath, outdir); 40 - } 41 - 42 - console.log(`\nEmitted lexicon schemas to ${outdir}`); 43 - } catch (error) { 44 - console.error("Error emitting lexicon schemas:", error); 45 - process.exit(1); 46 - } 47 - } 48 - 49 - async function processSourceFile( 50 - sourcePath: string, 51 - outdir: string, 52 - ): Promise<void> { 53 - try { 54 - // Convert file path to file URL for dynamic import 55 - const fileUrl = pathToFileURL(sourcePath).href; 56 - 57 - // Dynamically import the module 58 - const module = await import(fileUrl); 59 - 60 - // Find all exported lexicons 61 - const lexicons: LexiconNamespace[] = []; 62 - for (const key of Object.keys(module)) { 63 - const exported = module[key]; 64 - // Check if it's a lexicon with a json property 65 - if ( 66 - exported && 67 - typeof exported === "object" && 68 - "json" in exported && 69 - exported.json && 70 - typeof exported.json === "object" && 71 - "lexicon" in exported.json && 72 - "id" in exported.json && 73 - "defs" in exported.json 74 - ) { 75 - lexicons.push(exported as LexiconNamespace); 76 - } 77 - } 78 - 79 - if (lexicons.length === 0) { 80 - console.warn(` โš  ${sourcePath}: No lexicons found`); 81 - return; 82 - } 83 - 84 - // Emit JSON for each lexicon 85 - for (const lexicon of lexicons) { 86 - const { id } = lexicon.json; 87 - const outputPath = join(outdir, `${id}.json`); 88 - 89 - // Write the JSON file 90 - await writeFile( 91 - outputPath, 92 - JSON.stringify(lexicon.json, null, "\t"), 93 - "utf-8", 94 - ); 95 - 96 - console.log(` โœ“ ${id} -> ${id}.json`); 97 - } 98 - } catch (error) { 99 - console.error(` โœ— Error processing ${sourcePath}:`, error); 100 - throw error; 101 - } 102 - }
-71
packages/cli/src/commands/gen-inferred.ts
··· 1 - import { glob } from "tinyglobby"; 2 - import { readFile, mkdir, writeFile } from "node:fs/promises"; 3 - import { join, dirname, relative, parse } from "node:path"; 4 - import { generateInferredCode } from "../templates/inferred.ts"; 5 - 6 - interface LexiconSchema { 7 - lexicon: number; 8 - id: string; 9 - defs: Record<string, unknown>; 10 - } 11 - 12 - export async function genInferred( 13 - outdir: string, 14 - schemas: string | string[], 15 - ): Promise<void> { 16 - try { 17 - const schemaPatterns = Array.isArray(schemas) ? schemas : [schemas]; 18 - 19 - // Find all schema files matching the patterns 20 - const schemaFiles = await glob(schemaPatterns, { 21 - absolute: true, 22 - onlyFiles: true, 23 - }); 24 - 25 - if (schemaFiles.length === 0) { 26 - console.log("No schema files found matching patterns:", schemaPatterns); 27 - return; 28 - } 29 - 30 - console.log(`Found ${schemaFiles.length} schema file(s)`); 31 - 32 - // Process each schema file 33 - for (const schemaPath of schemaFiles) { 34 - await processSchema(schemaPath, outdir); 35 - } 36 - 37 - console.log(`\nGenerated inferred types in ${outdir}`); 38 - } catch (error) { 39 - console.error("Error generating inferred types:", error); 40 - process.exit(1); 41 - } 42 - } 43 - 44 - async function processSchema( 45 - schemaPath: string, 46 - outdir: string, 47 - ): Promise<void> { 48 - const content = await readFile(schemaPath, "utf-8"); 49 - const schema: LexiconSchema = JSON.parse(content); 50 - 51 - if (!schema.id || !schema.defs) { 52 - console.warn(`Skipping ${schemaPath}: Missing id or defs`); 53 - return; 54 - } 55 - 56 - // Convert NSID to file path: app.bsky.feed.post -> app/bsky/feed/post.ts 57 - const nsidParts = schema.id.split("."); 58 - const relativePath = join(...nsidParts) + ".ts"; 59 - const outputPath = join(outdir, relativePath); 60 - 61 - // Create directory structure 62 - await mkdir(dirname(outputPath), { recursive: true }); 63 - 64 - // Generate the TypeScript code 65 - const code = generateInferredCode(schema, schemaPath, outdir); 66 - 67 - // Write the file 68 - await writeFile(outputPath, code, "utf-8"); 69 - 70 - console.log(` โœ“ ${schema.id} -> ${relativePath}`); 71 - }
-28
packages/cli/src/index.ts
··· 1 - import { readFile } from "node:fs/promises"; 2 - import sade from "sade"; 3 - import { genInferred } from "./commands/gen-inferred.ts"; 4 - import { genEmit } from "./commands/gen-emit.ts"; 5 - 6 - const pkg = JSON.parse( 7 - await readFile(new URL("../package.json", import.meta.url), "utf-8"), 8 - ) as { version: string }; 9 - 10 - const prog = sade("prototypey"); 11 - 12 - prog 13 - .version(pkg.version) 14 - .describe("Type-safe lexicon inference and code generation"); 15 - 16 - prog 17 - .command("gen-inferred <outdir> <schemas...>") 18 - .describe("Generate type-inferred code from lexicon schemas") 19 - .example("gen-inferred ./generated/inferred ./lexicons/**/*.json") 20 - .action(genInferred); 21 - 22 - prog 23 - .command("gen-emit <outdir> <sources...>") 24 - .describe("Emit JSON lexicon schemas from authored TypeScript") 25 - .example("gen-emit ./lexicons ./src/lexicons/**/*.ts") 26 - .action(genEmit); 27 - 28 - prog.parse(process.argv);
-65
packages/cli/src/templates/inferred.ts
··· 1 - import { relative, dirname } from "node:path"; 2 - 3 - interface LexiconSchema { 4 - lexicon: number; 5 - id: string; 6 - defs: Record<string, unknown>; 7 - } 8 - 9 - export function generateInferredCode( 10 - schema: LexiconSchema, 11 - schemaPath: string, 12 - outdir: string, 13 - ): string { 14 - const { id } = schema; 15 - 16 - // Calculate relative import path from output file to schema file 17 - // We need to go from generated/{nsid}.ts to the original schema 18 - const nsidParts = id.split("."); 19 - const outputDir = dirname([outdir, ...nsidParts].join("/")); 20 - const relativeSchemaPath = relative(outputDir, schemaPath); 21 - 22 - // Generate a clean type name from the NSID 23 - const typeName = generateTypeName(id); 24 - 25 - return `// Generated by prototypey - DO NOT EDIT 26 - // Source: ${id} 27 - import type { Infer } from "prototypey"; 28 - import schema from "${relativeSchemaPath}" with { type: "json" }; 29 - 30 - /** 31 - * Type-inferred from lexicon schema: ${id} 32 - */ 33 - export type ${typeName} = Infer<typeof schema>; 34 - 35 - /** 36 - * The lexicon schema object 37 - */ 38 - export const ${typeName}Schema = schema; 39 - 40 - /** 41 - * Type guard to check if a value is a ${typeName} 42 - */ 43 - export function is${typeName}(v: unknown): v is ${typeName} { 44 - return ( 45 - typeof v === "object" && 46 - v !== null && 47 - "$type" in v && 48 - v.$type === "${id}" 49 - ); 50 - } 51 - `; 52 - } 53 - 54 - function generateTypeName(nsid: string): string { 55 - // Convert app.bsky.feed.post -> Post 56 - // Convert com.atproto.repo.createRecord -> CreateRecord 57 - const parts = nsid.split("."); 58 - const lastPart = parts[parts.length - 1]; 59 - 60 - // Convert kebab-case or camelCase to PascalCase 61 - return lastPart 62 - .split(/[-_]/) 63 - .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) 64 - .join(""); 65 - }
-525
packages/cli/tests/commands/gen-emit.test.ts
··· 1 - import { expect, test, describe, beforeEach, afterEach } from "vitest"; 2 - import { mkdir, writeFile, rm, readFile } from "node:fs/promises"; 3 - import { join } from "node:path"; 4 - import { genEmit } from "../../src/commands/gen-emit.ts"; 5 - import { tmpdir } from "node:os"; 6 - 7 - describe("genEmit", () => { 8 - let testDir: string; 9 - let outDir: string; 10 - 11 - beforeEach(async () => { 12 - // Create a temporary directory for test files 13 - testDir = join(tmpdir(), `prototypey-test-${Date.now()}`); 14 - outDir = join(testDir, "output"); 15 - await mkdir(testDir, { recursive: true }); 16 - await mkdir(outDir, { recursive: true }); 17 - }); 18 - 19 - afterEach(async () => { 20 - // Clean up test directory 21 - await rm(testDir, { recursive: true, force: true }); 22 - }); 23 - 24 - test("emits JSON from a simple lexicon file", async () => { 25 - // Create a test lexicon file 26 - const lexiconFile = join(testDir, "profile.ts"); 27 - await writeFile( 28 - lexiconFile, 29 - ` 30 - import { lx } from "prototypey"; 31 - 32 - export const profileNamespace = lx.lexicon("app.bsky.actor.profile", { 33 - main: lx.record({ 34 - key: "self", 35 - record: lx.object({ 36 - displayName: lx.string({ maxLength: 64, maxGraphemes: 64 }), 37 - description: lx.string({ maxLength: 256, maxGraphemes: 256 }), 38 - }), 39 - }), 40 - }); 41 - `, 42 - ); 43 - 44 - // Run the emit command 45 - await genEmit(outDir, lexiconFile); 46 - 47 - // Read the emitted JSON file 48 - const outputFile = join(outDir, "app.bsky.actor.profile.json"); 49 - const content = await readFile(outputFile, "utf-8"); 50 - const json = JSON.parse(content); 51 - 52 - // Verify the structure 53 - expect(json).toEqual({ 54 - lexicon: 1, 55 - id: "app.bsky.actor.profile", 56 - defs: { 57 - main: { 58 - type: "record", 59 - key: "self", 60 - record: { 61 - type: "object", 62 - properties: { 63 - displayName: { 64 - type: "string", 65 - maxLength: 64, 66 - maxGraphemes: 64, 67 - }, 68 - description: { 69 - type: "string", 70 - maxLength: 256, 71 - maxGraphemes: 256, 72 - }, 73 - }, 74 - }, 75 - }, 76 - }, 77 - }); 78 - }); 79 - 80 - test("emits JSON from multiple lexicon exports in one file", async () => { 81 - // Create a test file with multiple exports 82 - const lexiconFile = join(testDir, "multiple.ts"); 83 - await writeFile( 84 - lexiconFile, 85 - ` 86 - import { lx } from "prototypey"; 87 - 88 - export const profile = lx.lexicon("app.bsky.actor.profile", { 89 - main: lx.record({ 90 - key: "self", 91 - record: lx.object({ 92 - displayName: lx.string({ maxLength: 64 }), 93 - }), 94 - }), 95 - }); 96 - 97 - export const post = lx.lexicon("app.bsky.feed.post", { 98 - main: lx.record({ 99 - key: "tid", 100 - record: lx.object({ 101 - text: lx.string({ maxLength: 300 }), 102 - }), 103 - }), 104 - }); 105 - `, 106 - ); 107 - 108 - // Run the emit command 109 - await genEmit(outDir, lexiconFile); 110 - 111 - // Verify both files were created 112 - const profileJson = JSON.parse( 113 - await readFile(join(outDir, "app.bsky.actor.profile.json"), "utf-8"), 114 - ); 115 - const postJson = JSON.parse( 116 - await readFile(join(outDir, "app.bsky.feed.post.json"), "utf-8"), 117 - ); 118 - 119 - expect(profileJson.id).toBe("app.bsky.actor.profile"); 120 - expect(postJson.id).toBe("app.bsky.feed.post"); 121 - }); 122 - 123 - test("handles glob patterns for multiple files", async () => { 124 - // Create multiple test files 125 - const lexicons = join(testDir, "lexicons"); 126 - await mkdir(lexicons, { recursive: true }); 127 - 128 - await writeFile( 129 - join(lexicons, "profile.ts"), 130 - ` 131 - import { lx } from "prototypey"; 132 - export const schema = lx.lexicon("app.bsky.actor.profile", { 133 - main: lx.record({ key: "self", record: lx.object({}) }), 134 - }); 135 - `, 136 - ); 137 - 138 - await writeFile( 139 - join(lexicons, "post.ts"), 140 - ` 141 - import { lx } from "prototypey"; 142 - export const schema = lx.lexicon("app.bsky.feed.post", { 143 - main: lx.record({ key: "tid", record: lx.object({}) }), 144 - }); 145 - `, 146 - ); 147 - 148 - // Run with glob pattern 149 - await genEmit(outDir, `${lexicons}/*.ts`); 150 - 151 - // Verify both files were created 152 - const profileExists = await readFile( 153 - join(outDir, "app.bsky.actor.profile.json"), 154 - "utf-8", 155 - ); 156 - const postExists = await readFile( 157 - join(outDir, "app.bsky.feed.post.json"), 158 - "utf-8", 159 - ); 160 - 161 - expect(profileExists).toBeTruthy(); 162 - expect(postExists).toBeTruthy(); 163 - }); 164 - 165 - test("emits query endpoint with parameters and output", async () => { 166 - const lexiconFile = join(testDir, "search.ts"); 167 - await writeFile( 168 - lexiconFile, 169 - ` 170 - import { lx } from "prototypey"; 171 - 172 - export const searchPosts = lx.lexicon("app.bsky.feed.searchPosts", { 173 - main: lx.query({ 174 - description: "Find posts matching search criteria", 175 - parameters: lx.params({ 176 - q: lx.string({ required: true }), 177 - limit: lx.integer({ minimum: 1, maximum: 100, default: 25 }), 178 - cursor: lx.string(), 179 - }), 180 - output: { 181 - encoding: "application/json", 182 - schema: lx.object({ 183 - cursor: lx.string(), 184 - posts: lx.array(lx.ref("app.bsky.feed.defs#postView"), { required: true }), 185 - }), 186 - }, 187 - }), 188 - }); 189 - `, 190 - ); 191 - 192 - await genEmit(outDir, lexiconFile); 193 - 194 - const outputFile = join(outDir, "app.bsky.feed.searchPosts.json"); 195 - const content = await readFile(outputFile, "utf-8"); 196 - const json = JSON.parse(content); 197 - 198 - expect(json).toEqual({ 199 - lexicon: 1, 200 - id: "app.bsky.feed.searchPosts", 201 - defs: { 202 - main: { 203 - type: "query", 204 - description: "Find posts matching search criteria", 205 - parameters: { 206 - type: "params", 207 - properties: { 208 - q: { type: "string", required: true }, 209 - limit: { type: "integer", minimum: 1, maximum: 100, default: 25 }, 210 - cursor: { type: "string" }, 211 - }, 212 - required: ["q"], 213 - }, 214 - output: { 215 - encoding: "application/json", 216 - schema: { 217 - type: "object", 218 - properties: { 219 - cursor: { type: "string" }, 220 - posts: { 221 - type: "array", 222 - items: { type: "ref", ref: "app.bsky.feed.defs#postView" }, 223 - required: true, 224 - }, 225 - }, 226 - required: ["posts"], 227 - }, 228 - }, 229 - }, 230 - }, 231 - }); 232 - }); 233 - 234 - test("emits procedure endpoint with input and output", async () => { 235 - const lexiconFile = join(testDir, "create-post.ts"); 236 - await writeFile( 237 - lexiconFile, 238 - ` 239 - import { lx } from "prototypey"; 240 - 241 - export const createPost = lx.lexicon("com.atproto.repo.createRecord", { 242 - main: lx.procedure({ 243 - description: "Create a record", 244 - input: { 245 - encoding: "application/json", 246 - schema: lx.object({ 247 - repo: lx.string({ required: true }), 248 - collection: lx.string({ required: true }), 249 - record: lx.unknown({ required: true }), 250 - }), 251 - }, 252 - output: { 253 - encoding: "application/json", 254 - schema: lx.object({ 255 - uri: lx.string({ required: true }), 256 - cid: lx.string({ required: true }), 257 - }), 258 - }, 259 - }), 260 - }); 261 - `, 262 - ); 263 - 264 - await genEmit(outDir, lexiconFile); 265 - 266 - const outputFile = join(outDir, "com.atproto.repo.createRecord.json"); 267 - const content = await readFile(outputFile, "utf-8"); 268 - const json = JSON.parse(content); 269 - 270 - expect(json).toEqual({ 271 - lexicon: 1, 272 - id: "com.atproto.repo.createRecord", 273 - defs: { 274 - main: { 275 - type: "procedure", 276 - description: "Create a record", 277 - input: { 278 - encoding: "application/json", 279 - schema: { 280 - type: "object", 281 - properties: { 282 - repo: { type: "string", required: true }, 283 - collection: { type: "string", required: true }, 284 - record: { type: "unknown", required: true }, 285 - }, 286 - required: ["repo", "collection", "record"], 287 - }, 288 - }, 289 - output: { 290 - encoding: "application/json", 291 - schema: { 292 - type: "object", 293 - properties: { 294 - uri: { type: "string", required: true }, 295 - cid: { type: "string", required: true }, 296 - }, 297 - required: ["uri", "cid"], 298 - }, 299 - }, 300 - }, 301 - }, 302 - }); 303 - }); 304 - 305 - test("emits subscription endpoint with message union", async () => { 306 - const lexiconFile = join(testDir, "subscription.ts"); 307 - await writeFile( 308 - lexiconFile, 309 - ` 310 - import { lx } from "prototypey"; 311 - 312 - export const subscribeRepos = lx.lexicon("com.atproto.sync.subscribeRepos", { 313 - main: lx.subscription({ 314 - description: "Repository event stream", 315 - parameters: lx.params({ 316 - cursor: lx.integer(), 317 - }), 318 - message: { 319 - schema: lx.union(["#commit", "#identity", "#account"]), 320 - }, 321 - }), 322 - commit: lx.object({ 323 - seq: lx.integer({ required: true }), 324 - rebase: lx.boolean({ required: true }), 325 - }), 326 - identity: lx.object({ 327 - seq: lx.integer({ required: true }), 328 - did: lx.string({ required: true, format: "did" }), 329 - }), 330 - account: lx.object({ 331 - seq: lx.integer({ required: true }), 332 - active: lx.boolean({ required: true }), 333 - }), 334 - }); 335 - `, 336 - ); 337 - 338 - await genEmit(outDir, lexiconFile); 339 - 340 - const outputFile = join(outDir, "com.atproto.sync.subscribeRepos.json"); 341 - const content = await readFile(outputFile, "utf-8"); 342 - const json = JSON.parse(content); 343 - 344 - expect(json).toEqual({ 345 - lexicon: 1, 346 - id: "com.atproto.sync.subscribeRepos", 347 - defs: { 348 - main: { 349 - type: "subscription", 350 - description: "Repository event stream", 351 - parameters: { 352 - type: "params", 353 - properties: { 354 - cursor: { type: "integer" }, 355 - }, 356 - }, 357 - message: { 358 - schema: { 359 - type: "union", 360 - refs: ["#commit", "#identity", "#account"], 361 - }, 362 - }, 363 - }, 364 - commit: { 365 - type: "object", 366 - properties: { 367 - seq: { type: "integer", required: true }, 368 - rebase: { type: "boolean", required: true }, 369 - }, 370 - required: ["seq", "rebase"], 371 - }, 372 - identity: { 373 - type: "object", 374 - properties: { 375 - seq: { type: "integer", required: true }, 376 - did: { type: "string", format: "did", required: true }, 377 - }, 378 - required: ["seq", "did"], 379 - }, 380 - account: { 381 - type: "object", 382 - properties: { 383 - seq: { type: "integer", required: true }, 384 - active: { type: "boolean", required: true }, 385 - }, 386 - required: ["seq", "active"], 387 - }, 388 - }, 389 - }); 390 - }); 391 - 392 - test("emits complex namespace with tokens, refs, and unions", async () => { 393 - const lexiconFile = join(testDir, "complex.ts"); 394 - await writeFile( 395 - lexiconFile, 396 - ` 397 - import { lx } from "prototypey"; 398 - 399 - export const feedDefs = lx.lexicon("app.bsky.feed.defs", { 400 - postView: lx.object({ 401 - uri: lx.string({ required: true, format: "at-uri" }), 402 - cid: lx.string({ required: true, format: "cid" }), 403 - author: lx.ref("app.bsky.actor.defs#profileViewBasic", { required: true }), 404 - embed: lx.union([ 405 - "app.bsky.embed.images#view", 406 - "app.bsky.embed.video#view", 407 - ]), 408 - likeCount: lx.integer({ minimum: 0 }), 409 - }), 410 - requestLess: lx.token("Request less content like this"), 411 - requestMore: lx.token("Request more content like this"), 412 - }); 413 - `, 414 - ); 415 - 416 - await genEmit(outDir, lexiconFile); 417 - 418 - const outputFile = join(outDir, "app.bsky.feed.defs.json"); 419 - const content = await readFile(outputFile, "utf-8"); 420 - const json = JSON.parse(content); 421 - 422 - expect(json).toEqual({ 423 - lexicon: 1, 424 - id: "app.bsky.feed.defs", 425 - defs: { 426 - postView: { 427 - type: "object", 428 - properties: { 429 - uri: { type: "string", format: "at-uri", required: true }, 430 - cid: { type: "string", format: "cid", required: true }, 431 - author: { 432 - type: "ref", 433 - ref: "app.bsky.actor.defs#profileViewBasic", 434 - required: true, 435 - }, 436 - embed: { 437 - type: "union", 438 - refs: ["app.bsky.embed.images#view", "app.bsky.embed.video#view"], 439 - }, 440 - likeCount: { type: "integer", minimum: 0 }, 441 - }, 442 - required: ["uri", "cid", "author"], 443 - }, 444 - requestLess: { 445 - type: "token", 446 - description: "Request less content like this", 447 - }, 448 - requestMore: { 449 - type: "token", 450 - description: "Request more content like this", 451 - }, 452 - }, 453 - }); 454 - }); 455 - 456 - test("emits lexicon with arrays, blobs, and string formats", async () => { 457 - const lexiconFile = join(testDir, "primitives.ts"); 458 - await writeFile( 459 - lexiconFile, 460 - ` 461 - import { lx } from "prototypey"; 462 - 463 - export const imagePost = lx.lexicon("app.example.imagePost", { 464 - main: lx.record({ 465 - key: "tid", 466 - record: lx.object({ 467 - text: lx.string({ maxLength: 300, maxGraphemes: 300, required: true }), 468 - createdAt: lx.string({ format: "datetime", required: true }), 469 - images: lx.array(lx.blob({ accept: ["image/png", "image/jpeg"], maxSize: 1000000 }), { maxLength: 4 }), 470 - tags: lx.array(lx.string({ maxLength: 64 })), 471 - langs: lx.array(lx.string()), 472 - }), 473 - }), 474 - }); 475 - `, 476 - ); 477 - 478 - await genEmit(outDir, lexiconFile); 479 - 480 - const outputFile = join(outDir, "app.example.imagePost.json"); 481 - const content = await readFile(outputFile, "utf-8"); 482 - const json = JSON.parse(content); 483 - 484 - expect(json).toEqual({ 485 - lexicon: 1, 486 - id: "app.example.imagePost", 487 - defs: { 488 - main: { 489 - type: "record", 490 - key: "tid", 491 - record: { 492 - type: "object", 493 - properties: { 494 - text: { 495 - type: "string", 496 - maxLength: 300, 497 - maxGraphemes: 300, 498 - required: true, 499 - }, 500 - createdAt: { type: "string", format: "datetime", required: true }, 501 - images: { 502 - type: "array", 503 - items: { 504 - type: "blob", 505 - accept: ["image/png", "image/jpeg"], 506 - maxSize: 1000000, 507 - }, 508 - maxLength: 4, 509 - }, 510 - tags: { 511 - type: "array", 512 - items: { type: "string", maxLength: 64 }, 513 - }, 514 - langs: { 515 - type: "array", 516 - items: { type: "string" }, 517 - }, 518 - }, 519 - required: ["text", "createdAt"], 520 - }, 521 - }, 522 - }, 523 - }); 524 - }); 525 - });
-369
packages/cli/tests/commands/gen-inferred.test.ts
··· 1 - import { expect, test, describe, beforeEach, afterEach } from "vitest"; 2 - import { mkdir, writeFile, rm, readFile } from "node:fs/promises"; 3 - import { join } from "node:path"; 4 - import { genInferred } from "../../src/commands/gen-inferred.ts"; 5 - import { tmpdir } from "node:os"; 6 - 7 - describe("genInferred", () => { 8 - let testDir: string; 9 - let outDir: string; 10 - let schemasDir: string; 11 - 12 - beforeEach(async () => { 13 - // Create a temporary directory for test files 14 - testDir = join(tmpdir(), `prototypey-inferred-test-${Date.now()}`); 15 - outDir = join(testDir, "output"); 16 - schemasDir = join(testDir, "schemas"); 17 - await mkdir(testDir, { recursive: true }); 18 - await mkdir(outDir, { recursive: true }); 19 - await mkdir(schemasDir, { recursive: true }); 20 - }); 21 - 22 - afterEach(async () => { 23 - // Clean up test directory 24 - await rm(testDir, { recursive: true, force: true }); 25 - }); 26 - 27 - test("generates inferred types from a simple schema", async () => { 28 - // Create a test schema file 29 - const schemaFile = join(schemasDir, "app.bsky.actor.profile.json"); 30 - await writeFile( 31 - schemaFile, 32 - JSON.stringify( 33 - { 34 - lexicon: 1, 35 - id: "app.bsky.actor.profile", 36 - defs: { 37 - main: { 38 - type: "record", 39 - key: "self", 40 - record: { 41 - type: "object", 42 - properties: { 43 - displayName: { 44 - type: "string", 45 - maxLength: 64, 46 - maxGraphemes: 64, 47 - }, 48 - description: { 49 - type: "string", 50 - maxLength: 256, 51 - maxGraphemes: 256, 52 - }, 53 - }, 54 - }, 55 - }, 56 - }, 57 - }, 58 - null, 59 - "\t", 60 - ), 61 - ); 62 - 63 - // Run the inferred command 64 - await genInferred(outDir, schemaFile); 65 - 66 - // Read the generated TypeScript file 67 - const outputFile = join(outDir, "app/bsky/actor/profile.ts"); 68 - const content = await readFile(outputFile, "utf-8"); 69 - 70 - // Verify the generated code structure 71 - expect(content).toContain("// Generated by prototypey - DO NOT EDIT"); 72 - expect(content).toContain("// Source: app.bsky.actor.profile"); 73 - expect(content).toContain('import type { Infer } from "prototypey"'); 74 - expect(content).toContain('with { type: "json" }'); 75 - expect(content).toContain("export type Profile = Infer<typeof schema>"); 76 - expect(content).toContain("export const ProfileSchema = schema"); 77 - expect(content).toContain( 78 - "export function isProfile(v: unknown): v is Profile", 79 - ); 80 - expect(content).toContain('v.$type === "app.bsky.actor.profile"'); 81 - }); 82 - 83 - test("generates correct directory structure from NSID", async () => { 84 - // Create a test schema with nested NSID 85 - const schemaFile = join(schemasDir, "app.bsky.feed.post.json"); 86 - await writeFile( 87 - schemaFile, 88 - JSON.stringify({ 89 - lexicon: 1, 90 - id: "app.bsky.feed.post", 91 - defs: { 92 - main: { 93 - type: "record", 94 - key: "tid", 95 - record: { 96 - type: "object", 97 - properties: { 98 - text: { type: "string" }, 99 - }, 100 - }, 101 - }, 102 - }, 103 - }), 104 - ); 105 - 106 - await genInferred(outDir, schemaFile); 107 - 108 - // Verify the directory structure matches NSID 109 - const outputFile = join(outDir, "app/bsky/feed/post.ts"); 110 - const content = await readFile(outputFile, "utf-8"); 111 - 112 - expect(content).toBeTruthy(); 113 - expect(content).toContain("export type Post = Infer<typeof schema>"); 114 - }); 115 - 116 - test("handles multiple schema files with glob patterns", async () => { 117 - // Create multiple schema files 118 - await writeFile( 119 - join(schemasDir, "app.bsky.actor.profile.json"), 120 - JSON.stringify({ 121 - lexicon: 1, 122 - id: "app.bsky.actor.profile", 123 - defs: { main: { type: "record" } }, 124 - }), 125 - ); 126 - 127 - await writeFile( 128 - join(schemasDir, "app.bsky.feed.post.json"), 129 - JSON.stringify({ 130 - lexicon: 1, 131 - id: "app.bsky.feed.post", 132 - defs: { main: { type: "record" } }, 133 - }), 134 - ); 135 - 136 - // Run with glob pattern 137 - await genInferred(outDir, `${schemasDir}/*.json`); 138 - 139 - // Verify both files were created 140 - const profileContent = await readFile( 141 - join(outDir, "app/bsky/actor/profile.ts"), 142 - "utf-8", 143 - ); 144 - const postContent = await readFile( 145 - join(outDir, "app/bsky/feed/post.ts"), 146 - "utf-8", 147 - ); 148 - 149 - expect(profileContent).toContain("export type Profile"); 150 - expect(postContent).toContain("export type Post"); 151 - }); 152 - 153 - test("generates correct relative import path", async () => { 154 - // Create a deeply nested schema 155 - const schemaFile = join(schemasDir, "com.atproto.repo.createRecord.json"); 156 - await writeFile( 157 - schemaFile, 158 - JSON.stringify({ 159 - lexicon: 1, 160 - id: "com.atproto.repo.createRecord", 161 - defs: { 162 - main: { 163 - type: "procedure", 164 - input: { encoding: "application/json" }, 165 - }, 166 - }, 167 - }), 168 - ); 169 - 170 - await genInferred(outDir, schemaFile); 171 - 172 - // Read generated file and check the import path is relative 173 - const outputFile = join(outDir, "com/atproto/repo/createRecord.ts"); 174 - const content = await readFile(outputFile, "utf-8"); 175 - 176 - // The import should be relative to the generated file location 177 - expect(content).toContain('import schema from "'); 178 - expect(content).toContain('.json" with { type: "json" }'); 179 - // Should navigate up from com/atproto/repo/ to schemas/ 180 - expect(content).toMatch(/import schema from ".*createRecord\.json"/); 181 - }); 182 - 183 - test("generates proper type name from NSID", async () => { 184 - // Test various NSID formats 185 - const testCases = [ 186 - { id: "app.bsky.feed.post", expectedType: "Post" }, 187 - { id: "com.atproto.repo.createRecord", expectedType: "CreateRecord" }, 188 - { id: "app.bsky.actor.profile", expectedType: "Profile" }, 189 - { 190 - id: "app.bsky.feed.searchPosts", 191 - expectedType: "SearchPosts", 192 - }, 193 - ]; 194 - 195 - for (const { id, expectedType } of testCases) { 196 - const schemaFile = join(schemasDir, `${id}.json`); 197 - await writeFile( 198 - schemaFile, 199 - JSON.stringify({ 200 - lexicon: 1, 201 - id, 202 - defs: { main: { type: "record" } }, 203 - }), 204 - ); 205 - 206 - const testOutDir = join(testDir, `out-${id}`); 207 - await mkdir(testOutDir, { recursive: true }); 208 - await genInferred(testOutDir, schemaFile); 209 - 210 - const nsidParts = id.split("."); 211 - const outputFile = join(testOutDir, ...nsidParts) + ".ts"; 212 - const content = await readFile(outputFile, "utf-8"); 213 - 214 - expect(content).toContain(`export type ${expectedType}`); 215 - expect(content).toContain(`export const ${expectedType}Schema`); 216 - expect(content).toContain(`export function is${expectedType}`); 217 - } 218 - }); 219 - 220 - test("handles schema without id gracefully", async () => { 221 - // Create an invalid schema without id 222 - const schemaFile = join(schemasDir, "invalid.json"); 223 - await writeFile( 224 - schemaFile, 225 - JSON.stringify({ 226 - lexicon: 1, 227 - defs: { main: { type: "record" } }, 228 - }), 229 - ); 230 - 231 - // Should not throw, but should skip the file 232 - await expect(genInferred(outDir, schemaFile)).resolves.not.toThrow(); 233 - 234 - // Output directory should be empty or not contain generated files 235 - const files = await readFile(outDir, "utf-8").catch(() => null); 236 - expect(files).toBeNull(); 237 - }); 238 - 239 - test("handles schema without defs gracefully", async () => { 240 - // Create an invalid schema without defs 241 - const schemaFile = join(schemasDir, "invalid2.json"); 242 - await writeFile( 243 - schemaFile, 244 - JSON.stringify({ 245 - lexicon: 1, 246 - id: "app.test.invalid", 247 - }), 248 - ); 249 - 250 - // Should not throw, but should skip the file 251 - await expect(genInferred(outDir, schemaFile)).resolves.not.toThrow(); 252 - }); 253 - 254 - test("processes array of schema patterns", async () => { 255 - // Create schemas in different directories 256 - const schemasDir1 = join(testDir, "schemas1"); 257 - const schemasDir2 = join(testDir, "schemas2"); 258 - await mkdir(schemasDir1, { recursive: true }); 259 - await mkdir(schemasDir2, { recursive: true }); 260 - 261 - await writeFile( 262 - join(schemasDir1, "app.one.json"), 263 - JSON.stringify({ 264 - lexicon: 1, 265 - id: "app.one", 266 - defs: { main: { type: "record" } }, 267 - }), 268 - ); 269 - 270 - await writeFile( 271 - join(schemasDir2, "app.two.json"), 272 - JSON.stringify({ 273 - lexicon: 1, 274 - id: "app.two", 275 - defs: { main: { type: "record" } }, 276 - }), 277 - ); 278 - 279 - // Run with array of patterns 280 - await genInferred(outDir, [ 281 - `${schemasDir1}/*.json`, 282 - `${schemasDir2}/*.json`, 283 - ]); 284 - 285 - // Verify both were generated 286 - const oneContent = await readFile(join(outDir, "app/one.ts"), "utf-8"); 287 - const twoContent = await readFile(join(outDir, "app/two.ts"), "utf-8"); 288 - 289 - expect(oneContent).toContain("export type One"); 290 - expect(twoContent).toContain("export type Two"); 291 - }); 292 - 293 - test("generates code with all required components", async () => { 294 - // Create a comprehensive schema 295 - const schemaFile = join(schemasDir, "app.test.complete.json"); 296 - await writeFile( 297 - schemaFile, 298 - JSON.stringify({ 299 - lexicon: 1, 300 - id: "app.test.complete", 301 - defs: { 302 - main: { 303 - type: "record", 304 - key: "tid", 305 - record: { 306 - type: "object", 307 - required: ["text"], 308 - properties: { 309 - text: { type: "string", maxLength: 300 }, 310 - tags: { type: "array", items: { type: "string" } }, 311 - }, 312 - }, 313 - }, 314 - }, 315 - }), 316 - ); 317 - 318 - await genInferred(outDir, schemaFile); 319 - 320 - const outputFile = join(outDir, "app/test/complete.ts"); 321 - const content = await readFile(outputFile, "utf-8"); 322 - 323 - // Check all required exports 324 - expect(content).toContain('import type { Infer } from "prototypey"'); 325 - expect(content).toContain("export type Complete = Infer<typeof schema>"); 326 - expect(content).toContain("export const CompleteSchema = schema"); 327 - expect(content).toContain( 328 - "export function isComplete(v: unknown): v is Complete", 329 - ); 330 - 331 - // Check type guard implementation 332 - expect(content).toContain('typeof v === "object"'); 333 - expect(content).toContain("v !== null"); 334 - expect(content).toContain('"$type" in v'); 335 - expect(content).toContain('v.$type === "app.test.complete"'); 336 - 337 - // Check comments 338 - expect(content).toContain("// Generated by prototypey - DO NOT EDIT"); 339 - expect(content).toContain("// Source: app.test.complete"); 340 - expect(content).toContain( 341 - "* Type-inferred from lexicon schema: app.test.complete", 342 - ); 343 - expect(content).toContain("* The lexicon schema object"); 344 - expect(content).toContain("* Type guard to check if a value is a Complete"); 345 - }); 346 - 347 - test("handles kebab-case and mixed-case NSID parts", async () => { 348 - // Test NSID with different casing 349 - const schemaFile = join(schemasDir, "app.test.myCustomType.json"); 350 - await writeFile( 351 - schemaFile, 352 - JSON.stringify({ 353 - lexicon: 1, 354 - id: "app.test.myCustomType", 355 - defs: { main: { type: "record" } }, 356 - }), 357 - ); 358 - 359 - await genInferred(outDir, schemaFile); 360 - 361 - const outputFile = join(outDir, "app/test/myCustomType.ts"); 362 - const content = await readFile(outputFile, "utf-8"); 363 - 364 - // Should convert to PascalCase 365 - expect(content).toContain("export type MyCustomType"); 366 - expect(content).toContain("export const MyCustomTypeSchema"); 367 - expect(content).toContain("export function isMyCustomType"); 368 - }); 369 - });
-30
packages/cli/tests/fixtures/schemas/app.bsky.actor.profile.json
··· 1 - { 2 - "lexicon": 1, 3 - "id": "app.bsky.actor.profile", 4 - "defs": { 5 - "main": { 6 - "type": "record", 7 - "key": "self", 8 - "record": { 9 - "type": "object", 10 - "properties": { 11 - "displayName": { 12 - "type": "string", 13 - "maxLength": 64, 14 - "maxGraphemes": 64 15 - }, 16 - "description": { 17 - "type": "string", 18 - "maxLength": 256, 19 - "maxGraphemes": 256 20 - }, 21 - "avatar": { 22 - "type": "blob", 23 - "accept": ["image/png", "image/jpeg"], 24 - "maxSize": 1000000 25 - } 26 - } 27 - } 28 - } 29 - } 30 - }
-43
packages/cli/tests/fixtures/schemas/app.bsky.feed.post.json
··· 1 - { 2 - "lexicon": 1, 3 - "id": "app.bsky.feed.post", 4 - "defs": { 5 - "main": { 6 - "type": "record", 7 - "key": "tid", 8 - "record": { 9 - "type": "object", 10 - "required": ["text", "createdAt"], 11 - "properties": { 12 - "text": { 13 - "type": "string", 14 - "maxLength": 300, 15 - "maxGraphemes": 300 16 - }, 17 - "createdAt": { 18 - "type": "string", 19 - "format": "datetime" 20 - }, 21 - "reply": { 22 - "type": "ref", 23 - "ref": "app.bsky.feed.post#replyRef" 24 - } 25 - } 26 - } 27 - }, 28 - "replyRef": { 29 - "type": "object", 30 - "required": ["root", "parent"], 31 - "properties": { 32 - "root": { 33 - "type": "ref", 34 - "ref": "com.atproto.repo.strongRef" 35 - }, 36 - "parent": { 37 - "type": "ref", 38 - "ref": "com.atproto.repo.strongRef" 39 - } 40 - } 41 - } 42 - } 43 - }
-47
packages/cli/tests/fixtures/schemas/app.bsky.feed.searchPosts.json
··· 1 - { 2 - "lexicon": 1, 3 - "id": "app.bsky.feed.searchPosts", 4 - "defs": { 5 - "main": { 6 - "type": "query", 7 - "description": "Find posts matching search criteria", 8 - "parameters": { 9 - "type": "params", 10 - "required": ["q"], 11 - "properties": { 12 - "q": { 13 - "type": "string" 14 - }, 15 - "limit": { 16 - "type": "integer", 17 - "minimum": 1, 18 - "maximum": 100, 19 - "default": 25 20 - }, 21 - "cursor": { 22 - "type": "string" 23 - } 24 - } 25 - }, 26 - "output": { 27 - "encoding": "application/json", 28 - "schema": { 29 - "type": "object", 30 - "required": ["posts"], 31 - "properties": { 32 - "cursor": { 33 - "type": "string" 34 - }, 35 - "posts": { 36 - "type": "array", 37 - "items": { 38 - "type": "ref", 39 - "ref": "app.bsky.feed.defs#postView" 40 - } 41 - } 42 - } 43 - } 44 - } 45 - } 46 - } 47 - }
-11
packages/cli/tests/fixtures/simple-lexicon.ts
··· 1 - import { lx } from "prototypey"; 2 - 3 - export const profileNamespace = lx.lexicon("app.bsky.actor.profile", { 4 - main: lx.record({ 5 - key: "self", 6 - record: lx.object({ 7 - displayName: lx.string({ maxLength: 64, maxGraphemes: 64 }), 8 - description: lx.string({ maxLength: 256, maxGraphemes: 256 }), 9 - }), 10 - }), 11 - });
-51
packages/cli/tests/integration/cli.test.ts
··· 1 - import { expect, test, describe } from "vitest"; 2 - import { runCLI } from "../test-utils.js"; 3 - 4 - describe("CLI Integration", () => { 5 - test("shows error when called without arguments", async () => { 6 - const { stdout, stderr, code } = await runCLI(); 7 - expect(code).toBe(1); 8 - expect(stderr).toContain("No command specified"); 9 - expect(stderr).toContain("Run `$ prototypey --help` for more info"); 10 - }); 11 - 12 - test("shows version", async () => { 13 - const { stdout, stderr } = await runCLI(["--version"]); 14 - expect(stderr).toBe(""); 15 - expect(stdout).toContain("prototypey, 0.0.0"); 16 - }); 17 - 18 - test("shows help for gen-inferred command", async () => { 19 - const { stdout, stderr } = await runCLI(["gen-inferred", "--help"]); 20 - expect(stderr).toBe(""); 21 - expect(stdout).toContain("gen-inferred <outdir> <schemas...>"); 22 - expect(stdout).toContain( 23 - "Generate type-inferred code from lexicon schemas", 24 - ); 25 - }); 26 - 27 - test("shows help for gen-emit command", async () => { 28 - const { stdout, stderr } = await runCLI(["gen-emit", "--help"]); 29 - expect(stderr).toBe(""); 30 - expect(stdout).toContain("gen-emit <outdir> <sources...>"); 31 - expect(stdout).toContain( 32 - "Emit JSON lexicon schemas from authored TypeScript", 33 - ); 34 - }); 35 - 36 - test("handles unknown command", async () => { 37 - const { stdout, stderr, code } = await runCLI(["unknown-command"]); 38 - expect(code).toBe(1); 39 - expect(stderr).toContain("Invalid command: unknown-command"); 40 - expect(stderr).toContain("Run `$ prototypey --help` for more info"); 41 - }); 42 - 43 - test("handles missing arguments", async () => { 44 - const { stdout, stderr, code } = await runCLI(["gen-inferred"]); 45 - expect(code).toBe(1); 46 - expect(stderr).toContain("Insufficient arguments!"); 47 - expect(stderr).toContain( 48 - "Run `$ prototypey gen-inferred --help` for more info", 49 - ); 50 - }); 51 - });
-155
packages/cli/tests/integration/error-handling.test.ts
··· 1 - import { expect, test, describe, beforeEach, afterEach } from "vitest"; 2 - import { mkdir, writeFile, rm } from "node:fs/promises"; 3 - import { join } from "node:path"; 4 - import { tmpdir } from "node:os"; 5 - import { runCLI } from "../test-utils.js"; 6 - 7 - describe("CLI Error Handling", () => { 8 - let testDir: string; 9 - let outDir: string; 10 - let schemasDir: string; 11 - 12 - beforeEach(async () => { 13 - // Create a temporary directory for test files 14 - testDir = join(tmpdir(), `prototypey-error-test-${Date.now()}`); 15 - outDir = join(testDir, "output"); 16 - schemasDir = join(testDir, "schemas"); 17 - await mkdir(testDir, { recursive: true }); 18 - await mkdir(outDir, { recursive: true }); 19 - await mkdir(schemasDir, { recursive: true }); 20 - }); 21 - 22 - afterEach(async () => { 23 - // Clean up test directory 24 - await rm(testDir, { recursive: true, force: true }); 25 - }); 26 - 27 - test("handles non-existent schema files gracefully", async () => { 28 - const { stdout, stderr, code } = await runCLI([ 29 - "gen-inferred", 30 - outDir, 31 - join(schemasDir, "non-existent.json"), 32 - ]); 33 - 34 - expect(code).toBe(0); // Should not crash 35 - expect(stdout).toContain("No schema files found matching patterns"); 36 - expect(stderr).toBe(""); 37 - }); 38 - 39 - test("handles invalid JSON schema files", async () => { 40 - // Create an invalid JSON file 41 - const invalidSchema = join(schemasDir, "invalid.json"); 42 - await writeFile(invalidSchema, "not valid json"); 43 - 44 - const { stdout, stderr, code } = await runCLI([ 45 - "gen-inferred", 46 - outDir, 47 - invalidSchema, 48 - ]); 49 - 50 - expect(code).toBe(1); // Should exit with error 51 - expect(stderr).toContain("Error generating inferred types"); 52 - }); 53 - 54 - test("handles schema files with missing id", async () => { 55 - // Create a schema with missing id 56 - const schemaFile = join(schemasDir, "missing-id.json"); 57 - await writeFile( 58 - schemaFile, 59 - JSON.stringify({ 60 - lexicon: 1, 61 - defs: { main: { type: "record" } }, 62 - }), 63 - ); 64 - 65 - const { stdout, stderr, code } = await runCLI([ 66 - "gen-inferred", 67 - outDir, 68 - schemaFile, 69 - ]); 70 - 71 - expect(code).toBe(0); // Should not crash 72 - expect(stdout).toContain("Found 1 schema file(s)"); 73 - expect(stdout).toContain("Generated inferred types in"); 74 - // Should skip the invalid file silently 75 - }); 76 - 77 - test("handles schema files with missing defs", async () => { 78 - // Create a schema with missing defs 79 - const schemaFile = join(schemasDir, "missing-defs.json"); 80 - await writeFile( 81 - schemaFile, 82 - JSON.stringify({ 83 - lexicon: 1, 84 - id: "app.test.missing", 85 - }), 86 - ); 87 - 88 - const { stdout, stderr, code } = await runCLI([ 89 - "gen-inferred", 90 - outDir, 91 - schemaFile, 92 - ]); 93 - 94 - expect(code).toBe(0); // Should not crash 95 - expect(stdout).toContain("Found 1 schema file(s)"); 96 - expect(stdout).toContain("Generated inferred types in"); 97 - // Should skip the invalid file silently 98 - }); 99 - 100 - test("handles non-existent source files for gen-emit", async () => { 101 - const { stdout, stderr, code } = await runCLI([ 102 - "gen-emit", 103 - outDir, 104 - join(schemasDir, "non-existent.ts"), 105 - ]); 106 - 107 - expect(code).toBe(0); // Should not crash 108 - expect(stdout).toContain("No source files found matching patterns"); 109 - expect(stderr).toBe(""); 110 - }); 111 - 112 - test("handles valid TypeScript files with no lexicon exports for gen-emit", async () => { 113 - // Create a valid TypeScript file with no lexicon exports 114 - const validSource = join(schemasDir, "no-namespace.ts"); 115 - await writeFile(validSource, "export const x = 1;"); 116 - 117 - const { stdout, stderr, code } = await runCLI([ 118 - "gen-emit", 119 - outDir, 120 - validSource, 121 - ]); 122 - 123 - expect(code).toBe(0); // Should not crash 124 - expect(stdout).toContain("Found 1 source file(s)"); 125 - expect(stderr).toContain("No lexicons found"); 126 - }); 127 - 128 - test("handles permission errors when writing output", async () => { 129 - // This test might be platform-specific, so we'll make it lenient 130 - // Create a schema file first 131 - const schemaFile = join(schemasDir, "test.json"); 132 - await writeFile( 133 - schemaFile, 134 - JSON.stringify({ 135 - lexicon: 1, 136 - id: "app.test.permission", 137 - defs: { main: { type: "record" } }, 138 - }), 139 - ); 140 - 141 - // Try to write to a directory that might have permission issues 142 - // We'll use a path that likely won't exist and is invalid 143 - const invalidOutDir = "/invalid/path/that/does/not/exist"; 144 - 145 - const { stdout, stderr, code } = await runCLI([ 146 - "gen-inferred", 147 - invalidOutDir, 148 - schemaFile, 149 - ]); 150 - 151 - // Should handle the error gracefully 152 - expect(code).toBe(1); 153 - expect(stderr).toContain("Error generating inferred types"); 154 - }); 155 - });
-186
packages/cli/tests/integration/filesystem.test.ts
··· 1 - import { expect, test, describe, beforeEach, afterEach } from "vitest"; 2 - import { 3 - mkdir, 4 - writeFile, 5 - rm, 6 - chmod, 7 - access, 8 - constants, 9 - } from "node:fs/promises"; 10 - import { join } from "node:path"; 11 - import { tmpdir } from "node:os"; 12 - import { runCLI } from "../test-utils.js"; 13 - 14 - describe("CLI File System Handling", () => { 15 - let testDir: string; 16 - let outDir: string; 17 - let schemasDir: string; 18 - 19 - beforeEach(async () => { 20 - // Create a temporary directory for test files 21 - testDir = join(tmpdir(), `prototypey-fs-test-${Date.now()}`); 22 - outDir = join(testDir, "output"); 23 - schemasDir = join(testDir, "schemas"); 24 - await mkdir(testDir, { recursive: true }); 25 - await mkdir(schemasDir, { recursive: true }); 26 - }); 27 - 28 - afterEach(async () => { 29 - // Clean up test directory 30 - try { 31 - await rm(testDir, { recursive: true, force: true }); 32 - } catch (error) { 33 - // Ignore cleanup errors 34 - } 35 - }); 36 - 37 - test("creates nested output directories when they don't exist", async () => { 38 - // Create a schema file 39 - const schemaFile = join(schemasDir, "test.json"); 40 - await writeFile( 41 - schemaFile, 42 - JSON.stringify({ 43 - lexicon: 1, 44 - id: "app.deeply.nested.schema", 45 - defs: { main: { type: "record" } }, 46 - }), 47 - ); 48 - 49 - // Use a deeply nested output directory that doesn't exist 50 - const deepOutDir = join(outDir, "very", "deeply", "nested", "path"); 51 - 52 - const { stdout, stderr, code } = await runCLI([ 53 - "gen-inferred", 54 - deepOutDir, 55 - schemaFile, 56 - ]); 57 - 58 - expect(code).toBe(0); 59 - expect(stderr).toBe(""); 60 - expect(stdout).toContain( 61 - "app.deeply.nested.schema -> app/deeply/nested/schema.ts", 62 - ); 63 - 64 - // Verify the file was created in the deeply nested path 65 - const generatedFile = join(deepOutDir, "app/deeply/nested/schema.ts"); 66 - await access(generatedFile, constants.F_OK); 67 - }); 68 - 69 - test("handles special characters in NSID correctly", async () => { 70 - // Create a schema with special characters in the name 71 - const schemaFile = join(schemasDir, "special.json"); 72 - await writeFile( 73 - schemaFile, 74 - JSON.stringify({ 75 - lexicon: 1, 76 - id: "app.test.special-name_with.mixedChars123", 77 - defs: { main: { type: "record" } }, 78 - }), 79 - ); 80 - 81 - const { stdout, stderr, code } = await runCLI([ 82 - "gen-inferred", 83 - outDir, 84 - schemaFile, 85 - ]); 86 - 87 - expect(code).toBe(0); 88 - expect(stderr).toBe(""); 89 - // Should convert to proper PascalCase 90 - expect(stdout).toContain( 91 - "app.test.special-name_with.mixedChars123 -> app/test/special-name_with/mixedChars123.ts", 92 - ); 93 - }); 94 - 95 - test("handles very long NSID paths", async () => { 96 - // Create a schema with a very long NSID 97 - const longNSID = "com." + "verylongdomainname.".repeat(10) + "test"; 98 - const schemaFile = join(schemasDir, "long.json"); 99 - await writeFile( 100 - schemaFile, 101 - JSON.stringify({ 102 - lexicon: 1, 103 - id: longNSID, 104 - defs: { main: { type: "record" } }, 105 - }), 106 - ); 107 - 108 - const { stdout, stderr, code } = await runCLI([ 109 - "gen-inferred", 110 - outDir, 111 - schemaFile, 112 - ]); 113 - 114 - expect(code).toBe(0); 115 - expect(stderr).toBe(""); 116 - expect(stdout).toContain(`Found 1 schema file(s)`); 117 - }); 118 - 119 - test("handles existing files gracefully", async () => { 120 - // Create a schema file 121 - const schemaFile = join(schemasDir, "test.json"); 122 - await writeFile( 123 - schemaFile, 124 - JSON.stringify({ 125 - lexicon: 1, 126 - id: "app.test.overwrite", 127 - defs: { main: { type: "record" } }, 128 - }), 129 - ); 130 - 131 - // Run CLI once 132 - await runCLI(["gen-inferred", outDir, schemaFile]); 133 - 134 - // Verify file exists 135 - const generatedFile = join(outDir, "app/test/overwrite.ts"); 136 - await access(generatedFile, constants.F_OK); 137 - 138 - // Run CLI again (should overwrite) 139 - const { stdout, stderr, code } = await runCLI([ 140 - "gen-inferred", 141 - outDir, 142 - schemaFile, 143 - ]); 144 - 145 - expect(code).toBe(0); 146 - expect(stderr).toBe(""); 147 - expect(stdout).toContain("app.test.overwrite -> app/test/overwrite.ts"); 148 - }); 149 - 150 - test("handles read-only output directory gracefully", async () => { 151 - // This test might be platform-specific and could fail on some systems 152 - // We'll make it lenient to not fail the test suite 153 - 154 - // Create a schema file 155 - const schemaFile = join(schemasDir, "test.json"); 156 - await writeFile( 157 - schemaFile, 158 - JSON.stringify({ 159 - lexicon: 1, 160 - id: "app.test.permission", 161 - defs: { main: { type: "record" } }, 162 - }), 163 - ); 164 - 165 - // Create output directory and make it read-only 166 - const readOnlyDir = join(outDir, "readonly"); 167 - await mkdir(readOnlyDir, { recursive: true }); 168 - 169 - // Try to make read-only (might not work on all systems) 170 - try { 171 - await chmod(readOnlyDir, 0o444); 172 - } catch (error) { 173 - // Ignore if we can't change permissions 174 - } 175 - 176 - const { stdout, stderr, code } = await runCLI([ 177 - "gen-inferred", 178 - readOnlyDir, 179 - schemaFile, 180 - ]); 181 - 182 - // Should handle the error gracefully 183 - // On some systems this might succeed, on others fail - we just want it to not crash 184 - expect([0, 1]).toContain(code); 185 - }); 186 - });
-191
packages/cli/tests/integration/workflow.test.ts
··· 1 - import { expect, test, describe, beforeEach, afterEach } from "vitest"; 2 - import { mkdir, writeFile, rm, readFile } from "node:fs/promises"; 3 - import { join } from "node:path"; 4 - import { tmpdir } from "node:os"; 5 - import { runCLI } from "../test-utils.js"; 6 - 7 - describe("CLI End-to-End Workflow", () => { 8 - let testDir: string; 9 - let schemasDir: string; 10 - let generatedDir: string; 11 - 12 - beforeEach(async () => { 13 - // Create a temporary directory for test files 14 - testDir = join(tmpdir(), `prototypey-e2e-test-${Date.now()}`); 15 - schemasDir = join(testDir, "schemas"); 16 - generatedDir = join(testDir, "generated"); 17 - await mkdir(testDir, { recursive: true }); 18 - await mkdir(schemasDir, { recursive: true }); 19 - await mkdir(generatedDir, { recursive: true }); 20 - }); 21 - 22 - afterEach(async () => { 23 - // Clean up test directory 24 - await rm(testDir, { recursive: true, force: true }); 25 - }); 26 - 27 - test("complete workflow: JSON schema -> inferred types", async () => { 28 - // Step 1: Create JSON schema file directly (avoiding dynamic import issues) 29 - const schemaPath = join(schemasDir, "app.test.profile.json"); 30 - await writeFile( 31 - schemaPath, 32 - JSON.stringify( 33 - { 34 - lexicon: 1, 35 - id: "app.test.profile", 36 - defs: { 37 - main: { 38 - type: "record", 39 - key: "self", 40 - record: { 41 - type: "object", 42 - properties: { 43 - displayName: { type: "string", maxLength: 64 }, 44 - description: { type: "string", maxLength: 256 }, 45 - }, 46 - }, 47 - }, 48 - }, 49 - }, 50 - null, 51 - 2, 52 - ), 53 - ); 54 - 55 - // Step 2: Generate inferred TypeScript from JSON schema 56 - const inferResult = await runCLI([ 57 - "gen-inferred", 58 - generatedDir, 59 - schemaPath, 60 - ]); 61 - 62 - console.log("Infer result code:", inferResult.code); 63 - console.log("Infer stdout:", inferResult.stdout); 64 - console.log("Infer stderr:", inferResult.stderr); 65 - 66 - expect(inferResult.code).toBe(0); 67 - expect(inferResult.stdout).toContain("Generated inferred types in"); 68 - expect(inferResult.stdout).toContain( 69 - "app.test.profile -> app/test/profile.ts", 70 - ); 71 - 72 - // Verify generated TypeScript file 73 - const generatedPath = join(generatedDir, "app/test/profile.ts"); 74 - const generatedContent = await readFile(generatedPath, "utf-8"); 75 - expect(generatedContent).toContain( 76 - 'import type { Infer } from "prototypey"', 77 - ); 78 - expect(generatedContent).toContain( 79 - "export type Profile = Infer<typeof schema>", 80 - ); 81 - expect(generatedContent).toContain("export const ProfileSchema = schema"); 82 - expect(generatedContent).toContain( 83 - "export function isProfile(v: unknown): v is Profile", 84 - ); 85 - }); 86 - 87 - test("workflow with multiple schemas", async () => { 88 - // Create multiple JSON schema files 89 - const postSchema = join(schemasDir, "app.test.post.json"); 90 - await writeFile( 91 - postSchema, 92 - JSON.stringify( 93 - { 94 - lexicon: 1, 95 - id: "app.test.post", 96 - defs: { 97 - main: { 98 - type: "record", 99 - key: "tid", 100 - record: { 101 - type: "object", 102 - properties: { 103 - text: { type: "string", maxLength: 300, required: true }, 104 - createdAt: { 105 - type: "string", 106 - format: "datetime", 107 - required: true, 108 - }, 109 - }, 110 - }, 111 - }, 112 - }, 113 - }, 114 - null, 115 - 2, 116 - ), 117 - ); 118 - 119 - const searchSchema = join(schemasDir, "app.test.searchPosts.json"); 120 - await writeFile( 121 - searchSchema, 122 - JSON.stringify( 123 - { 124 - lexicon: 1, 125 - id: "app.test.searchPosts", 126 - defs: { 127 - main: { 128 - type: "query", 129 - parameters: { 130 - type: "params", 131 - properties: { 132 - q: { type: "string", required: true }, 133 - limit: { 134 - type: "integer", 135 - minimum: 1, 136 - maximum: 100, 137 - default: 25, 138 - }, 139 - }, 140 - required: ["q"], 141 - }, 142 - output: { 143 - encoding: "application/json", 144 - schema: { 145 - type: "object", 146 - properties: { 147 - posts: { 148 - type: "array", 149 - items: { type: "ref", ref: "app.test.post#main" }, 150 - required: true, 151 - }, 152 - }, 153 - required: ["posts"], 154 - }, 155 - }, 156 - }, 157 - }, 158 - }, 159 - null, 160 - 2, 161 - ), 162 - ); 163 - 164 - // Generate inferred types 165 - const inferResult = await runCLI([ 166 - "gen-inferred", 167 - generatedDir, 168 - `${schemasDir}/*.json`, 169 - ]); 170 - expect(inferResult.code).toBe(0); 171 - expect(inferResult.stdout).toContain("app.test.post -> app/test/post.ts"); 172 - expect(inferResult.stdout).toContain( 173 - "app.test.searchPosts -> app/test/searchPosts.ts", 174 - ); 175 - 176 - // Verify both generated files exist and have correct content 177 - const postContent = await readFile( 178 - join(generatedDir, "app/test/post.ts"), 179 - "utf-8", 180 - ); 181 - const searchContent = await readFile( 182 - join(generatedDir, "app/test/searchPosts.ts"), 183 - "utf-8", 184 - ); 185 - 186 - expect(postContent).toContain("export type Post = Infer<typeof schema>"); 187 - expect(searchContent).toContain( 188 - "export type SearchPosts = Infer<typeof schema>", 189 - ); 190 - }); 191 - });
-210
packages/cli/tests/performance/large-schema-set.test.ts
··· 1 - import { expect, test, describe, beforeEach, afterEach } from "vitest"; 2 - import { mkdir, writeFile, rm } from "node:fs/promises"; 3 - import { join } from "node:path"; 4 - import { tmpdir } from "node:os"; 5 - import { runCLI } from "../test-utils.js"; 6 - 7 - describe("CLI Performance", () => { 8 - let testDir: string; 9 - let outDir: string; 10 - let schemasDir: string; 11 - 12 - beforeEach(async () => { 13 - // Create a temporary directory for test files 14 - testDir = join(tmpdir(), `prototypey-perf-test-${Date.now()}`); 15 - outDir = join(testDir, "output"); 16 - schemasDir = join(testDir, "schemas"); 17 - await mkdir(testDir, { recursive: true }); 18 - await mkdir(outDir, { recursive: true }); 19 - await mkdir(schemasDir, { recursive: true }); 20 - }); 21 - 22 - afterEach(async () => { 23 - // Clean up test directory 24 - await rm(testDir, { recursive: true, force: true }); 25 - }); 26 - 27 - test("handles large number of schemas efficiently", async () => { 28 - // Create 50 schema files 29 - const schemaCount = 50; 30 - const schemaFiles = []; 31 - 32 - for (let i = 0; i < schemaCount; i++) { 33 - const schemaFile = join(schemasDir, `test${i}.json`); 34 - await writeFile( 35 - schemaFile, 36 - JSON.stringify({ 37 - lexicon: 1, 38 - id: `app.test.schema${i}`, 39 - defs: { 40 - main: { 41 - type: "record", 42 - key: "tid", 43 - record: { 44 - type: "object", 45 - properties: { 46 - name: { type: "string", maxLength: 64 }, 47 - value: { type: "integer" }, 48 - }, 49 - }, 50 - }, 51 - }, 52 - }), 53 - ); 54 - schemaFiles.push(schemaFile); 55 - } 56 - 57 - const startTime = Date.now(); 58 - const { stdout, stderr, code } = await runCLI([ 59 - "gen-inferred", 60 - outDir, 61 - `${schemasDir}/*.json`, 62 - ]); 63 - const endTime = Date.now(); 64 - 65 - const duration = endTime - startTime; 66 - 67 - expect(code).toBe(0); 68 - expect(stdout).toContain(`Found ${schemaCount} schema file(s)`); 69 - expect(stderr).toBe(""); 70 - 71 - // Should complete within reasonable time (less than 5 seconds for 50 files) 72 - expect(duration).toBeLessThan(5000); 73 - 74 - // Verify some generated files exist 75 - expect(stdout).toContain("app.test.schema0 -> app/test/schema0.ts"); 76 - expect(stdout).toContain("app.test.schema49 -> app/test/schema49.ts"); 77 - }); 78 - 79 - test("memory usage stays reasonable with large schemas", async () => { 80 - // Create a schema with complex nested structure 81 - const complexSchema = join(schemasDir, "complex.json"); 82 - await writeFile( 83 - complexSchema, 84 - JSON.stringify({ 85 - lexicon: 1, 86 - id: "app.test.complex", 87 - defs: { 88 - main: { 89 - type: "record", 90 - key: "tid", 91 - record: { 92 - type: "object", 93 - properties: { 94 - // Create a deeply nested structure 95 - level1: { 96 - type: "object", 97 - properties: { 98 - level2: { 99 - type: "object", 100 - properties: { 101 - level3: { 102 - type: "object", 103 - properties: { 104 - level4: { 105 - type: "object", 106 - properties: { 107 - items: { 108 - type: "array", 109 - items: { 110 - type: "object", 111 - properties: { 112 - id: { type: "string" }, 113 - data: { type: "string" }, 114 - metadata: { 115 - type: "object", 116 - properties: { 117 - created: { 118 - type: "string", 119 - format: "datetime", 120 - }, 121 - updated: { 122 - type: "string", 123 - format: "datetime", 124 - }, 125 - tags: { 126 - type: "array", 127 - items: { type: "string" }, 128 - }, 129 - }, 130 - }, 131 - }, 132 - }, 133 - }, 134 - }, 135 - }, 136 - }, 137 - }, 138 - }, 139 - }, 140 - }, 141 - }, 142 - }, 143 - }, 144 - }, 145 - }, 146 - }), 147 - ); 148 - 149 - const startTime = Date.now(); 150 - const { stdout, stderr, code } = await runCLI([ 151 - "gen-inferred", 152 - outDir, 153 - complexSchema, 154 - ]); 155 - const endTime = Date.now(); 156 - 157 - const duration = endTime - startTime; 158 - 159 - expect(code).toBe(0); 160 - expect(stdout).toContain("Found 1 schema file(s)"); 161 - expect(stdout).toContain("app.test.complex -> app/test/complex.ts"); 162 - expect(stderr).toBe(""); 163 - 164 - // Should complete within reasonable time (less than 2 seconds) 165 - expect(duration).toBeLessThan(2000); 166 - }); 167 - 168 - test("concurrent processing of multiple commands", async () => { 169 - // Create test schemas 170 - const schema1 = join(schemasDir, "test1.json"); 171 - const schema2 = join(schemasDir, "test2.json"); 172 - 173 - await writeFile( 174 - schema1, 175 - JSON.stringify({ 176 - lexicon: 1, 177 - id: "app.test.concurrent1", 178 - defs: { 179 - main: { type: "record", key: "tid", record: { type: "object" } }, 180 - }, 181 - }), 182 - ); 183 - 184 - await writeFile( 185 - schema2, 186 - JSON.stringify({ 187 - lexicon: 1, 188 - id: "app.test.concurrent2", 189 - defs: { 190 - main: { type: "record", key: "tid", record: { type: "object" } }, 191 - }, 192 - }), 193 - ); 194 - 195 - // Run two CLI commands concurrently 196 - const [result1, result2] = await Promise.all([ 197 - runCLI(["gen-inferred", join(outDir, "out1"), schema1]), 198 - runCLI(["gen-inferred", join(outDir, "out2"), schema2]), 199 - ]); 200 - 201 - expect(result1.code).toBe(0); 202 - expect(result2.code).toBe(0); 203 - expect(result1.stdout).toContain( 204 - "app.test.concurrent1 -> app/test/concurrent1.ts", 205 - ); 206 - expect(result2.stdout).toContain( 207 - "app.test.concurrent2 -> app/test/concurrent2.ts", 208 - ); 209 - }); 210 - });
-34
packages/cli/tests/test-utils.ts
··· 1 - import { spawn } from "node:child_process"; 2 - import { dirname, join } from "node:path"; 3 - import { fileURLToPath } from "node:url"; 4 - 5 - export function runCLI( 6 - args: string[] = [], 7 - options?: { cwd?: string; env?: NodeJS.ProcessEnv }, 8 - ): Promise<{ stdout: string; stderr: string; code: number }> { 9 - return new Promise((resolve) => { 10 - const cliPath = join( 11 - dirname(fileURLToPath(import.meta.url)), 12 - "../lib/index.js", 13 - ); 14 - const child = spawn("node", [cliPath, ...args], { 15 - cwd: options?.cwd ?? process.cwd(), 16 - env: options?.env ?? process.env, 17 - }); 18 - 19 - let stdout = ""; 20 - let stderr = ""; 21 - 22 - child.stdout.on("data", (data) => { 23 - stdout += data.toString(); 24 - }); 25 - 26 - child.stderr.on("data", (data) => { 27 - stderr += data.toString(); 28 - }); 29 - 30 - child.on("close", (code) => { 31 - resolve({ stdout, stderr, code: code ?? 0 }); 32 - }); 33 - }); 34 - }
-122
packages/cli/tests/unit/template-edge-cases.test.ts
··· 1 - import { expect, test, describe } from "vitest"; 2 - import { generateInferredCode } from "../../src/templates/inferred.ts"; 3 - 4 - describe("Template Edge Cases", () => { 5 - test("handles NSID with trailing numbers correctly", () => { 6 - const schema = { 7 - lexicon: 1, 8 - id: "app.test.v1", 9 - defs: { main: { type: "record" } }, 10 - }; 11 - 12 - const code = generateInferredCode(schema, "/test/v1.json", "/output"); 13 - expect(code).toContain("export type V1 = Infer<typeof schema>"); 14 - expect(code).toContain("export const V1Schema = schema"); 15 - expect(code).toContain("export function isV1(v: unknown): v is V1"); 16 - }); 17 - 18 - test("handles NSID with multiple consecutive separators", () => { 19 - const schema = { 20 - lexicon: 1, 21 - id: "app.test.my--double--dash", 22 - defs: { main: { type: "record" } }, 23 - }; 24 - 25 - const code = generateInferredCode(schema, "/test/double.json", "/output"); 26 - expect(code).toContain("export type MyDoubleDash = Infer<typeof schema>"); 27 - }); 28 - 29 - test("handles single character NSID parts", () => { 30 - const schema = { 31 - lexicon: 1, 32 - id: "a.b.c", 33 - defs: { main: { type: "record" } }, 34 - }; 35 - 36 - const code = generateInferredCode(schema, "/test/single.json", "/output"); 37 - expect(code).toContain("export type C = Infer<typeof schema>"); 38 - }); 39 - 40 - test("handles NSID with underscores and mixed case", () => { 41 - const schema = { 42 - lexicon: 1, 43 - id: "app.test.my_custom_Type_Name", 44 - defs: { main: { type: "record" } }, 45 - }; 46 - 47 - const code = generateInferredCode(schema, "/test/custom.json", "/output"); 48 - expect(code).toContain( 49 - "export type MyCustomTypeName = Infer<typeof schema>", 50 - ); 51 - }); 52 - 53 - test("handles very long NSID name", () => { 54 - const longName = "a".repeat(100); 55 - const schema = { 56 - lexicon: 1, 57 - id: `app.test.${longName}`, 58 - defs: { main: { type: "record" } }, 59 - }; 60 - 61 - const code = generateInferredCode(schema, "/test/long.json", "/output"); 62 - // Should not crash and should generate valid TypeScript 63 - expect(code).toContain("export type"); 64 - expect(code).toContain("Infer<typeof schema>"); 65 - }); 66 - 67 - test("handles schema with no main def", () => { 68 - const schema = { 69 - lexicon: 1, 70 - id: "app.test.no-main", 71 - defs: { 72 - other: { type: "object" }, 73 - }, 74 - }; 75 - 76 - const code = generateInferredCode(schema, "/test/no-main.json", "/output"); 77 - // Should still generate valid code even without main def 78 - expect(code).toContain("export type NoMain = Infer<typeof schema>"); 79 - // The path will be relative with ../../../ prefix 80 - expect(code).toContain( 81 - 'import schema from "../../../test/no-main.json" with { type: "json" };', 82 - ); 83 - }); 84 - 85 - test("generates correct relative paths for deeply nested output", () => { 86 - const schema = { 87 - lexicon: 1, 88 - id: "app.bsky.feed.post", 89 - defs: { main: { type: "record" } }, 90 - }; 91 - 92 - const code = generateInferredCode( 93 - schema, 94 - "/project/schemas/feed.json", 95 - "/project/generated/inferred", 96 - ); 97 - 98 - // Should have correct relative import path 99 - expect(code).toContain( 100 - 'import schema from "../../../../../schemas/feed.json" with { type: "json" };', 101 - ); 102 - }); 103 - 104 - test("handles special characters in import paths", () => { 105 - const schema = { 106 - lexicon: 1, 107 - id: "app.test.special", 108 - defs: { main: { type: "record" } }, 109 - }; 110 - 111 - const code = generateInferredCode( 112 - schema, 113 - "/project/schemas with spaces/special[chars].json", 114 - "/project/generated", 115 - ); 116 - 117 - // Should handle spaces and special characters in paths 118 - expect(code).toContain( 119 - 'import schema from "../../../schemas with spaces/special[chars].json" with { type: "json" };', 120 - ); 121 - }); 122 - });
-4
packages/cli/tsconfig.json
··· 1 - { 2 - "extends": "../../tsconfig.json", 3 - "include": ["src", "tests"] 4 - }
-8
packages/cli/tsdown.config.ts
··· 1 - import { defineConfig } from "tsdown"; 2 - 3 - export default defineConfig({ 4 - dts: true, 5 - entry: ["src/index.ts"], 6 - outDir: "lib", 7 - unbundle: true, 8 - });
-7
packages/cli/vitest.config.ts
··· 1 - import { defineConfig } from "vitest/config"; 2 - 3 - export default defineConfig({ 4 - test: { 5 - include: ["tests/**/*.test.ts"], 6 - }, 7 - });
+85
packages/prototypey/CHANGELOG.md
··· 1 + # prototypey 2 + 3 + ## 0.3.8 4 + 5 + ### Patch Changes 6 + 7 + - 7a19f90: releast changes from dep updates and #66 8 + 9 + ## 0.3.7 10 + 11 + ### Patch Changes 12 + 13 + - e75de54: update docs 14 + 15 + ## 0.3.6 16 + 17 + ### Patch Changes 18 + 19 + - 2b55317: fix exported type bug 20 + 21 + ## 0.3.5 22 + 23 + ### Patch Changes 24 + 25 + - abb4b31: updated docs 26 + 27 + ## 0.3.4 28 + 29 + ### Patch Changes 30 + 31 + - 3329654: fix for type of record key and description hint 32 + 33 + ## 0.3.3 34 + 35 + ### Patch Changes 36 + 37 + - e7a7497: documentation update 38 + 39 + ## 0.3.2 40 + 41 + ### Patch Changes 42 + 43 + - 6a6cae5: update deps 44 + 45 + ## 0.3.1 46 + 47 + ### Patch Changes 48 + 49 + - d5d3143: update docs - we're featured! 50 + 51 + ## 0.3.0 52 + 53 + ### Minor Changes 54 + 55 + - 91a8c84: generate prototypey lexicon utils from json definitions 56 + 57 + ## 0.2.6 58 + 59 + ### Patch Changes 60 + 61 + - 6c5569b: only export intended items 62 + 63 + ## 0.2.5 64 + 65 + ### Patch Changes 66 + 67 + - 15d5b7c: hide infer as ~infer 68 + 69 + ## 0.2.4 70 + 71 + ### Patch Changes 72 + 73 + - 0b16bc3: use spaces for readme so npm doesn't format it weird 74 + 75 + ## 0.2.3 76 + 77 + ### Patch Changes 78 + 79 + - 708fc60: fix loading of version in cli 80 + 81 + ## 0.2.2 82 + 83 + ### Patch Changes 84 + 85 + - 0bb8603: moved cli into core library - no more @prototypey/cli separate
+228
packages/prototypey/README.md
··· 1 + # prototypey 2 + 3 + A fully-featured sdk for developing lexicons with typescript. 4 + 5 + Below this is the docs and features of the library. If you'd like the story for why prototypey exists and what it's good for: [that's published here](https://notes.tylur.dev/3m5a3do4eus2w) 6 + 7 + ## Features 8 + 9 + - atproto spec lexicon authoring with in IDE docs & hints for each attribute (ts => json) 10 + - CLI to generate json from ts definitions 11 + - CLI to generate ts from json definitions 12 + - inference of usage type from full lexicon definition 13 + - the really cool part of this is that it fills in the refs from the defs all at the type level 14 + - `lx.lexicon(...).validate(data)` for validating data using `@atproto/lexicon` 15 + - `fromJSON()` helper for creating lexicons directly from JSON objects with full type inference 16 + 17 + ## Installation 18 + 19 + ```bash 20 + npm install prototypey 21 + ``` 22 + 23 + ## Usage 24 + 25 + Prototypey provides both a TypeScript library for authoring lexicons and a CLI for code generation. 26 + 27 + ### Authoring Lexicons 28 + 29 + **what you'll write:** 30 + 31 + ```ts 32 + const lex = lx.lexicon("app.bsky.actor.profile", { 33 + main: lx.record({ 34 + key: "self", 35 + record: lx.object({ 36 + displayName: lx.string({ maxLength: 64, maxGraphemes: 64 }), 37 + description: lx.string({ maxLength: 256, maxGraphemes: 256 }), 38 + }), 39 + }), 40 + }); 41 + ``` 42 + 43 + **generates to:** 44 + 45 + ```json 46 + { 47 + "lexicon": 1, 48 + "id": "app.bsky.actor.profile", 49 + "defs": { 50 + "main": { 51 + "type": "record", 52 + "key": "self", 53 + "record": { 54 + "type": "object", 55 + "properties": { 56 + "displayName": { 57 + "type": "string", 58 + "maxLength": 64, 59 + "maxGraphemes": 64 60 + }, 61 + "description": { 62 + "type": "string", 63 + "maxLength": 256, 64 + "maxGraphemes": 256 65 + } 66 + } 67 + } 68 + } 69 + } 70 + } 71 + ``` 72 + 73 + you could also access the json definition with `lex.json()`. 74 + 75 + ### Runtime Validation 76 + 77 + Prototypey provides runtime validation using [@atproto/lexicon](https://www.npmjs.com/package/@atproto/lexicon): 78 + 79 + ```ts 80 + const lex = lx.lexicon("app.bsky.actor.profile", { 81 + main: lx.record({ 82 + key: "self", 83 + record: lx.object({ 84 + displayName: lx.string({ maxLength: 64, maxGraphemes: 64 }), 85 + description: lx.string({ maxLength: 256, maxGraphemes: 256 }), 86 + }), 87 + }), 88 + }); 89 + 90 + // Validate data against the schema 91 + const result = lex.validate({ 92 + displayName: "Alice", 93 + description: "Software engineer", 94 + }); 95 + 96 + if (result.success) { 97 + console.log("Valid data:", result.value); 98 + } else { 99 + console.error("Validation error:", result.error); 100 + } 101 + ``` 102 + 103 + **Validating against specific definitions:** 104 + 105 + If your lexicon has multiple definitions, you can validate against a specific one: 106 + 107 + ```ts 108 + const lex = lx.lexicon("app.bsky.feed.post", { 109 + user: lx.object({ 110 + handle: lx.string({ required: true }), 111 + displayName: lx.string(), 112 + }), 113 + main: lx.record({ 114 + key: "tid", 115 + record: lx.object({ 116 + text: lx.string({ required: true }), 117 + author: lx.ref("#user", { required: true }), 118 + }), 119 + }), 120 + }); 121 + 122 + // Validate against the "user" definition 123 + const userResult = lex.validate( 124 + { handle: "alice.bsky.social", displayName: "Alice" }, 125 + "user", 126 + ); 127 + 128 + // Validate against "main" (default if not specified) 129 + const postResult = lex.validate({ 130 + text: "Hello world", 131 + author: { handle: "bob.bsky.social" }, 132 + }); 133 + ``` 134 + 135 + ### CLI Commands 136 + 137 + The `prototypey` package includes a CLI with two main commands: 138 + 139 + #### `gen-emit` - Emit JSON schemas from TypeScript 140 + 141 + ```bash 142 + prototypey gen-emit <outdir> <sources...> 143 + ``` 144 + 145 + Extracts JSON schemas from TypeScript lexicon definitions. 146 + 147 + **Example:** 148 + 149 + ```bash 150 + prototypey gen-emit ./lexicons ./src/lexicons/**/*.ts 151 + ``` 152 + 153 + #### `gen-from-json` - Generate TypeScript from JSON schemas 154 + 155 + ```bash 156 + prototypey gen-from-json <outdir> <sources...> 157 + ``` 158 + 159 + Generates TypeScript files from JSON lexicon schemas using the `fromJSON` helper. This is useful when you have existing lexicon JSON files and want to work with them in TypeScript with full type inference. 160 + 161 + **Example:** 162 + 163 + ```bash 164 + prototypey gen-from-json ./src/lexicons ./lexicons/**/*.json 165 + ``` 166 + 167 + This will create TypeScript files that export typed lexicon objects: 168 + 169 + ```ts 170 + // Generated file: src/lexicons/app.bsky.feed.post.ts 171 + import { fromJSON } from "prototypey"; 172 + 173 + export const appBskyFeedPost = fromJSON({ 174 + // ... lexicon JSON 175 + }); 176 + ``` 177 + 178 + ### Typical Workflows 179 + 180 + #### TypeScript-first workflow 181 + 182 + 1. Author lexicons in TypeScript using the library 183 + 2. Emit JSON schemas with `gen-emit` for runtime validation 184 + 185 + **Recommended:** Add as a script to your `package.json`: 186 + 187 + ```json 188 + { 189 + "scripts": { 190 + "lexicon:emit": "prototypey gen-emit ./schemas ./src/lexicons/**/*.ts" 191 + } 192 + } 193 + ``` 194 + 195 + Then run: 196 + 197 + ```bash 198 + npm run lexicon:emit 199 + ``` 200 + 201 + #### JSON-first workflow 202 + 203 + 1. Start with JSON lexicon schemas (e.g., from atproto) 204 + 2. Generate TypeScript with `gen-from-json` for type-safe access 205 + 206 + **Recommended:** Add as a script to your `package.json`: 207 + 208 + ```json 209 + { 210 + "scripts": { 211 + "lexicon:import": "prototypey gen-from-json ./src/lexicons ./lexicons/**/*.json" 212 + } 213 + } 214 + ``` 215 + 216 + Then run: 217 + 218 + ```bash 219 + npm run lexicon:import 220 + ``` 221 + 222 + --- 223 + 224 + Please give any and all feedback. I've not really written many lexicons much myself yet, so this project is at a point of "well I think this makes sense". Both the [issues page](https://github.com/tylersayshi/prototypey/issues) and [discussions](https://github.com/tylersayshi/prototypey/discussions) are open and ready for y'all ๐Ÿ™‚. 225 + 226 + **Call For Contribution:** 227 + 228 + We need library art! Please reach out if you'd be willing to contribute some drawings or anything :)
+102
packages/prototypey/cli/gen-emit.ts
··· 1 + import { glob } from "tinyglobby"; 2 + import { mkdir, writeFile } from "node:fs/promises"; 3 + import { join } from "node:path"; 4 + import { pathToFileURL } from "node:url"; 5 + 6 + interface LexiconNamespace { 7 + json: { 8 + lexicon: number; 9 + id: string; 10 + defs: Record<string, unknown>; 11 + }; 12 + } 13 + 14 + export async function genEmit( 15 + outdir: string, 16 + sources: string | string[], 17 + ): Promise<void> { 18 + try { 19 + const sourcePatterns = Array.isArray(sources) ? sources : [sources]; 20 + 21 + // Find all source files matching the patterns 22 + const sourceFiles = await glob(sourcePatterns, { 23 + absolute: true, 24 + onlyFiles: true, 25 + }); 26 + 27 + if (sourceFiles.length === 0) { 28 + console.log("No source files found matching patterns:", sourcePatterns); 29 + return; 30 + } 31 + 32 + console.log(`Found ${String(sourceFiles.length)} source file(s)`); 33 + 34 + // Ensure output directory exists 35 + await mkdir(outdir, { recursive: true }); 36 + 37 + // Process each source file 38 + for (const sourcePath of sourceFiles) { 39 + await processSourceFile(sourcePath, outdir); 40 + } 41 + 42 + console.log(`\nEmitted lexicon schemas to ${outdir}`); 43 + } catch (error) { 44 + console.error("Error emitting lexicon schemas:", error); 45 + process.exit(1); 46 + } 47 + } 48 + 49 + async function processSourceFile( 50 + sourcePath: string, 51 + outdir: string, 52 + ): Promise<void> { 53 + try { 54 + // Convert file path to file URL for dynamic import 55 + const fileUrl = pathToFileURL(sourcePath).href; 56 + 57 + // Dynamically import the module 58 + const module = await import(fileUrl); 59 + 60 + // Find all exported lexicons 61 + const lexicons: LexiconNamespace[] = []; 62 + for (const key of Object.keys(module)) { 63 + const exported = module[key]; 64 + // Check if it's a lexicon with a json property 65 + if ( 66 + exported && 67 + typeof exported === "object" && 68 + "json" in exported && 69 + exported.json && 70 + typeof exported.json === "object" && 71 + "lexicon" in exported.json && 72 + "id" in exported.json && 73 + "defs" in exported.json 74 + ) { 75 + lexicons.push(exported as LexiconNamespace); 76 + } 77 + } 78 + 79 + if (lexicons.length === 0) { 80 + console.warn(` โš  ${sourcePath}: No lexicons found`); 81 + return; 82 + } 83 + 84 + // Emit JSON for each lexicon 85 + for (const lexicon of lexicons) { 86 + const { id } = lexicon.json; 87 + const outputPath = join(outdir, `${id}.json`); 88 + 89 + // Write the JSON file 90 + await writeFile( 91 + outputPath, 92 + JSON.stringify(lexicon.json, null, "\t"), 93 + "utf-8", 94 + ); 95 + 96 + console.log(` โœ“ ${id} -> ${id}.json`); 97 + } 98 + } catch (error) { 99 + console.error(` โœ— Error processing ${sourcePath}:`, error); 100 + throw error; 101 + } 102 + }
+111
packages/prototypey/cli/gen-from-json.ts
··· 1 + import { glob } from "tinyglobby"; 2 + import { mkdir, writeFile, readFile } from "node:fs/promises"; 3 + import { join, dirname, basename } from "node:path"; 4 + 5 + interface LexiconJSON { 6 + lexicon: number; 7 + id: string; 8 + defs: Record<string, unknown>; 9 + } 10 + 11 + /** 12 + * Converts a lexicon ID to a valid TypeScript export name 13 + * e.g., "app.bsky.feed.post" -> "appBskyFeedPost" 14 + * "com.atproto.repo.createRecord" -> "comAtprotoRepoCreateRecord" 15 + */ 16 + function lexiconIdToExportName(id: string): string { 17 + // Split by dots and handle camelCase conversion 18 + const parts = id.split("."); 19 + 20 + // For the first part (e.g., "app", "com"), keep it lowercase 21 + // For subsequent parts, capitalize the first letter of each word 22 + // But preserve any existing camelCase within parts 23 + return parts 24 + .map((part, index) => { 25 + if (index === 0) return part; 26 + // Capitalize first letter of the part 27 + return part.charAt(0).toUpperCase() + part.slice(1); 28 + }) 29 + .join(""); 30 + } 31 + 32 + export async function genFromJSON( 33 + outdir: string, 34 + sources: string | string[], 35 + ): Promise<void> { 36 + try { 37 + const sourcePatterns = Array.isArray(sources) ? sources : [sources]; 38 + 39 + // Find all JSON files matching the patterns 40 + const jsonFiles = await glob(sourcePatterns, { 41 + absolute: true, 42 + onlyFiles: true, 43 + }); 44 + 45 + if (jsonFiles.length === 0) { 46 + console.log("No JSON files found matching patterns:", sourcePatterns); 47 + return; 48 + } 49 + 50 + console.log(`Found ${String(jsonFiles.length)} JSON file(s)`); 51 + 52 + // Ensure output directory exists 53 + await mkdir(outdir, { recursive: true }); 54 + 55 + // Process each JSON file 56 + for (const jsonPath of jsonFiles) { 57 + await processJSONFile(jsonPath, outdir); 58 + } 59 + 60 + console.log(`\nGenerated TypeScript files in ${outdir}`); 61 + } catch (error) { 62 + console.error("Error generating TypeScript from JSON:", error); 63 + process.exit(1); 64 + } 65 + } 66 + 67 + async function processJSONFile( 68 + jsonPath: string, 69 + outdir: string, 70 + ): Promise<void> { 71 + try { 72 + // Read and parse the JSON file 73 + const content = await readFile(jsonPath, "utf-8"); 74 + const lexiconJSON = JSON.parse(content); 75 + 76 + // Validate it's a lexicon 77 + if ( 78 + !lexiconJSON.lexicon || 79 + !lexiconJSON.id || 80 + !lexiconJSON.defs || 81 + typeof lexiconJSON.defs !== "object" 82 + ) { 83 + console.warn(` โš  ${jsonPath}: Not a valid lexicon JSON`); 84 + return; 85 + } 86 + 87 + const { id } = lexiconJSON as LexiconJSON; 88 + const exportName = lexiconIdToExportName(id); 89 + 90 + // Generate TypeScript content 91 + const tsContent = `import { fromJSON } from "prototypey"; 92 + 93 + export const ${exportName} = fromJSON(${JSON.stringify(lexiconJSON, null, "\t")}); 94 + `; 95 + 96 + // Determine output path - use same structure but in outdir 97 + const outputFileName = `${basename(jsonPath, ".json")}.ts`; 98 + const outputPath = join(outdir, outputFileName); 99 + 100 + // Ensure output directory exists 101 + await mkdir(dirname(outputPath), { recursive: true }); 102 + 103 + // Write the TypeScript file 104 + await writeFile(outputPath, tsContent, "utf-8"); 105 + 106 + console.log(` โœ“ ${id} -> ${outputFileName}`); 107 + } catch (error) { 108 + console.error(` โœ— Error processing ${jsonPath}:`, error); 109 + throw error; 110 + } 111 + }
+24
packages/prototypey/cli/main.ts
··· 1 + #!/usr/bin/env node 2 + 3 + import sade from "sade"; 4 + import { genEmit } from "./gen-emit.ts"; 5 + import { genFromJSON } from "./gen-from-json.ts"; 6 + import pkg from "../package.json" with { type: "json" }; 7 + 8 + const prog = sade("prototypey"); 9 + 10 + prog.version(pkg.version).describe("atproto lexicon typescript toolkit"); 11 + 12 + prog 13 + .command("gen-emit <outdir> <sources...>") 14 + .describe("Emit JSON lexicon schemas from authored TypeScript") 15 + .example("gen-emit ./lexicons ./src/lexicons/**/*.ts") 16 + .action(genEmit); 17 + 18 + prog 19 + .command("gen-from-json <outdir> <sources...>") 20 + .describe("Generate TypeScript files from JSON lexicon schemas") 21 + .example("gen-from-json ./src/lexicons ./lexicons/**/*.json") 22 + .action(genFromJSON); 23 + 24 + prog.parse(process.argv);
+34
packages/prototypey/cli/tests/cli.test.ts
··· 1 + import { expect, test, describe } from "vitest"; 2 + import { runCLI } from "./test-utils.ts"; 3 + import pkg from "../../package.json" with { type: "json" }; 4 + 5 + describe("CLI Integration", () => { 6 + test("shows error when called without arguments", async () => { 7 + const { stdout, stderr, code } = await runCLI(); 8 + expect(code).toBe(1); 9 + expect(stderr).toContain("No command specified"); 10 + expect(stderr).toContain("Run `$ prototypey --help` for more info"); 11 + }); 12 + 13 + test("shows version", async () => { 14 + const { stdout, stderr } = await runCLI(["--version"]); 15 + expect(stderr).toBe(""); 16 + expect(stdout).toContain(`prototypey, ${pkg.version}`); 17 + }); 18 + 19 + test("shows help for gen-emit command", async () => { 20 + const { stdout, stderr } = await runCLI(["gen-emit", "--help"]); 21 + expect(stderr).toBe(""); 22 + expect(stdout).toContain("gen-emit <outdir> <sources...>"); 23 + expect(stdout).toContain( 24 + "Emit JSON lexicon schemas from authored TypeScript", 25 + ); 26 + }); 27 + 28 + test("handles unknown command", async () => { 29 + const { stdout, stderr, code } = await runCLI(["unknown-command"]); 30 + expect(code).toBe(1); 31 + expect(stderr).toContain("Invalid command: unknown-command"); 32 + expect(stderr).toContain("Run `$ prototypey --help` for more info"); 33 + }); 34 + });
+54
packages/prototypey/cli/tests/error-handling.test.ts
··· 1 + import { expect, test, describe, beforeEach, afterEach } from "vitest"; 2 + import { mkdir, writeFile, rm } from "node:fs/promises"; 3 + import { join } from "node:path"; 4 + import { tmpdir } from "node:os"; 5 + import { runCLI } from "./test-utils.ts"; 6 + 7 + describe("CLI Error Handling", () => { 8 + let testDir: string; 9 + let outDir: string; 10 + let schemasDir: string; 11 + 12 + beforeEach(async () => { 13 + // Create a temporary directory for test files 14 + testDir = join(tmpdir(), `prototypey-error-test-${String(Date.now())}`); 15 + outDir = join(testDir, "output"); 16 + schemasDir = join(testDir, "schemas"); 17 + await mkdir(testDir, { recursive: true }); 18 + await mkdir(outDir, { recursive: true }); 19 + await mkdir(schemasDir, { recursive: true }); 20 + }); 21 + 22 + afterEach(async () => { 23 + // Clean up test directory 24 + await rm(testDir, { recursive: true, force: true }); 25 + }); 26 + 27 + test("handles non-existent source files for gen-emit", async () => { 28 + const { stdout, stderr, code } = await runCLI([ 29 + "gen-emit", 30 + outDir, 31 + join(schemasDir, "non-existent.ts"), 32 + ]); 33 + 34 + expect(code).toBe(0); // Should not crash 35 + expect(stdout).toContain("No source files found matching patterns"); 36 + expect(stderr).toBe(""); 37 + }); 38 + 39 + test("handles valid TypeScript files with no lexicon exports for gen-emit", async () => { 40 + // Create a valid TypeScript file with no lexicon exports 41 + const validSource = join(schemasDir, "no-namespace.ts"); 42 + await writeFile(validSource, "export const x = 1;"); 43 + 44 + const { stdout, stderr, code } = await runCLI([ 45 + "gen-emit", 46 + outDir, 47 + validSource, 48 + ]); 49 + 50 + expect(code).toBe(0); // Should not crash 51 + expect(stdout).toContain("Found 1 source file(s)"); 52 + expect(stderr).toContain("No lexicons found"); 53 + }); 54 + });
+526
packages/prototypey/cli/tests/gen-emit.test.ts
··· 1 + import { expect, test, describe, beforeEach, afterEach } from "vitest"; 2 + import { mkdir, writeFile, rm, readFile } from "node:fs/promises"; 3 + import { join } from "node:path"; 4 + 5 + import { tmpdir } from "node:os"; 6 + import { genEmit } from "../gen-emit.ts"; 7 + 8 + describe("genEmit", () => { 9 + let testDir: string; 10 + let outDir: string; 11 + 12 + beforeEach(async () => { 13 + // Create a temporary directory for test files 14 + testDir = join(tmpdir(), `prototypey-test-${String(Date.now())}`); 15 + outDir = join(testDir, "output"); 16 + await mkdir(testDir, { recursive: true }); 17 + await mkdir(outDir, { recursive: true }); 18 + }); 19 + 20 + afterEach(async () => { 21 + // Clean up test directory 22 + await rm(testDir, { recursive: true, force: true }); 23 + }); 24 + 25 + test("emits JSON from a simple lexicon file", async () => { 26 + // Create a test lexicon file 27 + const lexiconFile = join(testDir, "profile.ts"); 28 + await writeFile( 29 + lexiconFile, 30 + ` 31 + import { lx } from "prototypey"; 32 + 33 + export const profileNamespace = lx.lexicon("app.bsky.actor.profile", { 34 + main: lx.record({ 35 + key: "self", 36 + record: lx.object({ 37 + displayName: lx.string({ maxLength: 64, maxGraphemes: 64 }), 38 + description: lx.string({ maxLength: 256, maxGraphemes: 256 }), 39 + }), 40 + }), 41 + }); 42 + `, 43 + ); 44 + 45 + // Run the emit command 46 + await genEmit(outDir, lexiconFile); 47 + 48 + // Read the emitted JSON file 49 + const outputFile = join(outDir, "app.bsky.actor.profile.json"); 50 + const content = await readFile(outputFile, "utf-8"); 51 + const json = JSON.parse(content); 52 + 53 + // Verify the structure 54 + expect(json).toEqual({ 55 + lexicon: 1, 56 + id: "app.bsky.actor.profile", 57 + defs: { 58 + main: { 59 + type: "record", 60 + key: "self", 61 + record: { 62 + type: "object", 63 + properties: { 64 + displayName: { 65 + type: "string", 66 + maxLength: 64, 67 + maxGraphemes: 64, 68 + }, 69 + description: { 70 + type: "string", 71 + maxLength: 256, 72 + maxGraphemes: 256, 73 + }, 74 + }, 75 + }, 76 + }, 77 + }, 78 + }); 79 + }); 80 + 81 + test("emits JSON from multiple lexicon exports in one file", async () => { 82 + // Create a test file with multiple exports 83 + const lexiconFile = join(testDir, "multiple.ts"); 84 + await writeFile( 85 + lexiconFile, 86 + ` 87 + import { lx } from "prototypey"; 88 + 89 + export const profile = lx.lexicon("app.bsky.actor.profile", { 90 + main: lx.record({ 91 + key: "self", 92 + record: lx.object({ 93 + displayName: lx.string({ maxLength: 64 }), 94 + }), 95 + }), 96 + }); 97 + 98 + export const post = lx.lexicon("app.bsky.feed.post", { 99 + main: lx.record({ 100 + key: "tid", 101 + record: lx.object({ 102 + text: lx.string({ maxLength: 300 }), 103 + }), 104 + }), 105 + }); 106 + `, 107 + ); 108 + 109 + // Run the emit command 110 + await genEmit(outDir, lexiconFile); 111 + 112 + // Verify both files were created 113 + const profileJson = JSON.parse( 114 + await readFile(join(outDir, "app.bsky.actor.profile.json"), "utf-8"), 115 + ); 116 + const postJson = JSON.parse( 117 + await readFile(join(outDir, "app.bsky.feed.post.json"), "utf-8"), 118 + ); 119 + 120 + expect(profileJson.id).toBe("app.bsky.actor.profile"); 121 + expect(postJson.id).toBe("app.bsky.feed.post"); 122 + }); 123 + 124 + test("handles glob patterns for multiple files", async () => { 125 + // Create multiple test files 126 + const lexicons = join(testDir, "lexicons"); 127 + await mkdir(lexicons, { recursive: true }); 128 + 129 + await writeFile( 130 + join(lexicons, "profile.ts"), 131 + ` 132 + import { lx } from "prototypey"; 133 + export const schema = lx.lexicon("app.bsky.actor.profile", { 134 + main: lx.record({ key: "self", record: lx.object({}) }), 135 + }); 136 + `, 137 + ); 138 + 139 + await writeFile( 140 + join(lexicons, "post.ts"), 141 + ` 142 + import { lx } from "prototypey"; 143 + export const schema = lx.lexicon("app.bsky.feed.post", { 144 + main: lx.record({ key: "tid", record: lx.object({}) }), 145 + }); 146 + `, 147 + ); 148 + 149 + // Run with glob pattern 150 + await genEmit(outDir, `${lexicons}/*.ts`); 151 + 152 + // Verify both files were created 153 + const profileExists = await readFile( 154 + join(outDir, "app.bsky.actor.profile.json"), 155 + "utf-8", 156 + ); 157 + const postExists = await readFile( 158 + join(outDir, "app.bsky.feed.post.json"), 159 + "utf-8", 160 + ); 161 + 162 + expect(profileExists).toBeTruthy(); 163 + expect(postExists).toBeTruthy(); 164 + }); 165 + 166 + test("emits query endpoint with parameters and output", async () => { 167 + const lexiconFile = join(testDir, "search.ts"); 168 + await writeFile( 169 + lexiconFile, 170 + ` 171 + import { lx } from "prototypey"; 172 + 173 + export const searchPosts = lx.lexicon("app.bsky.feed.searchPosts", { 174 + main: lx.query({ 175 + description: "Find posts matching search criteria", 176 + parameters: lx.params({ 177 + q: lx.string({ required: true }), 178 + limit: lx.integer({ minimum: 1, maximum: 100, default: 25 }), 179 + cursor: lx.string(), 180 + }), 181 + output: { 182 + encoding: "application/json", 183 + schema: lx.object({ 184 + cursor: lx.string(), 185 + posts: lx.array(lx.ref("app.bsky.feed.defs#postView"), { required: true }), 186 + }), 187 + }, 188 + }), 189 + }); 190 + `, 191 + ); 192 + 193 + await genEmit(outDir, lexiconFile); 194 + 195 + const outputFile = join(outDir, "app.bsky.feed.searchPosts.json"); 196 + const content = await readFile(outputFile, "utf-8"); 197 + const json = JSON.parse(content); 198 + 199 + expect(json).toEqual({ 200 + lexicon: 1, 201 + id: "app.bsky.feed.searchPosts", 202 + defs: { 203 + main: { 204 + type: "query", 205 + description: "Find posts matching search criteria", 206 + parameters: { 207 + type: "params", 208 + properties: { 209 + q: { type: "string", required: true }, 210 + limit: { type: "integer", minimum: 1, maximum: 100, default: 25 }, 211 + cursor: { type: "string" }, 212 + }, 213 + required: ["q"], 214 + }, 215 + output: { 216 + encoding: "application/json", 217 + schema: { 218 + type: "object", 219 + properties: { 220 + cursor: { type: "string" }, 221 + posts: { 222 + type: "array", 223 + items: { type: "ref", ref: "app.bsky.feed.defs#postView" }, 224 + required: true, 225 + }, 226 + }, 227 + required: ["posts"], 228 + }, 229 + }, 230 + }, 231 + }, 232 + }); 233 + }); 234 + 235 + test("emits procedure endpoint with input and output", async () => { 236 + const lexiconFile = join(testDir, "create-post.ts"); 237 + await writeFile( 238 + lexiconFile, 239 + ` 240 + import { lx } from "prototypey"; 241 + 242 + export const createPost = lx.lexicon("com.atproto.repo.createRecord", { 243 + main: lx.procedure({ 244 + description: "Create a record", 245 + input: { 246 + encoding: "application/json", 247 + schema: lx.object({ 248 + repo: lx.string({ required: true }), 249 + collection: lx.string({ required: true }), 250 + record: lx.unknown({ required: true }), 251 + }), 252 + }, 253 + output: { 254 + encoding: "application/json", 255 + schema: lx.object({ 256 + uri: lx.string({ required: true }), 257 + cid: lx.string({ required: true }), 258 + }), 259 + }, 260 + }), 261 + }); 262 + `, 263 + ); 264 + 265 + await genEmit(outDir, lexiconFile); 266 + 267 + const outputFile = join(outDir, "com.atproto.repo.createRecord.json"); 268 + const content = await readFile(outputFile, "utf-8"); 269 + const json = JSON.parse(content); 270 + 271 + expect(json).toEqual({ 272 + lexicon: 1, 273 + id: "com.atproto.repo.createRecord", 274 + defs: { 275 + main: { 276 + type: "procedure", 277 + description: "Create a record", 278 + input: { 279 + encoding: "application/json", 280 + schema: { 281 + type: "object", 282 + properties: { 283 + repo: { type: "string", required: true }, 284 + collection: { type: "string", required: true }, 285 + record: { type: "unknown", required: true }, 286 + }, 287 + required: ["repo", "collection", "record"], 288 + }, 289 + }, 290 + output: { 291 + encoding: "application/json", 292 + schema: { 293 + type: "object", 294 + properties: { 295 + uri: { type: "string", required: true }, 296 + cid: { type: "string", required: true }, 297 + }, 298 + required: ["uri", "cid"], 299 + }, 300 + }, 301 + }, 302 + }, 303 + }); 304 + }); 305 + 306 + test("emits subscription endpoint with message union", async () => { 307 + const lexiconFile = join(testDir, "subscription.ts"); 308 + await writeFile( 309 + lexiconFile, 310 + ` 311 + import { lx } from "prototypey"; 312 + 313 + export const subscribeRepos = lx.lexicon("com.atproto.sync.subscribeRepos", { 314 + main: lx.subscription({ 315 + description: "Repository event stream", 316 + parameters: lx.params({ 317 + cursor: lx.integer(), 318 + }), 319 + message: { 320 + schema: lx.union(["#commit", "#identity", "#account"]), 321 + }, 322 + }), 323 + commit: lx.object({ 324 + seq: lx.integer({ required: true }), 325 + rebase: lx.boolean({ required: true }), 326 + }), 327 + identity: lx.object({ 328 + seq: lx.integer({ required: true }), 329 + did: lx.string({ required: true, format: "did" }), 330 + }), 331 + account: lx.object({ 332 + seq: lx.integer({ required: true }), 333 + active: lx.boolean({ required: true }), 334 + }), 335 + }); 336 + `, 337 + ); 338 + 339 + await genEmit(outDir, lexiconFile); 340 + 341 + const outputFile = join(outDir, "com.atproto.sync.subscribeRepos.json"); 342 + const content = await readFile(outputFile, "utf-8"); 343 + const json = JSON.parse(content); 344 + 345 + expect(json).toEqual({ 346 + lexicon: 1, 347 + id: "com.atproto.sync.subscribeRepos", 348 + defs: { 349 + main: { 350 + type: "subscription", 351 + description: "Repository event stream", 352 + parameters: { 353 + type: "params", 354 + properties: { 355 + cursor: { type: "integer" }, 356 + }, 357 + }, 358 + message: { 359 + schema: { 360 + type: "union", 361 + refs: ["#commit", "#identity", "#account"], 362 + }, 363 + }, 364 + }, 365 + commit: { 366 + type: "object", 367 + properties: { 368 + seq: { type: "integer", required: true }, 369 + rebase: { type: "boolean", required: true }, 370 + }, 371 + required: ["seq", "rebase"], 372 + }, 373 + identity: { 374 + type: "object", 375 + properties: { 376 + seq: { type: "integer", required: true }, 377 + did: { type: "string", format: "did", required: true }, 378 + }, 379 + required: ["seq", "did"], 380 + }, 381 + account: { 382 + type: "object", 383 + properties: { 384 + seq: { type: "integer", required: true }, 385 + active: { type: "boolean", required: true }, 386 + }, 387 + required: ["seq", "active"], 388 + }, 389 + }, 390 + }); 391 + }); 392 + 393 + test("emits complex namespace with tokens, refs, and unions", async () => { 394 + const lexiconFile = join(testDir, "complex.ts"); 395 + await writeFile( 396 + lexiconFile, 397 + ` 398 + import { lx } from "prototypey"; 399 + 400 + export const feedDefs = lx.lexicon("app.bsky.feed.defs", { 401 + postView: lx.object({ 402 + uri: lx.string({ required: true, format: "at-uri" }), 403 + cid: lx.string({ required: true, format: "cid" }), 404 + author: lx.ref("app.bsky.actor.defs#profileViewBasic", { required: true }), 405 + embed: lx.union([ 406 + "app.bsky.embed.images#view", 407 + "app.bsky.embed.video#view", 408 + ]), 409 + likeCount: lx.integer({ minimum: 0 }), 410 + }), 411 + requestLess: lx.token("Request less content like this"), 412 + requestMore: lx.token("Request more content like this"), 413 + }); 414 + `, 415 + ); 416 + 417 + await genEmit(outDir, lexiconFile); 418 + 419 + const outputFile = join(outDir, "app.bsky.feed.defs.json"); 420 + const content = await readFile(outputFile, "utf-8"); 421 + const json = JSON.parse(content); 422 + 423 + expect(json).toEqual({ 424 + lexicon: 1, 425 + id: "app.bsky.feed.defs", 426 + defs: { 427 + postView: { 428 + type: "object", 429 + properties: { 430 + uri: { type: "string", format: "at-uri", required: true }, 431 + cid: { type: "string", format: "cid", required: true }, 432 + author: { 433 + type: "ref", 434 + ref: "app.bsky.actor.defs#profileViewBasic", 435 + required: true, 436 + }, 437 + embed: { 438 + type: "union", 439 + refs: ["app.bsky.embed.images#view", "app.bsky.embed.video#view"], 440 + }, 441 + likeCount: { type: "integer", minimum: 0 }, 442 + }, 443 + required: ["uri", "cid", "author"], 444 + }, 445 + requestLess: { 446 + type: "token", 447 + description: "Request less content like this", 448 + }, 449 + requestMore: { 450 + type: "token", 451 + description: "Request more content like this", 452 + }, 453 + }, 454 + }); 455 + }); 456 + 457 + test("emits lexicon with arrays, blobs, and string formats", async () => { 458 + const lexiconFile = join(testDir, "primitives.ts"); 459 + await writeFile( 460 + lexiconFile, 461 + ` 462 + import { lx } from "prototypey"; 463 + 464 + export const imagePost = lx.lexicon("app.example.imagePost", { 465 + main: lx.record({ 466 + key: "tid", 467 + record: lx.object({ 468 + text: lx.string({ maxLength: 300, maxGraphemes: 300, required: true }), 469 + createdAt: lx.string({ format: "datetime", required: true }), 470 + images: lx.array(lx.blob({ accept: ["image/png", "image/jpeg"], maxSize: 1000000 }), { maxLength: 4 }), 471 + tags: lx.array(lx.string({ maxLength: 64 })), 472 + langs: lx.array(lx.string()), 473 + }), 474 + }), 475 + }); 476 + `, 477 + ); 478 + 479 + await genEmit(outDir, lexiconFile); 480 + 481 + const outputFile = join(outDir, "app.example.imagePost.json"); 482 + const content = await readFile(outputFile, "utf-8"); 483 + const json = JSON.parse(content); 484 + 485 + expect(json).toEqual({ 486 + lexicon: 1, 487 + id: "app.example.imagePost", 488 + defs: { 489 + main: { 490 + type: "record", 491 + key: "tid", 492 + record: { 493 + type: "object", 494 + properties: { 495 + text: { 496 + type: "string", 497 + maxLength: 300, 498 + maxGraphemes: 300, 499 + required: true, 500 + }, 501 + createdAt: { type: "string", format: "datetime", required: true }, 502 + images: { 503 + type: "array", 504 + items: { 505 + type: "blob", 506 + accept: ["image/png", "image/jpeg"], 507 + maxSize: 1000000, 508 + }, 509 + maxLength: 4, 510 + }, 511 + tags: { 512 + type: "array", 513 + items: { type: "string", maxLength: 64 }, 514 + }, 515 + langs: { 516 + type: "array", 517 + items: { type: "string" }, 518 + }, 519 + }, 520 + required: ["text", "createdAt"], 521 + }, 522 + }, 523 + }, 524 + }); 525 + }); 526 + });
+434
packages/prototypey/cli/tests/gen-from-json.test.ts
··· 1 + import { expect, test, describe, beforeEach, afterEach } from "vitest"; 2 + import { mkdir, writeFile, rm, readFile } from "node:fs/promises"; 3 + import { join } from "node:path"; 4 + import { tmpdir } from "node:os"; 5 + import { genFromJSON } from "../gen-from-json.ts"; 6 + 7 + describe("genFromJSON", () => { 8 + let testDir: string; 9 + let outDir: string; 10 + 11 + beforeEach(async () => { 12 + // Create a temporary directory for test files 13 + testDir = join(tmpdir(), `prototypey-test-import-${String(Date.now())}`); 14 + outDir = join(testDir, "output"); 15 + await mkdir(testDir, { recursive: true }); 16 + await mkdir(outDir, { recursive: true }); 17 + }); 18 + 19 + afterEach(async () => { 20 + // Clean up test directory 21 + await rm(testDir, { recursive: true, force: true }); 22 + }); 23 + 24 + test("generates TypeScript from a simple JSON lexicon", async () => { 25 + // Create a test JSON lexicon file 26 + const jsonFile = join(testDir, "app.bsky.actor.profile.json"); 27 + const lexiconJSON = { 28 + lexicon: 1, 29 + id: "app.bsky.actor.profile", 30 + defs: { 31 + main: { 32 + type: "record", 33 + key: "self", 34 + record: { 35 + type: "object", 36 + properties: { 37 + displayName: { 38 + type: "string", 39 + maxLength: 64, 40 + maxGraphemes: 64, 41 + }, 42 + description: { 43 + type: "string", 44 + maxLength: 256, 45 + maxGraphemes: 256, 46 + }, 47 + }, 48 + }, 49 + }, 50 + }, 51 + }; 52 + 53 + await writeFile(jsonFile, JSON.stringify(lexiconJSON, null, 2)); 54 + 55 + // Run the from-json command 56 + await genFromJSON(outDir, jsonFile); 57 + 58 + // Read the generated TypeScript file 59 + const outputFile = join(outDir, "app.bsky.actor.profile.ts"); 60 + const content = await readFile(outputFile, "utf-8"); 61 + 62 + // Verify the structure 63 + expect(content).toContain('import { fromJSON } from "prototypey"'); 64 + expect(content).toContain("export const appBskyActorProfile = fromJSON("); 65 + expect(content).toContain('"id": "app.bsky.actor.profile"'); 66 + expect(content).toContain('"lexicon": 1'); 67 + 68 + // Verify it can be parsed as JSON within the call 69 + const jsonMatch = content.match(/fromJSON\(([\s\S]+)\);/); 70 + expect(jsonMatch).toBeTruthy(); 71 + if (jsonMatch) { 72 + const parsedJSON = JSON.parse(jsonMatch[1]); 73 + expect(parsedJSON).toEqual(lexiconJSON); 74 + } 75 + }); 76 + 77 + test("handles multiple JSON files with glob pattern", async () => { 78 + // Create multiple test JSON files 79 + const lexicons = join(testDir, "lexicons"); 80 + await mkdir(lexicons, { recursive: true }); 81 + 82 + const profileJSON = { 83 + lexicon: 1, 84 + id: "app.bsky.actor.profile", 85 + defs: { 86 + main: { 87 + type: "record", 88 + key: "self", 89 + record: { type: "object", properties: {} }, 90 + }, 91 + }, 92 + }; 93 + 94 + const postJSON = { 95 + lexicon: 1, 96 + id: "app.bsky.feed.post", 97 + defs: { 98 + main: { 99 + type: "record", 100 + key: "tid", 101 + record: { type: "object", properties: {} }, 102 + }, 103 + }, 104 + }; 105 + 106 + await writeFile( 107 + join(lexicons, "app.bsky.actor.profile.json"), 108 + JSON.stringify(profileJSON, null, 2), 109 + ); 110 + await writeFile( 111 + join(lexicons, "app.bsky.feed.post.json"), 112 + JSON.stringify(postJSON, null, 2), 113 + ); 114 + 115 + // Run with glob pattern 116 + await genFromJSON(outDir, `${lexicons}/*.json`); 117 + 118 + // Verify both files were created 119 + const profileTS = await readFile( 120 + join(outDir, "app.bsky.actor.profile.ts"), 121 + "utf-8", 122 + ); 123 + const postTS = await readFile( 124 + join(outDir, "app.bsky.feed.post.ts"), 125 + "utf-8", 126 + ); 127 + 128 + expect(profileTS).toContain("appBskyActorProfile"); 129 + expect(postTS).toContain("appBskyFeedPost"); 130 + }); 131 + 132 + test("generates correct export names from lexicon IDs", async () => { 133 + const testCases = [ 134 + { id: "app.bsky.feed.post", expectedName: "appBskyFeedPost" }, 135 + { 136 + id: "com.atproto.repo.createRecord", 137 + expectedName: "comAtprotoRepoCreateRecord", 138 + }, 139 + { id: "app.bsky.actor.profile", expectedName: "appBskyActorProfile" }, 140 + { id: "simple", expectedName: "simple" }, 141 + ]; 142 + 143 + for (const { id, expectedName } of testCases) { 144 + const jsonFile = join(testDir, `${id}.json`); 145 + const lexiconJSON = { 146 + lexicon: 1, 147 + id, 148 + defs: { main: { type: "object", properties: {} } }, 149 + }; 150 + 151 + await writeFile(jsonFile, JSON.stringify(lexiconJSON, null, 2)); 152 + await genFromJSON(outDir, jsonFile); 153 + 154 + const outputFile = join(outDir, `${id}.ts`); 155 + const content = await readFile(outputFile, "utf-8"); 156 + 157 + expect(content).toContain(`export const ${expectedName} = fromJSON(`); 158 + } 159 + }); 160 + 161 + test("generates TypeScript from query endpoint JSON", async () => { 162 + const jsonFile = join(testDir, "app.bsky.feed.searchPosts.json"); 163 + const lexiconJSON = { 164 + lexicon: 1, 165 + id: "app.bsky.feed.searchPosts", 166 + defs: { 167 + main: { 168 + type: "query", 169 + description: "Find posts matching search criteria", 170 + parameters: { 171 + type: "params", 172 + properties: { 173 + q: { type: "string", required: true }, 174 + limit: { type: "integer", minimum: 1, maximum: 100, default: 25 }, 175 + cursor: { type: "string" }, 176 + }, 177 + required: ["q"], 178 + }, 179 + output: { 180 + encoding: "application/json", 181 + schema: { 182 + type: "object", 183 + properties: { 184 + cursor: { type: "string" }, 185 + posts: { 186 + type: "array", 187 + items: { type: "ref", ref: "app.bsky.feed.defs#postView" }, 188 + required: true, 189 + }, 190 + }, 191 + required: ["posts"], 192 + }, 193 + }, 194 + }, 195 + }, 196 + }; 197 + 198 + await writeFile(jsonFile, JSON.stringify(lexiconJSON, null, 2)); 199 + await genFromJSON(outDir, jsonFile); 200 + 201 + const outputFile = join(outDir, "app.bsky.feed.searchPosts.ts"); 202 + const content = await readFile(outputFile, "utf-8"); 203 + 204 + expect(content).toContain("appBskyFeedSearchPosts"); 205 + expect(content).toContain('"type": "query"'); 206 + expect(content).toContain("Find posts matching search criteria"); 207 + }); 208 + 209 + test("generates TypeScript from procedure endpoint JSON", async () => { 210 + const jsonFile = join(testDir, "com.atproto.repo.createRecord.json"); 211 + const lexiconJSON = { 212 + lexicon: 1, 213 + id: "com.atproto.repo.createRecord", 214 + defs: { 215 + main: { 216 + type: "procedure", 217 + description: "Create a record", 218 + input: { 219 + encoding: "application/json", 220 + schema: { 221 + type: "object", 222 + properties: { 223 + repo: { type: "string", required: true }, 224 + collection: { type: "string", required: true }, 225 + record: { type: "unknown", required: true }, 226 + }, 227 + required: ["repo", "collection", "record"], 228 + }, 229 + }, 230 + output: { 231 + encoding: "application/json", 232 + schema: { 233 + type: "object", 234 + properties: { 235 + uri: { type: "string", required: true }, 236 + cid: { type: "string", required: true }, 237 + }, 238 + required: ["uri", "cid"], 239 + }, 240 + }, 241 + }, 242 + }, 243 + }; 244 + 245 + await writeFile(jsonFile, JSON.stringify(lexiconJSON, null, 2)); 246 + await genFromJSON(outDir, jsonFile); 247 + 248 + const outputFile = join(outDir, "com.atproto.repo.createRecord.ts"); 249 + const content = await readFile(outputFile, "utf-8"); 250 + 251 + expect(content).toContain("comAtprotoRepoCreateRecord"); 252 + expect(content).toContain('"type": "procedure"'); 253 + }); 254 + 255 + test("generates TypeScript from subscription endpoint JSON", async () => { 256 + const jsonFile = join(testDir, "com.atproto.sync.subscribeRepos.json"); 257 + const lexiconJSON = { 258 + lexicon: 1, 259 + id: "com.atproto.sync.subscribeRepos", 260 + defs: { 261 + main: { 262 + type: "subscription", 263 + description: "Repository event stream", 264 + parameters: { 265 + type: "params", 266 + properties: { 267 + cursor: { type: "integer" }, 268 + }, 269 + }, 270 + message: { 271 + schema: { 272 + type: "union", 273 + refs: ["#commit", "#identity", "#account"], 274 + }, 275 + }, 276 + }, 277 + commit: { 278 + type: "object", 279 + properties: { 280 + seq: { type: "integer", required: true }, 281 + rebase: { type: "boolean", required: true }, 282 + }, 283 + required: ["seq", "rebase"], 284 + }, 285 + identity: { 286 + type: "object", 287 + properties: { 288 + seq: { type: "integer", required: true }, 289 + did: { type: "string", format: "did", required: true }, 290 + }, 291 + required: ["seq", "did"], 292 + }, 293 + account: { 294 + type: "object", 295 + properties: { 296 + seq: { type: "integer", required: true }, 297 + active: { type: "boolean", required: true }, 298 + }, 299 + required: ["seq", "active"], 300 + }, 301 + }, 302 + }; 303 + 304 + await writeFile(jsonFile, JSON.stringify(lexiconJSON, null, 2)); 305 + await genFromJSON(outDir, jsonFile); 306 + 307 + const outputFile = join(outDir, "com.atproto.sync.subscribeRepos.ts"); 308 + const content = await readFile(outputFile, "utf-8"); 309 + 310 + expect(content).toContain("comAtprotoSyncSubscribeRepos"); 311 + expect(content).toContain('"type": "subscription"'); 312 + expect(content).toContain("commit"); 313 + expect(content).toContain("identity"); 314 + expect(content).toContain("account"); 315 + }); 316 + 317 + test("generates TypeScript from complex namespace with refs and unions", async () => { 318 + const jsonFile = join(testDir, "app.bsky.feed.defs.json"); 319 + const lexiconJSON = { 320 + lexicon: 1, 321 + id: "app.bsky.feed.defs", 322 + defs: { 323 + postView: { 324 + type: "object", 325 + properties: { 326 + uri: { type: "string", format: "at-uri", required: true }, 327 + cid: { type: "string", format: "cid", required: true }, 328 + author: { 329 + type: "ref", 330 + ref: "app.bsky.actor.defs#profileViewBasic", 331 + required: true, 332 + }, 333 + embed: { 334 + type: "union", 335 + refs: ["app.bsky.embed.images#view", "app.bsky.embed.video#view"], 336 + }, 337 + likeCount: { type: "integer", minimum: 0 }, 338 + }, 339 + required: ["uri", "cid", "author"], 340 + }, 341 + requestLess: { 342 + type: "token", 343 + description: "Request less content like this", 344 + }, 345 + requestMore: { 346 + type: "token", 347 + description: "Request more content like this", 348 + }, 349 + }, 350 + }; 351 + 352 + await writeFile(jsonFile, JSON.stringify(lexiconJSON, null, 2)); 353 + await genFromJSON(outDir, jsonFile); 354 + 355 + const outputFile = join(outDir, "app.bsky.feed.defs.ts"); 356 + const content = await readFile(outputFile, "utf-8"); 357 + 358 + expect(content).toContain("appBskyFeedDefs"); 359 + expect(content).toContain("postView"); 360 + expect(content).toContain("requestLess"); 361 + expect(content).toContain("requestMore"); 362 + }); 363 + 364 + test("handles invalid JSON gracefully", async () => { 365 + const jsonFile = join(testDir, "invalid.json"); 366 + await writeFile(jsonFile, "{ this is not valid json }"); 367 + 368 + await expect(genFromJSON(outDir, jsonFile)).rejects.toThrow(); 369 + }); 370 + 371 + test("skips non-lexicon JSON files", async () => { 372 + const jsonFile = join(testDir, "not-a-lexicon.json"); 373 + await writeFile( 374 + jsonFile, 375 + JSON.stringify({ someKey: "someValue" }, null, 2), 376 + ); 377 + 378 + // Should not throw, just warn and skip 379 + await genFromJSON(outDir, jsonFile); 380 + 381 + // Verify no output file was created 382 + const outputFiles = await readdir(outDir).catch(() => []); 383 + expect(outputFiles.length).toBe(0); 384 + }); 385 + 386 + test("round-trip: gen-emit then gen-from-json produces equivalent types", async () => { 387 + // This is an integration test that verifies the round-trip works 388 + const intermediateDir = join(testDir, "json"); 389 + await mkdir(intermediateDir, { recursive: true }); 390 + 391 + // First, create a simple TypeScript lexicon 392 + const tsFile = join(testDir, "original.ts"); 393 + await writeFile( 394 + tsFile, 395 + ` 396 + import { lx } from "prototypey"; 397 + 398 + export const postSchema = lx.lexicon("app.bsky.feed.post", { 399 + main: lx.record({ 400 + key: "tid", 401 + record: lx.object({ 402 + text: lx.string({ maxLength: 300, required: true }), 403 + createdAt: lx.string({ format: "datetime", required: true }), 404 + }), 405 + }), 406 + }); 407 + `, 408 + ); 409 + 410 + // Import gen-emit dynamically to use it 411 + const { genEmit } = await import("../gen-emit.ts"); 412 + 413 + // Step 1: gen-emit to create JSON 414 + await genEmit(intermediateDir, tsFile); 415 + 416 + // Step 2: gen-from-json to create TypeScript from JSON 417 + const jsonFile = join(intermediateDir, "app.bsky.feed.post.json"); 418 + await genFromJSON(outDir, jsonFile); 419 + 420 + // Verify the output exists 421 + const outputFile = join(outDir, "app.bsky.feed.post.ts"); 422 + const content = await readFile(outputFile, "utf-8"); 423 + 424 + expect(content).toContain("appBskyFeedPost"); 425 + expect(content).toContain('"id": "app.bsky.feed.post"'); 426 + expect(content).toContain('"type": "record"'); 427 + }); 428 + }); 429 + 430 + // Helper function that was missing from imports 431 + async function readdir(path: string): Promise<string[]> { 432 + const { readdir: fsReaddir } = await import("node:fs/promises"); 433 + return fsReaddir(path); 434 + }
+35
packages/prototypey/cli/tests/test-utils.ts
··· 1 + import { spawn } from "node:child_process"; 2 + import { fileURLToPath } from "node:url"; 3 + 4 + const cliPath = fileURLToPath(new URL("../main.ts", import.meta.url)); 5 + 6 + export function runCLI( 7 + args: string[] = [], 8 + options?: { cwd?: string; env?: NodeJS.ProcessEnv }, 9 + ): Promise<{ stdout: string; stderr: string; code: number }> { 10 + return new Promise((resolve) => { 11 + const child = spawn( 12 + "node", 13 + ["--experimental-strip-types", cliPath, ...args], 14 + { 15 + cwd: options?.cwd ?? process.cwd(), 16 + env: options?.env ?? process.env, 17 + }, 18 + ); 19 + 20 + let stdout = ""; 21 + let stderr = ""; 22 + 23 + child.stdout.on("data", (data) => { 24 + stdout += data.toString(); 25 + }); 26 + 27 + child.stderr.on("data", (data) => { 28 + stderr += data.toString(); 29 + }); 30 + 31 + child.on("close", (code) => { 32 + resolve({ stdout, stderr, code: code ?? 0 }); 33 + }); 34 + }); 35 + }
+103
packages/prototypey/cli/tests/workflow.test.ts
··· 1 + import { test, describe, beforeEach, afterEach } from "vitest"; 2 + import { mkdir, writeFile, rm } from "node:fs/promises"; 3 + import { join } from "node:path"; 4 + import { tmpdir } from "node:os"; 5 + 6 + describe("CLI End-to-End Workflow", () => { 7 + let testDir: string; 8 + let schemasDir: string; 9 + let generatedDir: string; 10 + 11 + beforeEach(async () => { 12 + // Create a temporary directory for test files 13 + testDir = join(tmpdir(), `prototypey-e2e-test-${String(Date.now())}`); 14 + schemasDir = join(testDir, "schemas"); 15 + generatedDir = join(testDir, "generated"); 16 + await mkdir(testDir, { recursive: true }); 17 + await mkdir(schemasDir, { recursive: true }); 18 + await mkdir(generatedDir, { recursive: true }); 19 + }); 20 + 21 + afterEach(async () => { 22 + // Clean up test directory 23 + await rm(testDir, { recursive: true, force: true }); 24 + }); 25 + 26 + test("workflow with multiple schemas", async () => { 27 + // Create multiple JSON schema files 28 + const postSchema = join(schemasDir, "app.test.post.json"); 29 + await writeFile( 30 + postSchema, 31 + JSON.stringify( 32 + { 33 + lexicon: 1, 34 + id: "app.test.post", 35 + defs: { 36 + main: { 37 + type: "record", 38 + key: "tid", 39 + record: { 40 + type: "object", 41 + properties: { 42 + text: { type: "string", maxLength: 300, required: true }, 43 + createdAt: { 44 + type: "string", 45 + format: "datetime", 46 + required: true, 47 + }, 48 + }, 49 + }, 50 + }, 51 + }, 52 + }, 53 + null, 54 + 2, 55 + ), 56 + ); 57 + 58 + const searchSchema = join(schemasDir, "app.test.searchPosts.json"); 59 + await writeFile( 60 + searchSchema, 61 + JSON.stringify( 62 + { 63 + lexicon: 1, 64 + id: "app.test.searchPosts", 65 + defs: { 66 + main: { 67 + type: "query", 68 + parameters: { 69 + type: "params", 70 + properties: { 71 + q: { type: "string", required: true }, 72 + limit: { 73 + type: "integer", 74 + minimum: 1, 75 + maximum: 100, 76 + default: 25, 77 + }, 78 + }, 79 + required: ["q"], 80 + }, 81 + output: { 82 + encoding: "application/json", 83 + schema: { 84 + type: "object", 85 + properties: { 86 + posts: { 87 + type: "array", 88 + items: { type: "ref", ref: "app.test.post#main" }, 89 + required: true, 90 + }, 91 + }, 92 + required: ["posts"], 93 + }, 94 + }, 95 + }, 96 + }, 97 + }, 98 + null, 99 + 2, 100 + ), 101 + ); 102 + }); 103 + });
+142
packages/prototypey/core/infer.ts
··· 1 + import { Prettify } from "./type-utils.ts"; 2 + 3 + /* eslint-disable @typescript-eslint/no-empty-object-type */ 4 + type InferType<T> = T extends { type: "record" } 5 + ? InferRecord<T> 6 + : T extends { type: "object" } 7 + ? InferObject<T> 8 + : T extends { type: "array" } 9 + ? InferArray<T> 10 + : T extends { type: "params" } 11 + ? InferParams<T> 12 + : T extends { type: "union" } 13 + ? InferUnion<T> 14 + : T extends { type: "token" } 15 + ? InferToken<T> 16 + : T extends { type: "ref" } 17 + ? InferRef<T> 18 + : T extends { type: "unknown" } 19 + ? unknown 20 + : T extends { type: "null" } 21 + ? null 22 + : T extends { type: "boolean" } 23 + ? boolean 24 + : T extends { type: "integer" } 25 + ? number 26 + : T extends { type: "string" } 27 + ? string 28 + : T extends { type: "bytes" } 29 + ? Uint8Array 30 + : T extends { type: "cid-link" } 31 + ? string 32 + : T extends { type: "blob" } 33 + ? Blob 34 + : never; 35 + 36 + type InferToken<T> = T extends { enum: readonly (infer U)[] } ? U : string; 37 + 38 + export type GetRequired<T> = T extends { required: readonly (infer R)[] } 39 + ? R 40 + : never; 41 + export type GetNullable<T> = T extends { nullable: readonly (infer N)[] } 42 + ? N 43 + : never; 44 + 45 + type InferObject< 46 + T, 47 + Nullable extends string = GetNullable<T> & string, 48 + Required extends string = GetRequired<T> & string, 49 + NullableAndRequired extends string = Required & Nullable & string, 50 + Normal extends string = "properties" extends keyof T 51 + ? Exclude<keyof T["properties"], Required | Nullable> & string 52 + : never, 53 + > = Prettify< 54 + T extends { properties: infer P } 55 + ? { 56 + -readonly [K in Normal]?: InferType<P[K & keyof P]>; 57 + } & { 58 + -readonly [K in Exclude<Required, NullableAndRequired>]-?: InferType< 59 + P[K & keyof P] 60 + >; 61 + } & { 62 + -readonly [K in Exclude<Nullable, NullableAndRequired>]?: InferType< 63 + P[K & keyof P] 64 + > | null; 65 + } & { 66 + -readonly [K in NullableAndRequired]: InferType<P[K & keyof P]> | null; 67 + } 68 + : {} 69 + >; 70 + 71 + type InferArray<T> = T extends { items: infer Items } 72 + ? InferType<Items>[] 73 + : never[]; 74 + 75 + type InferUnion<T> = T extends { refs: readonly (infer R)[] } 76 + ? R extends string 77 + ? { $type: R; [key: string]: unknown } 78 + : never 79 + : never; 80 + 81 + type InferRef<T> = T extends { ref: infer R } 82 + ? R extends string 83 + ? { $type: R; [key: string]: unknown } 84 + : unknown 85 + : unknown; 86 + 87 + type InferParams<T> = InferObject<T>; 88 + 89 + type InferRecord<T> = T extends { record: infer R } 90 + ? R extends { type: "object" } 91 + ? InferObject<R> 92 + : R extends { type: "union" } 93 + ? InferUnion<R> 94 + : unknown 95 + : unknown; 96 + 97 + /** 98 + * Recursively replaces stub references in a type with their actual definitions. 99 + * Detects circular references and missing references, returning string literal error messages. 100 + */ 101 + type ReplaceRefsInType<T, Defs, Visited = never> = 102 + // Check if this is a ref stub type (has $type starting with #) 103 + T extends { $type: `#${infer DefName}` } 104 + ? DefName extends keyof Defs 105 + ? // Check for circular reference 106 + DefName extends Visited 107 + ? `[Circular reference detected: #${DefName}]` 108 + : // Recursively resolve the ref and preserve the $type marker 109 + Prettify< 110 + ReplaceRefsInType<Defs[DefName], Defs, Visited | DefName> & { 111 + $type: T["$type"]; 112 + } 113 + > 114 + : // Reference not found in definitions 115 + `[Reference not found: #${DefName}]` 116 + : // Handle arrays (but not Uint8Array or other typed arrays) 117 + T extends Uint8Array | Blob 118 + ? T 119 + : T extends readonly (infer Item)[] 120 + ? ReplaceRefsInType<Item, Defs, Visited>[] 121 + : // Handle plain objects (exclude built-in types and functions) 122 + T extends object 123 + ? T extends (...args: unknown[]) => unknown 124 + ? T 125 + : { [K in keyof T]: ReplaceRefsInType<T[K], Defs, Visited> } 126 + : // Primitives pass through unchanged 127 + T; 128 + 129 + /** 130 + * Infers the TypeScript type for a lexicon namespace, returning only the 'main' definition 131 + * with all local refs (#user, #post, etc.) resolved to their actual types. 132 + */ 133 + export type Infer< 134 + T extends { json: { id: string; defs: Record<string, unknown> } }, 135 + > = Prettify< 136 + "main" extends keyof T["json"]["defs"] 137 + ? { $type: T["json"]["id"] } & ReplaceRefsInType< 138 + InferType<T["json"]["defs"]["main"]>, 139 + { [K in keyof T["json"]["defs"]]: InferType<T["json"]["defs"][K]> } 140 + > 141 + : never 142 + >;
+637
packages/prototypey/core/lib.ts
··· 1 + /* eslint-disable @typescript-eslint/no-empty-object-type */ 2 + import type { Infer } from "./infer.ts"; 3 + import type { UnionToTuple } from "./type-utils.ts"; 4 + import type { LexiconDoc, ValidationResult } from "@atproto/lexicon"; 5 + import { Lexicons } from "@atproto/lexicon"; 6 + 7 + /** @see https://atproto.com/specs/lexicon#overview-of-types */ 8 + type LexiconType = 9 + // Concrete types 10 + | "null" 11 + | "boolean" 12 + | "integer" 13 + | "string" 14 + | "bytes" 15 + | "cid-link" 16 + | "blob" 17 + // Container types 18 + | "array" 19 + | "object" 20 + | "params" 21 + // Meta types 22 + | "token" 23 + | "ref" 24 + | "union" 25 + | "unknown" 26 + // Primary types 27 + | "record" 28 + | "query" 29 + | "procedure" 30 + | "subscription"; 31 + 32 + /** 33 + * Common options available for lexicon items. 34 + * @see https://atproto.com/specs/lexicon#string-formats 35 + */ 36 + type LexiconItemCommonOptions = { 37 + /** Indicates this field must be provided */ 38 + required?: boolean; 39 + /** Indicates this field can be explicitly set to null */ 40 + nullable?: boolean; 41 + /** Human-readable description */ 42 + description?: string; 43 + }; 44 + 45 + /** 46 + * Base interface for all lexicon items. 47 + * @see https://atproto.com/specs/lexicon#overview-of-types 48 + */ 49 + type LexiconItem = LexiconItemCommonOptions & { 50 + type: LexiconType; 51 + }; 52 + 53 + /** 54 + * Definition in a lexicon namespace. 55 + * @see https://atproto.com/specs/lexicon#lexicon-document 56 + */ 57 + type Def = { 58 + type: LexiconType; 59 + }; 60 + 61 + /** 62 + * Lexicon namespace document structure. 63 + * @see https://atproto.com/specs/lexicon#lexicon-document 64 + */ 65 + type LexiconNamespace = { 66 + /** Namespaced identifier (NSID) for this lexicon */ 67 + id: string; 68 + /** Named definitions within this namespace */ 69 + defs: Record<string, Def>; 70 + }; 71 + 72 + /** 73 + * String type options. 74 + * @see https://atproto.com/specs/lexicon#string 75 + */ 76 + type StringOptions = LexiconItemCommonOptions & { 77 + /** 78 + * Semantic string format constraint. 79 + * @see https://atproto.com/specs/lexicon#string-formats 80 + */ 81 + format?: 82 + | "at-identifier" // Handle or DID 83 + | "at-uri" // AT Protocol URI 84 + | "cid" // Content Identifier 85 + | "datetime" // Timestamp (UTC, ISO 8601) 86 + | "did" // Decentralized Identifier 87 + | "handle" // User handle identifier 88 + | "nsid" // Namespaced Identifier 89 + | "tid" // Timestamp Identifier 90 + | "record-key" // Repository record key 91 + | "uri" // Generic URI 92 + | "language"; // IETF BCP 47 language tag 93 + /** Maximum string length in bytes */ 94 + maxLength?: number; 95 + /** Minimum string length in bytes */ 96 + minLength?: number; 97 + /** Maximum string length in Unicode graphemes */ 98 + maxGraphemes?: number; 99 + /** Minimum string length in Unicode graphemes */ 100 + minGraphemes?: number; 101 + /** Hints at expected values, not enforced */ 102 + knownValues?: string[]; 103 + /** Restricts to an exact set of string values */ 104 + enum?: string[]; 105 + /** Default value if not provided */ 106 + default?: string; 107 + /** Fixed, unchangeable value */ 108 + const?: string; 109 + }; 110 + 111 + /** 112 + * Boolean type options. 113 + * @see https://atproto.com/specs/lexicon#boolean 114 + */ 115 + type BooleanOptions = LexiconItemCommonOptions & { 116 + /** Default value if not provided */ 117 + default?: boolean; 118 + /** Fixed, unchangeable value */ 119 + const?: boolean; 120 + }; 121 + 122 + /** 123 + * Integer type options. 124 + * @see https://atproto.com/specs/lexicon#integer 125 + */ 126 + type IntegerOptions = LexiconItemCommonOptions & { 127 + /** Minimum allowed value (inclusive) */ 128 + minimum?: number; 129 + /** Maximum allowed value (inclusive) */ 130 + maximum?: number; 131 + /** Restricts to an exact set of integer values */ 132 + enum?: number[]; 133 + /** Default value if not provided */ 134 + default?: number; 135 + /** Fixed, unchangeable value */ 136 + const?: number; 137 + }; 138 + 139 + /** 140 + * Bytes type options for arbitrary byte arrays. 141 + * @see https://atproto.com/specs/lexicon#bytes 142 + */ 143 + type BytesOptions = LexiconItemCommonOptions & { 144 + /** Minimum byte array length */ 145 + minLength?: number; 146 + /** Maximum byte array length */ 147 + maxLength?: number; 148 + }; 149 + 150 + /** 151 + * Blob type options for binary data with MIME types. 152 + * @see https://atproto.com/specs/lexicon#blob 153 + */ 154 + type BlobOptions = LexiconItemCommonOptions & { 155 + /** Allowed MIME types (e.g., ["image/png", "image/jpeg"]) */ 156 + accept?: string[]; 157 + /** Maximum blob size in bytes */ 158 + maxSize?: number; 159 + }; 160 + 161 + /** 162 + * Array type options. 163 + * @see https://atproto.com/specs/lexicon#array 164 + */ 165 + type ArrayOptions = LexiconItemCommonOptions & { 166 + /** Minimum array length */ 167 + minLength?: number; 168 + /** Maximum array length */ 169 + maxLength?: number; 170 + }; 171 + 172 + /** 173 + * Record type options for repository records. 174 + * @see https://atproto.com/specs/lexicon#record 175 + */ 176 + type RecordOptions = { 177 + /** 178 + * Record key strategy: "self" for self-describing or "tid" for timestamp IDs 179 + * @see https://atproto.com/specs/record-key 180 + */ 181 + key: string; 182 + /** Object schema defining the record structure */ 183 + record: { type: "object" }; 184 + /** Human-readable description */ 185 + description?: string; 186 + }; 187 + 188 + /** 189 + * Union type options for multiple possible types. 190 + * @see https://atproto.com/specs/lexicon#union 191 + */ 192 + type UnionOptions = LexiconItemCommonOptions & { 193 + /** If true, only listed refs are allowed; if false, additional types may be added */ 194 + closed?: boolean; 195 + }; 196 + 197 + /** 198 + * Map of property names to their lexicon item definitions. 199 + * @see https://atproto.com/specs/lexicon#object 200 + */ 201 + type ObjectProperties = Record< 202 + string, 203 + { 204 + type: LexiconType; 205 + } 206 + >; 207 + 208 + /** 209 + * Object-level options (not property-level). 210 + * @see https://atproto.com/specs/lexicon#object 211 + */ 212 + type ObjectOptions = { 213 + /** Human-readable description of the object */ 214 + description?: string; 215 + }; 216 + 217 + type RequiredKeys<T> = { 218 + [K in keyof T]: T[K] extends { required: true } ? K : never; 219 + }[keyof T]; 220 + 221 + type NullableKeys<T> = { 222 + [K in keyof T]: T[K] extends { nullable: true } ? K : never; 223 + }[keyof T]; 224 + 225 + /** 226 + * Resulting object schema with required and nullable fields extracted. 227 + * @see https://atproto.com/specs/lexicon#object 228 + */ 229 + type ObjectResult<T extends ObjectProperties, O extends ObjectOptions = {}> = { 230 + type: "object"; 231 + /** Property definitions */ 232 + properties: { 233 + [K in keyof T]: T[K] extends { type: "object" } 234 + ? T[K] 235 + : Omit<T[K], "required" | "nullable">; 236 + }; 237 + } & ([RequiredKeys<T>] extends [never] 238 + ? {} 239 + : { required: UnionToTuple<RequiredKeys<T>> }) & 240 + ([NullableKeys<T>] extends [never] 241 + ? {} 242 + : { nullable: UnionToTuple<NullableKeys<T>> }) & 243 + O; 244 + 245 + /** 246 + * Map of parameter names to their lexicon item definitions. 247 + * @see https://atproto.com/specs/lexicon#params 248 + */ 249 + type ParamsProperties = Record<string, LexiconItem>; 250 + 251 + /** 252 + * Resulting params schema with required fields extracted. 253 + * @see https://atproto.com/specs/lexicon#params 254 + */ 255 + type ParamsResult<T extends ParamsProperties> = { 256 + type: "params"; 257 + /** Parameter definitions */ 258 + properties: { 259 + [K in keyof T]: Omit<T[K], "required" | "nullable">; 260 + }; 261 + } & ([RequiredKeys<T>] extends [never] 262 + ? {} 263 + : { required: UnionToTuple<RequiredKeys<T>> }); 264 + 265 + /** 266 + * HTTP request or response body schema. 267 + * @see https://atproto.com/specs/lexicon#http-endpoints 268 + */ 269 + type BodySchema = { 270 + /** MIME type encoding (typically "application/json") */ 271 + encoding: "application/json" | (string & {}); 272 + /** Human-readable description */ 273 + description?: string; 274 + /** Object schema defining the body structure */ 275 + schema?: ObjectResult<ObjectProperties>; 276 + }; 277 + 278 + /** 279 + * Error definition for HTTP endpoints. 280 + * @see https://atproto.com/specs/lexicon#http-endpoints 281 + */ 282 + type ErrorDef = { 283 + /** Error name/code */ 284 + name: string; 285 + /** Human-readable error description */ 286 + description?: string; 287 + }; 288 + 289 + /** 290 + * Query endpoint options (HTTP GET). 291 + * @see https://atproto.com/specs/lexicon#query 292 + */ 293 + type QueryOptions = { 294 + /** Human-readable description */ 295 + description?: string; 296 + /** Query string parameters */ 297 + parameters?: ParamsResult<ParamsProperties>; 298 + /** Response body schema */ 299 + output?: BodySchema; 300 + /** Possible error responses */ 301 + errors?: ErrorDef[]; 302 + }; 303 + 304 + /** 305 + * Procedure endpoint options (HTTP POST). 306 + * @see https://atproto.com/specs/lexicon#procedure 307 + */ 308 + type ProcedureOptions = { 309 + /** Human-readable description */ 310 + description?: string; 311 + /** Query string parameters */ 312 + parameters?: ParamsResult<ParamsProperties>; 313 + /** Request body schema */ 314 + input?: BodySchema; 315 + /** Response body schema */ 316 + output?: BodySchema; 317 + /** Possible error responses */ 318 + errors?: ErrorDef[]; 319 + }; 320 + 321 + /** 322 + * WebSocket message schema for subscriptions. 323 + * @see https://atproto.com/specs/lexicon#subscription 324 + */ 325 + type MessageSchema = { 326 + /** Human-readable description */ 327 + description?: string; 328 + /** Union of possible message types */ 329 + schema: { type: "union"; refs: readonly string[] }; 330 + }; 331 + 332 + /** 333 + * Subscription endpoint options (WebSocket). 334 + * @see https://atproto.com/specs/lexicon#subscription 335 + */ 336 + type SubscriptionOptions = { 337 + /** Human-readable description */ 338 + description?: string; 339 + /** Query string parameters */ 340 + parameters?: ParamsResult<ParamsProperties>; 341 + /** Message schema for events */ 342 + message?: MessageSchema; 343 + /** Possible error responses */ 344 + errors?: ErrorDef[]; 345 + }; 346 + 347 + /** 348 + * Public interface for Lexicon to avoid exposing private implementation details 349 + */ 350 + export type LexiconSchema<T extends LexiconNamespace> = { 351 + json: T; 352 + "~infer": Infer<{ json: T }>; 353 + validate( 354 + data: unknown, 355 + def?: keyof T["defs"], 356 + ): ValidationResult<Infer<{ json: T }>>; 357 + }; 358 + 359 + class Lexicon<T extends LexiconNamespace> implements LexiconSchema<T> { 360 + public json: T; 361 + public "~infer": Infer<{ json: T }> = null as unknown as Infer<{ json: T }>; 362 + private _validator: Lexicons; 363 + 364 + constructor(json: T) { 365 + this.json = json; 366 + // Clone before passing to Lexicons to prevent mutation of this.json 367 + this._validator = new Lexicons([ 368 + structuredClone(json) as unknown as LexiconDoc, 369 + ]); 370 + } 371 + 372 + /** 373 + * Validate data against this lexicon's main definition. 374 + * @param data - The data to validate 375 + * @returns ValidationResult with success status and value or error 376 + */ 377 + validate( 378 + data: unknown, 379 + def: keyof T["defs"] = "main", 380 + ): ValidationResult<Infer<{ json: T }>> { 381 + return this._validator.validate( 382 + `${this.json.id}#${def as string}`, 383 + data, 384 + ) as ValidationResult<Infer<{ json: T }>>; 385 + } 386 + } 387 + 388 + /** 389 + * Main API for creating lexicon schemas. 390 + * @see https://atproto.com/specs/lexicon 391 + */ 392 + export const lx = { 393 + /** 394 + * Creates a null type. 395 + * @see https://atproto.com/specs/lexicon#null 396 + */ 397 + null( 398 + options?: LexiconItemCommonOptions, 399 + ): { type: "null" } & LexiconItemCommonOptions { 400 + return { 401 + type: "null", 402 + ...options, 403 + }; 404 + }, 405 + /** 406 + * Creates a boolean type with optional constraints. 407 + * @see https://atproto.com/specs/lexicon#boolean 408 + */ 409 + boolean<T extends BooleanOptions>(options?: T): T & { type: "boolean" } { 410 + return { 411 + type: "boolean", 412 + ...options, 413 + } as T & { type: "boolean" }; 414 + }, 415 + /** 416 + * Creates an integer type with optional min/max and enum constraints. 417 + * @see https://atproto.com/specs/lexicon#integer 418 + */ 419 + integer<T extends IntegerOptions>(options?: T): T & { type: "integer" } { 420 + return { 421 + type: "integer", 422 + ...options, 423 + } as T & { type: "integer" }; 424 + }, 425 + /** 426 + * Creates a string type with optional format, length, and value constraints. 427 + * @see https://atproto.com/specs/lexicon#string 428 + */ 429 + string<T extends StringOptions>(options?: T): T & { type: "string" } { 430 + return { 431 + type: "string", 432 + ...options, 433 + } as T & { type: "string" }; 434 + }, 435 + /** 436 + * Creates an unknown type for flexible, unvalidated objects. 437 + * @see https://atproto.com/specs/lexicon#unknown 438 + */ 439 + unknown( 440 + options?: LexiconItemCommonOptions, 441 + ): { type: "unknown" } & LexiconItemCommonOptions { 442 + return { 443 + type: "unknown", 444 + ...options, 445 + }; 446 + }, 447 + /** 448 + * Creates a bytes type for arbitrary byte arrays. 449 + * @see https://atproto.com/specs/lexicon#bytes 450 + */ 451 + bytes<T extends BytesOptions>(options?: T): T & { type: "bytes" } { 452 + return { 453 + type: "bytes", 454 + ...options, 455 + } as T & { type: "bytes" }; 456 + }, 457 + /** 458 + * Creates a CID link reference to content-addressed data. 459 + * @see https://atproto.com/specs/lexicon#cid-link 460 + */ 461 + cidLink<Link extends string>(link: Link): { type: "cid-link"; $link: Link } { 462 + return { 463 + type: "cid-link", 464 + $link: link, 465 + }; 466 + }, 467 + /** 468 + * Creates a blob type for binary data with MIME type constraints. 469 + * @see https://atproto.com/specs/lexicon#blob 470 + */ 471 + blob<T extends BlobOptions>(options?: T): T & { type: "blob" } { 472 + return { 473 + type: "blob", 474 + ...options, 475 + } as T & { type: "blob" }; 476 + }, 477 + /** 478 + * Creates an array type with item schema and length constraints. 479 + * @see https://atproto.com/specs/lexicon#array 480 + */ 481 + array<Items extends { type: LexiconType }, Options extends ArrayOptions>( 482 + items: Items, 483 + options?: Options, 484 + ): Options & { type: "array"; items: Items } { 485 + return { 486 + type: "array", 487 + items, 488 + ...options, 489 + } as Options & { type: "array"; items: Items }; 490 + }, 491 + /** 492 + * Creates a token type for symbolic values in unions. 493 + * @see https://atproto.com/specs/lexicon#token 494 + */ 495 + token<Description extends string>( 496 + description: Description, 497 + ): { type: "token"; description: Description } { 498 + return { type: "token", description }; 499 + }, 500 + /** 501 + * Creates a reference to another schema definition. 502 + * @see https://atproto.com/specs/lexicon#ref 503 + */ 504 + ref<Ref extends string>( 505 + ref: Ref, 506 + options?: LexiconItemCommonOptions, 507 + ): LexiconItemCommonOptions & { type: "ref"; ref: Ref } { 508 + return { 509 + type: "ref", 510 + ref, 511 + ...options, 512 + } as LexiconItemCommonOptions & { type: "ref"; ref: Ref }; 513 + }, 514 + /** 515 + * Creates a union type for multiple possible type variants. 516 + * @see https://atproto.com/specs/lexicon#union 517 + */ 518 + union<const Refs extends readonly string[], Options extends UnionOptions>( 519 + refs: Refs, 520 + options?: Options, 521 + ): Options & { type: "union"; refs: Refs } { 522 + return { 523 + type: "union", 524 + refs, 525 + ...options, 526 + } as Options & { type: "union"; refs: Refs }; 527 + }, 528 + /** 529 + * Creates a record type for repository records. 530 + * @see https://atproto.com/specs/lexicon#record 531 + */ 532 + record<T extends RecordOptions>(options: T): T & { type: "record" } { 533 + return { 534 + type: "record", 535 + ...options, 536 + }; 537 + }, 538 + /** 539 + * Creates an object type with defined properties. 540 + * @see https://atproto.com/specs/lexicon#object 541 + */ 542 + object<T extends ObjectProperties, O extends ObjectOptions>( 543 + properties: T, 544 + options?: O, 545 + ): ObjectResult<T, O> { 546 + const required = Object.keys(properties).filter( 547 + (key) => "required" in properties[key] && properties[key].required, 548 + ); 549 + const nullable = Object.keys(properties).filter( 550 + (key) => "nullable" in properties[key] && properties[key].nullable, 551 + ); 552 + const result: Record<string, unknown> = { 553 + type: "object", 554 + properties, 555 + ...options, 556 + }; 557 + if (required.length > 0) { 558 + result.required = required; 559 + } 560 + if (nullable.length > 0) { 561 + result.nullable = nullable; 562 + } 563 + return result as ObjectResult<T, O>; 564 + }, 565 + /** 566 + * Creates a params type for query string parameters. 567 + * @see https://atproto.com/specs/lexicon#params 568 + */ 569 + params<Properties extends ParamsProperties>( 570 + properties: Properties, 571 + ): ParamsResult<Properties> { 572 + const required = Object.keys(properties).filter( 573 + (key) => properties[key].required, 574 + ); 575 + const result: Record<string, unknown> = { 576 + type: "params", 577 + properties, 578 + }; 579 + if (required.length > 0) { 580 + result.required = required; 581 + } 582 + return result as ParamsResult<Properties>; 583 + }, 584 + /** 585 + * Creates a query endpoint definition (HTTP GET). 586 + * @see https://atproto.com/specs/lexicon#query 587 + */ 588 + query<T extends QueryOptions>(options?: T): T & { type: "query" } { 589 + return { 590 + type: "query", 591 + ...options, 592 + } as T & { type: "query" }; 593 + }, 594 + /** 595 + * Creates a procedure endpoint definition (HTTP POST). 596 + * @see https://atproto.com/specs/lexicon#procedure 597 + */ 598 + procedure<T extends ProcedureOptions>( 599 + options?: T, 600 + ): T & { type: "procedure" } { 601 + return { 602 + type: "procedure", 603 + ...options, 604 + } as T & { type: "procedure" }; 605 + }, 606 + /** 607 + * Creates a subscription endpoint definition (WebSocket). 608 + * @see https://atproto.com/specs/lexicon#subscription 609 + */ 610 + subscription<T extends SubscriptionOptions>( 611 + options?: T, 612 + ): T & { type: "subscription" } { 613 + return { 614 + type: "subscription", 615 + ...options, 616 + } as T & { type: "subscription" }; 617 + }, 618 + /** 619 + * Creates a lexicon schema document. 620 + * @see https://atproto.com/specs/lexicon#lexicon-document 621 + */ 622 + lexicon<ID extends string, D extends LexiconNamespace["defs"]>( 623 + id: ID, 624 + defs: D, 625 + ): LexiconSchema<{ lexicon: 1; id: ID; defs: D }> { 626 + return new Lexicon({ 627 + lexicon: 1, 628 + id, 629 + defs, 630 + }); 631 + }, 632 + }; 633 + 634 + /** helper to pull lexicon from json directly */ 635 + export function fromJSON<const Lex extends LexiconNamespace>(json: Lex) { 636 + return lx.lexicon<Lex["id"], Lex["defs"]>(json.id, json.defs); 637 + }
+3
packages/prototypey/core/main.ts
··· 1 + export { lx, fromJSON } from "./lib.ts"; 2 + export { type Infer } from "./infer.ts"; 3 + export type * from "@atproto/lexicon";
+40
packages/prototypey/core/tests/base-case.test.ts
··· 1 + import { expect, test } from "vitest"; 2 + import { lx } from "../lib.ts"; 3 + 4 + test("app.bsky.actor.profile", () => { 5 + const profileNamespace = lx.lexicon("app.bsky.actor.profile", { 6 + main: lx.record({ 7 + key: "self", 8 + record: lx.object({ 9 + displayName: lx.string({ maxLength: 64, maxGraphemes: 64 }), 10 + description: lx.string({ maxLength: 256, maxGraphemes: 256 }), 11 + }), 12 + }), 13 + }); 14 + 15 + expect(profileNamespace.json).toEqual({ 16 + lexicon: 1, 17 + id: "app.bsky.actor.profile", 18 + defs: { 19 + main: { 20 + type: "record", 21 + key: "self", 22 + record: { 23 + type: "object", 24 + properties: { 25 + displayName: { 26 + type: "string", 27 + maxLength: 64, 28 + maxGraphemes: 64, 29 + }, 30 + description: { 31 + type: "string", 32 + maxLength: 256, 33 + maxGraphemes: 256, 34 + }, 35 + }, 36 + }, 37 + }, 38 + }, 39 + }); 40 + });
+867
packages/prototypey/core/tests/bsky-actor.test.ts
··· 1 + import { expect, test } from "vitest"; 2 + import { lx } from "../lib.ts"; 3 + 4 + test("app.bsky.actor.defs - profileViewBasic", () => { 5 + const profileViewBasic = lx.object({ 6 + did: lx.string({ required: true, format: "did" }), 7 + handle: lx.string({ required: true, format: "handle" }), 8 + displayName: lx.string({ maxGraphemes: 64, maxLength: 640 }), 9 + pronouns: lx.string(), 10 + avatar: lx.string({ format: "uri" }), 11 + associated: lx.ref("#profileAssociated"), 12 + viewer: lx.ref("#viewerState"), 13 + labels: lx.array(lx.ref("com.atproto.label.defs#label")), 14 + createdAt: lx.string({ format: "datetime" }), 15 + verification: lx.ref("#verificationState"), 16 + status: lx.ref("#statusView"), 17 + }); 18 + 19 + expect(profileViewBasic).toEqual({ 20 + type: "object", 21 + properties: { 22 + did: { type: "string", required: true, format: "did" }, 23 + handle: { type: "string", required: true, format: "handle" }, 24 + displayName: { type: "string", maxGraphemes: 64, maxLength: 640 }, 25 + pronouns: { type: "string" }, 26 + avatar: { type: "string", format: "uri" }, 27 + associated: { type: "ref", ref: "#profileAssociated" }, 28 + viewer: { type: "ref", ref: "#viewerState" }, 29 + labels: { 30 + type: "array", 31 + items: { type: "ref", ref: "com.atproto.label.defs#label" }, 32 + }, 33 + createdAt: { type: "string", format: "datetime" }, 34 + verification: { type: "ref", ref: "#verificationState" }, 35 + status: { type: "ref", ref: "#statusView" }, 36 + }, 37 + required: ["did", "handle"], 38 + }); 39 + }); 40 + 41 + test("app.bsky.actor.defs - profileView", () => { 42 + const profileView = lx.object({ 43 + did: lx.string({ required: true, format: "did" }), 44 + handle: lx.string({ required: true, format: "handle" }), 45 + displayName: lx.string({ maxGraphemes: 64, maxLength: 640 }), 46 + pronouns: lx.string(), 47 + description: lx.string({ maxGraphemes: 256, maxLength: 2560 }), 48 + avatar: lx.string({ format: "uri" }), 49 + associated: lx.ref("#profileAssociated"), 50 + indexedAt: lx.string({ format: "datetime" }), 51 + createdAt: lx.string({ format: "datetime" }), 52 + viewer: lx.ref("#viewerState"), 53 + labels: lx.array(lx.ref("com.atproto.label.defs#label")), 54 + verification: lx.ref("#verificationState"), 55 + status: lx.ref("#statusView"), 56 + }); 57 + 58 + expect(profileView).toEqual({ 59 + type: "object", 60 + properties: { 61 + did: { type: "string", required: true, format: "did" }, 62 + handle: { type: "string", required: true, format: "handle" }, 63 + displayName: { type: "string", maxGraphemes: 64, maxLength: 640 }, 64 + pronouns: { type: "string" }, 65 + description: { type: "string", maxGraphemes: 256, maxLength: 2560 }, 66 + avatar: { type: "string", format: "uri" }, 67 + associated: { type: "ref", ref: "#profileAssociated" }, 68 + indexedAt: { type: "string", format: "datetime" }, 69 + createdAt: { type: "string", format: "datetime" }, 70 + viewer: { type: "ref", ref: "#viewerState" }, 71 + labels: { 72 + type: "array", 73 + items: { type: "ref", ref: "com.atproto.label.defs#label" }, 74 + }, 75 + verification: { type: "ref", ref: "#verificationState" }, 76 + status: { type: "ref", ref: "#statusView" }, 77 + }, 78 + required: ["did", "handle"], 79 + }); 80 + }); 81 + 82 + test("app.bsky.actor.defs - profileViewDetailed", () => { 83 + const profileViewDetailed = lx.object({ 84 + did: lx.string({ required: true, format: "did" }), 85 + handle: lx.string({ required: true, format: "handle" }), 86 + displayName: lx.string({ maxGraphemes: 64, maxLength: 640 }), 87 + description: lx.string({ maxGraphemes: 256, maxLength: 2560 }), 88 + pronouns: lx.string(), 89 + website: lx.string({ format: "uri" }), 90 + avatar: lx.string({ format: "uri" }), 91 + banner: lx.string({ format: "uri" }), 92 + followersCount: lx.integer(), 93 + followsCount: lx.integer(), 94 + postsCount: lx.integer(), 95 + associated: lx.ref("#profileAssociated"), 96 + joinedViaStarterPack: lx.ref("app.bsky.graph.defs#starterPackViewBasic"), 97 + indexedAt: lx.string({ format: "datetime" }), 98 + createdAt: lx.string({ format: "datetime" }), 99 + viewer: lx.ref("#viewerState"), 100 + labels: lx.array(lx.ref("com.atproto.label.defs#label")), 101 + pinnedPost: lx.ref("com.atproto.repo.strongRef"), 102 + verification: lx.ref("#verificationState"), 103 + status: lx.ref("#statusView"), 104 + }); 105 + 106 + expect(profileViewDetailed).toEqual({ 107 + type: "object", 108 + properties: { 109 + did: { type: "string", required: true, format: "did" }, 110 + handle: { type: "string", required: true, format: "handle" }, 111 + displayName: { type: "string", maxGraphemes: 64, maxLength: 640 }, 112 + description: { type: "string", maxGraphemes: 256, maxLength: 2560 }, 113 + pronouns: { type: "string" }, 114 + website: { type: "string", format: "uri" }, 115 + avatar: { type: "string", format: "uri" }, 116 + banner: { type: "string", format: "uri" }, 117 + followersCount: { type: "integer" }, 118 + followsCount: { type: "integer" }, 119 + postsCount: { type: "integer" }, 120 + associated: { type: "ref", ref: "#profileAssociated" }, 121 + joinedViaStarterPack: { 122 + type: "ref", 123 + ref: "app.bsky.graph.defs#starterPackViewBasic", 124 + }, 125 + indexedAt: { type: "string", format: "datetime" }, 126 + createdAt: { type: "string", format: "datetime" }, 127 + viewer: { type: "ref", ref: "#viewerState" }, 128 + labels: { 129 + type: "array", 130 + items: { type: "ref", ref: "com.atproto.label.defs#label" }, 131 + }, 132 + pinnedPost: { type: "ref", ref: "com.atproto.repo.strongRef" }, 133 + verification: { type: "ref", ref: "#verificationState" }, 134 + status: { type: "ref", ref: "#statusView" }, 135 + }, 136 + required: ["did", "handle"], 137 + }); 138 + }); 139 + 140 + test("app.bsky.actor.defs - profileAssociated", () => { 141 + const profileAssociated = lx.object({ 142 + lists: lx.integer(), 143 + feedgens: lx.integer(), 144 + starterPacks: lx.integer(), 145 + labeler: lx.boolean(), 146 + chat: lx.ref("#profileAssociatedChat"), 147 + activitySubscription: lx.ref("#profileAssociatedActivitySubscription"), 148 + }); 149 + 150 + expect(profileAssociated).toEqual({ 151 + type: "object", 152 + properties: { 153 + lists: { type: "integer" }, 154 + feedgens: { type: "integer" }, 155 + starterPacks: { type: "integer" }, 156 + labeler: { type: "boolean" }, 157 + chat: { type: "ref", ref: "#profileAssociatedChat" }, 158 + activitySubscription: { 159 + type: "ref", 160 + ref: "#profileAssociatedActivitySubscription", 161 + }, 162 + }, 163 + }); 164 + }); 165 + 166 + test("app.bsky.actor.defs - profileAssociatedChat", () => { 167 + const profileAssociatedChat = lx.object({ 168 + allowIncoming: lx.string({ 169 + required: true, 170 + knownValues: ["all", "none", "following"], 171 + }), 172 + }); 173 + 174 + expect(profileAssociatedChat).toEqual({ 175 + type: "object", 176 + properties: { 177 + allowIncoming: { 178 + type: "string", 179 + required: true, 180 + knownValues: ["all", "none", "following"], 181 + }, 182 + }, 183 + required: ["allowIncoming"], 184 + }); 185 + }); 186 + 187 + test("app.bsky.actor.defs - profileAssociatedActivitySubscription", () => { 188 + const profileAssociatedActivitySubscription = lx.object({ 189 + allowSubscriptions: lx.string({ 190 + required: true, 191 + knownValues: ["followers", "mutuals", "none"], 192 + }), 193 + }); 194 + 195 + expect(profileAssociatedActivitySubscription).toEqual({ 196 + type: "object", 197 + properties: { 198 + allowSubscriptions: { 199 + type: "string", 200 + required: true, 201 + knownValues: ["followers", "mutuals", "none"], 202 + }, 203 + }, 204 + required: ["allowSubscriptions"], 205 + }); 206 + }); 207 + 208 + test("app.bsky.actor.defs - viewerState", () => { 209 + const viewerState = lx.object({ 210 + muted: lx.boolean(), 211 + mutedByList: lx.ref("app.bsky.graph.defs#listViewBasic"), 212 + blockedBy: lx.boolean(), 213 + blocking: lx.string({ format: "at-uri" }), 214 + blockingByList: lx.ref("app.bsky.graph.defs#listViewBasic"), 215 + following: lx.string({ format: "at-uri" }), 216 + followedBy: lx.string({ format: "at-uri" }), 217 + knownFollowers: lx.ref("#knownFollowers"), 218 + activitySubscription: lx.ref( 219 + "app.bsky.notification.defs#activitySubscription", 220 + ), 221 + }); 222 + 223 + expect(viewerState).toEqual({ 224 + type: "object", 225 + properties: { 226 + muted: { type: "boolean" }, 227 + mutedByList: { type: "ref", ref: "app.bsky.graph.defs#listViewBasic" }, 228 + blockedBy: { type: "boolean" }, 229 + blocking: { type: "string", format: "at-uri" }, 230 + blockingByList: { type: "ref", ref: "app.bsky.graph.defs#listViewBasic" }, 231 + following: { type: "string", format: "at-uri" }, 232 + followedBy: { type: "string", format: "at-uri" }, 233 + knownFollowers: { type: "ref", ref: "#knownFollowers" }, 234 + activitySubscription: { 235 + type: "ref", 236 + ref: "app.bsky.notification.defs#activitySubscription", 237 + }, 238 + }, 239 + }); 240 + }); 241 + 242 + test("app.bsky.actor.defs - knownFollowers", () => { 243 + const knownFollowers = lx.object({ 244 + count: lx.integer({ required: true }), 245 + followers: lx.array(lx.ref("#profileViewBasic"), { 246 + required: true, 247 + minLength: 0, 248 + maxLength: 5, 249 + }), 250 + }); 251 + 252 + expect(knownFollowers).toEqual({ 253 + type: "object", 254 + properties: { 255 + count: { type: "integer", required: true }, 256 + followers: { 257 + type: "array", 258 + items: { type: "ref", ref: "#profileViewBasic" }, 259 + required: true, 260 + minLength: 0, 261 + maxLength: 5, 262 + }, 263 + }, 264 + required: ["count", "followers"], 265 + }); 266 + }); 267 + 268 + test("app.bsky.actor.defs - verificationState", () => { 269 + const verificationState = lx.object({ 270 + verifications: lx.array(lx.ref("#verificationView"), { required: true }), 271 + verifiedStatus: lx.string({ 272 + required: true, 273 + knownValues: ["valid", "invalid", "none"], 274 + }), 275 + trustedVerifierStatus: lx.string({ 276 + required: true, 277 + knownValues: ["valid", "invalid", "none"], 278 + }), 279 + }); 280 + 281 + expect(verificationState).toEqual({ 282 + type: "object", 283 + properties: { 284 + verifications: { 285 + type: "array", 286 + items: { type: "ref", ref: "#verificationView" }, 287 + required: true, 288 + }, 289 + verifiedStatus: { 290 + type: "string", 291 + required: true, 292 + knownValues: ["valid", "invalid", "none"], 293 + }, 294 + trustedVerifierStatus: { 295 + type: "string", 296 + required: true, 297 + knownValues: ["valid", "invalid", "none"], 298 + }, 299 + }, 300 + required: ["verifications", "verifiedStatus", "trustedVerifierStatus"], 301 + }); 302 + }); 303 + 304 + test("app.bsky.actor.defs - verificationView", () => { 305 + const verificationView = lx.object({ 306 + issuer: lx.string({ required: true, format: "did" }), 307 + uri: lx.string({ required: true, format: "at-uri" }), 308 + isValid: lx.boolean({ required: true }), 309 + createdAt: lx.string({ required: true, format: "datetime" }), 310 + }); 311 + 312 + expect(verificationView).toEqual({ 313 + type: "object", 314 + properties: { 315 + issuer: { type: "string", required: true, format: "did" }, 316 + uri: { type: "string", required: true, format: "at-uri" }, 317 + isValid: { type: "boolean", required: true }, 318 + createdAt: { type: "string", required: true, format: "datetime" }, 319 + }, 320 + required: ["issuer", "uri", "isValid", "createdAt"], 321 + }); 322 + }); 323 + 324 + test("app.bsky.actor.defs - preferences", () => { 325 + const preferences = lx.array( 326 + lx.union([ 327 + "#adultContentPref", 328 + "#contentLabelPref", 329 + "#savedFeedsPref", 330 + "#savedFeedsPrefV2", 331 + "#personalDetailsPref", 332 + "#feedViewPref", 333 + "#threadViewPref", 334 + "#interestsPref", 335 + "#mutedWordsPref", 336 + "#hiddenPostsPref", 337 + "#bskyAppStatePref", 338 + "#labelersPref", 339 + "#postInteractionSettingsPref", 340 + "#verificationPrefs", 341 + ]), 342 + ); 343 + 344 + expect(preferences).toEqual({ 345 + type: "array", 346 + items: { 347 + type: "union", 348 + refs: [ 349 + "#adultContentPref", 350 + "#contentLabelPref", 351 + "#savedFeedsPref", 352 + "#savedFeedsPrefV2", 353 + "#personalDetailsPref", 354 + "#feedViewPref", 355 + "#threadViewPref", 356 + "#interestsPref", 357 + "#mutedWordsPref", 358 + "#hiddenPostsPref", 359 + "#bskyAppStatePref", 360 + "#labelersPref", 361 + "#postInteractionSettingsPref", 362 + "#verificationPrefs", 363 + ], 364 + }, 365 + }); 366 + }); 367 + 368 + test("app.bsky.actor.defs - adultContentPref", () => { 369 + const adultContentPref = lx.object({ 370 + enabled: lx.boolean({ required: true, default: false }), 371 + }); 372 + 373 + expect(adultContentPref).toEqual({ 374 + type: "object", 375 + properties: { 376 + enabled: { type: "boolean", required: true, default: false }, 377 + }, 378 + required: ["enabled"], 379 + }); 380 + }); 381 + 382 + test("app.bsky.actor.defs - contentLabelPref", () => { 383 + const contentLabelPref = lx.object({ 384 + labelerDid: lx.string({ format: "did" }), 385 + label: lx.string({ required: true }), 386 + visibility: lx.string({ 387 + required: true, 388 + knownValues: ["ignore", "show", "warn", "hide"], 389 + }), 390 + }); 391 + 392 + expect(contentLabelPref).toEqual({ 393 + type: "object", 394 + properties: { 395 + labelerDid: { type: "string", format: "did" }, 396 + label: { type: "string", required: true }, 397 + visibility: { 398 + type: "string", 399 + required: true, 400 + knownValues: ["ignore", "show", "warn", "hide"], 401 + }, 402 + }, 403 + required: ["label", "visibility"], 404 + }); 405 + }); 406 + 407 + test("app.bsky.actor.defs - savedFeed", () => { 408 + const savedFeed = lx.object({ 409 + id: lx.string({ required: true }), 410 + type: lx.string({ 411 + required: true, 412 + knownValues: ["feed", "list", "timeline"], 413 + }), 414 + value: lx.string({ required: true }), 415 + pinned: lx.boolean({ required: true }), 416 + }); 417 + 418 + expect(savedFeed).toEqual({ 419 + type: "object", 420 + properties: { 421 + id: { type: "string", required: true }, 422 + type: { 423 + type: "string", 424 + required: true, 425 + knownValues: ["feed", "list", "timeline"], 426 + }, 427 + value: { type: "string", required: true }, 428 + pinned: { type: "boolean", required: true }, 429 + }, 430 + required: ["id", "type", "value", "pinned"], 431 + }); 432 + }); 433 + 434 + test("app.bsky.actor.defs - savedFeedsPrefV2", () => { 435 + const savedFeedsPrefV2 = lx.object({ 436 + items: lx.array(lx.ref("app.bsky.actor.defs#savedFeed"), { 437 + required: true, 438 + }), 439 + }); 440 + 441 + expect(savedFeedsPrefV2).toEqual({ 442 + type: "object", 443 + properties: { 444 + items: { 445 + type: "array", 446 + items: { type: "ref", ref: "app.bsky.actor.defs#savedFeed" }, 447 + required: true, 448 + }, 449 + }, 450 + required: ["items"], 451 + }); 452 + }); 453 + 454 + test("app.bsky.actor.defs - savedFeedsPref", () => { 455 + const savedFeedsPref = lx.object({ 456 + pinned: lx.array(lx.string({ format: "at-uri" }), { required: true }), 457 + saved: lx.array(lx.string({ format: "at-uri" }), { required: true }), 458 + timelineIndex: lx.integer(), 459 + }); 460 + 461 + expect(savedFeedsPref).toEqual({ 462 + type: "object", 463 + properties: { 464 + pinned: { 465 + type: "array", 466 + items: { type: "string", format: "at-uri" }, 467 + required: true, 468 + }, 469 + saved: { 470 + type: "array", 471 + items: { type: "string", format: "at-uri" }, 472 + required: true, 473 + }, 474 + timelineIndex: { type: "integer" }, 475 + }, 476 + required: ["pinned", "saved"], 477 + }); 478 + }); 479 + 480 + test("app.bsky.actor.defs - personalDetailsPref", () => { 481 + const personalDetailsPref = lx.object({ 482 + birthDate: lx.string({ format: "datetime" }), 483 + }); 484 + 485 + expect(personalDetailsPref).toEqual({ 486 + type: "object", 487 + properties: { 488 + birthDate: { type: "string", format: "datetime" }, 489 + }, 490 + }); 491 + }); 492 + 493 + test("app.bsky.actor.defs - feedViewPref", () => { 494 + const feedViewPref = lx.object({ 495 + feed: lx.string({ required: true }), 496 + hideReplies: lx.boolean(), 497 + hideRepliesByUnfollowed: lx.boolean({ default: true }), 498 + hideRepliesByLikeCount: lx.integer(), 499 + hideReposts: lx.boolean(), 500 + hideQuotePosts: lx.boolean(), 501 + }); 502 + 503 + expect(feedViewPref).toEqual({ 504 + type: "object", 505 + properties: { 506 + feed: { type: "string", required: true }, 507 + hideReplies: { type: "boolean" }, 508 + hideRepliesByUnfollowed: { type: "boolean", default: true }, 509 + hideRepliesByLikeCount: { type: "integer" }, 510 + hideReposts: { type: "boolean" }, 511 + hideQuotePosts: { type: "boolean" }, 512 + }, 513 + required: ["feed"], 514 + }); 515 + }); 516 + 517 + test("app.bsky.actor.defs - threadViewPref", () => { 518 + const threadViewPref = lx.object({ 519 + sort: lx.string({ 520 + knownValues: ["oldest", "newest", "most-likes", "random", "hotness"], 521 + }), 522 + prioritizeFollowedUsers: lx.boolean(), 523 + }); 524 + 525 + expect(threadViewPref).toEqual({ 526 + type: "object", 527 + properties: { 528 + sort: { 529 + type: "string", 530 + knownValues: ["oldest", "newest", "most-likes", "random", "hotness"], 531 + }, 532 + prioritizeFollowedUsers: { type: "boolean" }, 533 + }, 534 + }); 535 + }); 536 + 537 + test("app.bsky.actor.defs - interestsPref", () => { 538 + const interestsPref = lx.object({ 539 + tags: lx.array(lx.string({ maxLength: 640, maxGraphemes: 64 }), { 540 + required: true, 541 + maxLength: 100, 542 + }), 543 + }); 544 + 545 + expect(interestsPref).toEqual({ 546 + type: "object", 547 + properties: { 548 + tags: { 549 + type: "array", 550 + items: { type: "string", maxLength: 640, maxGraphemes: 64 }, 551 + required: true, 552 + maxLength: 100, 553 + }, 554 + }, 555 + required: ["tags"], 556 + }); 557 + }); 558 + 559 + test("app.bsky.actor.defs - mutedWordTarget", () => { 560 + const mutedWordTarget = lx.string({ 561 + knownValues: ["content", "tag"], 562 + maxLength: 640, 563 + maxGraphemes: 64, 564 + }); 565 + 566 + expect(mutedWordTarget).toEqual({ 567 + type: "string", 568 + knownValues: ["content", "tag"], 569 + maxLength: 640, 570 + maxGraphemes: 64, 571 + }); 572 + }); 573 + 574 + test("app.bsky.actor.defs - mutedWord", () => { 575 + const mutedWord = lx.object({ 576 + id: lx.string(), 577 + value: lx.string({ required: true, maxLength: 10000, maxGraphemes: 1000 }), 578 + targets: lx.array(lx.ref("app.bsky.actor.defs#mutedWordTarget"), { 579 + required: true, 580 + }), 581 + actorTarget: lx.string({ 582 + knownValues: ["all", "exclude-following"], 583 + default: "all", 584 + }), 585 + expiresAt: lx.string({ format: "datetime" }), 586 + }); 587 + 588 + expect(mutedWord).toEqual({ 589 + type: "object", 590 + properties: { 591 + id: { type: "string" }, 592 + value: { 593 + type: "string", 594 + required: true, 595 + maxLength: 10000, 596 + maxGraphemes: 1000, 597 + }, 598 + targets: { 599 + type: "array", 600 + items: { type: "ref", ref: "app.bsky.actor.defs#mutedWordTarget" }, 601 + required: true, 602 + }, 603 + actorTarget: { 604 + type: "string", 605 + knownValues: ["all", "exclude-following"], 606 + default: "all", 607 + }, 608 + expiresAt: { type: "string", format: "datetime" }, 609 + }, 610 + required: ["value", "targets"], 611 + }); 612 + }); 613 + 614 + test("app.bsky.actor.defs - mutedWordsPref", () => { 615 + const mutedWordsPref = lx.object({ 616 + items: lx.array(lx.ref("app.bsky.actor.defs#mutedWord"), { 617 + required: true, 618 + }), 619 + }); 620 + 621 + expect(mutedWordsPref).toEqual({ 622 + type: "object", 623 + properties: { 624 + items: { 625 + type: "array", 626 + items: { type: "ref", ref: "app.bsky.actor.defs#mutedWord" }, 627 + required: true, 628 + }, 629 + }, 630 + required: ["items"], 631 + }); 632 + }); 633 + 634 + test("app.bsky.actor.defs - hiddenPostsPref", () => { 635 + const hiddenPostsPref = lx.object({ 636 + items: lx.array(lx.string({ format: "at-uri" }), { required: true }), 637 + }); 638 + 639 + expect(hiddenPostsPref).toEqual({ 640 + type: "object", 641 + properties: { 642 + items: { 643 + type: "array", 644 + items: { type: "string", format: "at-uri" }, 645 + required: true, 646 + }, 647 + }, 648 + required: ["items"], 649 + }); 650 + }); 651 + 652 + test("app.bsky.actor.defs - labelersPref", () => { 653 + const labelersPref = lx.object({ 654 + labelers: lx.array(lx.ref("#labelerPrefItem"), { required: true }), 655 + }); 656 + 657 + expect(labelersPref).toEqual({ 658 + type: "object", 659 + properties: { 660 + labelers: { 661 + type: "array", 662 + items: { type: "ref", ref: "#labelerPrefItem" }, 663 + required: true, 664 + }, 665 + }, 666 + required: ["labelers"], 667 + }); 668 + }); 669 + 670 + test("app.bsky.actor.defs - labelerPrefItem", () => { 671 + const labelerPrefItem = lx.object({ 672 + did: lx.string({ required: true, format: "did" }), 673 + }); 674 + 675 + expect(labelerPrefItem).toEqual({ 676 + type: "object", 677 + properties: { 678 + did: { type: "string", required: true, format: "did" }, 679 + }, 680 + required: ["did"], 681 + }); 682 + }); 683 + 684 + test("app.bsky.actor.defs - bskyAppStatePref", () => { 685 + const bskyAppStatePref = lx.object({ 686 + activeProgressGuide: lx.ref("#bskyAppProgressGuide"), 687 + queuedNudges: lx.array(lx.string({ maxLength: 100 }), { maxLength: 1000 }), 688 + nuxs: lx.array(lx.ref("app.bsky.actor.defs#nux"), { maxLength: 100 }), 689 + }); 690 + 691 + expect(bskyAppStatePref).toEqual({ 692 + type: "object", 693 + properties: { 694 + activeProgressGuide: { type: "ref", ref: "#bskyAppProgressGuide" }, 695 + queuedNudges: { 696 + type: "array", 697 + items: { type: "string", maxLength: 100 }, 698 + maxLength: 1000, 699 + }, 700 + nuxs: { 701 + type: "array", 702 + items: { type: "ref", ref: "app.bsky.actor.defs#nux" }, 703 + maxLength: 100, 704 + }, 705 + }, 706 + }); 707 + }); 708 + 709 + test("app.bsky.actor.defs - bskyAppProgressGuide", () => { 710 + const bskyAppProgressGuide = lx.object({ 711 + guide: lx.string({ required: true, maxLength: 100 }), 712 + }); 713 + 714 + expect(bskyAppProgressGuide).toEqual({ 715 + type: "object", 716 + properties: { 717 + guide: { type: "string", required: true, maxLength: 100 }, 718 + }, 719 + required: ["guide"], 720 + }); 721 + }); 722 + 723 + test("app.bsky.actor.defs - nux", () => { 724 + const nux = lx.object({ 725 + id: lx.string({ required: true, maxLength: 100 }), 726 + completed: lx.boolean({ required: true, default: false }), 727 + data: lx.string({ maxLength: 3000, maxGraphemes: 300 }), 728 + expiresAt: lx.string({ format: "datetime" }), 729 + }); 730 + 731 + expect(nux).toEqual({ 732 + type: "object", 733 + properties: { 734 + id: { type: "string", required: true, maxLength: 100 }, 735 + completed: { type: "boolean", required: true, default: false }, 736 + data: { type: "string", maxLength: 3000, maxGraphemes: 300 }, 737 + expiresAt: { type: "string", format: "datetime" }, 738 + }, 739 + required: ["id", "completed"], 740 + }); 741 + }); 742 + 743 + test("app.bsky.actor.defs - verificationPrefs", () => { 744 + const verificationPrefs = lx.object({ 745 + hideBadges: lx.boolean({ default: false }), 746 + }); 747 + 748 + expect(verificationPrefs).toEqual({ 749 + type: "object", 750 + properties: { 751 + hideBadges: { type: "boolean", default: false }, 752 + }, 753 + }); 754 + }); 755 + 756 + test("app.bsky.actor.defs - postInteractionSettingsPref", () => { 757 + const postInteractionSettingsPref = lx.object({ 758 + threadgateAllowRules: lx.array( 759 + lx.union([ 760 + "app.bsky.feed.threadgate#mentionRule", 761 + "app.bsky.feed.threadgate#followerRule", 762 + "app.bsky.feed.threadgate#followingRule", 763 + "app.bsky.feed.threadgate#listRule", 764 + ]), 765 + { maxLength: 5 }, 766 + ), 767 + postgateEmbeddingRules: lx.array( 768 + lx.union(["app.bsky.feed.postgate#disableRule"]), 769 + { maxLength: 5 }, 770 + ), 771 + }); 772 + 773 + expect(postInteractionSettingsPref).toEqual({ 774 + type: "object", 775 + properties: { 776 + threadgateAllowRules: { 777 + type: "array", 778 + items: { 779 + type: "union", 780 + refs: [ 781 + "app.bsky.feed.threadgate#mentionRule", 782 + "app.bsky.feed.threadgate#followerRule", 783 + "app.bsky.feed.threadgate#followingRule", 784 + "app.bsky.feed.threadgate#listRule", 785 + ], 786 + }, 787 + maxLength: 5, 788 + }, 789 + postgateEmbeddingRules: { 790 + type: "array", 791 + items: { 792 + type: "union", 793 + refs: ["app.bsky.feed.postgate#disableRule"], 794 + }, 795 + maxLength: 5, 796 + }, 797 + }, 798 + }); 799 + }); 800 + 801 + test("app.bsky.actor.defs - statusView", () => { 802 + const statusView = lx.object({ 803 + status: lx.string({ 804 + required: true, 805 + knownValues: ["app.bsky.actor.status#live"], 806 + }), 807 + record: lx.unknown({ required: true }), 808 + embed: lx.union(["app.bsky.embed.external#view"]), 809 + expiresAt: lx.string({ format: "datetime" }), 810 + isActive: lx.boolean(), 811 + }); 812 + 813 + expect(statusView).toEqual({ 814 + type: "object", 815 + properties: { 816 + status: { 817 + type: "string", 818 + required: true, 819 + knownValues: ["app.bsky.actor.status#live"], 820 + }, 821 + record: { type: "unknown", required: true }, 822 + embed: { 823 + type: "union", 824 + refs: ["app.bsky.embed.external#view"], 825 + }, 826 + expiresAt: { type: "string", format: "datetime" }, 827 + isActive: { type: "boolean" }, 828 + }, 829 + required: ["status", "record"], 830 + }); 831 + }); 832 + 833 + test("app.bsky.actor.defs - full lexicon", () => { 834 + const actorDefs = lx.lexicon("app.bsky.actor.defs", { 835 + profileViewBasic: lx.object({ 836 + did: lx.string({ required: true, format: "did" }), 837 + handle: lx.string({ required: true, format: "handle" }), 838 + displayName: lx.string({ maxGraphemes: 64, maxLength: 640 }), 839 + pronouns: lx.string(), 840 + avatar: lx.string({ format: "uri" }), 841 + associated: lx.ref("#profileAssociated"), 842 + viewer: lx.ref("#viewerState"), 843 + labels: lx.array(lx.ref("com.atproto.label.defs#label")), 844 + createdAt: lx.string({ format: "datetime" }), 845 + verification: lx.ref("#verificationState"), 846 + status: lx.ref("#statusView"), 847 + }), 848 + viewerState: lx.object({ 849 + muted: lx.boolean(), 850 + mutedByList: lx.ref("app.bsky.graph.defs#listViewBasic"), 851 + blockedBy: lx.boolean(), 852 + blocking: lx.string({ format: "at-uri" }), 853 + blockingByList: lx.ref("app.bsky.graph.defs#listViewBasic"), 854 + following: lx.string({ format: "at-uri" }), 855 + followedBy: lx.string({ format: "at-uri" }), 856 + knownFollowers: lx.ref("#knownFollowers"), 857 + activitySubscription: lx.ref( 858 + "app.bsky.notification.defs#activitySubscription", 859 + ), 860 + }), 861 + }); 862 + 863 + expect(actorDefs.json.lexicon).toEqual(1); 864 + expect(actorDefs.json.id).toEqual("app.bsky.actor.defs"); 865 + expect(actorDefs.json.defs.profileViewBasic.type).toEqual("object"); 866 + expect(actorDefs.json.defs.viewerState.type).toEqual("object"); 867 + });
+681
packages/prototypey/core/tests/bsky-feed.test.ts
··· 1 + import { expect, test } from "vitest"; 2 + import { lx } from "../lib.ts"; 3 + 4 + test("app.bsky.feed.defs - postView", () => { 5 + const postView = lx.object({ 6 + uri: lx.string({ required: true, format: "at-uri" }), 7 + cid: lx.string({ required: true, format: "cid" }), 8 + author: lx.ref("app.bsky.actor.defs#profileViewBasic", { required: true }), 9 + record: lx.unknown({ required: true }), 10 + embed: lx.union([ 11 + "app.bsky.embed.images#view", 12 + "app.bsky.embed.video#view", 13 + "app.bsky.embed.external#view", 14 + "app.bsky.embed.record#view", 15 + "app.bsky.embed.recordWithMedia#view", 16 + ]), 17 + bookmarkCount: lx.integer(), 18 + replyCount: lx.integer(), 19 + repostCount: lx.integer(), 20 + likeCount: lx.integer(), 21 + quoteCount: lx.integer(), 22 + indexedAt: lx.string({ required: true, format: "datetime" }), 23 + viewer: lx.ref("#viewerState"), 24 + labels: lx.array(lx.ref("com.atproto.label.defs#label")), 25 + threadgate: lx.ref("#threadgateView"), 26 + }); 27 + 28 + expect(postView).toEqual({ 29 + type: "object", 30 + properties: { 31 + uri: { type: "string", required: true, format: "at-uri" }, 32 + cid: { type: "string", required: true, format: "cid" }, 33 + author: { 34 + type: "ref", 35 + ref: "app.bsky.actor.defs#profileViewBasic", 36 + required: true, 37 + }, 38 + record: { type: "unknown", required: true }, 39 + embed: { 40 + type: "union", 41 + refs: [ 42 + "app.bsky.embed.images#view", 43 + "app.bsky.embed.video#view", 44 + "app.bsky.embed.external#view", 45 + "app.bsky.embed.record#view", 46 + "app.bsky.embed.recordWithMedia#view", 47 + ], 48 + }, 49 + bookmarkCount: { type: "integer" }, 50 + replyCount: { type: "integer" }, 51 + repostCount: { type: "integer" }, 52 + likeCount: { type: "integer" }, 53 + quoteCount: { type: "integer" }, 54 + indexedAt: { type: "string", required: true, format: "datetime" }, 55 + viewer: { type: "ref", ref: "#viewerState" }, 56 + labels: { 57 + type: "array", 58 + items: { type: "ref", ref: "com.atproto.label.defs#label" }, 59 + }, 60 + threadgate: { type: "ref", ref: "#threadgateView" }, 61 + }, 62 + required: ["uri", "cid", "author", "record", "indexedAt"], 63 + }); 64 + }); 65 + 66 + test("app.bsky.feed.defs - viewerState", () => { 67 + const viewerState = lx.object({ 68 + repost: lx.string({ format: "at-uri" }), 69 + like: lx.string({ format: "at-uri" }), 70 + bookmarked: lx.boolean(), 71 + threadMuted: lx.boolean(), 72 + replyDisabled: lx.boolean(), 73 + embeddingDisabled: lx.boolean(), 74 + pinned: lx.boolean(), 75 + }); 76 + 77 + expect(viewerState).toEqual({ 78 + type: "object", 79 + properties: { 80 + repost: { type: "string", format: "at-uri" }, 81 + like: { type: "string", format: "at-uri" }, 82 + bookmarked: { type: "boolean" }, 83 + threadMuted: { type: "boolean" }, 84 + replyDisabled: { type: "boolean" }, 85 + embeddingDisabled: { type: "boolean" }, 86 + pinned: { type: "boolean" }, 87 + }, 88 + }); 89 + }); 90 + 91 + test("app.bsky.feed.defs - threadContext", () => { 92 + const threadContext = lx.object({ 93 + rootAuthorLike: lx.string({ format: "at-uri" }), 94 + }); 95 + 96 + expect(threadContext).toEqual({ 97 + type: "object", 98 + properties: { 99 + rootAuthorLike: { type: "string", format: "at-uri" }, 100 + }, 101 + }); 102 + }); 103 + 104 + test("app.bsky.feed.defs - feedViewPost", () => { 105 + const feedViewPost = lx.object({ 106 + post: lx.ref("#postView", { required: true }), 107 + reply: lx.ref("#replyRef"), 108 + reason: lx.union(["#reasonRepost", "#reasonPin"]), 109 + feedContext: lx.string({ maxLength: 2000 }), 110 + reqId: lx.string({ maxLength: 100 }), 111 + }); 112 + 113 + expect(feedViewPost).toEqual({ 114 + type: "object", 115 + properties: { 116 + post: { type: "ref", ref: "#postView", required: true }, 117 + reply: { type: "ref", ref: "#replyRef" }, 118 + reason: { 119 + type: "union", 120 + refs: ["#reasonRepost", "#reasonPin"], 121 + }, 122 + feedContext: { type: "string", maxLength: 2000 }, 123 + reqId: { type: "string", maxLength: 100 }, 124 + }, 125 + required: ["post"], 126 + }); 127 + }); 128 + 129 + test("app.bsky.feed.defs - replyRef", () => { 130 + const replyRef = lx.object({ 131 + root: lx.union(["#postView", "#notFoundPost", "#blockedPost"], { 132 + required: true, 133 + }), 134 + parent: lx.union(["#postView", "#notFoundPost", "#blockedPost"], { 135 + required: true, 136 + }), 137 + grandparentAuthor: lx.ref("app.bsky.actor.defs#profileViewBasic"), 138 + }); 139 + 140 + expect(replyRef).toEqual({ 141 + type: "object", 142 + properties: { 143 + root: { 144 + type: "union", 145 + refs: ["#postView", "#notFoundPost", "#blockedPost"], 146 + required: true, 147 + }, 148 + parent: { 149 + type: "union", 150 + refs: ["#postView", "#notFoundPost", "#blockedPost"], 151 + required: true, 152 + }, 153 + grandparentAuthor: { 154 + type: "ref", 155 + ref: "app.bsky.actor.defs#profileViewBasic", 156 + }, 157 + }, 158 + required: ["root", "parent"], 159 + }); 160 + }); 161 + 162 + test("app.bsky.feed.defs - reasonRepost", () => { 163 + const reasonRepost = lx.object({ 164 + by: lx.ref("app.bsky.actor.defs#profileViewBasic", { required: true }), 165 + uri: lx.string({ format: "at-uri" }), 166 + cid: lx.string({ format: "cid" }), 167 + indexedAt: lx.string({ required: true, format: "datetime" }), 168 + }); 169 + 170 + expect(reasonRepost).toEqual({ 171 + type: "object", 172 + properties: { 173 + by: { 174 + type: "ref", 175 + ref: "app.bsky.actor.defs#profileViewBasic", 176 + required: true, 177 + }, 178 + uri: { type: "string", format: "at-uri" }, 179 + cid: { type: "string", format: "cid" }, 180 + indexedAt: { type: "string", required: true, format: "datetime" }, 181 + }, 182 + required: ["by", "indexedAt"], 183 + }); 184 + }); 185 + 186 + test("app.bsky.feed.defs - reasonPin", () => { 187 + const reasonPin = lx.object({}); 188 + 189 + expect(reasonPin).toEqual({ 190 + type: "object", 191 + properties: {}, 192 + }); 193 + }); 194 + 195 + test("app.bsky.feed.defs - threadViewPost", () => { 196 + const threadViewPost = lx.object({ 197 + post: lx.ref("#postView", { required: true }), 198 + parent: lx.union(["#threadViewPost", "#notFoundPost", "#blockedPost"]), 199 + replies: lx.array( 200 + lx.union(["#threadViewPost", "#notFoundPost", "#blockedPost"]), 201 + ), 202 + threadContext: lx.ref("#threadContext"), 203 + }); 204 + 205 + expect(threadViewPost).toEqual({ 206 + type: "object", 207 + properties: { 208 + post: { type: "ref", ref: "#postView", required: true }, 209 + parent: { 210 + type: "union", 211 + refs: ["#threadViewPost", "#notFoundPost", "#blockedPost"], 212 + }, 213 + replies: { 214 + type: "array", 215 + items: { 216 + type: "union", 217 + refs: ["#threadViewPost", "#notFoundPost", "#blockedPost"], 218 + }, 219 + }, 220 + threadContext: { type: "ref", ref: "#threadContext" }, 221 + }, 222 + required: ["post"], 223 + }); 224 + }); 225 + 226 + test("app.bsky.feed.defs - notFoundPost", () => { 227 + const notFoundPost = lx.object({ 228 + uri: lx.string({ required: true, format: "at-uri" }), 229 + notFound: lx.boolean({ required: true, const: true }), 230 + }); 231 + 232 + expect(notFoundPost).toEqual({ 233 + type: "object", 234 + properties: { 235 + uri: { type: "string", required: true, format: "at-uri" }, 236 + notFound: { type: "boolean", required: true, const: true }, 237 + }, 238 + required: ["uri", "notFound"], 239 + }); 240 + }); 241 + 242 + test("app.bsky.feed.defs - blockedPost", () => { 243 + const blockedPost = lx.object({ 244 + uri: lx.string({ required: true, format: "at-uri" }), 245 + blocked: lx.boolean({ required: true, const: true }), 246 + author: lx.ref("#blockedAuthor", { required: true }), 247 + }); 248 + 249 + expect(blockedPost).toEqual({ 250 + type: "object", 251 + properties: { 252 + uri: { type: "string", required: true, format: "at-uri" }, 253 + blocked: { type: "boolean", required: true, const: true }, 254 + author: { type: "ref", ref: "#blockedAuthor", required: true }, 255 + }, 256 + required: ["uri", "blocked", "author"], 257 + }); 258 + }); 259 + 260 + test("app.bsky.feed.defs - blockedAuthor", () => { 261 + const blockedAuthor = lx.object({ 262 + did: lx.string({ required: true, format: "did" }), 263 + viewer: lx.ref("app.bsky.actor.defs#viewerState"), 264 + }); 265 + 266 + expect(blockedAuthor).toEqual({ 267 + type: "object", 268 + properties: { 269 + did: { type: "string", required: true, format: "did" }, 270 + viewer: { type: "ref", ref: "app.bsky.actor.defs#viewerState" }, 271 + }, 272 + required: ["did"], 273 + }); 274 + }); 275 + 276 + test("app.bsky.feed.defs - generatorView", () => { 277 + const generatorView = lx.object({ 278 + uri: lx.string({ required: true, format: "at-uri" }), 279 + cid: lx.string({ required: true, format: "cid" }), 280 + did: lx.string({ required: true, format: "did" }), 281 + creator: lx.ref("app.bsky.actor.defs#profileView", { required: true }), 282 + displayName: lx.string({ required: true }), 283 + description: lx.string({ maxGraphemes: 300, maxLength: 3000 }), 284 + descriptionFacets: lx.array(lx.ref("app.bsky.richtext.facet")), 285 + avatar: lx.string({ format: "uri" }), 286 + likeCount: lx.integer({ minimum: 0 }), 287 + acceptsInteractions: lx.boolean(), 288 + labels: lx.array(lx.ref("com.atproto.label.defs#label")), 289 + viewer: lx.ref("#generatorViewerState"), 290 + contentMode: lx.string({ 291 + knownValues: [ 292 + "app.bsky.feed.defs#contentModeUnspecified", 293 + "app.bsky.feed.defs#contentModeVideo", 294 + ], 295 + }), 296 + indexedAt: lx.string({ required: true, format: "datetime" }), 297 + }); 298 + 299 + expect(generatorView).toEqual({ 300 + type: "object", 301 + properties: { 302 + uri: { type: "string", required: true, format: "at-uri" }, 303 + cid: { type: "string", required: true, format: "cid" }, 304 + did: { type: "string", required: true, format: "did" }, 305 + creator: { 306 + type: "ref", 307 + ref: "app.bsky.actor.defs#profileView", 308 + required: true, 309 + }, 310 + displayName: { type: "string", required: true }, 311 + description: { type: "string", maxGraphemes: 300, maxLength: 3000 }, 312 + descriptionFacets: { 313 + type: "array", 314 + items: { type: "ref", ref: "app.bsky.richtext.facet" }, 315 + }, 316 + avatar: { type: "string", format: "uri" }, 317 + likeCount: { type: "integer", minimum: 0 }, 318 + acceptsInteractions: { type: "boolean" }, 319 + labels: { 320 + type: "array", 321 + items: { type: "ref", ref: "com.atproto.label.defs#label" }, 322 + }, 323 + viewer: { type: "ref", ref: "#generatorViewerState" }, 324 + contentMode: { 325 + type: "string", 326 + knownValues: [ 327 + "app.bsky.feed.defs#contentModeUnspecified", 328 + "app.bsky.feed.defs#contentModeVideo", 329 + ], 330 + }, 331 + indexedAt: { type: "string", required: true, format: "datetime" }, 332 + }, 333 + required: ["uri", "cid", "did", "creator", "displayName", "indexedAt"], 334 + }); 335 + }); 336 + 337 + test("app.bsky.feed.defs - generatorViewerState", () => { 338 + const generatorViewerState = lx.object({ 339 + like: lx.string({ format: "at-uri" }), 340 + }); 341 + 342 + expect(generatorViewerState).toEqual({ 343 + type: "object", 344 + properties: { 345 + like: { type: "string", format: "at-uri" }, 346 + }, 347 + }); 348 + }); 349 + 350 + test("app.bsky.feed.defs - skeletonFeedPost", () => { 351 + const skeletonFeedPost = lx.object({ 352 + post: lx.string({ required: true, format: "at-uri" }), 353 + reason: lx.union(["#skeletonReasonRepost", "#skeletonReasonPin"]), 354 + feedContext: lx.string({ maxLength: 2000 }), 355 + }); 356 + 357 + expect(skeletonFeedPost).toEqual({ 358 + type: "object", 359 + properties: { 360 + post: { type: "string", required: true, format: "at-uri" }, 361 + reason: { 362 + type: "union", 363 + refs: ["#skeletonReasonRepost", "#skeletonReasonPin"], 364 + }, 365 + feedContext: { type: "string", maxLength: 2000 }, 366 + }, 367 + required: ["post"], 368 + }); 369 + }); 370 + 371 + test("app.bsky.feed.defs - skeletonReasonRepost", () => { 372 + const skeletonReasonRepost = lx.object({ 373 + repost: lx.string({ required: true, format: "at-uri" }), 374 + }); 375 + 376 + expect(skeletonReasonRepost).toEqual({ 377 + type: "object", 378 + properties: { 379 + repost: { type: "string", required: true, format: "at-uri" }, 380 + }, 381 + required: ["repost"], 382 + }); 383 + }); 384 + 385 + test("app.bsky.feed.defs - skeletonReasonPin", () => { 386 + const skeletonReasonPin = lx.object({}); 387 + 388 + expect(skeletonReasonPin).toEqual({ 389 + type: "object", 390 + properties: {}, 391 + }); 392 + }); 393 + 394 + test("app.bsky.feed.defs - threadgateView", () => { 395 + const threadgateView = lx.object({ 396 + uri: lx.string({ format: "at-uri" }), 397 + cid: lx.string({ format: "cid" }), 398 + record: lx.unknown(), 399 + lists: lx.array(lx.ref("app.bsky.graph.defs#listViewBasic")), 400 + }); 401 + 402 + expect(threadgateView).toEqual({ 403 + type: "object", 404 + properties: { 405 + uri: { type: "string", format: "at-uri" }, 406 + cid: { type: "string", format: "cid" }, 407 + record: { type: "unknown" }, 408 + lists: { 409 + type: "array", 410 + items: { type: "ref", ref: "app.bsky.graph.defs#listViewBasic" }, 411 + }, 412 + }, 413 + }); 414 + }); 415 + 416 + test("app.bsky.feed.defs - interaction", () => { 417 + const interaction = lx.object({ 418 + item: lx.string({ format: "at-uri" }), 419 + event: lx.string({ 420 + knownValues: [ 421 + "app.bsky.feed.defs#requestLess", 422 + "app.bsky.feed.defs#requestMore", 423 + "app.bsky.feed.defs#clickthroughItem", 424 + "app.bsky.feed.defs#clickthroughAuthor", 425 + "app.bsky.feed.defs#clickthroughReposter", 426 + "app.bsky.feed.defs#clickthroughEmbed", 427 + "app.bsky.feed.defs#interactionSeen", 428 + "app.bsky.feed.defs#interactionLike", 429 + "app.bsky.feed.defs#interactionRepost", 430 + "app.bsky.feed.defs#interactionReply", 431 + "app.bsky.feed.defs#interactionQuote", 432 + "app.bsky.feed.defs#interactionShare", 433 + ], 434 + }), 435 + feedContext: lx.string({ maxLength: 2000 }), 436 + reqId: lx.string({ maxLength: 100 }), 437 + }); 438 + 439 + expect(interaction).toEqual({ 440 + type: "object", 441 + properties: { 442 + item: { type: "string", format: "at-uri" }, 443 + event: { 444 + type: "string", 445 + knownValues: [ 446 + "app.bsky.feed.defs#requestLess", 447 + "app.bsky.feed.defs#requestMore", 448 + "app.bsky.feed.defs#clickthroughItem", 449 + "app.bsky.feed.defs#clickthroughAuthor", 450 + "app.bsky.feed.defs#clickthroughReposter", 451 + "app.bsky.feed.defs#clickthroughEmbed", 452 + "app.bsky.feed.defs#interactionSeen", 453 + "app.bsky.feed.defs#interactionLike", 454 + "app.bsky.feed.defs#interactionRepost", 455 + "app.bsky.feed.defs#interactionReply", 456 + "app.bsky.feed.defs#interactionQuote", 457 + "app.bsky.feed.defs#interactionShare", 458 + ], 459 + }, 460 + feedContext: { type: "string", maxLength: 2000 }, 461 + reqId: { type: "string", maxLength: 100 }, 462 + }, 463 + }); 464 + }); 465 + 466 + test("app.bsky.feed.defs - requestLess token", () => { 467 + const requestLess = lx.token( 468 + "Request that less content like the given feed item be shown in the feed", 469 + ); 470 + 471 + expect(requestLess).toEqual({ 472 + type: "token", 473 + description: 474 + "Request that less content like the given feed item be shown in the feed", 475 + }); 476 + }); 477 + 478 + test("app.bsky.feed.defs - requestMore token", () => { 479 + const requestMore = lx.token( 480 + "Request that more content like the given feed item be shown in the feed", 481 + ); 482 + 483 + expect(requestMore).toEqual({ 484 + type: "token", 485 + description: 486 + "Request that more content like the given feed item be shown in the feed", 487 + }); 488 + }); 489 + 490 + test("app.bsky.feed.defs - clickthroughItem token", () => { 491 + const clickthroughItem = lx.token("User clicked through to the feed item"); 492 + 493 + expect(clickthroughItem).toEqual({ 494 + type: "token", 495 + description: "User clicked through to the feed item", 496 + }); 497 + }); 498 + 499 + test("app.bsky.feed.defs - clickthroughAuthor token", () => { 500 + const clickthroughAuthor = lx.token( 501 + "User clicked through to the author of the feed item", 502 + ); 503 + 504 + expect(clickthroughAuthor).toEqual({ 505 + type: "token", 506 + description: "User clicked through to the author of the feed item", 507 + }); 508 + }); 509 + 510 + test("app.bsky.feed.defs - clickthroughReposter token", () => { 511 + const clickthroughReposter = lx.token( 512 + "User clicked through to the reposter of the feed item", 513 + ); 514 + 515 + expect(clickthroughReposter).toEqual({ 516 + type: "token", 517 + description: "User clicked through to the reposter of the feed item", 518 + }); 519 + }); 520 + 521 + test("app.bsky.feed.defs - clickthroughEmbed token", () => { 522 + const clickthroughEmbed = lx.token( 523 + "User clicked through to the embedded content of the feed item", 524 + ); 525 + 526 + expect(clickthroughEmbed).toEqual({ 527 + type: "token", 528 + description: 529 + "User clicked through to the embedded content of the feed item", 530 + }); 531 + }); 532 + 533 + test("app.bsky.feed.defs - contentModeUnspecified token", () => { 534 + const contentModeUnspecified = lx.token( 535 + "Declares the feed generator returns any types of posts.", 536 + ); 537 + 538 + expect(contentModeUnspecified).toEqual({ 539 + type: "token", 540 + description: "Declares the feed generator returns any types of posts.", 541 + }); 542 + }); 543 + 544 + test("app.bsky.feed.defs - contentModeVideo token", () => { 545 + const contentModeVideo = lx.token( 546 + "Declares the feed generator returns posts containing app.bsky.embed.video embeds.", 547 + ); 548 + 549 + expect(contentModeVideo).toEqual({ 550 + type: "token", 551 + description: 552 + "Declares the feed generator returns posts containing app.bsky.embed.video embeds.", 553 + }); 554 + }); 555 + 556 + test("app.bsky.feed.defs - interactionSeen token", () => { 557 + const interactionSeen = lx.token("Feed item was seen by user"); 558 + 559 + expect(interactionSeen).toEqual({ 560 + type: "token", 561 + description: "Feed item was seen by user", 562 + }); 563 + }); 564 + 565 + test("app.bsky.feed.defs - interactionLike token", () => { 566 + const interactionLike = lx.token("User liked the feed item"); 567 + 568 + expect(interactionLike).toEqual({ 569 + type: "token", 570 + description: "User liked the feed item", 571 + }); 572 + }); 573 + 574 + test("app.bsky.feed.defs - interactionRepost token", () => { 575 + const interactionRepost = lx.token("User reposted the feed item"); 576 + 577 + expect(interactionRepost).toEqual({ 578 + type: "token", 579 + description: "User reposted the feed item", 580 + }); 581 + }); 582 + 583 + test("app.bsky.feed.defs - interactionReply token", () => { 584 + const interactionReply = lx.token("User replied to the feed item"); 585 + 586 + expect(interactionReply).toEqual({ 587 + type: "token", 588 + description: "User replied to the feed item", 589 + }); 590 + }); 591 + 592 + test("app.bsky.feed.defs - interactionQuote token", () => { 593 + const interactionQuote = lx.token("User quoted the feed item"); 594 + 595 + expect(interactionQuote).toEqual({ 596 + type: "token", 597 + description: "User quoted the feed item", 598 + }); 599 + }); 600 + 601 + test("app.bsky.feed.defs - interactionShare token", () => { 602 + const interactionShare = lx.token("User shared the feed item"); 603 + 604 + expect(interactionShare).toEqual({ 605 + type: "token", 606 + description: "User shared the feed item", 607 + }); 608 + }); 609 + 610 + test("app.bsky.feed.defs - full lexicon", () => { 611 + const feedDefs = lx.lexicon("app.bsky.feed.defs", { 612 + postView: lx.object({ 613 + uri: lx.string({ required: true, format: "at-uri" }), 614 + cid: lx.string({ required: true, format: "cid" }), 615 + author: lx.ref("app.bsky.actor.defs#profileViewBasic", { 616 + required: true, 617 + }), 618 + record: lx.unknown({ required: true }), 619 + embed: lx.union([ 620 + "app.bsky.embed.images#view", 621 + "app.bsky.embed.video#view", 622 + "app.bsky.embed.external#view", 623 + "app.bsky.embed.record#view", 624 + "app.bsky.embed.recordWithMedia#view", 625 + ]), 626 + bookmarkCount: lx.integer(), 627 + replyCount: lx.integer(), 628 + repostCount: lx.integer(), 629 + likeCount: lx.integer(), 630 + quoteCount: lx.integer(), 631 + indexedAt: lx.string({ required: true, format: "datetime" }), 632 + viewer: lx.ref("#viewerState"), 633 + labels: lx.array(lx.ref("com.atproto.label.defs#label")), 634 + threadgate: lx.ref("#threadgateView"), 635 + }), 636 + viewerState: lx.object({ 637 + repost: lx.string({ format: "at-uri" }), 638 + like: lx.string({ format: "at-uri" }), 639 + bookmarked: lx.boolean(), 640 + threadMuted: lx.boolean(), 641 + replyDisabled: lx.boolean(), 642 + embeddingDisabled: lx.boolean(), 643 + pinned: lx.boolean(), 644 + }), 645 + requestLess: lx.token( 646 + "Request that less content like the given feed item be shown in the feed", 647 + ), 648 + requestMore: lx.token( 649 + "Request that more content like the given feed item be shown in the feed", 650 + ), 651 + clickthroughItem: lx.token("User clicked through to the feed item"), 652 + clickthroughAuthor: lx.token( 653 + "User clicked through to the author of the feed item", 654 + ), 655 + clickthroughReposter: lx.token( 656 + "User clicked through to the reposter of the feed item", 657 + ), 658 + clickthroughEmbed: lx.token( 659 + "User clicked through to the embedded content of the feed item", 660 + ), 661 + contentModeUnspecified: lx.token( 662 + "Declares the feed generator returns any types of posts.", 663 + ), 664 + contentModeVideo: lx.token( 665 + "Declares the feed generator returns posts containing app.bsky.embed.video embeds.", 666 + ), 667 + interactionSeen: lx.token("Feed item was seen by user"), 668 + interactionLike: lx.token("User liked the feed item"), 669 + interactionRepost: lx.token("User reposted the feed item"), 670 + interactionReply: lx.token("User replied to the feed item"), 671 + interactionQuote: lx.token("User quoted the feed item"), 672 + interactionShare: lx.token("User shared the feed item"), 673 + }); 674 + 675 + expect(feedDefs.json.lexicon).toEqual(1); 676 + expect(feedDefs.json.id).toEqual("app.bsky.feed.defs"); 677 + expect(feedDefs.json.defs.postView.type).toEqual("object"); 678 + expect(feedDefs.json.defs.viewerState.type).toEqual("object"); 679 + expect(feedDefs.json.defs.requestLess.type).toEqual("token"); 680 + expect(feedDefs.json.defs.contentModeVideo.type).toEqual("token"); 681 + });
+1226
packages/prototypey/core/tests/from-json-infer.test.ts
··· 1 + import { test } from "vitest"; 2 + import { attest } from "@ark/attest"; 3 + import { fromJSON } from "../lib.ts"; 4 + import { Infer } from "../infer.ts"; 5 + 6 + test("fromJSON InferNS produces expected type shape", () => { 7 + const exampleLexicon = fromJSON({ 8 + id: "com.example.post", 9 + defs: { 10 + main: { 11 + type: "record", 12 + key: "tid", 13 + record: { 14 + type: "object", 15 + properties: { 16 + text: { type: "string", required: true }, 17 + createdAt: { type: "string", required: true, format: "datetime" }, 18 + likes: { type: "integer" }, 19 + tags: { type: "array", items: { type: "string" }, maxLength: 5 }, 20 + }, 21 + required: ["text", "createdAt"], 22 + }, 23 + }, 24 + }, 25 + }); 26 + 27 + // Type snapshot - this captures how types appear on hover 28 + attest(exampleLexicon["~infer"]).type.toString.snap(`{ 29 + $type: "com.example.post" 30 + tags?: string[] | undefined 31 + likes?: number | undefined 32 + createdAt: string 33 + text: string 34 + }`); 35 + }); 36 + 37 + test("fromJSON InferObject handles required fields", () => { 38 + const schema = fromJSON({ 39 + id: "test", 40 + defs: { 41 + main: { 42 + type: "object", 43 + properties: { 44 + required: { type: "string", required: true }, 45 + optional: { type: "string" }, 46 + }, 47 + required: ["required"], 48 + }, 49 + }, 50 + }); 51 + 52 + attest(schema["~infer"]).type.toString.snap(`{ 53 + $type: "test" 54 + optional?: string | undefined 55 + required: string 56 + }`); 57 + }); 58 + 59 + test("fromJSON InferObject handles nullable fields", () => { 60 + const schema = fromJSON({ 61 + id: "test", 62 + defs: { 63 + main: { 64 + type: "object", 65 + properties: { 66 + nullable: { type: "string", nullable: true, required: true }, 67 + }, 68 + required: ["nullable"], 69 + nullable: ["nullable"], 70 + }, 71 + }, 72 + }); 73 + 74 + attest(schema["~infer"]).type.toString.snap( 75 + '{ $type: "test"; nullable: string | null }', 76 + ); 77 + }); 78 + 79 + // ============================================================================ 80 + // PRIMITIVE TYPES TESTS 81 + // ============================================================================ 82 + 83 + test("fromJSON InferType handles string primitive", () => { 84 + const lexicon = fromJSON({ 85 + id: "test.string", 86 + defs: { 87 + main: { 88 + type: "object", 89 + properties: { 90 + simpleString: { type: "string" }, 91 + }, 92 + }, 93 + }, 94 + }); 95 + 96 + attest(lexicon["~infer"]).type.toString.snap(`{ 97 + $type: "test.string" 98 + simpleString?: string | undefined 99 + }`); 100 + }); 101 + 102 + test("fromJSON InferType handles integer primitive", () => { 103 + const lexicon = fromJSON({ 104 + id: "test.integer", 105 + defs: { 106 + main: { 107 + type: "object", 108 + properties: { 109 + count: { type: "integer" }, 110 + age: { type: "integer", minimum: 0, maximum: 120 }, 111 + }, 112 + }, 113 + }, 114 + }); 115 + 116 + attest(lexicon["~infer"]).type.toString.snap(`{ 117 + $type: "test.integer" 118 + count?: number | undefined 119 + age?: number | undefined 120 + }`); 121 + }); 122 + 123 + test("fromJSON InferType handles boolean primitive", () => { 124 + const lexicon = fromJSON({ 125 + id: "test.boolean", 126 + defs: { 127 + main: { 128 + type: "object", 129 + properties: { 130 + isActive: { type: "boolean" }, 131 + hasAccess: { type: "boolean", required: true }, 132 + }, 133 + required: ["hasAccess"], 134 + }, 135 + }, 136 + }); 137 + 138 + attest(lexicon["~infer"]).type.toString.snap(`{ 139 + $type: "test.boolean" 140 + isActive?: boolean | undefined 141 + hasAccess: boolean 142 + }`); 143 + }); 144 + 145 + test("fromJSON InferType handles null primitive", () => { 146 + const lexicon = fromJSON({ 147 + id: "test.null", 148 + defs: { 149 + main: { 150 + type: "object", 151 + properties: { 152 + nullValue: { type: "null" }, 153 + }, 154 + }, 155 + }, 156 + }); 157 + 158 + attest(lexicon["~infer"]).type.toString.snap(`{ 159 + $type: "test.null" 160 + nullValue?: null | undefined 161 + }`); 162 + }); 163 + 164 + test("fromJSON InferType handles unknown primitive", () => { 165 + const lexicon = fromJSON({ 166 + id: "test.unknown", 167 + defs: { 168 + main: { 169 + type: "object", 170 + properties: { 171 + metadata: { type: "unknown" }, 172 + }, 173 + }, 174 + }, 175 + }); 176 + 177 + attest(lexicon["~infer"]).type.toString.snap( 178 + '{ $type: "test.unknown"; metadata?: unknown }', 179 + ); 180 + }); 181 + 182 + test("fromJSON InferType handles bytes primitive", () => { 183 + const lexicon = fromJSON({ 184 + id: "test.bytes", 185 + defs: { 186 + main: { 187 + type: "object", 188 + properties: { 189 + data: { type: "bytes" }, 190 + }, 191 + }, 192 + }, 193 + }); 194 + 195 + attest(lexicon["~infer"]).type.toString.snap(`{ 196 + $type: "test.bytes" 197 + data?: Uint8Array<ArrayBufferLike> | undefined 198 + }`); 199 + }); 200 + 201 + test("fromJSON InferType handles blob primitive", () => { 202 + const lexicon = fromJSON({ 203 + id: "test.blob", 204 + defs: { 205 + main: { 206 + type: "object", 207 + properties: { 208 + image: { 209 + type: "blob", 210 + accept: ["image/png", "image/jpeg"], 211 + }, 212 + }, 213 + }, 214 + }, 215 + }); 216 + 217 + attest(lexicon["~infer"]).type.toString.snap( 218 + '{ $type: "test.blob"; image?: Blob | undefined }', 219 + ); 220 + }); 221 + 222 + // ============================================================================ 223 + // TOKEN TYPE TESTS 224 + // ============================================================================ 225 + 226 + test("fromJSON InferToken handles basic token without enum", () => { 227 + const lexicon = fromJSON({ 228 + id: "test.token", 229 + defs: { 230 + main: { 231 + type: "object", 232 + properties: { 233 + symbol: { type: "token", description: "A symbolic value" }, 234 + }, 235 + }, 236 + }, 237 + }); 238 + 239 + attest(lexicon["~infer"]).type.toString.snap(`{ 240 + $type: "test.token" 241 + symbol?: string | undefined 242 + }`); 243 + }); 244 + 245 + // ============================================================================ 246 + // ARRAY TYPE TESTS 247 + // ============================================================================ 248 + 249 + test("fromJSON InferArray handles string arrays", () => { 250 + const lexicon = fromJSON({ 251 + id: "test.array.string", 252 + defs: { 253 + main: { 254 + type: "object", 255 + properties: { 256 + tags: { type: "array", items: { type: "string" } }, 257 + }, 258 + }, 259 + }, 260 + }); 261 + 262 + attest(lexicon["~infer"]).type.toString.snap(`{ 263 + $type: "test.array.string" 264 + tags?: string[] | undefined 265 + }`); 266 + }); 267 + 268 + test("fromJSON InferArray handles integer arrays", () => { 269 + const lexicon = fromJSON({ 270 + id: "test.array.integer", 271 + defs: { 272 + main: { 273 + type: "object", 274 + properties: { 275 + scores: { 276 + type: "array", 277 + items: { type: "integer" }, 278 + minLength: 1, 279 + maxLength: 10, 280 + }, 281 + }, 282 + }, 283 + }, 284 + }); 285 + 286 + attest(lexicon["~infer"]).type.toString.snap(`{ 287 + $type: "test.array.integer" 288 + scores?: number[] | undefined 289 + }`); 290 + }); 291 + 292 + test("fromJSON InferArray handles boolean arrays", () => { 293 + const lexicon = fromJSON({ 294 + id: "test.array.boolean", 295 + defs: { 296 + main: { 297 + type: "object", 298 + properties: { 299 + flags: { type: "array", items: { type: "boolean" } }, 300 + }, 301 + }, 302 + }, 303 + }); 304 + 305 + attest(lexicon["~infer"]).type.toString.snap(`{ 306 + $type: "test.array.boolean" 307 + flags?: boolean[] | undefined 308 + }`); 309 + }); 310 + 311 + test("fromJSON InferArray handles unknown arrays", () => { 312 + const lexicon = fromJSON({ 313 + id: "test.array.unknown", 314 + defs: { 315 + main: { 316 + type: "object", 317 + properties: { 318 + items: { type: "array", items: { type: "unknown" } }, 319 + }, 320 + }, 321 + }, 322 + }); 323 + 324 + attest(lexicon["~infer"]).type.toString.snap(`{ 325 + $type: "test.array.unknown" 326 + items?: unknown[] | undefined 327 + }`); 328 + }); 329 + 330 + // ============================================================================ 331 + // OBJECT PROPERTY COMBINATIONS 332 + // ============================================================================ 333 + 334 + test("fromJSON InferObject handles mixed optional and required fields", () => { 335 + const lexicon = fromJSON({ 336 + id: "test.mixed", 337 + defs: { 338 + main: { 339 + type: "object", 340 + properties: { 341 + id: { type: "string", required: true }, 342 + name: { type: "string", required: true }, 343 + email: { type: "string" }, 344 + age: { type: "integer" }, 345 + }, 346 + required: ["id", "name"], 347 + }, 348 + }, 349 + }); 350 + 351 + attest(lexicon["~infer"]).type.toString.snap(`{ 352 + $type: "test.mixed" 353 + age?: number | undefined 354 + email?: string | undefined 355 + id: string 356 + name: string 357 + }`); 358 + }); 359 + 360 + test("fromJSON InferObject handles all optional fields", () => { 361 + const lexicon = fromJSON({ 362 + id: "test.allOptional", 363 + defs: { 364 + main: { 365 + type: "object", 366 + properties: { 367 + field1: { type: "string" }, 368 + field2: { type: "integer" }, 369 + field3: { type: "boolean" }, 370 + }, 371 + }, 372 + }, 373 + }); 374 + 375 + attest(lexicon["~infer"]).type.toString.snap(`{ 376 + $type: "test.allOptional" 377 + field1?: string | undefined 378 + field2?: number | undefined 379 + field3?: boolean | undefined 380 + }`); 381 + }); 382 + 383 + test("fromJSON InferObject handles all required fields", () => { 384 + const lexicon = fromJSON({ 385 + id: "test.allRequired", 386 + defs: { 387 + main: { 388 + type: "object", 389 + properties: { 390 + field1: { type: "string", required: true }, 391 + field2: { type: "integer", required: true }, 392 + field3: { type: "boolean", required: true }, 393 + }, 394 + required: ["field1", "field2", "field3"], 395 + }, 396 + }, 397 + }); 398 + 399 + attest(lexicon["~infer"]).type.toString.snap(`{ 400 + $type: "test.allRequired" 401 + field1: string 402 + field2: number 403 + field3: boolean 404 + }`); 405 + }); 406 + 407 + // ============================================================================ 408 + // NULLABLE FIELDS TESTS 409 + // ============================================================================ 410 + 411 + test("fromJSON InferObject handles nullable optional field", () => { 412 + const lexicon = fromJSON({ 413 + id: "test.nullableOptional", 414 + defs: { 415 + main: { 416 + type: "object", 417 + properties: { 418 + description: { type: "string", nullable: true }, 419 + }, 420 + nullable: ["description"], 421 + }, 422 + }, 423 + }); 424 + 425 + attest(lexicon["~infer"]).type.toString.snap(`{ 426 + $type: "test.nullableOptional" 427 + description?: string | null | undefined 428 + }`); 429 + }); 430 + 431 + test("fromJSON InferObject handles multiple nullable fields", () => { 432 + const lexicon = fromJSON({ 433 + id: "test.multipleNullable", 434 + defs: { 435 + main: { 436 + type: "object", 437 + properties: { 438 + field1: { type: "string", nullable: true }, 439 + field2: { type: "integer", nullable: true }, 440 + field3: { type: "boolean", nullable: true }, 441 + }, 442 + nullable: ["field1", "field2", "field3"], 443 + }, 444 + }, 445 + }); 446 + 447 + attest(lexicon["~infer"]).type.toString.snap(`{ 448 + $type: "test.multipleNullable" 449 + field1?: string | null | undefined 450 + field2?: number | null | undefined 451 + field3?: boolean | null | undefined 452 + }`); 453 + }); 454 + 455 + test("fromJSON InferObject handles nullable and required field", () => { 456 + const lexicon = fromJSON({ 457 + id: "test.nullableRequired", 458 + defs: { 459 + main: { 460 + type: "object", 461 + properties: { 462 + value: { type: "string", nullable: true, required: true }, 463 + }, 464 + required: ["value"], 465 + nullable: ["value"], 466 + }, 467 + }, 468 + }); 469 + 470 + attest(lexicon["~infer"]).type.toString.snap(`{ 471 + $type: "test.nullableRequired" 472 + value: string | null 473 + }`); 474 + }); 475 + 476 + test("fromJSON InferObject handles mixed nullable, required, and optional", () => { 477 + const lexicon = fromJSON({ 478 + id: "test.mixedNullable", 479 + defs: { 480 + main: { 481 + type: "object", 482 + properties: { 483 + requiredNullable: { type: "string", required: true, nullable: true }, 484 + optionalNullable: { type: "string", nullable: true }, 485 + required: { type: "string", required: true }, 486 + optional: { type: "string" }, 487 + }, 488 + required: ["requiredNullable", "required"], 489 + nullable: ["requiredNullable", "optionalNullable"], 490 + }, 491 + }, 492 + }); 493 + 494 + attest(lexicon["~infer"]).type.toString.snap(`{ 495 + $type: "test.mixedNullable" 496 + optional?: string | undefined 497 + required: string 498 + optionalNullable?: string | null | undefined 499 + requiredNullable: string | null 500 + }`); 501 + }); 502 + 503 + // ============================================================================ 504 + // REF TYPE TESTS 505 + // ============================================================================ 506 + 507 + test("fromJSON InferRef handles external reference (unknown)", () => { 508 + const lexicon = fromJSON({ 509 + id: "test.ref", 510 + defs: { 511 + main: { 512 + type: "object", 513 + properties: { 514 + post: { type: "ref", ref: "com.example.post" }, 515 + }, 516 + }, 517 + }, 518 + }); 519 + 520 + attest(lexicon["~infer"]).type.toString.snap(`{ 521 + $type: "test.ref" 522 + post?: 523 + | { [x: string]: unknown; $type: "com.example.post" } 524 + | undefined 525 + }`); 526 + }); 527 + 528 + // ============================================================================ 529 + // UNION TYPE TESTS 530 + // ============================================================================ 531 + 532 + test("fromJSON InferUnion handles external union (unknown)", () => { 533 + const lexicon = fromJSON({ 534 + id: "test.union", 535 + defs: { 536 + main: { 537 + type: "object", 538 + properties: { 539 + content: { 540 + type: "union", 541 + refs: ["com.example.text", "com.example.image"], 542 + }, 543 + }, 544 + }, 545 + }, 546 + }); 547 + 548 + attest(lexicon["~infer"]).type.toString.snap(`{ 549 + $type: "test.union" 550 + content?: 551 + | { [x: string]: unknown; $type: "com.example.text" } 552 + | { [x: string]: unknown; $type: "com.example.image" } 553 + | undefined 554 + }`); 555 + }); 556 + 557 + // ============================================================================ 558 + // PARAMS TYPE TESTS 559 + // ============================================================================ 560 + 561 + test("fromJSON InferParams handles basic params", () => { 562 + const lexicon = fromJSON({ 563 + id: "test.params", 564 + defs: { 565 + main: { 566 + type: "params", 567 + properties: { 568 + limit: { type: "integer" }, 569 + offset: { type: "integer" }, 570 + }, 571 + }, 572 + }, 573 + }); 574 + 575 + attest(lexicon["~infer"]).type.toString.snap(`{ 576 + $type: "test.params" 577 + limit?: number | undefined 578 + offset?: number | undefined 579 + }`); 580 + }); 581 + 582 + test("fromJSON InferParams handles required params", () => { 583 + const lexicon = fromJSON({ 584 + id: "test.paramsRequired", 585 + defs: { 586 + main: { 587 + type: "params", 588 + properties: { 589 + query: { type: "string", required: true }, 590 + limit: { type: "integer" }, 591 + }, 592 + required: ["query"], 593 + }, 594 + }, 595 + }); 596 + 597 + attest(lexicon["~infer"]).type.toString.snap(`{ 598 + $type: "test.paramsRequired" 599 + limit?: number | undefined 600 + query: string 601 + }`); 602 + }); 603 + 604 + // ============================================================================ 605 + // RECORD TYPE TESTS 606 + // ============================================================================ 607 + 608 + test("fromJSON InferRecord handles record with object schema", () => { 609 + const lexicon = fromJSON({ 610 + id: "test.record", 611 + defs: { 612 + main: { 613 + type: "record", 614 + key: "tid", 615 + record: { 616 + type: "object", 617 + properties: { 618 + title: { type: "string", required: true }, 619 + content: { type: "string", required: true }, 620 + published: { type: "boolean" }, 621 + }, 622 + required: ["title", "content"], 623 + }, 624 + }, 625 + }, 626 + }); 627 + 628 + attest(lexicon["~infer"]).type.toString.snap(`{ 629 + $type: "test.record" 630 + published?: boolean | undefined 631 + content: string 632 + title: string 633 + }`); 634 + }); 635 + 636 + // ============================================================================ 637 + // NESTED OBJECTS TESTS 638 + // ============================================================================ 639 + 640 + test("fromJSON InferObject handles nested objects", () => { 641 + const lexicon = fromJSON({ 642 + id: "test.nested", 643 + defs: { 644 + main: { 645 + type: "object", 646 + properties: { 647 + user: { 648 + type: "object", 649 + properties: { 650 + name: { type: "string", required: true }, 651 + email: { type: "string", required: true }, 652 + }, 653 + required: ["name", "email"], 654 + }, 655 + }, 656 + }, 657 + }, 658 + }); 659 + 660 + attest(lexicon["~infer"]).type.toString.snap(`{ 661 + $type: "test.nested" 662 + user?: { name: string; email: string } | undefined 663 + }`); 664 + }); 665 + 666 + test("fromJSON InferObject handles deeply nested objects", () => { 667 + const lexicon = fromJSON({ 668 + id: "test.deepNested", 669 + defs: { 670 + main: { 671 + type: "object", 672 + properties: { 673 + data: { 674 + type: "object", 675 + properties: { 676 + user: { 677 + type: "object", 678 + properties: { 679 + profile: { 680 + type: "object", 681 + properties: { 682 + name: { type: "string", required: true }, 683 + }, 684 + required: ["name"], 685 + }, 686 + }, 687 + }, 688 + }, 689 + }, 690 + }, 691 + }, 692 + }, 693 + }); 694 + 695 + attest(lexicon["~infer"]).type.toString.snap(`{ 696 + $type: "test.deepNested" 697 + data?: 698 + | { 699 + user?: 700 + | { profile?: { name: string } | undefined } 701 + | undefined 702 + } 703 + | undefined 704 + }`); 705 + }); 706 + 707 + // ============================================================================ 708 + // NESTED ARRAYS TESTS 709 + // ============================================================================ 710 + 711 + test("fromJSON InferArray handles arrays of objects", () => { 712 + const lexicon = fromJSON({ 713 + id: "test.arrayOfObjects", 714 + defs: { 715 + main: { 716 + type: "object", 717 + properties: { 718 + users: { 719 + type: "array", 720 + items: { 721 + type: "object", 722 + properties: { 723 + id: { type: "string", required: true }, 724 + name: { type: "string", required: true }, 725 + }, 726 + required: ["id", "name"], 727 + }, 728 + }, 729 + }, 730 + }, 731 + }, 732 + }); 733 + 734 + attest(lexicon["~infer"]).type.toString.snap(`{ 735 + $type: "test.arrayOfObjects" 736 + users?: { id: string; name: string }[] | undefined 737 + }`); 738 + }); 739 + 740 + test("fromJSON InferArray handles arrays of arrays", () => { 741 + const lexicon = fromJSON({ 742 + id: "test.nestedArrays", 743 + defs: { 744 + main: { 745 + type: "object", 746 + properties: { 747 + matrix: { 748 + type: "array", 749 + items: { type: "array", items: { type: "integer" } }, 750 + }, 751 + }, 752 + }, 753 + }, 754 + }); 755 + 756 + attest(lexicon["~infer"]).type.toString.snap(`{ 757 + $type: "test.nestedArrays" 758 + matrix?: number[][] | undefined 759 + }`); 760 + }); 761 + 762 + test("fromJSON InferArray handles arrays of refs", () => { 763 + const lexicon = fromJSON({ 764 + id: "test.arrayOfRefs", 765 + defs: { 766 + user: { 767 + type: "object", 768 + properties: { 769 + name: { type: "string", required: true }, 770 + handle: { type: "string", required: true }, 771 + }, 772 + required: ["name", "handle"], 773 + }, 774 + main: { 775 + type: "object", 776 + properties: { 777 + followers: { 778 + type: "array", 779 + items: { type: "ref", ref: "#user" }, 780 + }, 781 + }, 782 + }, 783 + }, 784 + }); 785 + 786 + attest(lexicon["~infer"]).type.toString.snap(`{ 787 + $type: "test.arrayOfRefs" 788 + followers?: 789 + | { handle: string; name: string; $type: "#user" }[] 790 + | undefined 791 + }`); 792 + }); 793 + 794 + // ============================================================================ 795 + // COMPLEX NESTED STRUCTURES 796 + // ============================================================================ 797 + 798 + test("fromJSON InferObject handles complex nested structure", () => { 799 + const lexicon = fromJSON({ 800 + id: "test.complex", 801 + defs: { 802 + text: { 803 + type: "object", 804 + properties: { 805 + content: { type: "string", required: true }, 806 + }, 807 + required: ["content"], 808 + }, 809 + image: { 810 + type: "object", 811 + properties: { 812 + url: { type: "string", required: true }, 813 + alt: { type: "string" }, 814 + }, 815 + required: ["url"], 816 + }, 817 + main: { 818 + type: "object", 819 + properties: { 820 + id: { type: "string", required: true }, 821 + author: { 822 + type: "object", 823 + properties: { 824 + did: { type: "string", required: true, format: "did" }, 825 + handle: { type: "string", required: true, format: "handle" }, 826 + avatar: { type: "string" }, 827 + }, 828 + required: ["did", "handle"], 829 + }, 830 + content: { 831 + type: "union", 832 + refs: ["#text", "#image"], 833 + }, 834 + tags: { type: "array", items: { type: "string" }, maxLength: 10 }, 835 + metadata: { 836 + type: "object", 837 + properties: { 838 + views: { type: "integer" }, 839 + likes: { type: "integer" }, 840 + shares: { type: "integer" }, 841 + }, 842 + }, 843 + }, 844 + required: ["id"], 845 + }, 846 + }, 847 + }); 848 + 849 + attest(lexicon["~infer"]).type.toString.snap(`{ 850 + $type: "test.complex" 851 + tags?: string[] | undefined 852 + content?: 853 + | { content: string; $type: "#text" } 854 + | { 855 + alt?: string | undefined 856 + url: string 857 + $type: "#image" 858 + } 859 + | undefined 860 + author?: 861 + | { 862 + avatar?: string | undefined 863 + did: string 864 + handle: string 865 + } 866 + | undefined 867 + metadata?: 868 + | { 869 + likes?: number | undefined 870 + views?: number | undefined 871 + shares?: number | undefined 872 + } 873 + | undefined 874 + id: string 875 + }`); 876 + }); 877 + 878 + // ============================================================================ 879 + // MULTIPLE DEFS IN NAMESPACE 880 + // ============================================================================ 881 + 882 + test("fromJSON InferNS handles multiple defs in namespace", () => { 883 + const lexicon = fromJSON({ 884 + id: "com.example.app", 885 + defs: { 886 + user: { 887 + type: "object", 888 + properties: { 889 + name: { type: "string", required: true }, 890 + email: { type: "string", required: true }, 891 + }, 892 + required: ["name", "email"], 893 + }, 894 + post: { 895 + type: "object", 896 + properties: { 897 + title: { type: "string", required: true }, 898 + content: { type: "string", required: true }, 899 + }, 900 + required: ["title", "content"], 901 + }, 902 + comment: { 903 + type: "object", 904 + properties: { 905 + text: { type: "string", required: true }, 906 + author: { type: "ref", ref: "#user" }, 907 + }, 908 + required: ["text"], 909 + }, 910 + }, 911 + }); 912 + 913 + attest(lexicon["~infer"]).type.toString.snap("never"); 914 + }); 915 + 916 + test("fromJSON InferNS handles namespace with record and object defs", () => { 917 + const lexicon = fromJSON({ 918 + id: "com.example.blog", 919 + defs: { 920 + main: { 921 + type: "record", 922 + key: "tid", 923 + record: { 924 + type: "object", 925 + properties: { 926 + title: { type: "string", required: true }, 927 + body: { type: "string", required: true }, 928 + }, 929 + required: ["title", "body"], 930 + }, 931 + }, 932 + metadata: { 933 + type: "object", 934 + properties: { 935 + category: { type: "string" }, 936 + tags: { type: "array", items: { type: "string" } }, 937 + }, 938 + }, 939 + }, 940 + }); 941 + 942 + attest(lexicon["~infer"]).type.toString.snap(`{ 943 + $type: "com.example.blog" 944 + title: string 945 + body: string 946 + }`); 947 + }); 948 + 949 + // ============================================================================ 950 + // LOCAL REF RESOLUTION TESTS 951 + // ============================================================================ 952 + 953 + test("fromJSON Local ref resolution: resolves refs to actual types", () => { 954 + const ns = fromJSON({ 955 + id: "test", 956 + defs: { 957 + user: { 958 + type: "object", 959 + properties: { 960 + name: { type: "string", required: true }, 961 + email: { type: "string", required: true }, 962 + }, 963 + required: ["name", "email"], 964 + }, 965 + main: { 966 + type: "object", 967 + properties: { 968 + author: { type: "ref", ref: "#user", required: true }, 969 + content: { type: "string", required: true }, 970 + }, 971 + required: ["author", "content"], 972 + }, 973 + }, 974 + }); 975 + 976 + attest(ns["~infer"]).type.toString.snap(`{ 977 + $type: "test" 978 + content: string 979 + author: { name: string; email: string; $type: "#user" } 980 + }`); 981 + }); 982 + 983 + test("fromJSON Local ref resolution: refs in arrays", () => { 984 + const ns = fromJSON({ 985 + id: "test", 986 + defs: { 987 + user: { 988 + type: "object", 989 + properties: { 990 + name: { type: "string", required: true }, 991 + }, 992 + required: ["name"], 993 + }, 994 + main: { 995 + type: "object", 996 + properties: { 997 + users: { type: "array", items: { type: "ref", ref: "#user" } }, 998 + }, 999 + }, 1000 + }, 1001 + }); 1002 + 1003 + attest(ns["~infer"]).type.toString.snap(`{ 1004 + $type: "test" 1005 + users?: { name: string; $type: "#user" }[] | undefined 1006 + }`); 1007 + }); 1008 + 1009 + test("fromJSON Local ref resolution: refs in unions", () => { 1010 + const ns = fromJSON({ 1011 + id: "test", 1012 + defs: { 1013 + text: { 1014 + type: "object", 1015 + properties: { content: { type: "string", required: true } }, 1016 + required: ["content"], 1017 + }, 1018 + image: { 1019 + type: "object", 1020 + properties: { url: { type: "string", required: true } }, 1021 + required: ["url"], 1022 + }, 1023 + main: { 1024 + type: "object", 1025 + properties: { 1026 + embed: { type: "union", refs: ["#text", "#image"] }, 1027 + }, 1028 + }, 1029 + }, 1030 + }); 1031 + 1032 + attest(ns["~infer"]).type.toString.snap(`{ 1033 + $type: "test" 1034 + embed?: 1035 + | { content: string; $type: "#text" } 1036 + | { url: string; $type: "#image" } 1037 + | undefined 1038 + }`); 1039 + }); 1040 + 1041 + test("fromJSON Local ref resolution: nested refs", () => { 1042 + const ns = fromJSON({ 1043 + id: "test", 1044 + defs: { 1045 + profile: { 1046 + type: "object", 1047 + properties: { 1048 + bio: { type: "string", required: true }, 1049 + }, 1050 + required: ["bio"], 1051 + }, 1052 + user: { 1053 + type: "object", 1054 + properties: { 1055 + name: { type: "string", required: true }, 1056 + profile: { type: "ref", ref: "#profile", required: true }, 1057 + }, 1058 + required: ["name", "profile"], 1059 + }, 1060 + main: { 1061 + type: "object", 1062 + properties: { 1063 + author: { type: "ref", ref: "#user", required: true }, 1064 + }, 1065 + required: ["author"], 1066 + }, 1067 + }, 1068 + }); 1069 + 1070 + attest(ns["~infer"]).type.toString.snap(`{ 1071 + $type: "test" 1072 + author: { 1073 + name: string 1074 + profile: { bio: string; $type: "#profile" } 1075 + $type: "#user" 1076 + } 1077 + }`); 1078 + }); 1079 + 1080 + // ============================================================================ 1081 + // EDGE CASE TESTS 1082 + // ============================================================================ 1083 + 1084 + test("fromJSON Edge case: circular reference detection", () => { 1085 + const ns = fromJSON({ 1086 + id: "test", 1087 + defs: { 1088 + main: { 1089 + type: "object", 1090 + properties: { 1091 + value: { type: "string", required: true }, 1092 + parent: { type: "ref", ref: "#main" }, 1093 + }, 1094 + required: ["value"], 1095 + }, 1096 + }, 1097 + }); 1098 + 1099 + attest(ns["~infer"]).type.toString.snap(`{ 1100 + $type: "test" 1101 + parent?: 1102 + | { 1103 + parent?: 1104 + | "[Circular reference detected: #main]" 1105 + | undefined 1106 + value: string 1107 + $type: "#main" 1108 + } 1109 + | undefined 1110 + value: string 1111 + }`); 1112 + }); 1113 + 1114 + test("fromJSON Edge case: circular reference between multiple types", () => { 1115 + const ns = fromJSON({ 1116 + id: "test", 1117 + defs: { 1118 + user: { 1119 + type: "object", 1120 + properties: { 1121 + name: { type: "string", required: true }, 1122 + posts: { type: "array", items: { type: "ref", ref: "#post" } }, 1123 + }, 1124 + required: ["name"], 1125 + }, 1126 + post: { 1127 + type: "object", 1128 + properties: { 1129 + title: { type: "string", required: true }, 1130 + author: { type: "ref", ref: "#user", required: true }, 1131 + }, 1132 + required: ["title", "author"], 1133 + }, 1134 + main: { 1135 + type: "object", 1136 + properties: { 1137 + users: { type: "array", items: { type: "ref", ref: "#user" } }, 1138 + }, 1139 + }, 1140 + }, 1141 + }); 1142 + 1143 + attest(ns["~infer"]).type.toString.snap(`{ 1144 + $type: "test" 1145 + users?: 1146 + | { 1147 + posts?: 1148 + | { 1149 + author: "[Circular reference detected: #user]" 1150 + title: string 1151 + $type: "#post" 1152 + }[] 1153 + | undefined 1154 + name: string 1155 + $type: "#user" 1156 + }[] 1157 + | undefined 1158 + }`); 1159 + }); 1160 + 1161 + test("fromJSON Edge case: missing reference detection", () => { 1162 + const ns = fromJSON({ 1163 + id: "test", 1164 + defs: { 1165 + main: { 1166 + type: "object", 1167 + properties: { 1168 + author: { type: "ref", ref: "#user", required: true }, 1169 + }, 1170 + required: ["author"], 1171 + }, 1172 + }, 1173 + }); 1174 + 1175 + attest(ns["~infer"]).type.toString.snap(`{ 1176 + $type: "test" 1177 + author: "[Reference not found: #user]" 1178 + }`); 1179 + }); 1180 + 1181 + // ============================================================================ 1182 + // REAL-WORLD EXAMPLE: BLUESKY PROFILE 1183 + // ============================================================================ 1184 + 1185 + test("fromJSON Real-world example: app.bsky.actor.profile", () => { 1186 + const lexicon = fromJSON({ 1187 + id: "app.bsky.actor.profile", 1188 + defs: { 1189 + main: { 1190 + type: "record", 1191 + key: "self", 1192 + record: { 1193 + type: "object", 1194 + properties: { 1195 + displayName: { 1196 + type: "string", 1197 + maxLength: 64, 1198 + maxGraphemes: 64, 1199 + }, 1200 + description: { 1201 + type: "string", 1202 + maxLength: 256, 1203 + maxGraphemes: 256, 1204 + }, 1205 + }, 1206 + }, 1207 + }, 1208 + }, 1209 + }); 1210 + 1211 + type Profile = Infer<typeof lexicon>; 1212 + 1213 + const george: Profile = { 1214 + $type: "app.bsky.actor.profile", 1215 + description: "George", 1216 + }; 1217 + 1218 + lexicon.validate({ foo: "bar" }); // will fail 1219 + lexicon.validate(george); // will pass ๐ŸŽ‰ 1220 + 1221 + attest(lexicon["~infer"]).type.toString.snap(`{ 1222 + $type: "app.bsky.actor.profile" 1223 + description?: string | undefined 1224 + displayName?: string | undefined 1225 + }`); 1226 + });
+136
packages/prototypey/core/tests/from-json.test.ts
··· 1 + import { expect, test } from "vitest"; 2 + import { fromJSON, lx } from "../lib.ts"; 3 + 4 + test("fromJSON creates lexicon from JSON", () => { 5 + const lexicon = fromJSON({ 6 + id: "app.bsky.actor.profile", 7 + defs: { 8 + main: { 9 + type: "record", 10 + key: "self", 11 + record: { 12 + type: "object", 13 + properties: { 14 + displayName: { 15 + type: "string", 16 + maxLength: 64, 17 + maxGraphemes: 64, 18 + }, 19 + description: { 20 + type: "string", 21 + maxLength: 256, 22 + maxGraphemes: 256, 23 + }, 24 + }, 25 + }, 26 + }, 27 + }, 28 + }); 29 + 30 + expect(lexicon.json).toEqual({ 31 + lexicon: 1, 32 + id: "app.bsky.actor.profile", 33 + defs: { 34 + main: { 35 + type: "record", 36 + key: "self", 37 + record: { 38 + type: "object", 39 + properties: { 40 + displayName: { 41 + type: "string", 42 + maxLength: 64, 43 + maxGraphemes: 64, 44 + }, 45 + description: { 46 + type: "string", 47 + maxLength: 256, 48 + maxGraphemes: 256, 49 + }, 50 + }, 51 + }, 52 + }, 53 + }, 54 + }); 55 + }); 56 + 57 + test("fromJSON and lx.lexicon produce equivalent results", () => { 58 + // Create using lx.lexicon 59 + const viaLx = lx.lexicon("app.bsky.feed.post", { 60 + main: lx.record({ 61 + key: "tid", 62 + record: lx.object({ 63 + text: lx.string({ maxLength: 300, required: true }), 64 + createdAt: lx.string({ format: "datetime", required: true }), 65 + }), 66 + }), 67 + }); 68 + 69 + // Create using fromJSON 70 + const viaJSON = fromJSON({ 71 + id: "app.bsky.feed.post", 72 + defs: { 73 + main: { 74 + type: "record", 75 + key: "tid", 76 + record: { 77 + type: "object", 78 + properties: { 79 + text: { 80 + type: "string", 81 + maxLength: 300, 82 + required: true, 83 + }, 84 + createdAt: { 85 + type: "string", 86 + format: "datetime", 87 + required: true, 88 + }, 89 + }, 90 + required: ["text", "createdAt"], 91 + }, 92 + }, 93 + }, 94 + }); 95 + 96 + expect(viaLx.json).toEqual(viaJSON.json); 97 + }); 98 + 99 + test("fromJSON supports validation", () => { 100 + const lexicon = fromJSON({ 101 + id: "com.example.post", 102 + defs: { 103 + main: { 104 + type: "record", 105 + key: "tid", 106 + record: { 107 + type: "object", 108 + properties: { 109 + text: { 110 + type: "string", 111 + maxLength: 100, 112 + required: true, 113 + }, 114 + }, 115 + required: ["text"], 116 + }, 117 + }, 118 + }, 119 + }); 120 + 121 + // Valid data 122 + const validResult = lexicon.validate({ 123 + text: "Hello world", 124 + }); 125 + expect(validResult.success).toBe(true); 126 + 127 + // Invalid data - missing required field 128 + const invalidResult = lexicon.validate({}); 129 + expect(invalidResult.success).toBe(false); 130 + 131 + // Invalid data - text too long 132 + const tooLongResult = lexicon.validate({ 133 + text: "a".repeat(101), 134 + }); 135 + expect(tooLongResult.success).toBe(false); 136 + });
+328
packages/prototypey/core/tests/infer.bench.ts
··· 1 + import { bench } from "@ark/attest"; 2 + import { lx } from "../lib.ts"; 3 + import { fromJSON } from "../lib.ts"; 4 + 5 + bench("infer with simple object", () => { 6 + const schema = lx.lexicon("test.simple", { 7 + main: lx.object({ 8 + id: lx.string({ required: true }), 9 + name: lx.string({ required: true }), 10 + }), 11 + }); 12 + return schema["~infer"]; 13 + }).types([685, "instantiations"]); 14 + 15 + bench("infer with complex nested structure", () => { 16 + const schema = lx.lexicon("test.complex", { 17 + user: lx.object({ 18 + handle: lx.string({ required: true }), 19 + displayName: lx.string(), 20 + }), 21 + reply: lx.object({ 22 + text: lx.string({ required: true }), 23 + author: lx.ref("#user", { required: true }), 24 + }), 25 + main: lx.record({ 26 + key: "tid", 27 + record: lx.object({ 28 + author: lx.ref("#user", { required: true }), 29 + replies: lx.array(lx.ref("#reply")), 30 + content: lx.string({ required: true }), 31 + createdAt: lx.string({ required: true, format: "datetime" }), 32 + }), 33 + }), 34 + }); 35 + return schema["~infer"]; 36 + }).types([956, "instantiations"]); 37 + 38 + bench("infer with circular reference", () => { 39 + const ns = lx.lexicon("test", { 40 + user: lx.object({ 41 + name: lx.string({ required: true }), 42 + posts: lx.array(lx.ref("#post")), 43 + }), 44 + post: lx.object({ 45 + title: lx.string({ required: true }), 46 + author: lx.ref("#user", { required: true }), 47 + }), 48 + main: lx.object({ 49 + users: lx.array(lx.ref("#user")), 50 + }), 51 + }); 52 + return ns["~infer"]; 53 + }).types([634, "instantiations"]); 54 + 55 + bench("infer with app.bsky.feed.defs lexicon", () => { 56 + const schema = lx.lexicon("app.bsky.feed.defs", { 57 + viewerState: lx.object({ 58 + repost: lx.string({ format: "at-uri" }), 59 + like: lx.string({ format: "at-uri" }), 60 + bookmarked: lx.boolean(), 61 + threadMuted: lx.boolean(), 62 + replyDisabled: lx.boolean(), 63 + embeddingDisabled: lx.boolean(), 64 + pinned: lx.boolean(), 65 + }), 66 + main: lx.object({ 67 + uri: lx.string({ required: true, format: "at-uri" }), 68 + cid: lx.string({ required: true, format: "cid" }), 69 + author: lx.ref("app.bsky.actor.defs#profileViewBasic", { 70 + required: true, 71 + }), 72 + record: lx.unknown({ required: true }), 73 + embed: lx.union([ 74 + "app.bsky.embed.images#view", 75 + "app.bsky.embed.video#view", 76 + "app.bsky.embed.external#view", 77 + "app.bsky.embed.record#view", 78 + "app.bsky.embed.recordWithMedia#view", 79 + ]), 80 + bookmarkCount: lx.integer(), 81 + replyCount: lx.integer(), 82 + repostCount: lx.integer(), 83 + likeCount: lx.integer(), 84 + quoteCount: lx.integer(), 85 + indexedAt: lx.string({ required: true, format: "datetime" }), 86 + viewer: lx.ref("#viewerState"), 87 + labels: lx.array(lx.ref("com.atproto.label.defs#label")), 88 + threadgate: lx.ref("#threadgateView"), 89 + }), 90 + requestLess: lx.token( 91 + "Request that less content like the given feed item be shown in the feed", 92 + ), 93 + requestMore: lx.token( 94 + "Request that more content like the given feed item be shown in the feed", 95 + ), 96 + clickthroughItem: lx.token("User clicked through to the feed item"), 97 + clickthroughAuthor: lx.token( 98 + "User clicked through to the author of the feed item", 99 + ), 100 + clickthroughReposter: lx.token( 101 + "User clicked through to the reposter of the feed item", 102 + ), 103 + clickthroughEmbed: lx.token( 104 + "User clicked through to the embedded content of the feed item", 105 + ), 106 + contentModeUnspecified: lx.token( 107 + "Declares the feed generator returns any types of posts.", 108 + ), 109 + contentModeVideo: lx.token( 110 + "Declares the feed generator returns posts containing app.bsky.embed.video embeds.", 111 + ), 112 + interactionSeen: lx.token("Feed item was seen by user"), 113 + interactionLike: lx.token("User liked the feed item"), 114 + interactionRepost: lx.token("User reposted the feed item"), 115 + interactionReply: lx.token("User replied to the feed item"), 116 + interactionQuote: lx.token("User quoted the feed item"), 117 + interactionShare: lx.token("User shared the feed item"), 118 + }); 119 + return schema["~infer"]; 120 + }).types([1237, "instantiations"]); 121 + 122 + bench("fromJSON infer with simple object", () => { 123 + const schema = fromJSON({ 124 + id: "test.simple", 125 + defs: { 126 + main: { 127 + type: "object", 128 + properties: { 129 + id: { type: "string", required: true }, 130 + name: { type: "string", required: true }, 131 + }, 132 + required: ["id", "name"], 133 + }, 134 + }, 135 + }); 136 + return schema["~infer"]; 137 + }).types([438, "instantiations"]); 138 + 139 + bench("fromJSON infer with complex nested structure", () => { 140 + const schema = fromJSON({ 141 + id: "test.complex", 142 + defs: { 143 + user: { 144 + type: "object", 145 + properties: { 146 + handle: { type: "string", required: true }, 147 + displayName: { type: "string" }, 148 + }, 149 + required: ["handle"], 150 + }, 151 + reply: { 152 + type: "object", 153 + properties: { 154 + text: { type: "string", required: true }, 155 + author: { type: "ref", ref: "#user", required: true }, 156 + }, 157 + required: ["text", "author"], 158 + }, 159 + main: { 160 + type: "record", 161 + key: "tid", 162 + record: { 163 + type: "object", 164 + properties: { 165 + author: { type: "ref", ref: "#user", required: true }, 166 + replies: { type: "array", items: { type: "ref", ref: "#reply" } }, 167 + content: { type: "string", required: true }, 168 + createdAt: { 169 + type: "string", 170 + required: true, 171 + format: "datetime", 172 + }, 173 + }, 174 + required: ["author", "content", "createdAt"], 175 + }, 176 + }, 177 + }, 178 + }); 179 + return schema["~infer"]; 180 + }).types([499, "instantiations"]); 181 + 182 + bench("fromJSON infer with circular reference", () => { 183 + const ns = fromJSON({ 184 + id: "test", 185 + defs: { 186 + user: { 187 + type: "object", 188 + properties: { 189 + name: { type: "string", required: true }, 190 + posts: { type: "array", items: { type: "ref", ref: "#post" } }, 191 + }, 192 + required: ["name"], 193 + }, 194 + post: { 195 + type: "object", 196 + properties: { 197 + title: { type: "string", required: true }, 198 + author: { type: "ref", ref: "#user", required: true }, 199 + }, 200 + required: ["title", "author"], 201 + }, 202 + main: { 203 + type: "object", 204 + properties: { 205 + users: { type: "array", items: { type: "ref", ref: "#user" } }, 206 + }, 207 + }, 208 + }, 209 + }); 210 + return ns["~infer"]; 211 + }).types([411, "instantiations"]); 212 + 213 + bench("fromJSON infer with app.bsky.feed.defs lexicon", () => { 214 + const schema = fromJSON({ 215 + id: "app.bsky.feed.defs", 216 + defs: { 217 + viewerState: { 218 + type: "object", 219 + properties: { 220 + repost: { type: "string", format: "at-uri" }, 221 + like: { type: "string", format: "at-uri" }, 222 + bookmarked: { type: "boolean" }, 223 + threadMuted: { type: "boolean" }, 224 + replyDisabled: { type: "boolean" }, 225 + embeddingDisabled: { type: "boolean" }, 226 + pinned: { type: "boolean" }, 227 + }, 228 + }, 229 + main: { 230 + type: "object", 231 + properties: { 232 + uri: { type: "string", required: true, format: "at-uri" }, 233 + cid: { type: "string", required: true, format: "cid" }, 234 + author: { 235 + type: "ref", 236 + ref: "app.bsky.actor.defs#profileViewBasic", 237 + required: true, 238 + }, 239 + record: { type: "unknown", required: true }, 240 + embed: { 241 + type: "union", 242 + refs: [ 243 + "app.bsky.embed.images#view", 244 + "app.bsky.embed.video#view", 245 + "app.bsky.embed.external#view", 246 + "app.bsky.embed.record#view", 247 + "app.bsky.embed.recordWithMedia#view", 248 + ], 249 + }, 250 + bookmarkCount: { type: "integer" }, 251 + replyCount: { type: "integer" }, 252 + repostCount: { type: "integer" }, 253 + likeCount: { type: "integer" }, 254 + quoteCount: { type: "integer" }, 255 + indexedAt: { type: "string", required: true, format: "datetime" }, 256 + viewer: { type: "ref", ref: "#viewerState" }, 257 + labels: { 258 + type: "array", 259 + items: { type: "ref", ref: "com.atproto.label.defs#label" }, 260 + }, 261 + threadgate: { type: "ref", ref: "#threadgateView" }, 262 + }, 263 + required: ["uri", "cid", "author", "record", "indexedAt"], 264 + }, 265 + requestLess: { 266 + type: "token", 267 + description: 268 + "Request that less content like the given feed item be shown in the feed", 269 + }, 270 + requestMore: { 271 + type: "token", 272 + description: 273 + "Request that more content like the given feed item be shown in the feed", 274 + }, 275 + clickthroughItem: { 276 + type: "token", 277 + description: "User clicked through to the feed item", 278 + }, 279 + clickthroughAuthor: { 280 + type: "token", 281 + description: "User clicked through to the author of the feed item", 282 + }, 283 + clickthroughReposter: { 284 + type: "token", 285 + description: "User clicked through to the reposter of the feed item", 286 + }, 287 + clickthroughEmbed: { 288 + type: "token", 289 + description: 290 + "User clicked through to the embedded content of the feed item", 291 + }, 292 + contentModeUnspecified: { 293 + type: "token", 294 + description: "Declares the feed generator returns any types of posts.", 295 + }, 296 + contentModeVideo: { 297 + type: "token", 298 + description: 299 + "Declares the feed generator returns posts containing app.bsky.embed.video embeds.", 300 + }, 301 + interactionSeen: { 302 + type: "token", 303 + description: "Feed item was seen by user", 304 + }, 305 + interactionLike: { 306 + type: "token", 307 + description: "User liked the feed item", 308 + }, 309 + interactionRepost: { 310 + type: "token", 311 + description: "User reposted the feed item", 312 + }, 313 + interactionReply: { 314 + type: "token", 315 + description: "User replied to the feed item", 316 + }, 317 + interactionQuote: { 318 + type: "token", 319 + description: "User quoted the feed item", 320 + }, 321 + interactionShare: { 322 + type: "token", 323 + description: "User shared the feed item", 324 + }, 325 + }, 326 + }); 327 + return schema["~infer"]; 328 + }).types([513, "instantiations"]);
+868
packages/prototypey/core/tests/infer.test.ts
··· 1 + import { test } from "vitest"; 2 + import { attest } from "@ark/attest"; 3 + import { lx } from "../lib.ts"; 4 + 5 + test("InferNS produces expected type shape", () => { 6 + const exampleLexicon = lx.lexicon("com.example.post", { 7 + main: lx.record({ 8 + key: "tid", 9 + record: lx.object({ 10 + text: lx.string({ required: true }), 11 + createdAt: lx.string({ required: true, format: "datetime" }), 12 + likes: lx.integer(), 13 + tags: lx.array(lx.string(), { maxLength: 5 }), 14 + }), 15 + }), 16 + }); 17 + 18 + // Type snapshot - this captures how types appear on hover 19 + attest(exampleLexicon["~infer"]).type.toString.snap(`{ 20 + $type: "com.example.post" 21 + tags?: string[] | undefined 22 + likes?: number | undefined 23 + createdAt: string 24 + text: string 25 + }`); 26 + }); 27 + 28 + test("InferObject handles required fields", () => { 29 + const schema = lx.lexicon("test", { 30 + main: lx.object({ 31 + required: lx.string({ required: true }), 32 + optional: lx.string(), 33 + }), 34 + }); 35 + 36 + attest(schema["~infer"]).type.toString.snap(`{ 37 + $type: "test" 38 + optional?: string | undefined 39 + required: string 40 + }`); 41 + }); 42 + 43 + test("InferObject handles nullable fields", () => { 44 + const schema = lx.lexicon("test", { 45 + main: lx.object({ 46 + nullable: lx.string({ nullable: true, required: true }), 47 + }), 48 + }); 49 + 50 + attest(schema["~infer"]).type.toString.snap( 51 + '{ $type: "test"; nullable: string | null }', 52 + ); 53 + }); 54 + 55 + // ============================================================================ 56 + // PRIMITIVE TYPES TESTS 57 + // ============================================================================ 58 + 59 + test("InferType handles string primitive", () => { 60 + const lexicon = lx.lexicon("test.string", { 61 + main: lx.object({ 62 + simpleString: lx.string(), 63 + }), 64 + }); 65 + 66 + attest(lexicon["~infer"]).type.toString.snap(`{ 67 + $type: "test.string" 68 + simpleString?: string | undefined 69 + }`); 70 + }); 71 + 72 + test("InferType handles integer primitive", () => { 73 + const lexicon = lx.lexicon("test.integer", { 74 + main: lx.object({ 75 + count: lx.integer(), 76 + age: lx.integer({ minimum: 0, maximum: 120 }), 77 + }), 78 + }); 79 + 80 + attest(lexicon["~infer"]).type.toString.snap(`{ 81 + $type: "test.integer" 82 + count?: number | undefined 83 + age?: number | undefined 84 + }`); 85 + }); 86 + 87 + test("InferType handles boolean primitive", () => { 88 + const lexicon = lx.lexicon("test.boolean", { 89 + main: lx.object({ 90 + isActive: lx.boolean(), 91 + hasAccess: lx.boolean({ required: true }), 92 + }), 93 + }); 94 + 95 + attest(lexicon["~infer"]).type.toString.snap(`{ 96 + $type: "test.boolean" 97 + isActive?: boolean | undefined 98 + hasAccess: boolean 99 + }`); 100 + }); 101 + 102 + test("InferType handles null primitive", () => { 103 + const lexicon = lx.lexicon("test.null", { 104 + main: lx.object({ 105 + nullValue: lx.null(), 106 + }), 107 + }); 108 + 109 + attest(lexicon["~infer"]).type.toString.snap(`{ 110 + $type: "test.null" 111 + nullValue?: null | undefined 112 + }`); 113 + }); 114 + 115 + test("InferType handles unknown primitive", () => { 116 + const lexicon = lx.lexicon("test.unknown", { 117 + main: lx.object({ 118 + metadata: lx.unknown(), 119 + }), 120 + }); 121 + 122 + attest(lexicon["~infer"]).type.toString.snap( 123 + '{ $type: "test.unknown"; metadata?: unknown }', 124 + ); 125 + }); 126 + 127 + test("InferType handles bytes primitive", () => { 128 + const lexicon = lx.lexicon("test.bytes", { 129 + main: lx.object({ 130 + data: lx.bytes(), 131 + }), 132 + }); 133 + 134 + attest(lexicon["~infer"]).type.toString.snap(`{ 135 + $type: "test.bytes" 136 + data?: Uint8Array<ArrayBufferLike> | undefined 137 + }`); 138 + }); 139 + 140 + test("InferType handles blob primitive", () => { 141 + const lexicon = lx.lexicon("test.blob", { 142 + main: lx.object({ 143 + image: lx.blob({ accept: ["image/png", "image/jpeg"] }), 144 + }), 145 + }); 146 + 147 + attest(lexicon["~infer"]).type.toString.snap( 148 + '{ $type: "test.blob"; image?: Blob | undefined }', 149 + ); 150 + }); 151 + 152 + // ============================================================================ 153 + // TOKEN TYPE TESTS 154 + // ============================================================================ 155 + 156 + test("InferToken handles basic token without enum", () => { 157 + const lexicon = lx.lexicon("test.token", { 158 + main: lx.object({ 159 + symbol: lx.token("A symbolic value"), 160 + }), 161 + }); 162 + 163 + attest(lexicon["~infer"]).type.toString.snap(`{ 164 + $type: "test.token" 165 + symbol?: string | undefined 166 + }`); 167 + }); 168 + 169 + // ============================================================================ 170 + // ARRAY TYPE TESTS 171 + // ============================================================================ 172 + 173 + test("InferArray handles string arrays", () => { 174 + const lexicon = lx.lexicon("test.array.string", { 175 + main: lx.object({ 176 + tags: lx.array(lx.string()), 177 + }), 178 + }); 179 + 180 + attest(lexicon["~infer"]).type.toString.snap(`{ 181 + $type: "test.array.string" 182 + tags?: string[] | undefined 183 + }`); 184 + }); 185 + 186 + test("InferArray handles integer arrays", () => { 187 + const lexicon = lx.lexicon("test.array.integer", { 188 + main: lx.object({ 189 + scores: lx.array(lx.integer(), { minLength: 1, maxLength: 10 }), 190 + }), 191 + }); 192 + 193 + attest(lexicon["~infer"]).type.toString.snap(`{ 194 + $type: "test.array.integer" 195 + scores?: number[] | undefined 196 + }`); 197 + }); 198 + 199 + test("InferArray handles boolean arrays", () => { 200 + const lexicon = lx.lexicon("test.array.boolean", { 201 + main: lx.object({ 202 + flags: lx.array(lx.boolean()), 203 + }), 204 + }); 205 + 206 + attest(lexicon["~infer"]).type.toString.snap(`{ 207 + $type: "test.array.boolean" 208 + flags?: boolean[] | undefined 209 + }`); 210 + }); 211 + 212 + test("InferArray handles unknown arrays", () => { 213 + const lexicon = lx.lexicon("test.array.unknown", { 214 + main: lx.object({ 215 + items: lx.array(lx.unknown()), 216 + }), 217 + }); 218 + 219 + attest(lexicon["~infer"]).type.toString.snap(`{ 220 + $type: "test.array.unknown" 221 + items?: unknown[] | undefined 222 + }`); 223 + }); 224 + 225 + // ============================================================================ 226 + // OBJECT PROPERTY COMBINATIONS 227 + // ============================================================================ 228 + 229 + test("InferObject handles mixed optional and required fields", () => { 230 + const lexicon = lx.lexicon("test.mixed", { 231 + main: lx.object({ 232 + id: lx.string({ required: true }), 233 + name: lx.string({ required: true }), 234 + email: lx.string(), 235 + age: lx.integer(), 236 + }), 237 + }); 238 + 239 + attest(lexicon["~infer"]).type.toString.snap(`{ 240 + $type: "test.mixed" 241 + age?: number | undefined 242 + email?: string | undefined 243 + id: string 244 + name: string 245 + }`); 246 + }); 247 + 248 + test("InferObject handles all optional fields", () => { 249 + const lexicon = lx.lexicon("test.allOptional", { 250 + main: lx.object({ 251 + field1: lx.string(), 252 + field2: lx.integer(), 253 + field3: lx.boolean(), 254 + }), 255 + }); 256 + 257 + attest(lexicon["~infer"]).type.toString.snap(`{ 258 + $type: "test.allOptional" 259 + field1?: string | undefined 260 + field2?: number | undefined 261 + field3?: boolean | undefined 262 + }`); 263 + }); 264 + 265 + test("InferObject handles all required fields", () => { 266 + const lexicon = lx.lexicon("test.allRequired", { 267 + main: lx.object({ 268 + field1: lx.string({ required: true }), 269 + field2: lx.integer({ required: true }), 270 + field3: lx.boolean({ required: true }), 271 + }), 272 + }); 273 + 274 + attest(lexicon["~infer"]).type.toString.snap(`{ 275 + $type: "test.allRequired" 276 + field1: string 277 + field2: number 278 + field3: boolean 279 + }`); 280 + }); 281 + 282 + // ============================================================================ 283 + // NULLABLE FIELDS TESTS 284 + // ============================================================================ 285 + 286 + test("InferObject handles nullable optional field", () => { 287 + const lexicon = lx.lexicon("test.nullableOptional", { 288 + main: lx.object({ 289 + description: lx.string({ nullable: true }), 290 + }), 291 + }); 292 + 293 + attest(lexicon["~infer"]).type.toString.snap(`{ 294 + $type: "test.nullableOptional" 295 + description?: string | null | undefined 296 + }`); 297 + }); 298 + 299 + test("InferObject handles multiple nullable fields", () => { 300 + const lexicon = lx.lexicon("test.multipleNullable", { 301 + main: lx.object({ 302 + field1: lx.string({ nullable: true }), 303 + field2: lx.integer({ nullable: true }), 304 + field3: lx.boolean({ nullable: true }), 305 + }), 306 + }); 307 + 308 + attest(lexicon["~infer"]).type.toString.snap(`{ 309 + $type: "test.multipleNullable" 310 + field1?: string | null | undefined 311 + field2?: number | null | undefined 312 + field3?: boolean | null | undefined 313 + }`); 314 + }); 315 + 316 + test("InferObject handles nullable and required field", () => { 317 + const lexicon = lx.lexicon("test.nullableRequired", { 318 + main: lx.object({ 319 + value: lx.string({ nullable: true, required: true }), 320 + }), 321 + }); 322 + 323 + attest(lexicon["~infer"]).type.toString.snap(`{ 324 + $type: "test.nullableRequired" 325 + value: string | null 326 + }`); 327 + }); 328 + 329 + test("InferObject handles mixed nullable, required, and optional", () => { 330 + const lexicon = lx.lexicon("test.mixedNullable", { 331 + main: lx.object({ 332 + requiredNullable: lx.string({ required: true, nullable: true }), 333 + optionalNullable: lx.string({ nullable: true }), 334 + required: lx.string({ required: true }), 335 + optional: lx.string(), 336 + }), 337 + }); 338 + 339 + attest(lexicon["~infer"]).type.toString.snap(`{ 340 + $type: "test.mixedNullable" 341 + optional?: string | undefined 342 + required: string 343 + optionalNullable?: string | null | undefined 344 + requiredNullable: string | null 345 + }`); 346 + }); 347 + 348 + // ============================================================================ 349 + // REF TYPE TESTS 350 + // ============================================================================ 351 + 352 + test("InferRef handles basic reference", () => { 353 + const lexicon = lx.lexicon("test.ref", { 354 + main: lx.object({ 355 + post: lx.ref("com.example.post"), 356 + }), 357 + }); 358 + 359 + attest(lexicon["~infer"]).type.toString.snap(`{ 360 + $type: "test.ref" 361 + post?: 362 + | { [x: string]: unknown; $type: "com.example.post" } 363 + | undefined 364 + }`); 365 + }); 366 + 367 + test("InferRef handles required reference", () => { 368 + const lexicon = lx.lexicon("test.refRequired", { 369 + main: lx.object({ 370 + author: lx.ref("com.example.user", { required: true }), 371 + }), 372 + }); 373 + 374 + attest(lexicon["~infer"]).type.toString.snap(`{ 375 + $type: "test.refRequired" 376 + author?: 377 + | { [x: string]: unknown; $type: "com.example.user" } 378 + | undefined 379 + }`); 380 + }); 381 + 382 + test("InferRef handles nullable reference", () => { 383 + const lexicon = lx.lexicon("test.refNullable", { 384 + main: lx.object({ 385 + parent: lx.ref("com.example.node", { nullable: true }), 386 + }), 387 + }); 388 + 389 + attest(lexicon["~infer"]).type.toString.snap(`{ 390 + $type: "test.refNullable" 391 + parent?: 392 + | { [x: string]: unknown; $type: "com.example.node" } 393 + | undefined 394 + }`); 395 + }); 396 + 397 + // ============================================================================ 398 + // UNION TYPE TESTS 399 + // ============================================================================ 400 + 401 + test("InferUnion handles basic union", () => { 402 + const lexicon = lx.lexicon("test.union", { 403 + main: lx.object({ 404 + content: lx.union(["com.example.text", "com.example.image"]), 405 + }), 406 + }); 407 + 408 + attest(lexicon["~infer"]).type.toString.snap(`{ 409 + $type: "test.union" 410 + content?: 411 + | { [x: string]: unknown; $type: "com.example.text" } 412 + | { [x: string]: unknown; $type: "com.example.image" } 413 + | undefined 414 + }`); 415 + }); 416 + 417 + test("InferUnion handles required union", () => { 418 + const lexicon = lx.lexicon("test.unionRequired", { 419 + main: lx.object({ 420 + media: lx.union(["com.example.video", "com.example.audio"], { 421 + required: true, 422 + }), 423 + }), 424 + }); 425 + 426 + attest(lexicon["~infer"]).type.toString.snap(`{ 427 + $type: "test.unionRequired" 428 + media: 429 + | { [x: string]: unknown; $type: "com.example.video" } 430 + | { [x: string]: unknown; $type: "com.example.audio" } 431 + }`); 432 + }); 433 + 434 + test("InferUnion handles union with many types", () => { 435 + const lexicon = lx.lexicon("test.unionMultiple", { 436 + main: lx.object({ 437 + attachment: lx.union([ 438 + "com.example.image", 439 + "com.example.video", 440 + "com.example.audio", 441 + "com.example.document", 442 + ]), 443 + }), 444 + }); 445 + 446 + attest(lexicon["~infer"]).type.toString.snap(`{ 447 + $type: "test.unionMultiple" 448 + attachment?: 449 + | { [x: string]: unknown; $type: "com.example.image" } 450 + | { [x: string]: unknown; $type: "com.example.video" } 451 + | { [x: string]: unknown; $type: "com.example.audio" } 452 + | { 453 + [x: string]: unknown 454 + $type: "com.example.document" 455 + } 456 + | undefined 457 + }`); 458 + }); 459 + 460 + // ============================================================================ 461 + // PARAMS TYPE TESTS 462 + // ============================================================================ 463 + 464 + test("InferParams handles basic params", () => { 465 + const lexicon = lx.lexicon("test.params", { 466 + main: lx.params({ 467 + limit: lx.integer(), 468 + offset: lx.integer(), 469 + }), 470 + }); 471 + 472 + attest(lexicon["~infer"]).type.toString.snap(`{ 473 + $type: "test.params" 474 + limit?: number | undefined 475 + offset?: number | undefined 476 + }`); 477 + }); 478 + 479 + test("InferParams handles required params", () => { 480 + const lexicon = lx.lexicon("test.paramsRequired", { 481 + main: lx.params({ 482 + query: lx.string({ required: true }), 483 + limit: lx.integer(), 484 + }), 485 + }); 486 + 487 + attest(lexicon["~infer"]).type.toString.snap(`{ 488 + $type: "test.paramsRequired" 489 + limit?: number | undefined 490 + query: string 491 + }`); 492 + }); 493 + 494 + // ============================================================================ 495 + // RECORD TYPE TESTS 496 + // ============================================================================ 497 + 498 + test("InferRecord handles record with object schema", () => { 499 + const lexicon = lx.lexicon("test.record", { 500 + main: lx.record({ 501 + key: "tid", 502 + record: lx.object({ 503 + title: lx.string({ required: true }), 504 + content: lx.string({ required: true }), 505 + published: lx.boolean(), 506 + }), 507 + }), 508 + }); 509 + 510 + attest(lexicon["~infer"]).type.toString.snap(`{ 511 + $type: "test.record" 512 + published?: boolean | undefined 513 + content: string 514 + title: string 515 + }`); 516 + }); 517 + 518 + // ============================================================================ 519 + // NESTED OBJECTS TESTS 520 + // ============================================================================ 521 + 522 + test("InferObject handles nested objects", () => { 523 + const lexicon = lx.lexicon("test.nested", { 524 + main: lx.object({ 525 + user: lx.object({ 526 + name: lx.string({ required: true }), 527 + email: lx.string({ required: true }), 528 + }), 529 + }), 530 + }); 531 + 532 + attest(lexicon["~infer"]).type.toString.snap(`{ 533 + $type: "test.nested" 534 + user?: { name: string; email: string } | undefined 535 + }`); 536 + }); 537 + 538 + test("InferObject handles deeply nested objects", () => { 539 + const lexicon = lx.lexicon("test.deepNested", { 540 + main: lx.object({ 541 + data: lx.object({ 542 + user: lx.object({ 543 + profile: lx.object({ 544 + name: lx.string({ required: true }), 545 + }), 546 + }), 547 + }), 548 + }), 549 + }); 550 + 551 + attest(lexicon["~infer"]).type.toString.snap(`{ 552 + $type: "test.deepNested" 553 + data?: 554 + | { 555 + user?: 556 + | { profile?: { name: string } | undefined } 557 + | undefined 558 + } 559 + | undefined 560 + }`); 561 + }); 562 + 563 + // ============================================================================ 564 + // NESTED ARRAYS TESTS 565 + // ============================================================================ 566 + 567 + test("InferArray handles arrays of objects", () => { 568 + const lexicon = lx.lexicon("test.arrayOfObjects", { 569 + main: lx.object({ 570 + users: lx.array( 571 + lx.object({ 572 + id: lx.string({ required: true }), 573 + name: lx.string({ required: true }), 574 + }), 575 + ), 576 + }), 577 + }); 578 + 579 + attest(lexicon["~infer"]).type.toString.snap(`{ 580 + $type: "test.arrayOfObjects" 581 + users?: { id: string; name: string }[] | undefined 582 + }`); 583 + }); 584 + 585 + test("InferArray handles arrays of arrays", () => { 586 + const schema = lx.object({ 587 + matrix: lx.array(lx.array(lx.integer())), 588 + }); 589 + 590 + const lexicon = lx.lexicon("test.nestedArrays", { 591 + main: schema, 592 + }); 593 + 594 + attest(lexicon["~infer"]).type.toString.snap(`{ 595 + $type: "test.nestedArrays" 596 + matrix?: number[][] | undefined 597 + }`); 598 + }); 599 + 600 + test("InferArray handles arrays of refs", () => { 601 + const lexicon = lx.lexicon("test.arrayOfRefs", { 602 + main: lx.object({ 603 + followers: lx.array(lx.ref("com.example.user")), 604 + }), 605 + }); 606 + 607 + attest(lexicon["~infer"]).type.toString.snap(`{ 608 + $type: "test.arrayOfRefs" 609 + followers?: 610 + | { [x: string]: unknown; $type: "com.example.user" }[] 611 + | undefined 612 + }`); 613 + }); 614 + 615 + // ============================================================================ 616 + // COMPLEX NESTED STRUCTURES 617 + // ============================================================================ 618 + 619 + test("InferObject handles complex nested structure", () => { 620 + const lexicon = lx.lexicon("test.complex", { 621 + main: lx.object({ 622 + id: lx.string({ required: true }), 623 + author: lx.object({ 624 + did: lx.string({ required: true, format: "did" }), 625 + handle: lx.string({ required: true, format: "handle" }), 626 + avatar: lx.string(), 627 + }), 628 + content: lx.union(["com.example.text", "com.example.image"]), 629 + tags: lx.array(lx.string(), { maxLength: 10 }), 630 + metadata: lx.object({ 631 + views: lx.integer(), 632 + likes: lx.integer(), 633 + shares: lx.integer(), 634 + }), 635 + }), 636 + }); 637 + 638 + attest(lexicon["~infer"]).type.toString.snap(`{ 639 + $type: "test.complex" 640 + tags?: string[] | undefined 641 + content?: 642 + | { [x: string]: unknown; $type: "com.example.text" } 643 + | { [x: string]: unknown; $type: "com.example.image" } 644 + | undefined 645 + author?: 646 + | { 647 + avatar?: string | undefined 648 + did: string 649 + handle: string 650 + } 651 + | undefined 652 + metadata?: 653 + | { 654 + likes?: number | undefined 655 + views?: number | undefined 656 + shares?: number | undefined 657 + } 658 + | undefined 659 + id: string 660 + }`); 661 + }); 662 + 663 + // ============================================================================ 664 + // MULTIPLE DEFS IN NAMESPACE 665 + // ============================================================================ 666 + 667 + test("InferNS handles multiple defs in namespace", () => { 668 + const lexicon = lx.lexicon("com.example.app", { 669 + user: lx.object({ 670 + name: lx.string({ required: true }), 671 + email: lx.string({ required: true }), 672 + }), 673 + post: lx.object({ 674 + title: lx.string({ required: true }), 675 + content: lx.string({ required: true }), 676 + }), 677 + comment: lx.object({ 678 + text: lx.string({ required: true }), 679 + author: lx.ref("com.example.user"), 680 + }), 681 + }); 682 + 683 + attest(lexicon["~infer"]).type.toString.snap("never"); 684 + }); 685 + 686 + test("InferNS handles namespace with record and object defs", () => { 687 + const lexicon = lx.lexicon("com.example.blog", { 688 + main: lx.record({ 689 + key: "tid", 690 + record: lx.object({ 691 + title: lx.string({ required: true }), 692 + body: lx.string({ required: true }), 693 + }), 694 + }), 695 + metadata: lx.object({ 696 + category: lx.string(), 697 + tags: lx.array(lx.string()), 698 + }), 699 + }); 700 + 701 + attest(lexicon["~infer"]).type.toString.snap(`{ 702 + $type: "com.example.blog" 703 + title: string 704 + body: string 705 + }`); 706 + }); 707 + 708 + // ============================================================================ 709 + // LOCAL REF RESOLUTION TESTS 710 + // ============================================================================ 711 + 712 + test("Local ref resolution: resolves refs to actual types", () => { 713 + const ns = lx.lexicon("test", { 714 + user: lx.object({ 715 + name: lx.string({ required: true }), 716 + email: lx.string({ required: true }), 717 + }), 718 + main: lx.object({ 719 + author: lx.ref("#user", { required: true }), 720 + content: lx.string({ required: true }), 721 + }), 722 + }); 723 + 724 + attest(ns["~infer"]).type.toString.snap(`{ 725 + $type: "test" 726 + author?: 727 + | { name: string; email: string; $type: "#user" } 728 + | undefined 729 + content: string 730 + }`); 731 + }); 732 + 733 + test("Local ref resolution: refs in arrays", () => { 734 + const ns = lx.lexicon("test", { 735 + user: lx.object({ 736 + name: lx.string({ required: true }), 737 + }), 738 + main: lx.object({ 739 + users: lx.array(lx.ref("#user")), 740 + }), 741 + }); 742 + 743 + attest(ns["~infer"]).type.toString.snap(`{ 744 + $type: "test" 745 + users?: { name: string; $type: "#user" }[] | undefined 746 + }`); 747 + }); 748 + 749 + test("Local ref resolution: refs in unions", () => { 750 + const ns = lx.lexicon("test", { 751 + text: lx.object({ content: lx.string({ required: true }) }), 752 + image: lx.object({ url: lx.string({ required: true }) }), 753 + main: lx.object({ 754 + embed: lx.union(["#text", "#image"]), 755 + }), 756 + }); 757 + 758 + attest(ns["~infer"]).type.toString.snap(`{ 759 + $type: "test" 760 + embed?: 761 + | { content: string; $type: "#text" } 762 + | { url: string; $type: "#image" } 763 + | undefined 764 + }`); 765 + }); 766 + 767 + test("Local ref resolution: nested refs", () => { 768 + const ns = lx.lexicon("test", { 769 + profile: lx.object({ 770 + bio: lx.string({ required: true }), 771 + }), 772 + user: lx.object({ 773 + name: lx.string({ required: true }), 774 + profile: lx.ref("#profile", { required: true }), 775 + }), 776 + main: lx.object({ 777 + author: lx.ref("#user", { required: true }), 778 + }), 779 + }); 780 + 781 + attest(ns["~infer"]).type.toString.snap(`{ 782 + $type: "test" 783 + author?: 784 + | { 785 + profile?: 786 + | { bio: string; $type: "#profile" } 787 + | undefined 788 + name: string 789 + $type: "#user" 790 + } 791 + | undefined 792 + }`); 793 + }); 794 + 795 + // ============================================================================ 796 + // EDGE CASE TESTS 797 + // ============================================================================ 798 + 799 + test("Edge case: circular reference detection", () => { 800 + const ns = lx.lexicon("test", { 801 + main: lx.object({ 802 + value: lx.string({ required: true }), 803 + parent: lx.ref("#main"), 804 + }), 805 + }); 806 + 807 + attest(ns["~infer"]).type.toString.snap(`{ 808 + $type: "test" 809 + parent?: 810 + | { 811 + parent?: 812 + | "[Circular reference detected: #main]" 813 + | undefined 814 + value: string 815 + $type: "#main" 816 + } 817 + | undefined 818 + value: string 819 + }`); 820 + }); 821 + 822 + test("Edge case: circular reference between multiple types", () => { 823 + const ns = lx.lexicon("test", { 824 + user: lx.object({ 825 + name: lx.string({ required: true }), 826 + posts: lx.array(lx.ref("#post")), 827 + }), 828 + post: lx.object({ 829 + title: lx.string({ required: true }), 830 + author: lx.ref("#user", { required: true }), 831 + }), 832 + main: lx.object({ 833 + users: lx.array(lx.ref("#user")), 834 + }), 835 + }); 836 + 837 + attest(ns["~infer"]).type.toString.snap(`{ 838 + $type: "test" 839 + users?: 840 + | { 841 + posts?: 842 + | { 843 + author?: 844 + | "[Circular reference detected: #user]" 845 + | undefined 846 + title: string 847 + $type: "#post" 848 + }[] 849 + | undefined 850 + name: string 851 + $type: "#user" 852 + }[] 853 + | undefined 854 + }`); 855 + }); 856 + 857 + test("Edge case: missing reference detection", () => { 858 + const ns = lx.lexicon("test", { 859 + main: lx.object({ 860 + author: lx.ref("#user", { required: true }), 861 + }), 862 + }); 863 + 864 + attest(ns["~infer"]).type.toString.snap(`{ 865 + $type: "test" 866 + author?: "[Reference not found: #user]" | undefined 867 + }`); 868 + });
+862
packages/prototypey/core/tests/primitives.test.ts
··· 1 + import { expect, test } from "vitest"; 2 + import { lx } from "../lib.ts"; 3 + 4 + test("lx.null()", () => { 5 + const result = lx.null(); 6 + expect(result).toEqual({ type: "null" }); 7 + }); 8 + 9 + test("lx.boolean()", () => { 10 + const result = lx.boolean(); 11 + expect(result).toEqual({ type: "boolean" }); 12 + }); 13 + 14 + test("lx.boolean() with default", () => { 15 + const result = lx.boolean({ default: true }); 16 + expect(result).toEqual({ type: "boolean", default: true }); 17 + }); 18 + 19 + test("lx.boolean() with const", () => { 20 + const result = lx.boolean({ const: false }); 21 + expect(result).toEqual({ type: "boolean", const: false }); 22 + }); 23 + 24 + test("lx.integer()", () => { 25 + const result = lx.integer(); 26 + expect(result).toEqual({ type: "integer" }); 27 + }); 28 + 29 + test("lx.integer() with minimum", () => { 30 + const result = lx.integer({ minimum: 0 }); 31 + expect(result).toEqual({ type: "integer", minimum: 0 }); 32 + }); 33 + 34 + test("lx.integer() with maximum", () => { 35 + const result = lx.integer({ maximum: 100 }); 36 + expect(result).toEqual({ type: "integer", maximum: 100 }); 37 + }); 38 + 39 + test("lx.integer() with minimum and maximum", () => { 40 + const result = lx.integer({ minimum: 0, maximum: 100 }); 41 + expect(result).toEqual({ type: "integer", minimum: 0, maximum: 100 }); 42 + }); 43 + 44 + test("lx.integer() with enum", () => { 45 + const result = lx.integer({ enum: [1, 2, 3, 5, 8, 13] }); 46 + expect(result).toEqual({ type: "integer", enum: [1, 2, 3, 5, 8, 13] }); 47 + }); 48 + 49 + test("lx.integer() with default", () => { 50 + const result = lx.integer({ default: 42 }); 51 + expect(result).toEqual({ type: "integer", default: 42 }); 52 + }); 53 + 54 + test("lx.integer() with const", () => { 55 + const result = lx.integer({ const: 7 }); 56 + expect(result).toEqual({ type: "integer", const: 7 }); 57 + }); 58 + 59 + test("lx.string()", () => { 60 + const result = lx.string(); 61 + expect(result).toEqual({ type: "string" }); 62 + }); 63 + 64 + test("lx.string() with maxLength", () => { 65 + const result = lx.string({ maxLength: 64 }); 66 + expect(result).toEqual({ type: "string", maxLength: 64 }); 67 + }); 68 + 69 + test("lx.string() with enum", () => { 70 + const result = lx.string({ enum: ["light", "dark", "auto"] }); 71 + expect(result).toEqual({ type: "string", enum: ["light", "dark", "auto"] }); 72 + }); 73 + 74 + test("lx.unknown()", () => { 75 + const result = lx.unknown(); 76 + expect(result).toEqual({ type: "unknown" }); 77 + }); 78 + 79 + test("lx.bytes()", () => { 80 + const result = lx.bytes(); 81 + expect(result).toEqual({ type: "bytes" }); 82 + }); 83 + 84 + test("lx.bytes() with minLength", () => { 85 + const result = lx.bytes({ minLength: 1 }); 86 + expect(result).toEqual({ type: "bytes", minLength: 1 }); 87 + }); 88 + 89 + test("lx.bytes() with maxLength", () => { 90 + const result = lx.bytes({ maxLength: 1024 }); 91 + expect(result).toEqual({ type: "bytes", maxLength: 1024 }); 92 + }); 93 + 94 + test("lx.bytes() with minLength and maxLength", () => { 95 + const result = lx.bytes({ minLength: 1, maxLength: 1024 }); 96 + expect(result).toEqual({ type: "bytes", minLength: 1, maxLength: 1024 }); 97 + }); 98 + 99 + test("lx.cidLink()", () => { 100 + const result = lx.cidLink( 101 + "bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a", 102 + ); 103 + expect(result).toEqual({ 104 + type: "cid-link", 105 + $link: "bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a", 106 + }); 107 + }); 108 + 109 + test("lx.blob()", () => { 110 + const result = lx.blob(); 111 + expect(result).toEqual({ type: "blob" }); 112 + }); 113 + 114 + test("lx.blob() with accept", () => { 115 + const result = lx.blob({ accept: ["image/png", "image/jpeg"] }); 116 + expect(result).toEqual({ 117 + type: "blob", 118 + accept: ["image/png", "image/jpeg"], 119 + }); 120 + }); 121 + 122 + test("lx.blob() with maxSize", () => { 123 + const result = lx.blob({ maxSize: 1000000 }); 124 + expect(result).toEqual({ type: "blob", maxSize: 1000000 }); 125 + }); 126 + 127 + test("lx.blob() with accept and maxSize", () => { 128 + const result = lx.blob({ 129 + accept: ["image/png", "image/jpeg"], 130 + maxSize: 5000000, 131 + }); 132 + expect(result).toEqual({ 133 + type: "blob", 134 + accept: ["image/png", "image/jpeg"], 135 + maxSize: 5000000, 136 + }); 137 + }); 138 + 139 + test("lx.array() with string items", () => { 140 + const result = lx.array(lx.string()); 141 + expect(result).toEqual({ type: "array", items: { type: "string" } }); 142 + }); 143 + 144 + test("lx.array() with integer items", () => { 145 + const result = lx.array(lx.integer()); 146 + expect(result).toEqual({ type: "array", items: { type: "integer" } }); 147 + }); 148 + 149 + test("lx.array() with minLength", () => { 150 + const result = lx.array(lx.string(), { minLength: 1 }); 151 + expect(result).toEqual({ 152 + type: "array", 153 + items: { type: "string" }, 154 + minLength: 1, 155 + }); 156 + }); 157 + 158 + test("lx.array() with maxLength", () => { 159 + const result = lx.array(lx.string(), { maxLength: 10 }); 160 + expect(result).toEqual({ 161 + type: "array", 162 + items: { type: "string" }, 163 + maxLength: 10, 164 + }); 165 + }); 166 + 167 + test("lx.array() with minLength and maxLength", () => { 168 + const result = lx.array(lx.string(), { minLength: 1, maxLength: 10 }); 169 + expect(result).toEqual({ 170 + type: "array", 171 + items: { type: "string" }, 172 + minLength: 1, 173 + maxLength: 10, 174 + }); 175 + }); 176 + 177 + test("lx.array() with required", () => { 178 + const result = lx.array(lx.string(), { required: true }); 179 + expect(result).toEqual({ 180 + type: "array", 181 + items: { type: "string" }, 182 + required: true, 183 + }); 184 + }); 185 + 186 + test("lx.object() basic", () => { 187 + const result = lx.object({ 188 + name: lx.string(), 189 + }); 190 + expect(result).toEqual({ 191 + type: "object", 192 + properties: { 193 + name: { type: "string" }, 194 + }, 195 + }); 196 + }); 197 + 198 + test("lx.object() with description", () => { 199 + const result = lx.object( 200 + { 201 + enabled: lx.boolean({ 202 + default: true, 203 + description: "Whether this feature is enabled.", 204 + }), 205 + }, 206 + { 207 + description: "Configuration options for the feature.", 208 + }, 209 + ); 210 + expect(result).toEqual({ 211 + type: "object", 212 + description: "Configuration options for the feature.", 213 + properties: { 214 + enabled: { 215 + type: "boolean", 216 + default: true, 217 + description: "Whether this feature is enabled.", 218 + }, 219 + }, 220 + }); 221 + }); 222 + 223 + test("lx.object() with required and description", () => { 224 + const result = lx.object( 225 + { 226 + id: lx.string({ required: true }), 227 + name: lx.string(), 228 + }, 229 + { description: "User profile object" }, 230 + ); 231 + expect(result).toEqual({ 232 + type: "object", 233 + description: "User profile object", 234 + properties: { 235 + id: { type: "string", required: true }, 236 + name: { type: "string" }, 237 + }, 238 + required: ["id"], 239 + }); 240 + }); 241 + 242 + test("lx.object() with nullable and description", () => { 243 + const result = lx.object( 244 + { 245 + bio: lx.string({ nullable: true }), 246 + }, 247 + { description: "Optional profile fields" }, 248 + ); 249 + expect(result).toEqual({ 250 + type: "object", 251 + description: "Optional profile fields", 252 + properties: { 253 + bio: { type: "string", nullable: true }, 254 + }, 255 + nullable: ["bio"], 256 + }); 257 + }); 258 + 259 + test("lx.token() with interaction event", () => { 260 + const result = lx.token( 261 + "Request that less content like the given feed item be shown in the feed", 262 + ); 263 + expect(result).toEqual({ 264 + type: "token", 265 + description: 266 + "Request that less content like the given feed item be shown in the feed", 267 + }); 268 + }); 269 + 270 + test("lx.token() with content mode", () => { 271 + const result = lx.token( 272 + "Declares the feed generator returns posts containing app.bsky.embed.video embeds", 273 + ); 274 + expect(result).toEqual({ 275 + type: "token", 276 + description: 277 + "Declares the feed generator returns posts containing app.bsky.embed.video embeds", 278 + }); 279 + }); 280 + 281 + test("lx.ref() with local definition", () => { 282 + const result = lx.ref("#profileAssociated"); 283 + expect(result).toEqual({ 284 + type: "ref", 285 + ref: "#profileAssociated", 286 + }); 287 + }); 288 + 289 + test("lx.ref() with external schema", () => { 290 + const result = lx.ref("com.atproto.label.defs#label"); 291 + expect(result).toEqual({ 292 + type: "ref", 293 + ref: "com.atproto.label.defs#label", 294 + }); 295 + }); 296 + 297 + test("lx.ref() with required option", () => { 298 + const result = lx.ref("#profileView", { required: true }); 299 + expect(result).toEqual({ 300 + type: "ref", 301 + ref: "#profileView", 302 + required: true, 303 + }); 304 + }); 305 + 306 + test("lx.ref() with nullable option", () => { 307 + const result = lx.ref("#profileView", { nullable: true }); 308 + expect(result).toEqual({ 309 + type: "ref", 310 + ref: "#profileView", 311 + nullable: true, 312 + }); 313 + }); 314 + 315 + test("lx.ref() with both required and nullable", () => { 316 + const result = lx.ref("app.bsky.actor.defs#profileView", { 317 + required: true, 318 + nullable: true, 319 + }); 320 + expect(result).toEqual({ 321 + type: "ref", 322 + ref: "app.bsky.actor.defs#profileView", 323 + required: true, 324 + nullable: true, 325 + }); 326 + }); 327 + 328 + test("lx.union() with local refs", () => { 329 + const result = lx.union(["#reasonRepost", "#reasonPin"]); 330 + expect(result).toEqual({ 331 + type: "union", 332 + refs: ["#reasonRepost", "#reasonPin"], 333 + }); 334 + }); 335 + 336 + test("lx.union() with external refs", () => { 337 + const result = lx.union([ 338 + "app.bsky.embed.images#view", 339 + "app.bsky.embed.video#view", 340 + "app.bsky.embed.external#view", 341 + "app.bsky.embed.record#view", 342 + "app.bsky.embed.recordWithMedia#view", 343 + ]); 344 + expect(result).toEqual({ 345 + type: "union", 346 + refs: [ 347 + "app.bsky.embed.images#view", 348 + "app.bsky.embed.video#view", 349 + "app.bsky.embed.external#view", 350 + "app.bsky.embed.record#view", 351 + "app.bsky.embed.recordWithMedia#view", 352 + ], 353 + }); 354 + }); 355 + 356 + test("lx.union() with closed option", () => { 357 + const result = lx.union(["#postView", "#notFoundPost", "#blockedPost"], { 358 + closed: true, 359 + }); 360 + expect(result).toEqual({ 361 + type: "union", 362 + refs: ["#postView", "#notFoundPost", "#blockedPost"], 363 + closed: true, 364 + }); 365 + }); 366 + 367 + test("lx.union() with closed: false (open union)", () => { 368 + const result = lx.union(["#threadViewPost", "#notFoundPost"], { 369 + closed: false, 370 + }); 371 + expect(result).toEqual({ 372 + type: "union", 373 + refs: ["#threadViewPost", "#notFoundPost"], 374 + closed: false, 375 + }); 376 + }); 377 + 378 + test("lx.params() with basic properties", () => { 379 + const result = lx.params({ 380 + q: lx.string(), 381 + limit: lx.integer(), 382 + }); 383 + expect(result).toEqual({ 384 + type: "params", 385 + properties: { 386 + q: { type: "string" }, 387 + limit: { type: "integer" }, 388 + }, 389 + }); 390 + }); 391 + 392 + test("lx.params() with required properties", () => { 393 + const result = lx.params({ 394 + q: lx.string({ required: true }), 395 + limit: lx.integer(), 396 + }); 397 + expect(result).toEqual({ 398 + type: "params", 399 + properties: { 400 + q: { type: "string", required: true }, 401 + limit: { type: "integer" }, 402 + }, 403 + required: ["q"], 404 + }); 405 + }); 406 + 407 + test("lx.params() with property options", () => { 408 + const result = lx.params({ 409 + q: lx.string(), 410 + limit: lx.integer({ minimum: 1, maximum: 100, default: 25 }), 411 + cursor: lx.string(), 412 + }); 413 + expect(result).toEqual({ 414 + type: "params", 415 + properties: { 416 + q: { type: "string" }, 417 + limit: { type: "integer", minimum: 1, maximum: 100, default: 25 }, 418 + cursor: { type: "string" }, 419 + }, 420 + }); 421 + }); 422 + 423 + test("lx.params() with array properties", () => { 424 + const result = lx.params({ 425 + tags: lx.array(lx.string()), 426 + ids: lx.array(lx.integer()), 427 + }); 428 + expect(result).toEqual({ 429 + type: "params", 430 + properties: { 431 + tags: { type: "array", items: { type: "string" } }, 432 + ids: { type: "array", items: { type: "integer" } }, 433 + }, 434 + }); 435 + }); 436 + 437 + test("lx.params() real-world example from searchActors", () => { 438 + const result = lx.params({ 439 + q: lx.string({ required: true }), 440 + limit: lx.integer({ minimum: 1, maximum: 100, default: 25 }), 441 + cursor: lx.string(), 442 + }); 443 + expect(result).toEqual({ 444 + type: "params", 445 + properties: { 446 + q: { type: "string", required: true }, 447 + limit: { type: "integer", minimum: 1, maximum: 100, default: 25 }, 448 + cursor: { type: "string" }, 449 + }, 450 + required: ["q"], 451 + }); 452 + }); 453 + 454 + test("lx.query() basic", () => { 455 + const result = lx.query(); 456 + expect(result).toEqual({ type: "query" }); 457 + }); 458 + 459 + test("lx.query() with description", () => { 460 + const result = lx.query({ description: "Search for actors" }); 461 + expect(result).toEqual({ type: "query", description: "Search for actors" }); 462 + }); 463 + 464 + test("lx.query() with parameters", () => { 465 + const result = lx.query({ 466 + parameters: lx.params({ 467 + q: lx.string({ required: true }), 468 + limit: lx.integer({ minimum: 1, maximum: 100, default: 25 }), 469 + }), 470 + }); 471 + expect(result).toEqual({ 472 + type: "query", 473 + parameters: { 474 + type: "params", 475 + properties: { 476 + q: { type: "string", required: true }, 477 + limit: { type: "integer", minimum: 1, maximum: 100, default: 25 }, 478 + }, 479 + required: ["q"], 480 + }, 481 + }); 482 + }); 483 + 484 + test("lx.query() with output", () => { 485 + const result = lx.query({ 486 + output: { 487 + encoding: "application/json", 488 + schema: lx.object({ 489 + posts: lx.array(lx.ref("app.bsky.feed.defs#postView"), { 490 + required: true, 491 + }), 492 + cursor: lx.string(), 493 + }), 494 + }, 495 + }); 496 + expect(result).toEqual({ 497 + type: "query", 498 + output: { 499 + encoding: "application/json", 500 + schema: { 501 + type: "object", 502 + properties: { 503 + posts: { 504 + type: "array", 505 + items: { type: "ref", ref: "app.bsky.feed.defs#postView" }, 506 + required: true, 507 + }, 508 + cursor: { type: "string" }, 509 + }, 510 + required: ["posts"], 511 + }, 512 + }, 513 + }); 514 + }); 515 + 516 + test("lx.query() with errors", () => { 517 + const result = lx.query({ 518 + errors: [{ name: "BadQueryString" }], 519 + }); 520 + expect(result).toEqual({ 521 + type: "query", 522 + errors: [{ name: "BadQueryString" }], 523 + }); 524 + }); 525 + 526 + test("lx.query() real-world example: searchPosts", () => { 527 + const result = lx.query({ 528 + description: "Find posts matching search criteria", 529 + parameters: lx.params({ 530 + q: lx.string({ required: true }), 531 + sort: lx.string({ enum: ["top", "latest"], default: "latest" }), 532 + limit: lx.integer({ minimum: 1, maximum: 100, default: 25 }), 533 + cursor: lx.string(), 534 + }), 535 + output: { 536 + encoding: "application/json", 537 + schema: lx.object({ 538 + cursor: lx.string(), 539 + hitsTotal: lx.integer(), 540 + posts: lx.array(lx.ref("app.bsky.feed.defs#postView"), { 541 + required: true, 542 + }), 543 + }), 544 + }, 545 + errors: [{ name: "BadQueryString" }], 546 + }); 547 + expect(result).toEqual({ 548 + type: "query", 549 + description: "Find posts matching search criteria", 550 + parameters: { 551 + type: "params", 552 + properties: { 553 + q: { type: "string", required: true }, 554 + sort: { type: "string", enum: ["top", "latest"], default: "latest" }, 555 + limit: { type: "integer", minimum: 1, maximum: 100, default: 25 }, 556 + cursor: { type: "string" }, 557 + }, 558 + required: ["q"], 559 + }, 560 + output: { 561 + encoding: "application/json", 562 + schema: { 563 + type: "object", 564 + properties: { 565 + cursor: { type: "string" }, 566 + hitsTotal: { type: "integer" }, 567 + posts: { 568 + type: "array", 569 + items: { type: "ref", ref: "app.bsky.feed.defs#postView" }, 570 + required: true, 571 + }, 572 + }, 573 + required: ["posts"], 574 + }, 575 + }, 576 + errors: [{ name: "BadQueryString" }], 577 + }); 578 + }); 579 + 580 + test("lx.procedure() basic", () => { 581 + const result = lx.procedure(); 582 + expect(result).toEqual({ type: "procedure" }); 583 + }); 584 + 585 + test("lx.procedure() with description", () => { 586 + const result = lx.procedure({ description: "Create a new post" }); 587 + expect(result).toEqual({ 588 + type: "procedure", 589 + description: "Create a new post", 590 + }); 591 + }); 592 + 593 + test("lx.procedure() with parameters", () => { 594 + const result = lx.procedure({ 595 + parameters: lx.params({ 596 + validate: lx.boolean({ default: true }), 597 + }), 598 + }); 599 + expect(result).toEqual({ 600 + type: "procedure", 601 + parameters: { 602 + type: "params", 603 + properties: { 604 + validate: { type: "boolean", default: true }, 605 + }, 606 + }, 607 + }); 608 + }); 609 + 610 + test("lx.procedure() with input", () => { 611 + const result = lx.procedure({ 612 + input: { 613 + encoding: "application/json", 614 + schema: lx.object({ 615 + text: lx.string({ required: true, maxGraphemes: 300 }), 616 + createdAt: lx.string({ format: "datetime" }), 617 + }), 618 + }, 619 + }); 620 + expect(result).toEqual({ 621 + type: "procedure", 622 + input: { 623 + encoding: "application/json", 624 + schema: { 625 + type: "object", 626 + properties: { 627 + text: { type: "string", required: true, maxGraphemes: 300 }, 628 + createdAt: { type: "string", format: "datetime" }, 629 + }, 630 + required: ["text"], 631 + }, 632 + }, 633 + }); 634 + }); 635 + 636 + test("lx.procedure() with output", () => { 637 + const result = lx.procedure({ 638 + output: { 639 + encoding: "application/json", 640 + schema: lx.object({ 641 + uri: lx.string({ required: true }), 642 + cid: lx.string({ required: true }), 643 + }), 644 + }, 645 + }); 646 + expect(result).toEqual({ 647 + type: "procedure", 648 + output: { 649 + encoding: "application/json", 650 + schema: { 651 + type: "object", 652 + properties: { 653 + uri: { type: "string", required: true }, 654 + cid: { type: "string", required: true }, 655 + }, 656 + required: ["uri", "cid"], 657 + }, 658 + }, 659 + }); 660 + }); 661 + 662 + test("lx.procedure() with errors", () => { 663 + const result = lx.procedure({ 664 + errors: [ 665 + { name: "InvalidRequest" }, 666 + { name: "RateLimitExceeded", description: "Too many requests" }, 667 + ], 668 + }); 669 + expect(result).toEqual({ 670 + type: "procedure", 671 + errors: [ 672 + { name: "InvalidRequest" }, 673 + { name: "RateLimitExceeded", description: "Too many requests" }, 674 + ], 675 + }); 676 + }); 677 + 678 + test("lx.procedure() real-world example: createPost", () => { 679 + const result = lx.procedure({ 680 + description: "Create a post", 681 + input: { 682 + encoding: "application/json", 683 + schema: lx.object({ 684 + repo: lx.string({ required: true }), 685 + collection: lx.string({ required: true }), 686 + record: lx.unknown({ required: true }), 687 + validate: lx.boolean({ default: true }), 688 + }), 689 + }, 690 + output: { 691 + encoding: "application/json", 692 + schema: lx.object({ 693 + uri: lx.string({ required: true }), 694 + cid: lx.string({ required: true }), 695 + }), 696 + }, 697 + errors: [{ name: "InvalidSwap" }, { name: "InvalidRecord" }], 698 + }); 699 + expect(result).toEqual({ 700 + type: "procedure", 701 + description: "Create a post", 702 + input: { 703 + encoding: "application/json", 704 + schema: { 705 + type: "object", 706 + properties: { 707 + repo: { type: "string", required: true }, 708 + collection: { type: "string", required: true }, 709 + record: { type: "unknown", required: true }, 710 + validate: { type: "boolean", default: true }, 711 + }, 712 + required: ["repo", "collection", "record"], 713 + }, 714 + }, 715 + output: { 716 + encoding: "application/json", 717 + schema: { 718 + type: "object", 719 + properties: { 720 + uri: { type: "string", required: true }, 721 + cid: { type: "string", required: true }, 722 + }, 723 + required: ["uri", "cid"], 724 + }, 725 + }, 726 + errors: [{ name: "InvalidSwap" }, { name: "InvalidRecord" }], 727 + }); 728 + }); 729 + 730 + test("lx.subscription() basic", () => { 731 + const result = lx.subscription(); 732 + expect(result).toEqual({ type: "subscription" }); 733 + }); 734 + 735 + test("lx.subscription() with description", () => { 736 + const result = lx.subscription({ 737 + description: "Repository event stream", 738 + }); 739 + expect(result).toEqual({ 740 + type: "subscription", 741 + description: "Repository event stream", 742 + }); 743 + }); 744 + 745 + test("lx.subscription() with parameters", () => { 746 + const result = lx.subscription({ 747 + parameters: lx.params({ 748 + cursor: lx.integer(), 749 + }), 750 + }); 751 + expect(result).toEqual({ 752 + type: "subscription", 753 + parameters: { 754 + type: "params", 755 + properties: { 756 + cursor: { type: "integer" }, 757 + }, 758 + }, 759 + }); 760 + }); 761 + 762 + test("lx.subscription() with message", () => { 763 + const result = lx.subscription({ 764 + message: { 765 + schema: lx.union(["#commit", "#identity", "#account"]), 766 + }, 767 + }); 768 + expect(result).toEqual({ 769 + type: "subscription", 770 + message: { 771 + schema: { 772 + type: "union", 773 + refs: ["#commit", "#identity", "#account"], 774 + }, 775 + }, 776 + }); 777 + }); 778 + 779 + test("lx.subscription() with message description", () => { 780 + const result = lx.subscription({ 781 + message: { 782 + description: "Event message types", 783 + schema: lx.union(["#commit", "#handle", "#migrate"]), 784 + }, 785 + }); 786 + expect(result).toEqual({ 787 + type: "subscription", 788 + message: { 789 + description: "Event message types", 790 + schema: { 791 + type: "union", 792 + refs: ["#commit", "#handle", "#migrate"], 793 + }, 794 + }, 795 + }); 796 + }); 797 + 798 + test("lx.subscription() with errors", () => { 799 + const result = lx.subscription({ 800 + errors: [ 801 + { name: "FutureCursor" }, 802 + { name: "ConsumerTooSlow", description: "Consumer is too slow" }, 803 + ], 804 + }); 805 + expect(result).toEqual({ 806 + type: "subscription", 807 + errors: [ 808 + { name: "FutureCursor" }, 809 + { name: "ConsumerTooSlow", description: "Consumer is too slow" }, 810 + ], 811 + }); 812 + }); 813 + 814 + test("lx.subscription() real-world example: subscribeRepos", () => { 815 + const result = lx.subscription({ 816 + description: "Repository event stream, aka Firehose endpoint", 817 + parameters: lx.params({ 818 + cursor: lx.integer(), 819 + }), 820 + message: { 821 + description: "Represents an update of repository state", 822 + schema: lx.union([ 823 + "#commit", 824 + "#identity", 825 + "#account", 826 + "#handle", 827 + "#migrate", 828 + "#tombstone", 829 + "#info", 830 + ]), 831 + }, 832 + errors: [{ name: "FutureCursor" }, { name: "ConsumerTooSlow" }], 833 + }); 834 + expect(result).toEqual({ 835 + type: "subscription", 836 + description: "Repository event stream, aka Firehose endpoint", 837 + parameters: { 838 + type: "params", 839 + properties: { 840 + cursor: { 841 + type: "integer", 842 + }, 843 + }, 844 + }, 845 + message: { 846 + description: "Represents an update of repository state", 847 + schema: { 848 + type: "union", 849 + refs: [ 850 + "#commit", 851 + "#identity", 852 + "#account", 853 + "#handle", 854 + "#migrate", 855 + "#tombstone", 856 + "#info", 857 + ], 858 + }, 859 + }, 860 + errors: [{ name: "FutureCursor" }, { name: "ConsumerTooSlow" }], 861 + }); 862 + });
+185
packages/prototypey/core/tests/validation-baseline.bench.ts
··· 1 + import { bench, describe } from "vitest"; 2 + import { Lexicons } from "@atproto/lexicon"; 3 + import { lx } from "../lib.ts"; 4 + 5 + // Phase 1 Benchmarks: Baseline measurements before implementation 6 + 7 + describe("baseline: lexicon instantiation", () => { 8 + bench("simple lexicon instantiation", () => { 9 + lx.lexicon("test.simple", { 10 + main: lx.object({ 11 + id: lx.string({ required: true }), 12 + name: lx.string({ required: true }), 13 + }), 14 + }); 15 + }); 16 + }); 17 + 18 + describe("baseline: Lexicons class", () => { 19 + bench("create empty Lexicons instance", () => { 20 + new Lexicons(); 21 + }); 22 + 23 + bench("load 1 lexicon into Lexicons", () => { 24 + const lexicons = new Lexicons(); 25 + lexicons.add({ 26 + lexicon: 1, 27 + id: "test.simple", 28 + defs: { 29 + main: { 30 + type: "object", 31 + properties: { 32 + id: { type: "string" }, 33 + name: { type: "string" }, 34 + }, 35 + required: ["id", "name"], 36 + }, 37 + }, 38 + }); 39 + }); 40 + 41 + bench("load 10 lexicons into Lexicons", () => { 42 + const lexicons = new Lexicons(); 43 + for (let i = 0; i < 10; i++) { 44 + lexicons.add({ 45 + lexicon: 1, 46 + id: `test.schema${i}`, 47 + defs: { 48 + main: { 49 + type: "object", 50 + properties: { 51 + id: { type: "string" }, 52 + name: { type: "string" }, 53 + }, 54 + required: ["id", "name"], 55 + }, 56 + }, 57 + }); 58 + } 59 + }); 60 + 61 + bench("load 100 lexicons into Lexicons", () => { 62 + const lexicons = new Lexicons(); 63 + for (let i = 0; i < 100; i++) { 64 + lexicons.add({ 65 + lexicon: 1, 66 + id: `test.schema${i}`, 67 + defs: { 68 + main: { 69 + type: "object", 70 + properties: { 71 + id: { type: "string" }, 72 + name: { type: "string" }, 73 + }, 74 + required: ["id", "name"], 75 + }, 76 + }, 77 + }); 78 + } 79 + }); 80 + }); 81 + 82 + describe("baseline: validation", () => { 83 + bench("validate simple object", () => { 84 + const lexicons = new Lexicons([ 85 + { 86 + lexicon: 1, 87 + id: "test.simple", 88 + defs: { 89 + main: { 90 + type: "object", 91 + properties: { 92 + id: { type: "string" }, 93 + name: { type: "string" }, 94 + }, 95 + required: ["id", "name"], 96 + }, 97 + }, 98 + }, 99 + ]); 100 + lexicons.validate("test.simple#main", { 101 + id: "123", 102 + name: "test", 103 + }); 104 + }); 105 + 106 + bench("validate complex nested object", () => { 107 + const lexicons = new Lexicons([ 108 + { 109 + lexicon: 1, 110 + id: "test.complex", 111 + defs: { 112 + user: { 113 + type: "object", 114 + properties: { 115 + handle: { type: "string" }, 116 + displayName: { type: "string" }, 117 + }, 118 + required: ["handle"], 119 + }, 120 + reply: { 121 + type: "object", 122 + properties: { 123 + text: { type: "string" }, 124 + author: { type: "ref", ref: "#user" }, 125 + }, 126 + required: ["text", "author"], 127 + }, 128 + main: { 129 + type: "record", 130 + key: "tid", 131 + record: { 132 + type: "object", 133 + properties: { 134 + author: { type: "ref", ref: "#user" }, 135 + replies: { 136 + type: "array", 137 + items: { type: "ref", ref: "#reply" }, 138 + }, 139 + content: { type: "string" }, 140 + createdAt: { type: "string", format: "datetime" }, 141 + }, 142 + required: ["author", "content", "createdAt"], 143 + }, 144 + }, 145 + }, 146 + }, 147 + ]); 148 + lexicons.validate("test.complex#main", { 149 + author: { handle: "alice.bsky.social", displayName: "Alice" }, 150 + replies: [ 151 + { 152 + text: "Great post!", 153 + author: { handle: "bob.bsky.social", displayName: "Bob" }, 154 + }, 155 + ], 156 + content: "Hello world", 157 + createdAt: "2025-01-01T00:00:00Z", 158 + }); 159 + }); 160 + 161 + bench("validate 1000 simple objects", () => { 162 + const lexicons = new Lexicons([ 163 + { 164 + lexicon: 1, 165 + id: "test.simple", 166 + defs: { 167 + main: { 168 + type: "object", 169 + properties: { 170 + id: { type: "string" }, 171 + name: { type: "string" }, 172 + }, 173 + required: ["id", "name"], 174 + }, 175 + }, 176 + }, 177 + ]); 178 + for (let i = 0; i < 1000; i++) { 179 + lexicons.validate("test.simple#main", { 180 + id: `${i}`, 181 + name: `test${i}`, 182 + }); 183 + } 184 + }); 185 + });
+113
packages/prototypey/core/tests/validation-eager.bench.ts
··· 1 + import { bench, describe } from "vitest"; 2 + import { lx } from "../lib.ts"; 3 + 4 + // Phase 2 Benchmarks: Eager loading strategy 5 + 6 + describe("eager: lexicon instantiation with validator", () => { 7 + bench("simple lexicon with eager validator", () => { 8 + lx.lexicon("test.simple", { 9 + main: lx.object({ 10 + id: lx.string({ required: true }), 11 + name: lx.string({ required: true }), 12 + }), 13 + }); 14 + }); 15 + 16 + bench("complex lexicon with eager validator", () => { 17 + lx.lexicon("test.complex", { 18 + user: lx.object({ 19 + handle: lx.string({ required: true }), 20 + displayName: lx.string(), 21 + }), 22 + reply: lx.object({ 23 + text: lx.string({ required: true }), 24 + author: lx.ref("#user", { required: true }), 25 + }), 26 + main: lx.record({ 27 + key: "tid", 28 + record: lx.object({ 29 + author: lx.ref("#user", { required: true }), 30 + replies: lx.array(lx.ref("#reply")), 31 + content: lx.string({ required: true }), 32 + createdAt: lx.string({ required: true, format: "datetime" }), 33 + }), 34 + }), 35 + }); 36 + }); 37 + 38 + bench("100 lexicons with eager validators", () => { 39 + for (let i = 0; i < 100; i++) { 40 + lx.lexicon(`test.schema${i}`, { 41 + main: lx.object({ 42 + id: lx.string({ required: true }), 43 + name: lx.string({ required: true }), 44 + }), 45 + }); 46 + } 47 + }); 48 + }); 49 + 50 + describe("eager: validation (validator already loaded)", () => { 51 + const simpleSchema = lx.lexicon("test.simple", { 52 + main: lx.object({ 53 + id: lx.string({ required: true }), 54 + name: lx.string({ required: true }), 55 + }), 56 + }); 57 + const complexSchema = lx.lexicon("test.complex", { 58 + user: lx.object({ 59 + handle: lx.string({ required: true }), 60 + displayName: lx.string(), 61 + }), 62 + reply: lx.object({ 63 + text: lx.string({ required: true }), 64 + author: lx.ref("#user", { required: true }), 65 + }), 66 + main: lx.record({ 67 + key: "tid", 68 + record: lx.object({ 69 + author: lx.ref("#user", { required: true }), 70 + replies: lx.array(lx.ref("#reply")), 71 + content: lx.string({ required: true }), 72 + createdAt: lx.string({ required: true, format: "datetime" }), 73 + }), 74 + }), 75 + }); 76 + 77 + bench("first validation call (already loaded)", () => { 78 + simpleSchema.validate({ 79 + id: "123", 80 + name: "test", 81 + }); 82 + }); 83 + 84 + bench("validate simple object", () => { 85 + simpleSchema.validate({ 86 + id: "123", 87 + name: "test", 88 + }); 89 + }); 90 + 91 + bench("validate complex object", () => { 92 + complexSchema.validate({ 93 + author: { handle: "alice.bsky.social", displayName: "Alice" }, 94 + replies: [ 95 + { 96 + text: "Great post!", 97 + author: { handle: "bob.bsky.social", displayName: "Bob" }, 98 + }, 99 + ], 100 + content: "Hello world", 101 + createdAt: "2025-01-01T00:00:00Z", 102 + }); 103 + }); 104 + 105 + bench("1000 sequential validations", () => { 106 + for (let i = 0; i < 1000; i++) { 107 + simpleSchema.validate({ 108 + id: `${i}`, 109 + name: `test${i}`, 110 + }); 111 + } 112 + }); 113 + });
+796
packages/prototypey/core/tests/validation.test.ts
··· 1 + import { describe, it, expect } from "vitest"; 2 + import { lx } from "../lib.ts"; 3 + 4 + describe("basic validation", () => { 5 + const schema = lx.lexicon("test.simple", { 6 + main: lx.object({ 7 + id: lx.string({ required: true }), 8 + name: lx.string({ required: true }), 9 + }), 10 + }); 11 + 12 + it("should validate valid data", () => { 13 + const result = schema.validate({ id: "123", name: "test" }); 14 + expect(result.success).toBe(true); 15 + if (result.success) { 16 + expect(result.value).toEqual({ id: "123", name: "test" }); 17 + } 18 + }); 19 + 20 + it("should reject missing required fields", () => { 21 + const result = schema.validate({ id: "123" }); 22 + expect(result.success).toBe(false); 23 + }); 24 + 25 + it("should reject invalid types", () => { 26 + const result = schema.validate({ id: 123, name: "test" }); 27 + expect(result.success).toBe(false); 28 + }); 29 + }); 30 + 31 + describe("complex types", () => { 32 + const schema = lx.lexicon("test.complex", { 33 + user: lx.object({ 34 + handle: lx.string({ required: true }), 35 + displayName: lx.string(), 36 + }), 37 + reply: lx.object({ 38 + text: lx.string({ required: true }), 39 + author: lx.ref("#user", { required: true }), 40 + }), 41 + main: lx.record({ 42 + key: "tid", 43 + record: lx.object({ 44 + author: lx.ref("#user", { required: true }), 45 + replies: lx.array(lx.ref("#reply")), 46 + content: lx.string({ required: true }), 47 + createdAt: lx.string({ required: true, format: "datetime" }), 48 + }), 49 + }), 50 + }); 51 + 52 + it("should validate complex nested objects", () => { 53 + const result = schema.validate({ 54 + author: { handle: "alice.bsky.social", displayName: "Alice" }, 55 + replies: [ 56 + { 57 + text: "Great post!", 58 + author: { handle: "bob.bsky.social", displayName: "Bob" }, 59 + }, 60 + ], 61 + content: "Hello world", 62 + createdAt: "2025-01-01T00:00:00Z", 63 + }); 64 + expect(result.success).toBe(true); 65 + }); 66 + 67 + it("should reject invalid nested objects", () => { 68 + const result = schema.validate({ 69 + author: { displayName: "Alice" }, // missing required handle 70 + content: "Hello world", 71 + createdAt: "2025-01-01T00:00:00Z", 72 + }); 73 + expect(result.success).toBe(false); 74 + }); 75 + }); 76 + 77 + describe("string formats", () => { 78 + const schema = lx.lexicon("test.formats", { 79 + main: lx.object({ 80 + timestamp: lx.string({ format: "datetime", required: true }), 81 + url: lx.string({ format: "uri", required: true }), 82 + atUri: lx.string({ format: "at-uri", required: true }), 83 + did: lx.string({ format: "did", required: true }), 84 + handle: lx.string({ format: "handle", required: true }), 85 + atIdentifier: lx.string({ format: "at-identifier", required: true }), 86 + nsid: lx.string({ format: "nsid", required: true }), 87 + cid: lx.string({ format: "cid", required: true }), 88 + language: lx.string({ format: "language", required: true }), 89 + }), 90 + }); 91 + 92 + it("should accept valid datetime format", () => { 93 + const result = schema.validate({ 94 + timestamp: "2025-01-01T00:00:00Z", 95 + url: "https://example.com", 96 + atUri: "at://did:plc:abc123/app.bsky.feed.post/123", 97 + did: "did:plc:abc123", 98 + handle: "alice.bsky.social", 99 + atIdentifier: "alice.bsky.social", 100 + nsid: "app.bsky.feed.post", 101 + cid: "bafyreigvpnl2njkqy7qbqthw3r3emgbz2v6w5xrr4yhwj5jzymlwnvscam", 102 + language: "en", 103 + }); 104 + expect(result.success).toBe(true); 105 + }); 106 + 107 + it("should reject invalid datetime format", () => { 108 + const result = schema.validate({ 109 + timestamp: "not-a-date", 110 + url: "https://example.com", 111 + atUri: "at://did:plc:abc123/app.bsky.feed.post/123", 112 + did: "did:plc:abc123", 113 + handle: "alice.bsky.social", 114 + atIdentifier: "alice.bsky.social", 115 + nsid: "app.bsky.feed.post", 116 + cid: "bafyreigvpnl2njkqy7qbqthw3r3emgbz2v6w5xrr4yhwj5jzymlwnvscam", 117 + language: "en", 118 + }); 119 + expect(result.success).toBe(false); 120 + }); 121 + 122 + it("should reject invalid uri format", () => { 123 + const result = schema.validate({ 124 + timestamp: "2025-01-01T00:00:00Z", 125 + url: "not a uri", 126 + atUri: "at://did:plc:abc123/app.bsky.feed.post/123", 127 + did: "did:plc:abc123", 128 + handle: "alice.bsky.social", 129 + atIdentifier: "alice.bsky.social", 130 + nsid: "app.bsky.feed.post", 131 + cid: "bafyreigvpnl2njkqy7qbqthw3r3emgbz2v6w5xrr4yhwj5jzymlwnvscam", 132 + language: "en", 133 + }); 134 + expect(result.success).toBe(false); 135 + }); 136 + 137 + it("should reject invalid did format", () => { 138 + const result = schema.validate({ 139 + timestamp: "2025-01-01T00:00:00Z", 140 + url: "https://example.com", 141 + atUri: "at://did:plc:abc123/app.bsky.feed.post/123", 142 + did: "not-a-did", 143 + handle: "alice.bsky.social", 144 + atIdentifier: "alice.bsky.social", 145 + nsid: "app.bsky.feed.post", 146 + cid: "bafyreigvpnl2njkqy7qbqthw3r3emgbz2v6w5xrr4yhwj5jzymlwnvscam", 147 + language: "en", 148 + }); 149 + expect(result.success).toBe(false); 150 + }); 151 + }); 152 + 153 + describe("array validation", () => { 154 + const schema = lx.lexicon("test.arrays", { 155 + main: lx.object({ 156 + tags: lx.array(lx.string(), { required: true }), 157 + limitedTags: lx.array(lx.string(), { 158 + required: true, 159 + minLength: 1, 160 + maxLength: 5, 161 + }), 162 + optionalTags: lx.array(lx.string()), 163 + }), 164 + }); 165 + 166 + it("should accept valid arrays", () => { 167 + const result = schema.validate({ 168 + tags: ["a", "b", "c"], 169 + limitedTags: ["x", "y"], 170 + }); 171 + expect(result.success).toBe(true); 172 + }); 173 + 174 + it("should accept empty arrays when no minLength", () => { 175 + const result = schema.validate({ 176 + tags: [], 177 + limitedTags: ["x"], 178 + }); 179 + expect(result.success).toBe(true); 180 + }); 181 + 182 + it("should reject arrays below minLength", () => { 183 + const result = schema.validate({ 184 + tags: [], 185 + limitedTags: [], 186 + }); 187 + expect(result.success).toBe(false); 188 + }); 189 + 190 + it("should reject arrays above maxLength", () => { 191 + const result = schema.validate({ 192 + tags: ["a"], 193 + limitedTags: ["a", "b", "c", "d", "e", "f"], 194 + }); 195 + expect(result.success).toBe(false); 196 + }); 197 + 198 + it("should reject arrays with invalid item types", () => { 199 + const result = schema.validate({ 200 + tags: ["a", 123, "c"], 201 + limitedTags: ["x"], 202 + }); 203 + expect(result.success).toBe(false); 204 + }); 205 + 206 + it("should allow omitting optional arrays", () => { 207 + const result = schema.validate({ 208 + tags: ["a"], 209 + limitedTags: ["x"], 210 + }); 211 + expect(result.success).toBe(true); 212 + }); 213 + }); 214 + 215 + describe("optional vs required fields", () => { 216 + const schema = lx.lexicon("test.optional", { 217 + main: lx.object({ 218 + requiredString: lx.string({ required: true }), 219 + optionalString: lx.string(), 220 + requiredNumber: lx.integer({ required: true }), 221 + optionalNumber: lx.integer(), 222 + requiredBool: lx.boolean({ required: true }), 223 + optionalBool: lx.boolean(), 224 + }), 225 + }); 226 + 227 + it("should accept data with all required fields", () => { 228 + const result = schema.validate({ 229 + requiredString: "test", 230 + requiredNumber: 42, 231 + requiredBool: true, 232 + }); 233 + expect(result.success).toBe(true); 234 + }); 235 + 236 + it("should accept data with optional fields included", () => { 237 + const result = schema.validate({ 238 + requiredString: "test", 239 + optionalString: "optional", 240 + requiredNumber: 42, 241 + optionalNumber: 100, 242 + requiredBool: true, 243 + optionalBool: false, 244 + }); 245 + expect(result.success).toBe(true); 246 + }); 247 + 248 + it("should reject data missing required string", () => { 249 + const result = schema.validate({ 250 + requiredNumber: 42, 251 + requiredBool: true, 252 + }); 253 + expect(result.success).toBe(false); 254 + }); 255 + 256 + it("should reject data missing required number", () => { 257 + const result = schema.validate({ 258 + requiredString: "test", 259 + requiredBool: true, 260 + }); 261 + expect(result.success).toBe(false); 262 + }); 263 + 264 + it("should reject data missing required boolean", () => { 265 + const result = schema.validate({ 266 + requiredString: "test", 267 + requiredNumber: 42, 268 + }); 269 + expect(result.success).toBe(false); 270 + }); 271 + 272 + it("should allow undefined for optional fields", () => { 273 + const result = schema.validate({ 274 + requiredString: "test", 275 + requiredNumber: 42, 276 + requiredBool: true, 277 + optionalString: undefined, 278 + }); 279 + expect(result.success).toBe(true); 280 + }); 281 + }); 282 + 283 + describe("error messages", () => { 284 + const schema = lx.lexicon("test.errors", { 285 + main: lx.object({ 286 + name: lx.string({ required: true }), 287 + age: lx.integer({ required: true }), 288 + }), 289 + }); 290 + 291 + it("should provide error details on validation failure", () => { 292 + const result = schema.validate({ 293 + name: 123, // wrong type 294 + }); 295 + expect(result.success).toBe(false); 296 + if (!result.success) { 297 + expect(result.error).toBeDefined(); 298 + expect(typeof result.error).toBe("object"); 299 + } 300 + }); 301 + 302 + it("should include error information for missing required fields", () => { 303 + const result = schema.validate({ 304 + name: "Alice", 305 + // missing age 306 + }); 307 + expect(result.success).toBe(false); 308 + if (!result.success) { 309 + expect(result.error).toBeDefined(); 310 + } 311 + }); 312 + 313 + it("should include error information for type mismatches", () => { 314 + const result = schema.validate({ 315 + name: "Alice", 316 + age: "not a number", 317 + }); 318 + expect(result.success).toBe(false); 319 + if (!result.success) { 320 + expect(result.error).toBeDefined(); 321 + } 322 + }); 323 + }); 324 + 325 + describe("edge cases", () => { 326 + const schema = lx.lexicon("test.edge", { 327 + main: lx.object({ 328 + name: lx.string({ required: true }), 329 + count: lx.integer({ required: true }), 330 + }), 331 + }); 332 + 333 + it("should reject null values for required fields", () => { 334 + const result = schema.validate({ 335 + name: null, 336 + count: 42, 337 + }); 338 + expect(result.success).toBe(false); 339 + }); 340 + 341 + it("should reject undefined values for required fields", () => { 342 + const result = schema.validate({ 343 + name: undefined, 344 + count: 42, 345 + }); 346 + expect(result.success).toBe(false); 347 + }); 348 + 349 + it("should handle empty strings", () => { 350 + const result = schema.validate({ 351 + name: "", 352 + count: 42, 353 + }); 354 + // Empty strings should be valid strings 355 + expect(result.success).toBe(true); 356 + }); 357 + 358 + it("should reject completely empty object", () => { 359 + const result = schema.validate({}); 360 + expect(result.success).toBe(false); 361 + }); 362 + 363 + it("should handle objects with extra properties", () => { 364 + const result = schema.validate({ 365 + name: "test", 366 + count: 42, 367 + extraProp: "should this be allowed?", 368 + }); 369 + // This test will reveal the current behavior 370 + // Lexicon spec typically allows additional properties 371 + expect(result.success).toBe(true); 372 + }); 373 + 374 + it("should reject wrong type primitives", () => { 375 + // The validator throws an error for completely wrong types 376 + try { 377 + const result = schema.validate("not an object"); 378 + expect(result.success).toBe(false); 379 + } catch (error) { 380 + // This is also acceptable - validator can throw for completely invalid types 381 + expect(error).toBeDefined(); 382 + } 383 + }); 384 + 385 + it("should reject arrays when expecting objects", () => { 386 + const result = schema.validate([]); 387 + expect(result.success).toBe(false); 388 + }); 389 + 390 + it("should handle deeply nested nulls", () => { 391 + const nestedSchema = lx.lexicon("test.nested", { 392 + main: lx.object({ 393 + user: lx.object({ 394 + name: lx.string({ required: true }), 395 + }), 396 + }), 397 + }); 398 + const result = nestedSchema.validate({ 399 + user: null, 400 + }); 401 + expect(result.success).toBe(false); 402 + }); 403 + }); 404 + 405 + describe("union types", () => { 406 + const schema = lx.lexicon("test.unions", { 407 + textPost: lx.object({ 408 + text: lx.string({ required: true }), 409 + }), 410 + imagePost: lx.object({ 411 + imageUrl: lx.string({ required: true }), 412 + }), 413 + main: lx.object({ 414 + post: lx.union(["#textPost", "#imagePost"], { required: true }), 415 + }), 416 + }); 417 + 418 + it("should accept valid first union variant", () => { 419 + const result = schema.validate({ 420 + post: { 421 + $type: "test.unions#textPost", 422 + text: "Hello world", 423 + }, 424 + }); 425 + expect(result.success).toBe(true); 426 + }); 427 + 428 + it("should accept valid second union variant", () => { 429 + const result = schema.validate({ 430 + post: { 431 + $type: "test.unions#imagePost", 432 + imageUrl: "https://example.com/image.png", 433 + }, 434 + }); 435 + expect(result.success).toBe(true); 436 + }); 437 + 438 + it("should reject data matching no union variant", () => { 439 + const result = schema.validate({ 440 + post: { 441 + $type: "test.unions#videoPost", 442 + videoUrl: "https://example.com/video.mp4", 443 + }, 444 + }); 445 + // AT Protocol unions are "open" - unknown types may be accepted 446 + // This test documents the actual behavior 447 + if (result.success) { 448 + // Open union behavior - accepts unknown types 449 + expect(result.success).toBe(true); 450 + } else { 451 + // Closed union behavior - rejects unknown types 452 + expect(result.success).toBe(false); 453 + } 454 + }); 455 + 456 + it("should reject incomplete union variant", () => { 457 + const result = schema.validate({ 458 + post: { 459 + $type: "test.unions#textPost", 460 + // missing text field 461 + }, 462 + }); 463 + expect(result.success).toBe(false); 464 + }); 465 + }); 466 + 467 + describe("token types", () => { 468 + const schema = lx.lexicon("test.tokens", { 469 + main: lx.object({ 470 + action: lx.string({ 471 + knownValues: ["like", "repost", "follow"], 472 + required: true, 473 + }), 474 + }), 475 + }); 476 + 477 + it("should accept known string values", () => { 478 + const result = schema.validate({ 479 + action: "like", 480 + }); 481 + expect(result.success).toBe(true); 482 + }); 483 + 484 + it("should accept other known string values", () => { 485 + const result = schema.validate({ 486 + action: "repost", 487 + }); 488 + expect(result.success).toBe(true); 489 + }); 490 + 491 + it("should handle unknown string values", () => { 492 + // String types with knownValues typically allow other values too 493 + const result = schema.validate({ 494 + action: "unknown-action", 495 + }); 496 + // This reveals current behavior - lexicon strings with knownValues are typically open 497 + expect(result.success).toBe(true); 498 + }); 499 + }); 500 + 501 + describe("record validation", () => { 502 + const schema = lx.lexicon("test.record", { 503 + main: lx.record({ 504 + key: "tid", 505 + record: lx.object({ 506 + title: lx.string({ required: true }), 507 + content: lx.string({ required: true }), 508 + createdAt: lx.string({ format: "datetime", required: true }), 509 + }), 510 + }), 511 + }); 512 + 513 + it("should accept valid record data", () => { 514 + const result = schema.validate({ 515 + title: "My Post", 516 + content: "This is the content", 517 + createdAt: "2025-01-01T00:00:00Z", 518 + }); 519 + expect(result.success).toBe(true); 520 + }); 521 + 522 + it("should reject record missing required fields", () => { 523 + const result = schema.validate({ 524 + title: "My Post", 525 + // missing content and createdAt 526 + }); 527 + expect(result.success).toBe(false); 528 + }); 529 + 530 + it("should reject record with invalid field types", () => { 531 + const result = schema.validate({ 532 + title: "My Post", 533 + content: 123, // wrong type 534 + createdAt: "2025-01-01T00:00:00Z", 535 + }); 536 + expect(result.success).toBe(false); 537 + }); 538 + 539 + it("should reject record with invalid datetime", () => { 540 + const result = schema.validate({ 541 + title: "My Post", 542 + content: "Content", 543 + createdAt: "not a datetime", 544 + }); 545 + expect(result.success).toBe(false); 546 + }); 547 + }); 548 + 549 + describe("bytes, CID, and unknown primitives", () => { 550 + const schema = lx.lexicon("test.primitives", { 551 + main: lx.object({ 552 + data: lx.bytes({ required: true }), 553 + hash: lx.string({ format: "cid", required: true }), 554 + metadata: lx.unknown(), 555 + }), 556 + }); 557 + 558 + it("should accept valid bytes data", () => { 559 + const result = schema.validate({ 560 + data: new Uint8Array([1, 2, 3, 4]), 561 + hash: "bafyreigvpnl2njkqy7qbqthw3r3emgbz2v6w5xrr4yhwj5jzymlwnvscam", 562 + metadata: { custom: "data" }, 563 + }); 564 + expect(result.success).toBe(true); 565 + }); 566 + 567 + it("should accept valid CID string", () => { 568 + const result = schema.validate({ 569 + data: new Uint8Array([1, 2, 3]), 570 + hash: "bafyreigvpnl2njkqy7qbqthw3r3emgbz2v6w5xrr4yhwj5jzymlwnvscam", 571 + }); 572 + expect(result.success).toBe(true); 573 + }); 574 + 575 + it("should reject invalid CID format", () => { 576 + const result = schema.validate({ 577 + data: new Uint8Array([1, 2, 3]), 578 + hash: "not-a-cid", 579 + }); 580 + expect(result.success).toBe(false); 581 + }); 582 + 583 + it("should accept any type for unknown field", () => { 584 + // Unknown fields accept object/array values but may have restrictions on primitives 585 + const objectResult = schema.validate({ 586 + data: new Uint8Array([1]), 587 + hash: "bafyreigvpnl2njkqy7qbqthw3r3emgbz2v6w5xrr4yhwj5jzymlwnvscam", 588 + metadata: { any: "object" }, 589 + }); 590 + expect(objectResult.success).toBe(true); 591 + 592 + const arrayResult = schema.validate({ 593 + data: new Uint8Array([1]), 594 + hash: "bafyreigvpnl2njkqy7qbqthw3r3emgbz2v6w5xrr4yhwj5jzymlwnvscam", 595 + metadata: [1, 2, 3], 596 + }); 597 + expect(arrayResult.success).toBe(true); 598 + }); 599 + 600 + it("should allow omitting optional unknown field", () => { 601 + const result = schema.validate({ 602 + data: new Uint8Array([1, 2, 3]), 603 + hash: "bafyreigvpnl2njkqy7qbqthw3r3emgbz2v6w5xrr4yhwj5jzymlwnvscam", 604 + }); 605 + expect(result.success).toBe(true); 606 + }); 607 + }); 608 + 609 + describe("validate with custom def parameter", () => { 610 + const schema = lx.lexicon("test.multidefs", { 611 + user: lx.object({ 612 + handle: lx.string({ required: true }), 613 + displayName: lx.string(), 614 + }), 615 + post: lx.object({ 616 + text: lx.string({ required: true }), 617 + author: lx.ref("#user", { required: true }), 618 + }), 619 + main: lx.object({ 620 + id: lx.string({ required: true }), 621 + name: lx.string({ required: true }), 622 + }), 623 + }); 624 + 625 + it("should validate against main def by default", () => { 626 + const result = schema.validate({ id: "123", name: "test" }); 627 + expect(result.success).toBe(true); 628 + }); 629 + 630 + it("should validate against main def when explicitly specified", () => { 631 + const result = schema.validate({ id: "123", name: "test" }, "main"); 632 + expect(result.success).toBe(true); 633 + }); 634 + 635 + it("should validate against user def when specified", () => { 636 + const result = schema.validate({ handle: "alice.bsky.social" }, "user"); 637 + expect(result.success).toBe(true); 638 + }); 639 + 640 + it("should validate against post def when specified", () => { 641 + const result = schema.validate( 642 + { 643 + text: "Hello world", 644 + author: { handle: "bob.bsky.social", displayName: "Bob" }, 645 + }, 646 + "post", 647 + ); 648 + expect(result.success).toBe(true); 649 + }); 650 + 651 + it("should reject invalid data for user def", () => { 652 + const result = schema.validate({ displayName: "Alice" }, "user"); 653 + expect(result.success).toBe(false); 654 + }); 655 + 656 + it("should reject invalid data for post def", () => { 657 + const result = schema.validate( 658 + { 659 + text: "Hello world", 660 + // missing author 661 + }, 662 + "post", 663 + ); 664 + expect(result.success).toBe(false); 665 + }); 666 + 667 + it("should reject main def data when validating against user def", () => { 668 + const result = schema.validate({ id: "123", name: "test" }, "user"); 669 + expect(result.success).toBe(false); 670 + }); 671 + 672 + it("should reject user def data when validating against main def", () => { 673 + const result = schema.validate({ handle: "alice.bsky.social" }); 674 + expect(result.success).toBe(false); 675 + }); 676 + 677 + it("should accept valid data with optional fields for user def", () => { 678 + const result = schema.validate( 679 + { handle: "alice.bsky.social", displayName: "Alice" }, 680 + "user", 681 + ); 682 + expect(result.success).toBe(true); 683 + }); 684 + }); 685 + 686 + describe("deep nesting", () => { 687 + const schema = lx.lexicon("test.deep", { 688 + level3: lx.object({ 689 + value: lx.string({ required: true }), 690 + }), 691 + level2: lx.object({ 692 + nested: lx.ref("#level3", { required: true }), 693 + id: lx.string({ required: true }), 694 + }), 695 + level1: lx.object({ 696 + nested: lx.ref("#level2", { required: true }), 697 + name: lx.string({ required: true }), 698 + }), 699 + main: lx.object({ 700 + nested: lx.ref("#level1", { required: true }), 701 + title: lx.string({ required: true }), 702 + }), 703 + }); 704 + 705 + it("should validate deeply nested valid data", () => { 706 + const result = schema.validate({ 707 + title: "Top level", 708 + nested: { 709 + name: "Level 1", 710 + nested: { 711 + id: "level2-id", 712 + nested: { 713 + value: "deep value", 714 + }, 715 + }, 716 + }, 717 + }); 718 + expect(result.success).toBe(true); 719 + }); 720 + 721 + it("should reject invalid data at deepest level", () => { 722 + const result = schema.validate({ 723 + title: "Top level", 724 + nested: { 725 + name: "Level 1", 726 + nested: { 727 + id: "level2-id", 728 + nested: { 729 + // missing value 730 + }, 731 + }, 732 + }, 733 + }); 734 + expect(result.success).toBe(false); 735 + }); 736 + 737 + it("should reject invalid data at middle level", () => { 738 + const result = schema.validate({ 739 + title: "Top level", 740 + nested: { 741 + name: "Level 1", 742 + nested: { 743 + // missing id 744 + nested: { 745 + value: "deep value", 746 + }, 747 + }, 748 + }, 749 + }); 750 + expect(result.success).toBe(false); 751 + }); 752 + 753 + it("should handle arrays of deeply nested objects", () => { 754 + const arraySchema = lx.lexicon("test.array-deep", { 755 + item: lx.object({ 756 + data: lx.object({ 757 + value: lx.string({ required: true }), 758 + }), 759 + }), 760 + main: lx.object({ 761 + items: lx.array(lx.ref("#item"), { required: true }), 762 + }), 763 + }); 764 + 765 + const result = arraySchema.validate({ 766 + items: [ 767 + { data: { value: "first" } }, 768 + { data: { value: "second" } }, 769 + { data: { value: "third" } }, 770 + ], 771 + }); 772 + expect(result.success).toBe(true); 773 + }); 774 + 775 + it("should reject invalid item in array of nested objects", () => { 776 + const arraySchema = lx.lexicon("test.array-deep-invalid", { 777 + item: lx.object({ 778 + data: lx.object({ 779 + value: lx.string({ required: true }), 780 + }), 781 + }), 782 + main: lx.object({ 783 + items: lx.array(lx.ref("#item"), { required: true }), 784 + }), 785 + }); 786 + 787 + const result = arraySchema.validate({ 788 + items: [ 789 + { data: { value: "first" } }, 790 + { data: {} }, // missing value in second item 791 + { data: { value: "third" } }, 792 + ], 793 + }); 794 + expect(result.success).toBe(false); 795 + }); 796 + });
+19
packages/prototypey/core/type-utils.ts
··· 1 + /** 2 + * Converts a string union type to a tuple type 3 + * @example 4 + * type Colors = "red" | "green" | "blue"; 5 + * type ColorTuple = UnionToTuple<Colors>; // ["red", "green", "blue"] 6 + */ 7 + export type UnionToTuple<T> = ( 8 + (T extends unknown ? (x: () => T) => void : never) extends ( 9 + x: infer I, 10 + ) => void 11 + ? I 12 + : never 13 + ) extends () => infer R 14 + ? [...UnionToTuple<Exclude<T, R>>, R] 15 + : []; 16 + 17 + export type Prettify<T> = { 18 + [K in keyof T]: T[K]; 19 + } & {};
+19 -8
packages/prototypey/package.json
··· 1 1 { 2 2 "name": "prototypey", 3 - "version": "0.0.0", 4 - "description": "Type-safe lexicon inference for ATProto schemas", 3 + "version": "0.3.8", 4 + "description": "atproto lexicon typescript toolkit", 5 5 "repository": { 6 6 "type": "git", 7 7 "url": "git+https://github.com/tylersayshi/prototypey.git", 8 8 "directory": "packages/prototypey" 9 9 }, 10 + "homepage": "https://prototypey.org", 10 11 "license": "MIT", 12 + "main": "./lib/core/main.js", 11 13 "author": { 12 14 "name": "tylersayshi", 13 15 "email": "hi@tylur.dev" 14 16 }, 15 17 "type": "module", 16 - "main": "lib/index.js", 18 + "bin": { 19 + "prototypey": "./lib/cli/main.js" 20 + }, 17 21 "exports": { 18 - ".": "./lib/index.js", 19 - "./lib/*.d.ts": "./lib/*.d.ts" 22 + ".": "./lib/core/main.js", 23 + "./lib/core/*.d.ts": "./lib/core/*.d.ts" 20 24 }, 21 25 "files": [ 22 26 "lib/", ··· 24 28 ], 25 29 "scripts": { 26 30 "build": "tsdown", 31 + "lint": "eslint .", 27 32 "test": "vitest run", 28 - "test:bench": "node tests/infer.bench.ts", 33 + "test:bench": "node core/tests/infer.bench.ts", 34 + "test:bench:validation": "vitest bench --run tests/validation-baseline.bench.ts", 29 35 "test:update-snapshots": "vitest run -u", 30 36 "tsc": "tsc" 31 37 }, 38 + "dependencies": { 39 + "@atproto/lexicon": "^0.5.2", 40 + "sade": "^1.8.1", 41 + "tinyglobby": "^0.2.15" 42 + }, 32 43 "devDependencies": { 33 44 "@ark/attest": "^0.49.0", 34 45 "@types/node": "24.0.4", 35 - "tsdown": "0.12.7", 36 - "typescript": "5.8.3", 46 + "tsdown": "^0.15.12", 47 + "typescript": "5.9.3", 37 48 "vitest": "^3.2.4" 38 49 }, 39 50 "engines": {
-2
packages/prototypey/src/index.ts
··· 1 - export * from "./lib.ts"; 2 - export * from "./infer.ts";
-142
packages/prototypey/src/infer.ts
··· 1 - import { Prettify } from "./type-utils.ts"; 2 - 3 - /* eslint-disable @typescript-eslint/no-empty-object-type */ 4 - type InferType<T> = T extends { type: "record" } 5 - ? InferRecord<T> 6 - : T extends { type: "object" } 7 - ? InferObject<T> 8 - : T extends { type: "array" } 9 - ? InferArray<T> 10 - : T extends { type: "params" } 11 - ? InferParams<T> 12 - : T extends { type: "union" } 13 - ? InferUnion<T> 14 - : T extends { type: "token" } 15 - ? InferToken<T> 16 - : T extends { type: "ref" } 17 - ? InferRef<T> 18 - : T extends { type: "unknown" } 19 - ? unknown 20 - : T extends { type: "null" } 21 - ? null 22 - : T extends { type: "boolean" } 23 - ? boolean 24 - : T extends { type: "integer" } 25 - ? number 26 - : T extends { type: "string" } 27 - ? string 28 - : T extends { type: "bytes" } 29 - ? Uint8Array 30 - : T extends { type: "cid-link" } 31 - ? string 32 - : T extends { type: "blob" } 33 - ? Blob 34 - : never; 35 - 36 - type InferToken<T> = T extends { enum: readonly (infer U)[] } ? U : string; 37 - 38 - export type GetRequired<T> = T extends { required: readonly (infer R)[] } 39 - ? R 40 - : never; 41 - export type GetNullable<T> = T extends { nullable: readonly (infer N)[] } 42 - ? N 43 - : never; 44 - 45 - type InferObject< 46 - T, 47 - Nullable extends string = GetNullable<T> & string, 48 - Required extends string = GetRequired<T> & string, 49 - NullableAndRequired extends string = Required & Nullable & string, 50 - Normal extends string = "properties" extends keyof T 51 - ? Exclude<keyof T["properties"], Required | Nullable> & string 52 - : never, 53 - > = Prettify< 54 - T extends { properties: infer P } 55 - ? { 56 - -readonly [K in Normal]?: InferType<P[K & keyof P]>; 57 - } & { 58 - -readonly [K in Exclude<Required, NullableAndRequired>]-?: InferType< 59 - P[K & keyof P] 60 - >; 61 - } & { 62 - -readonly [K in Exclude<Nullable, NullableAndRequired>]?: InferType< 63 - P[K & keyof P] 64 - > | null; 65 - } & { 66 - -readonly [K in NullableAndRequired]: InferType<P[K & keyof P]> | null; 67 - } 68 - : {} 69 - >; 70 - 71 - type InferArray<T> = T extends { items: infer Items } 72 - ? InferType<Items>[] 73 - : never[]; 74 - 75 - type InferUnion<T> = T extends { refs: readonly (infer R)[] } 76 - ? R extends string 77 - ? { $type: R; [key: string]: unknown } 78 - : never 79 - : never; 80 - 81 - type InferRef<T> = T extends { ref: infer R } 82 - ? R extends string 83 - ? { $type: R; [key: string]: unknown } 84 - : unknown 85 - : unknown; 86 - 87 - type InferParams<T> = InferObject<T>; 88 - 89 - type InferRecord<T> = T extends { record: infer R } 90 - ? R extends { type: "object" } 91 - ? InferObject<R> 92 - : R extends { type: "union" } 93 - ? InferUnion<R> 94 - : unknown 95 - : unknown; 96 - 97 - /** 98 - * Recursively replaces stub references in a type with their actual definitions. 99 - * Detects circular references and missing references, returning string literal error messages. 100 - */ 101 - type ReplaceRefsInType<T, Defs, Visited = never> = 102 - // Check if this is a ref stub type (has $type starting with #) 103 - T extends { $type: `#${infer DefName}` } 104 - ? DefName extends keyof Defs 105 - ? // Check for circular reference 106 - DefName extends Visited 107 - ? `[Circular reference detected: #${DefName}]` 108 - : // Recursively resolve the ref and preserve the $type marker 109 - Prettify< 110 - ReplaceRefsInType<Defs[DefName], Defs, Visited | DefName> & { 111 - $type: T["$type"]; 112 - } 113 - > 114 - : // Reference not found in definitions 115 - `[Reference not found: #${DefName}]` 116 - : // Handle arrays (but not Uint8Array or other typed arrays) 117 - T extends Uint8Array | Blob 118 - ? T 119 - : T extends readonly (infer Item)[] 120 - ? ReplaceRefsInType<Item, Defs, Visited>[] 121 - : // Handle plain objects (exclude built-in types and functions) 122 - T extends object 123 - ? T extends (...args: unknown[]) => unknown 124 - ? T 125 - : { [K in keyof T]: ReplaceRefsInType<T[K], Defs, Visited> } 126 - : // Primitives pass through unchanged 127 - T; 128 - 129 - /** 130 - * Infers the TypeScript type for a lexicon namespace, returning only the 'main' definition 131 - * with all local refs (#user, #post, etc.) resolved to their actual types. 132 - */ 133 - export type Infer< 134 - T extends { json: { id: string; defs: Record<string, unknown> } }, 135 - > = Prettify< 136 - "main" extends keyof T["json"]["defs"] 137 - ? { $type: T["json"]["id"] } & ReplaceRefsInType< 138 - InferType<T["json"]["defs"]["main"]>, 139 - { [K in keyof T["json"]["defs"]]: InferType<T["json"]["defs"][K]> } 140 - > 141 - : never 142 - >;
-579
packages/prototypey/src/lib.ts
··· 1 - /* eslint-disable @typescript-eslint/no-empty-object-type */ 2 - import type { Infer } from "./infer.ts"; 3 - import type { UnionToTuple } from "./type-utils.ts"; 4 - 5 - /** @see https://atproto.com/specs/lexicon#overview-of-types */ 6 - type LexiconType = 7 - // Concrete types 8 - | "null" 9 - | "boolean" 10 - | "integer" 11 - | "string" 12 - | "bytes" 13 - | "cid-link" 14 - | "blob" 15 - // Container types 16 - | "array" 17 - | "object" 18 - | "params" 19 - // Meta types 20 - | "token" 21 - | "ref" 22 - | "union" 23 - | "unknown" 24 - // Primary types 25 - | "record" 26 - | "query" 27 - | "procedure" 28 - | "subscription"; 29 - 30 - /** 31 - * Common options available for lexicon items. 32 - * @see https://atproto.com/specs/lexicon#string-formats 33 - */ 34 - interface LexiconItemCommonOptions { 35 - /** Indicates this field must be provided */ 36 - required?: boolean; 37 - /** Indicates this field can be explicitly set to null */ 38 - nullable?: boolean; 39 - } 40 - 41 - /** 42 - * Base interface for all lexicon items. 43 - * @see https://atproto.com/specs/lexicon#overview-of-types 44 - */ 45 - interface LexiconItem extends LexiconItemCommonOptions { 46 - type: LexiconType; 47 - } 48 - 49 - /** 50 - * Definition in a lexicon namespace. 51 - * @see https://atproto.com/specs/lexicon#lexicon-document 52 - */ 53 - interface Def { 54 - type: LexiconType; 55 - } 56 - 57 - /** 58 - * Lexicon namespace document structure. 59 - * @see https://atproto.com/specs/lexicon#lexicon-document 60 - */ 61 - interface LexiconNamespace { 62 - /** Namespaced identifier (NSID) for this lexicon */ 63 - id: string; 64 - /** Named definitions within this namespace */ 65 - defs: Record<string, Def>; 66 - } 67 - 68 - /** 69 - * String type options. 70 - * @see https://atproto.com/specs/lexicon#string 71 - */ 72 - interface StringOptions extends LexiconItemCommonOptions { 73 - /** 74 - * Semantic string format constraint. 75 - * @see https://atproto.com/specs/lexicon#string-formats 76 - */ 77 - format?: 78 - | "at-identifier" // Handle or DID 79 - | "at-uri" // AT Protocol URI 80 - | "cid" // Content Identifier 81 - | "datetime" // Timestamp (UTC, ISO 8601) 82 - | "did" // Decentralized Identifier 83 - | "handle" // User handle identifier 84 - | "nsid" // Namespaced Identifier 85 - | "tid" // Timestamp Identifier 86 - | "record-key" // Repository record key 87 - | "uri" // Generic URI 88 - | "language"; // IETF BCP 47 language tag 89 - /** Maximum string length in bytes */ 90 - maxLength?: number; 91 - /** Minimum string length in bytes */ 92 - minLength?: number; 93 - /** Maximum string length in Unicode graphemes */ 94 - maxGraphemes?: number; 95 - /** Minimum string length in Unicode graphemes */ 96 - minGraphemes?: number; 97 - /** Hints at expected values, not enforced */ 98 - knownValues?: string[]; 99 - /** Restricts to an exact set of string values */ 100 - enum?: string[]; 101 - /** Default value if not provided */ 102 - default?: string; 103 - /** Fixed, unchangeable value */ 104 - const?: string; 105 - } 106 - 107 - /** 108 - * Boolean type options. 109 - * @see https://atproto.com/specs/lexicon#boolean 110 - */ 111 - interface BooleanOptions extends LexiconItemCommonOptions { 112 - /** Default value if not provided */ 113 - default?: boolean; 114 - /** Fixed, unchangeable value */ 115 - const?: boolean; 116 - } 117 - 118 - /** 119 - * Integer type options. 120 - * @see https://atproto.com/specs/lexicon#integer 121 - */ 122 - interface IntegerOptions extends LexiconItemCommonOptions { 123 - /** Minimum allowed value (inclusive) */ 124 - minimum?: number; 125 - /** Maximum allowed value (inclusive) */ 126 - maximum?: number; 127 - /** Restricts to an exact set of integer values */ 128 - enum?: number[]; 129 - /** Default value if not provided */ 130 - default?: number; 131 - /** Fixed, unchangeable value */ 132 - const?: number; 133 - } 134 - 135 - /** 136 - * Bytes type options for arbitrary byte arrays. 137 - * @see https://atproto.com/specs/lexicon#bytes 138 - */ 139 - interface BytesOptions extends LexiconItemCommonOptions { 140 - /** Minimum byte array length */ 141 - minLength?: number; 142 - /** Maximum byte array length */ 143 - maxLength?: number; 144 - } 145 - 146 - /** 147 - * Blob type options for binary data with MIME types. 148 - * @see https://atproto.com/specs/lexicon#blob 149 - */ 150 - interface BlobOptions extends LexiconItemCommonOptions { 151 - /** Allowed MIME types (e.g., ["image/png", "image/jpeg"]) */ 152 - accept?: string[]; 153 - /** Maximum blob size in bytes */ 154 - maxSize?: number; 155 - } 156 - 157 - /** 158 - * Array type options. 159 - * @see https://atproto.com/specs/lexicon#array 160 - */ 161 - interface ArrayOptions extends LexiconItemCommonOptions { 162 - /** Minimum array length */ 163 - minLength?: number; 164 - /** Maximum array length */ 165 - maxLength?: number; 166 - } 167 - 168 - /** 169 - * Record type options for repository records. 170 - * @see https://atproto.com/specs/lexicon#record 171 - */ 172 - interface RecordOptions { 173 - /** Record key strategy: "self" for self-describing or "tid" for timestamp IDs */ 174 - key: "self" | "tid"; 175 - /** Object schema defining the record structure */ 176 - record: { type: "object" }; 177 - /** Human-readable description */ 178 - description?: string; 179 - } 180 - 181 - /** 182 - * Union type options for multiple possible types. 183 - * @see https://atproto.com/specs/lexicon#union 184 - */ 185 - interface UnionOptions extends LexiconItemCommonOptions { 186 - /** If true, only listed refs are allowed; if false, additional types may be added */ 187 - closed?: boolean; 188 - } 189 - 190 - /** 191 - * Map of property names to their lexicon item definitions. 192 - * @see https://atproto.com/specs/lexicon#object 193 - */ 194 - type ObjectProperties = Record< 195 - string, 196 - { 197 - type: LexiconType; 198 - } 199 - >; 200 - 201 - type RequiredKeys<T> = { 202 - [K in keyof T]: T[K] extends { required: true } ? K : never; 203 - }[keyof T]; 204 - 205 - type NullableKeys<T> = { 206 - [K in keyof T]: T[K] extends { nullable: true } ? K : never; 207 - }[keyof T]; 208 - 209 - /** 210 - * Resulting object schema with required and nullable fields extracted. 211 - * @see https://atproto.com/specs/lexicon#object 212 - */ 213 - type ObjectResult<T extends ObjectProperties> = { 214 - type: "object"; 215 - /** Property definitions */ 216 - properties: { 217 - [K in keyof T]: T[K] extends { type: "object" } 218 - ? T[K] 219 - : Omit<T[K], "required" | "nullable">; 220 - }; 221 - } & ([RequiredKeys<T>] extends [never] 222 - ? {} 223 - : { required: UnionToTuple<RequiredKeys<T>> }) & 224 - ([NullableKeys<T>] extends [never] 225 - ? {} 226 - : { nullable: UnionToTuple<NullableKeys<T>> }); 227 - 228 - /** 229 - * Map of parameter names to their lexicon item definitions. 230 - * @see https://atproto.com/specs/lexicon#params 231 - */ 232 - type ParamsProperties = Record<string, LexiconItem>; 233 - 234 - /** 235 - * Resulting params schema with required fields extracted. 236 - * @see https://atproto.com/specs/lexicon#params 237 - */ 238 - type ParamsResult<T extends ParamsProperties> = { 239 - type: "params"; 240 - /** Parameter definitions */ 241 - properties: { 242 - [K in keyof T]: Omit<T[K], "required" | "nullable">; 243 - }; 244 - } & ([RequiredKeys<T>] extends [never] 245 - ? {} 246 - : { required: UnionToTuple<RequiredKeys<T>> }); 247 - 248 - /** 249 - * HTTP request or response body schema. 250 - * @see https://atproto.com/specs/lexicon#http-endpoints 251 - */ 252 - interface BodySchema { 253 - /** MIME type encoding (typically "application/json") */ 254 - encoding: "application/json" | (string & {}); 255 - /** Human-readable description */ 256 - description?: string; 257 - /** Object schema defining the body structure */ 258 - schema?: ObjectResult<ObjectProperties>; 259 - } 260 - 261 - /** 262 - * Error definition for HTTP endpoints. 263 - * @see https://atproto.com/specs/lexicon#http-endpoints 264 - */ 265 - interface ErrorDef { 266 - /** Error name/code */ 267 - name: string; 268 - /** Human-readable error description */ 269 - description?: string; 270 - } 271 - 272 - /** 273 - * Query endpoint options (HTTP GET). 274 - * @see https://atproto.com/specs/lexicon#query 275 - */ 276 - interface QueryOptions { 277 - /** Human-readable description */ 278 - description?: string; 279 - /** Query string parameters */ 280 - parameters?: ParamsResult<ParamsProperties>; 281 - /** Response body schema */ 282 - output?: BodySchema; 283 - /** Possible error responses */ 284 - errors?: ErrorDef[]; 285 - } 286 - 287 - /** 288 - * Procedure endpoint options (HTTP POST). 289 - * @see https://atproto.com/specs/lexicon#procedure 290 - */ 291 - interface ProcedureOptions { 292 - /** Human-readable description */ 293 - description?: string; 294 - /** Query string parameters */ 295 - parameters?: ParamsResult<ParamsProperties>; 296 - /** Request body schema */ 297 - input?: BodySchema; 298 - /** Response body schema */ 299 - output?: BodySchema; 300 - /** Possible error responses */ 301 - errors?: ErrorDef[]; 302 - } 303 - 304 - /** 305 - * WebSocket message schema for subscriptions. 306 - * @see https://atproto.com/specs/lexicon#subscription 307 - */ 308 - interface MessageSchema { 309 - /** Human-readable description */ 310 - description?: string; 311 - /** Union of possible message types */ 312 - schema: { type: "union"; refs: readonly string[] }; 313 - } 314 - 315 - /** 316 - * Subscription endpoint options (WebSocket). 317 - * @see https://atproto.com/specs/lexicon#subscription 318 - */ 319 - interface SubscriptionOptions { 320 - /** Human-readable description */ 321 - description?: string; 322 - /** Query string parameters */ 323 - parameters?: ParamsResult<ParamsProperties>; 324 - /** Message schema for events */ 325 - message?: MessageSchema; 326 - /** Possible error responses */ 327 - errors?: ErrorDef[]; 328 - } 329 - 330 - class Namespace<T extends LexiconNamespace> { 331 - public json: T; 332 - public infer: Infer<{ json: T }> = null as unknown as Infer<{ json: T }>; 333 - 334 - constructor(json: T) { 335 - this.json = json; 336 - } 337 - } 338 - 339 - /** 340 - * Main API for creating lexicon schemas. 341 - * @see https://atproto.com/specs/lexicon 342 - */ 343 - export const lx = { 344 - /** 345 - * Creates a null type. 346 - * @see https://atproto.com/specs/lexicon#null 347 - */ 348 - null( 349 - options?: LexiconItemCommonOptions, 350 - ): { type: "null" } & LexiconItemCommonOptions { 351 - return { 352 - type: "null", 353 - ...options, 354 - }; 355 - }, 356 - /** 357 - * Creates a boolean type with optional constraints. 358 - * @see https://atproto.com/specs/lexicon#boolean 359 - */ 360 - boolean<T extends BooleanOptions>(options?: T): T & { type: "boolean" } { 361 - return { 362 - type: "boolean", 363 - ...options, 364 - } as T & { type: "boolean" }; 365 - }, 366 - /** 367 - * Creates an integer type with optional min/max and enum constraints. 368 - * @see https://atproto.com/specs/lexicon#integer 369 - */ 370 - integer<T extends IntegerOptions>(options?: T): T & { type: "integer" } { 371 - return { 372 - type: "integer", 373 - ...options, 374 - } as T & { type: "integer" }; 375 - }, 376 - /** 377 - * Creates a string type with optional format, length, and value constraints. 378 - * @see https://atproto.com/specs/lexicon#string 379 - */ 380 - string<T extends StringOptions>(options?: T): T & { type: "string" } { 381 - return { 382 - type: "string", 383 - ...options, 384 - } as T & { type: "string" }; 385 - }, 386 - /** 387 - * Creates an unknown type for flexible, unvalidated objects. 388 - * @see https://atproto.com/specs/lexicon#unknown 389 - */ 390 - unknown( 391 - options?: LexiconItemCommonOptions, 392 - ): { type: "unknown" } & LexiconItemCommonOptions { 393 - return { 394 - type: "unknown", 395 - ...options, 396 - }; 397 - }, 398 - /** 399 - * Creates a bytes type for arbitrary byte arrays. 400 - * @see https://atproto.com/specs/lexicon#bytes 401 - */ 402 - bytes<T extends BytesOptions>(options?: T): T & { type: "bytes" } { 403 - return { 404 - type: "bytes", 405 - ...options, 406 - } as T & { type: "bytes" }; 407 - }, 408 - /** 409 - * Creates a CID link reference to content-addressed data. 410 - * @see https://atproto.com/specs/lexicon#cid-link 411 - */ 412 - cidLink<Link extends string>(link: Link): { type: "cid-link"; $link: Link } { 413 - return { 414 - type: "cid-link", 415 - $link: link, 416 - }; 417 - }, 418 - /** 419 - * Creates a blob type for binary data with MIME type constraints. 420 - * @see https://atproto.com/specs/lexicon#blob 421 - */ 422 - blob<T extends BlobOptions>(options?: T): T & { type: "blob" } { 423 - return { 424 - type: "blob", 425 - ...options, 426 - } as T & { type: "blob" }; 427 - }, 428 - /** 429 - * Creates an array type with item schema and length constraints. 430 - * @see https://atproto.com/specs/lexicon#array 431 - */ 432 - array<Items extends { type: LexiconType }, Options extends ArrayOptions>( 433 - items: Items, 434 - options?: Options, 435 - ): Options & { type: "array"; items: Items } { 436 - return { 437 - type: "array", 438 - items, 439 - ...options, 440 - } as Options & { type: "array"; items: Items }; 441 - }, 442 - /** 443 - * Creates a token type for symbolic values in unions. 444 - * @see https://atproto.com/specs/lexicon#token 445 - */ 446 - token<Description extends string>( 447 - description: Description, 448 - ): { type: "token"; description: Description } { 449 - return { type: "token", description }; 450 - }, 451 - /** 452 - * Creates a reference to another schema definition. 453 - * @see https://atproto.com/specs/lexicon#ref 454 - */ 455 - ref<Ref extends string>( 456 - ref: Ref, 457 - options?: LexiconItemCommonOptions, 458 - ): LexiconItemCommonOptions & { type: "ref"; ref: Ref } { 459 - return { 460 - type: "ref", 461 - ref, 462 - ...options, 463 - } as LexiconItemCommonOptions & { type: "ref"; ref: Ref }; 464 - }, 465 - /** 466 - * Creates a union type for multiple possible type variants. 467 - * @see https://atproto.com/specs/lexicon#union 468 - */ 469 - union<const Refs extends readonly string[], Options extends UnionOptions>( 470 - refs: Refs, 471 - options?: Options, 472 - ): Options & { type: "union"; refs: Refs } { 473 - return { 474 - type: "union", 475 - refs, 476 - ...options, 477 - } as Options & { type: "union"; refs: Refs }; 478 - }, 479 - /** 480 - * Creates a record type for repository records. 481 - * @see https://atproto.com/specs/lexicon#record 482 - */ 483 - record<T extends RecordOptions>(options: T): T & { type: "record" } { 484 - return { 485 - type: "record", 486 - ...options, 487 - }; 488 - }, 489 - /** 490 - * Creates an object type with defined properties. 491 - * @see https://atproto.com/specs/lexicon#object 492 - */ 493 - object<T extends ObjectProperties>(options: T): ObjectResult<T> { 494 - const required = Object.keys(options).filter( 495 - (key) => "required" in options[key] && options[key].required, 496 - ); 497 - const nullable = Object.keys(options).filter( 498 - (key) => "nullable" in options[key] && options[key].nullable, 499 - ); 500 - const result: Record<string, unknown> = { 501 - type: "object", 502 - properties: options, 503 - }; 504 - if (required.length > 0) { 505 - result.required = required; 506 - } 507 - if (nullable.length > 0) { 508 - result.nullable = nullable; 509 - } 510 - return result as ObjectResult<T>; 511 - }, 512 - /** 513 - * Creates a params type for query string parameters. 514 - * @see https://atproto.com/specs/lexicon#params 515 - */ 516 - params<Properties extends ParamsProperties>( 517 - properties: Properties, 518 - ): ParamsResult<Properties> { 519 - const required = Object.keys(properties).filter( 520 - (key) => properties[key].required, 521 - ); 522 - const result: Record<string, unknown> = { 523 - type: "params", 524 - properties, 525 - }; 526 - if (required.length > 0) { 527 - result.required = required; 528 - } 529 - return result as ParamsResult<Properties>; 530 - }, 531 - /** 532 - * Creates a query endpoint definition (HTTP GET). 533 - * @see https://atproto.com/specs/lexicon#query 534 - */ 535 - query<T extends QueryOptions>(options?: T): T & { type: "query" } { 536 - return { 537 - type: "query", 538 - ...options, 539 - } as T & { type: "query" }; 540 - }, 541 - /** 542 - * Creates a procedure endpoint definition (HTTP POST). 543 - * @see https://atproto.com/specs/lexicon#procedure 544 - */ 545 - procedure<T extends ProcedureOptions>( 546 - options?: T, 547 - ): T & { type: "procedure" } { 548 - return { 549 - type: "procedure", 550 - ...options, 551 - } as T & { type: "procedure" }; 552 - }, 553 - /** 554 - * Creates a subscription endpoint definition (WebSocket). 555 - * @see https://atproto.com/specs/lexicon#subscription 556 - */ 557 - subscription<T extends SubscriptionOptions>( 558 - options?: T, 559 - ): T & { type: "subscription" } { 560 - return { 561 - type: "subscription", 562 - ...options, 563 - } as T & { type: "subscription" }; 564 - }, 565 - /** 566 - * Creates a lexicon schema document. 567 - * @see https://atproto.com/specs/lexicon#lexicon-document 568 - */ 569 - lexicon<ID extends string, D extends LexiconNamespace["defs"]>( 570 - id: ID, 571 - defs: D, 572 - ): Namespace<{ lexicon: 1; id: ID; defs: D }> { 573 - return new Namespace({ 574 - lexicon: 1, 575 - id, 576 - defs, 577 - }); 578 - }, 579 - };
-19
packages/prototypey/src/type-utils.ts
··· 1 - /** 2 - * Converts a string union type to a tuple type 3 - * @example 4 - * type Colors = "red" | "green" | "blue"; 5 - * type ColorTuple = UnionToTuple<Colors>; // ["red", "green", "blue"] 6 - */ 7 - export type UnionToTuple<T> = ( 8 - (T extends unknown ? (x: () => T) => void : never) extends ( 9 - x: infer I, 10 - ) => void 11 - ? I 12 - : never 13 - ) extends () => infer R 14 - ? [...UnionToTuple<Exclude<T, R>>, R] 15 - : []; 16 - 17 - export type Prettify<T> = { 18 - [K in keyof T]: T[K]; 19 - } & {};
-40
packages/prototypey/tests/base-case.test.ts
··· 1 - import { expect, test } from "vitest"; 2 - import { lx } from "../src/lib.ts"; 3 - 4 - test("app.bsky.actor.profile", () => { 5 - const profileNamespace = lx.lexicon("app.bsky.actor.profile", { 6 - main: lx.record({ 7 - key: "self", 8 - record: lx.object({ 9 - displayName: lx.string({ maxLength: 64, maxGraphemes: 64 }), 10 - description: lx.string({ maxLength: 256, maxGraphemes: 256 }), 11 - }), 12 - }), 13 - }); 14 - 15 - expect(profileNamespace.json).toEqual({ 16 - lexicon: 1, 17 - id: "app.bsky.actor.profile", 18 - defs: { 19 - main: { 20 - type: "record", 21 - key: "self", 22 - record: { 23 - type: "object", 24 - properties: { 25 - displayName: { 26 - type: "string", 27 - maxLength: 64, 28 - maxGraphemes: 64, 29 - }, 30 - description: { 31 - type: "string", 32 - maxLength: 256, 33 - maxGraphemes: 256, 34 - }, 35 - }, 36 - }, 37 - }, 38 - }, 39 - }); 40 - });
-867
packages/prototypey/tests/bsky-actor.test.ts
··· 1 - import { expect, test } from "vitest"; 2 - import { lx } from "../src/lib.ts"; 3 - 4 - test("app.bsky.actor.defs - profileViewBasic", () => { 5 - const profileViewBasic = lx.object({ 6 - did: lx.string({ required: true, format: "did" }), 7 - handle: lx.string({ required: true, format: "handle" }), 8 - displayName: lx.string({ maxGraphemes: 64, maxLength: 640 }), 9 - pronouns: lx.string(), 10 - avatar: lx.string({ format: "uri" }), 11 - associated: lx.ref("#profileAssociated"), 12 - viewer: lx.ref("#viewerState"), 13 - labels: lx.array(lx.ref("com.atproto.label.defs#label")), 14 - createdAt: lx.string({ format: "datetime" }), 15 - verification: lx.ref("#verificationState"), 16 - status: lx.ref("#statusView"), 17 - }); 18 - 19 - expect(profileViewBasic).toEqual({ 20 - type: "object", 21 - properties: { 22 - did: { type: "string", required: true, format: "did" }, 23 - handle: { type: "string", required: true, format: "handle" }, 24 - displayName: { type: "string", maxGraphemes: 64, maxLength: 640 }, 25 - pronouns: { type: "string" }, 26 - avatar: { type: "string", format: "uri" }, 27 - associated: { type: "ref", ref: "#profileAssociated" }, 28 - viewer: { type: "ref", ref: "#viewerState" }, 29 - labels: { 30 - type: "array", 31 - items: { type: "ref", ref: "com.atproto.label.defs#label" }, 32 - }, 33 - createdAt: { type: "string", format: "datetime" }, 34 - verification: { type: "ref", ref: "#verificationState" }, 35 - status: { type: "ref", ref: "#statusView" }, 36 - }, 37 - required: ["did", "handle"], 38 - }); 39 - }); 40 - 41 - test("app.bsky.actor.defs - profileView", () => { 42 - const profileView = lx.object({ 43 - did: lx.string({ required: true, format: "did" }), 44 - handle: lx.string({ required: true, format: "handle" }), 45 - displayName: lx.string({ maxGraphemes: 64, maxLength: 640 }), 46 - pronouns: lx.string(), 47 - description: lx.string({ maxGraphemes: 256, maxLength: 2560 }), 48 - avatar: lx.string({ format: "uri" }), 49 - associated: lx.ref("#profileAssociated"), 50 - indexedAt: lx.string({ format: "datetime" }), 51 - createdAt: lx.string({ format: "datetime" }), 52 - viewer: lx.ref("#viewerState"), 53 - labels: lx.array(lx.ref("com.atproto.label.defs#label")), 54 - verification: lx.ref("#verificationState"), 55 - status: lx.ref("#statusView"), 56 - }); 57 - 58 - expect(profileView).toEqual({ 59 - type: "object", 60 - properties: { 61 - did: { type: "string", required: true, format: "did" }, 62 - handle: { type: "string", required: true, format: "handle" }, 63 - displayName: { type: "string", maxGraphemes: 64, maxLength: 640 }, 64 - pronouns: { type: "string" }, 65 - description: { type: "string", maxGraphemes: 256, maxLength: 2560 }, 66 - avatar: { type: "string", format: "uri" }, 67 - associated: { type: "ref", ref: "#profileAssociated" }, 68 - indexedAt: { type: "string", format: "datetime" }, 69 - createdAt: { type: "string", format: "datetime" }, 70 - viewer: { type: "ref", ref: "#viewerState" }, 71 - labels: { 72 - type: "array", 73 - items: { type: "ref", ref: "com.atproto.label.defs#label" }, 74 - }, 75 - verification: { type: "ref", ref: "#verificationState" }, 76 - status: { type: "ref", ref: "#statusView" }, 77 - }, 78 - required: ["did", "handle"], 79 - }); 80 - }); 81 - 82 - test("app.bsky.actor.defs - profileViewDetailed", () => { 83 - const profileViewDetailed = lx.object({ 84 - did: lx.string({ required: true, format: "did" }), 85 - handle: lx.string({ required: true, format: "handle" }), 86 - displayName: lx.string({ maxGraphemes: 64, maxLength: 640 }), 87 - description: lx.string({ maxGraphemes: 256, maxLength: 2560 }), 88 - pronouns: lx.string(), 89 - website: lx.string({ format: "uri" }), 90 - avatar: lx.string({ format: "uri" }), 91 - banner: lx.string({ format: "uri" }), 92 - followersCount: lx.integer(), 93 - followsCount: lx.integer(), 94 - postsCount: lx.integer(), 95 - associated: lx.ref("#profileAssociated"), 96 - joinedViaStarterPack: lx.ref("app.bsky.graph.defs#starterPackViewBasic"), 97 - indexedAt: lx.string({ format: "datetime" }), 98 - createdAt: lx.string({ format: "datetime" }), 99 - viewer: lx.ref("#viewerState"), 100 - labels: lx.array(lx.ref("com.atproto.label.defs#label")), 101 - pinnedPost: lx.ref("com.atproto.repo.strongRef"), 102 - verification: lx.ref("#verificationState"), 103 - status: lx.ref("#statusView"), 104 - }); 105 - 106 - expect(profileViewDetailed).toEqual({ 107 - type: "object", 108 - properties: { 109 - did: { type: "string", required: true, format: "did" }, 110 - handle: { type: "string", required: true, format: "handle" }, 111 - displayName: { type: "string", maxGraphemes: 64, maxLength: 640 }, 112 - description: { type: "string", maxGraphemes: 256, maxLength: 2560 }, 113 - pronouns: { type: "string" }, 114 - website: { type: "string", format: "uri" }, 115 - avatar: { type: "string", format: "uri" }, 116 - banner: { type: "string", format: "uri" }, 117 - followersCount: { type: "integer" }, 118 - followsCount: { type: "integer" }, 119 - postsCount: { type: "integer" }, 120 - associated: { type: "ref", ref: "#profileAssociated" }, 121 - joinedViaStarterPack: { 122 - type: "ref", 123 - ref: "app.bsky.graph.defs#starterPackViewBasic", 124 - }, 125 - indexedAt: { type: "string", format: "datetime" }, 126 - createdAt: { type: "string", format: "datetime" }, 127 - viewer: { type: "ref", ref: "#viewerState" }, 128 - labels: { 129 - type: "array", 130 - items: { type: "ref", ref: "com.atproto.label.defs#label" }, 131 - }, 132 - pinnedPost: { type: "ref", ref: "com.atproto.repo.strongRef" }, 133 - verification: { type: "ref", ref: "#verificationState" }, 134 - status: { type: "ref", ref: "#statusView" }, 135 - }, 136 - required: ["did", "handle"], 137 - }); 138 - }); 139 - 140 - test("app.bsky.actor.defs - profileAssociated", () => { 141 - const profileAssociated = lx.object({ 142 - lists: lx.integer(), 143 - feedgens: lx.integer(), 144 - starterPacks: lx.integer(), 145 - labeler: lx.boolean(), 146 - chat: lx.ref("#profileAssociatedChat"), 147 - activitySubscription: lx.ref("#profileAssociatedActivitySubscription"), 148 - }); 149 - 150 - expect(profileAssociated).toEqual({ 151 - type: "object", 152 - properties: { 153 - lists: { type: "integer" }, 154 - feedgens: { type: "integer" }, 155 - starterPacks: { type: "integer" }, 156 - labeler: { type: "boolean" }, 157 - chat: { type: "ref", ref: "#profileAssociatedChat" }, 158 - activitySubscription: { 159 - type: "ref", 160 - ref: "#profileAssociatedActivitySubscription", 161 - }, 162 - }, 163 - }); 164 - }); 165 - 166 - test("app.bsky.actor.defs - profileAssociatedChat", () => { 167 - const profileAssociatedChat = lx.object({ 168 - allowIncoming: lx.string({ 169 - required: true, 170 - knownValues: ["all", "none", "following"], 171 - }), 172 - }); 173 - 174 - expect(profileAssociatedChat).toEqual({ 175 - type: "object", 176 - properties: { 177 - allowIncoming: { 178 - type: "string", 179 - required: true, 180 - knownValues: ["all", "none", "following"], 181 - }, 182 - }, 183 - required: ["allowIncoming"], 184 - }); 185 - }); 186 - 187 - test("app.bsky.actor.defs - profileAssociatedActivitySubscription", () => { 188 - const profileAssociatedActivitySubscription = lx.object({ 189 - allowSubscriptions: lx.string({ 190 - required: true, 191 - knownValues: ["followers", "mutuals", "none"], 192 - }), 193 - }); 194 - 195 - expect(profileAssociatedActivitySubscription).toEqual({ 196 - type: "object", 197 - properties: { 198 - allowSubscriptions: { 199 - type: "string", 200 - required: true, 201 - knownValues: ["followers", "mutuals", "none"], 202 - }, 203 - }, 204 - required: ["allowSubscriptions"], 205 - }); 206 - }); 207 - 208 - test("app.bsky.actor.defs - viewerState", () => { 209 - const viewerState = lx.object({ 210 - muted: lx.boolean(), 211 - mutedByList: lx.ref("app.bsky.graph.defs#listViewBasic"), 212 - blockedBy: lx.boolean(), 213 - blocking: lx.string({ format: "at-uri" }), 214 - blockingByList: lx.ref("app.bsky.graph.defs#listViewBasic"), 215 - following: lx.string({ format: "at-uri" }), 216 - followedBy: lx.string({ format: "at-uri" }), 217 - knownFollowers: lx.ref("#knownFollowers"), 218 - activitySubscription: lx.ref( 219 - "app.bsky.notification.defs#activitySubscription", 220 - ), 221 - }); 222 - 223 - expect(viewerState).toEqual({ 224 - type: "object", 225 - properties: { 226 - muted: { type: "boolean" }, 227 - mutedByList: { type: "ref", ref: "app.bsky.graph.defs#listViewBasic" }, 228 - blockedBy: { type: "boolean" }, 229 - blocking: { type: "string", format: "at-uri" }, 230 - blockingByList: { type: "ref", ref: "app.bsky.graph.defs#listViewBasic" }, 231 - following: { type: "string", format: "at-uri" }, 232 - followedBy: { type: "string", format: "at-uri" }, 233 - knownFollowers: { type: "ref", ref: "#knownFollowers" }, 234 - activitySubscription: { 235 - type: "ref", 236 - ref: "app.bsky.notification.defs#activitySubscription", 237 - }, 238 - }, 239 - }); 240 - }); 241 - 242 - test("app.bsky.actor.defs - knownFollowers", () => { 243 - const knownFollowers = lx.object({ 244 - count: lx.integer({ required: true }), 245 - followers: lx.array(lx.ref("#profileViewBasic"), { 246 - required: true, 247 - minLength: 0, 248 - maxLength: 5, 249 - }), 250 - }); 251 - 252 - expect(knownFollowers).toEqual({ 253 - type: "object", 254 - properties: { 255 - count: { type: "integer", required: true }, 256 - followers: { 257 - type: "array", 258 - items: { type: "ref", ref: "#profileViewBasic" }, 259 - required: true, 260 - minLength: 0, 261 - maxLength: 5, 262 - }, 263 - }, 264 - required: ["count", "followers"], 265 - }); 266 - }); 267 - 268 - test("app.bsky.actor.defs - verificationState", () => { 269 - const verificationState = lx.object({ 270 - verifications: lx.array(lx.ref("#verificationView"), { required: true }), 271 - verifiedStatus: lx.string({ 272 - required: true, 273 - knownValues: ["valid", "invalid", "none"], 274 - }), 275 - trustedVerifierStatus: lx.string({ 276 - required: true, 277 - knownValues: ["valid", "invalid", "none"], 278 - }), 279 - }); 280 - 281 - expect(verificationState).toEqual({ 282 - type: "object", 283 - properties: { 284 - verifications: { 285 - type: "array", 286 - items: { type: "ref", ref: "#verificationView" }, 287 - required: true, 288 - }, 289 - verifiedStatus: { 290 - type: "string", 291 - required: true, 292 - knownValues: ["valid", "invalid", "none"], 293 - }, 294 - trustedVerifierStatus: { 295 - type: "string", 296 - required: true, 297 - knownValues: ["valid", "invalid", "none"], 298 - }, 299 - }, 300 - required: ["verifications", "verifiedStatus", "trustedVerifierStatus"], 301 - }); 302 - }); 303 - 304 - test("app.bsky.actor.defs - verificationView", () => { 305 - const verificationView = lx.object({ 306 - issuer: lx.string({ required: true, format: "did" }), 307 - uri: lx.string({ required: true, format: "at-uri" }), 308 - isValid: lx.boolean({ required: true }), 309 - createdAt: lx.string({ required: true, format: "datetime" }), 310 - }); 311 - 312 - expect(verificationView).toEqual({ 313 - type: "object", 314 - properties: { 315 - issuer: { type: "string", required: true, format: "did" }, 316 - uri: { type: "string", required: true, format: "at-uri" }, 317 - isValid: { type: "boolean", required: true }, 318 - createdAt: { type: "string", required: true, format: "datetime" }, 319 - }, 320 - required: ["issuer", "uri", "isValid", "createdAt"], 321 - }); 322 - }); 323 - 324 - test("app.bsky.actor.defs - preferences", () => { 325 - const preferences = lx.array( 326 - lx.union([ 327 - "#adultContentPref", 328 - "#contentLabelPref", 329 - "#savedFeedsPref", 330 - "#savedFeedsPrefV2", 331 - "#personalDetailsPref", 332 - "#feedViewPref", 333 - "#threadViewPref", 334 - "#interestsPref", 335 - "#mutedWordsPref", 336 - "#hiddenPostsPref", 337 - "#bskyAppStatePref", 338 - "#labelersPref", 339 - "#postInteractionSettingsPref", 340 - "#verificationPrefs", 341 - ]), 342 - ); 343 - 344 - expect(preferences).toEqual({ 345 - type: "array", 346 - items: { 347 - type: "union", 348 - refs: [ 349 - "#adultContentPref", 350 - "#contentLabelPref", 351 - "#savedFeedsPref", 352 - "#savedFeedsPrefV2", 353 - "#personalDetailsPref", 354 - "#feedViewPref", 355 - "#threadViewPref", 356 - "#interestsPref", 357 - "#mutedWordsPref", 358 - "#hiddenPostsPref", 359 - "#bskyAppStatePref", 360 - "#labelersPref", 361 - "#postInteractionSettingsPref", 362 - "#verificationPrefs", 363 - ], 364 - }, 365 - }); 366 - }); 367 - 368 - test("app.bsky.actor.defs - adultContentPref", () => { 369 - const adultContentPref = lx.object({ 370 - enabled: lx.boolean({ required: true, default: false }), 371 - }); 372 - 373 - expect(adultContentPref).toEqual({ 374 - type: "object", 375 - properties: { 376 - enabled: { type: "boolean", required: true, default: false }, 377 - }, 378 - required: ["enabled"], 379 - }); 380 - }); 381 - 382 - test("app.bsky.actor.defs - contentLabelPref", () => { 383 - const contentLabelPref = lx.object({ 384 - labelerDid: lx.string({ format: "did" }), 385 - label: lx.string({ required: true }), 386 - visibility: lx.string({ 387 - required: true, 388 - knownValues: ["ignore", "show", "warn", "hide"], 389 - }), 390 - }); 391 - 392 - expect(contentLabelPref).toEqual({ 393 - type: "object", 394 - properties: { 395 - labelerDid: { type: "string", format: "did" }, 396 - label: { type: "string", required: true }, 397 - visibility: { 398 - type: "string", 399 - required: true, 400 - knownValues: ["ignore", "show", "warn", "hide"], 401 - }, 402 - }, 403 - required: ["label", "visibility"], 404 - }); 405 - }); 406 - 407 - test("app.bsky.actor.defs - savedFeed", () => { 408 - const savedFeed = lx.object({ 409 - id: lx.string({ required: true }), 410 - type: lx.string({ 411 - required: true, 412 - knownValues: ["feed", "list", "timeline"], 413 - }), 414 - value: lx.string({ required: true }), 415 - pinned: lx.boolean({ required: true }), 416 - }); 417 - 418 - expect(savedFeed).toEqual({ 419 - type: "object", 420 - properties: { 421 - id: { type: "string", required: true }, 422 - type: { 423 - type: "string", 424 - required: true, 425 - knownValues: ["feed", "list", "timeline"], 426 - }, 427 - value: { type: "string", required: true }, 428 - pinned: { type: "boolean", required: true }, 429 - }, 430 - required: ["id", "type", "value", "pinned"], 431 - }); 432 - }); 433 - 434 - test("app.bsky.actor.defs - savedFeedsPrefV2", () => { 435 - const savedFeedsPrefV2 = lx.object({ 436 - items: lx.array(lx.ref("app.bsky.actor.defs#savedFeed"), { 437 - required: true, 438 - }), 439 - }); 440 - 441 - expect(savedFeedsPrefV2).toEqual({ 442 - type: "object", 443 - properties: { 444 - items: { 445 - type: "array", 446 - items: { type: "ref", ref: "app.bsky.actor.defs#savedFeed" }, 447 - required: true, 448 - }, 449 - }, 450 - required: ["items"], 451 - }); 452 - }); 453 - 454 - test("app.bsky.actor.defs - savedFeedsPref", () => { 455 - const savedFeedsPref = lx.object({ 456 - pinned: lx.array(lx.string({ format: "at-uri" }), { required: true }), 457 - saved: lx.array(lx.string({ format: "at-uri" }), { required: true }), 458 - timelineIndex: lx.integer(), 459 - }); 460 - 461 - expect(savedFeedsPref).toEqual({ 462 - type: "object", 463 - properties: { 464 - pinned: { 465 - type: "array", 466 - items: { type: "string", format: "at-uri" }, 467 - required: true, 468 - }, 469 - saved: { 470 - type: "array", 471 - items: { type: "string", format: "at-uri" }, 472 - required: true, 473 - }, 474 - timelineIndex: { type: "integer" }, 475 - }, 476 - required: ["pinned", "saved"], 477 - }); 478 - }); 479 - 480 - test("app.bsky.actor.defs - personalDetailsPref", () => { 481 - const personalDetailsPref = lx.object({ 482 - birthDate: lx.string({ format: "datetime" }), 483 - }); 484 - 485 - expect(personalDetailsPref).toEqual({ 486 - type: "object", 487 - properties: { 488 - birthDate: { type: "string", format: "datetime" }, 489 - }, 490 - }); 491 - }); 492 - 493 - test("app.bsky.actor.defs - feedViewPref", () => { 494 - const feedViewPref = lx.object({ 495 - feed: lx.string({ required: true }), 496 - hideReplies: lx.boolean(), 497 - hideRepliesByUnfollowed: lx.boolean({ default: true }), 498 - hideRepliesByLikeCount: lx.integer(), 499 - hideReposts: lx.boolean(), 500 - hideQuotePosts: lx.boolean(), 501 - }); 502 - 503 - expect(feedViewPref).toEqual({ 504 - type: "object", 505 - properties: { 506 - feed: { type: "string", required: true }, 507 - hideReplies: { type: "boolean" }, 508 - hideRepliesByUnfollowed: { type: "boolean", default: true }, 509 - hideRepliesByLikeCount: { type: "integer" }, 510 - hideReposts: { type: "boolean" }, 511 - hideQuotePosts: { type: "boolean" }, 512 - }, 513 - required: ["feed"], 514 - }); 515 - }); 516 - 517 - test("app.bsky.actor.defs - threadViewPref", () => { 518 - const threadViewPref = lx.object({ 519 - sort: lx.string({ 520 - knownValues: ["oldest", "newest", "most-likes", "random", "hotness"], 521 - }), 522 - prioritizeFollowedUsers: lx.boolean(), 523 - }); 524 - 525 - expect(threadViewPref).toEqual({ 526 - type: "object", 527 - properties: { 528 - sort: { 529 - type: "string", 530 - knownValues: ["oldest", "newest", "most-likes", "random", "hotness"], 531 - }, 532 - prioritizeFollowedUsers: { type: "boolean" }, 533 - }, 534 - }); 535 - }); 536 - 537 - test("app.bsky.actor.defs - interestsPref", () => { 538 - const interestsPref = lx.object({ 539 - tags: lx.array(lx.string({ maxLength: 640, maxGraphemes: 64 }), { 540 - required: true, 541 - maxLength: 100, 542 - }), 543 - }); 544 - 545 - expect(interestsPref).toEqual({ 546 - type: "object", 547 - properties: { 548 - tags: { 549 - type: "array", 550 - items: { type: "string", maxLength: 640, maxGraphemes: 64 }, 551 - required: true, 552 - maxLength: 100, 553 - }, 554 - }, 555 - required: ["tags"], 556 - }); 557 - }); 558 - 559 - test("app.bsky.actor.defs - mutedWordTarget", () => { 560 - const mutedWordTarget = lx.string({ 561 - knownValues: ["content", "tag"], 562 - maxLength: 640, 563 - maxGraphemes: 64, 564 - }); 565 - 566 - expect(mutedWordTarget).toEqual({ 567 - type: "string", 568 - knownValues: ["content", "tag"], 569 - maxLength: 640, 570 - maxGraphemes: 64, 571 - }); 572 - }); 573 - 574 - test("app.bsky.actor.defs - mutedWord", () => { 575 - const mutedWord = lx.object({ 576 - id: lx.string(), 577 - value: lx.string({ required: true, maxLength: 10000, maxGraphemes: 1000 }), 578 - targets: lx.array(lx.ref("app.bsky.actor.defs#mutedWordTarget"), { 579 - required: true, 580 - }), 581 - actorTarget: lx.string({ 582 - knownValues: ["all", "exclude-following"], 583 - default: "all", 584 - }), 585 - expiresAt: lx.string({ format: "datetime" }), 586 - }); 587 - 588 - expect(mutedWord).toEqual({ 589 - type: "object", 590 - properties: { 591 - id: { type: "string" }, 592 - value: { 593 - type: "string", 594 - required: true, 595 - maxLength: 10000, 596 - maxGraphemes: 1000, 597 - }, 598 - targets: { 599 - type: "array", 600 - items: { type: "ref", ref: "app.bsky.actor.defs#mutedWordTarget" }, 601 - required: true, 602 - }, 603 - actorTarget: { 604 - type: "string", 605 - knownValues: ["all", "exclude-following"], 606 - default: "all", 607 - }, 608 - expiresAt: { type: "string", format: "datetime" }, 609 - }, 610 - required: ["value", "targets"], 611 - }); 612 - }); 613 - 614 - test("app.bsky.actor.defs - mutedWordsPref", () => { 615 - const mutedWordsPref = lx.object({ 616 - items: lx.array(lx.ref("app.bsky.actor.defs#mutedWord"), { 617 - required: true, 618 - }), 619 - }); 620 - 621 - expect(mutedWordsPref).toEqual({ 622 - type: "object", 623 - properties: { 624 - items: { 625 - type: "array", 626 - items: { type: "ref", ref: "app.bsky.actor.defs#mutedWord" }, 627 - required: true, 628 - }, 629 - }, 630 - required: ["items"], 631 - }); 632 - }); 633 - 634 - test("app.bsky.actor.defs - hiddenPostsPref", () => { 635 - const hiddenPostsPref = lx.object({ 636 - items: lx.array(lx.string({ format: "at-uri" }), { required: true }), 637 - }); 638 - 639 - expect(hiddenPostsPref).toEqual({ 640 - type: "object", 641 - properties: { 642 - items: { 643 - type: "array", 644 - items: { type: "string", format: "at-uri" }, 645 - required: true, 646 - }, 647 - }, 648 - required: ["items"], 649 - }); 650 - }); 651 - 652 - test("app.bsky.actor.defs - labelersPref", () => { 653 - const labelersPref = lx.object({ 654 - labelers: lx.array(lx.ref("#labelerPrefItem"), { required: true }), 655 - }); 656 - 657 - expect(labelersPref).toEqual({ 658 - type: "object", 659 - properties: { 660 - labelers: { 661 - type: "array", 662 - items: { type: "ref", ref: "#labelerPrefItem" }, 663 - required: true, 664 - }, 665 - }, 666 - required: ["labelers"], 667 - }); 668 - }); 669 - 670 - test("app.bsky.actor.defs - labelerPrefItem", () => { 671 - const labelerPrefItem = lx.object({ 672 - did: lx.string({ required: true, format: "did" }), 673 - }); 674 - 675 - expect(labelerPrefItem).toEqual({ 676 - type: "object", 677 - properties: { 678 - did: { type: "string", required: true, format: "did" }, 679 - }, 680 - required: ["did"], 681 - }); 682 - }); 683 - 684 - test("app.bsky.actor.defs - bskyAppStatePref", () => { 685 - const bskyAppStatePref = lx.object({ 686 - activeProgressGuide: lx.ref("#bskyAppProgressGuide"), 687 - queuedNudges: lx.array(lx.string({ maxLength: 100 }), { maxLength: 1000 }), 688 - nuxs: lx.array(lx.ref("app.bsky.actor.defs#nux"), { maxLength: 100 }), 689 - }); 690 - 691 - expect(bskyAppStatePref).toEqual({ 692 - type: "object", 693 - properties: { 694 - activeProgressGuide: { type: "ref", ref: "#bskyAppProgressGuide" }, 695 - queuedNudges: { 696 - type: "array", 697 - items: { type: "string", maxLength: 100 }, 698 - maxLength: 1000, 699 - }, 700 - nuxs: { 701 - type: "array", 702 - items: { type: "ref", ref: "app.bsky.actor.defs#nux" }, 703 - maxLength: 100, 704 - }, 705 - }, 706 - }); 707 - }); 708 - 709 - test("app.bsky.actor.defs - bskyAppProgressGuide", () => { 710 - const bskyAppProgressGuide = lx.object({ 711 - guide: lx.string({ required: true, maxLength: 100 }), 712 - }); 713 - 714 - expect(bskyAppProgressGuide).toEqual({ 715 - type: "object", 716 - properties: { 717 - guide: { type: "string", required: true, maxLength: 100 }, 718 - }, 719 - required: ["guide"], 720 - }); 721 - }); 722 - 723 - test("app.bsky.actor.defs - nux", () => { 724 - const nux = lx.object({ 725 - id: lx.string({ required: true, maxLength: 100 }), 726 - completed: lx.boolean({ required: true, default: false }), 727 - data: lx.string({ maxLength: 3000, maxGraphemes: 300 }), 728 - expiresAt: lx.string({ format: "datetime" }), 729 - }); 730 - 731 - expect(nux).toEqual({ 732 - type: "object", 733 - properties: { 734 - id: { type: "string", required: true, maxLength: 100 }, 735 - completed: { type: "boolean", required: true, default: false }, 736 - data: { type: "string", maxLength: 3000, maxGraphemes: 300 }, 737 - expiresAt: { type: "string", format: "datetime" }, 738 - }, 739 - required: ["id", "completed"], 740 - }); 741 - }); 742 - 743 - test("app.bsky.actor.defs - verificationPrefs", () => { 744 - const verificationPrefs = lx.object({ 745 - hideBadges: lx.boolean({ default: false }), 746 - }); 747 - 748 - expect(verificationPrefs).toEqual({ 749 - type: "object", 750 - properties: { 751 - hideBadges: { type: "boolean", default: false }, 752 - }, 753 - }); 754 - }); 755 - 756 - test("app.bsky.actor.defs - postInteractionSettingsPref", () => { 757 - const postInteractionSettingsPref = lx.object({ 758 - threadgateAllowRules: lx.array( 759 - lx.union([ 760 - "app.bsky.feed.threadgate#mentionRule", 761 - "app.bsky.feed.threadgate#followerRule", 762 - "app.bsky.feed.threadgate#followingRule", 763 - "app.bsky.feed.threadgate#listRule", 764 - ]), 765 - { maxLength: 5 }, 766 - ), 767 - postgateEmbeddingRules: lx.array( 768 - lx.union(["app.bsky.feed.postgate#disableRule"]), 769 - { maxLength: 5 }, 770 - ), 771 - }); 772 - 773 - expect(postInteractionSettingsPref).toEqual({ 774 - type: "object", 775 - properties: { 776 - threadgateAllowRules: { 777 - type: "array", 778 - items: { 779 - type: "union", 780 - refs: [ 781 - "app.bsky.feed.threadgate#mentionRule", 782 - "app.bsky.feed.threadgate#followerRule", 783 - "app.bsky.feed.threadgate#followingRule", 784 - "app.bsky.feed.threadgate#listRule", 785 - ], 786 - }, 787 - maxLength: 5, 788 - }, 789 - postgateEmbeddingRules: { 790 - type: "array", 791 - items: { 792 - type: "union", 793 - refs: ["app.bsky.feed.postgate#disableRule"], 794 - }, 795 - maxLength: 5, 796 - }, 797 - }, 798 - }); 799 - }); 800 - 801 - test("app.bsky.actor.defs - statusView", () => { 802 - const statusView = lx.object({ 803 - status: lx.string({ 804 - required: true, 805 - knownValues: ["app.bsky.actor.status#live"], 806 - }), 807 - record: lx.unknown({ required: true }), 808 - embed: lx.union(["app.bsky.embed.external#view"]), 809 - expiresAt: lx.string({ format: "datetime" }), 810 - isActive: lx.boolean(), 811 - }); 812 - 813 - expect(statusView).toEqual({ 814 - type: "object", 815 - properties: { 816 - status: { 817 - type: "string", 818 - required: true, 819 - knownValues: ["app.bsky.actor.status#live"], 820 - }, 821 - record: { type: "unknown", required: true }, 822 - embed: { 823 - type: "union", 824 - refs: ["app.bsky.embed.external#view"], 825 - }, 826 - expiresAt: { type: "string", format: "datetime" }, 827 - isActive: { type: "boolean" }, 828 - }, 829 - required: ["status", "record"], 830 - }); 831 - }); 832 - 833 - test("app.bsky.actor.defs - full lexicon", () => { 834 - const actorDefs = lx.lexicon("app.bsky.actor.defs", { 835 - profileViewBasic: lx.object({ 836 - did: lx.string({ required: true, format: "did" }), 837 - handle: lx.string({ required: true, format: "handle" }), 838 - displayName: lx.string({ maxGraphemes: 64, maxLength: 640 }), 839 - pronouns: lx.string(), 840 - avatar: lx.string({ format: "uri" }), 841 - associated: lx.ref("#profileAssociated"), 842 - viewer: lx.ref("#viewerState"), 843 - labels: lx.array(lx.ref("com.atproto.label.defs#label")), 844 - createdAt: lx.string({ format: "datetime" }), 845 - verification: lx.ref("#verificationState"), 846 - status: lx.ref("#statusView"), 847 - }), 848 - viewerState: lx.object({ 849 - muted: lx.boolean(), 850 - mutedByList: lx.ref("app.bsky.graph.defs#listViewBasic"), 851 - blockedBy: lx.boolean(), 852 - blocking: lx.string({ format: "at-uri" }), 853 - blockingByList: lx.ref("app.bsky.graph.defs#listViewBasic"), 854 - following: lx.string({ format: "at-uri" }), 855 - followedBy: lx.string({ format: "at-uri" }), 856 - knownFollowers: lx.ref("#knownFollowers"), 857 - activitySubscription: lx.ref( 858 - "app.bsky.notification.defs#activitySubscription", 859 - ), 860 - }), 861 - }); 862 - 863 - expect(actorDefs.json.lexicon).toEqual(1); 864 - expect(actorDefs.json.id).toEqual("app.bsky.actor.defs"); 865 - expect(actorDefs.json.defs.profileViewBasic.type).toEqual("object"); 866 - expect(actorDefs.json.defs.viewerState.type).toEqual("object"); 867 - });
-681
packages/prototypey/tests/bsky-feed.test.ts
··· 1 - import { expect, test } from "vitest"; 2 - import { lx } from "../src/lib.ts"; 3 - 4 - test("app.bsky.feed.defs - postView", () => { 5 - const postView = lx.object({ 6 - uri: lx.string({ required: true, format: "at-uri" }), 7 - cid: lx.string({ required: true, format: "cid" }), 8 - author: lx.ref("app.bsky.actor.defs#profileViewBasic", { required: true }), 9 - record: lx.unknown({ required: true }), 10 - embed: lx.union([ 11 - "app.bsky.embed.images#view", 12 - "app.bsky.embed.video#view", 13 - "app.bsky.embed.external#view", 14 - "app.bsky.embed.record#view", 15 - "app.bsky.embed.recordWithMedia#view", 16 - ]), 17 - bookmarkCount: lx.integer(), 18 - replyCount: lx.integer(), 19 - repostCount: lx.integer(), 20 - likeCount: lx.integer(), 21 - quoteCount: lx.integer(), 22 - indexedAt: lx.string({ required: true, format: "datetime" }), 23 - viewer: lx.ref("#viewerState"), 24 - labels: lx.array(lx.ref("com.atproto.label.defs#label")), 25 - threadgate: lx.ref("#threadgateView"), 26 - }); 27 - 28 - expect(postView).toEqual({ 29 - type: "object", 30 - properties: { 31 - uri: { type: "string", required: true, format: "at-uri" }, 32 - cid: { type: "string", required: true, format: "cid" }, 33 - author: { 34 - type: "ref", 35 - ref: "app.bsky.actor.defs#profileViewBasic", 36 - required: true, 37 - }, 38 - record: { type: "unknown", required: true }, 39 - embed: { 40 - type: "union", 41 - refs: [ 42 - "app.bsky.embed.images#view", 43 - "app.bsky.embed.video#view", 44 - "app.bsky.embed.external#view", 45 - "app.bsky.embed.record#view", 46 - "app.bsky.embed.recordWithMedia#view", 47 - ], 48 - }, 49 - bookmarkCount: { type: "integer" }, 50 - replyCount: { type: "integer" }, 51 - repostCount: { type: "integer" }, 52 - likeCount: { type: "integer" }, 53 - quoteCount: { type: "integer" }, 54 - indexedAt: { type: "string", required: true, format: "datetime" }, 55 - viewer: { type: "ref", ref: "#viewerState" }, 56 - labels: { 57 - type: "array", 58 - items: { type: "ref", ref: "com.atproto.label.defs#label" }, 59 - }, 60 - threadgate: { type: "ref", ref: "#threadgateView" }, 61 - }, 62 - required: ["uri", "cid", "author", "record", "indexedAt"], 63 - }); 64 - }); 65 - 66 - test("app.bsky.feed.defs - viewerState", () => { 67 - const viewerState = lx.object({ 68 - repost: lx.string({ format: "at-uri" }), 69 - like: lx.string({ format: "at-uri" }), 70 - bookmarked: lx.boolean(), 71 - threadMuted: lx.boolean(), 72 - replyDisabled: lx.boolean(), 73 - embeddingDisabled: lx.boolean(), 74 - pinned: lx.boolean(), 75 - }); 76 - 77 - expect(viewerState).toEqual({ 78 - type: "object", 79 - properties: { 80 - repost: { type: "string", format: "at-uri" }, 81 - like: { type: "string", format: "at-uri" }, 82 - bookmarked: { type: "boolean" }, 83 - threadMuted: { type: "boolean" }, 84 - replyDisabled: { type: "boolean" }, 85 - embeddingDisabled: { type: "boolean" }, 86 - pinned: { type: "boolean" }, 87 - }, 88 - }); 89 - }); 90 - 91 - test("app.bsky.feed.defs - threadContext", () => { 92 - const threadContext = lx.object({ 93 - rootAuthorLike: lx.string({ format: "at-uri" }), 94 - }); 95 - 96 - expect(threadContext).toEqual({ 97 - type: "object", 98 - properties: { 99 - rootAuthorLike: { type: "string", format: "at-uri" }, 100 - }, 101 - }); 102 - }); 103 - 104 - test("app.bsky.feed.defs - feedViewPost", () => { 105 - const feedViewPost = lx.object({ 106 - post: lx.ref("#postView", { required: true }), 107 - reply: lx.ref("#replyRef"), 108 - reason: lx.union(["#reasonRepost", "#reasonPin"]), 109 - feedContext: lx.string({ maxLength: 2000 }), 110 - reqId: lx.string({ maxLength: 100 }), 111 - }); 112 - 113 - expect(feedViewPost).toEqual({ 114 - type: "object", 115 - properties: { 116 - post: { type: "ref", ref: "#postView", required: true }, 117 - reply: { type: "ref", ref: "#replyRef" }, 118 - reason: { 119 - type: "union", 120 - refs: ["#reasonRepost", "#reasonPin"], 121 - }, 122 - feedContext: { type: "string", maxLength: 2000 }, 123 - reqId: { type: "string", maxLength: 100 }, 124 - }, 125 - required: ["post"], 126 - }); 127 - }); 128 - 129 - test("app.bsky.feed.defs - replyRef", () => { 130 - const replyRef = lx.object({ 131 - root: lx.union(["#postView", "#notFoundPost", "#blockedPost"], { 132 - required: true, 133 - }), 134 - parent: lx.union(["#postView", "#notFoundPost", "#blockedPost"], { 135 - required: true, 136 - }), 137 - grandparentAuthor: lx.ref("app.bsky.actor.defs#profileViewBasic"), 138 - }); 139 - 140 - expect(replyRef).toEqual({ 141 - type: "object", 142 - properties: { 143 - root: { 144 - type: "union", 145 - refs: ["#postView", "#notFoundPost", "#blockedPost"], 146 - required: true, 147 - }, 148 - parent: { 149 - type: "union", 150 - refs: ["#postView", "#notFoundPost", "#blockedPost"], 151 - required: true, 152 - }, 153 - grandparentAuthor: { 154 - type: "ref", 155 - ref: "app.bsky.actor.defs#profileViewBasic", 156 - }, 157 - }, 158 - required: ["root", "parent"], 159 - }); 160 - }); 161 - 162 - test("app.bsky.feed.defs - reasonRepost", () => { 163 - const reasonRepost = lx.object({ 164 - by: lx.ref("app.bsky.actor.defs#profileViewBasic", { required: true }), 165 - uri: lx.string({ format: "at-uri" }), 166 - cid: lx.string({ format: "cid" }), 167 - indexedAt: lx.string({ required: true, format: "datetime" }), 168 - }); 169 - 170 - expect(reasonRepost).toEqual({ 171 - type: "object", 172 - properties: { 173 - by: { 174 - type: "ref", 175 - ref: "app.bsky.actor.defs#profileViewBasic", 176 - required: true, 177 - }, 178 - uri: { type: "string", format: "at-uri" }, 179 - cid: { type: "string", format: "cid" }, 180 - indexedAt: { type: "string", required: true, format: "datetime" }, 181 - }, 182 - required: ["by", "indexedAt"], 183 - }); 184 - }); 185 - 186 - test("app.bsky.feed.defs - reasonPin", () => { 187 - const reasonPin = lx.object({}); 188 - 189 - expect(reasonPin).toEqual({ 190 - type: "object", 191 - properties: {}, 192 - }); 193 - }); 194 - 195 - test("app.bsky.feed.defs - threadViewPost", () => { 196 - const threadViewPost = lx.object({ 197 - post: lx.ref("#postView", { required: true }), 198 - parent: lx.union(["#threadViewPost", "#notFoundPost", "#blockedPost"]), 199 - replies: lx.array( 200 - lx.union(["#threadViewPost", "#notFoundPost", "#blockedPost"]), 201 - ), 202 - threadContext: lx.ref("#threadContext"), 203 - }); 204 - 205 - expect(threadViewPost).toEqual({ 206 - type: "object", 207 - properties: { 208 - post: { type: "ref", ref: "#postView", required: true }, 209 - parent: { 210 - type: "union", 211 - refs: ["#threadViewPost", "#notFoundPost", "#blockedPost"], 212 - }, 213 - replies: { 214 - type: "array", 215 - items: { 216 - type: "union", 217 - refs: ["#threadViewPost", "#notFoundPost", "#blockedPost"], 218 - }, 219 - }, 220 - threadContext: { type: "ref", ref: "#threadContext" }, 221 - }, 222 - required: ["post"], 223 - }); 224 - }); 225 - 226 - test("app.bsky.feed.defs - notFoundPost", () => { 227 - const notFoundPost = lx.object({ 228 - uri: lx.string({ required: true, format: "at-uri" }), 229 - notFound: lx.boolean({ required: true, const: true }), 230 - }); 231 - 232 - expect(notFoundPost).toEqual({ 233 - type: "object", 234 - properties: { 235 - uri: { type: "string", required: true, format: "at-uri" }, 236 - notFound: { type: "boolean", required: true, const: true }, 237 - }, 238 - required: ["uri", "notFound"], 239 - }); 240 - }); 241 - 242 - test("app.bsky.feed.defs - blockedPost", () => { 243 - const blockedPost = lx.object({ 244 - uri: lx.string({ required: true, format: "at-uri" }), 245 - blocked: lx.boolean({ required: true, const: true }), 246 - author: lx.ref("#blockedAuthor", { required: true }), 247 - }); 248 - 249 - expect(blockedPost).toEqual({ 250 - type: "object", 251 - properties: { 252 - uri: { type: "string", required: true, format: "at-uri" }, 253 - blocked: { type: "boolean", required: true, const: true }, 254 - author: { type: "ref", ref: "#blockedAuthor", required: true }, 255 - }, 256 - required: ["uri", "blocked", "author"], 257 - }); 258 - }); 259 - 260 - test("app.bsky.feed.defs - blockedAuthor", () => { 261 - const blockedAuthor = lx.object({ 262 - did: lx.string({ required: true, format: "did" }), 263 - viewer: lx.ref("app.bsky.actor.defs#viewerState"), 264 - }); 265 - 266 - expect(blockedAuthor).toEqual({ 267 - type: "object", 268 - properties: { 269 - did: { type: "string", required: true, format: "did" }, 270 - viewer: { type: "ref", ref: "app.bsky.actor.defs#viewerState" }, 271 - }, 272 - required: ["did"], 273 - }); 274 - }); 275 - 276 - test("app.bsky.feed.defs - generatorView", () => { 277 - const generatorView = lx.object({ 278 - uri: lx.string({ required: true, format: "at-uri" }), 279 - cid: lx.string({ required: true, format: "cid" }), 280 - did: lx.string({ required: true, format: "did" }), 281 - creator: lx.ref("app.bsky.actor.defs#profileView", { required: true }), 282 - displayName: lx.string({ required: true }), 283 - description: lx.string({ maxGraphemes: 300, maxLength: 3000 }), 284 - descriptionFacets: lx.array(lx.ref("app.bsky.richtext.facet")), 285 - avatar: lx.string({ format: "uri" }), 286 - likeCount: lx.integer({ minimum: 0 }), 287 - acceptsInteractions: lx.boolean(), 288 - labels: lx.array(lx.ref("com.atproto.label.defs#label")), 289 - viewer: lx.ref("#generatorViewerState"), 290 - contentMode: lx.string({ 291 - knownValues: [ 292 - "app.bsky.feed.defs#contentModeUnspecified", 293 - "app.bsky.feed.defs#contentModeVideo", 294 - ], 295 - }), 296 - indexedAt: lx.string({ required: true, format: "datetime" }), 297 - }); 298 - 299 - expect(generatorView).toEqual({ 300 - type: "object", 301 - properties: { 302 - uri: { type: "string", required: true, format: "at-uri" }, 303 - cid: { type: "string", required: true, format: "cid" }, 304 - did: { type: "string", required: true, format: "did" }, 305 - creator: { 306 - type: "ref", 307 - ref: "app.bsky.actor.defs#profileView", 308 - required: true, 309 - }, 310 - displayName: { type: "string", required: true }, 311 - description: { type: "string", maxGraphemes: 300, maxLength: 3000 }, 312 - descriptionFacets: { 313 - type: "array", 314 - items: { type: "ref", ref: "app.bsky.richtext.facet" }, 315 - }, 316 - avatar: { type: "string", format: "uri" }, 317 - likeCount: { type: "integer", minimum: 0 }, 318 - acceptsInteractions: { type: "boolean" }, 319 - labels: { 320 - type: "array", 321 - items: { type: "ref", ref: "com.atproto.label.defs#label" }, 322 - }, 323 - viewer: { type: "ref", ref: "#generatorViewerState" }, 324 - contentMode: { 325 - type: "string", 326 - knownValues: [ 327 - "app.bsky.feed.defs#contentModeUnspecified", 328 - "app.bsky.feed.defs#contentModeVideo", 329 - ], 330 - }, 331 - indexedAt: { type: "string", required: true, format: "datetime" }, 332 - }, 333 - required: ["uri", "cid", "did", "creator", "displayName", "indexedAt"], 334 - }); 335 - }); 336 - 337 - test("app.bsky.feed.defs - generatorViewerState", () => { 338 - const generatorViewerState = lx.object({ 339 - like: lx.string({ format: "at-uri" }), 340 - }); 341 - 342 - expect(generatorViewerState).toEqual({ 343 - type: "object", 344 - properties: { 345 - like: { type: "string", format: "at-uri" }, 346 - }, 347 - }); 348 - }); 349 - 350 - test("app.bsky.feed.defs - skeletonFeedPost", () => { 351 - const skeletonFeedPost = lx.object({ 352 - post: lx.string({ required: true, format: "at-uri" }), 353 - reason: lx.union(["#skeletonReasonRepost", "#skeletonReasonPin"]), 354 - feedContext: lx.string({ maxLength: 2000 }), 355 - }); 356 - 357 - expect(skeletonFeedPost).toEqual({ 358 - type: "object", 359 - properties: { 360 - post: { type: "string", required: true, format: "at-uri" }, 361 - reason: { 362 - type: "union", 363 - refs: ["#skeletonReasonRepost", "#skeletonReasonPin"], 364 - }, 365 - feedContext: { type: "string", maxLength: 2000 }, 366 - }, 367 - required: ["post"], 368 - }); 369 - }); 370 - 371 - test("app.bsky.feed.defs - skeletonReasonRepost", () => { 372 - const skeletonReasonRepost = lx.object({ 373 - repost: lx.string({ required: true, format: "at-uri" }), 374 - }); 375 - 376 - expect(skeletonReasonRepost).toEqual({ 377 - type: "object", 378 - properties: { 379 - repost: { type: "string", required: true, format: "at-uri" }, 380 - }, 381 - required: ["repost"], 382 - }); 383 - }); 384 - 385 - test("app.bsky.feed.defs - skeletonReasonPin", () => { 386 - const skeletonReasonPin = lx.object({}); 387 - 388 - expect(skeletonReasonPin).toEqual({ 389 - type: "object", 390 - properties: {}, 391 - }); 392 - }); 393 - 394 - test("app.bsky.feed.defs - threadgateView", () => { 395 - const threadgateView = lx.object({ 396 - uri: lx.string({ format: "at-uri" }), 397 - cid: lx.string({ format: "cid" }), 398 - record: lx.unknown(), 399 - lists: lx.array(lx.ref("app.bsky.graph.defs#listViewBasic")), 400 - }); 401 - 402 - expect(threadgateView).toEqual({ 403 - type: "object", 404 - properties: { 405 - uri: { type: "string", format: "at-uri" }, 406 - cid: { type: "string", format: "cid" }, 407 - record: { type: "unknown" }, 408 - lists: { 409 - type: "array", 410 - items: { type: "ref", ref: "app.bsky.graph.defs#listViewBasic" }, 411 - }, 412 - }, 413 - }); 414 - }); 415 - 416 - test("app.bsky.feed.defs - interaction", () => { 417 - const interaction = lx.object({ 418 - item: lx.string({ format: "at-uri" }), 419 - event: lx.string({ 420 - knownValues: [ 421 - "app.bsky.feed.defs#requestLess", 422 - "app.bsky.feed.defs#requestMore", 423 - "app.bsky.feed.defs#clickthroughItem", 424 - "app.bsky.feed.defs#clickthroughAuthor", 425 - "app.bsky.feed.defs#clickthroughReposter", 426 - "app.bsky.feed.defs#clickthroughEmbed", 427 - "app.bsky.feed.defs#interactionSeen", 428 - "app.bsky.feed.defs#interactionLike", 429 - "app.bsky.feed.defs#interactionRepost", 430 - "app.bsky.feed.defs#interactionReply", 431 - "app.bsky.feed.defs#interactionQuote", 432 - "app.bsky.feed.defs#interactionShare", 433 - ], 434 - }), 435 - feedContext: lx.string({ maxLength: 2000 }), 436 - reqId: lx.string({ maxLength: 100 }), 437 - }); 438 - 439 - expect(interaction).toEqual({ 440 - type: "object", 441 - properties: { 442 - item: { type: "string", format: "at-uri" }, 443 - event: { 444 - type: "string", 445 - knownValues: [ 446 - "app.bsky.feed.defs#requestLess", 447 - "app.bsky.feed.defs#requestMore", 448 - "app.bsky.feed.defs#clickthroughItem", 449 - "app.bsky.feed.defs#clickthroughAuthor", 450 - "app.bsky.feed.defs#clickthroughReposter", 451 - "app.bsky.feed.defs#clickthroughEmbed", 452 - "app.bsky.feed.defs#interactionSeen", 453 - "app.bsky.feed.defs#interactionLike", 454 - "app.bsky.feed.defs#interactionRepost", 455 - "app.bsky.feed.defs#interactionReply", 456 - "app.bsky.feed.defs#interactionQuote", 457 - "app.bsky.feed.defs#interactionShare", 458 - ], 459 - }, 460 - feedContext: { type: "string", maxLength: 2000 }, 461 - reqId: { type: "string", maxLength: 100 }, 462 - }, 463 - }); 464 - }); 465 - 466 - test("app.bsky.feed.defs - requestLess token", () => { 467 - const requestLess = lx.token( 468 - "Request that less content like the given feed item be shown in the feed", 469 - ); 470 - 471 - expect(requestLess).toEqual({ 472 - type: "token", 473 - description: 474 - "Request that less content like the given feed item be shown in the feed", 475 - }); 476 - }); 477 - 478 - test("app.bsky.feed.defs - requestMore token", () => { 479 - const requestMore = lx.token( 480 - "Request that more content like the given feed item be shown in the feed", 481 - ); 482 - 483 - expect(requestMore).toEqual({ 484 - type: "token", 485 - description: 486 - "Request that more content like the given feed item be shown in the feed", 487 - }); 488 - }); 489 - 490 - test("app.bsky.feed.defs - clickthroughItem token", () => { 491 - const clickthroughItem = lx.token("User clicked through to the feed item"); 492 - 493 - expect(clickthroughItem).toEqual({ 494 - type: "token", 495 - description: "User clicked through to the feed item", 496 - }); 497 - }); 498 - 499 - test("app.bsky.feed.defs - clickthroughAuthor token", () => { 500 - const clickthroughAuthor = lx.token( 501 - "User clicked through to the author of the feed item", 502 - ); 503 - 504 - expect(clickthroughAuthor).toEqual({ 505 - type: "token", 506 - description: "User clicked through to the author of the feed item", 507 - }); 508 - }); 509 - 510 - test("app.bsky.feed.defs - clickthroughReposter token", () => { 511 - const clickthroughReposter = lx.token( 512 - "User clicked through to the reposter of the feed item", 513 - ); 514 - 515 - expect(clickthroughReposter).toEqual({ 516 - type: "token", 517 - description: "User clicked through to the reposter of the feed item", 518 - }); 519 - }); 520 - 521 - test("app.bsky.feed.defs - clickthroughEmbed token", () => { 522 - const clickthroughEmbed = lx.token( 523 - "User clicked through to the embedded content of the feed item", 524 - ); 525 - 526 - expect(clickthroughEmbed).toEqual({ 527 - type: "token", 528 - description: 529 - "User clicked through to the embedded content of the feed item", 530 - }); 531 - }); 532 - 533 - test("app.bsky.feed.defs - contentModeUnspecified token", () => { 534 - const contentModeUnspecified = lx.token( 535 - "Declares the feed generator returns any types of posts.", 536 - ); 537 - 538 - expect(contentModeUnspecified).toEqual({ 539 - type: "token", 540 - description: "Declares the feed generator returns any types of posts.", 541 - }); 542 - }); 543 - 544 - test("app.bsky.feed.defs - contentModeVideo token", () => { 545 - const contentModeVideo = lx.token( 546 - "Declares the feed generator returns posts containing app.bsky.embed.video embeds.", 547 - ); 548 - 549 - expect(contentModeVideo).toEqual({ 550 - type: "token", 551 - description: 552 - "Declares the feed generator returns posts containing app.bsky.embed.video embeds.", 553 - }); 554 - }); 555 - 556 - test("app.bsky.feed.defs - interactionSeen token", () => { 557 - const interactionSeen = lx.token("Feed item was seen by user"); 558 - 559 - expect(interactionSeen).toEqual({ 560 - type: "token", 561 - description: "Feed item was seen by user", 562 - }); 563 - }); 564 - 565 - test("app.bsky.feed.defs - interactionLike token", () => { 566 - const interactionLike = lx.token("User liked the feed item"); 567 - 568 - expect(interactionLike).toEqual({ 569 - type: "token", 570 - description: "User liked the feed item", 571 - }); 572 - }); 573 - 574 - test("app.bsky.feed.defs - interactionRepost token", () => { 575 - const interactionRepost = lx.token("User reposted the feed item"); 576 - 577 - expect(interactionRepost).toEqual({ 578 - type: "token", 579 - description: "User reposted the feed item", 580 - }); 581 - }); 582 - 583 - test("app.bsky.feed.defs - interactionReply token", () => { 584 - const interactionReply = lx.token("User replied to the feed item"); 585 - 586 - expect(interactionReply).toEqual({ 587 - type: "token", 588 - description: "User replied to the feed item", 589 - }); 590 - }); 591 - 592 - test("app.bsky.feed.defs - interactionQuote token", () => { 593 - const interactionQuote = lx.token("User quoted the feed item"); 594 - 595 - expect(interactionQuote).toEqual({ 596 - type: "token", 597 - description: "User quoted the feed item", 598 - }); 599 - }); 600 - 601 - test("app.bsky.feed.defs - interactionShare token", () => { 602 - const interactionShare = lx.token("User shared the feed item"); 603 - 604 - expect(interactionShare).toEqual({ 605 - type: "token", 606 - description: "User shared the feed item", 607 - }); 608 - }); 609 - 610 - test("app.bsky.feed.defs - full lexicon", () => { 611 - const feedDefs = lx.lexicon("app.bsky.feed.defs", { 612 - postView: lx.object({ 613 - uri: lx.string({ required: true, format: "at-uri" }), 614 - cid: lx.string({ required: true, format: "cid" }), 615 - author: lx.ref("app.bsky.actor.defs#profileViewBasic", { 616 - required: true, 617 - }), 618 - record: lx.unknown({ required: true }), 619 - embed: lx.union([ 620 - "app.bsky.embed.images#view", 621 - "app.bsky.embed.video#view", 622 - "app.bsky.embed.external#view", 623 - "app.bsky.embed.record#view", 624 - "app.bsky.embed.recordWithMedia#view", 625 - ]), 626 - bookmarkCount: lx.integer(), 627 - replyCount: lx.integer(), 628 - repostCount: lx.integer(), 629 - likeCount: lx.integer(), 630 - quoteCount: lx.integer(), 631 - indexedAt: lx.string({ required: true, format: "datetime" }), 632 - viewer: lx.ref("#viewerState"), 633 - labels: lx.array(lx.ref("com.atproto.label.defs#label")), 634 - threadgate: lx.ref("#threadgateView"), 635 - }), 636 - viewerState: lx.object({ 637 - repost: lx.string({ format: "at-uri" }), 638 - like: lx.string({ format: "at-uri" }), 639 - bookmarked: lx.boolean(), 640 - threadMuted: lx.boolean(), 641 - replyDisabled: lx.boolean(), 642 - embeddingDisabled: lx.boolean(), 643 - pinned: lx.boolean(), 644 - }), 645 - requestLess: lx.token( 646 - "Request that less content like the given feed item be shown in the feed", 647 - ), 648 - requestMore: lx.token( 649 - "Request that more content like the given feed item be shown in the feed", 650 - ), 651 - clickthroughItem: lx.token("User clicked through to the feed item"), 652 - clickthroughAuthor: lx.token( 653 - "User clicked through to the author of the feed item", 654 - ), 655 - clickthroughReposter: lx.token( 656 - "User clicked through to the reposter of the feed item", 657 - ), 658 - clickthroughEmbed: lx.token( 659 - "User clicked through to the embedded content of the feed item", 660 - ), 661 - contentModeUnspecified: lx.token( 662 - "Declares the feed generator returns any types of posts.", 663 - ), 664 - contentModeVideo: lx.token( 665 - "Declares the feed generator returns posts containing app.bsky.embed.video embeds.", 666 - ), 667 - interactionSeen: lx.token("Feed item was seen by user"), 668 - interactionLike: lx.token("User liked the feed item"), 669 - interactionRepost: lx.token("User reposted the feed item"), 670 - interactionReply: lx.token("User replied to the feed item"), 671 - interactionQuote: lx.token("User quoted the feed item"), 672 - interactionShare: lx.token("User shared the feed item"), 673 - }); 674 - 675 - expect(feedDefs.json.lexicon).toEqual(1); 676 - expect(feedDefs.json.id).toEqual("app.bsky.feed.defs"); 677 - expect(feedDefs.json.defs.postView.type).toEqual("object"); 678 - expect(feedDefs.json.defs.viewerState.type).toEqual("object"); 679 - expect(feedDefs.json.defs.requestLess.type).toEqual("token"); 680 - expect(feedDefs.json.defs.contentModeVideo.type).toEqual("token"); 681 - });
-119
packages/prototypey/tests/infer.bench.ts
··· 1 - import { bench } from "@ark/attest"; 2 - import { lx } from "../src/lib.ts"; 3 - 4 - bench("infer with simple object", () => { 5 - const schema = lx.lexicon("test.simple", { 6 - main: lx.object({ 7 - id: lx.string({ required: true }), 8 - name: lx.string({ required: true }), 9 - }), 10 - }); 11 - return schema.infer; 12 - }).types([741, "instantiations"]); 13 - 14 - bench("infer with complex nested structure", () => { 15 - const schema = lx.lexicon("test.complex", { 16 - user: lx.object({ 17 - handle: lx.string({ required: true }), 18 - displayName: lx.string(), 19 - }), 20 - reply: lx.object({ 21 - text: lx.string({ required: true }), 22 - author: lx.ref("#user", { required: true }), 23 - }), 24 - main: lx.record({ 25 - key: "tid", 26 - record: lx.object({ 27 - author: lx.ref("#user", { required: true }), 28 - replies: lx.array(lx.ref("#reply")), 29 - content: lx.string({ required: true }), 30 - createdAt: lx.string({ required: true, format: "datetime" }), 31 - }), 32 - }), 33 - }); 34 - return schema.infer; 35 - }).types([1040, "instantiations"]); 36 - 37 - bench("infer with circular reference", () => { 38 - const ns = lx.lexicon("test", { 39 - user: lx.object({ 40 - name: lx.string({ required: true }), 41 - posts: lx.array(lx.ref("#post")), 42 - }), 43 - post: lx.object({ 44 - title: lx.string({ required: true }), 45 - author: lx.ref("#user", { required: true }), 46 - }), 47 - main: lx.object({ 48 - users: lx.array(lx.ref("#user")), 49 - }), 50 - }); 51 - return ns.infer; 52 - }).types([692, "instantiations"]); 53 - 54 - bench("infer with app.bsky.feed.defs lexicon", () => { 55 - const schema = lx.lexicon("app.bsky.feed.defs", { 56 - viewerState: lx.object({ 57 - repost: lx.string({ format: "at-uri" }), 58 - like: lx.string({ format: "at-uri" }), 59 - bookmarked: lx.boolean(), 60 - threadMuted: lx.boolean(), 61 - replyDisabled: lx.boolean(), 62 - embeddingDisabled: lx.boolean(), 63 - pinned: lx.boolean(), 64 - }), 65 - main: lx.object({ 66 - uri: lx.string({ required: true, format: "at-uri" }), 67 - cid: lx.string({ required: true, format: "cid" }), 68 - author: lx.ref("app.bsky.actor.defs#profileViewBasic", { 69 - required: true, 70 - }), 71 - record: lx.unknown({ required: true }), 72 - embed: lx.union([ 73 - "app.bsky.embed.images#view", 74 - "app.bsky.embed.video#view", 75 - "app.bsky.embed.external#view", 76 - "app.bsky.embed.record#view", 77 - "app.bsky.embed.recordWithMedia#view", 78 - ]), 79 - bookmarkCount: lx.integer(), 80 - replyCount: lx.integer(), 81 - repostCount: lx.integer(), 82 - likeCount: lx.integer(), 83 - quoteCount: lx.integer(), 84 - indexedAt: lx.string({ required: true, format: "datetime" }), 85 - viewer: lx.ref("#viewerState"), 86 - labels: lx.array(lx.ref("com.atproto.label.defs#label")), 87 - threadgate: lx.ref("#threadgateView"), 88 - }), 89 - requestLess: lx.token( 90 - "Request that less content like the given feed item be shown in the feed", 91 - ), 92 - requestMore: lx.token( 93 - "Request that more content like the given feed item be shown in the feed", 94 - ), 95 - clickthroughItem: lx.token("User clicked through to the feed item"), 96 - clickthroughAuthor: lx.token( 97 - "User clicked through to the author of the feed item", 98 - ), 99 - clickthroughReposter: lx.token( 100 - "User clicked through to the reposter of the feed item", 101 - ), 102 - clickthroughEmbed: lx.token( 103 - "User clicked through to the embedded content of the feed item", 104 - ), 105 - contentModeUnspecified: lx.token( 106 - "Declares the feed generator returns any types of posts.", 107 - ), 108 - contentModeVideo: lx.token( 109 - "Declares the feed generator returns posts containing app.bsky.embed.video embeds.", 110 - ), 111 - interactionSeen: lx.token("Feed item was seen by user"), 112 - interactionLike: lx.token("User liked the feed item"), 113 - interactionRepost: lx.token("User reposted the feed item"), 114 - interactionReply: lx.token("User replied to the feed item"), 115 - interactionQuote: lx.token("User quoted the feed item"), 116 - interactionShare: lx.token("User shared the feed item"), 117 - }); 118 - return schema.infer; 119 - }).types([1285, "instantiations"]);
-868
packages/prototypey/tests/infer.test.ts
··· 1 - import { test } from "vitest"; 2 - import { attest } from "@ark/attest"; 3 - import { lx } from "../src/lib.ts"; 4 - 5 - test("InferNS produces expected type shape", () => { 6 - const exampleLexicon = lx.lexicon("com.example.post", { 7 - main: lx.record({ 8 - key: "tid", 9 - record: lx.object({ 10 - text: lx.string({ required: true }), 11 - createdAt: lx.string({ required: true, format: "datetime" }), 12 - likes: lx.integer(), 13 - tags: lx.array(lx.string(), { maxLength: 5 }), 14 - }), 15 - }), 16 - }); 17 - 18 - // Type snapshot - this captures how types appear on hover 19 - attest(exampleLexicon.infer).type.toString.snap(`{ 20 - $type: "com.example.post" 21 - tags?: string[] | undefined 22 - likes?: number | undefined 23 - createdAt: string 24 - text: string 25 - }`); 26 - }); 27 - 28 - test("InferObject handles required fields", () => { 29 - const schema = lx.lexicon("test", { 30 - main: lx.object({ 31 - required: lx.string({ required: true }), 32 - optional: lx.string(), 33 - }), 34 - }); 35 - 36 - attest(schema.infer).type.toString.snap(`{ 37 - $type: "test" 38 - optional?: string | undefined 39 - required: string 40 - }`); 41 - }); 42 - 43 - test("InferObject handles nullable fields", () => { 44 - const schema = lx.lexicon("test", { 45 - main: lx.object({ 46 - nullable: lx.string({ nullable: true, required: true }), 47 - }), 48 - }); 49 - 50 - attest(schema.infer).type.toString.snap( 51 - '{ $type: "test"; nullable: string | null }', 52 - ); 53 - }); 54 - 55 - // ============================================================================ 56 - // PRIMITIVE TYPES TESTS 57 - // ============================================================================ 58 - 59 - test("InferType handles string primitive", () => { 60 - const lexicon = lx.lexicon("test.string", { 61 - main: lx.object({ 62 - simpleString: lx.string(), 63 - }), 64 - }); 65 - 66 - attest(lexicon.infer).type.toString.snap(`{ 67 - $type: "test.string" 68 - simpleString?: string | undefined 69 - }`); 70 - }); 71 - 72 - test("InferType handles integer primitive", () => { 73 - const lexicon = lx.lexicon("test.integer", { 74 - main: lx.object({ 75 - count: lx.integer(), 76 - age: lx.integer({ minimum: 0, maximum: 120 }), 77 - }), 78 - }); 79 - 80 - attest(lexicon.infer).type.toString.snap(`{ 81 - $type: "test.integer" 82 - count?: number | undefined 83 - age?: number | undefined 84 - }`); 85 - }); 86 - 87 - test("InferType handles boolean primitive", () => { 88 - const lexicon = lx.lexicon("test.boolean", { 89 - main: lx.object({ 90 - isActive: lx.boolean(), 91 - hasAccess: lx.boolean({ required: true }), 92 - }), 93 - }); 94 - 95 - attest(lexicon.infer).type.toString.snap(`{ 96 - $type: "test.boolean" 97 - isActive?: boolean | undefined 98 - hasAccess: boolean 99 - }`); 100 - }); 101 - 102 - test("InferType handles null primitive", () => { 103 - const lexicon = lx.lexicon("test.null", { 104 - main: lx.object({ 105 - nullValue: lx.null(), 106 - }), 107 - }); 108 - 109 - attest(lexicon.infer).type.toString.snap(`{ 110 - $type: "test.null" 111 - nullValue?: null | undefined 112 - }`); 113 - }); 114 - 115 - test("InferType handles unknown primitive", () => { 116 - const lexicon = lx.lexicon("test.unknown", { 117 - main: lx.object({ 118 - metadata: lx.unknown(), 119 - }), 120 - }); 121 - 122 - attest(lexicon.infer).type.toString.snap( 123 - '{ $type: "test.unknown"; metadata?: unknown }', 124 - ); 125 - }); 126 - 127 - test("InferType handles bytes primitive", () => { 128 - const lexicon = lx.lexicon("test.bytes", { 129 - main: lx.object({ 130 - data: lx.bytes(), 131 - }), 132 - }); 133 - 134 - attest(lexicon.infer).type.toString.snap(`{ 135 - $type: "test.bytes" 136 - data?: Uint8Array<ArrayBufferLike> | undefined 137 - }`); 138 - }); 139 - 140 - test("InferType handles blob primitive", () => { 141 - const lexicon = lx.lexicon("test.blob", { 142 - main: lx.object({ 143 - image: lx.blob({ accept: ["image/png", "image/jpeg"] }), 144 - }), 145 - }); 146 - 147 - attest(lexicon.infer).type.toString.snap( 148 - '{ $type: "test.blob"; image?: Blob | undefined }', 149 - ); 150 - }); 151 - 152 - // ============================================================================ 153 - // TOKEN TYPE TESTS 154 - // ============================================================================ 155 - 156 - test("InferToken handles basic token without enum", () => { 157 - const lexicon = lx.lexicon("test.token", { 158 - main: lx.object({ 159 - symbol: lx.token("A symbolic value"), 160 - }), 161 - }); 162 - 163 - attest(lexicon.infer).type.toString.snap(`{ 164 - $type: "test.token" 165 - symbol?: string | undefined 166 - }`); 167 - }); 168 - 169 - // ============================================================================ 170 - // ARRAY TYPE TESTS 171 - // ============================================================================ 172 - 173 - test("InferArray handles string arrays", () => { 174 - const lexicon = lx.lexicon("test.array.string", { 175 - main: lx.object({ 176 - tags: lx.array(lx.string()), 177 - }), 178 - }); 179 - 180 - attest(lexicon.infer).type.toString.snap(`{ 181 - $type: "test.array.string" 182 - tags?: string[] | undefined 183 - }`); 184 - }); 185 - 186 - test("InferArray handles integer arrays", () => { 187 - const lexicon = lx.lexicon("test.array.integer", { 188 - main: lx.object({ 189 - scores: lx.array(lx.integer(), { minLength: 1, maxLength: 10 }), 190 - }), 191 - }); 192 - 193 - attest(lexicon.infer).type.toString.snap(`{ 194 - $type: "test.array.integer" 195 - scores?: number[] | undefined 196 - }`); 197 - }); 198 - 199 - test("InferArray handles boolean arrays", () => { 200 - const lexicon = lx.lexicon("test.array.boolean", { 201 - main: lx.object({ 202 - flags: lx.array(lx.boolean()), 203 - }), 204 - }); 205 - 206 - attest(lexicon.infer).type.toString.snap(`{ 207 - $type: "test.array.boolean" 208 - flags?: boolean[] | undefined 209 - }`); 210 - }); 211 - 212 - test("InferArray handles unknown arrays", () => { 213 - const lexicon = lx.lexicon("test.array.unknown", { 214 - main: lx.object({ 215 - items: lx.array(lx.unknown()), 216 - }), 217 - }); 218 - 219 - attest(lexicon.infer).type.toString.snap(`{ 220 - $type: "test.array.unknown" 221 - items?: unknown[] | undefined 222 - }`); 223 - }); 224 - 225 - // ============================================================================ 226 - // OBJECT PROPERTY COMBINATIONS 227 - // ============================================================================ 228 - 229 - test("InferObject handles mixed optional and required fields", () => { 230 - const lexicon = lx.lexicon("test.mixed", { 231 - main: lx.object({ 232 - id: lx.string({ required: true }), 233 - name: lx.string({ required: true }), 234 - email: lx.string(), 235 - age: lx.integer(), 236 - }), 237 - }); 238 - 239 - attest(lexicon.infer).type.toString.snap(`{ 240 - $type: "test.mixed" 241 - age?: number | undefined 242 - email?: string | undefined 243 - id: string 244 - name: string 245 - }`); 246 - }); 247 - 248 - test("InferObject handles all optional fields", () => { 249 - const lexicon = lx.lexicon("test.allOptional", { 250 - main: lx.object({ 251 - field1: lx.string(), 252 - field2: lx.integer(), 253 - field3: lx.boolean(), 254 - }), 255 - }); 256 - 257 - attest(lexicon.infer).type.toString.snap(`{ 258 - $type: "test.allOptional" 259 - field1?: string | undefined 260 - field2?: number | undefined 261 - field3?: boolean | undefined 262 - }`); 263 - }); 264 - 265 - test("InferObject handles all required fields", () => { 266 - const lexicon = lx.lexicon("test.allRequired", { 267 - main: lx.object({ 268 - field1: lx.string({ required: true }), 269 - field2: lx.integer({ required: true }), 270 - field3: lx.boolean({ required: true }), 271 - }), 272 - }); 273 - 274 - attest(lexicon.infer).type.toString.snap(`{ 275 - $type: "test.allRequired" 276 - field1: string 277 - field2: number 278 - field3: boolean 279 - }`); 280 - }); 281 - 282 - // ============================================================================ 283 - // NULLABLE FIELDS TESTS 284 - // ============================================================================ 285 - 286 - test("InferObject handles nullable optional field", () => { 287 - const lexicon = lx.lexicon("test.nullableOptional", { 288 - main: lx.object({ 289 - description: lx.string({ nullable: true }), 290 - }), 291 - }); 292 - 293 - attest(lexicon.infer).type.toString.snap(`{ 294 - $type: "test.nullableOptional" 295 - description?: string | null | undefined 296 - }`); 297 - }); 298 - 299 - test("InferObject handles multiple nullable fields", () => { 300 - const lexicon = lx.lexicon("test.multipleNullable", { 301 - main: lx.object({ 302 - field1: lx.string({ nullable: true }), 303 - field2: lx.integer({ nullable: true }), 304 - field3: lx.boolean({ nullable: true }), 305 - }), 306 - }); 307 - 308 - attest(lexicon.infer).type.toString.snap(`{ 309 - $type: "test.multipleNullable" 310 - field1?: string | null | undefined 311 - field2?: number | null | undefined 312 - field3?: boolean | null | undefined 313 - }`); 314 - }); 315 - 316 - test("InferObject handles nullable and required field", () => { 317 - const lexicon = lx.lexicon("test.nullableRequired", { 318 - main: lx.object({ 319 - value: lx.string({ nullable: true, required: true }), 320 - }), 321 - }); 322 - 323 - attest(lexicon.infer).type.toString.snap(`{ 324 - $type: "test.nullableRequired" 325 - value: string | null 326 - }`); 327 - }); 328 - 329 - test("InferObject handles mixed nullable, required, and optional", () => { 330 - const lexicon = lx.lexicon("test.mixedNullable", { 331 - main: lx.object({ 332 - requiredNullable: lx.string({ required: true, nullable: true }), 333 - optionalNullable: lx.string({ nullable: true }), 334 - required: lx.string({ required: true }), 335 - optional: lx.string(), 336 - }), 337 - }); 338 - 339 - attest(lexicon.infer).type.toString.snap(`{ 340 - $type: "test.mixedNullable" 341 - optional?: string | undefined 342 - required: string 343 - optionalNullable?: string | null | undefined 344 - requiredNullable: string | null 345 - }`); 346 - }); 347 - 348 - // ============================================================================ 349 - // REF TYPE TESTS 350 - // ============================================================================ 351 - 352 - test("InferRef handles basic reference", () => { 353 - const lexicon = lx.lexicon("test.ref", { 354 - main: lx.object({ 355 - post: lx.ref("com.example.post"), 356 - }), 357 - }); 358 - 359 - attest(lexicon.infer).type.toString.snap(`{ 360 - $type: "test.ref" 361 - post?: 362 - | { [x: string]: unknown; $type: "com.example.post" } 363 - | undefined 364 - }`); 365 - }); 366 - 367 - test("InferRef handles required reference", () => { 368 - const lexicon = lx.lexicon("test.refRequired", { 369 - main: lx.object({ 370 - author: lx.ref("com.example.user", { required: true }), 371 - }), 372 - }); 373 - 374 - attest(lexicon.infer).type.toString.snap(`{ 375 - $type: "test.refRequired" 376 - author?: 377 - | { [x: string]: unknown; $type: "com.example.user" } 378 - | undefined 379 - }`); 380 - }); 381 - 382 - test("InferRef handles nullable reference", () => { 383 - const lexicon = lx.lexicon("test.refNullable", { 384 - main: lx.object({ 385 - parent: lx.ref("com.example.node", { nullable: true }), 386 - }), 387 - }); 388 - 389 - attest(lexicon.infer).type.toString.snap(`{ 390 - $type: "test.refNullable" 391 - parent?: 392 - | { [x: string]: unknown; $type: "com.example.node" } 393 - | undefined 394 - }`); 395 - }); 396 - 397 - // ============================================================================ 398 - // UNION TYPE TESTS 399 - // ============================================================================ 400 - 401 - test("InferUnion handles basic union", () => { 402 - const lexicon = lx.lexicon("test.union", { 403 - main: lx.object({ 404 - content: lx.union(["com.example.text", "com.example.image"]), 405 - }), 406 - }); 407 - 408 - attest(lexicon.infer).type.toString.snap(`{ 409 - $type: "test.union" 410 - content?: 411 - | { [x: string]: unknown; $type: "com.example.text" } 412 - | { [x: string]: unknown; $type: "com.example.image" } 413 - | undefined 414 - }`); 415 - }); 416 - 417 - test("InferUnion handles required union", () => { 418 - const lexicon = lx.lexicon("test.unionRequired", { 419 - main: lx.object({ 420 - media: lx.union(["com.example.video", "com.example.audio"], { 421 - required: true, 422 - }), 423 - }), 424 - }); 425 - 426 - attest(lexicon.infer).type.toString.snap(`{ 427 - $type: "test.unionRequired" 428 - media: 429 - | { [x: string]: unknown; $type: "com.example.video" } 430 - | { [x: string]: unknown; $type: "com.example.audio" } 431 - }`); 432 - }); 433 - 434 - test("InferUnion handles union with many types", () => { 435 - const lexicon = lx.lexicon("test.unionMultiple", { 436 - main: lx.object({ 437 - attachment: lx.union([ 438 - "com.example.image", 439 - "com.example.video", 440 - "com.example.audio", 441 - "com.example.document", 442 - ]), 443 - }), 444 - }); 445 - 446 - attest(lexicon.infer).type.toString.snap(`{ 447 - $type: "test.unionMultiple" 448 - attachment?: 449 - | { [x: string]: unknown; $type: "com.example.image" } 450 - | { [x: string]: unknown; $type: "com.example.video" } 451 - | { [x: string]: unknown; $type: "com.example.audio" } 452 - | { 453 - [x: string]: unknown 454 - $type: "com.example.document" 455 - } 456 - | undefined 457 - }`); 458 - }); 459 - 460 - // ============================================================================ 461 - // PARAMS TYPE TESTS 462 - // ============================================================================ 463 - 464 - test("InferParams handles basic params", () => { 465 - const lexicon = lx.lexicon("test.params", { 466 - main: lx.params({ 467 - limit: lx.integer(), 468 - offset: lx.integer(), 469 - }), 470 - }); 471 - 472 - attest(lexicon.infer).type.toString.snap(`{ 473 - $type: "test.params" 474 - limit?: number | undefined 475 - offset?: number | undefined 476 - }`); 477 - }); 478 - 479 - test("InferParams handles required params", () => { 480 - const lexicon = lx.lexicon("test.paramsRequired", { 481 - main: lx.params({ 482 - query: lx.string({ required: true }), 483 - limit: lx.integer(), 484 - }), 485 - }); 486 - 487 - attest(lexicon.infer).type.toString.snap(`{ 488 - $type: "test.paramsRequired" 489 - limit?: number | undefined 490 - query: string 491 - }`); 492 - }); 493 - 494 - // ============================================================================ 495 - // RECORD TYPE TESTS 496 - // ============================================================================ 497 - 498 - test("InferRecord handles record with object schema", () => { 499 - const lexicon = lx.lexicon("test.record", { 500 - main: lx.record({ 501 - key: "tid", 502 - record: lx.object({ 503 - title: lx.string({ required: true }), 504 - content: lx.string({ required: true }), 505 - published: lx.boolean(), 506 - }), 507 - }), 508 - }); 509 - 510 - attest(lexicon.infer).type.toString.snap(`{ 511 - $type: "test.record" 512 - published?: boolean | undefined 513 - content: string 514 - title: string 515 - }`); 516 - }); 517 - 518 - // ============================================================================ 519 - // NESTED OBJECTS TESTS 520 - // ============================================================================ 521 - 522 - test("InferObject handles nested objects", () => { 523 - const lexicon = lx.lexicon("test.nested", { 524 - main: lx.object({ 525 - user: lx.object({ 526 - name: lx.string({ required: true }), 527 - email: lx.string({ required: true }), 528 - }), 529 - }), 530 - }); 531 - 532 - attest(lexicon.infer).type.toString.snap(`{ 533 - $type: "test.nested" 534 - user?: { name: string; email: string } | undefined 535 - }`); 536 - }); 537 - 538 - test("InferObject handles deeply nested objects", () => { 539 - const lexicon = lx.lexicon("test.deepNested", { 540 - main: lx.object({ 541 - data: lx.object({ 542 - user: lx.object({ 543 - profile: lx.object({ 544 - name: lx.string({ required: true }), 545 - }), 546 - }), 547 - }), 548 - }), 549 - }); 550 - 551 - attest(lexicon.infer).type.toString.snap(`{ 552 - $type: "test.deepNested" 553 - data?: 554 - | { 555 - user?: 556 - | { profile?: { name: string } | undefined } 557 - | undefined 558 - } 559 - | undefined 560 - }`); 561 - }); 562 - 563 - // ============================================================================ 564 - // NESTED ARRAYS TESTS 565 - // ============================================================================ 566 - 567 - test("InferArray handles arrays of objects", () => { 568 - const lexicon = lx.lexicon("test.arrayOfObjects", { 569 - main: lx.object({ 570 - users: lx.array( 571 - lx.object({ 572 - id: lx.string({ required: true }), 573 - name: lx.string({ required: true }), 574 - }), 575 - ), 576 - }), 577 - }); 578 - 579 - attest(lexicon.infer).type.toString.snap(`{ 580 - $type: "test.arrayOfObjects" 581 - users?: { id: string; name: string }[] | undefined 582 - }`); 583 - }); 584 - 585 - test("InferArray handles arrays of arrays", () => { 586 - const schema = lx.object({ 587 - matrix: lx.array(lx.array(lx.integer())), 588 - }); 589 - 590 - const lexicon = lx.lexicon("test.nestedArrays", { 591 - main: schema, 592 - }); 593 - 594 - attest(lexicon.infer).type.toString.snap(`{ 595 - $type: "test.nestedArrays" 596 - matrix?: number[][] | undefined 597 - }`); 598 - }); 599 - 600 - test("InferArray handles arrays of refs", () => { 601 - const lexicon = lx.lexicon("test.arrayOfRefs", { 602 - main: lx.object({ 603 - followers: lx.array(lx.ref("com.example.user")), 604 - }), 605 - }); 606 - 607 - attest(lexicon.infer).type.toString.snap(`{ 608 - $type: "test.arrayOfRefs" 609 - followers?: 610 - | { [x: string]: unknown; $type: "com.example.user" }[] 611 - | undefined 612 - }`); 613 - }); 614 - 615 - // ============================================================================ 616 - // COMPLEX NESTED STRUCTURES 617 - // ============================================================================ 618 - 619 - test("InferObject handles complex nested structure", () => { 620 - const lexicon = lx.lexicon("test.complex", { 621 - main: lx.object({ 622 - id: lx.string({ required: true }), 623 - author: lx.object({ 624 - did: lx.string({ required: true, format: "did" }), 625 - handle: lx.string({ required: true, format: "handle" }), 626 - avatar: lx.string(), 627 - }), 628 - content: lx.union(["com.example.text", "com.example.image"]), 629 - tags: lx.array(lx.string(), { maxLength: 10 }), 630 - metadata: lx.object({ 631 - views: lx.integer(), 632 - likes: lx.integer(), 633 - shares: lx.integer(), 634 - }), 635 - }), 636 - }); 637 - 638 - attest(lexicon.infer).type.toString.snap(`{ 639 - $type: "test.complex" 640 - tags?: string[] | undefined 641 - content?: 642 - | { [x: string]: unknown; $type: "com.example.text" } 643 - | { [x: string]: unknown; $type: "com.example.image" } 644 - | undefined 645 - author?: 646 - | { 647 - avatar?: string | undefined 648 - did: string 649 - handle: string 650 - } 651 - | undefined 652 - metadata?: 653 - | { 654 - likes?: number | undefined 655 - views?: number | undefined 656 - shares?: number | undefined 657 - } 658 - | undefined 659 - id: string 660 - }`); 661 - }); 662 - 663 - // ============================================================================ 664 - // MULTIPLE DEFS IN NAMESPACE 665 - // ============================================================================ 666 - 667 - test("InferNS handles multiple defs in namespace", () => { 668 - const lexicon = lx.lexicon("com.example.app", { 669 - user: lx.object({ 670 - name: lx.string({ required: true }), 671 - email: lx.string({ required: true }), 672 - }), 673 - post: lx.object({ 674 - title: lx.string({ required: true }), 675 - content: lx.string({ required: true }), 676 - }), 677 - comment: lx.object({ 678 - text: lx.string({ required: true }), 679 - author: lx.ref("com.example.user"), 680 - }), 681 - }); 682 - 683 - attest(lexicon.infer).type.toString.snap("never"); 684 - }); 685 - 686 - test("InferNS handles namespace with record and object defs", () => { 687 - const lexicon = lx.lexicon("com.example.blog", { 688 - main: lx.record({ 689 - key: "tid", 690 - record: lx.object({ 691 - title: lx.string({ required: true }), 692 - body: lx.string({ required: true }), 693 - }), 694 - }), 695 - metadata: lx.object({ 696 - category: lx.string(), 697 - tags: lx.array(lx.string()), 698 - }), 699 - }); 700 - 701 - attest(lexicon.infer).type.toString.snap(`{ 702 - $type: "com.example.blog" 703 - title: string 704 - body: string 705 - }`); 706 - }); 707 - 708 - // ============================================================================ 709 - // LOCAL REF RESOLUTION TESTS 710 - // ============================================================================ 711 - 712 - test("Local ref resolution: resolves refs to actual types", () => { 713 - const ns = lx.lexicon("test", { 714 - user: lx.object({ 715 - name: lx.string({ required: true }), 716 - email: lx.string({ required: true }), 717 - }), 718 - main: lx.object({ 719 - author: lx.ref("#user", { required: true }), 720 - content: lx.string({ required: true }), 721 - }), 722 - }); 723 - 724 - attest(ns.infer).type.toString.snap(`{ 725 - $type: "test" 726 - author?: 727 - | { name: string; email: string; $type: "#user" } 728 - | undefined 729 - content: string 730 - }`); 731 - }); 732 - 733 - test("Local ref resolution: refs in arrays", () => { 734 - const ns = lx.lexicon("test", { 735 - user: lx.object({ 736 - name: lx.string({ required: true }), 737 - }), 738 - main: lx.object({ 739 - users: lx.array(lx.ref("#user")), 740 - }), 741 - }); 742 - 743 - attest(ns.infer).type.toString.snap(`{ 744 - $type: "test" 745 - users?: { name: string; $type: "#user" }[] | undefined 746 - }`); 747 - }); 748 - 749 - test("Local ref resolution: refs in unions", () => { 750 - const ns = lx.lexicon("test", { 751 - text: lx.object({ content: lx.string({ required: true }) }), 752 - image: lx.object({ url: lx.string({ required: true }) }), 753 - main: lx.object({ 754 - embed: lx.union(["#text", "#image"]), 755 - }), 756 - }); 757 - 758 - attest(ns.infer).type.toString.snap(`{ 759 - $type: "test" 760 - embed?: 761 - | { content: string; $type: "#text" } 762 - | { url: string; $type: "#image" } 763 - | undefined 764 - }`); 765 - }); 766 - 767 - test("Local ref resolution: nested refs", () => { 768 - const ns = lx.lexicon("test", { 769 - profile: lx.object({ 770 - bio: lx.string({ required: true }), 771 - }), 772 - user: lx.object({ 773 - name: lx.string({ required: true }), 774 - profile: lx.ref("#profile", { required: true }), 775 - }), 776 - main: lx.object({ 777 - author: lx.ref("#user", { required: true }), 778 - }), 779 - }); 780 - 781 - attest(ns.infer).type.toString.snap(`{ 782 - $type: "test" 783 - author?: 784 - | { 785 - profile?: 786 - | { bio: string; $type: "#profile" } 787 - | undefined 788 - name: string 789 - $type: "#user" 790 - } 791 - | undefined 792 - }`); 793 - }); 794 - 795 - // ============================================================================ 796 - // EDGE CASE TESTS 797 - // ============================================================================ 798 - 799 - test("Edge case: circular reference detection", () => { 800 - const ns = lx.lexicon("test", { 801 - main: lx.object({ 802 - value: lx.string({ required: true }), 803 - parent: lx.ref("#main"), 804 - }), 805 - }); 806 - 807 - attest(ns.infer).type.toString.snap(`{ 808 - $type: "test" 809 - parent?: 810 - | { 811 - parent?: 812 - | "[Circular reference detected: #main]" 813 - | undefined 814 - value: string 815 - $type: "#main" 816 - } 817 - | undefined 818 - value: string 819 - }`); 820 - }); 821 - 822 - test("Edge case: circular reference between multiple types", () => { 823 - const ns = lx.lexicon("test", { 824 - user: lx.object({ 825 - name: lx.string({ required: true }), 826 - posts: lx.array(lx.ref("#post")), 827 - }), 828 - post: lx.object({ 829 - title: lx.string({ required: true }), 830 - author: lx.ref("#user", { required: true }), 831 - }), 832 - main: lx.object({ 833 - users: lx.array(lx.ref("#user")), 834 - }), 835 - }); 836 - 837 - attest(ns.infer).type.toString.snap(`{ 838 - $type: "test" 839 - users?: 840 - | { 841 - posts?: 842 - | { 843 - author?: 844 - | "[Circular reference detected: #user]" 845 - | undefined 846 - title: string 847 - $type: "#post" 848 - }[] 849 - | undefined 850 - name: string 851 - $type: "#user" 852 - }[] 853 - | undefined 854 - }`); 855 - }); 856 - 857 - test("Edge case: missing reference detection", () => { 858 - const ns = lx.lexicon("test", { 859 - main: lx.object({ 860 - author: lx.ref("#user", { required: true }), 861 - }), 862 - }); 863 - 864 - attest(ns.infer).type.toString.snap(`{ 865 - $type: "test" 866 - author?: "[Reference not found: #user]" | undefined 867 - }`); 868 - });
-789
packages/prototypey/tests/primitives.test.ts
··· 1 - import { expect, test } from "vitest"; 2 - import { lx } from "../src/lib.ts"; 3 - 4 - test("lx.null()", () => { 5 - const result = lx.null(); 6 - expect(result).toEqual({ type: "null" }); 7 - }); 8 - 9 - test("lx.boolean()", () => { 10 - const result = lx.boolean(); 11 - expect(result).toEqual({ type: "boolean" }); 12 - }); 13 - 14 - test("lx.boolean() with default", () => { 15 - const result = lx.boolean({ default: true }); 16 - expect(result).toEqual({ type: "boolean", default: true }); 17 - }); 18 - 19 - test("lx.boolean() with const", () => { 20 - const result = lx.boolean({ const: false }); 21 - expect(result).toEqual({ type: "boolean", const: false }); 22 - }); 23 - 24 - test("lx.integer()", () => { 25 - const result = lx.integer(); 26 - expect(result).toEqual({ type: "integer" }); 27 - }); 28 - 29 - test("lx.integer() with minimum", () => { 30 - const result = lx.integer({ minimum: 0 }); 31 - expect(result).toEqual({ type: "integer", minimum: 0 }); 32 - }); 33 - 34 - test("lx.integer() with maximum", () => { 35 - const result = lx.integer({ maximum: 100 }); 36 - expect(result).toEqual({ type: "integer", maximum: 100 }); 37 - }); 38 - 39 - test("lx.integer() with minimum and maximum", () => { 40 - const result = lx.integer({ minimum: 0, maximum: 100 }); 41 - expect(result).toEqual({ type: "integer", minimum: 0, maximum: 100 }); 42 - }); 43 - 44 - test("lx.integer() with enum", () => { 45 - const result = lx.integer({ enum: [1, 2, 3, 5, 8, 13] }); 46 - expect(result).toEqual({ type: "integer", enum: [1, 2, 3, 5, 8, 13] }); 47 - }); 48 - 49 - test("lx.integer() with default", () => { 50 - const result = lx.integer({ default: 42 }); 51 - expect(result).toEqual({ type: "integer", default: 42 }); 52 - }); 53 - 54 - test("lx.integer() with const", () => { 55 - const result = lx.integer({ const: 7 }); 56 - expect(result).toEqual({ type: "integer", const: 7 }); 57 - }); 58 - 59 - test("lx.string()", () => { 60 - const result = lx.string(); 61 - expect(result).toEqual({ type: "string" }); 62 - }); 63 - 64 - test("lx.string() with maxLength", () => { 65 - const result = lx.string({ maxLength: 64 }); 66 - expect(result).toEqual({ type: "string", maxLength: 64 }); 67 - }); 68 - 69 - test("lx.string() with enum", () => { 70 - const result = lx.string({ enum: ["light", "dark", "auto"] }); 71 - expect(result).toEqual({ type: "string", enum: ["light", "dark", "auto"] }); 72 - }); 73 - 74 - test("lx.unknown()", () => { 75 - const result = lx.unknown(); 76 - expect(result).toEqual({ type: "unknown" }); 77 - }); 78 - 79 - test("lx.bytes()", () => { 80 - const result = lx.bytes(); 81 - expect(result).toEqual({ type: "bytes" }); 82 - }); 83 - 84 - test("lx.bytes() with minLength", () => { 85 - const result = lx.bytes({ minLength: 1 }); 86 - expect(result).toEqual({ type: "bytes", minLength: 1 }); 87 - }); 88 - 89 - test("lx.bytes() with maxLength", () => { 90 - const result = lx.bytes({ maxLength: 1024 }); 91 - expect(result).toEqual({ type: "bytes", maxLength: 1024 }); 92 - }); 93 - 94 - test("lx.bytes() with minLength and maxLength", () => { 95 - const result = lx.bytes({ minLength: 1, maxLength: 1024 }); 96 - expect(result).toEqual({ type: "bytes", minLength: 1, maxLength: 1024 }); 97 - }); 98 - 99 - test("lx.cidLink()", () => { 100 - const result = lx.cidLink( 101 - "bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a", 102 - ); 103 - expect(result).toEqual({ 104 - type: "cid-link", 105 - $link: "bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a", 106 - }); 107 - }); 108 - 109 - test("lx.blob()", () => { 110 - const result = lx.blob(); 111 - expect(result).toEqual({ type: "blob" }); 112 - }); 113 - 114 - test("lx.blob() with accept", () => { 115 - const result = lx.blob({ accept: ["image/png", "image/jpeg"] }); 116 - expect(result).toEqual({ 117 - type: "blob", 118 - accept: ["image/png", "image/jpeg"], 119 - }); 120 - }); 121 - 122 - test("lx.blob() with maxSize", () => { 123 - const result = lx.blob({ maxSize: 1000000 }); 124 - expect(result).toEqual({ type: "blob", maxSize: 1000000 }); 125 - }); 126 - 127 - test("lx.blob() with accept and maxSize", () => { 128 - const result = lx.blob({ 129 - accept: ["image/png", "image/jpeg"], 130 - maxSize: 5000000, 131 - }); 132 - expect(result).toEqual({ 133 - type: "blob", 134 - accept: ["image/png", "image/jpeg"], 135 - maxSize: 5000000, 136 - }); 137 - }); 138 - 139 - test("lx.array() with string items", () => { 140 - const result = lx.array(lx.string()); 141 - expect(result).toEqual({ type: "array", items: { type: "string" } }); 142 - }); 143 - 144 - test("lx.array() with integer items", () => { 145 - const result = lx.array(lx.integer()); 146 - expect(result).toEqual({ type: "array", items: { type: "integer" } }); 147 - }); 148 - 149 - test("lx.array() with minLength", () => { 150 - const result = lx.array(lx.string(), { minLength: 1 }); 151 - expect(result).toEqual({ 152 - type: "array", 153 - items: { type: "string" }, 154 - minLength: 1, 155 - }); 156 - }); 157 - 158 - test("lx.array() with maxLength", () => { 159 - const result = lx.array(lx.string(), { maxLength: 10 }); 160 - expect(result).toEqual({ 161 - type: "array", 162 - items: { type: "string" }, 163 - maxLength: 10, 164 - }); 165 - }); 166 - 167 - test("lx.array() with minLength and maxLength", () => { 168 - const result = lx.array(lx.string(), { minLength: 1, maxLength: 10 }); 169 - expect(result).toEqual({ 170 - type: "array", 171 - items: { type: "string" }, 172 - minLength: 1, 173 - maxLength: 10, 174 - }); 175 - }); 176 - 177 - test("lx.array() with required", () => { 178 - const result = lx.array(lx.string(), { required: true }); 179 - expect(result).toEqual({ 180 - type: "array", 181 - items: { type: "string" }, 182 - required: true, 183 - }); 184 - }); 185 - 186 - test("lx.token() with interaction event", () => { 187 - const result = lx.token( 188 - "Request that less content like the given feed item be shown in the feed", 189 - ); 190 - expect(result).toEqual({ 191 - type: "token", 192 - description: 193 - "Request that less content like the given feed item be shown in the feed", 194 - }); 195 - }); 196 - 197 - test("lx.token() with content mode", () => { 198 - const result = lx.token( 199 - "Declares the feed generator returns posts containing app.bsky.embed.video embeds", 200 - ); 201 - expect(result).toEqual({ 202 - type: "token", 203 - description: 204 - "Declares the feed generator returns posts containing app.bsky.embed.video embeds", 205 - }); 206 - }); 207 - 208 - test("lx.ref() with local definition", () => { 209 - const result = lx.ref("#profileAssociated"); 210 - expect(result).toEqual({ 211 - type: "ref", 212 - ref: "#profileAssociated", 213 - }); 214 - }); 215 - 216 - test("lx.ref() with external schema", () => { 217 - const result = lx.ref("com.atproto.label.defs#label"); 218 - expect(result).toEqual({ 219 - type: "ref", 220 - ref: "com.atproto.label.defs#label", 221 - }); 222 - }); 223 - 224 - test("lx.ref() with required option", () => { 225 - const result = lx.ref("#profileView", { required: true }); 226 - expect(result).toEqual({ 227 - type: "ref", 228 - ref: "#profileView", 229 - required: true, 230 - }); 231 - }); 232 - 233 - test("lx.ref() with nullable option", () => { 234 - const result = lx.ref("#profileView", { nullable: true }); 235 - expect(result).toEqual({ 236 - type: "ref", 237 - ref: "#profileView", 238 - nullable: true, 239 - }); 240 - }); 241 - 242 - test("lx.ref() with both required and nullable", () => { 243 - const result = lx.ref("app.bsky.actor.defs#profileView", { 244 - required: true, 245 - nullable: true, 246 - }); 247 - expect(result).toEqual({ 248 - type: "ref", 249 - ref: "app.bsky.actor.defs#profileView", 250 - required: true, 251 - nullable: true, 252 - }); 253 - }); 254 - 255 - test("lx.union() with local refs", () => { 256 - const result = lx.union(["#reasonRepost", "#reasonPin"]); 257 - expect(result).toEqual({ 258 - type: "union", 259 - refs: ["#reasonRepost", "#reasonPin"], 260 - }); 261 - }); 262 - 263 - test("lx.union() with external refs", () => { 264 - const result = lx.union([ 265 - "app.bsky.embed.images#view", 266 - "app.bsky.embed.video#view", 267 - "app.bsky.embed.external#view", 268 - "app.bsky.embed.record#view", 269 - "app.bsky.embed.recordWithMedia#view", 270 - ]); 271 - expect(result).toEqual({ 272 - type: "union", 273 - refs: [ 274 - "app.bsky.embed.images#view", 275 - "app.bsky.embed.video#view", 276 - "app.bsky.embed.external#view", 277 - "app.bsky.embed.record#view", 278 - "app.bsky.embed.recordWithMedia#view", 279 - ], 280 - }); 281 - }); 282 - 283 - test("lx.union() with closed option", () => { 284 - const result = lx.union(["#postView", "#notFoundPost", "#blockedPost"], { 285 - closed: true, 286 - }); 287 - expect(result).toEqual({ 288 - type: "union", 289 - refs: ["#postView", "#notFoundPost", "#blockedPost"], 290 - closed: true, 291 - }); 292 - }); 293 - 294 - test("lx.union() with closed: false (open union)", () => { 295 - const result = lx.union(["#threadViewPost", "#notFoundPost"], { 296 - closed: false, 297 - }); 298 - expect(result).toEqual({ 299 - type: "union", 300 - refs: ["#threadViewPost", "#notFoundPost"], 301 - closed: false, 302 - }); 303 - }); 304 - 305 - test("lx.params() with basic properties", () => { 306 - const result = lx.params({ 307 - q: lx.string(), 308 - limit: lx.integer(), 309 - }); 310 - expect(result).toEqual({ 311 - type: "params", 312 - properties: { 313 - q: { type: "string" }, 314 - limit: { type: "integer" }, 315 - }, 316 - }); 317 - }); 318 - 319 - test("lx.params() with required properties", () => { 320 - const result = lx.params({ 321 - q: lx.string({ required: true }), 322 - limit: lx.integer(), 323 - }); 324 - expect(result).toEqual({ 325 - type: "params", 326 - properties: { 327 - q: { type: "string", required: true }, 328 - limit: { type: "integer" }, 329 - }, 330 - required: ["q"], 331 - }); 332 - }); 333 - 334 - test("lx.params() with property options", () => { 335 - const result = lx.params({ 336 - q: lx.string(), 337 - limit: lx.integer({ minimum: 1, maximum: 100, default: 25 }), 338 - cursor: lx.string(), 339 - }); 340 - expect(result).toEqual({ 341 - type: "params", 342 - properties: { 343 - q: { type: "string" }, 344 - limit: { type: "integer", minimum: 1, maximum: 100, default: 25 }, 345 - cursor: { type: "string" }, 346 - }, 347 - }); 348 - }); 349 - 350 - test("lx.params() with array properties", () => { 351 - const result = lx.params({ 352 - tags: lx.array(lx.string()), 353 - ids: lx.array(lx.integer()), 354 - }); 355 - expect(result).toEqual({ 356 - type: "params", 357 - properties: { 358 - tags: { type: "array", items: { type: "string" } }, 359 - ids: { type: "array", items: { type: "integer" } }, 360 - }, 361 - }); 362 - }); 363 - 364 - test("lx.params() real-world example from searchActors", () => { 365 - const result = lx.params({ 366 - q: lx.string({ required: true }), 367 - limit: lx.integer({ minimum: 1, maximum: 100, default: 25 }), 368 - cursor: lx.string(), 369 - }); 370 - expect(result).toEqual({ 371 - type: "params", 372 - properties: { 373 - q: { type: "string", required: true }, 374 - limit: { type: "integer", minimum: 1, maximum: 100, default: 25 }, 375 - cursor: { type: "string" }, 376 - }, 377 - required: ["q"], 378 - }); 379 - }); 380 - 381 - test("lx.query() basic", () => { 382 - const result = lx.query(); 383 - expect(result).toEqual({ type: "query" }); 384 - }); 385 - 386 - test("lx.query() with description", () => { 387 - const result = lx.query({ description: "Search for actors" }); 388 - expect(result).toEqual({ type: "query", description: "Search for actors" }); 389 - }); 390 - 391 - test("lx.query() with parameters", () => { 392 - const result = lx.query({ 393 - parameters: lx.params({ 394 - q: lx.string({ required: true }), 395 - limit: lx.integer({ minimum: 1, maximum: 100, default: 25 }), 396 - }), 397 - }); 398 - expect(result).toEqual({ 399 - type: "query", 400 - parameters: { 401 - type: "params", 402 - properties: { 403 - q: { type: "string", required: true }, 404 - limit: { type: "integer", minimum: 1, maximum: 100, default: 25 }, 405 - }, 406 - required: ["q"], 407 - }, 408 - }); 409 - }); 410 - 411 - test("lx.query() with output", () => { 412 - const result = lx.query({ 413 - output: { 414 - encoding: "application/json", 415 - schema: lx.object({ 416 - posts: lx.array(lx.ref("app.bsky.feed.defs#postView"), { 417 - required: true, 418 - }), 419 - cursor: lx.string(), 420 - }), 421 - }, 422 - }); 423 - expect(result).toEqual({ 424 - type: "query", 425 - output: { 426 - encoding: "application/json", 427 - schema: { 428 - type: "object", 429 - properties: { 430 - posts: { 431 - type: "array", 432 - items: { type: "ref", ref: "app.bsky.feed.defs#postView" }, 433 - required: true, 434 - }, 435 - cursor: { type: "string" }, 436 - }, 437 - required: ["posts"], 438 - }, 439 - }, 440 - }); 441 - }); 442 - 443 - test("lx.query() with errors", () => { 444 - const result = lx.query({ 445 - errors: [{ name: "BadQueryString" }], 446 - }); 447 - expect(result).toEqual({ 448 - type: "query", 449 - errors: [{ name: "BadQueryString" }], 450 - }); 451 - }); 452 - 453 - test("lx.query() real-world example: searchPosts", () => { 454 - const result = lx.query({ 455 - description: "Find posts matching search criteria", 456 - parameters: lx.params({ 457 - q: lx.string({ required: true }), 458 - sort: lx.string({ enum: ["top", "latest"], default: "latest" }), 459 - limit: lx.integer({ minimum: 1, maximum: 100, default: 25 }), 460 - cursor: lx.string(), 461 - }), 462 - output: { 463 - encoding: "application/json", 464 - schema: lx.object({ 465 - cursor: lx.string(), 466 - hitsTotal: lx.integer(), 467 - posts: lx.array(lx.ref("app.bsky.feed.defs#postView"), { 468 - required: true, 469 - }), 470 - }), 471 - }, 472 - errors: [{ name: "BadQueryString" }], 473 - }); 474 - expect(result).toEqual({ 475 - type: "query", 476 - description: "Find posts matching search criteria", 477 - parameters: { 478 - type: "params", 479 - properties: { 480 - q: { type: "string", required: true }, 481 - sort: { type: "string", enum: ["top", "latest"], default: "latest" }, 482 - limit: { type: "integer", minimum: 1, maximum: 100, default: 25 }, 483 - cursor: { type: "string" }, 484 - }, 485 - required: ["q"], 486 - }, 487 - output: { 488 - encoding: "application/json", 489 - schema: { 490 - type: "object", 491 - properties: { 492 - cursor: { type: "string" }, 493 - hitsTotal: { type: "integer" }, 494 - posts: { 495 - type: "array", 496 - items: { type: "ref", ref: "app.bsky.feed.defs#postView" }, 497 - required: true, 498 - }, 499 - }, 500 - required: ["posts"], 501 - }, 502 - }, 503 - errors: [{ name: "BadQueryString" }], 504 - }); 505 - }); 506 - 507 - test("lx.procedure() basic", () => { 508 - const result = lx.procedure(); 509 - expect(result).toEqual({ type: "procedure" }); 510 - }); 511 - 512 - test("lx.procedure() with description", () => { 513 - const result = lx.procedure({ description: "Create a new post" }); 514 - expect(result).toEqual({ 515 - type: "procedure", 516 - description: "Create a new post", 517 - }); 518 - }); 519 - 520 - test("lx.procedure() with parameters", () => { 521 - const result = lx.procedure({ 522 - parameters: lx.params({ 523 - validate: lx.boolean({ default: true }), 524 - }), 525 - }); 526 - expect(result).toEqual({ 527 - type: "procedure", 528 - parameters: { 529 - type: "params", 530 - properties: { 531 - validate: { type: "boolean", default: true }, 532 - }, 533 - }, 534 - }); 535 - }); 536 - 537 - test("lx.procedure() with input", () => { 538 - const result = lx.procedure({ 539 - input: { 540 - encoding: "application/json", 541 - schema: lx.object({ 542 - text: lx.string({ required: true, maxGraphemes: 300 }), 543 - createdAt: lx.string({ format: "datetime" }), 544 - }), 545 - }, 546 - }); 547 - expect(result).toEqual({ 548 - type: "procedure", 549 - input: { 550 - encoding: "application/json", 551 - schema: { 552 - type: "object", 553 - properties: { 554 - text: { type: "string", required: true, maxGraphemes: 300 }, 555 - createdAt: { type: "string", format: "datetime" }, 556 - }, 557 - required: ["text"], 558 - }, 559 - }, 560 - }); 561 - }); 562 - 563 - test("lx.procedure() with output", () => { 564 - const result = lx.procedure({ 565 - output: { 566 - encoding: "application/json", 567 - schema: lx.object({ 568 - uri: lx.string({ required: true }), 569 - cid: lx.string({ required: true }), 570 - }), 571 - }, 572 - }); 573 - expect(result).toEqual({ 574 - type: "procedure", 575 - output: { 576 - encoding: "application/json", 577 - schema: { 578 - type: "object", 579 - properties: { 580 - uri: { type: "string", required: true }, 581 - cid: { type: "string", required: true }, 582 - }, 583 - required: ["uri", "cid"], 584 - }, 585 - }, 586 - }); 587 - }); 588 - 589 - test("lx.procedure() with errors", () => { 590 - const result = lx.procedure({ 591 - errors: [ 592 - { name: "InvalidRequest" }, 593 - { name: "RateLimitExceeded", description: "Too many requests" }, 594 - ], 595 - }); 596 - expect(result).toEqual({ 597 - type: "procedure", 598 - errors: [ 599 - { name: "InvalidRequest" }, 600 - { name: "RateLimitExceeded", description: "Too many requests" }, 601 - ], 602 - }); 603 - }); 604 - 605 - test("lx.procedure() real-world example: createPost", () => { 606 - const result = lx.procedure({ 607 - description: "Create a post", 608 - input: { 609 - encoding: "application/json", 610 - schema: lx.object({ 611 - repo: lx.string({ required: true }), 612 - collection: lx.string({ required: true }), 613 - record: lx.unknown({ required: true }), 614 - validate: lx.boolean({ default: true }), 615 - }), 616 - }, 617 - output: { 618 - encoding: "application/json", 619 - schema: lx.object({ 620 - uri: lx.string({ required: true }), 621 - cid: lx.string({ required: true }), 622 - }), 623 - }, 624 - errors: [{ name: "InvalidSwap" }, { name: "InvalidRecord" }], 625 - }); 626 - expect(result).toEqual({ 627 - type: "procedure", 628 - description: "Create a post", 629 - input: { 630 - encoding: "application/json", 631 - schema: { 632 - type: "object", 633 - properties: { 634 - repo: { type: "string", required: true }, 635 - collection: { type: "string", required: true }, 636 - record: { type: "unknown", required: true }, 637 - validate: { type: "boolean", default: true }, 638 - }, 639 - required: ["repo", "collection", "record"], 640 - }, 641 - }, 642 - output: { 643 - encoding: "application/json", 644 - schema: { 645 - type: "object", 646 - properties: { 647 - uri: { type: "string", required: true }, 648 - cid: { type: "string", required: true }, 649 - }, 650 - required: ["uri", "cid"], 651 - }, 652 - }, 653 - errors: [{ name: "InvalidSwap" }, { name: "InvalidRecord" }], 654 - }); 655 - }); 656 - 657 - test("lx.subscription() basic", () => { 658 - const result = lx.subscription(); 659 - expect(result).toEqual({ type: "subscription" }); 660 - }); 661 - 662 - test("lx.subscription() with description", () => { 663 - const result = lx.subscription({ 664 - description: "Repository event stream", 665 - }); 666 - expect(result).toEqual({ 667 - type: "subscription", 668 - description: "Repository event stream", 669 - }); 670 - }); 671 - 672 - test("lx.subscription() with parameters", () => { 673 - const result = lx.subscription({ 674 - parameters: lx.params({ 675 - cursor: lx.integer(), 676 - }), 677 - }); 678 - expect(result).toEqual({ 679 - type: "subscription", 680 - parameters: { 681 - type: "params", 682 - properties: { 683 - cursor: { type: "integer" }, 684 - }, 685 - }, 686 - }); 687 - }); 688 - 689 - test("lx.subscription() with message", () => { 690 - const result = lx.subscription({ 691 - message: { 692 - schema: lx.union(["#commit", "#identity", "#account"]), 693 - }, 694 - }); 695 - expect(result).toEqual({ 696 - type: "subscription", 697 - message: { 698 - schema: { 699 - type: "union", 700 - refs: ["#commit", "#identity", "#account"], 701 - }, 702 - }, 703 - }); 704 - }); 705 - 706 - test("lx.subscription() with message description", () => { 707 - const result = lx.subscription({ 708 - message: { 709 - description: "Event message types", 710 - schema: lx.union(["#commit", "#handle", "#migrate"]), 711 - }, 712 - }); 713 - expect(result).toEqual({ 714 - type: "subscription", 715 - message: { 716 - description: "Event message types", 717 - schema: { 718 - type: "union", 719 - refs: ["#commit", "#handle", "#migrate"], 720 - }, 721 - }, 722 - }); 723 - }); 724 - 725 - test("lx.subscription() with errors", () => { 726 - const result = lx.subscription({ 727 - errors: [ 728 - { name: "FutureCursor" }, 729 - { name: "ConsumerTooSlow", description: "Consumer is too slow" }, 730 - ], 731 - }); 732 - expect(result).toEqual({ 733 - type: "subscription", 734 - errors: [ 735 - { name: "FutureCursor" }, 736 - { name: "ConsumerTooSlow", description: "Consumer is too slow" }, 737 - ], 738 - }); 739 - }); 740 - 741 - test("lx.subscription() real-world example: subscribeRepos", () => { 742 - const result = lx.subscription({ 743 - description: "Repository event stream, aka Firehose endpoint", 744 - parameters: lx.params({ 745 - cursor: lx.integer(), 746 - }), 747 - message: { 748 - description: "Represents an update of repository state", 749 - schema: lx.union([ 750 - "#commit", 751 - "#identity", 752 - "#account", 753 - "#handle", 754 - "#migrate", 755 - "#tombstone", 756 - "#info", 757 - ]), 758 - }, 759 - errors: [{ name: "FutureCursor" }, { name: "ConsumerTooSlow" }], 760 - }); 761 - expect(result).toEqual({ 762 - type: "subscription", 763 - description: "Repository event stream, aka Firehose endpoint", 764 - parameters: { 765 - type: "params", 766 - properties: { 767 - cursor: { 768 - type: "integer", 769 - }, 770 - }, 771 - }, 772 - message: { 773 - description: "Represents an update of repository state", 774 - schema: { 775 - type: "union", 776 - refs: [ 777 - "#commit", 778 - "#identity", 779 - "#account", 780 - "#handle", 781 - "#migrate", 782 - "#tombstone", 783 - "#info", 784 - ], 785 - }, 786 - }, 787 - errors: [{ name: "FutureCursor" }, { name: "ConsumerTooSlow" }], 788 - }); 789 - });
+1 -1
packages/prototypey/tsconfig.json
··· 1 1 { 2 2 "extends": "../../tsconfig.json", 3 - "include": ["src", "tests"] 3 + "include": ["core", "cli"] 4 4 }
+2 -1
packages/prototypey/tsdown.config.ts
··· 2 2 3 3 export default defineConfig({ 4 4 dts: true, 5 - entry: ["src/index.ts"], 5 + entry: ["core/main.ts", "cli/main.ts"], 6 + clean: true, 6 7 outDir: "lib", 7 8 unbundle: true, 8 9 });
+1 -1
packages/prototypey/vitest.config.ts
··· 2 2 3 3 export default defineConfig({ 4 4 test: { 5 - include: ["tests/*.test.ts"], 5 + include: ["**/*.test.ts"], 6 6 globalSetup: ["setup-vitest.ts"], 7 7 }, 8 8 });
+23
packages/site/eslint.config.js
··· 1 + import baseConfig from "../../eslint.config.js"; 2 + import reactCompiler from "eslint-plugin-react-compiler"; 3 + import tseslint from "typescript-eslint"; 4 + 5 + export default tseslint.config(...baseConfig, { 6 + files: ["**/*.{jsx,tsx}"], 7 + extends: [ 8 + tseslint.configs.strictTypeChecked, 9 + tseslint.configs.stylisticTypeChecked, 10 + ], 11 + languageOptions: { 12 + parserOptions: { 13 + projectService: { allowDefaultProject: ["*.config.*s"] }, 14 + }, 15 + }, 16 + plugins: { 17 + "react-compiler": reactCompiler, 18 + }, 19 + rules: { 20 + "@typescript-eslint/consistent-type-definitions": "off", 21 + "react-compiler/react-compiler": "error", 22 + }, 23 + });
+1 -10
packages/site/index.html
··· 3 3 <head> 4 4 <meta charset="UTF-8" /> 5 5 <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 6 - <title>prototypey - Type-safe lexicon inference for ATProto</title> 6 + <title>prototypey</title> 7 7 </head> 8 8 <body> 9 9 <div id="root"></div> 10 10 <script type="module" src="/src/main.tsx"></script> 11 - <script> 12 - // Append to the <body>; can use a CSS selector to append somewhere else. 13 - window.goatcounter.visit_count({ append: "body" }); 14 - </script> 15 - <script 16 - data-goatcounter="https://tylerlaws0n.goatcounter.com/count" 17 - async 18 - src="//gc.zgo.at/count.js" 19 - ></script> 20 11 </body> 21 12 </html>
+14 -11
packages/site/package.json
··· 6 6 "scripts": { 7 7 "dev": "vite", 8 8 "build": "tsc && vite build", 9 + "lint": "eslint .", 9 10 "preview": "vite preview", 10 11 "test": "vitest" 11 12 }, 12 13 "dependencies": { 13 - "@monaco-editor/react": "^4.6.0", 14 + "@monaco-editor/react": "^4.7.0", 14 15 "lz-string": "^1.5.0", 15 16 "monaco-editor": "0.52.0", 16 - "nuqs": "^2.7.2", 17 + "nuqs": "^2.8.8", 17 18 "prototypey": "workspace:*", 18 - "react": "^19.2.0", 19 - "react-dom": "^19.2.0" 19 + "react": "^19.2.4", 20 + "react-dom": "^19.2.4" 20 21 }, 21 22 "devDependencies": { 23 + "@tailwindcss/vite": "^4.1.18", 22 24 "@testing-library/jest-dom": "^6.9.1", 23 - "@testing-library/react": "^16.1.0", 24 - "@testing-library/user-event": "^14.5.2", 25 - "@types/react": "^19.2.2", 26 - "@types/react-dom": "^19.2.2", 27 - "@vitejs/plugin-react": "^5.0.4", 25 + "@testing-library/react": "^16.3.2", 26 + "@testing-library/user-event": "^14.6.1", 27 + "@types/react": "^19.2.13", 28 + "@types/react-dom": "^19.2.3", 29 + "@vitejs/plugin-react": "^5.1.3", 28 30 "babel-plugin-react-compiler": "^1.0.0", 29 31 "eslint-plugin-react-compiler": "19.1.0-rc.2", 30 32 "jsdom": "^25.0.1", 31 - "typescript": "5.8.3", 32 - "vite": "^6.0.5", 33 + "tailwindcss": "^4.1.18", 34 + "typescript": "5.9.3", 35 + "vite": "^6.4.1", 33 36 "vitest": "^3.2.4" 34 37 } 35 38 }
+2 -2
packages/site/src/App.tsx
··· 3 3 4 4 export function App() { 5 5 return ( 6 - <> 6 + <div className="flex flex-col min-h-screen w-full bg-white dark:bg-gray-900 text-gray-700 dark:text-gray-200"> 7 7 <Header /> 8 8 <Playground /> 9 - </> 9 + </div> 10 10 ); 11 11 }
+16 -37
packages/site/src/components/Editor.tsx
··· 22 22 setTheme(e.matches ? "vs-dark" : "vs-light"); 23 23 }; 24 24 mediaQuery.addEventListener("change", handleChange); 25 - return () => mediaQuery.removeEventListener("change", handleChange); 25 + return () => { 26 + mediaQuery.removeEventListener("change", handleChange); 27 + }; 26 28 }, []); 27 29 28 30 useEffect(() => { ··· 44 46 noSyntaxValidation: false, 45 47 }); 46 48 47 - Promise.all([ 48 - import("prototypey/lib/type-utils.d.ts?raw"), 49 - import("prototypey/lib/infer.d.ts?raw"), 50 - import("prototypey/lib/lib.d.ts?raw"), 49 + void Promise.all([ 50 + import("prototypey/lib/core/type-utils.d.ts?raw"), 51 + import("prototypey/lib/core/infer.d.ts?raw"), 52 + import("prototypey/lib/core/lib.d.ts?raw"), 51 53 ]).then(([typeUtilsModule, inferModule, libModule]) => { 52 54 const stripImportsExports = (content: string) => 53 55 content ··· 81 83 82 84 if (!isReady) { 83 85 return ( 84 - <div style={{ flex: 1, display: "flex", flexDirection: "column" }}> 85 - <div 86 - style={{ 87 - padding: "0.75rem 1rem", 88 - backgroundColor: "var(--color-bg-secondary)", 89 - borderBottom: "1px solid var(--color-border)", 90 - fontSize: "0.875rem", 91 - fontWeight: "600", 92 - color: "var(--color-text-secondary)", 93 - }} 94 - > 86 + <div className="flex-1 flex flex-col"> 87 + <div className="py-3 px-4 bg-gray-50 dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 text-sm font-semibold text-gray-500 dark:text-gray-400"> 95 88 Input 96 89 </div> 97 - <div 98 - style={{ 99 - flex: 1, 100 - display: "flex", 101 - alignItems: "center", 102 - justifyContent: "center", 103 - }} 104 - > 90 + <div className="flex-1 flex items-center justify-center"> 105 91 Loading... 106 92 </div> 107 93 </div> ··· 109 95 } 110 96 111 97 return ( 112 - <div style={{ flex: 1, display: "flex", flexDirection: "column" }}> 113 - <div 114 - style={{ 115 - padding: "0.75rem 1rem", 116 - backgroundColor: "var(--color-bg-secondary)", 117 - borderBottom: "1px solid var(--color-border)", 118 - fontSize: "0.875rem", 119 - fontWeight: "600", 120 - color: "var(--color-text-secondary)", 121 - }} 122 - > 98 + <div className="flex-1 flex flex-col"> 99 + <div className="py-3 px-4 bg-gray-50 dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 text-sm font-semibold text-gray-500 dark:text-gray-400"> 123 100 Input 124 101 </div> 125 - <div style={{ flex: 1 }}> 102 + <div className="flex-1"> 126 103 <MonacoEditor 127 104 height="100%" 128 105 defaultLanguage="typescript" 129 106 path="file:///main.ts" 130 107 value={value} 131 - onChange={(value) => onChange(value || "")} 108 + onChange={(value) => { 109 + onChange(value ?? ""); 110 + }} 132 111 theme={theme} 133 112 options={{ 134 113 minimap: { enabled: false },
+151 -88
packages/site/src/components/Header.tsx
··· 1 - export function Header() { 1 + import { useState } from "react"; 2 + 3 + interface HeaderLinkProps { 4 + href: string; 5 + icon: React.ReactNode; 6 + label: string; 7 + } 8 + 9 + function HeaderLink({ href, icon, label }: HeaderLinkProps) { 2 10 return ( 3 - <header 4 - style={{ 5 - padding: "2rem 2rem 1rem 2rem", 6 - borderBottom: "1px solid var(--color-border)", 7 - }} 11 + <a 12 + href={href} 13 + target="_blank" 14 + rel="noopener noreferrer" 15 + className="text-gray-900 dark:text-gray-100 no-underline text-base font-semibold flex items-center gap-2 transition-opacity duration-200 hover:opacity-60" 8 16 > 9 - <div style={{ maxWidth: "1400px", margin: "0 auto" }}> 10 - <div className="header-content"> 17 + {icon} 18 + {label} 19 + </a> 20 + ); 21 + } 22 + 23 + export function Header() { 24 + const [copied, setCopied] = useState(false); 25 + 26 + const handleShare = () => { 27 + try { 28 + void navigator.clipboard.writeText(window.location.href); 29 + setCopied(true); 30 + setTimeout(() => { 31 + setCopied(false); 32 + }, 1500); 33 + } catch (err) { 34 + console.error("Failed to copy URL:", err); 35 + } 36 + }; 37 + 38 + return ( 39 + <header className="py-8 px-8 pb-4 border-b border-gray-200 dark:border-gray-700"> 40 + <div className="max-w-[1400px] mx-auto"> 41 + <div className="flex justify-between items-start flex-col md:flex-row gap-4 md:gap-0"> 11 42 <div> 12 - <h1 13 - style={{ 14 - fontSize: "2.5rem", 15 - fontWeight: "700", 16 - marginBottom: "0.5rem", 17 - }} 18 - > 19 - <span style={{ color: "var(--color-text-secondary)" }}> 20 - at:// 21 - </span> 43 + <h1 className="text-4xl font-bold mb-2"> 44 + <span className="text-gray-500 dark:text-gray-400">at://</span> 22 45 prototypey 23 46 </h1> 24 - <p 25 - style={{ 26 - fontSize: "1.125rem", 27 - color: "var(--color-text-secondary)", 28 - marginTop: "0.5rem", 29 - }} 30 - > 31 - Type-safe lexicon inference for ATProto schemas 47 + <p className="text-lg text-gray-500 dark:text-gray-400 mt-2"> 48 + ATProto lexicon typescript toolkit 32 49 </p> 33 50 </div> 34 - <div className="header-links"> 35 - <a 36 - href="https://github.com/tylersayshi/prototypey" 37 - target="_blank" 38 - rel="noopener noreferrer" 39 - style={{ 40 - color: "var(--color-text-heading)", 41 - textDecoration: "none", 42 - fontSize: "1rem", 43 - fontWeight: "600", 44 - display: "flex", 45 - alignItems: "center", 46 - gap: "0.5rem", 47 - transition: "opacity 0.2s", 48 - }} 49 - onMouseEnter={(e) => (e.currentTarget.style.opacity = "0.6")} 50 - onMouseLeave={(e) => (e.currentTarget.style.opacity = "1")} 51 + <div className="flex flex-col items-end gap-4 w-full md:w-auto"> 52 + <div className="flex gap-5 pt-2 md:pt-0 w-full md:w-auto justify-start"> 53 + <HeaderLink 54 + href="https://github.com/tylersayshi/prototypey?tab=readme-ov-file#prototypey" 55 + icon={ 56 + <svg 57 + width="20" 58 + height="20" 59 + viewBox="0 0 24 24" 60 + fill="currentColor" 61 + aria-hidden="true" 62 + className="shrink-0" 63 + > 64 + <path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z" /> 65 + </svg> 66 + } 67 + label="docs" 68 + /> 69 + <HeaderLink 70 + href="https://www.npmjs.com/package/prototypey" 71 + icon={ 72 + <svg 73 + width="20" 74 + height="20" 75 + viewBox="0 0 24 24" 76 + fill="none" 77 + stroke="currentColor" 78 + strokeWidth="2" 79 + strokeLinecap="round" 80 + strokeLinejoin="round" 81 + aria-hidden="true" 82 + className="shrink-0" 83 + > 84 + <path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z" /> 85 + <polyline points="3.27 6.96 12 12.01 20.73 6.96" /> 86 + <line x1="12" y1="22.08" x2="12" y2="12" /> 87 + </svg> 88 + } 89 + label="npm" 90 + /> 91 + <HeaderLink 92 + href="https://notes.tylur.dev/3m5a3do4eus2w" 93 + icon={ 94 + <svg 95 + width="20" 96 + height="20" 97 + viewBox="0 0 24 24" 98 + fill="none" 99 + stroke="currentColor" 100 + strokeWidth="2" 101 + strokeLinecap="round" 102 + strokeLinejoin="round" 103 + aria-hidden="true" 104 + className="shrink-0" 105 + > 106 + <path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20" /> 107 + <path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z" /> 108 + </svg> 109 + } 110 + label="story" 111 + /> 112 + </div> 113 + <button 114 + onClick={handleShare} 115 + className="text-gray-900 dark:text-gray-100 text-sm font-semibold flex items-center gap-2 transition-all duration-200 bg-transparent border border-gray-200 dark:border-gray-700 rounded-lg cursor-pointer py-2 px-4 shadow-none outline-none shrink-0 whitespace-nowrap hover:bg-gray-50 dark:hover:bg-gray-800 hover:border-gray-900 dark:hover:border-gray-100" 116 + aria-label="share playground" 51 117 > 52 - <svg 53 - width="20" 54 - height="20" 55 - viewBox="0 0 24 24" 56 - fill="currentColor" 57 - aria-hidden="true" 58 - > 59 - <path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z" /> 60 - </svg> 61 - GitHub 62 - </a> 63 - <a 64 - href="https://www.npmjs.com/package/prototypey" 65 - target="_blank" 66 - rel="noopener noreferrer" 67 - style={{ 68 - color: "var(--color-text-heading)", 69 - textDecoration: "none", 70 - fontSize: "1rem", 71 - fontWeight: "600", 72 - display: "flex", 73 - alignItems: "center", 74 - gap: "0.5rem", 75 - transition: "opacity 0.2s", 76 - }} 77 - onMouseEnter={(e) => (e.currentTarget.style.opacity = "0.6")} 78 - onMouseLeave={(e) => (e.currentTarget.style.opacity = "1")} 79 - > 80 - <svg 81 - width="20" 82 - height="20" 83 - viewBox="0 0 24 24" 84 - fill="none" 85 - stroke="currentColor" 86 - strokeWidth="2" 87 - strokeLinecap="round" 88 - strokeLinejoin="round" 89 - aria-hidden="true" 90 - > 91 - <path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z" /> 92 - <polyline points="3.27 6.96 12 12.01 20.73 6.96" /> 93 - <line x1="12" y1="22.08" x2="12" y2="12" /> 94 - </svg> 95 - npm 96 - </a> 118 + {copied ? ( 119 + <> 120 + <svg 121 + width="16" 122 + height="16" 123 + viewBox="0 0 24 24" 124 + fill="none" 125 + stroke="currentColor" 126 + strokeWidth="2" 127 + strokeLinecap="round" 128 + strokeLinejoin="round" 129 + aria-hidden="true" 130 + className="shrink-0" 131 + > 132 + <polyline points="20 6 9 17 4 12" /> 133 + </svg> 134 + copied url! 135 + </> 136 + ) : ( 137 + <> 138 + <svg 139 + width="16" 140 + height="16" 141 + viewBox="0 0 24 24" 142 + fill="none" 143 + stroke="currentColor" 144 + strokeWidth="2" 145 + strokeLinecap="round" 146 + strokeLinejoin="round" 147 + aria-hidden="true" 148 + className="shrink-0" 149 + > 150 + <circle cx="18" cy="5" r="3" /> 151 + <circle cx="6" cy="12" r="3" /> 152 + <circle cx="18" cy="19" r="3" /> 153 + <line x1="8.59" y1="13.51" x2="15.42" y2="17.49" /> 154 + <line x1="15.41" y1="6.51" x2="8.59" y2="10.49" /> 155 + </svg> 156 + share 157 + </> 158 + )} 159 + </button> 97 160 </div> 98 161 </div> 99 162 </div>
+7 -23
packages/site/src/components/OutputPanel.tsx
··· 4 4 interface OutputPanelProps { 5 5 output: { 6 6 json: string; 7 - typeInfo: string; 8 7 error: string; 9 8 }; 10 9 } ··· 22 21 setTheme(e.matches ? "vs-dark" : "vs-light"); 23 22 }; 24 23 mediaQuery.addEventListener("change", handleChange); 25 - return () => mediaQuery.removeEventListener("change", handleChange); 24 + return () => { 25 + mediaQuery.removeEventListener("change", handleChange); 26 + }; 26 27 }, []); 27 28 28 29 return ( 29 - <div style={{ flex: 1, display: "flex", flexDirection: "column" }}> 30 - <div 31 - style={{ 32 - padding: "0.75rem 1rem", 33 - backgroundColor: "var(--color-bg-secondary)", 34 - borderBottom: "1px solid var(--color-border)", 35 - fontSize: "0.875rem", 36 - fontWeight: "600", 37 - color: "var(--color-text-secondary)", 38 - }} 39 - > 30 + <div className="flex-1 flex flex-col"> 31 + <div className="py-3 px-4 bg-gray-50 dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 text-sm font-semibold text-gray-500 dark:text-gray-400"> 40 32 Output 41 33 </div> 42 - <div style={{ flex: 1 }}> 34 + <div className="flex-1"> 43 35 {output.error ? ( 44 - <div 45 - style={{ 46 - padding: "1rem", 47 - color: "var(--color-error)", 48 - backgroundColor: "var(--color-error-bg)", 49 - height: "100%", 50 - overflow: "auto", 51 - }} 52 - > 36 + <div className="p-4 text-red-600 dark:text-red-400 bg-red-50 dark:bg-red-950 h-full overflow-auto"> 53 37 <strong>Error:</strong> {output.error} 54 38 </div> 55 39 ) : (
+37 -179
packages/site/src/components/Playground.tsx
··· 1 + /* eslint-disable @typescript-eslint/no-unsafe-assignment */ 2 + /* eslint-disable @typescript-eslint/no-unsafe-call */ 1 3 import { useState, useEffect, useRef } from "react"; 2 4 import { Editor } from "./Editor"; 3 5 import { OutputPanel } from "./OutputPanel"; 4 6 import { lx } from "prototypey"; 5 7 import { useMonaco } from "@monaco-editor/react"; 6 8 import type * as Monaco from "monaco-editor"; 7 - import { parseAsString, useQueryState } from "nuqs"; 8 - import LZString from "lz-string"; 9 + import { useQueryState } from "nuqs"; 9 10 import MonacoEditor from "@monaco-editor/react"; 11 + import { parseAsCompressed } from "../utils/parsers"; 10 12 11 13 let tsWorkerInstance: Monaco.languages.typescript.TypeScriptWorker | null = 12 14 null; 13 15 14 16 export function Playground() { 15 - const [compressedCode, setCompressedCode] = useQueryState( 17 + const [code, setCode] = useQueryState( 16 18 "code", 17 - parseAsString.withDefault(""), 19 + parseAsCompressed.withDefault(DEFAULT_CODE), 18 20 ); 19 - 20 - const initialCode = 21 - compressedCode && compressedCode.trim() !== "" 22 - ? LZString.decompressFromEncodedURIComponent(compressedCode) || 23 - DEFAULT_CODE 24 - : DEFAULT_CODE; 25 - 26 - const [code, setCode] = useState(initialCode); 27 - const [output, setOutput] = useState({ json: "", typeInfo: "", error: "" }); 21 + const [output, setOutput] = useState({ json: "", error: "" }); 28 22 const [editorReady, setEditorReady] = useState(false); 29 23 const [theme, setTheme] = useState<"vs-light" | "vs-dark">( 30 24 window.matchMedia("(prefers-color-scheme: dark)").matches ··· 36 30 useRef<Monaco.languages.typescript.TypeScriptWorker | null>(null); 37 31 38 32 const handleCodeChange = (newCode: string) => { 39 - setCode(newCode); 40 - // Compress and update URL 41 - const compressed = LZString.compressToEncodedURIComponent(newCode); 42 - setCompressedCode(compressed); 33 + void setCode(newCode); 43 34 }; 44 35 45 36 const handleEditorReady = () => { ··· 52 43 setTheme(e.matches ? "vs-dark" : "vs-light"); 53 44 }; 54 45 mediaQuery.addEventListener("change", handleChange); 55 - return () => mediaQuery.removeEventListener("change", handleChange); 46 + return () => { 47 + mediaQuery.removeEventListener("change", handleChange); 48 + }; 56 49 }, []); 57 50 58 51 useEffect(() => { ··· 70 63 console.error("Failed to initialize TypeScript worker:", err); 71 64 } 72 65 }; 73 - initWorker(); 66 + void initWorker(); 74 67 } 75 68 }, [monaco, editorReady]); 76 69 77 70 useEffect(() => { 78 - const timeoutId = setTimeout(async () => { 71 + const timeoutId = setTimeout(() => { 79 72 try { 80 - const nsMatch = code.match( 81 - /const\s+lex\s*=\s*lx\.lexicon\([^]*?\}\s*\);/, 73 + const nsMatch = /const\s+lex\s*=\s*lx\.lexicon\([^]*?\}\s*\);/.exec( 74 + code, 82 75 ); 83 76 if (!nsMatch) { 84 77 throw new Error("No lexicon definition found"); ··· 86 79 87 80 const cleanedCode = nsMatch[0]; 88 81 const wrappedCode = `${cleanedCode}\nreturn lex;`; 82 + // eslint-disable-next-line @typescript-eslint/no-implied-eval 89 83 const fn = new Function("lx", wrappedCode); 90 - const result = fn(lx); 91 - let typeInfo = "// Hover over .infer in the editor to see the type"; 92 84 93 - if (monaco && tsWorkerRef.current) { 94 - try { 95 - const uri = monaco.Uri.parse("file:///main.ts"); 96 - const existingModel = monaco.editor.getModel(uri); 97 - 98 - if (existingModel) { 99 - const inferPosition = code.indexOf(`ns.infer`); 100 - if (inferPosition !== -1) { 101 - const offset = inferPosition + `ns.infer`.length - 1; 102 - 103 - const quickInfo = 104 - await tsWorkerRef.current.getQuickInfoAtPosition( 105 - uri.toString(), 106 - offset, 107 - ); 108 - 109 - if (quickInfo?.displayParts) { 110 - const typeText = quickInfo.displayParts 111 - .map((part: { text: string }) => part.text) 112 - .join(""); 113 - 114 - const propertyMatch = typeText.match( 115 - /\(property\)\s+.*?\.infer:\s*([\s\S]+?)$/, 116 - ); 117 - if (propertyMatch) { 118 - typeInfo = formatTypeString(propertyMatch[1]); 119 - } 120 - } 121 - } 122 - } 123 - } catch (err) { 124 - console.error("Type extraction error:", err); 125 - } 126 - } 85 + const result = fn(lx); 127 86 128 87 if (result && typeof result === "object" && "json" in result) { 129 88 const jsonOutput = (result as { json: unknown }).json; 130 89 setOutput({ 131 - json: JSON.stringify(jsonOutput, null, 2), 132 - typeInfo, 90 + json: JSON.stringify(jsonOutput, null, 2) + "\n", 133 91 error: "", 134 92 }); 135 93 } else { 136 94 setOutput({ 137 95 json: JSON.stringify(result, null, 2), 138 - typeInfo, 139 96 error: "", 140 97 }); 141 98 } 142 99 } catch (error) { 143 100 setOutput({ 144 101 json: "", 145 - typeInfo: "", 146 102 error: error instanceof Error ? error.message : "Unknown error", 147 103 }); 148 104 } 149 105 }, 500); 150 106 151 - return () => clearTimeout(timeoutId); 107 + return () => { 108 + clearTimeout(timeoutId); 109 + }; 152 110 }, [code, monaco]); 153 111 154 112 return ( 155 113 <> 156 114 {/* Desktop playground */} 157 - <div 158 - className="desktop-only" 159 - style={{ 160 - flex: 1, 161 - overflow: "hidden", 162 - }} 163 - > 164 - <div 165 - style={{ 166 - flex: 1, 167 - display: "flex", 168 - borderRight: "1px solid var(--color-border)", 169 - }} 170 - > 115 + <div className="hidden md:flex flex-1 overflow-hidden"> 116 + <div className="flex-1 flex border-r border-gray-200 dark:border-gray-700"> 171 117 <Editor 172 118 value={code} 173 119 onChange={handleCodeChange} 174 120 onReady={handleEditorReady} 175 121 /> 176 122 </div> 177 - <div style={{ flex: 1, display: "flex" }}> 123 + <div className="flex-1 flex"> 178 124 <OutputPanel output={output} /> 179 125 </div> 180 126 </div> 181 127 182 128 {/* Mobile static demo */} 183 - <div 184 - className="mobile-only" 185 - style={{ 186 - flex: 1, 187 - flexDirection: "column", 188 - overflow: "auto", 189 - padding: "1rem", 190 - }} 191 - > 192 - <div 193 - style={{ 194 - backgroundColor: "var(--color-bg-secondary)", 195 - padding: "1rem", 196 - borderRadius: "0.5rem", 197 - marginBottom: "1rem", 198 - textAlign: "center", 199 - color: "var(--color-text-secondary)", 200 - fontSize: "0.875rem", 201 - }} 202 - > 129 + <div className="flex md:hidden flex-1 flex-col overflow-auto p-4"> 130 + <div className="bg-gray-50 dark:bg-gray-800 p-4 rounded-lg mb-4 text-center text-gray-500 dark:text-gray-400 text-sm"> 203 131 Playground available on desktop 204 132 </div> 205 133 ··· 209 137 ); 210 138 } 211 139 212 - function formatTypeString(typeStr: string): string { 213 - let formatted = typeStr.trim(); 214 - 215 - formatted = formatted.replace(/\s+/g, " "); 216 - formatted = formatted.replace(/;\s*/g, "\n"); 217 - formatted = formatted.replace(/{\s*/g, "{\n"); 218 - formatted = formatted.replace(/\s*}/g, "\n}"); 219 - 220 - const lines = formatted.split("\n"); 221 - let indentLevel = 0; 222 - const indentedLines: string[] = []; 223 - 224 - for (const line of lines) { 225 - const trimmed = line.trim(); 226 - if (!trimmed) continue; 227 - 228 - if (trimmed.startsWith("}")) { 229 - indentLevel = Math.max(0, indentLevel - 1); 230 - } 231 - 232 - indentedLines.push(" ".repeat(indentLevel) + trimmed); 233 - 234 - if (trimmed.endsWith("{") && !trimmed.includes("}")) { 235 - indentLevel++; 236 - } 237 - } 238 - 239 - return indentedLines.join("\n"); 240 - } 241 - 242 140 function MobileStaticDemo({ 243 141 code, 244 142 json, ··· 249 147 theme: "vs-light" | "vs-dark"; 250 148 }) { 251 149 // Calculate line counts to size editors appropriately 252 - const codeLines = code.split("\n").length; 253 - const jsonLines = json.split("\n").length; 254 - 255 150 const estimateWrappedLines = (text: string, maxCharsPerLine: number) => { 256 151 return text.split("\n").reduce((total, line) => { 257 152 const wrappedLines = Math.ceil( ··· 265 160 const jsonWrappedLines = estimateWrappedLines(json, 50); 266 161 267 162 return ( 268 - <div style={{ display: "flex", flexDirection: "column", gap: "1rem" }}> 163 + <div className="flex flex-col gap-4"> 269 164 {/* You write section */} 270 - <div style={{ display: "flex", flexDirection: "column" }}> 271 - <div 272 - style={{ 273 - padding: "0.75rem 1rem", 274 - backgroundColor: "var(--color-bg-secondary)", 275 - borderBottom: "1px solid var(--color-border)", 276 - fontSize: "0.875rem", 277 - fontWeight: "600", 278 - color: "var(--color-text-secondary)", 279 - borderTopLeftRadius: "0.5rem", 280 - borderTopRightRadius: "0.5rem", 281 - }} 282 - > 165 + <div className="flex flex-col"> 166 + <div className="py-3 px-4 bg-gray-50 dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 text-sm font-semibold text-gray-500 dark:text-gray-400 rounded-t-lg"> 283 167 You write 284 168 </div> 285 - <div 286 - style={{ 287 - border: "1px solid var(--color-border)", 288 - borderTop: "none", 289 - borderBottomLeftRadius: "0.5rem", 290 - borderBottomRightRadius: "0.5rem", 291 - overflow: "hidden", 292 - }} 293 - > 169 + <div className="border border-gray-200 dark:border-gray-700 border-t-0 rounded-b-lg overflow-hidden"> 294 170 <MonacoEditor 295 - height={`${codeWrappedLines * 18 + 32}px`} 171 + height={`${String(codeWrappedLines * 18 + 32)}px`} 296 172 defaultLanguage="typescript" 297 173 value={code} 298 174 theme={theme} ··· 319 195 </div> 320 196 321 197 {/* JSON generated section */} 322 - <div style={{ display: "flex", flexDirection: "column" }}> 323 - <div 324 - style={{ 325 - padding: "0.75rem 1rem", 326 - backgroundColor: "var(--color-bg-secondary)", 327 - borderBottom: "1px solid var(--color-border)", 328 - fontSize: "0.875rem", 329 - fontWeight: "600", 330 - color: "var(--color-text-secondary)", 331 - borderTopLeftRadius: "0.5rem", 332 - borderTopRightRadius: "0.5rem", 333 - }} 334 - > 198 + <div className="flex flex-col"> 199 + <div className="py-3 px-4 bg-gray-50 dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 text-sm font-semibold text-gray-500 dark:text-gray-400 rounded-t-lg"> 335 200 JSON generated 336 201 </div> 337 - <div 338 - style={{ 339 - border: "1px solid var(--color-border)", 340 - borderTop: "none", 341 - borderBottomLeftRadius: "0.5rem", 342 - borderBottomRightRadius: "0.5rem", 343 - overflow: "hidden", 344 - }} 345 - > 202 + <div className="border border-gray-200 dark:border-gray-700 border-t-0 rounded-b-lg overflow-hidden"> 346 203 <MonacoEditor 347 - height={`${jsonWrappedLines * 18 + 32}px`} 204 + height={`${String(jsonWrappedLines * 18 + 32)}px`} 348 205 defaultLanguage="json" 349 206 value={json} 350 207 theme={theme} ··· 390 247 const aProfile: Profile = { 391 248 $type: "app.bsky.actor.profile", 392 249 displayName: "Benny Harvey" 393 - }`; 250 + } 251 + `;
+2 -81
packages/site/src/index.css
··· 1 - * { 2 - box-sizing: border-box; 3 - margin: 0; 4 - padding: 0; 5 - } 1 + @import "tailwindcss"; 6 2 7 3 :root { 8 4 font-family: 9 5 -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", 10 6 "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; 11 - line-height: 1.5; 12 - font-weight: 400; 13 7 font-synthesis: none; 14 8 text-rendering: optimizeLegibility; 15 9 -webkit-font-smoothing: antialiased; 16 10 -moz-osx-font-smoothing: grayscale; 17 - 18 - --color-text: #213547; 19 - --color-text-secondary: #6b7280; 20 - --color-text-heading: #111827; 21 - --color-bg: #ffffff; 22 - --color-bg-secondary: #f9fafb; 23 - --color-border: #e5e7eb; 24 - --color-error: #dc2626; 25 - --color-error-bg: #fef2f2; 26 - 27 - color: var(--color-text); 28 - background-color: var(--color-bg); 29 - } 30 - 31 - @media (prefers-color-scheme: dark) { 32 - :root { 33 - --color-text: #e5e7eb; 34 - --color-text-secondary: #9ca3af; 35 - --color-text-heading: #f9fafb; 36 - --color-bg: #111827; 37 - --color-bg-secondary: #1f2937; 38 - --color-border: #374151; 39 - --color-error: #f87171; 40 - --color-error-bg: #3f1f1f; 41 - } 42 11 } 43 12 44 13 body { 45 - margin: 0; 46 - display: flex; 47 14 min-width: 320px; 48 15 min-height: 100vh; 49 16 } 50 17 51 18 #root { 52 - width: 100%; 53 - display: flex; 54 - flex-direction: column; 55 - } 56 - 57 - /* Desktop layout - default */ 58 - .header-content { 59 - display: flex; 60 - justify-content: space-between; 61 - align-items: flex-start; 62 - } 63 - 64 - .header-links { 65 - display: flex; 66 - gap: 1.25rem; 67 - padding-top: 0.5rem; 68 - } 69 - 70 - .mobile-only { 71 - display: none; 72 - } 73 - 74 - .desktop-only { 75 - display: flex; 76 - } 77 - 78 - /* Mobile layout */ 79 - @media (max-width: 768px) { 80 - .header-content { 81 - flex-direction: column; 82 - gap: 1rem; 83 - } 84 - 85 - .header-links { 86 - padding-top: 0; 87 - width: 100%; 88 - justify-content: flex-start; 89 - } 90 - 91 - .mobile-only { 92 - display: flex !important; 93 - flex-direction: column; 94 - } 95 - 96 - .desktop-only { 97 - display: none !important; 98 - } 19 + isolation: isolate; 99 20 }
+1
packages/site/src/main.tsx
··· 4 4 import "./index.css"; 5 5 import { App } from "./App.tsx"; 6 6 7 + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 7 8 createRoot(document.getElementById("root")!).render( 8 9 <StrictMode> 9 10 <NuqsAdapter>
+31
packages/site/src/utils/parsers.ts
··· 1 + import { createParser } from "nuqs"; 2 + import LZString from "lz-string"; 3 + 4 + /** 5 + * Custom nuqs parser for LZ-string compressed values 6 + * 7 + * This parser automatically compresses values when serializing to URL 8 + * and decompresses when parsing from URL, keeping URLs shorter for large data. 9 + */ 10 + export const parseAsCompressed = createParser({ 11 + parse(query: string): string | null { 12 + if (!query || query.trim() === "") { 13 + return null; 14 + } 15 + 16 + // Decompress the value from the URL 17 + const decompressed = LZString.decompressFromEncodedURIComponent(query); 18 + 19 + // Return null if decompression fails 20 + return decompressed || null; 21 + }, 22 + 23 + serialize(value: string): string { 24 + // Compress the value for the URL 25 + return LZString.compressToEncodedURIComponent(value); 26 + }, 27 + 28 + eq(a: string, b: string): boolean { 29 + return a === b; 30 + }, 31 + });
+1 -1
packages/site/tests/components/Header.test.tsx
··· 12 12 it("renders the description", () => { 13 13 render(<Header />); 14 14 expect( 15 - screen.getByText("Type-safe lexicon inference for ATProto schemas"), 15 + screen.getByText("ATProto lexicon typescript toolkit"), 16 16 ).toBeInTheDocument(); 17 17 }); 18 18 });
+2
packages/site/vite.config.ts
··· 1 1 import { defineConfig } from "vite"; 2 2 import react from "@vitejs/plugin-react"; 3 + import tailwindcss from "@tailwindcss/vite"; 3 4 4 5 export default defineConfig({ 5 6 plugins: [ 7 + tailwindcss(), 6 8 react({ 7 9 babel: { 8 10 plugins: [["babel-plugin-react-compiler", {}]],
+1647 -285
pnpm-lock.yaml
··· 11 11 12 12 .: 13 13 devDependencies: 14 + '@changesets/cli': 15 + specifier: ^2.29.8 16 + version: 2.29.8(@types/node@24.0.4) 14 17 '@eslint/js': 15 18 specifier: 9.29.0 16 19 version: 9.29.0 17 20 eslint: 18 21 specifier: 9.29.0 19 22 version: 9.29.0(jiti@2.6.1) 23 + knip: 24 + specifier: ^5.83.1 25 + version: 5.83.1(@types/node@24.0.4)(typescript@5.9.3) 20 26 prettier: 21 27 specifier: 3.6.1 22 28 version: 3.6.1 23 29 typescript-eslint: 24 30 specifier: 8.35.0 25 - version: 8.35.0(eslint@9.29.0(jiti@2.6.1))(typescript@5.8.3) 31 + version: 8.35.0(eslint@9.29.0(jiti@2.6.1))(typescript@5.9.3) 26 32 27 - packages/cli: 33 + packages/prototypey: 28 34 dependencies: 29 - prototypey: 30 - specifier: workspace:* 31 - version: link:../prototypey 35 + '@atproto/lexicon': 36 + specifier: ^0.5.2 37 + version: 0.5.2 32 38 sade: 33 39 specifier: ^1.8.1 34 40 version: 1.8.1 35 41 tinyglobby: 36 42 specifier: ^0.2.15 37 43 version: 0.2.15 38 - devDependencies: 39 - '@types/node': 40 - specifier: 24.0.4 41 - version: 24.0.4 42 - tsdown: 43 - specifier: 0.12.7 44 - version: 0.12.7(typescript@5.8.3) 45 - typescript: 46 - specifier: 5.8.3 47 - version: 5.8.3 48 - vitest: 49 - specifier: ^3.2.4 50 - version: 3.2.4(@types/node@24.0.4)(esbuild@0.25.10)(jiti@2.6.1)(jsdom@25.0.1) 51 - 52 - packages/prototypey: 53 44 devDependencies: 54 45 '@ark/attest': 55 46 specifier: ^0.49.0 56 - version: 0.49.0(typescript@5.8.3) 47 + version: 0.49.0(typescript@5.9.3) 57 48 '@types/node': 58 49 specifier: 24.0.4 59 50 version: 24.0.4 60 51 tsdown: 61 - specifier: 0.12.7 62 - version: 0.12.7(typescript@5.8.3) 52 + specifier: ^0.15.12 53 + version: 0.15.12(ms@2.1.3)(oxc-resolver@11.15.0)(typescript@5.9.3) 63 54 typescript: 64 - specifier: 5.8.3 65 - version: 5.8.3 55 + specifier: 5.9.3 56 + version: 5.9.3 66 57 vitest: 67 58 specifier: ^3.2.4 68 59 version: 3.2.4(@types/node@24.0.4)(esbuild@0.25.10)(jiti@2.6.1)(jsdom@25.0.1) ··· 70 61 packages/site: 71 62 dependencies: 72 63 '@monaco-editor/react': 73 - specifier: ^4.6.0 74 - version: 4.7.0(monaco-editor@0.52.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) 64 + specifier: ^4.7.0 65 + version: 4.7.0(monaco-editor@0.52.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) 75 66 lz-string: 76 67 specifier: ^1.5.0 77 68 version: 1.5.0 ··· 79 70 specifier: 0.52.0 80 71 version: 0.52.0 81 72 nuqs: 82 - specifier: ^2.7.2 83 - version: 2.7.2(react@19.2.0) 73 + specifier: ^2.8.8 74 + version: 2.8.8(react@19.2.4) 84 75 prototypey: 85 76 specifier: workspace:* 86 77 version: link:../prototypey 87 78 react: 88 - specifier: ^19.2.0 89 - version: 19.2.0 79 + specifier: ^19.2.4 80 + version: 19.2.4 90 81 react-dom: 91 - specifier: ^19.2.0 92 - version: 19.2.0(react@19.2.0) 82 + specifier: ^19.2.4 83 + version: 19.2.4(react@19.2.4) 93 84 devDependencies: 85 + '@tailwindcss/vite': 86 + specifier: ^4.1.18 87 + version: 4.1.18(rolldown-vite@7.0.6(@types/node@24.0.4)(esbuild@0.25.10)(jiti@2.6.1)) 94 88 '@testing-library/jest-dom': 95 89 specifier: ^6.9.1 96 90 version: 6.9.1 97 91 '@testing-library/react': 98 - specifier: ^16.1.0 99 - version: 16.3.0(@testing-library/dom@10.4.1)(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) 92 + specifier: ^16.3.2 93 + version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) 100 94 '@testing-library/user-event': 101 - specifier: ^14.5.2 95 + specifier: ^14.6.1 102 96 version: 14.6.1(@testing-library/dom@10.4.1) 103 97 '@types/react': 104 - specifier: ^19.2.2 105 - version: 19.2.2 98 + specifier: ^19.2.13 99 + version: 19.2.13 106 100 '@types/react-dom': 107 - specifier: ^19.2.2 108 - version: 19.2.2(@types/react@19.2.2) 101 + specifier: ^19.2.3 102 + version: 19.2.3(@types/react@19.2.13) 109 103 '@vitejs/plugin-react': 110 - specifier: ^5.0.4 111 - version: 5.0.4(rolldown-vite@7.0.6(@types/node@24.0.4)(esbuild@0.25.10)(jiti@2.6.1)) 104 + specifier: ^5.1.3 105 + version: 5.1.3(rolldown-vite@7.0.6(@types/node@24.0.4)(esbuild@0.25.10)(jiti@2.6.1)) 112 106 babel-plugin-react-compiler: 113 107 specifier: ^1.0.0 114 108 version: 1.0.0 ··· 118 112 jsdom: 119 113 specifier: ^25.0.1 120 114 version: 25.0.1 115 + tailwindcss: 116 + specifier: ^4.1.18 117 + version: 4.1.18 121 118 typescript: 122 - specifier: 5.8.3 123 - version: 5.8.3 119 + specifier: 5.9.3 120 + version: 5.9.3 124 121 vite: 125 122 specifier: npm:rolldown-vite@7.0.6 126 123 version: rolldown-vite@7.0.6(@types/node@24.0.4)(esbuild@0.25.10)(jiti@2.6.1) ··· 151 148 '@asamuzakjp/css-color@3.2.0': 152 149 resolution: {integrity: sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==} 153 150 151 + '@atproto/common-web@0.4.5': 152 + resolution: {integrity: sha512-Tx0xUafLm3vRvOQpbBl5eb9V8xlC7TaRXs6dAulHRkDG3Kb+P9qn3pkDteq+aeMshbVXbVa1rm3Ok4vFyuoyYA==} 153 + 154 + '@atproto/lex-data@0.0.1': 155 + resolution: {integrity: sha512-DrS/8cQcQs3s5t9ELAFNtyDZ8/PdiCx47ALtFEP2GnX2uCBHZRkqWG7xmu6ehjc787nsFzZBvlnz3T/gov5fGA==} 156 + 157 + '@atproto/lex-json@0.0.1': 158 + resolution: {integrity: sha512-ivcF7+pDRuD/P97IEKQ/9TruunXj0w58Khvwk3M6psaI5eZT6LRsRZ4cWcKaXiFX4SHnjy+x43g0f7pPtIsERg==} 159 + 160 + '@atproto/lexicon@0.5.2': 161 + resolution: {integrity: sha512-lRmJgMA8f5j7VB5Iu5cp188ald5FuI4FlmZ7nn6EBrk1dgOstWVrI5Ft6K3z2vjyLZRG6nzknlsw+tDP63p7bQ==} 162 + 163 + '@atproto/syntax@0.4.1': 164 + resolution: {integrity: sha512-CJdImtLAiFO+0z3BWTtxwk6aY5w4t8orHTMVJgkf++QRJWTxPbIFko/0hrkADB7n2EruDxDSeAgfUGehpH6ngw==} 165 + 154 166 '@babel/code-frame@7.27.1': 155 167 resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} 156 168 engines: {node: '>=6.9.0'} 157 169 170 + '@babel/code-frame@7.29.0': 171 + resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} 172 + engines: {node: '>=6.9.0'} 173 + 158 174 '@babel/compat-data@7.28.4': 159 175 resolution: {integrity: sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw==} 160 176 engines: {node: '>=6.9.0'} 161 177 178 + '@babel/compat-data@7.29.0': 179 + resolution: {integrity: sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==} 180 + engines: {node: '>=6.9.0'} 181 + 162 182 '@babel/core@7.28.4': 163 183 resolution: {integrity: sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==} 164 184 engines: {node: '>=6.9.0'} 165 185 186 + '@babel/core@7.29.0': 187 + resolution: {integrity: sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==} 188 + engines: {node: '>=6.9.0'} 189 + 166 190 '@babel/generator@7.28.3': 167 191 resolution: {integrity: sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==} 168 192 engines: {node: '>=6.9.0'} 169 193 194 + '@babel/generator@7.28.5': 195 + resolution: {integrity: sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==} 196 + engines: {node: '>=6.9.0'} 197 + 198 + '@babel/generator@7.29.1': 199 + resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==} 200 + engines: {node: '>=6.9.0'} 201 + 170 202 '@babel/helper-annotate-as-pure@7.27.3': 171 203 resolution: {integrity: sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==} 172 204 engines: {node: '>=6.9.0'} ··· 175 207 resolution: {integrity: sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==} 176 208 engines: {node: '>=6.9.0'} 177 209 210 + '@babel/helper-compilation-targets@7.28.6': 211 + resolution: {integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==} 212 + engines: {node: '>=6.9.0'} 213 + 178 214 '@babel/helper-create-class-features-plugin@7.28.3': 179 215 resolution: {integrity: sha512-V9f6ZFIYSLNEbuGA/92uOvYsGCJNsuA8ESZ4ldc09bWk/j8H8TKiPw8Mk1eG6olpnO0ALHJmYfZvF4MEE4gajg==} 180 216 engines: {node: '>=6.9.0'} ··· 193 229 resolution: {integrity: sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==} 194 230 engines: {node: '>=6.9.0'} 195 231 232 + '@babel/helper-module-imports@7.28.6': 233 + resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==} 234 + engines: {node: '>=6.9.0'} 235 + 196 236 '@babel/helper-module-transforms@7.28.3': 197 237 resolution: {integrity: sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==} 198 238 engines: {node: '>=6.9.0'} 199 239 peerDependencies: 200 240 '@babel/core': ^7.0.0 201 241 242 + '@babel/helper-module-transforms@7.28.6': 243 + resolution: {integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==} 244 + engines: {node: '>=6.9.0'} 245 + peerDependencies: 246 + '@babel/core': ^7.0.0 247 + 202 248 '@babel/helper-optimise-call-expression@7.27.1': 203 249 resolution: {integrity: sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==} 204 250 engines: {node: '>=6.9.0'} ··· 225 271 resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==} 226 272 engines: {node: '>=6.9.0'} 227 273 274 + '@babel/helper-validator-identifier@7.28.5': 275 + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} 276 + engines: {node: '>=6.9.0'} 277 + 228 278 '@babel/helper-validator-option@7.27.1': 229 279 resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} 230 280 engines: {node: '>=6.9.0'} ··· 233 283 resolution: {integrity: sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==} 234 284 engines: {node: '>=6.9.0'} 235 285 286 + '@babel/helpers@7.28.6': 287 + resolution: {integrity: sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==} 288 + engines: {node: '>=6.9.0'} 289 + 236 290 '@babel/parser@7.28.4': 237 291 resolution: {integrity: sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==} 292 + engines: {node: '>=6.0.0'} 293 + hasBin: true 294 + 295 + '@babel/parser@7.28.5': 296 + resolution: {integrity: sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==} 297 + engines: {node: '>=6.0.0'} 298 + hasBin: true 299 + 300 + '@babel/parser@7.29.0': 301 + resolution: {integrity: sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==} 238 302 engines: {node: '>=6.0.0'} 239 303 hasBin: true 240 304 ··· 265 329 resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} 266 330 engines: {node: '>=6.9.0'} 267 331 332 + '@babel/template@7.28.6': 333 + resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} 334 + engines: {node: '>=6.9.0'} 335 + 268 336 '@babel/traverse@7.28.4': 269 337 resolution: {integrity: sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==} 338 + engines: {node: '>=6.9.0'} 339 + 340 + '@babel/traverse@7.29.0': 341 + resolution: {integrity: sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==} 270 342 engines: {node: '>=6.9.0'} 271 343 272 344 '@babel/types@7.28.4': 273 345 resolution: {integrity: sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==} 274 346 engines: {node: '>=6.9.0'} 275 347 348 + '@babel/types@7.28.5': 349 + resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==} 350 + engines: {node: '>=6.9.0'} 351 + 352 + '@babel/types@7.29.0': 353 + resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} 354 + engines: {node: '>=6.9.0'} 355 + 356 + '@changesets/apply-release-plan@7.0.14': 357 + resolution: {integrity: sha512-ddBvf9PHdy2YY0OUiEl3TV78mH9sckndJR14QAt87KLEbIov81XO0q0QAmvooBxXlqRRP8I9B7XOzZwQG7JkWA==} 358 + 359 + '@changesets/assemble-release-plan@6.0.9': 360 + resolution: {integrity: sha512-tPgeeqCHIwNo8sypKlS3gOPmsS3wP0zHt67JDuL20P4QcXiw/O4Hl7oXiuLnP9yg+rXLQ2sScdV1Kkzde61iSQ==} 361 + 362 + '@changesets/changelog-git@0.2.1': 363 + resolution: {integrity: sha512-x/xEleCFLH28c3bQeQIyeZf8lFXyDFVn1SgcBiR2Tw/r4IAWlk1fzxCEZ6NxQAjF2Nwtczoen3OA2qR+UawQ8Q==} 364 + 365 + '@changesets/cli@2.29.8': 366 + resolution: {integrity: sha512-1weuGZpP63YWUYjay/E84qqwcnt5yJMM0tep10Up7Q5cS/DGe2IZ0Uj3HNMxGhCINZuR7aO9WBMdKnPit5ZDPA==} 367 + hasBin: true 368 + 369 + '@changesets/config@3.1.2': 370 + resolution: {integrity: sha512-CYiRhA4bWKemdYi/uwImjPxqWNpqGPNbEBdX1BdONALFIDK7MCUj6FPkzD+z9gJcvDFUQJn9aDVf4UG7OT6Kog==} 371 + 372 + '@changesets/errors@0.2.0': 373 + resolution: {integrity: sha512-6BLOQUscTpZeGljvyQXlWOItQyU71kCdGz7Pi8H8zdw6BI0g3m43iL4xKUVPWtG+qrrL9DTjpdn8eYuCQSRpow==} 374 + 375 + '@changesets/get-dependents-graph@2.1.3': 376 + resolution: {integrity: sha512-gphr+v0mv2I3Oxt19VdWRRUxq3sseyUpX9DaHpTUmLj92Y10AGy+XOtV+kbM6L/fDcpx7/ISDFK6T8A/P3lOdQ==} 377 + 378 + '@changesets/get-release-plan@4.0.14': 379 + resolution: {integrity: sha512-yjZMHpUHgl4Xl5gRlolVuxDkm4HgSJqT93Ri1Uz8kGrQb+5iJ8dkXJ20M2j/Y4iV5QzS2c5SeTxVSKX+2eMI0g==} 380 + 381 + '@changesets/get-version-range-type@0.4.0': 382 + resolution: {integrity: sha512-hwawtob9DryoGTpixy1D3ZXbGgJu1Rhr+ySH2PvTLHvkZuQ7sRT4oQwMh0hbqZH1weAooedEjRsbrWcGLCeyVQ==} 383 + 384 + '@changesets/git@3.0.4': 385 + resolution: {integrity: sha512-BXANzRFkX+XcC1q/d27NKvlJ1yf7PSAgi8JG6dt8EfbHFHi4neau7mufcSca5zRhwOL8j9s6EqsxmT+s+/E6Sw==} 386 + 387 + '@changesets/logger@0.1.1': 388 + resolution: {integrity: sha512-OQtR36ZlnuTxKqoW4Sv6x5YIhOmClRd5pWsjZsddYxpWs517R0HkyiefQPIytCVh4ZcC5x9XaG8KTdd5iRQUfg==} 389 + 390 + '@changesets/parse@0.4.2': 391 + resolution: {integrity: sha512-Uo5MC5mfg4OM0jU3up66fmSn6/NE9INK+8/Vn/7sMVcdWg46zfbvvUSjD9EMonVqPi9fbrJH9SXHn48Tr1f2yA==} 392 + 393 + '@changesets/pre@2.0.2': 394 + resolution: {integrity: sha512-HaL/gEyFVvkf9KFg6484wR9s0qjAXlZ8qWPDkTyKF6+zqjBe/I2mygg3MbpZ++hdi0ToqNUF8cjj7fBy0dg8Ug==} 395 + 396 + '@changesets/read@0.6.6': 397 + resolution: {integrity: sha512-P5QaN9hJSQQKJShzzpBT13FzOSPyHbqdoIBUd2DJdgvnECCyO6LmAOWSV+O8se2TaZJVwSXjL+v9yhb+a9JeJg==} 398 + 399 + '@changesets/should-skip-package@0.1.2': 400 + resolution: {integrity: sha512-qAK/WrqWLNCP22UDdBTMPH5f41elVDlsNyat180A33dWxuUDyNpg6fPi/FyTZwRriVjg0L8gnjJn2F9XAoF0qw==} 401 + 402 + '@changesets/types@4.1.0': 403 + resolution: {integrity: sha512-LDQvVDv5Kb50ny2s25Fhm3d9QSZimsoUGBsUioj6MC3qbMUCuC8GPIvk/M6IvXx3lYhAs0lwWUQLb+VIEUCECw==} 404 + 405 + '@changesets/types@6.1.0': 406 + resolution: {integrity: sha512-rKQcJ+o1nKNgeoYRHKOS07tAMNd3YSN0uHaJOZYjBAgxfV7TUE7JE+z4BzZdQwb5hKaYbayKN5KrYV7ODb2rAA==} 407 + 408 + '@changesets/write@0.4.0': 409 + resolution: {integrity: sha512-CdTLvIOPiCNuH71pyDu3rA+Q0n65cmAbXnwWH84rKGiFumFzkmHNT8KHTMEchcxN+Kl8I54xGUhJ7l3E7X396Q==} 410 + 276 411 '@csstools/color-helpers@5.1.0': 277 412 resolution: {integrity: sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==} 278 413 engines: {node: '>=18'} ··· 304 439 '@emnapi/core@1.5.0': 305 440 resolution: {integrity: sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg==} 306 441 442 + '@emnapi/core@1.7.1': 443 + resolution: {integrity: sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg==} 444 + 307 445 '@emnapi/runtime@1.5.0': 308 446 resolution: {integrity: sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==} 447 + 448 + '@emnapi/runtime@1.7.1': 449 + resolution: {integrity: sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==} 309 450 310 451 '@emnapi/wasi-threads@1.1.0': 311 452 resolution: {integrity: sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==} ··· 524 665 resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} 525 666 engines: {node: '>=18.18'} 526 667 668 + '@inquirer/external-editor@1.0.2': 669 + resolution: {integrity: sha512-yy9cOoBnx58TlsPrIxauKIFQTiyH+0MK4e97y4sV9ERbI+zDxw7i2hxHLCIEGIE/8PPvDxGhgzIOTSOWcs6/MQ==} 670 + engines: {node: '>=18'} 671 + peerDependencies: 672 + '@types/node': '>=18' 673 + peerDependenciesMeta: 674 + '@types/node': 675 + optional: true 676 + 527 677 '@jridgewell/gen-mapping@0.3.13': 528 678 resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} 529 679 ··· 540 690 '@jridgewell/trace-mapping@0.3.31': 541 691 resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} 542 692 693 + '@manypkg/find-root@1.1.0': 694 + resolution: {integrity: sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA==} 695 + 696 + '@manypkg/get-packages@1.1.3': 697 + resolution: {integrity: sha512-fo+QhuU3qE/2TQMQmbVMqaQ6EWbMhi4ABWP+O4AM1NqPBuy0OrApV5LO6BrrgnhtAHS2NH6RrVk9OL181tTi8A==} 698 + 543 699 '@monaco-editor/loader@1.6.1': 544 700 resolution: {integrity: sha512-w3tEnj9HYEC73wtjdpR089AqkUPskFRcdkxsiSFt3SoUc3OHpmu+leP94CXBm4mHfefmhsdfI0ZQu6qJ0wgtPg==} 545 701 ··· 552 708 553 709 '@napi-rs/wasm-runtime@0.2.12': 554 710 resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==} 711 + 712 + '@napi-rs/wasm-runtime@1.0.7': 713 + resolution: {integrity: sha512-SeDnOO0Tk7Okiq6DbXmmBODgOAb9dp9gjlphokTUxmt8U3liIP1ZsozBahH69j/RJv+Rfs6IwUKHTgQYJ/HBAw==} 714 + 715 + '@napi-rs/wasm-runtime@1.1.0': 716 + resolution: {integrity: sha512-Fq6DJW+Bb5jaWE69/qOE0D1TUN9+6uWhCeZpdnSBk14pjLcCWR7Q8n49PTSPHazM37JqrsdpEthXy2xn6jWWiA==} 555 717 556 718 '@nodelib/fs.scandir@2.1.5': 557 719 resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} ··· 565 727 resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} 566 728 engines: {node: '>= 8'} 567 729 568 - '@oxc-project/runtime@0.72.2': 569 - resolution: {integrity: sha512-J2lsPDen2mFs3cOA1gIBd0wsHEhum2vTnuKIRwmj3HJJcIz/XgeNdzvgSOioIXOJgURIpcDaK05jwaDG1rhDwg==} 570 - engines: {node: '>=6.9.0'} 571 - 572 730 '@oxc-project/runtime@0.75.0': 573 731 resolution: {integrity: sha512-gzRmVI/vorsPmbDXt7GD4Uh2lD3rCOku/1xWPB4Yx48k0EP4TZmzQudWapjN4+7Vv+rgXr0RqCHQadeaMvdBuw==} 574 732 engines: {node: '>=6.9.0'} ··· 577 735 resolution: {integrity: sha512-UH07DRi7xXqAsJ/sFbJJg0liIXnapB6P5uADXIiF1s6WQjZzcTIkKHca0s522QVxmijPxVX5ijCYxSr7eSq5CQ==} 578 736 engines: {node: '>=6.9.0'} 579 737 580 - '@oxc-project/types@0.72.2': 581 - resolution: {integrity: sha512-il5RF8AP85XC0CMjHF4cnVT9nT/v/ocm6qlZQpSiAR9qBbQMGkFKloBZwm7PcnOdiUX97yHgsKM7uDCCWCu3tg==} 582 - 583 738 '@oxc-project/types@0.75.1': 584 739 resolution: {integrity: sha512-7ZJy+51qWpZRvynaQUezeYfjCtaSdiXIWFUZIlOuTSfDXpXqnSl/m1IUPLx6XrOy6s0SFv3CLE14vcZy63bz7g==} 585 740 741 + '@oxc-project/types@0.95.0': 742 + resolution: {integrity: sha512-vACy7vhpMPhjEJhULNxrdR0D943TkA/MigMpJCHmBHvMXxRStRi/dPtTlfQ3uDwWSzRpT8z+7ImjZVf8JWBocQ==} 743 + 744 + '@oxc-resolver/binding-android-arm-eabi@11.15.0': 745 + resolution: {integrity: sha512-Q+lWuFfq7whNelNJIP1dhXaVz4zO9Tu77GcQHyxDWh3MaCoO2Bisphgzmsh4ZoUe2zIchQh6OvQL99GlWHg9Tw==} 746 + cpu: [arm] 747 + os: [android] 748 + 749 + '@oxc-resolver/binding-android-arm64@11.15.0': 750 + resolution: {integrity: sha512-vbdBttesHR0W1oJaxgWVTboyMUuu+VnPsHXJ6jrXf4czELzB6GIg5DrmlyhAmFBhjwov+yJH/DfTnHS+2sDgOw==} 751 + cpu: [arm64] 752 + os: [android] 753 + 754 + '@oxc-resolver/binding-darwin-arm64@11.15.0': 755 + resolution: {integrity: sha512-R67lsOe1UzNjqVBCwCZX1rlItTsj/cVtBw4Uy19CvTicqEWvwaTn8t34zLD75LQwDDPCY3C8n7NbD+LIdw+ZoA==} 756 + cpu: [arm64] 757 + os: [darwin] 758 + 759 + '@oxc-resolver/binding-darwin-x64@11.15.0': 760 + resolution: {integrity: sha512-77mya5F8WV0EtCxI0MlVZcqkYlaQpfNwl/tZlfg4jRsoLpFbaTeWv75hFm6TE84WULVlJtSgvf7DhoWBxp9+ZQ==} 761 + cpu: [x64] 762 + os: [darwin] 763 + 764 + '@oxc-resolver/binding-freebsd-x64@11.15.0': 765 + resolution: {integrity: sha512-X1Sz7m5PC+6D3KWIDXMUtux+0Imj6HfHGdBStSvgdI60OravzI1t83eyn6eN0LPTrynuPrUgjk7tOnOsBzSWHw==} 766 + cpu: [x64] 767 + os: [freebsd] 768 + 769 + '@oxc-resolver/binding-linux-arm-gnueabihf@11.15.0': 770 + resolution: {integrity: sha512-L1x/wCaIRre+18I4cH/lTqSAymlV0k4HqfSYNNuI9oeL28Ks86lI6O5VfYL6sxxWYgjuWB98gNGo7tq7d4GarQ==} 771 + cpu: [arm] 772 + os: [linux] 773 + 774 + '@oxc-resolver/binding-linux-arm-musleabihf@11.15.0': 775 + resolution: {integrity: sha512-abGXd/zMGa0tH8nKlAXdOnRy4G7jZmkU0J85kMKWns161bxIgGn/j7zxqh3DKEW98wAzzU9GofZMJ0P5YCVPVw==} 776 + cpu: [arm] 777 + os: [linux] 778 + 779 + '@oxc-resolver/binding-linux-arm64-gnu@11.15.0': 780 + resolution: {integrity: sha512-SVjjjtMW66Mza76PBGJLqB0KKyFTBnxmtDXLJPbL6ZPGSctcXVmujz7/WAc0rb9m2oV0cHQTtVjnq6orQnI/jg==} 781 + cpu: [arm64] 782 + os: [linux] 783 + libc: [glibc] 784 + 785 + '@oxc-resolver/binding-linux-arm64-musl@11.15.0': 786 + resolution: {integrity: sha512-JDv2/AycPF2qgzEiDeMJCcSzKNDm3KxNg0KKWipoKEMDFqfM7LxNwwSVyAOGmrYlE4l3dg290hOMsr9xG7jv9g==} 787 + cpu: [arm64] 788 + os: [linux] 789 + libc: [musl] 790 + 791 + '@oxc-resolver/binding-linux-ppc64-gnu@11.15.0': 792 + resolution: {integrity: sha512-zbu9FhvBLW4KJxo7ElFvZWbSt4vP685Qc/Gyk/Ns3g2gR9qh2qWXouH8PWySy+Ko/qJ42+HJCLg+ZNcxikERfg==} 793 + cpu: [ppc64] 794 + os: [linux] 795 + libc: [glibc] 796 + 797 + '@oxc-resolver/binding-linux-riscv64-gnu@11.15.0': 798 + resolution: {integrity: sha512-Kfleehe6B09C2qCnyIU01xLFqFXCHI4ylzkicfX/89j+gNHh9xyNdpEvit88Kq6i5tTGdavVnM6DQfOE2qNtlg==} 799 + cpu: [riscv64] 800 + os: [linux] 801 + libc: [glibc] 802 + 803 + '@oxc-resolver/binding-linux-riscv64-musl@11.15.0': 804 + resolution: {integrity: sha512-J7LPiEt27Tpm8P+qURDwNc8q45+n+mWgyys4/V6r5A8v5gDentHRGUx3iVk5NxdKhgoGulrzQocPTZVosq25Eg==} 805 + cpu: [riscv64] 806 + os: [linux] 807 + libc: [musl] 808 + 809 + '@oxc-resolver/binding-linux-s390x-gnu@11.15.0': 810 + resolution: {integrity: sha512-+8/d2tAScPjVJNyqa7GPGnqleTB/XW9dZJQ2D/oIM3wpH3TG+DaFEXBbk4QFJ9K9AUGBhvQvWU2mQyhK/yYn3Q==} 811 + cpu: [s390x] 812 + os: [linux] 813 + libc: [glibc] 814 + 815 + '@oxc-resolver/binding-linux-x64-gnu@11.15.0': 816 + resolution: {integrity: sha512-xtvSzH7Nr5MCZI2FKImmOdTl9kzuQ51RPyLh451tvD2qnkg3BaqI9Ox78bTk57YJhlXPuxWSOL5aZhKAc9J6qg==} 817 + cpu: [x64] 818 + os: [linux] 819 + libc: [glibc] 820 + 821 + '@oxc-resolver/binding-linux-x64-musl@11.15.0': 822 + resolution: {integrity: sha512-14YL1zuXj06+/tqsuUZuzL0T425WA/I4nSVN1kBXeC5WHxem6lQ+2HGvG+crjeJEqHgZUT62YIgj88W+8E7eyg==} 823 + cpu: [x64] 824 + os: [linux] 825 + libc: [musl] 826 + 827 + '@oxc-resolver/binding-openharmony-arm64@11.15.0': 828 + resolution: {integrity: sha512-/7Qli+1Wk93coxnrQaU8ySlICYN8HsgyIrzqjgIkQEpI//9eUeaeIHZptNl2fMvBGeXa7k2QgLbRNaBRgpnvMw==} 829 + cpu: [arm64] 830 + os: [openharmony] 831 + 832 + '@oxc-resolver/binding-wasm32-wasi@11.15.0': 833 + resolution: {integrity: sha512-q5rn2eIMQLuc/AVGR2rQKb2EVlgreATGG8xXg8f4XbbYCVgpxaq+dgMbiPStyNywW1MH8VU2T09UEm30UtOQvg==} 834 + engines: {node: '>=14.0.0'} 835 + cpu: [wasm32] 836 + 837 + '@oxc-resolver/binding-win32-arm64-msvc@11.15.0': 838 + resolution: {integrity: sha512-yCAh2RWjU/8wWTxQDgGPgzV9QBv0/Ojb5ej1c/58iOjyTuy/J1ZQtYi2SpULjKmwIxLJdTiCHpMilauWimE31w==} 839 + cpu: [arm64] 840 + os: [win32] 841 + 842 + '@oxc-resolver/binding-win32-ia32-msvc@11.15.0': 843 + resolution: {integrity: sha512-lmXKb6lvA6M6QIbtYfgjd+AryJqExZVSY2bfECC18OPu7Lv1mHFF171Mai5l9hG3r4IhHPPIwT10EHoilSCYeA==} 844 + cpu: [ia32] 845 + os: [win32] 846 + 847 + '@oxc-resolver/binding-win32-x64-msvc@11.15.0': 848 + resolution: {integrity: sha512-HZsfne0s/tGOcJK9ZdTGxsNU2P/dH0Shf0jqrPvsC6wX0Wk+6AyhSpHFLQCnLOuFQiHHU0ePfM8iYsoJb5hHpQ==} 849 + cpu: [x64] 850 + os: [win32] 851 + 586 852 '@prettier/sync@0.5.5': 587 853 resolution: {integrity: sha512-6BMtNr7aQhyNcGzmumkL0tgr1YQGfm9d7ZdmRpWqWuqpc9vZBind4xMe5NMiRECOhjuSiWHfBWLBnXkpeE90bw==} 588 854 peerDependencies: ··· 591 857 '@quansync/fs@0.1.5': 592 858 resolution: {integrity: sha512-lNS9hL2aS2NZgNW7BBj+6EBl4rOf8l+tQ0eRY6JWCI8jI2kc53gSoqbjojU0OnAWhzoXiOjFyGsHcDGePB3lhA==} 593 859 594 - '@rolldown/binding-darwin-arm64@1.0.0-beta.11-commit.f051675': 595 - resolution: {integrity: sha512-Hlt/h+lOJ+ksC2wED2M9Hku/9CA2Hr17ENK82gNMmi3OqwcZLdZFqJDpASTli65wIOeT4p9rIUMdkfshCoJpYA==} 860 + '@rolldown/binding-android-arm64@1.0.0-beta.45': 861 + resolution: {integrity: sha512-bfgKYhFiXJALeA/riil908+2vlyWGdwa7Ju5S+JgWZYdR4jtiPOGdM6WLfso1dojCh+4ZWeiTwPeV9IKQEX+4g==} 862 + engines: {node: ^20.19.0 || >=22.12.0} 596 863 cpu: [arm64] 597 - os: [darwin] 864 + os: [android] 598 865 599 866 '@rolldown/binding-darwin-arm64@1.0.0-beta.24': 600 867 resolution: {integrity: sha512-gE4HGjIioZaMGZupq2zQQdqhlRV2b2qnjFHHkJEW50zVDmiVNWwdHjwvZDPx9JfW5y4GuHgp/zKDLZZbJlQ1/Q==} 601 868 cpu: [arm64] 602 869 os: [darwin] 603 870 604 - '@rolldown/binding-darwin-x64@1.0.0-beta.11-commit.f051675': 605 - resolution: {integrity: sha512-Bnst+HBwhW2YrNybEiNf9TJkI1myDgXmiPBVIOS0apzrLCmByzei6PilTClOpTpNFYB+UviL3Ox2gKUmcgUjGw==} 606 - cpu: [x64] 871 + '@rolldown/binding-darwin-arm64@1.0.0-beta.45': 872 + resolution: {integrity: sha512-xjCv4CRVsSnnIxTuyH1RDJl5OEQ1c9JYOwfDAHddjJDxCw46ZX9q80+xq7Eok7KC4bRSZudMJllkvOKv0T9SeA==} 873 + engines: {node: ^20.19.0 || >=22.12.0} 874 + cpu: [arm64] 607 875 os: [darwin] 608 876 609 877 '@rolldown/binding-darwin-x64@1.0.0-beta.24': ··· 611 879 cpu: [x64] 612 880 os: [darwin] 613 881 614 - '@rolldown/binding-freebsd-x64@1.0.0-beta.11-commit.f051675': 615 - resolution: {integrity: sha512-3jAxVmYDPc8vMZZOfZI1aokGB9cP6VNeU9XNCx0UJ6ShlSPK3qkAa0sWgueMhaQkgBVf8MOfGpjo47ohGd7QrA==} 882 + '@rolldown/binding-darwin-x64@1.0.0-beta.45': 883 + resolution: {integrity: sha512-ddcO9TD3D/CLUa/l8GO8LHzBOaZqWg5ClMy3jICoxwCuoz47h9dtqPsIeTiB6yR501LQTeDsjA4lIFd7u3Ljfw==} 884 + engines: {node: ^20.19.0 || >=22.12.0} 616 885 cpu: [x64] 617 - os: [freebsd] 886 + os: [darwin] 618 887 619 888 '@rolldown/binding-freebsd-x64@1.0.0-beta.24': 620 889 resolution: {integrity: sha512-lx3Q2TU2bbY4yDCZ6e+Wiom3VMLFlZmQswx/1CyjFd+Vv3Q+99SZm6CSfNAIZBaWD246yQRRr1Vx+iIoWCdYzQ==} 621 890 cpu: [x64] 622 891 os: [freebsd] 623 892 624 - '@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.11-commit.f051675': 625 - resolution: {integrity: sha512-TpUltUdvcsAf2WvXXD8AVc3BozvhgazJ2gJLXp4DVV2V82m26QelI373Bzx8d/4hB167EEIg4wWW/7GXB/ltoQ==} 626 - cpu: [arm] 627 - os: [linux] 893 + '@rolldown/binding-freebsd-x64@1.0.0-beta.45': 894 + resolution: {integrity: sha512-MBTWdrzW9w+UMYDUvnEuh0pQvLENkl2Sis15fHTfHVW7ClbGuez+RWopZudIDEGkpZXdeI4CkRXk+vdIIebrmg==} 895 + engines: {node: ^20.19.0 || >=22.12.0} 896 + cpu: [x64] 897 + os: [freebsd] 628 898 629 899 '@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.24': 630 900 resolution: {integrity: sha512-PLtsV6uf3uS1/cNF8Wu/kitTpXT2YpOZbN6VJm7oMi5A8o5oO0vh8STCB71O5k2kwQMVycsmxHWFk2ZyEa6aMw==} 631 901 cpu: [arm] 632 902 os: [linux] 633 903 634 - '@rolldown/binding-linux-arm64-gnu@1.0.0-beta.11-commit.f051675': 635 - resolution: {integrity: sha512-eGvHnYQSdbdhsTdjdp/+83LrN81/7X9HD6y3jg7mEmdsicxEMEIt6CsP7tvYS/jn4489jgO/6mLxW/7Vg+B8pw==} 636 - cpu: [arm64] 904 + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.45': 905 + resolution: {integrity: sha512-4YgoCFiki1HR6oSg+GxxfzfnVCesQxLF1LEnw9uXS/MpBmuog0EOO2rYfy69rWP4tFZL9IWp6KEfGZLrZ7aUog==} 906 + engines: {node: ^20.19.0 || >=22.12.0} 907 + cpu: [arm] 637 908 os: [linux] 638 909 639 910 '@rolldown/binding-linux-arm64-gnu@1.0.0-beta.24': 640 911 resolution: {integrity: sha512-UxGukDkWnv7uS5R+BPVeJ4FSuwA+lgC62LRsyPPSJhJhKMNGZ2W9sQPIpEtBRlww8t0qR6QBsiD5TGLW98ktGw==} 641 912 cpu: [arm64] 642 913 os: [linux] 914 + libc: [glibc] 643 915 644 - '@rolldown/binding-linux-arm64-musl@1.0.0-beta.11-commit.f051675': 645 - resolution: {integrity: sha512-0NJZWXJls83FpBRzkTbGBsXXstaQLsfodnyeOghxbnNdsjn+B4dcNPpMK5V3QDsjC0pNjDLaDdzB2jWKlZbP/Q==} 916 + '@rolldown/binding-linux-arm64-gnu@1.0.0-beta.45': 917 + resolution: {integrity: sha512-LE1gjAwQRrbCOorJJ7LFr10s5vqYf5a00V5Ea9wXcT2+56n5YosJkcp8eQ12FxRBv2YX8dsdQJb+ZTtYJwb6XQ==} 918 + engines: {node: ^20.19.0 || >=22.12.0} 646 919 cpu: [arm64] 647 920 os: [linux] 921 + libc: [glibc] 648 922 649 923 '@rolldown/binding-linux-arm64-musl@1.0.0-beta.24': 650 924 resolution: {integrity: sha512-vB99yGYW9FOQe4lk3MNKa13+vRj+7waZFlRE3Ba/IpEy7RFxZ78ASkPLXkz4+kYYbUvMnRaOfk9RKX2fqYZRUg==} 651 925 cpu: [arm64] 652 926 os: [linux] 927 + libc: [musl] 653 928 654 - '@rolldown/binding-linux-x64-gnu@1.0.0-beta.11-commit.f051675': 655 - resolution: {integrity: sha512-9vXnu27r4zgS/BHP6RCLBOrJoV2xxtLYHT68IVpSOdCkBHGpf1oOJt6blv1y5NRRJBEfAFCvj5NmwSMhETF96w==} 656 - cpu: [x64] 929 + '@rolldown/binding-linux-arm64-musl@1.0.0-beta.45': 930 + resolution: {integrity: sha512-tdy8ThO/fPp40B81v0YK3QC+KODOmzJzSUOO37DinQxzlTJ026gqUSOM8tzlVixRbQJltgVDCTYF8HNPRErQTA==} 931 + engines: {node: ^20.19.0 || >=22.12.0} 932 + cpu: [arm64] 657 933 os: [linux] 934 + libc: [musl] 658 935 659 936 '@rolldown/binding-linux-x64-gnu@1.0.0-beta.24': 660 937 resolution: {integrity: sha512-fAMZBWutuKWHsyvHVsKjFYRXVgTbzBfNmomzPPpog8UtdkHk5Vnb0qVEeZP4hR4TsXnKfzD2EQ98NRqFej5QYA==} 661 938 cpu: [x64] 662 939 os: [linux] 940 + libc: [glibc] 663 941 664 - '@rolldown/binding-linux-x64-musl@1.0.0-beta.11-commit.f051675': 665 - resolution: {integrity: sha512-e6tvsZbtHt4kzl82oCajOUxwIN8uMfjhuQ0qxIVRzPekRRjKEzyH9agYPW6toN0cnHpkhPsu51tyZKJOdUl7jg==} 942 + '@rolldown/binding-linux-x64-gnu@1.0.0-beta.45': 943 + resolution: {integrity: sha512-lS082ROBWdmOyVY/0YB3JmsiClaWoxvC+dA8/rbhyB9VLkvVEaihLEOr4CYmrMse151C4+S6hCw6oa1iewox7g==} 944 + engines: {node: ^20.19.0 || >=22.12.0} 666 945 cpu: [x64] 667 946 os: [linux] 947 + libc: [glibc] 668 948 669 949 '@rolldown/binding-linux-x64-musl@1.0.0-beta.24': 670 950 resolution: {integrity: sha512-0UY/Qo8fAlpolcWOg2ZU7SCUrsCJWifdRMliV9GXlZaBKbMoVNFw0pHGDm9cj/3TWhJu/iB0peZK00dm22LlNw==} 671 951 cpu: [x64] 672 952 os: [linux] 953 + libc: [musl] 673 954 674 - '@rolldown/binding-wasm32-wasi@1.0.0-beta.11-commit.f051675': 675 - resolution: {integrity: sha512-nBQVizPoUQiViANhWrOyihXNf2booP2iq3S396bI1tmHftdgUXWKa6yAoleJBgP0oF0idXpTPU82ciaROUcjpg==} 676 - engines: {node: '>=14.21.3'} 677 - cpu: [wasm32] 955 + '@rolldown/binding-linux-x64-musl@1.0.0-beta.45': 956 + resolution: {integrity: sha512-Hi73aYY0cBkr1/SvNQqH8Cd+rSV6S9RB5izCv0ySBcRnd/Wfn5plguUoGYwBnhHgFbh6cPw9m2dUVBR6BG1gxA==} 957 + engines: {node: ^20.19.0 || >=22.12.0} 958 + cpu: [x64] 959 + os: [linux] 960 + libc: [musl] 961 + 962 + '@rolldown/binding-openharmony-arm64@1.0.0-beta.45': 963 + resolution: {integrity: sha512-fljEqbO7RHHogNDxYtTzr+GNjlfOx21RUyGmF+NrkebZ8emYYiIqzPxsaMZuRx0rgZmVmliOzEp86/CQFDKhJQ==} 964 + engines: {node: ^20.19.0 || >=22.12.0} 965 + cpu: [arm64] 966 + os: [openharmony] 678 967 679 968 '@rolldown/binding-wasm32-wasi@1.0.0-beta.24': 680 969 resolution: {integrity: sha512-7ubbtKCo6FBuAM4q6LoT5dOea7f/zj9OYXgumbwSmA0fw18mN5h8SrFTUjl7h9MpPkOyhi2uY6ss4pb39KXkcw==} 681 970 engines: {node: '>=14.21.3'} 682 971 cpu: [wasm32] 683 972 684 - '@rolldown/binding-win32-arm64-msvc@1.0.0-beta.11-commit.f051675': 685 - resolution: {integrity: sha512-Rey/ECXKI/UEykrKfJX3oVAPXDH2k1p2BKzYGza0z3S2X5I3sTDOeBn2I0IQgyyf7U3+DCBhYjkDFnmSePrU/A==} 686 - cpu: [arm64] 687 - os: [win32] 973 + '@rolldown/binding-wasm32-wasi@1.0.0-beta.45': 974 + resolution: {integrity: sha512-ZJDB7lkuZE9XUnWQSYrBObZxczut+8FZ5pdanm8nNS1DAo8zsrPuvGwn+U3fwU98WaiFsNrA4XHngesCGr8tEQ==} 975 + engines: {node: '>=14.0.0'} 976 + cpu: [wasm32] 688 977 689 978 '@rolldown/binding-win32-arm64-msvc@1.0.0-beta.24': 690 979 resolution: {integrity: sha512-S5WKIabtRBJyzu31KnJRlbZRR6FMrTMzYRrNTnIY2hWWXfpcB1PNuHqbo+98ODLpH8knul4Vyf5sCL61okLTjA==} 691 980 cpu: [arm64] 692 981 os: [win32] 693 982 694 - '@rolldown/binding-win32-ia32-msvc@1.0.0-beta.11-commit.f051675': 695 - resolution: {integrity: sha512-LtuMKJe6iFH4iV55dy+gDwZ9v23Tfxx5cd7ZAxvhYFGoVNSvarxAgl844BvFGReERCnLTGRvo85FUR6fDHQX+A==} 696 - cpu: [ia32] 983 + '@rolldown/binding-win32-arm64-msvc@1.0.0-beta.45': 984 + resolution: {integrity: sha512-zyzAjItHPUmxg6Z8SyRhLdXlJn3/D9KL5b9mObUrBHhWS/GwRH4665xCiFqeuktAhhWutqfc+rOV2LjK4VYQGQ==} 985 + engines: {node: ^20.19.0 || >=22.12.0} 986 + cpu: [arm64] 697 987 os: [win32] 698 988 699 989 '@rolldown/binding-win32-ia32-msvc@1.0.0-beta.24': ··· 701 991 cpu: [ia32] 702 992 os: [win32] 703 993 704 - '@rolldown/binding-win32-x64-msvc@1.0.0-beta.11-commit.f051675': 705 - resolution: {integrity: sha512-YY8UYfBm4dbWa4psgEPPD9T9X0nAvlYu0BOsQC5vDfCwzzU7IHT4jAfetvlQq+4+M6qWHSTr6v+/WX5EmlM1WA==} 706 - cpu: [x64] 994 + '@rolldown/binding-win32-ia32-msvc@1.0.0-beta.45': 995 + resolution: {integrity: sha512-wODcGzlfxqS6D7BR0srkJk3drPwXYLu7jPHN27ce2c4PUnVVmJnp9mJzUQGT4LpmHmmVdMZ+P6hKvyTGBzc1CA==} 996 + engines: {node: ^20.19.0 || >=22.12.0} 997 + cpu: [ia32] 707 998 os: [win32] 708 999 709 1000 '@rolldown/binding-win32-x64-msvc@1.0.0-beta.24': ··· 711 1002 cpu: [x64] 712 1003 os: [win32] 713 1004 714 - '@rolldown/pluginutils@1.0.0-beta.11-commit.f051675': 715 - resolution: {integrity: sha512-TAqMYehvpauLKz7v4TZOTUQNjxa5bUQWw2+51/+Zk3ItclBxgoSWhnZ31sXjdoX6le6OXdK2vZfV3KoyW/O/GA==} 1005 + '@rolldown/binding-win32-x64-msvc@1.0.0-beta.45': 1006 + resolution: {integrity: sha512-wiU40G1nQo9rtfvF9jLbl79lUgjfaD/LTyUEw2Wg/gdF5OhjzpKMVugZQngO+RNdwYaNj+Fs+kWBWfp4VXPMHA==} 1007 + engines: {node: ^20.19.0 || >=22.12.0} 1008 + cpu: [x64] 1009 + os: [win32] 716 1010 717 1011 '@rolldown/pluginutils@1.0.0-beta.24': 718 1012 resolution: {integrity: sha512-NMiim/enJlffMP16IanVj1ajFNEg8SaMEYyxyYfJoEyt5EiFT3HUH/T2GRdeStNWp+/kg5U8DiJqnQBgLQ8uCw==} 719 1013 720 - '@rolldown/pluginutils@1.0.0-beta.38': 721 - resolution: {integrity: sha512-N/ICGKleNhA5nc9XXQG/kkKHJ7S55u0x0XUJbbkmdCnFuoRkM1Il12q9q0eX19+M7KKUEPw/daUPIRnxhcxAIw==} 1014 + '@rolldown/pluginutils@1.0.0-beta.45': 1015 + resolution: {integrity: sha512-Le9ulGCrD8ggInzWw/k2J8QcbPz7eGIOWqfJ2L+1R0Opm7n6J37s2hiDWlh6LJN0Lk9L5sUzMvRHKW7UxBZsQA==} 1016 + 1017 + '@rolldown/pluginutils@1.0.0-rc.2': 1018 + resolution: {integrity: sha512-izyXV/v+cHiRfozX62W9htOAvwMo4/bXKDrQ+vom1L1qRuexPock/7VZDAhnpHCLNejd3NJ6hiab+tO0D44Rgw==} 722 1019 723 1020 '@standard-schema/spec@1.0.0': 724 1021 resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==} 725 1022 1023 + '@tailwindcss/node@4.1.18': 1024 + resolution: {integrity: sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ==} 1025 + 1026 + '@tailwindcss/oxide-android-arm64@4.1.18': 1027 + resolution: {integrity: sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q==} 1028 + engines: {node: '>= 10'} 1029 + cpu: [arm64] 1030 + os: [android] 1031 + 1032 + '@tailwindcss/oxide-darwin-arm64@4.1.18': 1033 + resolution: {integrity: sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A==} 1034 + engines: {node: '>= 10'} 1035 + cpu: [arm64] 1036 + os: [darwin] 1037 + 1038 + '@tailwindcss/oxide-darwin-x64@4.1.18': 1039 + resolution: {integrity: sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw==} 1040 + engines: {node: '>= 10'} 1041 + cpu: [x64] 1042 + os: [darwin] 1043 + 1044 + '@tailwindcss/oxide-freebsd-x64@4.1.18': 1045 + resolution: {integrity: sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA==} 1046 + engines: {node: '>= 10'} 1047 + cpu: [x64] 1048 + os: [freebsd] 1049 + 1050 + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.18': 1051 + resolution: {integrity: sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA==} 1052 + engines: {node: '>= 10'} 1053 + cpu: [arm] 1054 + os: [linux] 1055 + 1056 + '@tailwindcss/oxide-linux-arm64-gnu@4.1.18': 1057 + resolution: {integrity: sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw==} 1058 + engines: {node: '>= 10'} 1059 + cpu: [arm64] 1060 + os: [linux] 1061 + libc: [glibc] 1062 + 1063 + '@tailwindcss/oxide-linux-arm64-musl@4.1.18': 1064 + resolution: {integrity: sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==} 1065 + engines: {node: '>= 10'} 1066 + cpu: [arm64] 1067 + os: [linux] 1068 + libc: [musl] 1069 + 1070 + '@tailwindcss/oxide-linux-x64-gnu@4.1.18': 1071 + resolution: {integrity: sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==} 1072 + engines: {node: '>= 10'} 1073 + cpu: [x64] 1074 + os: [linux] 1075 + libc: [glibc] 1076 + 1077 + '@tailwindcss/oxide-linux-x64-musl@4.1.18': 1078 + resolution: {integrity: sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==} 1079 + engines: {node: '>= 10'} 1080 + cpu: [x64] 1081 + os: [linux] 1082 + libc: [musl] 1083 + 1084 + '@tailwindcss/oxide-wasm32-wasi@4.1.18': 1085 + resolution: {integrity: sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==} 1086 + engines: {node: '>=14.0.0'} 1087 + cpu: [wasm32] 1088 + bundledDependencies: 1089 + - '@napi-rs/wasm-runtime' 1090 + - '@emnapi/core' 1091 + - '@emnapi/runtime' 1092 + - '@tybys/wasm-util' 1093 + - '@emnapi/wasi-threads' 1094 + - tslib 1095 + 1096 + '@tailwindcss/oxide-win32-arm64-msvc@4.1.18': 1097 + resolution: {integrity: sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA==} 1098 + engines: {node: '>= 10'} 1099 + cpu: [arm64] 1100 + os: [win32] 1101 + 1102 + '@tailwindcss/oxide-win32-x64-msvc@4.1.18': 1103 + resolution: {integrity: sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q==} 1104 + engines: {node: '>= 10'} 1105 + cpu: [x64] 1106 + os: [win32] 1107 + 1108 + '@tailwindcss/oxide@4.1.18': 1109 + resolution: {integrity: sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A==} 1110 + engines: {node: '>= 10'} 1111 + 1112 + '@tailwindcss/vite@4.1.18': 1113 + resolution: {integrity: sha512-jVA+/UpKL1vRLg6Hkao5jldawNmRo7mQYrZtNHMIVpLfLhDml5nMRUo/8MwoX2vNXvnaXNNMedrMfMugAVX1nA==} 1114 + peerDependencies: 1115 + vite: ^5.2.0 || ^6 || ^7 1116 + 726 1117 '@testing-library/dom@10.4.1': 727 1118 resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==} 728 1119 engines: {node: '>=18'} ··· 731 1122 resolution: {integrity: sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==} 732 1123 engines: {node: '>=14', npm: '>=6', yarn: '>=1'} 733 1124 734 - '@testing-library/react@16.3.0': 735 - resolution: {integrity: sha512-kFSyxiEDwv1WLl2fgsq6pPBbw5aWKrsY2/noi1Id0TK0UParSF62oFQFGHXIyaG4pp2tEub/Zlel+fjjZILDsw==} 1125 + '@testing-library/react@16.3.2': 1126 + resolution: {integrity: sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==} 736 1127 engines: {node: '>=18'} 737 1128 peerDependencies: 738 1129 '@testing-library/dom': ^10.0.0 ··· 782 1173 '@types/json-schema@7.0.15': 783 1174 resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} 784 1175 1176 + '@types/node@12.20.55': 1177 + resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==} 1178 + 785 1179 '@types/node@24.0.4': 786 1180 resolution: {integrity: sha512-ulyqAkrhnuNq9pB76DRBTkcS6YsmDALy6Ua63V8OhrOBgbcYt6IOdzpw5P1+dyRIyMerzLkeYWBeOXPpA9GMAA==} 787 1181 788 - '@types/react-dom@19.2.2': 789 - resolution: {integrity: sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==} 1182 + '@types/react-dom@19.2.3': 1183 + resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} 790 1184 peerDependencies: 791 1185 '@types/react': ^19.2.0 792 1186 793 - '@types/react@19.2.2': 794 - resolution: {integrity: sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==} 1187 + '@types/react@19.2.13': 1188 + resolution: {integrity: sha512-KkiJeU6VbYbUOp5ITMIc7kBfqlYkKA5KhEHVrGMmUUMt7NeaZg65ojdPk+FtNrBAOXNVM5QM72jnADjM+XVRAQ==} 795 1189 796 1190 '@typescript-eslint/eslint-plugin@8.35.0': 797 1191 resolution: {integrity: sha512-ijItUYaiWuce0N1SoSMrEd0b6b6lYkYt99pqCPfybd+HKVXtEvYhICfLdwp42MhiI5mp0oq7PKEL+g1cNiz/Eg==} ··· 861 1255 peerDependencies: 862 1256 typescript: '*' 863 1257 864 - '@vitejs/plugin-react@5.0.4': 865 - resolution: {integrity: sha512-La0KD0vGkVkSk6K+piWDKRUyg8Rl5iAIKRMH0vMJI0Eg47bq1eOxmoObAaQG37WMW9MSyk7Cs8EIWwJC1PtzKA==} 1258 + '@vitejs/plugin-react@5.1.3': 1259 + resolution: {integrity: sha512-NVUnA6gQCl8jfoYqKqQU5Clv0aPw14KkZYCsX6T9Lfu9slI0LOU10OTwFHS/WmptsMMpshNd/1tuWsHQ2Uk+cg==} 866 1260 engines: {node: ^20.19.0 || >=22.12.0} 867 1261 peerDependencies: 868 1262 vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 ··· 913 1307 ajv@6.12.6: 914 1308 resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} 915 1309 1310 + ansi-colors@4.1.3: 1311 + resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} 1312 + engines: {node: '>=6'} 1313 + 916 1314 ansi-regex@5.0.1: 917 1315 resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} 918 1316 engines: {node: '>=8'} ··· 929 1327 resolution: {integrity: sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==} 930 1328 engines: {node: '>=14'} 931 1329 1330 + argparse@1.0.10: 1331 + resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} 1332 + 932 1333 argparse@2.0.1: 933 1334 resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} 934 1335 ··· 938 1339 arktype@2.1.22: 939 1340 resolution: {integrity: sha512-xdzl6WcAhrdahvRRnXaNwsipCgHuNoLobRqhiP8RjnfL9Gp947abGlo68GAIyLtxbD+MLzNyH2YR4kEqioMmYQ==} 940 1341 1342 + array-union@2.1.0: 1343 + resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} 1344 + engines: {node: '>=8'} 1345 + 941 1346 assertion-error@2.0.1: 942 1347 resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} 943 1348 engines: {node: '>=12'} 944 1349 945 - ast-kit@2.1.3: 946 - resolution: {integrity: sha512-TH+b3Lv6pUjy/Nu0m6A2JULtdzLpmqF9x1Dhj00ZoEiML8qvVA9j1flkzTKNYgdEhWrjDwtWNpyyCUbfQe514g==} 1350 + ast-kit@2.2.0: 1351 + resolution: {integrity: sha512-m1Q/RaVOnTp9JxPX+F+Zn7IcLYMzM8kZofDImfsKZd8MbR+ikdOzTeztStWqfrqIxZnYWryyI9ePm3NGjnZgGw==} 947 1352 engines: {node: '>=20.19.0'} 948 1353 949 1354 asynckit@0.4.0: ··· 959 1364 resolution: {integrity: sha512-UYmTpOBwgPScZpS4A+YbapwWuBwasxvO/2IOHArSsAhL/+ZdmATBXTex3t+l2hXwLVYK382ibr/nKoY9GKe86w==} 960 1365 hasBin: true 961 1366 962 - birpc@2.6.1: 963 - resolution: {integrity: sha512-LPnFhlDpdSH6FJhJyn4M0kFO7vtQ5iPw24FnG0y21q09xC7e8+1LeR31S1MAIrDAHp4m7aas4bEkTDTvMAtebQ==} 1367 + better-path-resolve@1.0.0: 1368 + resolution: {integrity: sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g==} 1369 + engines: {node: '>=4'} 1370 + 1371 + birpc@2.8.0: 1372 + resolution: {integrity: sha512-Bz2a4qD/5GRhiHSwj30c/8kC8QGj12nNDwz3D4ErQ4Xhy35dsSDvF+RA/tWpjyU0pdGtSDiEk6B5fBGE1qNVhw==} 964 1373 965 1374 brace-expansion@1.1.12: 966 1375 resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} ··· 999 1408 chalk@4.1.2: 1000 1409 resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} 1001 1410 engines: {node: '>=10'} 1411 + 1412 + chardet@2.1.0: 1413 + resolution: {integrity: sha512-bNFETTG/pM5ryzQ9Ad0lJOTa6HWD/YsScAR3EnCPZRPlQh77JocYktSHOUHelyhm8IARL+o4c4F1bP5KVOjiRA==} 1002 1414 1003 1415 check-error@2.1.1: 1004 1416 resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==} ··· 1008 1420 resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} 1009 1421 engines: {node: '>= 14.16.0'} 1010 1422 1423 + ci-info@3.9.0: 1424 + resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} 1425 + engines: {node: '>=8'} 1426 + 1011 1427 cliui@7.0.4: 1012 1428 resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==} 1013 1429 ··· 1039 1455 resolution: {integrity: sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==} 1040 1456 engines: {node: '>=18'} 1041 1457 1042 - csstype@3.1.3: 1043 - resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} 1458 + csstype@3.2.3: 1459 + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} 1044 1460 1045 1461 data-urls@5.0.0: 1046 1462 resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==} ··· 1076 1492 resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} 1077 1493 engines: {node: '>=6'} 1078 1494 1495 + detect-indent@6.1.0: 1496 + resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==} 1497 + engines: {node: '>=8'} 1498 + 1079 1499 detect-libc@2.1.2: 1080 1500 resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} 1081 1501 engines: {node: '>=8'} ··· 1084 1504 resolution: {integrity: sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg==} 1085 1505 engines: {node: '>=0.3.1'} 1086 1506 1507 + dir-glob@3.0.1: 1508 + resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} 1509 + engines: {node: '>=8'} 1510 + 1087 1511 dom-accessibility-api@0.5.16: 1088 1512 resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} 1089 1513 1090 1514 dom-accessibility-api@0.6.3: 1091 1515 resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==} 1092 1516 1093 - dts-resolver@2.1.2: 1094 - resolution: {integrity: sha512-xeXHBQkn2ISSXxbJWD828PFjtyg+/UrMDo7W4Ffcs7+YWCquxU8YjV1KoxuiL+eJ5pg3ll+bC6flVv61L3LKZg==} 1095 - engines: {node: '>=20.18.0'} 1517 + dts-resolver@2.1.3: 1518 + resolution: {integrity: sha512-bihc7jPC90VrosXNzK0LTE2cuLP6jr0Ro8jk+kMugHReJVLIpHz/xadeq3MhuwyO4TD4OA3L1Q8pBBFRc08Tsw==} 1519 + engines: {node: '>=20.19.0'} 1096 1520 peerDependencies: 1097 1521 oxc-resolver: '>=11.0.0' 1098 1522 peerDependenciesMeta: ··· 1109 1533 emoji-regex@8.0.0: 1110 1534 resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} 1111 1535 1112 - empathic@1.1.0: 1113 - resolution: {integrity: sha512-rsPft6CK3eHtrlp9Y5ALBb+hfK+DWnA4WFebbazxjWyx8vSm3rZeoM3z9irsjcqO3PYRzlfv27XIB4tz2DV7RA==} 1536 + empathic@2.0.0: 1537 + resolution: {integrity: sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==} 1114 1538 engines: {node: '>=14'} 1115 1539 1540 + enhanced-resolve@5.18.3: 1541 + resolution: {integrity: sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==} 1542 + engines: {node: '>=10.13.0'} 1543 + 1544 + enquirer@2.4.1: 1545 + resolution: {integrity: sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==} 1546 + engines: {node: '>=8.6'} 1547 + 1116 1548 entities@6.0.1: 1117 1549 resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} 1118 1550 engines: {node: '>=0.12'} ··· 1181 1613 resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} 1182 1614 engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 1183 1615 1616 + esprima@4.0.1: 1617 + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} 1618 + engines: {node: '>=4'} 1619 + hasBin: true 1620 + 1184 1621 esquery@1.6.0: 1185 1622 resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} 1186 1623 engines: {node: '>=0.10'} ··· 1208 1645 resolution: {integrity: sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==} 1209 1646 engines: {node: '>=12.0.0'} 1210 1647 1648 + extendable-error@0.1.7: 1649 + resolution: {integrity: sha512-UOiS2in6/Q0FK0R0q6UY9vYpQ21mr/Qn1KOnte7vsACuNJf514WvCCUHSRCPcgjPT2bAhNIJdlE6bVap1GKmeg==} 1650 + 1211 1651 fast-deep-equal@3.1.3: 1212 1652 resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} 1213 1653 ··· 1223 1663 1224 1664 fastq@1.19.1: 1225 1665 resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} 1666 + 1667 + fd-package-json@2.0.0: 1668 + resolution: {integrity: sha512-jKmm9YtsNXN789RS/0mSzOC1NUq9mkVd65vbSSVsKdjGvYXBuE4oWe2QOEoFeRmJg+lPuZxpmrfFclNhoRMneQ==} 1226 1669 1227 1670 fdir@6.5.0: 1228 1671 resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} ··· 1241 1684 resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} 1242 1685 engines: {node: '>=8'} 1243 1686 1687 + find-up@4.1.0: 1688 + resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} 1689 + engines: {node: '>=8'} 1690 + 1244 1691 find-up@5.0.0: 1245 1692 resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} 1246 1693 engines: {node: '>=10'} ··· 1256 1703 resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==} 1257 1704 engines: {node: '>= 6'} 1258 1705 1706 + formatly@0.3.0: 1707 + resolution: {integrity: sha512-9XNj/o4wrRFyhSMJOvsuyMwy8aUfBaZ1VrqHVfohyXf0Sw0e+yfKG+xZaY3arGCOMdwFsqObtzVOc1gU9KiT9w==} 1708 + engines: {node: '>=18.3.0'} 1709 + hasBin: true 1710 + 1711 + fs-extra@7.0.1: 1712 + resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==} 1713 + engines: {node: '>=6 <7 || >=8'} 1714 + 1715 + fs-extra@8.1.0: 1716 + resolution: {integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==} 1717 + engines: {node: '>=6 <7 || >=8'} 1718 + 1259 1719 fsevents@2.3.3: 1260 1720 resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} 1261 1721 engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} ··· 1280 1740 resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} 1281 1741 engines: {node: '>= 0.4'} 1282 1742 1283 - get-tsconfig@4.12.0: 1284 - resolution: {integrity: sha512-LScr2aNr2FbjAjZh2C6X6BxRx1/x+aTDExct/xyq2XKbYOiG5c0aK7pMsSuyc0brz3ibr/lbQiHD9jzt4lccJw==} 1743 + get-tsconfig@4.13.0: 1744 + resolution: {integrity: sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==} 1285 1745 1286 1746 glob-parent@5.1.2: 1287 1747 resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} ··· 1295 1755 resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} 1296 1756 engines: {node: '>=18'} 1297 1757 1758 + globby@11.1.0: 1759 + resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} 1760 + engines: {node: '>=10'} 1761 + 1298 1762 gopd@1.2.0: 1299 1763 resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} 1300 1764 engines: {node: '>= 0.4'} 1765 + 1766 + graceful-fs@4.2.11: 1767 + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} 1301 1768 1302 1769 graphemer@1.4.0: 1303 1770 resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} ··· 1339 1806 resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} 1340 1807 engines: {node: '>= 14'} 1341 1808 1809 + human-id@4.1.2: 1810 + resolution: {integrity: sha512-v/J+4Z/1eIJovEBdlV5TYj1IR+ZiohcYGRY+qN/oC9dAfKzVT023N/Bgw37hrKCoVRBvk3bqyzpr2PP5YeTMSg==} 1811 + hasBin: true 1812 + 1342 1813 iconv-lite@0.6.3: 1343 1814 resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} 1815 + engines: {node: '>=0.10.0'} 1816 + 1817 + iconv-lite@0.7.0: 1818 + resolution: {integrity: sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==} 1344 1819 engines: {node: '>=0.10.0'} 1345 1820 1346 1821 ignore@5.3.2: ··· 1385 1860 is-potential-custom-element-name@1.0.1: 1386 1861 resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} 1387 1862 1863 + is-subdir@1.2.0: 1864 + resolution: {integrity: sha512-2AT6j+gXe/1ueqbW6fLZJiIw3F8iXGJtt0yDrZaBhAZEG1raiTxKWU+IPqMCzQAXOUCKdA4UDMgacKH25XG2Cw==} 1865 + engines: {node: '>=4'} 1866 + 1867 + is-windows@1.0.2: 1868 + resolution: {integrity: sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==} 1869 + engines: {node: '>=0.10.0'} 1870 + 1388 1871 isexe@2.0.0: 1389 1872 resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} 1390 1873 1874 + iso-datestring-validator@2.2.2: 1875 + resolution: {integrity: sha512-yLEMkBbLZTlVQqOnQ4FiMujR6T4DEcCb1xizmvXS+OxuhwcbtynoosRzdMA69zZCShCNAbi+gJ71FxZBBXx1SA==} 1876 + 1391 1877 jiti@2.6.1: 1392 1878 resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} 1393 1879 hasBin: true ··· 1398 1884 js-tokens@9.0.1: 1399 1885 resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} 1400 1886 1887 + js-yaml@3.14.1: 1888 + resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} 1889 + hasBin: true 1890 + 1401 1891 js-yaml@4.1.0: 1402 1892 resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} 1403 1893 hasBin: true 1404 1894 1895 + js-yaml@4.1.1: 1896 + resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} 1897 + hasBin: true 1898 + 1405 1899 jsdom@25.0.1: 1406 1900 resolution: {integrity: sha512-8i7LzZj7BF8uplX+ZyOlIz86V6TAsSs+np6m1kpW9u0JWi4z/1t+FzcK1aek+ybTnAC4KhBL4uXCNT0wcUIeCw==} 1407 1901 engines: {node: '>=18'} ··· 1430 1924 engines: {node: '>=6'} 1431 1925 hasBin: true 1432 1926 1927 + jsonfile@4.0.0: 1928 + resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} 1929 + 1433 1930 jsonparse@1.3.1: 1434 1931 resolution: {integrity: sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==} 1435 1932 engines: {'0': node >= 0.2.0} ··· 1442 1939 keyv@4.5.4: 1443 1940 resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} 1444 1941 1942 + knip@5.83.1: 1943 + resolution: {integrity: sha512-av3ZG/Nui6S/BNL8Tmj12yGxYfTnwWnslouW97m40him7o8MwiMjZBY9TPvlEWUci45aVId0/HbgTwSKIDGpMw==} 1944 + engines: {node: '>=18.18.0'} 1945 + hasBin: true 1946 + peerDependencies: 1947 + '@types/node': '>=18' 1948 + typescript: '>=5.0.4 <7' 1949 + 1445 1950 levn@0.4.1: 1446 1951 resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} 1447 1952 engines: {node: '>= 0.8.0'} ··· 1481 1986 engines: {node: '>= 12.0.0'} 1482 1987 cpu: [arm64] 1483 1988 os: [linux] 1989 + libc: [glibc] 1484 1990 1485 1991 lightningcss-linux-arm64-musl@1.30.2: 1486 1992 resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==} 1487 1993 engines: {node: '>= 12.0.0'} 1488 1994 cpu: [arm64] 1489 1995 os: [linux] 1996 + libc: [musl] 1490 1997 1491 1998 lightningcss-linux-x64-gnu@1.30.2: 1492 1999 resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==} 1493 2000 engines: {node: '>= 12.0.0'} 1494 2001 cpu: [x64] 1495 2002 os: [linux] 2003 + libc: [glibc] 1496 2004 1497 2005 lightningcss-linux-x64-musl@1.30.2: 1498 2006 resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==} 1499 2007 engines: {node: '>= 12.0.0'} 1500 2008 cpu: [x64] 1501 2009 os: [linux] 2010 + libc: [musl] 1502 2011 1503 2012 lightningcss-win32-arm64-msvc@1.30.2: 1504 2013 resolution: {integrity: sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==} ··· 1516 2025 resolution: {integrity: sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==} 1517 2026 engines: {node: '>= 12.0.0'} 1518 2027 2028 + locate-path@5.0.0: 2029 + resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} 2030 + engines: {node: '>=8'} 2031 + 1519 2032 locate-path@6.0.0: 1520 2033 resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} 1521 2034 engines: {node: '>=10'} ··· 1523 2036 lodash.merge@4.6.2: 1524 2037 resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} 1525 2038 2039 + lodash.startcase@4.4.0: 2040 + resolution: {integrity: sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==} 2041 + 1526 2042 loupe@3.2.1: 1527 2043 resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} 1528 2044 ··· 1538 2054 1539 2055 magic-string@0.30.19: 1540 2056 resolution: {integrity: sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==} 2057 + 2058 + magic-string@0.30.21: 2059 + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} 1541 2060 1542 2061 make-synchronized@0.4.2: 1543 2062 resolution: {integrity: sha512-EwEJSg8gSGLicKXp/VzNi1tvzhdmNBxOzslkkJSoNUCQFZKH/NIUIp7xlfN+noaHrz4BJDN73gne8IHnjl/F/A==} ··· 1573 2092 resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} 1574 2093 engines: {node: '>=16 || 14 >=14.17'} 1575 2094 2095 + minimist@1.2.8: 2096 + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} 2097 + 1576 2098 monaco-editor@0.52.0: 1577 2099 resolution: {integrity: sha512-OeWhNpABLCeTqubfqLMXGsqf6OmPU6pHM85kF3dhy6kq5hnhuVS1p3VrEW/XhWHc71P2tHyS5JFySD8mgs1crw==} 1578 2100 ··· 1583 2105 ms@2.1.3: 1584 2106 resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} 1585 2107 2108 + multiformats@9.9.0: 2109 + resolution: {integrity: sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==} 2110 + 1586 2111 nanoid@3.3.11: 1587 2112 resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} 1588 2113 engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} ··· 1594 2119 node-releases@2.0.25: 1595 2120 resolution: {integrity: sha512-4auku8B/vw5psvTiiN9j1dAOsXvMoGqJuKJcR+dTdqiXEK20mMTk1UEo3HS16LeGQsVG6+qKTPM9u/qQ2LqATA==} 1596 2121 1597 - nuqs@2.7.2: 1598 - resolution: {integrity: sha512-wOPJoz5om7jMJQick9zU1S/Q+joL+B2DZTZxfCleHEcUzjUnPoujGod4+nAmUWb+G9TwZnyv+mfNqlyfEi8Zag==} 2122 + nuqs@2.8.8: 2123 + resolution: {integrity: sha512-LF5sw9nWpHyPWzMMu9oho3r9C5DvkpmBIg4LQN78sexIzGaeRx8DWr0uy3YiFx5i2QGZN1Qqcb+OAtEVRa2bnA==} 1599 2124 peerDependencies: 1600 2125 '@remix-run/react': '>=2' 1601 2126 '@tanstack/react-router': ^1 1602 2127 next: '>=14.2.0' 1603 2128 react: '>=18.2.0 || ^19.0.0-0' 1604 - react-router: ^6 || ^7 1605 - react-router-dom: ^6 || ^7 2129 + react-router: ^5 || ^6 || ^7 2130 + react-router-dom: ^5 || ^6 || ^7 1606 2131 peerDependenciesMeta: 1607 2132 '@remix-run/react': 1608 2133 optional: true ··· 1618 2143 nwsapi@2.2.22: 1619 2144 resolution: {integrity: sha512-ujSMe1OWVn55euT1ihwCI1ZcAaAU3nxUiDwfDQldc51ZXaB9m2AyOn6/jh1BLe2t/G8xd6uKG1UBF2aZJeg2SQ==} 1620 2145 2146 + obug@1.0.0: 2147 + resolution: {integrity: sha512-WKcS43Yl6YPJekid7KiRdT6CHMSmYWVfJiSFbTaGxWQlC+cEBPxHa9jR1uS2cMiQmXd8Hsa2ipAKErQ/GLhSpg==} 2148 + peerDependencies: 2149 + ms: ^2.0.0 2150 + peerDependenciesMeta: 2151 + ms: 2152 + optional: true 2153 + 1621 2154 optionator@0.9.4: 1622 2155 resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} 1623 2156 engines: {node: '>= 0.8.0'} 1624 2157 2158 + outdent@0.5.0: 2159 + resolution: {integrity: sha512-/jHxFIzoMXdqPzTaCpFzAAWhpkSjZPF4Vsn6jAfNpmbH/ymsmd7Qc6VE9BGn0L6YMj6uwpQLxCECpus4ukKS9Q==} 2160 + 2161 + oxc-resolver@11.15.0: 2162 + resolution: {integrity: sha512-Hk2J8QMYwmIO9XTCUiOH00+Xk2/+aBxRUnhrSlANDyCnLYc32R1WSIq1sU2yEdlqd53FfMpPEpnBYIKQMzliJw==} 2163 + 2164 + p-filter@2.1.0: 2165 + resolution: {integrity: sha512-ZBxxZ5sL2HghephhpGAQdoskxplTwr7ICaehZwLIlfL6acuVgZPm8yBNuRAFBGEqtD/hmUeq9eqLg2ys9Xr/yw==} 2166 + engines: {node: '>=8'} 2167 + 2168 + p-limit@2.3.0: 2169 + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} 2170 + engines: {node: '>=6'} 2171 + 1625 2172 p-limit@3.1.0: 1626 2173 resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} 1627 2174 engines: {node: '>=10'} 1628 2175 2176 + p-locate@4.1.0: 2177 + resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} 2178 + engines: {node: '>=8'} 2179 + 1629 2180 p-locate@5.0.0: 1630 2181 resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} 1631 2182 engines: {node: '>=10'} 1632 2183 2184 + p-map@2.1.0: 2185 + resolution: {integrity: sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==} 2186 + engines: {node: '>=6'} 2187 + 2188 + p-try@2.2.0: 2189 + resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} 2190 + engines: {node: '>=6'} 2191 + 2192 + package-manager-detector@0.2.11: 2193 + resolution: {integrity: sha512-BEnLolu+yuz22S56CU1SUKq3XC3PkwD5wv4ikR4MfGvnRVcmzXR9DwSlW2fEamyTPyXHomBJRzgapeuBvRNzJQ==} 2194 + 1633 2195 parent-module@1.0.1: 1634 2196 resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} 1635 2197 engines: {node: '>=6'} ··· 1643 2205 1644 2206 path-key@3.1.1: 1645 2207 resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} 2208 + engines: {node: '>=8'} 2209 + 2210 + path-type@4.0.0: 2211 + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} 1646 2212 engines: {node: '>=8'} 1647 2213 1648 2214 pathe@2.0.3: ··· 1663 2229 resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} 1664 2230 engines: {node: '>=12'} 1665 2231 2232 + pify@4.0.1: 2233 + resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==} 2234 + engines: {node: '>=6'} 2235 + 1666 2236 postcss@8.5.6: 1667 2237 resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} 1668 2238 engines: {node: ^10 || ^12 || >=14} ··· 1670 2240 prelude-ls@1.2.1: 1671 2241 resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} 1672 2242 engines: {node: '>= 0.8.0'} 2243 + 2244 + prettier@2.8.8: 2245 + resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==} 2246 + engines: {node: '>=10.13.0'} 2247 + hasBin: true 1673 2248 1674 2249 prettier@3.5.3: 1675 2250 resolution: {integrity: sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==} ··· 1695 2270 queue-microtask@1.2.3: 1696 2271 resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} 1697 2272 1698 - react-dom@19.2.0: 1699 - resolution: {integrity: sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==} 2273 + react-dom@19.2.4: 2274 + resolution: {integrity: sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==} 1700 2275 peerDependencies: 1701 - react: ^19.2.0 2276 + react: ^19.2.4 1702 2277 1703 2278 react-is@17.0.2: 1704 2279 resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} 1705 2280 1706 - react-refresh@0.17.0: 1707 - resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==} 2281 + react-refresh@0.18.0: 2282 + resolution: {integrity: sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==} 1708 2283 engines: {node: '>=0.10.0'} 1709 2284 1710 - react@19.2.0: 1711 - resolution: {integrity: sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==} 2285 + react@19.2.4: 2286 + resolution: {integrity: sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==} 1712 2287 engines: {node: '>=0.10.0'} 2288 + 2289 + read-yaml-file@1.1.0: 2290 + resolution: {integrity: sha512-VIMnQi/Z4HT2Fxuwg5KrY174U1VdUIASQVWXXyqtNRtxSr9IYkn1rsI6Tb6HsrHCmB7gVpNwX6JxPTHcH6IoTA==} 2291 + engines: {node: '>=6'} 1713 2292 1714 2293 readable-stream@3.6.2: 1715 2294 resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} ··· 1730 2309 resolve-from@4.0.0: 1731 2310 resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} 1732 2311 engines: {node: '>=4'} 2312 + 2313 + resolve-from@5.0.0: 2314 + resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} 2315 + engines: {node: '>=8'} 1733 2316 1734 2317 resolve-pkg-maps@1.0.0: 1735 2318 resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} ··· 1738 2321 resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} 1739 2322 engines: {iojs: '>=1.0.0', node: '>=0.10.0'} 1740 2323 1741 - rolldown-plugin-dts@0.13.14: 1742 - resolution: {integrity: sha512-wjNhHZz9dlN6PTIXyizB6u/mAg1wEFMW9yw7imEVe3CxHSRnNHVyycIX0yDEOVJfDNISLPbkCIPEpFpizy5+PQ==} 2324 + rolldown-plugin-dts@0.17.7: 2325 + resolution: {integrity: sha512-ZGgXMhzCItmznNzbJlTcC/KdM6bIwcZoYUykJ2q14HOGvnMhnl2RXU+XrIrdjA2Hyzq3nWqDH7qWaM5a4uCVnw==} 1743 2326 engines: {node: '>=20.18.0'} 1744 2327 peerDependencies: 2328 + '@ts-macro/tsc': ^0.3.6 1745 2329 '@typescript/native-preview': '>=7.0.0-dev.20250601.1' 1746 - rolldown: ^1.0.0-beta.9 2330 + rolldown: ^1.0.0-beta.44 1747 2331 typescript: ^5.0.0 1748 - vue-tsc: ^2.2.0 || ^3.0.0 2332 + vue-tsc: ~3.1.0 1749 2333 peerDependenciesMeta: 2334 + '@ts-macro/tsc': 2335 + optional: true 1750 2336 '@typescript/native-preview': 1751 2337 optional: true 1752 2338 typescript: ··· 1794 2380 yaml: 1795 2381 optional: true 1796 2382 1797 - rolldown@1.0.0-beta.11-commit.f051675: 1798 - resolution: {integrity: sha512-g8MCVkvg2GnrrG+j+WplOTx1nAmjSwYOMSOQI0qfxf8D4NmYZqJuG3f85yWK64XXQv6pKcXZsfMkOPs9B6B52A==} 2383 + rolldown@1.0.0-beta.24: 2384 + resolution: {integrity: sha512-eDyipoOnoHQ5p6INkJ8g31eKGlqPSCAN9PapyOTw5HET4FYIWALZnSgpMZ67mdn+xT3jAsqGidNnBcIM6EAUhA==} 1799 2385 hasBin: true 1800 2386 1801 - rolldown@1.0.0-beta.24: 1802 - resolution: {integrity: sha512-eDyipoOnoHQ5p6INkJ8g31eKGlqPSCAN9PapyOTw5HET4FYIWALZnSgpMZ67mdn+xT3jAsqGidNnBcIM6EAUhA==} 2387 + rolldown@1.0.0-beta.45: 2388 + resolution: {integrity: sha512-iMmuD72XXLf26Tqrv1cryNYLX6NNPLhZ3AmNkSf8+xda0H+yijjGJ+wVT9UdBUHOpKzq9RjKtQKRCWoEKQQBZQ==} 2389 + engines: {node: ^20.19.0 || >=22.12.0} 1803 2390 hasBin: true 1804 2391 1805 2392 rrweb-cssom@0.7.1: ··· 1848 2435 siginfo@2.0.0: 1849 2436 resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} 1850 2437 2438 + signal-exit@4.1.0: 2439 + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} 2440 + engines: {node: '>=14'} 2441 + 2442 + slash@3.0.0: 2443 + resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} 2444 + engines: {node: '>=8'} 2445 + 2446 + smol-toml@1.5.2: 2447 + resolution: {integrity: sha512-QlaZEqcAH3/RtNyet1IPIYPsEWAaYyXXv1Krsi+1L/QHppjX4Ifm8MQsBISz9vE8cHicIq3clogsheili5vhaQ==} 2448 + engines: {node: '>= 18'} 2449 + 1851 2450 source-map-js@1.2.1: 1852 2451 resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} 1853 2452 engines: {node: '>=0.10.0'} 1854 2453 2454 + spawndamnit@3.0.1: 2455 + resolution: {integrity: sha512-MmnduQUuHCoFckZoWnXsTg7JaiLBJrKFj9UI2MbRPGaJeVpsLcVBu6P/IGZovziM/YBsellCmsprgNA+w0CzVg==} 2456 + 1855 2457 split2@3.2.2: 1856 2458 resolution: {integrity: sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg==} 2459 + 2460 + sprintf-js@1.0.3: 2461 + resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} 1857 2462 1858 2463 stackback@0.0.2: 1859 2464 resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} ··· 1875 2480 resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} 1876 2481 engines: {node: '>=8'} 1877 2482 2483 + strip-bom@3.0.0: 2484 + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} 2485 + engines: {node: '>=4'} 2486 + 1878 2487 strip-indent@3.0.0: 1879 2488 resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} 1880 2489 engines: {node: '>=8'} ··· 1883 2492 resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} 1884 2493 engines: {node: '>=8'} 1885 2494 2495 + strip-json-comments@5.0.3: 2496 + resolution: {integrity: sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==} 2497 + engines: {node: '>=14.16'} 2498 + 1886 2499 strip-literal@3.1.0: 1887 2500 resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==} 1888 2501 ··· 1893 2506 symbol-tree@3.2.4: 1894 2507 resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} 1895 2508 2509 + tailwindcss@4.1.18: 2510 + resolution: {integrity: sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==} 2511 + 2512 + tapable@2.3.0: 2513 + resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} 2514 + engines: {node: '>=6'} 2515 + 2516 + term-size@2.2.1: 2517 + resolution: {integrity: sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==} 2518 + engines: {node: '>=8'} 2519 + 1896 2520 through2@4.0.2: 1897 2521 resolution: {integrity: sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==} 1898 2522 ··· 1902 2526 tinyexec@0.3.2: 1903 2527 resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} 1904 2528 1905 - tinyexec@1.0.1: 1906 - resolution: {integrity: sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw==} 2529 + tinyexec@1.0.2: 2530 + resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==} 2531 + engines: {node: '>=18'} 1907 2532 1908 2533 tinyglobby@0.2.15: 1909 2534 resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} ··· 1940 2565 resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==} 1941 2566 engines: {node: '>=18'} 1942 2567 2568 + tree-kill@1.2.2: 2569 + resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} 2570 + hasBin: true 2571 + 1943 2572 treeify@1.1.0: 1944 2573 resolution: {integrity: sha512-1m4RA7xVAJrSGrrXGs0L3YTwyvBs2S8PbRHaLZAkFw7JR8oIFwYtysxlBZhYIa7xSyiYJKZ3iGrrk55cGA3i9A==} 1945 2574 engines: {node: '>=0.6'} ··· 1950 2579 peerDependencies: 1951 2580 typescript: '>=4.8.4' 1952 2581 1953 - tsdown@0.12.7: 1954 - resolution: {integrity: sha512-VJjVaqJfIQuQwtOoeuEJMOJUf3MPDrfX0X7OUNx3nq5pQeuIl3h58tmdbM1IZcu8Dn2j8NQjLh+5TXa0yPb9zg==} 1955 - engines: {node: '>=18.0.0'} 2582 + tsdown@0.15.12: 2583 + resolution: {integrity: sha512-c8VLlQm8/lFrOAg5VMVeN4NAbejZyVQkzd+ErjuaQgJFI/9MhR9ivr0H/CM7UlOF1+ELlF6YaI7sU/4itgGQ8w==} 2584 + engines: {node: '>=20.19.0'} 1956 2585 hasBin: true 1957 2586 peerDependencies: 1958 2587 '@arethetypeswrong/core': ^0.18.1 ··· 1960 2589 typescript: ^5.0.0 1961 2590 unplugin-lightningcss: ^0.4.0 1962 2591 unplugin-unused: ^0.5.0 2592 + unrun: ^0.2.1 1963 2593 peerDependenciesMeta: 1964 2594 '@arethetypeswrong/core': 1965 2595 optional: true ··· 1970 2600 unplugin-lightningcss: 1971 2601 optional: true 1972 2602 unplugin-unused: 2603 + optional: true 2604 + unrun: 1973 2605 optional: true 1974 2606 1975 2607 tslib@2.8.1: ··· 1986 2618 eslint: ^8.57.0 || ^9.0.0 1987 2619 typescript: '>=4.8.4 <5.9.0' 1988 2620 1989 - typescript@5.8.3: 1990 - resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==} 2621 + typescript@5.9.3: 2622 + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} 1991 2623 engines: {node: '>=14.17'} 1992 2624 hasBin: true 1993 2625 1994 - unconfig@7.3.3: 1995 - resolution: {integrity: sha512-QCkQoOnJF8L107gxfHL0uavn7WD9b3dpBcFX6HtfQYmjw2YzWxGuFQ0N0J6tE9oguCBJn9KOvfqYDCMPHIZrBA==} 2626 + uint8arrays@3.0.0: 2627 + resolution: {integrity: sha512-HRCx0q6O9Bfbp+HHSfQQKD7wU70+lydKVt4EghkdOvlK/NlrF90z+eXV34mUd48rNvVJXwkrMSPpCATkct8fJA==} 2628 + 2629 + unconfig-core@7.4.1: 2630 + resolution: {integrity: sha512-Bp/bPZjV2Vl/fofoA2OYLSnw1Z0MOhCX7zHnVCYrazpfZvseBbGhwcNQMxsg185Mqh7VZQqK3C8hFG/Dyng+yA==} 2631 + 2632 + unconfig@7.4.1: 2633 + resolution: {integrity: sha512-uyQ7LElcGizrOGZyIq9KU+xkuEjcRf9IpmDTkCSYv5mEeZzrXSj6rb51C0L+WTedsmAoVxW9WKrLWhSwebIM9Q==} 1996 2634 1997 2635 undici-types@7.8.0: 1998 2636 resolution: {integrity: sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==} 1999 2637 2638 + unicode-segmenter@0.14.0: 2639 + resolution: {integrity: sha512-AH4lhPCJANUnSLEKnM4byboctePJzltF4xj8b+NbNiYeAkAXGh7px2K/4NANFp7dnr6+zB3e6HLu8Jj8SKyvYg==} 2640 + 2641 + universalify@0.1.2: 2642 + resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} 2643 + engines: {node: '>= 4.0.0'} 2644 + 2000 2645 update-browserslist-db@1.1.3: 2001 2646 resolution: {integrity: sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==} 2002 2647 hasBin: true ··· 2045 2690 w3c-xmlserializer@5.0.0: 2046 2691 resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} 2047 2692 engines: {node: '>=18'} 2693 + 2694 + walk-up-path@4.0.0: 2695 + resolution: {integrity: sha512-3hu+tD8YzSLGuFYtPRb48vdhKMi0KQV5sn+uWr8+7dMEq/2G/dtLrdDinkLjqq5TIbIBjYJ4Ax/n3YiaW7QM8A==} 2696 + engines: {node: 20 || >=22} 2048 2697 2049 2698 webidl-conversions@7.0.0: 2050 2699 resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} ··· 2127 2776 zod@3.25.76: 2128 2777 resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} 2129 2778 2779 + zod@4.1.12: 2780 + resolution: {integrity: sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==} 2781 + 2130 2782 snapshots: 2131 2783 2132 2784 '@adobe/css-tools@4.4.4': {} 2133 2785 2134 - '@ark/attest@0.49.0(typescript@5.8.3)': 2786 + '@ark/attest@0.49.0(typescript@5.9.3)': 2135 2787 dependencies: 2136 2788 '@ark/fs': 0.49.0 2137 2789 '@ark/util': 0.49.0 2138 2790 '@prettier/sync': 0.5.5(prettier@3.5.3) 2139 2791 '@typescript/analyze-trace': 0.10.1 2140 - '@typescript/vfs': 1.6.1(typescript@5.8.3) 2792 + '@typescript/vfs': 1.6.1(typescript@5.9.3) 2141 2793 arktype: 2.1.22 2142 2794 prettier: 3.5.3 2143 - typescript: 5.8.3 2795 + typescript: 5.9.3 2144 2796 transitivePeerDependencies: 2145 2797 - supports-color 2146 2798 ··· 2160 2812 '@csstools/css-tokenizer': 3.0.4 2161 2813 lru-cache: 10.4.3 2162 2814 2815 + '@atproto/common-web@0.4.5': 2816 + dependencies: 2817 + '@atproto/lex-data': 0.0.1 2818 + '@atproto/lex-json': 0.0.1 2819 + zod: 3.25.76 2820 + 2821 + '@atproto/lex-data@0.0.1': 2822 + dependencies: 2823 + '@atproto/syntax': 0.4.1 2824 + multiformats: 9.9.0 2825 + tslib: 2.8.1 2826 + uint8arrays: 3.0.0 2827 + unicode-segmenter: 0.14.0 2828 + 2829 + '@atproto/lex-json@0.0.1': 2830 + dependencies: 2831 + '@atproto/lex-data': 0.0.1 2832 + tslib: 2.8.1 2833 + 2834 + '@atproto/lexicon@0.5.2': 2835 + dependencies: 2836 + '@atproto/common-web': 0.4.5 2837 + '@atproto/syntax': 0.4.1 2838 + iso-datestring-validator: 2.2.2 2839 + multiformats: 9.9.0 2840 + zod: 3.25.76 2841 + 2842 + '@atproto/syntax@0.4.1': {} 2843 + 2163 2844 '@babel/code-frame@7.27.1': 2164 2845 dependencies: 2165 2846 '@babel/helper-validator-identifier': 7.27.1 2166 2847 js-tokens: 4.0.0 2167 2848 picocolors: 1.1.1 2168 2849 2850 + '@babel/code-frame@7.29.0': 2851 + dependencies: 2852 + '@babel/helper-validator-identifier': 7.28.5 2853 + js-tokens: 4.0.0 2854 + picocolors: 1.1.1 2855 + 2169 2856 '@babel/compat-data@7.28.4': {} 2857 + 2858 + '@babel/compat-data@7.29.0': {} 2170 2859 2171 2860 '@babel/core@7.28.4': 2172 2861 dependencies: ··· 2179 2868 '@babel/template': 7.27.2 2180 2869 '@babel/traverse': 7.28.4 2181 2870 '@babel/types': 7.28.4 2871 + '@jridgewell/remapping': 2.3.5 2872 + convert-source-map: 2.0.0 2873 + debug: 4.4.3 2874 + gensync: 1.0.0-beta.2 2875 + json5: 2.2.3 2876 + semver: 6.3.1 2877 + transitivePeerDependencies: 2878 + - supports-color 2879 + 2880 + '@babel/core@7.29.0': 2881 + dependencies: 2882 + '@babel/code-frame': 7.29.0 2883 + '@babel/generator': 7.29.1 2884 + '@babel/helper-compilation-targets': 7.28.6 2885 + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) 2886 + '@babel/helpers': 7.28.6 2887 + '@babel/parser': 7.29.0 2888 + '@babel/template': 7.28.6 2889 + '@babel/traverse': 7.29.0 2890 + '@babel/types': 7.29.0 2182 2891 '@jridgewell/remapping': 2.3.5 2183 2892 convert-source-map: 2.0.0 2184 2893 debug: 4.4.3 ··· 2196 2905 '@jridgewell/trace-mapping': 0.3.31 2197 2906 jsesc: 3.1.0 2198 2907 2908 + '@babel/generator@7.28.5': 2909 + dependencies: 2910 + '@babel/parser': 7.28.5 2911 + '@babel/types': 7.28.5 2912 + '@jridgewell/gen-mapping': 0.3.13 2913 + '@jridgewell/trace-mapping': 0.3.31 2914 + jsesc: 3.1.0 2915 + 2916 + '@babel/generator@7.29.1': 2917 + dependencies: 2918 + '@babel/parser': 7.29.0 2919 + '@babel/types': 7.29.0 2920 + '@jridgewell/gen-mapping': 0.3.13 2921 + '@jridgewell/trace-mapping': 0.3.31 2922 + jsesc: 3.1.0 2923 + 2199 2924 '@babel/helper-annotate-as-pure@7.27.3': 2200 2925 dependencies: 2201 2926 '@babel/types': 7.28.4 ··· 2203 2928 '@babel/helper-compilation-targets@7.27.2': 2204 2929 dependencies: 2205 2930 '@babel/compat-data': 7.28.4 2931 + '@babel/helper-validator-option': 7.27.1 2932 + browserslist: 4.26.3 2933 + lru-cache: 5.1.1 2934 + semver: 6.3.1 2935 + 2936 + '@babel/helper-compilation-targets@7.28.6': 2937 + dependencies: 2938 + '@babel/compat-data': 7.29.0 2206 2939 '@babel/helper-validator-option': 7.27.1 2207 2940 browserslist: 4.26.3 2208 2941 lru-cache: 5.1.1 ··· 2237 2970 transitivePeerDependencies: 2238 2971 - supports-color 2239 2972 2973 + '@babel/helper-module-imports@7.28.6': 2974 + dependencies: 2975 + '@babel/traverse': 7.29.0 2976 + '@babel/types': 7.29.0 2977 + transitivePeerDependencies: 2978 + - supports-color 2979 + 2240 2980 '@babel/helper-module-transforms@7.28.3(@babel/core@7.28.4)': 2241 2981 dependencies: 2242 2982 '@babel/core': 7.28.4 2243 2983 '@babel/helper-module-imports': 7.27.1 2244 2984 '@babel/helper-validator-identifier': 7.27.1 2245 2985 '@babel/traverse': 7.28.4 2986 + transitivePeerDependencies: 2987 + - supports-color 2988 + 2989 + '@babel/helper-module-transforms@7.28.6(@babel/core@7.29.0)': 2990 + dependencies: 2991 + '@babel/core': 7.29.0 2992 + '@babel/helper-module-imports': 7.28.6 2993 + '@babel/helper-validator-identifier': 7.28.5 2994 + '@babel/traverse': 7.29.0 2246 2995 transitivePeerDependencies: 2247 2996 - supports-color 2248 2997 ··· 2272 3021 2273 3022 '@babel/helper-validator-identifier@7.27.1': {} 2274 3023 3024 + '@babel/helper-validator-identifier@7.28.5': {} 3025 + 2275 3026 '@babel/helper-validator-option@7.27.1': {} 2276 3027 2277 3028 '@babel/helpers@7.28.4': ··· 2279 3030 '@babel/template': 7.27.2 2280 3031 '@babel/types': 7.28.4 2281 3032 3033 + '@babel/helpers@7.28.6': 3034 + dependencies: 3035 + '@babel/template': 7.28.6 3036 + '@babel/types': 7.29.0 3037 + 2282 3038 '@babel/parser@7.28.4': 2283 3039 dependencies: 2284 3040 '@babel/types': 7.28.4 2285 3041 3042 + '@babel/parser@7.28.5': 3043 + dependencies: 3044 + '@babel/types': 7.28.5 3045 + 3046 + '@babel/parser@7.29.0': 3047 + dependencies: 3048 + '@babel/types': 7.29.0 3049 + 2286 3050 '@babel/plugin-proposal-private-methods@7.18.6(@babel/core@7.28.4)': 2287 3051 dependencies: 2288 3052 '@babel/core': 7.28.4 ··· 2291 3055 transitivePeerDependencies: 2292 3056 - supports-color 2293 3057 2294 - '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.28.4)': 3058 + '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.29.0)': 2295 3059 dependencies: 2296 - '@babel/core': 7.28.4 3060 + '@babel/core': 7.29.0 2297 3061 '@babel/helper-plugin-utils': 7.27.1 2298 3062 2299 - '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.28.4)': 3063 + '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.29.0)': 2300 3064 dependencies: 2301 - '@babel/core': 7.28.4 3065 + '@babel/core': 7.29.0 2302 3066 '@babel/helper-plugin-utils': 7.27.1 2303 3067 2304 3068 '@babel/runtime@7.28.4': {} ··· 2309 3073 '@babel/parser': 7.28.4 2310 3074 '@babel/types': 7.28.4 2311 3075 3076 + '@babel/template@7.28.6': 3077 + dependencies: 3078 + '@babel/code-frame': 7.29.0 3079 + '@babel/parser': 7.29.0 3080 + '@babel/types': 7.29.0 3081 + 2312 3082 '@babel/traverse@7.28.4': 2313 3083 dependencies: 2314 3084 '@babel/code-frame': 7.27.1 ··· 2321 3091 transitivePeerDependencies: 2322 3092 - supports-color 2323 3093 3094 + '@babel/traverse@7.29.0': 3095 + dependencies: 3096 + '@babel/code-frame': 7.29.0 3097 + '@babel/generator': 7.29.1 3098 + '@babel/helper-globals': 7.28.0 3099 + '@babel/parser': 7.29.0 3100 + '@babel/template': 7.28.6 3101 + '@babel/types': 7.29.0 3102 + debug: 4.4.3 3103 + transitivePeerDependencies: 3104 + - supports-color 3105 + 2324 3106 '@babel/types@7.28.4': 2325 3107 dependencies: 2326 3108 '@babel/helper-string-parser': 7.27.1 2327 3109 '@babel/helper-validator-identifier': 7.27.1 2328 3110 3111 + '@babel/types@7.28.5': 3112 + dependencies: 3113 + '@babel/helper-string-parser': 7.27.1 3114 + '@babel/helper-validator-identifier': 7.28.5 3115 + 3116 + '@babel/types@7.29.0': 3117 + dependencies: 3118 + '@babel/helper-string-parser': 7.27.1 3119 + '@babel/helper-validator-identifier': 7.28.5 3120 + 3121 + '@changesets/apply-release-plan@7.0.14': 3122 + dependencies: 3123 + '@changesets/config': 3.1.2 3124 + '@changesets/get-version-range-type': 0.4.0 3125 + '@changesets/git': 3.0.4 3126 + '@changesets/should-skip-package': 0.1.2 3127 + '@changesets/types': 6.1.0 3128 + '@manypkg/get-packages': 1.1.3 3129 + detect-indent: 6.1.0 3130 + fs-extra: 7.0.1 3131 + lodash.startcase: 4.4.0 3132 + outdent: 0.5.0 3133 + prettier: 2.8.8 3134 + resolve-from: 5.0.0 3135 + semver: 7.7.3 3136 + 3137 + '@changesets/assemble-release-plan@6.0.9': 3138 + dependencies: 3139 + '@changesets/errors': 0.2.0 3140 + '@changesets/get-dependents-graph': 2.1.3 3141 + '@changesets/should-skip-package': 0.1.2 3142 + '@changesets/types': 6.1.0 3143 + '@manypkg/get-packages': 1.1.3 3144 + semver: 7.7.3 3145 + 3146 + '@changesets/changelog-git@0.2.1': 3147 + dependencies: 3148 + '@changesets/types': 6.1.0 3149 + 3150 + '@changesets/cli@2.29.8(@types/node@24.0.4)': 3151 + dependencies: 3152 + '@changesets/apply-release-plan': 7.0.14 3153 + '@changesets/assemble-release-plan': 6.0.9 3154 + '@changesets/changelog-git': 0.2.1 3155 + '@changesets/config': 3.1.2 3156 + '@changesets/errors': 0.2.0 3157 + '@changesets/get-dependents-graph': 2.1.3 3158 + '@changesets/get-release-plan': 4.0.14 3159 + '@changesets/git': 3.0.4 3160 + '@changesets/logger': 0.1.1 3161 + '@changesets/pre': 2.0.2 3162 + '@changesets/read': 0.6.6 3163 + '@changesets/should-skip-package': 0.1.2 3164 + '@changesets/types': 6.1.0 3165 + '@changesets/write': 0.4.0 3166 + '@inquirer/external-editor': 1.0.2(@types/node@24.0.4) 3167 + '@manypkg/get-packages': 1.1.3 3168 + ansi-colors: 4.1.3 3169 + ci-info: 3.9.0 3170 + enquirer: 2.4.1 3171 + fs-extra: 7.0.1 3172 + mri: 1.2.0 3173 + p-limit: 2.3.0 3174 + package-manager-detector: 0.2.11 3175 + picocolors: 1.1.1 3176 + resolve-from: 5.0.0 3177 + semver: 7.7.3 3178 + spawndamnit: 3.0.1 3179 + term-size: 2.2.1 3180 + transitivePeerDependencies: 3181 + - '@types/node' 3182 + 3183 + '@changesets/config@3.1.2': 3184 + dependencies: 3185 + '@changesets/errors': 0.2.0 3186 + '@changesets/get-dependents-graph': 2.1.3 3187 + '@changesets/logger': 0.1.1 3188 + '@changesets/types': 6.1.0 3189 + '@manypkg/get-packages': 1.1.3 3190 + fs-extra: 7.0.1 3191 + micromatch: 4.0.8 3192 + 3193 + '@changesets/errors@0.2.0': 3194 + dependencies: 3195 + extendable-error: 0.1.7 3196 + 3197 + '@changesets/get-dependents-graph@2.1.3': 3198 + dependencies: 3199 + '@changesets/types': 6.1.0 3200 + '@manypkg/get-packages': 1.1.3 3201 + picocolors: 1.1.1 3202 + semver: 7.7.3 3203 + 3204 + '@changesets/get-release-plan@4.0.14': 3205 + dependencies: 3206 + '@changesets/assemble-release-plan': 6.0.9 3207 + '@changesets/config': 3.1.2 3208 + '@changesets/pre': 2.0.2 3209 + '@changesets/read': 0.6.6 3210 + '@changesets/types': 6.1.0 3211 + '@manypkg/get-packages': 1.1.3 3212 + 3213 + '@changesets/get-version-range-type@0.4.0': {} 3214 + 3215 + '@changesets/git@3.0.4': 3216 + dependencies: 3217 + '@changesets/errors': 0.2.0 3218 + '@manypkg/get-packages': 1.1.3 3219 + is-subdir: 1.2.0 3220 + micromatch: 4.0.8 3221 + spawndamnit: 3.0.1 3222 + 3223 + '@changesets/logger@0.1.1': 3224 + dependencies: 3225 + picocolors: 1.1.1 3226 + 3227 + '@changesets/parse@0.4.2': 3228 + dependencies: 3229 + '@changesets/types': 6.1.0 3230 + js-yaml: 4.1.1 3231 + 3232 + '@changesets/pre@2.0.2': 3233 + dependencies: 3234 + '@changesets/errors': 0.2.0 3235 + '@changesets/types': 6.1.0 3236 + '@manypkg/get-packages': 1.1.3 3237 + fs-extra: 7.0.1 3238 + 3239 + '@changesets/read@0.6.6': 3240 + dependencies: 3241 + '@changesets/git': 3.0.4 3242 + '@changesets/logger': 0.1.1 3243 + '@changesets/parse': 0.4.2 3244 + '@changesets/types': 6.1.0 3245 + fs-extra: 7.0.1 3246 + p-filter: 2.1.0 3247 + picocolors: 1.1.1 3248 + 3249 + '@changesets/should-skip-package@0.1.2': 3250 + dependencies: 3251 + '@changesets/types': 6.1.0 3252 + '@manypkg/get-packages': 1.1.3 3253 + 3254 + '@changesets/types@4.1.0': {} 3255 + 3256 + '@changesets/types@6.1.0': {} 3257 + 3258 + '@changesets/write@0.4.0': 3259 + dependencies: 3260 + '@changesets/types': 6.1.0 3261 + fs-extra: 7.0.1 3262 + human-id: 4.1.2 3263 + prettier: 2.8.8 3264 + 2329 3265 '@csstools/color-helpers@5.1.0': {} 2330 3266 2331 3267 '@csstools/css-calc@2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': ··· 2352 3288 tslib: 2.8.1 2353 3289 optional: true 2354 3290 3291 + '@emnapi/core@1.7.1': 3292 + dependencies: 3293 + '@emnapi/wasi-threads': 1.1.0 3294 + tslib: 2.8.1 3295 + optional: true 3296 + 2355 3297 '@emnapi/runtime@1.5.0': 3298 + dependencies: 3299 + tslib: 2.8.1 3300 + optional: true 3301 + 3302 + '@emnapi/runtime@1.7.1': 2356 3303 dependencies: 2357 3304 tslib: 2.8.1 2358 3305 optional: true ··· 2499 3446 2500 3447 '@humanwhocodes/retry@0.4.3': {} 2501 3448 3449 + '@inquirer/external-editor@1.0.2(@types/node@24.0.4)': 3450 + dependencies: 3451 + chardet: 2.1.0 3452 + iconv-lite: 0.7.0 3453 + optionalDependencies: 3454 + '@types/node': 24.0.4 3455 + 2502 3456 '@jridgewell/gen-mapping@0.3.13': 2503 3457 dependencies: 2504 3458 '@jridgewell/sourcemap-codec': 1.5.5 ··· 2518 3472 '@jridgewell/resolve-uri': 3.1.2 2519 3473 '@jridgewell/sourcemap-codec': 1.5.5 2520 3474 3475 + '@manypkg/find-root@1.1.0': 3476 + dependencies: 3477 + '@babel/runtime': 7.28.4 3478 + '@types/node': 12.20.55 3479 + find-up: 4.1.0 3480 + fs-extra: 8.1.0 3481 + 3482 + '@manypkg/get-packages@1.1.3': 3483 + dependencies: 3484 + '@babel/runtime': 7.28.4 3485 + '@changesets/types': 4.1.0 3486 + '@manypkg/find-root': 1.1.0 3487 + fs-extra: 8.1.0 3488 + globby: 11.1.0 3489 + read-yaml-file: 1.1.0 3490 + 2521 3491 '@monaco-editor/loader@1.6.1': 2522 3492 dependencies: 2523 3493 state-local: 1.0.7 2524 3494 2525 - '@monaco-editor/react@4.7.0(monaco-editor@0.52.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': 3495 + '@monaco-editor/react@4.7.0(monaco-editor@0.52.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': 2526 3496 dependencies: 2527 3497 '@monaco-editor/loader': 1.6.1 2528 3498 monaco-editor: 0.52.0 2529 - react: 19.2.0 2530 - react-dom: 19.2.0(react@19.2.0) 3499 + react: 19.2.4 3500 + react-dom: 19.2.4(react@19.2.4) 2531 3501 2532 3502 '@napi-rs/wasm-runtime@0.2.12': 2533 3503 dependencies: ··· 2536 3506 '@tybys/wasm-util': 0.10.1 2537 3507 optional: true 2538 3508 3509 + '@napi-rs/wasm-runtime@1.0.7': 3510 + dependencies: 3511 + '@emnapi/core': 1.5.0 3512 + '@emnapi/runtime': 1.5.0 3513 + '@tybys/wasm-util': 0.10.1 3514 + optional: true 3515 + 3516 + '@napi-rs/wasm-runtime@1.1.0': 3517 + dependencies: 3518 + '@emnapi/core': 1.7.1 3519 + '@emnapi/runtime': 1.7.1 3520 + '@tybys/wasm-util': 0.10.1 3521 + optional: true 3522 + 2539 3523 '@nodelib/fs.scandir@2.1.5': 2540 3524 dependencies: 2541 3525 '@nodelib/fs.stat': 2.0.5 ··· 2548 3532 '@nodelib/fs.scandir': 2.1.5 2549 3533 fastq: 1.19.1 2550 3534 2551 - '@oxc-project/runtime@0.72.2': {} 2552 - 2553 3535 '@oxc-project/runtime@0.75.0': {} 2554 3536 2555 3537 '@oxc-project/runtime@0.75.1': {} 2556 3538 2557 - '@oxc-project/types@0.72.2': {} 3539 + '@oxc-project/types@0.75.1': {} 3540 + 3541 + '@oxc-project/types@0.95.0': {} 3542 + 3543 + '@oxc-resolver/binding-android-arm-eabi@11.15.0': 3544 + optional: true 3545 + 3546 + '@oxc-resolver/binding-android-arm64@11.15.0': 3547 + optional: true 3548 + 3549 + '@oxc-resolver/binding-darwin-arm64@11.15.0': 3550 + optional: true 3551 + 3552 + '@oxc-resolver/binding-darwin-x64@11.15.0': 3553 + optional: true 3554 + 3555 + '@oxc-resolver/binding-freebsd-x64@11.15.0': 3556 + optional: true 3557 + 3558 + '@oxc-resolver/binding-linux-arm-gnueabihf@11.15.0': 3559 + optional: true 3560 + 3561 + '@oxc-resolver/binding-linux-arm-musleabihf@11.15.0': 3562 + optional: true 3563 + 3564 + '@oxc-resolver/binding-linux-arm64-gnu@11.15.0': 3565 + optional: true 3566 + 3567 + '@oxc-resolver/binding-linux-arm64-musl@11.15.0': 3568 + optional: true 3569 + 3570 + '@oxc-resolver/binding-linux-ppc64-gnu@11.15.0': 3571 + optional: true 3572 + 3573 + '@oxc-resolver/binding-linux-riscv64-gnu@11.15.0': 3574 + optional: true 3575 + 3576 + '@oxc-resolver/binding-linux-riscv64-musl@11.15.0': 3577 + optional: true 3578 + 3579 + '@oxc-resolver/binding-linux-s390x-gnu@11.15.0': 3580 + optional: true 3581 + 3582 + '@oxc-resolver/binding-linux-x64-gnu@11.15.0': 3583 + optional: true 3584 + 3585 + '@oxc-resolver/binding-linux-x64-musl@11.15.0': 3586 + optional: true 2558 3587 2559 - '@oxc-project/types@0.75.1': {} 3588 + '@oxc-resolver/binding-openharmony-arm64@11.15.0': 3589 + optional: true 3590 + 3591 + '@oxc-resolver/binding-wasm32-wasi@11.15.0': 3592 + dependencies: 3593 + '@napi-rs/wasm-runtime': 1.1.0 3594 + optional: true 3595 + 3596 + '@oxc-resolver/binding-win32-arm64-msvc@11.15.0': 3597 + optional: true 3598 + 3599 + '@oxc-resolver/binding-win32-ia32-msvc@11.15.0': 3600 + optional: true 3601 + 3602 + '@oxc-resolver/binding-win32-x64-msvc@11.15.0': 3603 + optional: true 2560 3604 2561 3605 '@prettier/sync@0.5.5(prettier@3.5.3)': 2562 3606 dependencies: ··· 2567 3611 dependencies: 2568 3612 quansync: 0.2.11 2569 3613 2570 - '@rolldown/binding-darwin-arm64@1.0.0-beta.11-commit.f051675': 3614 + '@rolldown/binding-android-arm64@1.0.0-beta.45': 2571 3615 optional: true 2572 3616 2573 3617 '@rolldown/binding-darwin-arm64@1.0.0-beta.24': 2574 3618 optional: true 2575 3619 2576 - '@rolldown/binding-darwin-x64@1.0.0-beta.11-commit.f051675': 3620 + '@rolldown/binding-darwin-arm64@1.0.0-beta.45': 2577 3621 optional: true 2578 3622 2579 3623 '@rolldown/binding-darwin-x64@1.0.0-beta.24': 2580 3624 optional: true 2581 3625 2582 - '@rolldown/binding-freebsd-x64@1.0.0-beta.11-commit.f051675': 3626 + '@rolldown/binding-darwin-x64@1.0.0-beta.45': 2583 3627 optional: true 2584 3628 2585 3629 '@rolldown/binding-freebsd-x64@1.0.0-beta.24': 2586 3630 optional: true 2587 3631 2588 - '@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.11-commit.f051675': 3632 + '@rolldown/binding-freebsd-x64@1.0.0-beta.45': 2589 3633 optional: true 2590 3634 2591 3635 '@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.24': 2592 3636 optional: true 2593 3637 2594 - '@rolldown/binding-linux-arm64-gnu@1.0.0-beta.11-commit.f051675': 3638 + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.45': 2595 3639 optional: true 2596 3640 2597 3641 '@rolldown/binding-linux-arm64-gnu@1.0.0-beta.24': 2598 3642 optional: true 2599 3643 2600 - '@rolldown/binding-linux-arm64-musl@1.0.0-beta.11-commit.f051675': 3644 + '@rolldown/binding-linux-arm64-gnu@1.0.0-beta.45': 2601 3645 optional: true 2602 3646 2603 3647 '@rolldown/binding-linux-arm64-musl@1.0.0-beta.24': 2604 3648 optional: true 2605 3649 2606 - '@rolldown/binding-linux-x64-gnu@1.0.0-beta.11-commit.f051675': 3650 + '@rolldown/binding-linux-arm64-musl@1.0.0-beta.45': 2607 3651 optional: true 2608 3652 2609 3653 '@rolldown/binding-linux-x64-gnu@1.0.0-beta.24': 2610 3654 optional: true 2611 3655 2612 - '@rolldown/binding-linux-x64-musl@1.0.0-beta.11-commit.f051675': 3656 + '@rolldown/binding-linux-x64-gnu@1.0.0-beta.45': 2613 3657 optional: true 2614 3658 2615 3659 '@rolldown/binding-linux-x64-musl@1.0.0-beta.24': 2616 3660 optional: true 2617 3661 2618 - '@rolldown/binding-wasm32-wasi@1.0.0-beta.11-commit.f051675': 2619 - dependencies: 2620 - '@napi-rs/wasm-runtime': 0.2.12 3662 + '@rolldown/binding-linux-x64-musl@1.0.0-beta.45': 3663 + optional: true 3664 + 3665 + '@rolldown/binding-openharmony-arm64@1.0.0-beta.45': 2621 3666 optional: true 2622 3667 2623 3668 '@rolldown/binding-wasm32-wasi@1.0.0-beta.24': ··· 2625 3670 '@napi-rs/wasm-runtime': 0.2.12 2626 3671 optional: true 2627 3672 2628 - '@rolldown/binding-win32-arm64-msvc@1.0.0-beta.11-commit.f051675': 3673 + '@rolldown/binding-wasm32-wasi@1.0.0-beta.45': 3674 + dependencies: 3675 + '@napi-rs/wasm-runtime': 1.0.7 2629 3676 optional: true 2630 3677 2631 3678 '@rolldown/binding-win32-arm64-msvc@1.0.0-beta.24': 2632 3679 optional: true 2633 3680 2634 - '@rolldown/binding-win32-ia32-msvc@1.0.0-beta.11-commit.f051675': 3681 + '@rolldown/binding-win32-arm64-msvc@1.0.0-beta.45': 2635 3682 optional: true 2636 3683 2637 3684 '@rolldown/binding-win32-ia32-msvc@1.0.0-beta.24': 2638 3685 optional: true 2639 3686 2640 - '@rolldown/binding-win32-x64-msvc@1.0.0-beta.11-commit.f051675': 3687 + '@rolldown/binding-win32-ia32-msvc@1.0.0-beta.45': 2641 3688 optional: true 2642 3689 2643 3690 '@rolldown/binding-win32-x64-msvc@1.0.0-beta.24': 2644 3691 optional: true 2645 3692 2646 - '@rolldown/pluginutils@1.0.0-beta.11-commit.f051675': {} 3693 + '@rolldown/binding-win32-x64-msvc@1.0.0-beta.45': 3694 + optional: true 2647 3695 2648 3696 '@rolldown/pluginutils@1.0.0-beta.24': {} 2649 3697 2650 - '@rolldown/pluginutils@1.0.0-beta.38': {} 3698 + '@rolldown/pluginutils@1.0.0-beta.45': {} 3699 + 3700 + '@rolldown/pluginutils@1.0.0-rc.2': {} 2651 3701 2652 3702 '@standard-schema/spec@1.0.0': {} 2653 3703 3704 + '@tailwindcss/node@4.1.18': 3705 + dependencies: 3706 + '@jridgewell/remapping': 2.3.5 3707 + enhanced-resolve: 5.18.3 3708 + jiti: 2.6.1 3709 + lightningcss: 1.30.2 3710 + magic-string: 0.30.21 3711 + source-map-js: 1.2.1 3712 + tailwindcss: 4.1.18 3713 + 3714 + '@tailwindcss/oxide-android-arm64@4.1.18': 3715 + optional: true 3716 + 3717 + '@tailwindcss/oxide-darwin-arm64@4.1.18': 3718 + optional: true 3719 + 3720 + '@tailwindcss/oxide-darwin-x64@4.1.18': 3721 + optional: true 3722 + 3723 + '@tailwindcss/oxide-freebsd-x64@4.1.18': 3724 + optional: true 3725 + 3726 + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.18': 3727 + optional: true 3728 + 3729 + '@tailwindcss/oxide-linux-arm64-gnu@4.1.18': 3730 + optional: true 3731 + 3732 + '@tailwindcss/oxide-linux-arm64-musl@4.1.18': 3733 + optional: true 3734 + 3735 + '@tailwindcss/oxide-linux-x64-gnu@4.1.18': 3736 + optional: true 3737 + 3738 + '@tailwindcss/oxide-linux-x64-musl@4.1.18': 3739 + optional: true 3740 + 3741 + '@tailwindcss/oxide-wasm32-wasi@4.1.18': 3742 + optional: true 3743 + 3744 + '@tailwindcss/oxide-win32-arm64-msvc@4.1.18': 3745 + optional: true 3746 + 3747 + '@tailwindcss/oxide-win32-x64-msvc@4.1.18': 3748 + optional: true 3749 + 3750 + '@tailwindcss/oxide@4.1.18': 3751 + optionalDependencies: 3752 + '@tailwindcss/oxide-android-arm64': 4.1.18 3753 + '@tailwindcss/oxide-darwin-arm64': 4.1.18 3754 + '@tailwindcss/oxide-darwin-x64': 4.1.18 3755 + '@tailwindcss/oxide-freebsd-x64': 4.1.18 3756 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.18 3757 + '@tailwindcss/oxide-linux-arm64-gnu': 4.1.18 3758 + '@tailwindcss/oxide-linux-arm64-musl': 4.1.18 3759 + '@tailwindcss/oxide-linux-x64-gnu': 4.1.18 3760 + '@tailwindcss/oxide-linux-x64-musl': 4.1.18 3761 + '@tailwindcss/oxide-wasm32-wasi': 4.1.18 3762 + '@tailwindcss/oxide-win32-arm64-msvc': 4.1.18 3763 + '@tailwindcss/oxide-win32-x64-msvc': 4.1.18 3764 + 3765 + '@tailwindcss/vite@4.1.18(rolldown-vite@7.0.6(@types/node@24.0.4)(esbuild@0.25.10)(jiti@2.6.1))': 3766 + dependencies: 3767 + '@tailwindcss/node': 4.1.18 3768 + '@tailwindcss/oxide': 4.1.18 3769 + tailwindcss: 4.1.18 3770 + vite: rolldown-vite@7.0.6(@types/node@24.0.4)(esbuild@0.25.10)(jiti@2.6.1) 3771 + 2654 3772 '@testing-library/dom@10.4.1': 2655 3773 dependencies: 2656 3774 '@babel/code-frame': 7.27.1 ··· 2671 3789 picocolors: 1.1.1 2672 3790 redent: 3.0.0 2673 3791 2674 - '@testing-library/react@16.3.0(@testing-library/dom@10.4.1)(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': 3792 + '@testing-library/react@16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': 2675 3793 dependencies: 2676 3794 '@babel/runtime': 7.28.4 2677 3795 '@testing-library/dom': 10.4.1 2678 - react: 19.2.0 2679 - react-dom: 19.2.0(react@19.2.0) 3796 + react: 19.2.4 3797 + react-dom: 19.2.4(react@19.2.4) 2680 3798 optionalDependencies: 2681 - '@types/react': 19.2.2 2682 - '@types/react-dom': 19.2.2(@types/react@19.2.2) 3799 + '@types/react': 19.2.13 3800 + '@types/react-dom': 19.2.3(@types/react@19.2.13) 2683 3801 2684 3802 '@testing-library/user-event@14.6.1(@testing-library/dom@10.4.1)': 2685 3803 dependencies: ··· 2694 3812 2695 3813 '@types/babel__core@7.20.5': 2696 3814 dependencies: 2697 - '@babel/parser': 7.28.4 2698 - '@babel/types': 7.28.4 3815 + '@babel/parser': 7.28.5 3816 + '@babel/types': 7.28.5 2699 3817 '@types/babel__generator': 7.27.0 2700 3818 '@types/babel__template': 7.4.4 2701 3819 '@types/babel__traverse': 7.28.0 2702 3820 2703 3821 '@types/babel__generator@7.27.0': 2704 3822 dependencies: 2705 - '@babel/types': 7.28.4 3823 + '@babel/types': 7.28.5 2706 3824 2707 3825 '@types/babel__template@7.4.4': 2708 3826 dependencies: 2709 - '@babel/parser': 7.28.4 2710 - '@babel/types': 7.28.4 3827 + '@babel/parser': 7.28.5 3828 + '@babel/types': 7.28.5 2711 3829 2712 3830 '@types/babel__traverse@7.28.0': 2713 3831 dependencies: 2714 - '@babel/types': 7.28.4 3832 + '@babel/types': 7.28.5 2715 3833 2716 3834 '@types/chai@5.2.2': 2717 3835 dependencies: ··· 2723 3841 2724 3842 '@types/json-schema@7.0.15': {} 2725 3843 3844 + '@types/node@12.20.55': {} 3845 + 2726 3846 '@types/node@24.0.4': 2727 3847 dependencies: 2728 3848 undici-types: 7.8.0 2729 3849 2730 - '@types/react-dom@19.2.2(@types/react@19.2.2)': 3850 + '@types/react-dom@19.2.3(@types/react@19.2.13)': 2731 3851 dependencies: 2732 - '@types/react': 19.2.2 3852 + '@types/react': 19.2.13 2733 3853 2734 - '@types/react@19.2.2': 3854 + '@types/react@19.2.13': 2735 3855 dependencies: 2736 - csstype: 3.1.3 3856 + csstype: 3.2.3 2737 3857 2738 - '@typescript-eslint/eslint-plugin@8.35.0(@typescript-eslint/parser@8.35.0(eslint@9.29.0(jiti@2.6.1))(typescript@5.8.3))(eslint@9.29.0(jiti@2.6.1))(typescript@5.8.3)': 3858 + '@typescript-eslint/eslint-plugin@8.35.0(@typescript-eslint/parser@8.35.0(eslint@9.29.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.29.0(jiti@2.6.1))(typescript@5.9.3)': 2739 3859 dependencies: 2740 3860 '@eslint-community/regexpp': 4.12.1 2741 - '@typescript-eslint/parser': 8.35.0(eslint@9.29.0(jiti@2.6.1))(typescript@5.8.3) 3861 + '@typescript-eslint/parser': 8.35.0(eslint@9.29.0(jiti@2.6.1))(typescript@5.9.3) 2742 3862 '@typescript-eslint/scope-manager': 8.35.0 2743 - '@typescript-eslint/type-utils': 8.35.0(eslint@9.29.0(jiti@2.6.1))(typescript@5.8.3) 2744 - '@typescript-eslint/utils': 8.35.0(eslint@9.29.0(jiti@2.6.1))(typescript@5.8.3) 3863 + '@typescript-eslint/type-utils': 8.35.0(eslint@9.29.0(jiti@2.6.1))(typescript@5.9.3) 3864 + '@typescript-eslint/utils': 8.35.0(eslint@9.29.0(jiti@2.6.1))(typescript@5.9.3) 2745 3865 '@typescript-eslint/visitor-keys': 8.35.0 2746 3866 eslint: 9.29.0(jiti@2.6.1) 2747 3867 graphemer: 1.4.0 2748 3868 ignore: 7.0.5 2749 3869 natural-compare: 1.4.0 2750 - ts-api-utils: 2.1.0(typescript@5.8.3) 2751 - typescript: 5.8.3 3870 + ts-api-utils: 2.1.0(typescript@5.9.3) 3871 + typescript: 5.9.3 2752 3872 transitivePeerDependencies: 2753 3873 - supports-color 2754 3874 2755 - '@typescript-eslint/parser@8.35.0(eslint@9.29.0(jiti@2.6.1))(typescript@5.8.3)': 3875 + '@typescript-eslint/parser@8.35.0(eslint@9.29.0(jiti@2.6.1))(typescript@5.9.3)': 2756 3876 dependencies: 2757 3877 '@typescript-eslint/scope-manager': 8.35.0 2758 3878 '@typescript-eslint/types': 8.35.0 2759 - '@typescript-eslint/typescript-estree': 8.35.0(typescript@5.8.3) 3879 + '@typescript-eslint/typescript-estree': 8.35.0(typescript@5.9.3) 2760 3880 '@typescript-eslint/visitor-keys': 8.35.0 2761 3881 debug: 4.4.3 2762 3882 eslint: 9.29.0(jiti@2.6.1) 2763 - typescript: 5.8.3 3883 + typescript: 5.9.3 2764 3884 transitivePeerDependencies: 2765 3885 - supports-color 2766 3886 2767 - '@typescript-eslint/project-service@8.35.0(typescript@5.8.3)': 3887 + '@typescript-eslint/project-service@8.35.0(typescript@5.9.3)': 2768 3888 dependencies: 2769 - '@typescript-eslint/tsconfig-utils': 8.35.0(typescript@5.8.3) 3889 + '@typescript-eslint/tsconfig-utils': 8.35.0(typescript@5.9.3) 2770 3890 '@typescript-eslint/types': 8.35.0 2771 3891 debug: 4.4.3 2772 - typescript: 5.8.3 3892 + typescript: 5.9.3 2773 3893 transitivePeerDependencies: 2774 3894 - supports-color 2775 3895 ··· 2778 3898 '@typescript-eslint/types': 8.35.0 2779 3899 '@typescript-eslint/visitor-keys': 8.35.0 2780 3900 2781 - '@typescript-eslint/tsconfig-utils@8.35.0(typescript@5.8.3)': 3901 + '@typescript-eslint/tsconfig-utils@8.35.0(typescript@5.9.3)': 2782 3902 dependencies: 2783 - typescript: 5.8.3 3903 + typescript: 5.9.3 2784 3904 2785 - '@typescript-eslint/type-utils@8.35.0(eslint@9.29.0(jiti@2.6.1))(typescript@5.8.3)': 3905 + '@typescript-eslint/type-utils@8.35.0(eslint@9.29.0(jiti@2.6.1))(typescript@5.9.3)': 2786 3906 dependencies: 2787 - '@typescript-eslint/typescript-estree': 8.35.0(typescript@5.8.3) 2788 - '@typescript-eslint/utils': 8.35.0(eslint@9.29.0(jiti@2.6.1))(typescript@5.8.3) 3907 + '@typescript-eslint/typescript-estree': 8.35.0(typescript@5.9.3) 3908 + '@typescript-eslint/utils': 8.35.0(eslint@9.29.0(jiti@2.6.1))(typescript@5.9.3) 2789 3909 debug: 4.4.3 2790 3910 eslint: 9.29.0(jiti@2.6.1) 2791 - ts-api-utils: 2.1.0(typescript@5.8.3) 2792 - typescript: 5.8.3 3911 + ts-api-utils: 2.1.0(typescript@5.9.3) 3912 + typescript: 5.9.3 2793 3913 transitivePeerDependencies: 2794 3914 - supports-color 2795 3915 2796 3916 '@typescript-eslint/types@8.35.0': {} 2797 3917 2798 - '@typescript-eslint/typescript-estree@8.35.0(typescript@5.8.3)': 3918 + '@typescript-eslint/typescript-estree@8.35.0(typescript@5.9.3)': 2799 3919 dependencies: 2800 - '@typescript-eslint/project-service': 8.35.0(typescript@5.8.3) 2801 - '@typescript-eslint/tsconfig-utils': 8.35.0(typescript@5.8.3) 3920 + '@typescript-eslint/project-service': 8.35.0(typescript@5.9.3) 3921 + '@typescript-eslint/tsconfig-utils': 8.35.0(typescript@5.9.3) 2802 3922 '@typescript-eslint/types': 8.35.0 2803 3923 '@typescript-eslint/visitor-keys': 8.35.0 2804 3924 debug: 4.4.3 ··· 2806 3926 is-glob: 4.0.3 2807 3927 minimatch: 9.0.5 2808 3928 semver: 7.7.3 2809 - ts-api-utils: 2.1.0(typescript@5.8.3) 2810 - typescript: 5.8.3 3929 + ts-api-utils: 2.1.0(typescript@5.9.3) 3930 + typescript: 5.9.3 2811 3931 transitivePeerDependencies: 2812 3932 - supports-color 2813 3933 2814 - '@typescript-eslint/utils@8.35.0(eslint@9.29.0(jiti@2.6.1))(typescript@5.8.3)': 3934 + '@typescript-eslint/utils@8.35.0(eslint@9.29.0(jiti@2.6.1))(typescript@5.9.3)': 2815 3935 dependencies: 2816 3936 '@eslint-community/eslint-utils': 4.9.0(eslint@9.29.0(jiti@2.6.1)) 2817 3937 '@typescript-eslint/scope-manager': 8.35.0 2818 3938 '@typescript-eslint/types': 8.35.0 2819 - '@typescript-eslint/typescript-estree': 8.35.0(typescript@5.8.3) 3939 + '@typescript-eslint/typescript-estree': 8.35.0(typescript@5.9.3) 2820 3940 eslint: 9.29.0(jiti@2.6.1) 2821 - typescript: 5.8.3 3941 + typescript: 5.9.3 2822 3942 transitivePeerDependencies: 2823 3943 - supports-color 2824 3944 ··· 2838 3958 treeify: 1.1.0 2839 3959 yargs: 16.2.0 2840 3960 2841 - '@typescript/vfs@1.6.1(typescript@5.8.3)': 3961 + '@typescript/vfs@1.6.1(typescript@5.9.3)': 2842 3962 dependencies: 2843 3963 debug: 4.4.3 2844 - typescript: 5.8.3 3964 + typescript: 5.9.3 2845 3965 transitivePeerDependencies: 2846 3966 - supports-color 2847 3967 2848 - '@vitejs/plugin-react@5.0.4(rolldown-vite@7.0.6(@types/node@24.0.4)(esbuild@0.25.10)(jiti@2.6.1))': 3968 + '@vitejs/plugin-react@5.1.3(rolldown-vite@7.0.6(@types/node@24.0.4)(esbuild@0.25.10)(jiti@2.6.1))': 2849 3969 dependencies: 2850 - '@babel/core': 7.28.4 2851 - '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.4) 2852 - '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.28.4) 2853 - '@rolldown/pluginutils': 1.0.0-beta.38 3970 + '@babel/core': 7.29.0 3971 + '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.29.0) 3972 + '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.29.0) 3973 + '@rolldown/pluginutils': 1.0.0-rc.2 2854 3974 '@types/babel__core': 7.20.5 2855 - react-refresh: 0.17.0 3975 + react-refresh: 0.18.0 2856 3976 vite: rolldown-vite@7.0.6(@types/node@24.0.4)(esbuild@0.25.10)(jiti@2.6.1) 2857 3977 transitivePeerDependencies: 2858 3978 - supports-color ··· 2914 4034 json-schema-traverse: 0.4.1 2915 4035 uri-js: 4.4.1 2916 4036 4037 + ansi-colors@4.1.3: {} 4038 + 2917 4039 ansi-regex@5.0.1: {} 2918 4040 2919 4041 ansi-styles@4.3.0: ··· 2924 4046 2925 4047 ansis@4.2.0: {} 2926 4048 4049 + argparse@1.0.10: 4050 + dependencies: 4051 + sprintf-js: 1.0.3 4052 + 2927 4053 argparse@2.0.1: {} 2928 4054 2929 4055 aria-query@5.3.0: ··· 2934 4060 dependencies: 2935 4061 '@ark/schema': 0.49.0 2936 4062 '@ark/util': 0.49.0 4063 + 4064 + array-union@2.1.0: {} 2937 4065 2938 4066 assertion-error@2.0.1: {} 2939 4067 2940 - ast-kit@2.1.3: 4068 + ast-kit@2.2.0: 2941 4069 dependencies: 2942 - '@babel/parser': 7.28.4 4070 + '@babel/parser': 7.28.5 2943 4071 pathe: 2.0.3 2944 4072 2945 4073 asynckit@0.4.0: {} ··· 2952 4080 2953 4081 baseline-browser-mapping@2.8.18: {} 2954 4082 2955 - birpc@2.6.1: {} 4083 + better-path-resolve@1.0.0: 4084 + dependencies: 4085 + is-windows: 1.0.2 4086 + 4087 + birpc@2.8.0: {} 2956 4088 2957 4089 brace-expansion@1.1.12: 2958 4090 dependencies: ··· 2998 4130 dependencies: 2999 4131 ansi-styles: 4.3.0 3000 4132 supports-color: 7.2.0 4133 + 4134 + chardet@2.1.0: {} 3001 4135 3002 4136 check-error@2.1.1: {} 3003 4137 ··· 3005 4139 dependencies: 3006 4140 readdirp: 4.1.2 3007 4141 4142 + ci-info@3.9.0: {} 4143 + 3008 4144 cliui@7.0.4: 3009 4145 dependencies: 3010 4146 string-width: 4.2.3 ··· 3038 4174 '@asamuzakjp/css-color': 3.2.0 3039 4175 rrweb-cssom: 0.8.0 3040 4176 3041 - csstype@3.1.3: {} 4177 + csstype@3.2.3: {} 3042 4178 3043 4179 data-urls@5.0.0: 3044 4180 dependencies: ··· 3061 4197 3062 4198 dequal@2.0.3: {} 3063 4199 4200 + detect-indent@6.1.0: {} 4201 + 3064 4202 detect-libc@2.1.2: {} 3065 4203 3066 4204 diff@8.0.2: {} 3067 4205 4206 + dir-glob@3.0.1: 4207 + dependencies: 4208 + path-type: 4.0.0 4209 + 3068 4210 dom-accessibility-api@0.5.16: {} 3069 4211 3070 4212 dom-accessibility-api@0.6.3: {} 3071 4213 3072 - dts-resolver@2.1.2: {} 4214 + dts-resolver@2.1.3(oxc-resolver@11.15.0): 4215 + optionalDependencies: 4216 + oxc-resolver: 11.15.0 3073 4217 3074 4218 dunder-proto@1.0.1: 3075 4219 dependencies: ··· 3081 4225 3082 4226 emoji-regex@8.0.0: {} 3083 4227 3084 - empathic@1.1.0: {} 4228 + empathic@2.0.0: {} 4229 + 4230 + enhanced-resolve@5.18.3: 4231 + dependencies: 4232 + graceful-fs: 4.2.11 4233 + tapable: 2.3.0 4234 + 4235 + enquirer@2.4.1: 4236 + dependencies: 4237 + ansi-colors: 4.1.3 4238 + strip-ansi: 6.0.1 3085 4239 3086 4240 entities@6.0.1: {} 3087 4241 ··· 3205 4359 acorn-jsx: 5.3.2(acorn@8.15.0) 3206 4360 eslint-visitor-keys: 4.2.1 3207 4361 4362 + esprima@4.0.1: {} 4363 + 3208 4364 esquery@1.6.0: 3209 4365 dependencies: 3210 4366 estraverse: 5.3.0 ··· 3225 4381 3226 4382 expect-type@1.2.2: {} 3227 4383 4384 + extendable-error@0.1.7: {} 4385 + 3228 4386 fast-deep-equal@3.1.3: {} 3229 4387 3230 4388 fast-glob@3.3.3: ··· 3243 4401 dependencies: 3244 4402 reusify: 1.1.0 3245 4403 4404 + fd-package-json@2.0.0: 4405 + dependencies: 4406 + walk-up-path: 4.0.0 4407 + 3246 4408 fdir@6.5.0(picomatch@4.0.3): 3247 4409 optionalDependencies: 3248 4410 picomatch: 4.0.3 ··· 3255 4417 dependencies: 3256 4418 to-regex-range: 5.0.1 3257 4419 4420 + find-up@4.1.0: 4421 + dependencies: 4422 + locate-path: 5.0.0 4423 + path-exists: 4.0.0 4424 + 3258 4425 find-up@5.0.0: 3259 4426 dependencies: 3260 4427 locate-path: 6.0.0 ··· 3275 4442 hasown: 2.0.2 3276 4443 mime-types: 2.1.35 3277 4444 4445 + formatly@0.3.0: 4446 + dependencies: 4447 + fd-package-json: 2.0.0 4448 + 4449 + fs-extra@7.0.1: 4450 + dependencies: 4451 + graceful-fs: 4.2.11 4452 + jsonfile: 4.0.0 4453 + universalify: 0.1.2 4454 + 4455 + fs-extra@8.1.0: 4456 + dependencies: 4457 + graceful-fs: 4.2.11 4458 + jsonfile: 4.0.0 4459 + universalify: 0.1.2 4460 + 3278 4461 fsevents@2.3.3: 3279 4462 optional: true 3280 4463 ··· 3302 4485 dunder-proto: 1.0.1 3303 4486 es-object-atoms: 1.1.1 3304 4487 3305 - get-tsconfig@4.12.0: 4488 + get-tsconfig@4.13.0: 3306 4489 dependencies: 3307 4490 resolve-pkg-maps: 1.0.0 3308 4491 ··· 3316 4499 3317 4500 globals@14.0.0: {} 3318 4501 4502 + globby@11.1.0: 4503 + dependencies: 4504 + array-union: 2.1.0 4505 + dir-glob: 3.0.1 4506 + fast-glob: 3.3.3 4507 + ignore: 5.3.2 4508 + merge2: 1.4.1 4509 + slash: 3.0.0 4510 + 3319 4511 gopd@1.2.0: {} 4512 + 4513 + graceful-fs@4.2.11: {} 3320 4514 3321 4515 graphemer@1.4.0: {} 3322 4516 ··· 3358 4552 transitivePeerDependencies: 3359 4553 - supports-color 3360 4554 4555 + human-id@4.1.2: {} 4556 + 3361 4557 iconv-lite@0.6.3: 3362 4558 dependencies: 3363 4559 safer-buffer: 2.1.2 3364 4560 4561 + iconv-lite@0.7.0: 4562 + dependencies: 4563 + safer-buffer: 2.1.2 4564 + 3365 4565 ignore@5.3.2: {} 3366 4566 3367 4567 ignore@7.0.5: {} ··· 3389 4589 3390 4590 is-potential-custom-element-name@1.0.1: {} 3391 4591 4592 + is-subdir@1.2.0: 4593 + dependencies: 4594 + better-path-resolve: 1.0.0 4595 + 4596 + is-windows@1.0.2: {} 4597 + 3392 4598 isexe@2.0.0: {} 4599 + 4600 + iso-datestring-validator@2.2.2: {} 3393 4601 3394 4602 jiti@2.6.1: {} 3395 4603 ··· 3397 4605 3398 4606 js-tokens@9.0.1: {} 3399 4607 4608 + js-yaml@3.14.1: 4609 + dependencies: 4610 + argparse: 1.0.10 4611 + esprima: 4.0.1 4612 + 3400 4613 js-yaml@4.1.0: 4614 + dependencies: 4615 + argparse: 2.0.1 4616 + 4617 + js-yaml@4.1.1: 3401 4618 dependencies: 3402 4619 argparse: 2.0.1 3403 4620 ··· 3439 4656 3440 4657 json5@2.2.3: {} 3441 4658 4659 + jsonfile@4.0.0: 4660 + optionalDependencies: 4661 + graceful-fs: 4.2.11 4662 + 3442 4663 jsonparse@1.3.1: {} 3443 4664 3444 4665 jsonstream-next@3.0.0: ··· 3450 4671 dependencies: 3451 4672 json-buffer: 3.0.1 3452 4673 4674 + knip@5.83.1(@types/node@24.0.4)(typescript@5.9.3): 4675 + dependencies: 4676 + '@nodelib/fs.walk': 1.2.8 4677 + '@types/node': 24.0.4 4678 + fast-glob: 3.3.3 4679 + formatly: 0.3.0 4680 + jiti: 2.6.1 4681 + js-yaml: 4.1.1 4682 + minimist: 1.2.8 4683 + oxc-resolver: 11.15.0 4684 + picocolors: 1.1.1 4685 + picomatch: 4.0.3 4686 + smol-toml: 1.5.2 4687 + strip-json-comments: 5.0.3 4688 + typescript: 5.9.3 4689 + zod: 4.1.12 4690 + 3453 4691 levn@0.4.1: 3454 4692 dependencies: 3455 4693 prelude-ls: 1.2.1 ··· 3504 4742 lightningcss-win32-arm64-msvc: 1.30.2 3505 4743 lightningcss-win32-x64-msvc: 1.30.2 3506 4744 4745 + locate-path@5.0.0: 4746 + dependencies: 4747 + p-locate: 4.1.0 4748 + 3507 4749 locate-path@6.0.0: 3508 4750 dependencies: 3509 4751 p-locate: 5.0.0 3510 4752 3511 4753 lodash.merge@4.6.2: {} 3512 4754 4755 + lodash.startcase@4.4.0: {} 4756 + 3513 4757 loupe@3.2.1: {} 3514 4758 3515 4759 lru-cache@10.4.3: {} ··· 3521 4765 lz-string@1.5.0: {} 3522 4766 3523 4767 magic-string@0.30.19: 4768 + dependencies: 4769 + '@jridgewell/sourcemap-codec': 1.5.5 4770 + 4771 + magic-string@0.30.21: 3524 4772 dependencies: 3525 4773 '@jridgewell/sourcemap-codec': 1.5.5 3526 4774 ··· 3551 4799 dependencies: 3552 4800 brace-expansion: 2.0.2 3553 4801 4802 + minimist@1.2.8: {} 4803 + 3554 4804 monaco-editor@0.52.0: {} 3555 4805 3556 4806 mri@1.2.0: {} 3557 4807 3558 4808 ms@2.1.3: {} 3559 4809 4810 + multiformats@9.9.0: {} 4811 + 3560 4812 nanoid@3.3.11: {} 3561 4813 3562 4814 natural-compare@1.4.0: {} 3563 4815 3564 4816 node-releases@2.0.25: {} 3565 4817 3566 - nuqs@2.7.2(react@19.2.0): 4818 + nuqs@2.8.8(react@19.2.4): 3567 4819 dependencies: 3568 4820 '@standard-schema/spec': 1.0.0 3569 - react: 19.2.0 4821 + react: 19.2.4 3570 4822 3571 4823 nwsapi@2.2.22: {} 3572 4824 4825 + obug@1.0.0(ms@2.1.3): 4826 + optionalDependencies: 4827 + ms: 2.1.3 4828 + 3573 4829 optionator@0.9.4: 3574 4830 dependencies: 3575 4831 deep-is: 0.1.4 ··· 3579 4835 type-check: 0.4.0 3580 4836 word-wrap: 1.2.5 3581 4837 4838 + outdent@0.5.0: {} 4839 + 4840 + oxc-resolver@11.15.0: 4841 + optionalDependencies: 4842 + '@oxc-resolver/binding-android-arm-eabi': 11.15.0 4843 + '@oxc-resolver/binding-android-arm64': 11.15.0 4844 + '@oxc-resolver/binding-darwin-arm64': 11.15.0 4845 + '@oxc-resolver/binding-darwin-x64': 11.15.0 4846 + '@oxc-resolver/binding-freebsd-x64': 11.15.0 4847 + '@oxc-resolver/binding-linux-arm-gnueabihf': 11.15.0 4848 + '@oxc-resolver/binding-linux-arm-musleabihf': 11.15.0 4849 + '@oxc-resolver/binding-linux-arm64-gnu': 11.15.0 4850 + '@oxc-resolver/binding-linux-arm64-musl': 11.15.0 4851 + '@oxc-resolver/binding-linux-ppc64-gnu': 11.15.0 4852 + '@oxc-resolver/binding-linux-riscv64-gnu': 11.15.0 4853 + '@oxc-resolver/binding-linux-riscv64-musl': 11.15.0 4854 + '@oxc-resolver/binding-linux-s390x-gnu': 11.15.0 4855 + '@oxc-resolver/binding-linux-x64-gnu': 11.15.0 4856 + '@oxc-resolver/binding-linux-x64-musl': 11.15.0 4857 + '@oxc-resolver/binding-openharmony-arm64': 11.15.0 4858 + '@oxc-resolver/binding-wasm32-wasi': 11.15.0 4859 + '@oxc-resolver/binding-win32-arm64-msvc': 11.15.0 4860 + '@oxc-resolver/binding-win32-ia32-msvc': 11.15.0 4861 + '@oxc-resolver/binding-win32-x64-msvc': 11.15.0 4862 + 4863 + p-filter@2.1.0: 4864 + dependencies: 4865 + p-map: 2.1.0 4866 + 4867 + p-limit@2.3.0: 4868 + dependencies: 4869 + p-try: 2.2.0 4870 + 3582 4871 p-limit@3.1.0: 3583 4872 dependencies: 3584 4873 yocto-queue: 0.1.0 3585 4874 4875 + p-locate@4.1.0: 4876 + dependencies: 4877 + p-limit: 2.3.0 4878 + 3586 4879 p-locate@5.0.0: 3587 4880 dependencies: 3588 4881 p-limit: 3.1.0 3589 4882 4883 + p-map@2.1.0: {} 4884 + 4885 + p-try@2.2.0: {} 4886 + 4887 + package-manager-detector@0.2.11: 4888 + dependencies: 4889 + quansync: 0.2.11 4890 + 3590 4891 parent-module@1.0.1: 3591 4892 dependencies: 3592 4893 callsites: 3.1.0 ··· 3599 4900 3600 4901 path-key@3.1.1: {} 3601 4902 4903 + path-type@4.0.0: {} 4904 + 3602 4905 pathe@2.0.3: {} 3603 4906 3604 4907 pathval@2.0.1: {} ··· 3609 4912 3610 4913 picomatch@4.0.3: {} 3611 4914 4915 + pify@4.0.1: {} 4916 + 3612 4917 postcss@8.5.6: 3613 4918 dependencies: 3614 4919 nanoid: 3.3.11 ··· 3616 4921 source-map-js: 1.2.1 3617 4922 3618 4923 prelude-ls@1.2.1: {} 4924 + 4925 + prettier@2.8.8: {} 3619 4926 3620 4927 prettier@3.5.3: {} 3621 4928 ··· 3633 4940 3634 4941 queue-microtask@1.2.3: {} 3635 4942 3636 - react-dom@19.2.0(react@19.2.0): 4943 + react-dom@19.2.4(react@19.2.4): 3637 4944 dependencies: 3638 - react: 19.2.0 4945 + react: 19.2.4 3639 4946 scheduler: 0.27.0 3640 4947 3641 4948 react-is@17.0.2: {} 3642 4949 3643 - react-refresh@0.17.0: {} 4950 + react-refresh@0.18.0: {} 4951 + 4952 + react@19.2.4: {} 3644 4953 3645 - react@19.2.0: {} 4954 + read-yaml-file@1.1.0: 4955 + dependencies: 4956 + graceful-fs: 4.2.11 4957 + js-yaml: 3.14.1 4958 + pify: 4.0.1 4959 + strip-bom: 3.0.0 3646 4960 3647 4961 readable-stream@3.6.2: 3648 4962 dependencies: ··· 3661 4975 3662 4976 resolve-from@4.0.0: {} 3663 4977 4978 + resolve-from@5.0.0: {} 4979 + 3664 4980 resolve-pkg-maps@1.0.0: {} 3665 4981 3666 4982 reusify@1.1.0: {} 3667 4983 3668 - rolldown-plugin-dts@0.13.14(rolldown@1.0.0-beta.11-commit.f051675)(typescript@5.8.3): 4984 + rolldown-plugin-dts@0.17.7(ms@2.1.3)(oxc-resolver@11.15.0)(rolldown@1.0.0-beta.45)(typescript@5.9.3): 3669 4985 dependencies: 3670 - '@babel/generator': 7.28.3 3671 - '@babel/parser': 7.28.4 3672 - '@babel/types': 7.28.4 3673 - ast-kit: 2.1.3 3674 - birpc: 2.6.1 3675 - debug: 4.4.3 3676 - dts-resolver: 2.1.2 3677 - get-tsconfig: 4.12.0 3678 - rolldown: 1.0.0-beta.11-commit.f051675 4986 + '@babel/generator': 7.28.5 4987 + '@babel/parser': 7.28.5 4988 + '@babel/types': 7.28.5 4989 + ast-kit: 2.2.0 4990 + birpc: 2.8.0 4991 + dts-resolver: 2.1.3(oxc-resolver@11.15.0) 4992 + get-tsconfig: 4.13.0 4993 + magic-string: 0.30.21 4994 + obug: 1.0.0(ms@2.1.3) 4995 + rolldown: 1.0.0-beta.45 3679 4996 optionalDependencies: 3680 - typescript: 5.8.3 4997 + typescript: 5.9.3 3681 4998 transitivePeerDependencies: 4999 + - ms 3682 5000 - oxc-resolver 3683 - - supports-color 3684 5001 3685 5002 rolldown-vite@7.0.6(@types/node@24.0.4)(esbuild@0.25.10)(jiti@2.6.1): 3686 5003 dependencies: ··· 3697 5014 fsevents: 2.3.3 3698 5015 jiti: 2.6.1 3699 5016 3700 - rolldown@1.0.0-beta.11-commit.f051675: 3701 - dependencies: 3702 - '@oxc-project/runtime': 0.72.2 3703 - '@oxc-project/types': 0.72.2 3704 - '@rolldown/pluginutils': 1.0.0-beta.11-commit.f051675 3705 - ansis: 4.2.0 3706 - optionalDependencies: 3707 - '@rolldown/binding-darwin-arm64': 1.0.0-beta.11-commit.f051675 3708 - '@rolldown/binding-darwin-x64': 1.0.0-beta.11-commit.f051675 3709 - '@rolldown/binding-freebsd-x64': 1.0.0-beta.11-commit.f051675 3710 - '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-beta.11-commit.f051675 3711 - '@rolldown/binding-linux-arm64-gnu': 1.0.0-beta.11-commit.f051675 3712 - '@rolldown/binding-linux-arm64-musl': 1.0.0-beta.11-commit.f051675 3713 - '@rolldown/binding-linux-x64-gnu': 1.0.0-beta.11-commit.f051675 3714 - '@rolldown/binding-linux-x64-musl': 1.0.0-beta.11-commit.f051675 3715 - '@rolldown/binding-wasm32-wasi': 1.0.0-beta.11-commit.f051675 3716 - '@rolldown/binding-win32-arm64-msvc': 1.0.0-beta.11-commit.f051675 3717 - '@rolldown/binding-win32-ia32-msvc': 1.0.0-beta.11-commit.f051675 3718 - '@rolldown/binding-win32-x64-msvc': 1.0.0-beta.11-commit.f051675 3719 - 3720 5017 rolldown@1.0.0-beta.24: 3721 5018 dependencies: 3722 5019 '@oxc-project/runtime': 0.75.1 ··· 3737 5034 '@rolldown/binding-win32-ia32-msvc': 1.0.0-beta.24 3738 5035 '@rolldown/binding-win32-x64-msvc': 1.0.0-beta.24 3739 5036 5037 + rolldown@1.0.0-beta.45: 5038 + dependencies: 5039 + '@oxc-project/types': 0.95.0 5040 + '@rolldown/pluginutils': 1.0.0-beta.45 5041 + optionalDependencies: 5042 + '@rolldown/binding-android-arm64': 1.0.0-beta.45 5043 + '@rolldown/binding-darwin-arm64': 1.0.0-beta.45 5044 + '@rolldown/binding-darwin-x64': 1.0.0-beta.45 5045 + '@rolldown/binding-freebsd-x64': 1.0.0-beta.45 5046 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-beta.45 5047 + '@rolldown/binding-linux-arm64-gnu': 1.0.0-beta.45 5048 + '@rolldown/binding-linux-arm64-musl': 1.0.0-beta.45 5049 + '@rolldown/binding-linux-x64-gnu': 1.0.0-beta.45 5050 + '@rolldown/binding-linux-x64-musl': 1.0.0-beta.45 5051 + '@rolldown/binding-openharmony-arm64': 1.0.0-beta.45 5052 + '@rolldown/binding-wasm32-wasi': 1.0.0-beta.45 5053 + '@rolldown/binding-win32-arm64-msvc': 1.0.0-beta.45 5054 + '@rolldown/binding-win32-ia32-msvc': 1.0.0-beta.45 5055 + '@rolldown/binding-win32-x64-msvc': 1.0.0-beta.45 5056 + 3740 5057 rrweb-cssom@0.7.1: {} 3741 5058 3742 5059 rrweb-cssom@0.8.0: {} ··· 3771 5088 3772 5089 siginfo@2.0.0: {} 3773 5090 5091 + signal-exit@4.1.0: {} 5092 + 5093 + slash@3.0.0: {} 5094 + 5095 + smol-toml@1.5.2: {} 5096 + 3774 5097 source-map-js@1.2.1: {} 3775 5098 5099 + spawndamnit@3.0.1: 5100 + dependencies: 5101 + cross-spawn: 7.0.6 5102 + signal-exit: 4.1.0 5103 + 3776 5104 split2@3.2.2: 3777 5105 dependencies: 3778 5106 readable-stream: 3.6.2 5107 + 5108 + sprintf-js@1.0.3: {} 3779 5109 3780 5110 stackback@0.0.2: {} 3781 5111 ··· 3797 5127 dependencies: 3798 5128 ansi-regex: 5.0.1 3799 5129 5130 + strip-bom@3.0.0: {} 5131 + 3800 5132 strip-indent@3.0.0: 3801 5133 dependencies: 3802 5134 min-indent: 1.0.1 3803 5135 3804 5136 strip-json-comments@3.1.1: {} 3805 5137 5138 + strip-json-comments@5.0.3: {} 5139 + 3806 5140 strip-literal@3.1.0: 3807 5141 dependencies: 3808 5142 js-tokens: 9.0.1 ··· 3812 5146 has-flag: 4.0.0 3813 5147 3814 5148 symbol-tree@3.2.4: {} 5149 + 5150 + tailwindcss@4.1.18: {} 5151 + 5152 + tapable@2.3.0: {} 5153 + 5154 + term-size@2.2.1: {} 3815 5155 3816 5156 through2@4.0.2: 3817 5157 dependencies: ··· 3821 5161 3822 5162 tinyexec@0.3.2: {} 3823 5163 3824 - tinyexec@1.0.1: {} 5164 + tinyexec@1.0.2: {} 3825 5165 3826 5166 tinyglobby@0.2.15: 3827 5167 dependencies: ··· 3852 5192 dependencies: 3853 5193 punycode: 2.3.1 3854 5194 5195 + tree-kill@1.2.2: {} 5196 + 3855 5197 treeify@1.1.0: {} 3856 5198 3857 - ts-api-utils@2.1.0(typescript@5.8.3): 5199 + ts-api-utils@2.1.0(typescript@5.9.3): 3858 5200 dependencies: 3859 - typescript: 5.8.3 5201 + typescript: 5.9.3 3860 5202 3861 - tsdown@0.12.7(typescript@5.8.3): 5203 + tsdown@0.15.12(ms@2.1.3)(oxc-resolver@11.15.0)(typescript@5.9.3): 3862 5204 dependencies: 3863 5205 ansis: 4.2.0 3864 5206 cac: 6.7.14 3865 5207 chokidar: 4.0.3 3866 5208 debug: 4.4.3 3867 5209 diff: 8.0.2 3868 - empathic: 1.1.0 5210 + empathic: 2.0.0 3869 5211 hookable: 5.5.3 3870 - rolldown: 1.0.0-beta.11-commit.f051675 3871 - rolldown-plugin-dts: 0.13.14(rolldown@1.0.0-beta.11-commit.f051675)(typescript@5.8.3) 5212 + rolldown: 1.0.0-beta.45 5213 + rolldown-plugin-dts: 0.17.7(ms@2.1.3)(oxc-resolver@11.15.0)(rolldown@1.0.0-beta.45)(typescript@5.9.3) 3872 5214 semver: 7.7.3 3873 - tinyexec: 1.0.1 5215 + tinyexec: 1.0.2 3874 5216 tinyglobby: 0.2.15 3875 - unconfig: 7.3.3 5217 + tree-kill: 1.2.2 5218 + unconfig: 7.4.1 3876 5219 optionalDependencies: 3877 - typescript: 5.8.3 5220 + typescript: 5.9.3 3878 5221 transitivePeerDependencies: 5222 + - '@ts-macro/tsc' 3879 5223 - '@typescript/native-preview' 5224 + - ms 3880 5225 - oxc-resolver 3881 5226 - supports-color 3882 5227 - vue-tsc 3883 5228 3884 - tslib@2.8.1: 3885 - optional: true 5229 + tslib@2.8.1: {} 3886 5230 3887 5231 type-check@0.4.0: 3888 5232 dependencies: 3889 5233 prelude-ls: 1.2.1 3890 5234 3891 - typescript-eslint@8.35.0(eslint@9.29.0(jiti@2.6.1))(typescript@5.8.3): 5235 + typescript-eslint@8.35.0(eslint@9.29.0(jiti@2.6.1))(typescript@5.9.3): 3892 5236 dependencies: 3893 - '@typescript-eslint/eslint-plugin': 8.35.0(@typescript-eslint/parser@8.35.0(eslint@9.29.0(jiti@2.6.1))(typescript@5.8.3))(eslint@9.29.0(jiti@2.6.1))(typescript@5.8.3) 3894 - '@typescript-eslint/parser': 8.35.0(eslint@9.29.0(jiti@2.6.1))(typescript@5.8.3) 3895 - '@typescript-eslint/utils': 8.35.0(eslint@9.29.0(jiti@2.6.1))(typescript@5.8.3) 5237 + '@typescript-eslint/eslint-plugin': 8.35.0(@typescript-eslint/parser@8.35.0(eslint@9.29.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.29.0(jiti@2.6.1))(typescript@5.9.3) 5238 + '@typescript-eslint/parser': 8.35.0(eslint@9.29.0(jiti@2.6.1))(typescript@5.9.3) 5239 + '@typescript-eslint/utils': 8.35.0(eslint@9.29.0(jiti@2.6.1))(typescript@5.9.3) 3896 5240 eslint: 9.29.0(jiti@2.6.1) 3897 - typescript: 5.8.3 5241 + typescript: 5.9.3 3898 5242 transitivePeerDependencies: 3899 5243 - supports-color 3900 5244 3901 - typescript@5.8.3: {} 5245 + typescript@5.9.3: {} 3902 5246 3903 - unconfig@7.3.3: 5247 + uint8arrays@3.0.0: 5248 + dependencies: 5249 + multiformats: 9.9.0 5250 + 5251 + unconfig-core@7.4.1: 5252 + dependencies: 5253 + '@quansync/fs': 0.1.5 5254 + quansync: 0.2.11 5255 + 5256 + unconfig@7.4.1: 3904 5257 dependencies: 3905 5258 '@quansync/fs': 0.1.5 3906 5259 defu: 6.1.4 3907 5260 jiti: 2.6.1 3908 5261 quansync: 0.2.11 5262 + unconfig-core: 7.4.1 3909 5263 3910 5264 undici-types@7.8.0: {} 5265 + 5266 + unicode-segmenter@0.14.0: {} 5267 + 5268 + universalify@0.1.2: {} 3911 5269 3912 5270 update-browserslist-db@1.1.3(browserslist@4.26.3): 3913 5271 dependencies: ··· 3988 5346 dependencies: 3989 5347 xml-name-validator: 5.0.0 3990 5348 5349 + walk-up-path@4.0.0: {} 5350 + 3991 5351 webidl-conversions@7.0.0: {} 3992 5352 3993 5353 whatwg-encoding@3.1.1: ··· 4047 5407 zod: 3.25.76 4048 5408 4049 5409 zod@3.25.76: {} 5410 + 5411 + zod@4.1.12: {}