diff --git a/IMPROVEMENTS.md b/IMPROVEMENTS.md new file mode 100644 index 0000000..fa2eda9 --- /dev/null +++ b/IMPROVEMENTS.md @@ -0,0 +1,133 @@ +# Typlex Emitter Improvement Plan + +## Philosophy +Keep TypeSpec-first design. Use native TypeSpec features where possible, add Lexicon-specific decorators only where necessary. Ensure all schemas can potentially work with other emitters. + +## Immediate Improvements (High Impact) + +### 1. Use TypeSpec stdlib `@minItems`/`@maxItems` ✅ +**Current:** Using custom `@maxItems` import from compiler +**Change:** Import and use from TypeSpec standard library consistently +**Files:** +- `src/emitter.ts` - Update imports +- Remove custom implementation if any +**Test Impact:** None (already working) + +### 2. Add Default Value Support +**Current:** TypeSpec default syntax (`name?: string = "Rex"`) not mapped +**Change:** Emit Lexicon `default` field from TypeSpec default values +**Files:** +- `src/emitter.ts` - Read default values from ModelProperty +- Check for `prop.default` and emit to lexicon +**Test Impact:** Need tests with defaults (but won't match existing lexicons yet) +**Lexicon Support:** boolean, integer, string have `default` field + +### 3. Add Enum Constraint Support +**Current:** No way to express closed enums on primitives +**Change:** Detect TypeSpec enum types and emit Lexicon `enum` field +**Files:** +- `src/emitter.ts` - Check if type is Enum, extract values +**Test Impact:** Need new test scenarios +**Lexicon Support:** integer, string have `enum` field (array of values) + +### 4. Add Nullable Support +**Current:** Not handling `T | null` unions +**Change:** Detect unions with null, emit to `nullable` array on objects +**Files:** +- `src/emitter.ts` - Check union variants for null type +- Add `nullable` field to object schema +**Test Impact:** Need new test scenarios +**Lexicon Support:** objects have `nullable: string[]` field + +## High Value Improvements + +### 5. Provide Pre-defined Format Scalars +**Current:** Only `@lexFormat("did")` - not type-safe +**Change:** Add pre-defined scalars in lib/decorators.tsp: +```typespec +scalar did extends string; +scalar handle extends string; +scalar atUri extends string; +scalar datetime extends string; +scalar cid extends string; +scalar tid extends string; +scalar nsid extends string; +scalar recordKey extends string; +scalar uri extends string; +scalar language extends string; +``` +**Files:** +- `lib/decorators.tsp` - Add scalar definitions +- `src/emitter.ts` - Detect these scalars and map to format automatically +**Test Impact:** Can start using in new tests +**Note:** Keep `@lexFormat` for backward compatibility and flexibility + +### 6. Add Const Support for String/Integer +**Current:** Only `@lexConst` for boolean +**Change:** Extend to support string and integer const values +**Files:** +- `lib/decorators.tsp` - Make `@lexConst` accept any value +- `src/decorators.ts` - Handle string/integer +- `src/emitter.ts` - Emit const on string/integer primitives +**Test Impact:** Need new test scenarios +**Lexicon Support:** string, integer, boolean all support `const` + +## Nice to Have (Lower Priority) + +### 7. Native DateTime Type Mapping +**Current:** `@lexFormat("datetime")` on string +**Enhancement:** Optionally map `utcDateTime` → `format: "datetime"` +**Note:** Lexicon only has one datetime format, TypeSpec has multiple +**Verdict:** Lower priority, explicit `@lexFormat` is clearer + +### 8. Token Type Support +**When needed:** Implement token pattern +**Likely approach:** `@token` decorator on model or scalar +**Not implementing yet:** Wait until we port a lexicon that uses tokens + +## Future Major Features + +### 9. XRPC Query/Procedure/Subscription Support +**Approach:** Use TypeSpec operations with `@typespec/http` decorators +**Example:** +```typespec +import "@typespec/http"; + +@route("/xrpc/app.bsky.actor.getProfile") +@get +op getProfile(@query actor: atIdentifier): ProfileViewDetailed; +``` +**Not implementing yet:** Focus on record types first + +## Implementation Order + +### Phase 1: Core Improvements (Keep tests green) +1. ✅ Stdlib `@minItems`/`@maxItems` - Quick win, no test changes +2. Default values - Add support, test separately +3. Enum constraints - Add support, test separately +4. Nullable support - Add support, test separately + +### Phase 2: Developer Experience +5. Pre-defined format scalars - Big DX improvement +6. Extend const to string/integer - Complete the feature + +### Phase 3: Future (when needed) +7. Native datetime mapping +8. Token types +9. XRPC operations + +## Testing Strategy + +- Keep ALL existing tests green +- Don't modify output fixtures +- Add NEW test scenarios for new features +- Test features in isolation first +- Combine features once all work independently + +## Success Criteria + +- All 23 existing tests remain green +- New features have test coverage +- Code is more idiomatic TypeSpec +- Better developer experience with type-safe scalars +- Can still emit identical JSON to atproto/lexicons for ported schemas diff --git a/packages/cli/src/cli.ts b/packages/cli/src/cli.ts index c4fdeb2..f4919d7 100644 --- a/packages/cli/src/cli.ts +++ b/packages/cli/src/cli.ts @@ -7,23 +7,18 @@ async function main() { const args = process.argv.slice(2); if (args.length === 0) { - console.error("Usage: typlex "); + console.error("Usage: typlex "); console.error("Compiles TypeSpec lexicon definitions to JSON"); process.exit(1); } - const tspPath = resolve(process.cwd(), args[0]); + const entrypoint = resolve(process.cwd(), args[0]); const host = NodeHost; try { - const program = await compile(host, tspPath, { - emit: ["@typlex/emitter"], - options: { - "@typlex/emitter": { - "output-dir": resolve(process.cwd(), "lexicons"), - }, - }, - }); + // The compile function will automatically find and load tspconfig.yaml + // when given a project directory or file path + const program = await compile(host, entrypoint); if (program.diagnostics.length > 0) { logDiagnostics(program.diagnostics, host.logSink); @@ -33,7 +28,7 @@ async function main() { process.exit(1); } - console.log("✓ Lexicons compiled successfully to ./lexicons"); + console.log("✓ Lexicons compiled successfully"); } catch (error) { console.error("Error compiling lexicons:", error); process.exit(1); diff --git a/packages/emitter/src/emitter.ts b/packages/emitter/src/emitter.ts index cb22a73..8a75dbe 100644 --- a/packages/emitter/src/emitter.ts +++ b/packages/emitter/src/emitter.ts @@ -526,6 +526,19 @@ export class TyplexEmitter { if (constValue !== undefined && primitive.type === "boolean") { (primitive as any).const = constValue; } + + // Check for default values (supported on string, integer, boolean) + const defaultValue = (prop as any).default; + if (defaultValue && (defaultValue as any).value !== undefined) { + const value = (defaultValue as any).value; + if (primitive.type === "string" && typeof value === "string") { + (primitive as any).default = value; + } else if (primitive.type === "integer" && typeof value === "number") { + (primitive as any).default = value; + } else if (primitive.type === "boolean" && typeof value === "boolean") { + (primitive as any).default = value; + } + } } // Add minimum constraint for integer/number types diff --git a/packages/emitter/src/index.ts b/packages/emitter/src/index.ts index 313257c..4117783 100644 --- a/packages/emitter/src/index.ts +++ b/packages/emitter/src/index.ts @@ -6,8 +6,11 @@ export interface TyplexEmitterOptions { } export async function $onEmit(context: EmitContext) { + // Use custom output-dir from options if provided, otherwise use emitterOutputDir + const outputDir = context.options["output-dir"] || context.emitterOutputDir; + const emitter = new TyplexEmitter(context.program, { - outputDir: context.emitterOutputDir, + outputDir, }); await emitter.emit(); diff --git a/packages/example/@typlex/emitter/com/example/simple/post.json b/packages/example/@typlex/emitter/com/example/simple/post.json deleted file mode 100644 index 4ddb9a4..0000000 --- a/packages/example/@typlex/emitter/com/example/simple/post.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "lexicon": 1, - "id": "com.example.simple.post", - "defs": { - "main": { - "type": "object", - "required": [ - "text", - "createdAt" - ], - "properties": { - "text": { - "type": "string" - }, - "createdAt": { - "type": "string", - "format": "datetime" - } - } - } - }, - "description": "A simple post record" -} diff --git a/packages/example/lexicon/app/example/defs.json b/packages/example/lexicon/app/example/defs.json new file mode 100644 index 0000000..0bf39cb --- /dev/null +++ b/packages/example/lexicon/app/example/defs.json @@ -0,0 +1,72 @@ +{ + "lexicon": 1, + "id": "app.example.defs", + "defs": { + "postRef": { + "type": "object", + "description": "Reference to a post", + "required": [ + "uri", + "cid" + ], + "properties": { + "uri": { + "type": "string", + "description": "AT URI of the post" + }, + "cid": { + "type": "string", + "description": "CID of the post" + } + } + }, + "replyRef": { + "type": "object", + "description": "Reference to a parent post in a reply chain", + "required": [ + "root", + "parent" + ], + "properties": { + "root": { + "type": "ref", + "ref": "#postRef", + "description": "Root post in the thread" + }, + "parent": { + "type": "ref", + "ref": "#postRef", + "description": "Direct parent post being replied to" + } + } + }, + "entity": { + "type": "object", + "description": "Text entity (mention, link, or tag)", + "required": [ + "start", + "end", + "type", + "value" + ], + "properties": { + "start": { + "type": "integer", + "description": "Start index in text" + }, + "end": { + "type": "integer", + "description": "End index in text" + }, + "type": { + "type": "string", + "description": "Entity type" + }, + "value": { + "type": "string", + "description": "Entity value (handle, URL, or tag)" + } + } + } + } +} diff --git a/packages/example/lexicon/app/example/entity.json b/packages/example/lexicon/app/example/entity.json deleted file mode 100644 index e6df469..0000000 --- a/packages/example/lexicon/app/example/entity.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "lexicon": 1, - "id": "app.example.entity", - "defs": { - "main": { - "type": "record", - "key": "tid", - "record": { - "type": "object", - "properties": { - "start": { - "type": "integer", - "description": "Start index in text" - }, - "end": { - "type": "integer", - "description": "End index in text" - }, - "value": { - "type": "string", - "description": "Entity value (handle, URL, or tag)" - } - }, - "required": [ - "start", - "end", - "type", - "value" - ] - }, - "description": "Entity mentioned or linked in a post" - } - } -} \ No newline at end of file diff --git a/packages/example/lexicon/app/example/follow.json b/packages/example/lexicon/app/example/follow.json index 136b222..5390841 100644 --- a/packages/example/lexicon/app/example/follow.json +++ b/packages/example/lexicon/app/example/follow.json @@ -3,27 +3,23 @@ "id": "app.example.follow", "defs": { "main": { - "type": "record", - "key": "tid", - "record": { - "type": "object", - "properties": { - "subject": { - "type": "string", - "description": "DID of the account being followed" - }, - "createdAt": { - "type": "string", - "format": "datetime", - "description": "When the follow was created" - } + "type": "object", + "required": [ + "subject", + "createdAt" + ], + "properties": { + "subject": { + "type": "string", + "description": "DID of the account being followed" }, - "required": [ - "subject", - "createdAt" - ] - }, - "description": "A follow relationship" + "createdAt": { + "type": "string", + "format": "datetime", + "description": "When the follow was created" + } + } } - } -} \ No newline at end of file + }, + "description": "A follow relationship" +} diff --git a/packages/example/lexicon/app/example/like.json b/packages/example/lexicon/app/example/like.json index b7d6c65..77739ac 100644 --- a/packages/example/lexicon/app/example/like.json +++ b/packages/example/lexicon/app/example/like.json @@ -3,41 +3,24 @@ "id": "app.example.like", "defs": { "main": { - "type": "record", - "key": "tid", - "record": { - "type": "object", - "properties": { - "subject": { - "type": "object", - "properties": { - "uri": { - "type": "string", - "description": "AT URI of the post" - }, - "cid": { - "type": "string", - "description": "CID of the post" - } - }, - "description": "The post being liked", - "required": [ - "uri", - "cid" - ] - }, - "createdAt": { - "type": "string", - "format": "datetime", - "description": "When the like was created" - } + "type": "object", + "required": [ + "subject", + "createdAt" + ], + "properties": { + "subject": { + "type": "ref", + "ref": "app.example.defs#postRef", + "description": "Post being liked" }, - "required": [ - "subject", - "createdAt" - ] - }, - "description": "A like on a post" + "createdAt": { + "type": "string", + "format": "datetime", + "description": "When the like was created" + } + } } - } -} \ No newline at end of file + }, + "description": "A like on a post" +} diff --git a/packages/example/lexicon/app/example/post.json b/packages/example/lexicon/app/example/post.json index b32ec97..e96feff 100644 --- a/packages/example/lexicon/app/example/post.json +++ b/packages/example/lexicon/app/example/post.json @@ -3,108 +3,43 @@ "id": "app.example.post", "defs": { "main": { - "type": "record", - "key": "tid", - "record": { - "type": "object", - "properties": { - "text": { - "type": "string", - "description": "Post text content" - }, - "createdAt": { - "type": "string", - "format": "datetime", - "description": "Creation timestamp" - }, - "langs": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Languages the post is written in" + "type": "object", + "required": [ + "text", + "createdAt" + ], + "properties": { + "text": { + "type": "string", + "description": "Post text content" + }, + "createdAt": { + "type": "string", + "format": "datetime", + "description": "Creation timestamp" + }, + "langs": { + "type": "array", + "items": { + "type": "string" }, - "entities": { - "type": "array", - "items": { - "type": "object", - "properties": { - "start": { - "type": "integer", - "description": "Start index in text" - }, - "end": { - "type": "integer", - "description": "End index in text" - }, - "value": { - "type": "string", - "description": "Entity value (handle, URL, or tag)" - } - }, - "description": "Entity mentioned or linked in a post", - "required": [ - "start", - "end", - "type", - "value" - ] - }, - "description": "Referenced entities in the post" + "description": "Languages the post is written in" + }, + "entities": { + "type": "array", + "items": { + "type": "ref", + "ref": "app.example.defs#entity" }, - "reply": { - "type": "object", - "properties": { - "root": { - "type": "object", - "properties": { - "uri": { - "type": "string", - "description": "AT URI of the post" - }, - "cid": { - "type": "string", - "description": "CID of the post" - } - }, - "description": "Root post in thread", - "required": [ - "uri", - "cid" - ] - }, - "parent": { - "type": "object", - "properties": { - "uri": { - "type": "string", - "description": "AT URI of the post" - }, - "cid": { - "type": "string", - "description": "CID of the post" - } - }, - "description": "Direct parent being replied to", - "required": [ - "uri", - "cid" - ] - } - }, - "description": "Post the user is replying to", - "required": [ - "root", - "parent" - ] - } + "description": "Referenced entities in the post" }, - "required": [ - "text", - "createdAt" - ] - }, - "description": "A post in the feed" + "reply": { + "type": "ref", + "ref": "app.example.defs#replyRef", + "description": "Post the user is replying to" + } + } } - } -} \ No newline at end of file + }, + "description": "A post in the feed" +} diff --git a/packages/example/lexicon/app/example/postRef.json b/packages/example/lexicon/app/example/postRef.json deleted file mode 100644 index 2ed3baa..0000000 --- a/packages/example/lexicon/app/example/postRef.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "lexicon": 1, - "id": "app.example.postRef", - "defs": { - "main": { - "type": "record", - "key": "tid", - "record": { - "type": "object", - "properties": { - "uri": { - "type": "string", - "description": "AT URI of the post" - }, - "cid": { - "type": "string", - "description": "CID of the post" - } - }, - "required": [ - "uri", - "cid" - ] - }, - "description": "Reference to a post" - } - } -} \ No newline at end of file diff --git a/packages/example/lexicon/app/example/profile.json b/packages/example/lexicon/app/example/profile.json index 0c4a69e..bd2d5d9 100644 --- a/packages/example/lexicon/app/example/profile.json +++ b/packages/example/lexicon/app/example/profile.json @@ -3,48 +3,26 @@ "id": "app.example.profile", "defs": { "main": { - "type": "record", - "key": "tid", - "record": { - "type": "object", - "properties": { - "did": { - "type": "string", - "description": "Decentralized identifier" - }, - "handle": { - "type": "string", - "description": "Handle identifier" - }, - "displayName": { - "type": "string", - "description": "Display name shown in UI" - }, - "description": { - "type": "string", - "description": "User biography" - }, - "avatar": { - "type": "string", - "description": "Avatar image URL" - }, - "banner": { - "type": "string", - "description": "Banner image URL" - }, - "createdAt": { - "type": "string", - "format": "datetime", - "description": "When the profile was created" - } + "type": "object", + "properties": { + "displayName": { + "type": "string", + "description": "Display name" }, - "required": [ - "did", - "handle", - "createdAt" - ] - }, - "description": "User profile information" + "description": { + "type": "string", + "description": "Profile description" + }, + "avatar": { + "type": "string", + "description": "Profile avatar image" + }, + "banner": { + "type": "string", + "description": "Profile banner image" + } + } } - } -} \ No newline at end of file + }, + "description": "User profile information" +} diff --git a/packages/example/lexicon/app/example/replyRef.json b/packages/example/lexicon/app/example/replyRef.json deleted file mode 100644 index 2c9bae7..0000000 --- a/packages/example/lexicon/app/example/replyRef.json +++ /dev/null @@ -1,56 +0,0 @@ -{ - "lexicon": 1, - "id": "app.example.replyRef", - "defs": { - "main": { - "type": "record", - "key": "tid", - "record": { - "type": "object", - "properties": { - "root": { - "type": "object", - "properties": { - "uri": { - "type": "string", - "description": "AT URI of the post" - }, - "cid": { - "type": "string", - "description": "CID of the post" - } - }, - "description": "Root post in thread", - "required": [ - "uri", - "cid" - ] - }, - "parent": { - "type": "object", - "properties": { - "uri": { - "type": "string", - "description": "AT URI of the post" - }, - "cid": { - "type": "string", - "description": "CID of the post" - } - }, - "description": "Direct parent being replied to", - "required": [ - "uri", - "cid" - ] - } - }, - "required": [ - "root", - "parent" - ] - }, - "description": "Reference to the post being replied to" - } - } -} \ No newline at end of file diff --git a/packages/example/lexicon/app/example/repost.json b/packages/example/lexicon/app/example/repost.json index ac915f5..5ca59b1 100644 --- a/packages/example/lexicon/app/example/repost.json +++ b/packages/example/lexicon/app/example/repost.json @@ -3,41 +3,24 @@ "id": "app.example.repost", "defs": { "main": { - "type": "record", - "key": "tid", - "record": { - "type": "object", - "properties": { - "subject": { - "type": "object", - "properties": { - "uri": { - "type": "string", - "description": "AT URI of the post" - }, - "cid": { - "type": "string", - "description": "CID of the post" - } - }, - "description": "The post being reposted", - "required": [ - "uri", - "cid" - ] - }, - "createdAt": { - "type": "string", - "format": "datetime", - "description": "When the repost was created" - } + "type": "object", + "required": [ + "subject", + "createdAt" + ], + "properties": { + "subject": { + "type": "ref", + "ref": "app.example.defs#postRef", + "description": "Post being reposted" }, - "required": [ - "subject", - "createdAt" - ] - }, - "description": "A repost of another post" + "createdAt": { + "type": "string", + "format": "datetime", + "description": "When the repost was created" + } + } } - } -} \ No newline at end of file + }, + "description": "A repost of another post" +} diff --git a/packages/example/package.json b/packages/example/package.json index 2e8b64e..51d9b7b 100644 --- a/packages/example/package.json +++ b/packages/example/package.json @@ -4,10 +4,12 @@ "private": true, "type": "module", "scripts": { - "build": "typlex typlex/main.tsp" + "build": "pnpm run build:lexicon && pnpm run build:codegen", + "build:lexicon": "tsp compile typlex/main.tsp", + "build:codegen": "lex gen-server --yes ./src ./lexicon" }, "dependencies": { - "@atproto/lex-cli": "^0.0.6", + "@atproto/lex-cli": "^0.9.5", "@typespec/compiler": "^0.64.0", "@typlex/emitter": "workspace:*", "@typlex/cli": "workspace:*" diff --git a/packages/example/src/index.ts b/packages/example/src/index.ts new file mode 100644 index 0000000..0a36a07 --- /dev/null +++ b/packages/example/src/index.ts @@ -0,0 +1,24 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import { + type Auth, + type Options as XrpcOptions, + Server as XrpcServer, + type StreamConfigOrHandler, + type MethodConfigOrHandler, + createServer as createXrpcServer, +} from '@atproto/xrpc-server' +import { schemas } from './lexicons.js' + +export function createServer(options?: XrpcOptions): Server { + return new Server(options) +} + +export class Server { + xrpc: XrpcServer + + constructor(options?: XrpcOptions) { + this.xrpc = createXrpcServer(schemas, options) + } +} diff --git a/packages/example/src/lexicons.ts b/packages/example/src/lexicons.ts new file mode 100644 index 0000000..3942991 --- /dev/null +++ b/packages/example/src/lexicons.ts @@ -0,0 +1,44 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import { + type LexiconDoc, + Lexicons, + ValidationError, + type ValidationResult, +} from '@atproto/lexicon' +import { type $Typed, is$typed, maybe$typed } from './util.js' + +export const schemaDict = {} as const satisfies Record +export const schemas = Object.values(schemaDict) satisfies LexiconDoc[] +export const lexicons: Lexicons = new Lexicons(schemas) + +export function validate( + v: unknown, + id: string, + hash: string, + requiredType: true, +): ValidationResult +export function validate( + v: unknown, + id: string, + hash: string, + requiredType?: false, +): ValidationResult +export function validate( + v: unknown, + id: string, + hash: string, + requiredType?: boolean, +): ValidationResult { + return (requiredType ? is$typed : maybe$typed)(v, id, hash) + ? lexicons.validate(`${id}#${hash}`, v) + : { + success: false, + error: new ValidationError( + `Must be an object with "${hash === 'main' ? id : `${id}#${hash}`}" $type property`, + ), + } +} + +export const ids = {} as const diff --git a/packages/example/src/util.ts b/packages/example/src/util.ts new file mode 100644 index 0000000..816ca10 --- /dev/null +++ b/packages/example/src/util.ts @@ -0,0 +1,82 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ + +import { type ValidationResult } from '@atproto/lexicon' + +export type OmitKey = { + [K2 in keyof T as K2 extends K ? never : K2]: T[K2] +} + +export type $Typed = V & { $type: T } +export type Un$Typed = OmitKey + +export type $Type = Hash extends 'main' + ? Id + : `${Id}#${Hash}` + +function isObject(v: V): v is V & object { + return v != null && typeof v === 'object' +} + +function is$type( + $type: unknown, + id: Id, + hash: Hash, +): $type is $Type { + return hash === 'main' + ? $type === id + : // $type === `${id}#${hash}` + typeof $type === 'string' && + $type.length === id.length + 1 + hash.length && + $type.charCodeAt(id.length) === 35 /* '#' */ && + $type.startsWith(id) && + $type.endsWith(hash) +} + +export type $TypedObject< + V, + Id extends string, + Hash extends string, +> = V extends { + $type: $Type +} + ? V + : V extends { $type?: string } + ? V extends { $type?: infer T extends $Type } + ? V & { $type: T } + : never + : V & { $type: $Type } + +export function is$typed( + v: V, + id: Id, + hash: Hash, +): v is $TypedObject { + return isObject(v) && '$type' in v && is$type(v.$type, id, hash) +} + +export function maybe$typed( + v: V, + id: Id, + hash: Hash, +): v is V & object & { $type?: $Type } { + return ( + isObject(v) && + ('$type' in v ? v.$type === undefined || is$type(v.$type, id, hash) : true) + ) +} + +export type Validator = (v: unknown) => ValidationResult +export type ValidatorParam = + V extends Validator ? R : never + +/** + * Utility function that allows to convert a "validate*" utility function into a + * type predicate. + */ +export function asPredicate(validate: V) { + return function (v: T): v is T & ValidatorParam { + return validate(v).success + } +} diff --git a/packages/example/tspconfig.yaml b/packages/example/tspconfig.yaml index ca5ce22..a2cadc7 100644 --- a/packages/example/tspconfig.yaml +++ b/packages/example/tspconfig.yaml @@ -2,4 +2,4 @@ emit: - "@typlex/emitter" options: "@typlex/emitter": - output-dir: "./lexicons" + output-dir: "./lexicon" diff --git a/packages/example/typlex/main.tsp b/packages/example/typlex/main.tsp index 9c147af..aa6e68e 100644 --- a/packages/example/typlex/main.tsp +++ b/packages/example/typlex/main.tsp @@ -1,13 +1,110 @@ import "@typlex/emitter"; -// Simple example showing typlex can be used before atproto lexicon CLI -namespace com.example.simple { +// Example showing typlex as source of truth for atproto lexicons +namespace app.example { + // ============ Common Types ============ + + @doc("Reference to a post") + model PostRef { + @doc("AT URI of the post") + uri: string; + + @doc("CID of the post") + cid: string; + } + + @doc("Reference to a parent post in a reply chain") + model ReplyRef { + @doc("Root post in the thread") + root: PostRef; + + @doc("Direct parent post being replied to") + parent: PostRef; + } + + @doc("Text entity (mention, link, or tag)") + model Entity { + @doc("Start index in text") + start: int32; + + @doc("End index in text") + end: int32; + + @doc("Entity type") + type: string; + + @doc("Entity value (handle, URL, or tag)") + value: string; + } + + // ============ Records ============ + @lexiconMain - @doc("A simple post record") + @doc("A post in the feed") model Post { + @doc("Post text content") text: string; @lexFormat("datetime") + @doc("Creation timestamp") + createdAt: string; + + @doc("Languages the post is written in") + langs?: string[]; + + @doc("Referenced entities in the post") + entities?: Entity[]; + + @doc("Post the user is replying to") + reply?: ReplyRef; + } + + @lexiconMain + @doc("A follow relationship") + model Follow { + @doc("DID of the account being followed") + subject: string; + + @lexFormat("datetime") + @doc("When the follow was created") + createdAt: string; + } + + @lexiconMain + @doc("A like on a post") + model Like { + @doc("Post being liked") + subject: PostRef; + + @lexFormat("datetime") + @doc("When the like was created") + createdAt: string; + } + + @lexiconMain + @doc("A repost of another post") + model Repost { + @doc("Post being reposted") + subject: PostRef; + + @lexFormat("datetime") + @doc("When the repost was created") createdAt: string; } + + @lexiconMain + @doc("User profile information") + model Profile { + @doc("Display name") + displayName?: string; + + @doc("Profile description") + description?: string; + + @doc("Profile avatar image") + avatar?: string; + + @doc("Profile banner image") + banner?: string; + } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 62fe893..0ddf816 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -52,6 +52,9 @@ importers: packages/example: dependencies: + '@atproto/lex-cli': + specifier: ^0.9.5 + version: 0.9.5 '@typespec/compiler': specifier: ^0.64.0 version: 0.64.0 @@ -68,6 +71,20 @@ importers: packages: + '@atproto/common-web@0.4.3': + resolution: {integrity: sha512-nRDINmSe4VycJzPo6fP/hEltBcULFxt9Kw7fQk6405FyAWZiTluYHlXOnU7GkQfeUK44OENG1qFTBcmCJ7e8pg==} + + '@atproto/lex-cli@0.9.5': + resolution: {integrity: sha512-zun4jhD1dbjD7IHtLIjh/1UsMx+6E8+OyOT2GXYAKIj9N6wmLKM/v2OeQBKxcyqUmtRi57lxWnGikWjjU7pplQ==} + engines: {node: '>=18.7.0'} + hasBin: true + + '@atproto/lexicon@0.5.1': + resolution: {integrity: sha512-y8AEtYmfgVl4fqFxqXAeGvhesiGkxiy3CWoJIfsFDDdTlZUC8DFnZrYhcqkIop3OlCkkljvpSJi1hbeC1tbi8A==} + + '@atproto/syntax@0.4.1': + resolution: {integrity: sha512-CJdImtLAiFO+0z3BWTtxwk6aY5w4t8orHTMVJgkf++QRJWTxPbIFko/0hrkADB7n2EruDxDSeAgfUGehpH6ngw==} + '@babel/code-frame@7.25.9': resolution: {integrity: sha512-z88xeGxnzehn2sqZ8UdGQEvYErF1odv2CftxInpSYJt6uHuPe9YjahKZITGs3l5LeI9d2ROG+obuDAoSlqbNfQ==} engines: {node: '>=6.9.0'} @@ -354,6 +371,9 @@ packages: resolution: {integrity: sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==} engines: {node: '>=18'} + '@ts-morph/common@0.25.0': + resolution: {integrity: sha512-kMnZz+vGGHi4GoHnLmMhGNjm44kGtKUXGnOvrKmMwAuvNjM/PgKVGfUnL7IDvK7Jb2QQ82jq3Zmp04Gy+r3Dkg==} + '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} @@ -428,6 +448,12 @@ packages: assertion-error@1.1.0: resolution: {integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==} + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + brace-expansion@2.0.2: + resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} + braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} @@ -444,6 +470,10 @@ packages: resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} engines: {node: '>=4'} + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + change-case@5.4.4: resolution: {integrity: sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==} @@ -454,6 +484,9 @@ packages: resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} engines: {node: '>=12'} + code-block-writer@13.0.3: + resolution: {integrity: sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg==} + color-convert@1.9.3: resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} @@ -467,6 +500,10 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + commander@9.5.0: + resolution: {integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==} + engines: {node: ^12.20.0 || >=14} + confbox@0.1.8: resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} @@ -527,6 +564,15 @@ packages: fastq@1.19.1: resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + fill-range@7.1.1: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} @@ -555,10 +601,17 @@ packages: resolution: {integrity: sha512-s3Fq41ZVh7vbbe2PN3nrW7yC7U7MFVc5c98/iTl9c2GawNMKx/J648KQRW6WKkuU8GIbbh2IXfIRQjOZnXcTnw==} engines: {node: '>=18'} + graphemer@1.4.0: + resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + has-flag@3.0.0: resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} engines: {node: '>=4'} + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + human-signals@5.0.0: resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==} engines: {node: '>=16.17.0'} @@ -590,6 +643,9 @@ packages: isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + iso-datestring-validator@2.2.2: + resolution: {integrity: sha512-yLEMkBbLZTlVQqOnQ4FiMujR6T4DEcCb1xizmvXS+OxuhwcbtynoosRzdMA69zZCShCNAbi+gJ71FxZBBXx1SA==} + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -628,12 +684,19 @@ packages: resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} engines: {node: '>=12'} + minimatch@9.0.5: + resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} + engines: {node: '>=16 || 14 >=14.17'} + mlly@1.8.0: resolution: {integrity: sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==} ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + multiformats@9.9.0: + resolution: {integrity: sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==} + mustache@4.2.0: resolution: {integrity: sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==} hasBin: true @@ -655,6 +718,9 @@ packages: resolution: {integrity: sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==} engines: {node: '>=18'} + path-browserify@1.0.1: + resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} + path-key@3.1.1: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} @@ -683,6 +749,10 @@ packages: resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} engines: {node: '>=8.6'} + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + engines: {node: '>=12'} + pkg-types@1.3.1: resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} @@ -785,6 +855,10 @@ packages: resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} engines: {node: '>=4'} + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + temporal-polyfill@0.2.5: resolution: {integrity: sha512-ye47xp8Cb0nDguAhrrDS1JT1SzwEV9e26sSsrWzVu+yPZ7LzceEcH0i2gci9jWfOfSCCgM3Qv5nOYShVUUFUXA==} @@ -794,6 +868,10 @@ packages: tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + engines: {node: '>=12.0.0'} + tinypool@0.8.4: resolution: {integrity: sha512-i11VH5gS6IFeLY3gMBQ00/MmLncVP7JLXOw1vlgkytLmJK7QnEr7NXf0LBdxfmNPAeyetukOk0bOYrJrFGjYJQ==} engines: {node: '>=14.0.0'} @@ -806,6 +884,9 @@ packages: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} + ts-morph@24.0.0: + resolution: {integrity: sha512-2OAOg/Ob5yx9Et7ZX4CvTCc0UFoZHwLEJ+dpDPSUi5TgwwlTlX47w+iFRrEwzUZwYACjq83cgjS/Da50Ga37uw==} + type-detect@4.1.0: resolution: {integrity: sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==} engines: {node: '>=4'} @@ -818,6 +899,9 @@ packages: ufo@1.6.1: resolution: {integrity: sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==} + uint8arrays@3.0.0: + resolution: {integrity: sha512-HRCx0q6O9Bfbp+HHSfQQKD7wU70+lydKVt4EghkdOvlK/NlrF90z+eXV34mUd48rNvVJXwkrMSPpCATkct8fJA==} + undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} @@ -934,12 +1018,46 @@ packages: resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} engines: {node: '>=12'} + yesno@0.4.0: + resolution: {integrity: sha512-tdBxmHvbXPBKYIg81bMCB7bVeDmHkRzk5rVJyYYXurwKkHq/MCd8rz4HSJUP7hW0H2NlXiq8IFiWvYKEHhlotA==} + yocto-queue@1.2.1: resolution: {integrity: sha512-AyeEbWOu/TAXdxlV9wmGcR0+yh2j3vYPGOECcIj2S7MkrLyC7ne+oye2BKTItt0ii2PHk4cDy+95+LshzbXnGg==} engines: {node: '>=12.20'} + zod@3.25.76: + resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + snapshots: + '@atproto/common-web@0.4.3': + dependencies: + graphemer: 1.4.0 + multiformats: 9.9.0 + uint8arrays: 3.0.0 + zod: 3.25.76 + + '@atproto/lex-cli@0.9.5': + dependencies: + '@atproto/lexicon': 0.5.1 + '@atproto/syntax': 0.4.1 + chalk: 4.1.2 + commander: 9.5.0 + prettier: 3.3.3 + ts-morph: 24.0.0 + yesno: 0.4.0 + zod: 3.25.76 + + '@atproto/lexicon@0.5.1': + dependencies: + '@atproto/common-web': 0.4.3 + '@atproto/syntax': 0.4.1 + iso-datestring-validator: 2.2.2 + multiformats: 9.9.0 + zod: 3.25.76 + + '@atproto/syntax@0.4.1': {} + '@babel/code-frame@7.25.9': dependencies: '@babel/highlight': 7.25.9 @@ -1111,6 +1229,12 @@ snapshots: '@sindresorhus/merge-streams@2.3.0': {} + '@ts-morph/common@0.25.0': + dependencies: + minimatch: 9.0.5 + path-browserify: 1.0.1 + tinyglobby: 0.2.15 + '@types/estree@1.0.8': {} '@types/node@20.19.19': @@ -1199,6 +1323,12 @@ snapshots: assertion-error@1.1.0: {} + balanced-match@1.0.2: {} + + brace-expansion@2.0.2: + dependencies: + balanced-match: 1.0.2 + braces@3.0.3: dependencies: fill-range: 7.1.1 @@ -1221,6 +1351,11 @@ snapshots: escape-string-regexp: 1.0.5 supports-color: 5.5.0 + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + change-case@5.4.4: {} check-error@1.0.3: @@ -1233,6 +1368,8 @@ snapshots: strip-ansi: 6.0.1 wrap-ansi: 7.0.0 + code-block-writer@13.0.3: {} + color-convert@1.9.3: dependencies: color-name: 1.1.3 @@ -1245,6 +1382,8 @@ snapshots: color-name@1.1.4: {} + commander@9.5.0: {} + confbox@0.1.8: {} cross-spawn@7.0.6: @@ -1327,6 +1466,10 @@ snapshots: dependencies: reusify: 1.1.0 + fdir@6.5.0(picomatch@4.0.3): + optionalDependencies: + picomatch: 4.0.3 + fill-range@7.1.1: dependencies: to-regex-range: 5.0.1 @@ -1353,8 +1496,12 @@ snapshots: slash: 5.1.0 unicorn-magic: 0.1.0 + graphemer@1.4.0: {} + has-flag@3.0.0: {} + has-flag@4.0.0: {} + human-signals@5.0.0: {} ignore@5.3.2: {} @@ -1373,6 +1520,8 @@ snapshots: isexe@2.0.0: {} + iso-datestring-validator@2.2.2: {} + js-tokens@4.0.0: {} js-tokens@9.0.1: {} @@ -1405,6 +1554,10 @@ snapshots: mimic-fn@4.0.0: {} + minimatch@9.0.5: + dependencies: + brace-expansion: 2.0.2 + mlly@1.8.0: dependencies: acorn: 8.15.0 @@ -1414,6 +1567,8 @@ snapshots: ms@2.1.3: {} + multiformats@9.9.0: {} + mustache@4.2.0: {} nanoid@3.3.11: {} @@ -1430,6 +1585,8 @@ snapshots: dependencies: yocto-queue: 1.2.1 + path-browserify@1.0.1: {} + path-key@3.1.1: {} path-key@4.0.0: {} @@ -1446,6 +1603,8 @@ snapshots: picomatch@2.3.1: {} + picomatch@4.0.3: {} + pkg-types@1.3.1: dependencies: confbox: 0.1.8 @@ -1555,6 +1714,10 @@ snapshots: dependencies: has-flag: 3.0.0 + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + temporal-polyfill@0.2.5: dependencies: temporal-spec: 0.2.4 @@ -1563,6 +1726,11 @@ snapshots: tinybench@2.9.0: {} + tinyglobby@0.2.15: + dependencies: + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + tinypool@0.8.4: {} tinyspy@2.2.1: {} @@ -1571,12 +1739,21 @@ snapshots: dependencies: is-number: 7.0.0 + ts-morph@24.0.0: + dependencies: + '@ts-morph/common': 0.25.0 + code-block-writer: 13.0.3 + type-detect@4.1.0: {} typescript@5.9.3: {} ufo@1.6.1: {} + uint8arrays@3.0.0: + dependencies: + multiformats: 9.9.0 + undici-types@6.21.0: {} unicorn-magic@0.1.0: {} @@ -1688,4 +1865,8 @@ snapshots: y18n: 5.0.8 yargs-parser: 21.1.1 + yesno@0.4.0: {} + yocto-queue@1.2.1: {} + + zod@3.25.76: {}