From 8a4a2b5c2eb95041889310acf08a386930b241d5 Mon Sep 17 00:00:00 2001 From: TAKAHASHI Shuuji Date: Sun, 19 Jan 2025 22:14:45 +0900 Subject: [PATCH 1/3] chore: switch line ending biome setting from CRLF to LF --- biome.json | 88 +++++++++++++++++++++++++++--------------------------- 1 file changed, 44 insertions(+), 44 deletions(-) diff --git a/biome.json b/biome.json index 046ccc7..122ed63 100644 --- a/biome.json +++ b/biome.json @@ -1,44 +1,44 @@ -{ - "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", - "organizeImports": { - "enabled": true - }, - "files": { - "include": ["docs", "packages/*"], - "ignore": [ - ".github", - ".gitignore", - ".vitepress", - ".vscode", - "*.md", - "*.toml", - "build", - "dist", - "node_modules", - "package.json", - "tsconfig.json", - "packages/lexicons/src/lib/lexicons.ts" - ] - }, - "linter": { - "enabled": true, - "rules": { - "recommended": true - } - }, - "formatter": { - "indentStyle": "space", - "lineWidth": 80, - "lineEnding": "crlf" - }, - "vcs": { - "enabled": true, - "clientKind": "git", - "useIgnoreFile": true - }, - "javascript": { - "formatter": { - "quoteStyle": "single" - } - } -} +{ + "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", + "organizeImports": { + "enabled": true + }, + "files": { + "include": ["docs", "packages/*"], + "ignore": [ + ".github", + ".gitignore", + ".vitepress", + ".vscode", + "*.md", + "*.toml", + "build", + "dist", + "node_modules", + "package.json", + "tsconfig.json", + "packages/lexicons/src/lib/lexicons.ts" + ] + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true + } + }, + "formatter": { + "indentStyle": "space", + "lineWidth": 80, + "lineEnding": "lf" + }, + "vcs": { + "enabled": true, + "clientKind": "git", + "useIgnoreFile": true + }, + "javascript": { + "formatter": { + "quoteStyle": "single" + } + } +} From 41ed7264e650c78e8cd15e1cd49ef043c23a2478 Mon Sep 17 00:00:00 2001 From: TAKAHASHI Shuuji Date: Sun, 19 Jan 2025 22:15:04 +0900 Subject: [PATCH 2/3] chore: apply new biome format --- packages/client/src/bsky/feed.ts | 100 +-- packages/client/src/bsky/index.ts | 56 +- packages/client/src/index.ts | 4 +- packages/client/src/tsky/client.ts | 112 +-- packages/client/src/tsky/index.ts | 4 +- packages/client/src/tsky/paginator.ts | 98 +-- packages/client/src/tsky/tsky.ts | 36 +- packages/client/vitest.config.mjs | 18 +- packages/lex-cli/src/generator/index.ts | 646 +++++++------- .../src/generator/resolvers/complex.ts | 110 +-- .../lex-cli/src/generator/resolvers/index.ts | 18 +- .../src/generator/resolvers/numeric.ts | 34 +- .../src/generator/resolvers/primitives.ts | 28 +- .../lex-cli/src/generator/resolvers/string.ts | 98 +-- .../lex-cli/src/generator/resolvers/type.ts | 198 ++--- packages/lex-cli/src/generator/schema.ts | 816 +++++++++--------- packages/lex-cli/src/index.ts | 234 ++--- packages/lex-cli/src/utils/cache.ts | 52 +- packages/lex-cli/src/utils/docs.ts | 48 +- packages/lex-cli/src/utils/formats.ts | 28 +- packages/lex-cli/src/utils/index.ts | 10 +- packages/lex-cli/src/utils/prelude.ts | 114 +-- packages/lex-cli/src/utils/sort.ts | 56 +- packages/lexicons/index.ts | 2 +- .../lexicons/scripts/check-version-change.ts | 62 +- packages/lexicons/scripts/generate-types.ts | 236 ++--- packages/lexicons/src/index.ts | 274 +++--- 27 files changed, 1746 insertions(+), 1746 deletions(-) diff --git a/packages/client/src/bsky/feed.ts b/packages/client/src/bsky/feed.ts index 3676c9a..ffc1a30 100644 --- a/packages/client/src/bsky/feed.ts +++ b/packages/client/src/bsky/feed.ts @@ -1,50 +1,50 @@ -import type { - AppBskyFeedGetFeed, - AppBskyFeedGetTimeline, -} from '@tsky/lexicons'; -import type { Client } from '~/tsky/client'; -import { Paginator } from '~/tsky/paginator'; - -export class Feed { - constructor(private client: Client) {} - - /** - * Get a hydrated feed from an actor's selected feed generator. Implemented by App View. - */ - async getFeed( - params: AppBskyFeedGetFeed.Params, - options?: AppBskyFeedGetFeed.Input, - ): Promise> { - return Paginator.init(async (cursor) => { - const res = await this.client.get('app.bsky.feed.getFeed', { - ...(options ?? {}), - params: { - cursor, - ...params, - }, - }); - - return res.data; - }); - } - - /** - * Get a view of the requesting account's home timeline. This is expected to be some form of reverse-chronological feed. - */ - getTimeline( - params: AppBskyFeedGetTimeline.Params, - options?: AppBskyFeedGetTimeline.Input, - ): Promise> { - return Paginator.init(async (cursor) => { - const res = await this.client.get('app.bsky.feed.getTimeline', { - ...(options ?? {}), - params: { - cursor, - ...params, - }, - }); - - return res.data; - }); - } -} +import type { + AppBskyFeedGetFeed, + AppBskyFeedGetTimeline, +} from '@tsky/lexicons'; +import type { Client } from '~/tsky/client'; +import { Paginator } from '~/tsky/paginator'; + +export class Feed { + constructor(private client: Client) {} + + /** + * Get a hydrated feed from an actor's selected feed generator. Implemented by App View. + */ + async getFeed( + params: AppBskyFeedGetFeed.Params, + options?: AppBskyFeedGetFeed.Input, + ): Promise> { + return Paginator.init(async (cursor) => { + const res = await this.client.get('app.bsky.feed.getFeed', { + ...(options ?? {}), + params: { + cursor, + ...params, + }, + }); + + return res.data; + }); + } + + /** + * Get a view of the requesting account's home timeline. This is expected to be some form of reverse-chronological feed. + */ + getTimeline( + params: AppBskyFeedGetTimeline.Params, + options?: AppBskyFeedGetTimeline.Input, + ): Promise> { + return Paginator.init(async (cursor) => { + const res = await this.client.get('app.bsky.feed.getTimeline', { + ...(options ?? {}), + params: { + cursor, + ...params, + }, + }); + + return res.data; + }); + } +} diff --git a/packages/client/src/bsky/index.ts b/packages/client/src/bsky/index.ts index 5080797..61df734 100644 --- a/packages/client/src/bsky/index.ts +++ b/packages/client/src/bsky/index.ts @@ -1,28 +1,28 @@ -import type { AppBskyActorDefs } from '@tsky/lexicons'; -import { Feed } from '~/bsky/feed'; -import type { Client } from '~/tsky/client'; - -export class Bsky { - client: Client; - - constructor(client: Client) { - this.client = client; - } - - /** - * Get detailed profile view of an actor. Does not require auth, but contains relevant metadata with auth. - */ - async profile( - identifier: string, - ): Promise { - const res = await this.client.get('app.bsky.actor.getProfile', { - params: { actor: identifier }, - }); - - return res.data; - } - - get feed() { - return new Feed(this.client); - } -} +import type { AppBskyActorDefs } from '@tsky/lexicons'; +import { Feed } from '~/bsky/feed'; +import type { Client } from '~/tsky/client'; + +export class Bsky { + client: Client; + + constructor(client: Client) { + this.client = client; + } + + /** + * Get detailed profile view of an actor. Does not require auth, but contains relevant metadata with auth. + */ + async profile( + identifier: string, + ): Promise { + const res = await this.client.get('app.bsky.actor.getProfile', { + params: { actor: identifier }, + }); + + return res.data; + } + + get feed() { + return new Feed(this.client); + } +} diff --git a/packages/client/src/index.ts b/packages/client/src/index.ts index 4849dc1..9450186 100644 --- a/packages/client/src/index.ts +++ b/packages/client/src/index.ts @@ -1,2 +1,2 @@ -export * from './bsky'; -export * from './tsky'; +export * from './bsky'; +export * from './tsky'; diff --git a/packages/client/src/tsky/client.ts b/packages/client/src/tsky/client.ts index d83d372..34a017c 100644 --- a/packages/client/src/tsky/client.ts +++ b/packages/client/src/tsky/client.ts @@ -1,56 +1,56 @@ -import type { - RPCOptions, - XRPC, - XRPCRequestOptions, - XRPCResponse, -} from '@atcute/client'; -import type { Procedures, Queries } from '@tsky/lexicons'; - -// From @atcute/client -type OutputOf = T extends { - // biome-ignore lint/suspicious/noExplicitAny: - output: any; -} - ? T['output'] - : never; - -export class Client { - xrpc: XRPC; - - constructor(xrpc: XRPC) { - this.xrpc = xrpc; - } - - /** - * Makes a query (GET) request - * @param nsid Namespace ID of a query endpoint - * @param options Options to include like parameters - * @returns The response of the request - */ - async get( - nsid: K, - options: RPCOptions, - ): Promise>> { - // biome-ignore lint/suspicious/noExplicitAny: - return this.xrpc.get(nsid as any, options); - } - - /** - * Makes a procedure (POST) request - * @param nsid Namespace ID of a procedure endpoint - * @param options Options to include like input body or parameters - * @returns The response of the request - */ - async call( - nsid: K, - options: RPCOptions, - ): Promise>> { - // biome-ignore lint/suspicious/noExplicitAny: - return this.xrpc.call(nsid as any, options); - } - - /** Makes a request to the XRPC service */ - async request(options: XRPCRequestOptions): Promise { - return this.xrpc.request(options); - } -} +import type { + RPCOptions, + XRPC, + XRPCRequestOptions, + XRPCResponse, +} from '@atcute/client'; +import type { Procedures, Queries } from '@tsky/lexicons'; + +// From @atcute/client +type OutputOf = T extends { + // biome-ignore lint/suspicious/noExplicitAny: + output: any; +} + ? T['output'] + : never; + +export class Client { + xrpc: XRPC; + + constructor(xrpc: XRPC) { + this.xrpc = xrpc; + } + + /** + * Makes a query (GET) request + * @param nsid Namespace ID of a query endpoint + * @param options Options to include like parameters + * @returns The response of the request + */ + async get( + nsid: K, + options: RPCOptions, + ): Promise>> { + // biome-ignore lint/suspicious/noExplicitAny: + return this.xrpc.get(nsid as any, options); + } + + /** + * Makes a procedure (POST) request + * @param nsid Namespace ID of a procedure endpoint + * @param options Options to include like input body or parameters + * @returns The response of the request + */ + async call( + nsid: K, + options: RPCOptions, + ): Promise>> { + // biome-ignore lint/suspicious/noExplicitAny: + return this.xrpc.call(nsid as any, options); + } + + /** Makes a request to the XRPC service */ + async request(options: XRPCRequestOptions): Promise { + return this.xrpc.request(options); + } +} diff --git a/packages/client/src/tsky/index.ts b/packages/client/src/tsky/index.ts index 929c294..3ef8518 100644 --- a/packages/client/src/tsky/index.ts +++ b/packages/client/src/tsky/index.ts @@ -1,2 +1,2 @@ -export * from './paginator'; -export * from './tsky'; +export * from './paginator'; +export * from './tsky'; diff --git a/packages/client/src/tsky/paginator.ts b/packages/client/src/tsky/paginator.ts index a774fb3..f75220e 100644 --- a/packages/client/src/tsky/paginator.ts +++ b/packages/client/src/tsky/paginator.ts @@ -1,49 +1,49 @@ -interface CursorResponse { - cursor?: string; -} - -export class Paginator { - readonly values: T[] = []; - - private constructor( - private onNext: (cursor?: string) => Promise, - defaultValues?: T[], - ) { - if (defaultValues) { - this.values = defaultValues; - } - } - - static async init( - onNext: (cursor?: string) => Promise, - defaultValues?: T[], - ): Promise> { - const paginator = new Paginator(onNext, defaultValues); - - // load the first page - await paginator.next(); - - return paginator; - } - - clone() { - return new Paginator(this.onNext, this.values); - } - - async next() { - const hasValues = this.values.length > 0; - - const cursor = hasValues - ? this.values[this.values.length - 1].cursor - : undefined; - - // When we are at the end of the list - if (hasValues && !cursor) return null; - - const data = await this.onNext(cursor); - - this.values.push(data); - - return data; - } -} +interface CursorResponse { + cursor?: string; +} + +export class Paginator { + readonly values: T[] = []; + + private constructor( + private onNext: (cursor?: string) => Promise, + defaultValues?: T[], + ) { + if (defaultValues) { + this.values = defaultValues; + } + } + + static async init( + onNext: (cursor?: string) => Promise, + defaultValues?: T[], + ): Promise> { + const paginator = new Paginator(onNext, defaultValues); + + // load the first page + await paginator.next(); + + return paginator; + } + + clone() { + return new Paginator(this.onNext, this.values); + } + + async next() { + const hasValues = this.values.length > 0; + + const cursor = hasValues + ? this.values[this.values.length - 1].cursor + : undefined; + + // When we are at the end of the list + if (hasValues && !cursor) return null; + + const data = await this.onNext(cursor); + + this.values.push(data); + + return data; + } +} diff --git a/packages/client/src/tsky/tsky.ts b/packages/client/src/tsky/tsky.ts index 8f123af..c6a3fc5 100644 --- a/packages/client/src/tsky/tsky.ts +++ b/packages/client/src/tsky/tsky.ts @@ -1,18 +1,18 @@ -import type { CredentialManager } from '@atcute/client'; -import { XRPC } from '@atcute/client'; -import type { Queries } from '@tsky/lexicons'; -import { Bsky } from '~/bsky'; -import { Client } from './client'; - -export class Tsky { - client: Client; - - constructor(manager: CredentialManager) { - const xrpc = new XRPC({ handler: manager }); - this.client = new Client(xrpc); - } - - get bsky() { - return new Bsky(this.client); - } -} +import type { CredentialManager } from '@atcute/client'; +import { XRPC } from '@atcute/client'; +import type { Queries } from '@tsky/lexicons'; +import { Bsky } from '~/bsky'; +import { Client } from './client'; + +export class Tsky { + client: Client; + + constructor(manager: CredentialManager) { + const xrpc = new XRPC({ handler: manager }); + this.client = new Client(xrpc); + } + + get bsky() { + return new Bsky(this.client); + } +} diff --git a/packages/client/vitest.config.mjs b/packages/client/vitest.config.mjs index 75ff975..eb547e3 100644 --- a/packages/client/vitest.config.mjs +++ b/packages/client/vitest.config.mjs @@ -1,9 +1,9 @@ -import { defineConfig } from 'vitest/config'; - -export default defineConfig({ - resolve: { - alias: { - '~': '/src', - }, - }, -}); +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + resolve: { + alias: { + '~': '/src', + }, + }, +}); diff --git a/packages/lex-cli/src/generator/index.ts b/packages/lex-cli/src/generator/index.ts index 204a18a..68e371e 100644 --- a/packages/lex-cli/src/generator/index.ts +++ b/packages/lex-cli/src/generator/index.ts @@ -1,323 +1,323 @@ -import { readFile } from 'node:fs/promises'; -import { - toNamespace, - getDescriptions, - writeJsdoc, - mainPrelude, - sortDefinition, - sortName, - sortPropertyKeys, -} from '../utils/index.js'; -import { resolveType } from './resolvers/index.js'; -import { type DocumentSchema, documentSchema } from './schema.js'; - -export interface GenerateDefinitionsOptions { - files: string[]; - banner?: string; - description?: string; - debug?: boolean; - lexiconMetadata?: { - commitSha?: string; - version?: string; - sourceUrl?: string; - }; - onProgress?: (filename: string, index: number, total: number) => void; -} - -export async function generateDefinitions(opts: GenerateDefinitionsOptions) { - const { files, banner, description, lexiconMetadata } = opts; - - let queries = ''; - let procedures = ''; - let records = ''; - let subscriptions = ''; - - const buildDate = new Date().toISOString(); - const metadataLines = [ - '/**', - ' * @module', - description ? ` * ${description}` : ' * ATProto lexicon type definitions', - ' * @generated', - ` * Generated on: ${buildDate}`, - ]; - - if (lexiconMetadata?.version) { - metadataLines.push(` * Version: ${lexiconMetadata.version}`); - } - if (lexiconMetadata?.commitSha) { - metadataLines.push(` * Commit: ${lexiconMetadata.commitSha}`); - } - if (lexiconMetadata?.sourceUrl) { - metadataLines.push(` * Source: ${lexiconMetadata.sourceUrl}`); - } - - metadataLines.push(' */'); - - let code = `/* eslint-disable */ -// This file is automatically generated by @tsky/lex-cli, do not edit! - -${metadataLines.join('\n')}`; - - if (banner) { - code += `\n\n${banner}`; - } - - code += `\n\n${mainPrelude}`; - - for await (const filename of files.sort(sortName)) { - let document: DocumentSchema; - - try { - const jsonString = await readFile(filename, 'utf8'); - const parsed = JSON.parse(jsonString); - - if (!documentSchema(parsed)) { - throw new Error('Invalid document schema'); - } - document = parsed; - } catch (err) { - throw new Error(`failed to read ${filename}`, { cause: err }); - } - - const ns = document.id; - const tsNamespace = toNamespace(ns); - - let descs: string[] = []; - let chunk = ''; - - const definitions = document.defs; - const keys = Object.keys(definitions).sort(sortDefinition); - - for (const key of keys) { - const def = definitions[key]; - const type = def.type; - - const nsid = `${ns}${key !== 'main' ? `#${key}` : ''}`; - const typeName = key[0].toUpperCase() + key.slice(1); - - if (type === 'string') { - const { value, descriptions } = resolveType(nsid, def); - - chunk += writeJsdoc(descriptions); - chunk += `type ${typeName} = ${value};`; - } else if (type === 'token') { - chunk += `type ${typeName} = '${nsid}';`; - } else if (type === 'object') { - const required = def.required; - const nullable = def.nullable; - const properties = def.properties; - - const propKeys = sortPropertyKeys(Object.keys(properties), required); - const descs = getDescriptions(def); - - chunk += writeJsdoc(descs); - chunk += `interface ${typeName} extends TypedBase {`; - - for (const prop of propKeys) { - const isOptional = !required || !required.includes(prop); - const isNullable = nullable?.includes(prop); - const { value, descriptions } = resolveType( - `${nsid}/${prop}`, - properties[prop], - ); - - chunk += writeJsdoc(descriptions); - chunk += `${prop}${isOptional ? '?' : ''}:${value}${isNullable ? '| null' : ''};`; - } - - chunk += '}'; - } else if (type === 'array') { - const { value, descriptions } = resolveType(nsid, def.items); - const descs = []; - - if (def.maxLength !== undefined) { - descs.push(`Maximum array length: ${def.maxLength}`); - } - - if (def.minLength !== undefined) { - descs.push(`Minimum array length: ${def.minLength}`); - } - - chunk += writeJsdoc(descs.concat(descriptions)); - chunk += `type ${typeName} = (${value})[];`; - } else if (type === 'record') { - const obj = def.record; - const required = obj.required; - const nullable = obj.nullable; - const properties = obj.properties; - - const propKeys = sortPropertyKeys(Object.keys(properties), required); - const descs = getDescriptions(def); - - chunk += writeJsdoc(descs); - chunk += 'interface Record extends RecordBase {'; - chunk += `$type: '${nsid}';`; - - for (const prop of propKeys) { - const isOptional = !required || !required.includes(prop); - const isNullable = nullable?.includes(prop); - const { value, descriptions } = resolveType( - `${nsid}/${prop}`, - properties[prop], - ); - - chunk += writeJsdoc(descriptions); - chunk += `${prop}${isOptional ? '?' : ''}:${value}${isNullable ? '| null' : ''};`; - } - - chunk += '}'; - - records += `\n'${nsid}': ${tsNamespace}.Record;`; - } else if (type === 'query' || type === 'procedure') { - let parameters = def.parameters; - const input = type === 'procedure' ? def.input : undefined; - const output = def.output; - const errors = def.errors; - - descs = getDescriptions(def); - - if (parameters) { - if (Object.values(parameters.properties).length === 0) { - parameters = undefined; - } else { - const { value, descriptions } = resolveType(nsid, parameters); - - chunk += writeJsdoc(descriptions); - chunk += `interface Params extends TypedBase ${value}`; - } - } else { - chunk += 'interface Params extends TypedBase {}'; - } - - if (input) { - if (input.encoding === 'application/json' && input.schema) { - const { value, descriptions } = resolveType(nsid, input.schema); - - chunk += writeJsdoc(descriptions); - - if (input.schema?.type === 'object') { - chunk += `interface Input extends TypedBase ${value}`; - } else { - chunk += `type Input = ${value};`; - } - } else { - chunk += 'type Input = Blob | ArrayBufferView;'; - } - } else { - chunk += 'type Input = undefined;'; - } - - if (output) { - if (output.encoding === 'application/json' && output.schema) { - const { value, descriptions } = resolveType(nsid, output.schema); - - chunk += writeJsdoc(descriptions); - - if (output.schema?.type === 'object') { - chunk += `interface Output extends TypedBase ${value}`; - } else { - chunk += `type Output = ${value};`; - } - } else { - chunk += 'type Output = Uint8Array;'; - } - } else { - chunk += 'type Output = undefined;'; - } - - if (errors) { - chunk += 'interface Errors extends TypedBase {'; - - for (const error of errors) { - chunk += `${error.name}: {};`; - } - - chunk += '}'; - } - - { - let rc = `'${ns}':{\n`; - - if (parameters) { - rc += `params: ${tsNamespace}.Params;`; - } - if (input) { - rc += `input: ${tsNamespace}.Input;`; - } - if (output) { - rc += `output: ${tsNamespace}.Output;`; - } - - rc += '};'; - - if (type === 'query') { - queries += rc; - } else if (type === 'procedure') { - procedures += rc; - } - } - } else if (type === 'blob') { - const { value, descriptions } = resolveType(nsid, def); - - chunk += writeJsdoc(descriptions); - chunk += `type ${typeName} = ${value};`; - } else if (type === 'bytes') { - const { value, descriptions } = resolveType(nsid, def); - - chunk += writeJsdoc(descriptions); - chunk += `type ${typeName} = ${value};`; - } else if (type === 'subscription') { - if (def.parameters) { - const { value, descriptions } = resolveType(nsid, def.parameters); - chunk += writeJsdoc(descriptions); - chunk += `interface Params extends TypedBase ${value}`; - } else { - chunk += 'interface Params extends TypedBase {}'; - } - - if (def.message?.schema) { - const { value: messageValue, descriptions: messageDesc } = - resolveType(nsid, def.message.schema); - chunk += writeJsdoc(messageDesc); - chunk += `type Message = ${messageValue};`; - } - - if (def.errors) { - chunk += 'interface Errors extends TypedBase {'; - for (const error of def.errors) { - chunk += `${error.name}: {};`; - } - chunk += '}'; - } - - let rc = `'${ns}':{\n`; - if (def.parameters) { - rc += `params: ${tsNamespace}.Params;`; - } - if (def.message?.schema) { - rc += `message: ${tsNamespace}.Message;`; - } - if (def.errors) { - rc += `errors: ${tsNamespace}.Errors;`; - } - rc += '};'; - subscriptions += rc; - } else { - // eslint-disable-next-line no-console - console.log(`${nsid}: unhandled type ${type}`); - } - } - - code += writeJsdoc(descs); - code += `export declare namespace ${tsNamespace} {`; - code += chunk; - code += '}\n\n'; - } - - code += `export declare interface Records extends RecordBase {${records}}\n\n`; - code += `export declare interface Queries {${queries}}\n\n`; - code += `export declare interface Procedures {${procedures}}\n\n`; - code += `export declare interface Subscriptions {${subscriptions}}\n\n`; - - return code; -} +import { readFile } from 'node:fs/promises'; +import { + toNamespace, + getDescriptions, + writeJsdoc, + mainPrelude, + sortDefinition, + sortName, + sortPropertyKeys, +} from '../utils/index.js'; +import { resolveType } from './resolvers/index.js'; +import { type DocumentSchema, documentSchema } from './schema.js'; + +export interface GenerateDefinitionsOptions { + files: string[]; + banner?: string; + description?: string; + debug?: boolean; + lexiconMetadata?: { + commitSha?: string; + version?: string; + sourceUrl?: string; + }; + onProgress?: (filename: string, index: number, total: number) => void; +} + +export async function generateDefinitions(opts: GenerateDefinitionsOptions) { + const { files, banner, description, lexiconMetadata } = opts; + + let queries = ''; + let procedures = ''; + let records = ''; + let subscriptions = ''; + + const buildDate = new Date().toISOString(); + const metadataLines = [ + '/**', + ' * @module', + description ? ` * ${description}` : ' * ATProto lexicon type definitions', + ' * @generated', + ` * Generated on: ${buildDate}`, + ]; + + if (lexiconMetadata?.version) { + metadataLines.push(` * Version: ${lexiconMetadata.version}`); + } + if (lexiconMetadata?.commitSha) { + metadataLines.push(` * Commit: ${lexiconMetadata.commitSha}`); + } + if (lexiconMetadata?.sourceUrl) { + metadataLines.push(` * Source: ${lexiconMetadata.sourceUrl}`); + } + + metadataLines.push(' */'); + + let code = `/* eslint-disable */ +// This file is automatically generated by @tsky/lex-cli, do not edit! + +${metadataLines.join('\n')}`; + + if (banner) { + code += `\n\n${banner}`; + } + + code += `\n\n${mainPrelude}`; + + for await (const filename of files.sort(sortName)) { + let document: DocumentSchema; + + try { + const jsonString = await readFile(filename, 'utf8'); + const parsed = JSON.parse(jsonString); + + if (!documentSchema(parsed)) { + throw new Error('Invalid document schema'); + } + document = parsed; + } catch (err) { + throw new Error(`failed to read ${filename}`, { cause: err }); + } + + const ns = document.id; + const tsNamespace = toNamespace(ns); + + let descs: string[] = []; + let chunk = ''; + + const definitions = document.defs; + const keys = Object.keys(definitions).sort(sortDefinition); + + for (const key of keys) { + const def = definitions[key]; + const type = def.type; + + const nsid = `${ns}${key !== 'main' ? `#${key}` : ''}`; + const typeName = key[0].toUpperCase() + key.slice(1); + + if (type === 'string') { + const { value, descriptions } = resolveType(nsid, def); + + chunk += writeJsdoc(descriptions); + chunk += `type ${typeName} = ${value};`; + } else if (type === 'token') { + chunk += `type ${typeName} = '${nsid}';`; + } else if (type === 'object') { + const required = def.required; + const nullable = def.nullable; + const properties = def.properties; + + const propKeys = sortPropertyKeys(Object.keys(properties), required); + const descs = getDescriptions(def); + + chunk += writeJsdoc(descs); + chunk += `interface ${typeName} extends TypedBase {`; + + for (const prop of propKeys) { + const isOptional = !required || !required.includes(prop); + const isNullable = nullable?.includes(prop); + const { value, descriptions } = resolveType( + `${nsid}/${prop}`, + properties[prop], + ); + + chunk += writeJsdoc(descriptions); + chunk += `${prop}${isOptional ? '?' : ''}:${value}${isNullable ? '| null' : ''};`; + } + + chunk += '}'; + } else if (type === 'array') { + const { value, descriptions } = resolveType(nsid, def.items); + const descs = []; + + if (def.maxLength !== undefined) { + descs.push(`Maximum array length: ${def.maxLength}`); + } + + if (def.minLength !== undefined) { + descs.push(`Minimum array length: ${def.minLength}`); + } + + chunk += writeJsdoc(descs.concat(descriptions)); + chunk += `type ${typeName} = (${value})[];`; + } else if (type === 'record') { + const obj = def.record; + const required = obj.required; + const nullable = obj.nullable; + const properties = obj.properties; + + const propKeys = sortPropertyKeys(Object.keys(properties), required); + const descs = getDescriptions(def); + + chunk += writeJsdoc(descs); + chunk += 'interface Record extends RecordBase {'; + chunk += `$type: '${nsid}';`; + + for (const prop of propKeys) { + const isOptional = !required || !required.includes(prop); + const isNullable = nullable?.includes(prop); + const { value, descriptions } = resolveType( + `${nsid}/${prop}`, + properties[prop], + ); + + chunk += writeJsdoc(descriptions); + chunk += `${prop}${isOptional ? '?' : ''}:${value}${isNullable ? '| null' : ''};`; + } + + chunk += '}'; + + records += `\n'${nsid}': ${tsNamespace}.Record;`; + } else if (type === 'query' || type === 'procedure') { + let parameters = def.parameters; + const input = type === 'procedure' ? def.input : undefined; + const output = def.output; + const errors = def.errors; + + descs = getDescriptions(def); + + if (parameters) { + if (Object.values(parameters.properties).length === 0) { + parameters = undefined; + } else { + const { value, descriptions } = resolveType(nsid, parameters); + + chunk += writeJsdoc(descriptions); + chunk += `interface Params extends TypedBase ${value}`; + } + } else { + chunk += 'interface Params extends TypedBase {}'; + } + + if (input) { + if (input.encoding === 'application/json' && input.schema) { + const { value, descriptions } = resolveType(nsid, input.schema); + + chunk += writeJsdoc(descriptions); + + if (input.schema?.type === 'object') { + chunk += `interface Input extends TypedBase ${value}`; + } else { + chunk += `type Input = ${value};`; + } + } else { + chunk += 'type Input = Blob | ArrayBufferView;'; + } + } else { + chunk += 'type Input = undefined;'; + } + + if (output) { + if (output.encoding === 'application/json' && output.schema) { + const { value, descriptions } = resolveType(nsid, output.schema); + + chunk += writeJsdoc(descriptions); + + if (output.schema?.type === 'object') { + chunk += `interface Output extends TypedBase ${value}`; + } else { + chunk += `type Output = ${value};`; + } + } else { + chunk += 'type Output = Uint8Array;'; + } + } else { + chunk += 'type Output = undefined;'; + } + + if (errors) { + chunk += 'interface Errors extends TypedBase {'; + + for (const error of errors) { + chunk += `${error.name}: {};`; + } + + chunk += '}'; + } + + { + let rc = `'${ns}':{\n`; + + if (parameters) { + rc += `params: ${tsNamespace}.Params;`; + } + if (input) { + rc += `input: ${tsNamespace}.Input;`; + } + if (output) { + rc += `output: ${tsNamespace}.Output;`; + } + + rc += '};'; + + if (type === 'query') { + queries += rc; + } else if (type === 'procedure') { + procedures += rc; + } + } + } else if (type === 'blob') { + const { value, descriptions } = resolveType(nsid, def); + + chunk += writeJsdoc(descriptions); + chunk += `type ${typeName} = ${value};`; + } else if (type === 'bytes') { + const { value, descriptions } = resolveType(nsid, def); + + chunk += writeJsdoc(descriptions); + chunk += `type ${typeName} = ${value};`; + } else if (type === 'subscription') { + if (def.parameters) { + const { value, descriptions } = resolveType(nsid, def.parameters); + chunk += writeJsdoc(descriptions); + chunk += `interface Params extends TypedBase ${value}`; + } else { + chunk += 'interface Params extends TypedBase {}'; + } + + if (def.message?.schema) { + const { value: messageValue, descriptions: messageDesc } = + resolveType(nsid, def.message.schema); + chunk += writeJsdoc(messageDesc); + chunk += `type Message = ${messageValue};`; + } + + if (def.errors) { + chunk += 'interface Errors extends TypedBase {'; + for (const error of def.errors) { + chunk += `${error.name}: {};`; + } + chunk += '}'; + } + + let rc = `'${ns}':{\n`; + if (def.parameters) { + rc += `params: ${tsNamespace}.Params;`; + } + if (def.message?.schema) { + rc += `message: ${tsNamespace}.Message;`; + } + if (def.errors) { + rc += `errors: ${tsNamespace}.Errors;`; + } + rc += '};'; + subscriptions += rc; + } else { + // eslint-disable-next-line no-console + console.log(`${nsid}: unhandled type ${type}`); + } + } + + code += writeJsdoc(descs); + code += `export declare namespace ${tsNamespace} {`; + code += chunk; + code += '}\n\n'; + } + + code += `export declare interface Records extends RecordBase {${records}}\n\n`; + code += `export declare interface Queries {${queries}}\n\n`; + code += `export declare interface Procedures {${procedures}}\n\n`; + code += `export declare interface Subscriptions {${subscriptions}}\n\n`; + + return code; +} diff --git a/packages/lex-cli/src/generator/resolvers/complex.ts b/packages/lex-cli/src/generator/resolvers/complex.ts index 8c2d2c0..51f8930 100644 --- a/packages/lex-cli/src/generator/resolvers/complex.ts +++ b/packages/lex-cli/src/generator/resolvers/complex.ts @@ -1,55 +1,55 @@ -import type { - ObjectSchema, - RefSchema, - RefUnionSchema, - XrpcParametersSchema, -} from '../schema.js'; -import { - toNamespace, - toUpper, - writeJsdoc, - sortName, - sortPropertyKeys, -} from '../../utils/index.js'; -import { resolveType } from './type.js'; - -export function resolveRefType(def: RefSchema): string { - const [ns, ref] = def.ref.split('#'); - return (ns ? `${toNamespace(ns)}.` : '') + (ref ? toUpper(ref) : 'Main'); -} - -export function resolveUnionType(def: RefUnionSchema): string { - const refs = def.refs.toSorted(sortName).map((raw) => { - const [ns, ref] = raw.split('#'); - return (ns ? `${toNamespace(ns)}.` : '') + (ref ? toUpper(ref) : 'Main'); - }); - return `TypeUnion<${refs.join('|')}>`; -} - -export function resolveObjectType( - def: ObjectSchema | XrpcParametersSchema, - type: 'object' | 'params', - nsid: string, -): { value: string; descriptions: string[] } { - const required = def.required; - const nullable = type === 'object' && 'nullable' in def ? def.nullable : []; - const properties = def.properties; - - const propKeys = sortPropertyKeys(Object.keys(properties), required); - let chunk = '{'; - - for (const prop of propKeys) { - const isOptional = !required || !required.includes(prop); - const isNullable = nullable?.includes(prop); - const { value, descriptions } = resolveType( - `${nsid}/${prop}`, - properties[prop], - ); - - chunk += writeJsdoc(descriptions); - chunk += `${prop}${isOptional ? '?' : ''}:${value}${isNullable ? '| null' : ''};`; - } - - chunk += '}'; - return { value: chunk, descriptions: [] }; -} +import type { + ObjectSchema, + RefSchema, + RefUnionSchema, + XrpcParametersSchema, +} from '../schema.js'; +import { + toNamespace, + toUpper, + writeJsdoc, + sortName, + sortPropertyKeys, +} from '../../utils/index.js'; +import { resolveType } from './type.js'; + +export function resolveRefType(def: RefSchema): string { + const [ns, ref] = def.ref.split('#'); + return (ns ? `${toNamespace(ns)}.` : '') + (ref ? toUpper(ref) : 'Main'); +} + +export function resolveUnionType(def: RefUnionSchema): string { + const refs = def.refs.toSorted(sortName).map((raw) => { + const [ns, ref] = raw.split('#'); + return (ns ? `${toNamespace(ns)}.` : '') + (ref ? toUpper(ref) : 'Main'); + }); + return `TypeUnion<${refs.join('|')}>`; +} + +export function resolveObjectType( + def: ObjectSchema | XrpcParametersSchema, + type: 'object' | 'params', + nsid: string, +): { value: string; descriptions: string[] } { + const required = def.required; + const nullable = type === 'object' && 'nullable' in def ? def.nullable : []; + const properties = def.properties; + + const propKeys = sortPropertyKeys(Object.keys(properties), required); + let chunk = '{'; + + for (const prop of propKeys) { + const isOptional = !required || !required.includes(prop); + const isNullable = nullable?.includes(prop); + const { value, descriptions } = resolveType( + `${nsid}/${prop}`, + properties[prop], + ); + + chunk += writeJsdoc(descriptions); + chunk += `${prop}${isOptional ? '?' : ''}:${value}${isNullable ? '| null' : ''};`; + } + + chunk += '}'; + return { value: chunk, descriptions: [] }; +} diff --git a/packages/lex-cli/src/generator/resolvers/index.ts b/packages/lex-cli/src/generator/resolvers/index.ts index d5d467f..d41cbb6 100644 --- a/packages/lex-cli/src/generator/resolvers/index.ts +++ b/packages/lex-cli/src/generator/resolvers/index.ts @@ -1,9 +1,9 @@ -export { - resolveObjectType, - resolveRefType, - resolveUnionType, -} from './complex.js'; -export { resolveIntegerType } from './numeric.js'; -export { resolvePrimitiveType } from './primitives.js'; -export { resolveStringFormat, resolveStringType } from './string.js'; -export { resolveType } from './type.js'; +export { + resolveObjectType, + resolveRefType, + resolveUnionType, +} from './complex.js'; +export { resolveIntegerType } from './numeric.js'; +export { resolvePrimitiveType } from './primitives.js'; +export { resolveStringFormat, resolveStringType } from './string.js'; +export { resolveType } from './type.js'; diff --git a/packages/lex-cli/src/generator/resolvers/numeric.ts b/packages/lex-cli/src/generator/resolvers/numeric.ts index 643f76a..5477aca 100644 --- a/packages/lex-cli/src/generator/resolvers/numeric.ts +++ b/packages/lex-cli/src/generator/resolvers/numeric.ts @@ -1,17 +1,17 @@ -import type { IntegerSchema } from '../schema.js'; - -export function resolveIntegerType( - def: IntegerSchema, - descs: string[], -): string { - if (def.minimum !== undefined) { - descs.push(`Minimum: ${def.minimum}`); - } - if (def.maximum !== undefined) { - descs.push(`Maximum: ${def.maximum}`); - } - if (def.default !== undefined) { - descs.push(`@default ${def.default}`); - } - return 'number'; -} +import type { IntegerSchema } from '../schema.js'; + +export function resolveIntegerType( + def: IntegerSchema, + descs: string[], +): string { + if (def.minimum !== undefined) { + descs.push(`Minimum: ${def.minimum}`); + } + if (def.maximum !== undefined) { + descs.push(`Maximum: ${def.maximum}`); + } + if (def.default !== undefined) { + descs.push(`@default ${def.default}`); + } + return 'number'; +} diff --git a/packages/lex-cli/src/generator/resolvers/primitives.ts b/packages/lex-cli/src/generator/resolvers/primitives.ts index b8fe08c..4530615 100644 --- a/packages/lex-cli/src/generator/resolvers/primitives.ts +++ b/packages/lex-cli/src/generator/resolvers/primitives.ts @@ -1,14 +1,14 @@ -export function resolvePrimitiveType(type: string): string { - switch (type) { - case 'unknown': - return 'unknown'; - case 'cid-link': - return 'At.CIDLink'; - case 'bytes': - return 'At.Bytes'; - case 'blob': - return 'At.Blob'; - default: - return 'unknown'; - } -} +export function resolvePrimitiveType(type: string): string { + switch (type) { + case 'unknown': + return 'unknown'; + case 'cid-link': + return 'At.CIDLink'; + case 'bytes': + return 'At.Bytes'; + case 'blob': + return 'At.Blob'; + default: + return 'unknown'; + } +} diff --git a/packages/lex-cli/src/generator/resolvers/string.ts b/packages/lex-cli/src/generator/resolvers/string.ts index 3a54667..5a1c738 100644 --- a/packages/lex-cli/src/generator/resolvers/string.ts +++ b/packages/lex-cli/src/generator/resolvers/string.ts @@ -1,49 +1,49 @@ -import type { StringSchema } from '../schema.js'; -import { IGNORED_FORMATS, TYPE_FORMATS, sortName } from '../../utils/index.js'; - -export function resolveStringFormat(format: string, nsid: string): string { - if (format === 'did') return TYPE_FORMATS.DID; - if (format === 'cid') return TYPE_FORMATS.CID; - if (format === 'handle') return TYPE_FORMATS.HANDLE; - if (format === 'at-uri') return TYPE_FORMATS.URI; - if (IGNORED_FORMATS.has(format)) return 'string'; - - console.warn(`${nsid}: unknown format ${format}`); - return 'string'; -} - -export function resolveStringType( - def: StringSchema, - nsid: string, - descs: string[], -): string { - const { format, enum: enums, knownValues: known } = def; - - if (format !== undefined) { - return resolveStringFormat(format, nsid); - } - - if (def.minLength !== undefined) { - descs.push(`Minimum string length: ${def.minLength}`); - } - if (def.maxLength !== undefined) { - descs.push(`Maximum string length: ${def.maxLength}`); - } - if (def.maxGraphemes !== undefined) { - descs.push(`Maximum grapheme length: ${def.maxGraphemes}`); - } - if (def.default !== undefined) { - descs.push(`@default ${JSON.stringify(def.default)}`); - } - - if (enums) { - return enums.map((val: string) => JSON.stringify(val)).join('|'); - } - if (known) { - return `${known - .toSorted(sortName) - .map((val: string) => JSON.stringify(val)) - .join('|')} | (string & {})`; - } - return 'string'; -} +import type { StringSchema } from '../schema.js'; +import { IGNORED_FORMATS, TYPE_FORMATS, sortName } from '../../utils/index.js'; + +export function resolveStringFormat(format: string, nsid: string): string { + if (format === 'did') return TYPE_FORMATS.DID; + if (format === 'cid') return TYPE_FORMATS.CID; + if (format === 'handle') return TYPE_FORMATS.HANDLE; + if (format === 'at-uri') return TYPE_FORMATS.URI; + if (IGNORED_FORMATS.has(format)) return 'string'; + + console.warn(`${nsid}: unknown format ${format}`); + return 'string'; +} + +export function resolveStringType( + def: StringSchema, + nsid: string, + descs: string[], +): string { + const { format, enum: enums, knownValues: known } = def; + + if (format !== undefined) { + return resolveStringFormat(format, nsid); + } + + if (def.minLength !== undefined) { + descs.push(`Minimum string length: ${def.minLength}`); + } + if (def.maxLength !== undefined) { + descs.push(`Maximum string length: ${def.maxLength}`); + } + if (def.maxGraphemes !== undefined) { + descs.push(`Maximum grapheme length: ${def.maxGraphemes}`); + } + if (def.default !== undefined) { + descs.push(`@default ${JSON.stringify(def.default)}`); + } + + if (enums) { + return enums.map((val: string) => JSON.stringify(val)).join('|'); + } + if (known) { + return `${known + .toSorted(sortName) + .map((val: string) => JSON.stringify(val)) + .join('|')} | (string & {})`; + } + return 'string'; +} diff --git a/packages/lex-cli/src/generator/resolvers/type.ts b/packages/lex-cli/src/generator/resolvers/type.ts index cd8e2bd..d08ac65 100644 --- a/packages/lex-cli/src/generator/resolvers/type.ts +++ b/packages/lex-cli/src/generator/resolvers/type.ts @@ -1,99 +1,99 @@ -import type { - RefVariantSchema, - UserTypeSchema, - XrpcParametersSchema, -} from '../schema.js'; -import { getDescriptions } from '../../utils/index.js'; -import { - resolveObjectType, - resolveRefType, - resolveUnionType, -} from './complex.js'; -import { resolveIntegerType } from './numeric.js'; -import { resolvePrimitiveType } from './primitives.js'; -import { resolveStringType } from './string.js'; - -export function resolveType( - nsid: string, - def: UserTypeSchema | RefVariantSchema | XrpcParametersSchema, -): { value: string; descriptions: string[] } { - if (!def?.type) { - throw new Error(`Invalid schema definition for ${nsid}`); - } - - let descs = getDescriptions(def); - let val: string; - - switch (def.type) { - case 'unknown': - case 'cid-link': - case 'blob': - case 'bytes': - val = resolvePrimitiveType(def.type); - break; - case 'integer': - val = resolveIntegerType(def, descs); - break; - case 'boolean': - val = 'boolean'; - if (def.default !== undefined) { - descs.push(`@default ${def.default}`); - } - break; - case 'string': - val = resolveStringType(def, nsid, descs); - break; - case 'array': { - const { value, descriptions } = resolveType(`${nsid}/0`, def.items); - if (def.minLength !== undefined) { - descs.push(`Minimum array length: ${def.minLength}`); - } - if (def.maxLength !== undefined) { - descs.push(`Maximum array length: ${def.maxLength}`); - } - val = `(${value})[]`; - descs = descs.concat(descriptions); - break; - } - case 'ref': - val = resolveRefType(def); - break; - case 'union': - val = resolveUnionType(def); - break; - case 'object': - case 'params': { - const result = resolveObjectType(def, def.type, nsid); - val = result.value; - descs = descs.concat(result.descriptions); - break; - } - case 'subscription': { - const output: { value: string; descriptions: string[] }[] = []; - if (def.parameters) { - output.push(resolveObjectType(def.parameters, 'params', nsid)); - } - if (def.message?.schema) { - output.push(resolveType(nsid, def.message.schema)); - } - if (def.errors) { - output.push({ - value: `interface Errors {${def.errors - .map((error) => `${error.name}: {};`) - .join('')}}`, - descriptions: [], - }); - } - - val = output.map((o) => o.value).join('\n'); - descs = descs.concat(output.flatMap((o) => o.descriptions)); - break; - } - default: - // eslint-disable-next-line no-console - console.log(`${nsid}: unknown type ${def.type}`); - val = 'unknown'; - } - - return { value: val, descriptions: descs }; -} +import type { + RefVariantSchema, + UserTypeSchema, + XrpcParametersSchema, +} from '../schema.js'; +import { getDescriptions } from '../../utils/index.js'; +import { + resolveObjectType, + resolveRefType, + resolveUnionType, +} from './complex.js'; +import { resolveIntegerType } from './numeric.js'; +import { resolvePrimitiveType } from './primitives.js'; +import { resolveStringType } from './string.js'; + +export function resolveType( + nsid: string, + def: UserTypeSchema | RefVariantSchema | XrpcParametersSchema, +): { value: string; descriptions: string[] } { + if (!def?.type) { + throw new Error(`Invalid schema definition for ${nsid}`); + } + + let descs = getDescriptions(def); + let val: string; + + switch (def.type) { + case 'unknown': + case 'cid-link': + case 'blob': + case 'bytes': + val = resolvePrimitiveType(def.type); + break; + case 'integer': + val = resolveIntegerType(def, descs); + break; + case 'boolean': + val = 'boolean'; + if (def.default !== undefined) { + descs.push(`@default ${def.default}`); + } + break; + case 'string': + val = resolveStringType(def, nsid, descs); + break; + case 'array': { + const { value, descriptions } = resolveType(`${nsid}/0`, def.items); + if (def.minLength !== undefined) { + descs.push(`Minimum array length: ${def.minLength}`); + } + if (def.maxLength !== undefined) { + descs.push(`Maximum array length: ${def.maxLength}`); + } + val = `(${value})[]`; + descs = descs.concat(descriptions); + break; + } + case 'ref': + val = resolveRefType(def); + break; + case 'union': + val = resolveUnionType(def); + break; + case 'object': + case 'params': { + const result = resolveObjectType(def, def.type, nsid); + val = result.value; + descs = descs.concat(result.descriptions); + break; + } + case 'subscription': { + const output: { value: string; descriptions: string[] }[] = []; + if (def.parameters) { + output.push(resolveObjectType(def.parameters, 'params', nsid)); + } + if (def.message?.schema) { + output.push(resolveType(nsid, def.message.schema)); + } + if (def.errors) { + output.push({ + value: `interface Errors {${def.errors + .map((error) => `${error.name}: {};`) + .join('')}}`, + descriptions: [], + }); + } + + val = output.map((o) => o.value).join('\n'); + descs = descs.concat(output.flatMap((o) => o.descriptions)); + break; + } + default: + // eslint-disable-next-line no-console + console.log(`${nsid}: unknown type ${def.type}`); + val = 'unknown'; + } + + return { value: val, descriptions: descs }; +} diff --git a/packages/lex-cli/src/generator/schema.ts b/packages/lex-cli/src/generator/schema.ts index 6a7e823..8a365f5 100644 --- a/packages/lex-cli/src/generator/schema.ts +++ b/packages/lex-cli/src/generator/schema.ts @@ -1,408 +1,408 @@ -import * as t from 'typanion'; - -const isPositiveInteger = t.cascade(t.isNumber(), (value): value is number => { - return Number.isInteger(value) && value >= 0; -}); - -export const booleanSchema = t.isObject({ - type: t.isLiteral('boolean'), - description: t.isOptional(t.isString()), - default: t.isOptional(t.isBoolean()), - const: t.isOptional(t.isBoolean()), -}); - -export type BooleanSchema = t.InferType; - -export const integerSchema = t.isObject({ - type: t.isLiteral('integer'), - description: t.isOptional(t.isString()), - default: t.isOptional(isPositiveInteger), - const: t.isOptional(isPositiveInteger), - enum: t.isOptional(t.isArray(t.isNumber())), - maximum: t.isOptional(isPositiveInteger), - minimum: t.isOptional(isPositiveInteger), -}); - -export type IntegerSchema = t.InferType; - -const stringFormatSchema = t.isOneOf([ - t.isLiteral('at-identifier'), - t.isLiteral('at-uri'), - t.isLiteral('cid'), - t.isLiteral('datetime'), - t.isLiteral('did'), - t.isLiteral('handle'), - t.isLiteral('language'), - t.isLiteral('nsid'), - t.isLiteral('record-key'), - t.isLiteral('tid'), - t.isLiteral('uri'), -]); - -export const stringSchema: t.StrictValidator< - unknown, - { - type: 'string'; - description?: string; - format?: string; - default?: string; - const?: string; - enum?: string[]; - knownValues?: string[]; - maxLength?: number; - minLength?: number; - maxGraphemes?: number; - minGraphemes?: number; - } -> = t.cascade( - t.isObject({ - type: t.isLiteral('string'), - description: t.isOptional(t.isString()), - format: t.isOptional(stringFormatSchema), - default: t.isOptional(t.isString()), - const: t.isOptional(t.isString()), - enum: t.isOptional(t.isArray(t.isString())), - knownValues: t.isOptional(t.isArray(t.isString())), - maxLength: t.isOptional(isPositiveInteger), - minLength: t.isOptional(isPositiveInteger), - maxGraphemes: t.isOptional(isPositiveInteger), - minGraphemes: t.isOptional(isPositiveInteger), - }), - ( - value, - ): value is { - type: 'string'; - description?: string; - format?: - | 'at-identifier' - | 'at-uri' - | 'cid' - | 'datetime' - | 'did' - | 'handle' - | 'language' - | 'nsid' - | 'record-key' - | 'tid' - | 'uri'; - default?: string; - const?: string; - enum?: string[]; - knownValues?: string[]; - maxLength?: number; - minLength?: number; - maxGraphemes?: number; - minGraphemes?: number; - } => { - if (value.format !== undefined && value.format !== 'uri') { - if ( - value.maxLength !== undefined || - value.minLength !== undefined || - value.maxGraphemes !== undefined || - value.minGraphemes !== undefined - ) { - throw new Error( - `${value.format} format can't be used with length or grapheme constraints`, - ); - } - } - return true; - }, -); - -export type StringSchema = t.InferType; - -export const unknownSchema = t.isObject({ - type: t.isLiteral('unknown'), - description: t.isOptional(t.isString()), -}); - -export type UnknownSchema = t.InferType; - -export const primitiveSchema = t.isOneOf([ - booleanSchema, - integerSchema, - stringSchema, - unknownSchema, -]); - -export type PrimitiveSchema = t.InferType; - -export const bytesSchema = t.isObject({ - type: t.isLiteral('bytes'), - description: t.isOptional(t.isString()), - maxLength: t.isOptional(isPositiveInteger), - minLength: t.isOptional(isPositiveInteger), -}); - -export type BytesSchema = t.InferType; - -export const cidLinkSchema = t.isObject({ - type: t.isLiteral('cid-link'), - description: t.isOptional(t.isString()), -}); - -export type CidLinkSchema = t.InferType; - -export const ipldTypeSchema = t.isOneOf([bytesSchema, cidLinkSchema]); - -export type IpldTypeSchema = t.InferType; - -export const refSchema = t.isObject({ - type: t.isLiteral('ref'), - description: t.isOptional(t.isString()), - ref: t.isString(), -}); - -export type RefSchema = t.InferType; - -export const refUnionSchema = t.cascade( - t.isObject({ - type: t.isLiteral('union'), - description: t.isOptional(t.isString()), - refs: t.isArray(t.isString()), - closed: t.isOptional(t.isBoolean()), - }), - ( - value, - ): value is { - type: 'union'; - description?: string; - refs: string[]; - closed?: boolean; - } => { - if (value.closed && value.refs.length === 0) { - throw new Error(`A closed union can't have empty refs list`); - } - return true; - }, -); - -export type RefUnionSchema = t.InferType; - -export const refVariantSchema = t.isOneOf([refSchema, refUnionSchema]); - -export type RefVariantSchema = t.InferType; - -export const blobSchema = t.isObject({ - type: t.isLiteral('blob'), - description: t.isOptional(t.isString()), - accept: t.isOptional(t.isArray(t.isString())), - maxSize: t.isOptional(isPositiveInteger), -}); - -export type BlobSchema = t.InferType; - -export const arraySchema = t.isObject({ - type: t.isLiteral('array'), - description: t.isOptional(t.isString()), - items: t.isOneOf([ - primitiveSchema, - ipldTypeSchema, - blobSchema, - refVariantSchema, - ]), - maxLength: t.isOptional(isPositiveInteger), - minLength: t.isOptional(isPositiveInteger), -}); - -export type ArraySchema = t.InferType; - -export const primitiveArraySchema = t.cascade( - arraySchema, - (value): value is ArraySchema => { - if (!t.isOneOf([primitiveSchema])(value.items)) { - throw new Error('Array items must be primitive types'); - } - return true; - }, -); - -export type PrimitiveArraySchema = t.InferType; - -export const tokenSchema = t.isObject({ - type: t.isLiteral('token'), - description: t.isOptional(t.isString()), -}); - -export type TokenSchema = t.InferType; - -function refineRequiredProperties< - T extends { required?: string[]; properties: Record }, ->(schema: t.StrictValidator): t.StrictValidator { - interface RequiredPropertiesSchema { - required?: string[]; - properties: Record; - } - - return t.cascade( - schema, - (value: RequiredPropertiesSchema): value is RequiredPropertiesSchema => { - if (value.required) { - for (const field of value.required) { - if (value.properties[field] === undefined) { - throw new Error(`Required field "${field}" not defined`); - } - } - } - return true; - }, - ); -} - -export const objectSchema = refineRequiredProperties( - t.isObject({ - type: t.isLiteral('object'), - description: t.isOptional(t.isString()), - required: t.isOptional(t.isArray(t.isString())), - nullable: t.isOptional(t.isArray(t.isString())), - properties: t.isRecord( - t.isOneOf([ - refVariantSchema, - ipldTypeSchema, - arraySchema, - blobSchema, - primitiveSchema, - ]), - ), - }), -); - -export type ObjectSchema = t.InferType; - -export const xrpcParametersSchema = refineRequiredProperties( - t.isObject({ - type: t.isLiteral('params'), - description: t.isOptional(t.isString()), - required: t.isOptional(t.isArray(t.isString())), - properties: t.isRecord(t.isOneOf([primitiveSchema, primitiveArraySchema])), - }), -); - -export type XrpcParametersSchema = t.InferType; - -export const xrpcBodySchema = t.isObject({ - description: t.isOptional(t.isString()), - encoding: t.isString(), - schema: t.isOptional(t.isOneOf([refVariantSchema, objectSchema])), -}); - -export type XrpcBodySchema = t.InferType; - -export const xrpcSubscriptionMessageSchema = t.isObject({ - description: t.isOptional(t.isString()), - schema: t.isOptional(t.isOneOf([refVariantSchema, objectSchema])), -}); - -export type XrpcSubscriptionMessageSchema = t.InferType< - typeof xrpcSubscriptionMessageSchema ->; - -export const xrpcErrorSchema = t.isObject({ - name: t.isString(), - description: t.isOptional(t.isString()), -}); - -export type XrpcErrorSchema = t.InferType; - -export const xrpcQuerySchema = t.isObject({ - type: t.isLiteral('query'), - description: t.isOptional(t.isString()), - parameters: t.isOptional(xrpcParametersSchema), - output: t.isOptional(xrpcBodySchema), - errors: t.isOptional(t.isArray(xrpcErrorSchema)), -}); - -export type XrpcQuerySchema = t.InferType; - -export const xrpcProcedureSchema = t.isObject({ - type: t.isLiteral('procedure'), - description: t.isOptional(t.isString()), - parameters: t.isOptional(xrpcParametersSchema), - input: t.isOptional(xrpcBodySchema), - output: t.isOptional(xrpcBodySchema), - errors: t.isOptional(t.isArray(xrpcErrorSchema)), -}); - -export type XrpcProcedureSchema = t.InferType; - -export const xrpcSubscriptionSchema = t.isObject({ - type: t.isLiteral('subscription'), - description: t.isOptional(t.isString()), - parameters: t.isOptional(xrpcParametersSchema), - message: t.isOptional(xrpcSubscriptionMessageSchema), - errors: t.isOptional(t.isArray(xrpcErrorSchema)), -}); - -export type XrpcSubscriptionSchema = t.InferType; - -export const recordSchema = t.isObject({ - type: t.isLiteral('record'), - description: t.isOptional(t.isString()), - key: t.isOptional(t.isString()), - record: objectSchema, -}); - -export type RecordSchema = t.InferType; - -export const userTypeSchema = t.isOneOf([ - recordSchema, - xrpcQuerySchema, - xrpcProcedureSchema, - xrpcSubscriptionSchema, - blobSchema, - arraySchema, - tokenSchema, - objectSchema, - booleanSchema, - integerSchema, - stringSchema, - bytesSchema, - cidLinkSchema, - unknownSchema, -]); - -export type UserTypeSchema = t.InferType; - -const NSID_RE = - /^[a-z](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)+\.[a-z]{1,63}$/i; -const nsidType = t.cascade(t.isString(), (value) => NSID_RE.test(value)); - -export const documentSchema = t.cascade( - t.isObject({ - lexicon: t.isLiteral(1), - id: nsidType, - revision: t.isOptional(t.isNumber()), - description: t.isOptional(t.isString()), - defs: t.isRecord(userTypeSchema), - }), - ( - value, - ): value is { - lexicon: 1; - id: string; - revision?: number; - description?: string; - defs: Record; - } => { - for (const id in value.defs) { - const def = value.defs[id]; - const type = def.type; - - if ( - id !== 'main' && - (type === 'record' || - type === 'query' || - type === 'procedure' || - type === 'subscription') - ) { - throw new Error( - `${type} must be the \`main\` definition (in defs.${id})`, - ); - } - } - return true; - }, -); - -export type DocumentSchema = t.InferType; +import * as t from 'typanion'; + +const isPositiveInteger = t.cascade(t.isNumber(), (value): value is number => { + return Number.isInteger(value) && value >= 0; +}); + +export const booleanSchema = t.isObject({ + type: t.isLiteral('boolean'), + description: t.isOptional(t.isString()), + default: t.isOptional(t.isBoolean()), + const: t.isOptional(t.isBoolean()), +}); + +export type BooleanSchema = t.InferType; + +export const integerSchema = t.isObject({ + type: t.isLiteral('integer'), + description: t.isOptional(t.isString()), + default: t.isOptional(isPositiveInteger), + const: t.isOptional(isPositiveInteger), + enum: t.isOptional(t.isArray(t.isNumber())), + maximum: t.isOptional(isPositiveInteger), + minimum: t.isOptional(isPositiveInteger), +}); + +export type IntegerSchema = t.InferType; + +const stringFormatSchema = t.isOneOf([ + t.isLiteral('at-identifier'), + t.isLiteral('at-uri'), + t.isLiteral('cid'), + t.isLiteral('datetime'), + t.isLiteral('did'), + t.isLiteral('handle'), + t.isLiteral('language'), + t.isLiteral('nsid'), + t.isLiteral('record-key'), + t.isLiteral('tid'), + t.isLiteral('uri'), +]); + +export const stringSchema: t.StrictValidator< + unknown, + { + type: 'string'; + description?: string; + format?: string; + default?: string; + const?: string; + enum?: string[]; + knownValues?: string[]; + maxLength?: number; + minLength?: number; + maxGraphemes?: number; + minGraphemes?: number; + } +> = t.cascade( + t.isObject({ + type: t.isLiteral('string'), + description: t.isOptional(t.isString()), + format: t.isOptional(stringFormatSchema), + default: t.isOptional(t.isString()), + const: t.isOptional(t.isString()), + enum: t.isOptional(t.isArray(t.isString())), + knownValues: t.isOptional(t.isArray(t.isString())), + maxLength: t.isOptional(isPositiveInteger), + minLength: t.isOptional(isPositiveInteger), + maxGraphemes: t.isOptional(isPositiveInteger), + minGraphemes: t.isOptional(isPositiveInteger), + }), + ( + value, + ): value is { + type: 'string'; + description?: string; + format?: + | 'at-identifier' + | 'at-uri' + | 'cid' + | 'datetime' + | 'did' + | 'handle' + | 'language' + | 'nsid' + | 'record-key' + | 'tid' + | 'uri'; + default?: string; + const?: string; + enum?: string[]; + knownValues?: string[]; + maxLength?: number; + minLength?: number; + maxGraphemes?: number; + minGraphemes?: number; + } => { + if (value.format !== undefined && value.format !== 'uri') { + if ( + value.maxLength !== undefined || + value.minLength !== undefined || + value.maxGraphemes !== undefined || + value.minGraphemes !== undefined + ) { + throw new Error( + `${value.format} format can't be used with length or grapheme constraints`, + ); + } + } + return true; + }, +); + +export type StringSchema = t.InferType; + +export const unknownSchema = t.isObject({ + type: t.isLiteral('unknown'), + description: t.isOptional(t.isString()), +}); + +export type UnknownSchema = t.InferType; + +export const primitiveSchema = t.isOneOf([ + booleanSchema, + integerSchema, + stringSchema, + unknownSchema, +]); + +export type PrimitiveSchema = t.InferType; + +export const bytesSchema = t.isObject({ + type: t.isLiteral('bytes'), + description: t.isOptional(t.isString()), + maxLength: t.isOptional(isPositiveInteger), + minLength: t.isOptional(isPositiveInteger), +}); + +export type BytesSchema = t.InferType; + +export const cidLinkSchema = t.isObject({ + type: t.isLiteral('cid-link'), + description: t.isOptional(t.isString()), +}); + +export type CidLinkSchema = t.InferType; + +export const ipldTypeSchema = t.isOneOf([bytesSchema, cidLinkSchema]); + +export type IpldTypeSchema = t.InferType; + +export const refSchema = t.isObject({ + type: t.isLiteral('ref'), + description: t.isOptional(t.isString()), + ref: t.isString(), +}); + +export type RefSchema = t.InferType; + +export const refUnionSchema = t.cascade( + t.isObject({ + type: t.isLiteral('union'), + description: t.isOptional(t.isString()), + refs: t.isArray(t.isString()), + closed: t.isOptional(t.isBoolean()), + }), + ( + value, + ): value is { + type: 'union'; + description?: string; + refs: string[]; + closed?: boolean; + } => { + if (value.closed && value.refs.length === 0) { + throw new Error(`A closed union can't have empty refs list`); + } + return true; + }, +); + +export type RefUnionSchema = t.InferType; + +export const refVariantSchema = t.isOneOf([refSchema, refUnionSchema]); + +export type RefVariantSchema = t.InferType; + +export const blobSchema = t.isObject({ + type: t.isLiteral('blob'), + description: t.isOptional(t.isString()), + accept: t.isOptional(t.isArray(t.isString())), + maxSize: t.isOptional(isPositiveInteger), +}); + +export type BlobSchema = t.InferType; + +export const arraySchema = t.isObject({ + type: t.isLiteral('array'), + description: t.isOptional(t.isString()), + items: t.isOneOf([ + primitiveSchema, + ipldTypeSchema, + blobSchema, + refVariantSchema, + ]), + maxLength: t.isOptional(isPositiveInteger), + minLength: t.isOptional(isPositiveInteger), +}); + +export type ArraySchema = t.InferType; + +export const primitiveArraySchema = t.cascade( + arraySchema, + (value): value is ArraySchema => { + if (!t.isOneOf([primitiveSchema])(value.items)) { + throw new Error('Array items must be primitive types'); + } + return true; + }, +); + +export type PrimitiveArraySchema = t.InferType; + +export const tokenSchema = t.isObject({ + type: t.isLiteral('token'), + description: t.isOptional(t.isString()), +}); + +export type TokenSchema = t.InferType; + +function refineRequiredProperties< + T extends { required?: string[]; properties: Record }, +>(schema: t.StrictValidator): t.StrictValidator { + interface RequiredPropertiesSchema { + required?: string[]; + properties: Record; + } + + return t.cascade( + schema, + (value: RequiredPropertiesSchema): value is RequiredPropertiesSchema => { + if (value.required) { + for (const field of value.required) { + if (value.properties[field] === undefined) { + throw new Error(`Required field "${field}" not defined`); + } + } + } + return true; + }, + ); +} + +export const objectSchema = refineRequiredProperties( + t.isObject({ + type: t.isLiteral('object'), + description: t.isOptional(t.isString()), + required: t.isOptional(t.isArray(t.isString())), + nullable: t.isOptional(t.isArray(t.isString())), + properties: t.isRecord( + t.isOneOf([ + refVariantSchema, + ipldTypeSchema, + arraySchema, + blobSchema, + primitiveSchema, + ]), + ), + }), +); + +export type ObjectSchema = t.InferType; + +export const xrpcParametersSchema = refineRequiredProperties( + t.isObject({ + type: t.isLiteral('params'), + description: t.isOptional(t.isString()), + required: t.isOptional(t.isArray(t.isString())), + properties: t.isRecord(t.isOneOf([primitiveSchema, primitiveArraySchema])), + }), +); + +export type XrpcParametersSchema = t.InferType; + +export const xrpcBodySchema = t.isObject({ + description: t.isOptional(t.isString()), + encoding: t.isString(), + schema: t.isOptional(t.isOneOf([refVariantSchema, objectSchema])), +}); + +export type XrpcBodySchema = t.InferType; + +export const xrpcSubscriptionMessageSchema = t.isObject({ + description: t.isOptional(t.isString()), + schema: t.isOptional(t.isOneOf([refVariantSchema, objectSchema])), +}); + +export type XrpcSubscriptionMessageSchema = t.InferType< + typeof xrpcSubscriptionMessageSchema +>; + +export const xrpcErrorSchema = t.isObject({ + name: t.isString(), + description: t.isOptional(t.isString()), +}); + +export type XrpcErrorSchema = t.InferType; + +export const xrpcQuerySchema = t.isObject({ + type: t.isLiteral('query'), + description: t.isOptional(t.isString()), + parameters: t.isOptional(xrpcParametersSchema), + output: t.isOptional(xrpcBodySchema), + errors: t.isOptional(t.isArray(xrpcErrorSchema)), +}); + +export type XrpcQuerySchema = t.InferType; + +export const xrpcProcedureSchema = t.isObject({ + type: t.isLiteral('procedure'), + description: t.isOptional(t.isString()), + parameters: t.isOptional(xrpcParametersSchema), + input: t.isOptional(xrpcBodySchema), + output: t.isOptional(xrpcBodySchema), + errors: t.isOptional(t.isArray(xrpcErrorSchema)), +}); + +export type XrpcProcedureSchema = t.InferType; + +export const xrpcSubscriptionSchema = t.isObject({ + type: t.isLiteral('subscription'), + description: t.isOptional(t.isString()), + parameters: t.isOptional(xrpcParametersSchema), + message: t.isOptional(xrpcSubscriptionMessageSchema), + errors: t.isOptional(t.isArray(xrpcErrorSchema)), +}); + +export type XrpcSubscriptionSchema = t.InferType; + +export const recordSchema = t.isObject({ + type: t.isLiteral('record'), + description: t.isOptional(t.isString()), + key: t.isOptional(t.isString()), + record: objectSchema, +}); + +export type RecordSchema = t.InferType; + +export const userTypeSchema = t.isOneOf([ + recordSchema, + xrpcQuerySchema, + xrpcProcedureSchema, + xrpcSubscriptionSchema, + blobSchema, + arraySchema, + tokenSchema, + objectSchema, + booleanSchema, + integerSchema, + stringSchema, + bytesSchema, + cidLinkSchema, + unknownSchema, +]); + +export type UserTypeSchema = t.InferType; + +const NSID_RE = + /^[a-z](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)+\.[a-z]{1,63}$/i; +const nsidType = t.cascade(t.isString(), (value) => NSID_RE.test(value)); + +export const documentSchema = t.cascade( + t.isObject({ + lexicon: t.isLiteral(1), + id: nsidType, + revision: t.isOptional(t.isNumber()), + description: t.isOptional(t.isString()), + defs: t.isRecord(userTypeSchema), + }), + ( + value, + ): value is { + lexicon: 1; + id: string; + revision?: number; + description?: string; + defs: Record; + } => { + for (const id in value.defs) { + const def = value.defs[id]; + const type = def.type; + + if ( + id !== 'main' && + (type === 'record' || + type === 'query' || + type === 'procedure' || + type === 'subscription') + ) { + throw new Error( + `${type} must be the \`main\` definition (in defs.${id})`, + ); + } + } + return true; + }, +); + +export type DocumentSchema = t.InferType; diff --git a/packages/lex-cli/src/index.ts b/packages/lex-cli/src/index.ts index 9bb65d0..c90b431 100644 --- a/packages/lex-cli/src/index.ts +++ b/packages/lex-cli/src/index.ts @@ -1,117 +1,117 @@ -import { writeFile } from 'node:fs/promises'; -import process from 'node:process'; -import { Builtins, Cli, Command, Option } from 'clipanion'; -import pc from 'picocolors'; -import prettier from 'prettier'; -import * as t from 'typanion'; -import { generateDefinitions } from './generator/index.js'; - -const cli = new Cli({ - binaryName: 'lex-cli', - binaryVersion: '0.0.0', -}); - -cli.register(Builtins.HelpCommand); - -cli.register( - class GenerateTypes extends Command { - static paths = [['generate-types']]; - - static usage = Command.Usage({ - description: - 'Generates TypeScript type definitions from Lexicon schema files', - details: ` - This command takes Lexicon JSON schema files as input and generates corresponding TypeScript - type definitions. It handles all AT Protocol lexicons (app.bsky, com.atproto, etc.) and - outputs a single consolidated TypeScript declaration file. - `, - examples: [ - [ - 'Basic usage', - 'lex-cli generate-types ./lexicons/**/*.json -o types.ts', - ], - [ - 'With module description', - 'lex-cli generate-types ./lexicons/**/*.json -o types.ts --description "AT Protocol Types"', - ], - ], - }); - - output = Option.String('-o,--output', { - required: false, - description: - 'Path for the generated TypeScript definition file. If not specified, outputs to stdout', - validator: t.cascade(t.isString(), t.matchesRegExp(/\.ts$/)), - }); - - desc = Option.String('--description', { - required: false, - description: 'JSDoc description to add to the generated module', - }); - - banner = Option.String('--banner', { - required: false, - description: - 'Custom banner text to insert at the top of the generated file', - }); - - metadata = Option.String('--metadata', { - required: false, - description: 'JSON metadata to add to the generated file header', - }); - - files = Option.Rest({ - required: 1, - name: 'files', - }); - - async execute(): Promise { - let code: string; - - try { - code = await generateDefinitions({ - files: this.files, - banner: this.banner, - description: this.desc, - lexiconMetadata: this.metadata - ? JSON.parse(this.metadata) - : undefined, - }); - } catch (err) { - if (err instanceof Error) { - console.error(pc.bold(`${pc.red('error:')} ${err.message}`)); - - if (err.cause instanceof Error) { - console.error(` ${pc.gray('caused by:')} ${err.cause.message}`); - } - } else { - console.error(pc.bold(pc.red('unknown error occurred:'))); - console.error(err); - } - - return 1; - } - - const config = await prettier.resolveConfig( - this.output || process.cwd(), - { editorconfig: true }, - ); - const formatted = await prettier.format(code, { - ...config, - parser: 'typescript', - }); - - if (this.output) { - await writeFile(this.output, formatted); - } else { - // eslint-disable-next-line no-console - console.log(formatted); - } - } - }, -); - -(async () => { - const exitCode = await cli.run(process.argv.slice(2)); - process.exitCode = exitCode; -})(); +import { writeFile } from 'node:fs/promises'; +import process from 'node:process'; +import { Builtins, Cli, Command, Option } from 'clipanion'; +import pc from 'picocolors'; +import prettier from 'prettier'; +import * as t from 'typanion'; +import { generateDefinitions } from './generator/index.js'; + +const cli = new Cli({ + binaryName: 'lex-cli', + binaryVersion: '0.0.0', +}); + +cli.register(Builtins.HelpCommand); + +cli.register( + class GenerateTypes extends Command { + static paths = [['generate-types']]; + + static usage = Command.Usage({ + description: + 'Generates TypeScript type definitions from Lexicon schema files', + details: ` + This command takes Lexicon JSON schema files as input and generates corresponding TypeScript + type definitions. It handles all AT Protocol lexicons (app.bsky, com.atproto, etc.) and + outputs a single consolidated TypeScript declaration file. + `, + examples: [ + [ + 'Basic usage', + 'lex-cli generate-types ./lexicons/**/*.json -o types.ts', + ], + [ + 'With module description', + 'lex-cli generate-types ./lexicons/**/*.json -o types.ts --description "AT Protocol Types"', + ], + ], + }); + + output = Option.String('-o,--output', { + required: false, + description: + 'Path for the generated TypeScript definition file. If not specified, outputs to stdout', + validator: t.cascade(t.isString(), t.matchesRegExp(/\.ts$/)), + }); + + desc = Option.String('--description', { + required: false, + description: 'JSDoc description to add to the generated module', + }); + + banner = Option.String('--banner', { + required: false, + description: + 'Custom banner text to insert at the top of the generated file', + }); + + metadata = Option.String('--metadata', { + required: false, + description: 'JSON metadata to add to the generated file header', + }); + + files = Option.Rest({ + required: 1, + name: 'files', + }); + + async execute(): Promise { + let code: string; + + try { + code = await generateDefinitions({ + files: this.files, + banner: this.banner, + description: this.desc, + lexiconMetadata: this.metadata + ? JSON.parse(this.metadata) + : undefined, + }); + } catch (err) { + if (err instanceof Error) { + console.error(pc.bold(`${pc.red('error:')} ${err.message}`)); + + if (err.cause instanceof Error) { + console.error(` ${pc.gray('caused by:')} ${err.cause.message}`); + } + } else { + console.error(pc.bold(pc.red('unknown error occurred:'))); + console.error(err); + } + + return 1; + } + + const config = await prettier.resolveConfig( + this.output || process.cwd(), + { editorconfig: true }, + ); + const formatted = await prettier.format(code, { + ...config, + parser: 'typescript', + }); + + if (this.output) { + await writeFile(this.output, formatted); + } else { + // eslint-disable-next-line no-console + console.log(formatted); + } + } + }, +); + +(async () => { + const exitCode = await cli.run(process.argv.slice(2)); + process.exitCode = exitCode; +})(); diff --git a/packages/lex-cli/src/utils/cache.ts b/packages/lex-cli/src/utils/cache.ts index 2bf247b..73adc94 100644 --- a/packages/lex-cli/src/utils/cache.ts +++ b/packages/lex-cli/src/utils/cache.ts @@ -1,26 +1,26 @@ -export const toUpperCache: Record = Object.create( - null, -) as Record; -export const toNamespaceCache: Record = Object.create( - null, -) as Record; - -export function toUpper(s: string) { - if (s in toUpperCache) { - return toUpperCache[s]; - } - const value = s[0].toUpperCase() + s.slice(1); - toUpperCache[s] = value; - return value; -} - -export function toNamespace(s: string) { - if (s in toNamespaceCache) { - return toNamespaceCache[s]; - } - const value = s.replace(/^\w|\.\w/g, (m) => - m[m.length === 1 ? 0 : 1].toUpperCase(), - ); - toNamespaceCache[s] = value; - return value; -} +export const toUpperCache: Record = Object.create( + null, +) as Record; +export const toNamespaceCache: Record = Object.create( + null, +) as Record; + +export function toUpper(s: string) { + if (s in toUpperCache) { + return toUpperCache[s]; + } + const value = s[0].toUpperCase() + s.slice(1); + toUpperCache[s] = value; + return value; +} + +export function toNamespace(s: string) { + if (s in toNamespaceCache) { + return toNamespaceCache[s]; + } + const value = s.replace(/^\w|\.\w/g, (m) => + m[m.length === 1 ? 0 : 1].toUpperCase(), + ); + toNamespaceCache[s] = value; + return value; +} diff --git a/packages/lex-cli/src/utils/docs.ts b/packages/lex-cli/src/utils/docs.ts index ab10772..d4b1e21 100644 --- a/packages/lex-cli/src/utils/docs.ts +++ b/packages/lex-cli/src/utils/docs.ts @@ -1,24 +1,24 @@ -export function getDescriptions(def: { description?: string }): string[] { - const descs: string[] = []; - if (def.description) { - descs.push(def.description); - if (def.description.toLowerCase().startsWith('deprecated')) { - descs.push('@deprecated'); - } - } - return descs; -} - -export function writeJsdoc(descriptions: string[]) { - if (!descriptions.length) return ''; - - const escaped = descriptions.map((desc) => - desc.replace(/\*\//g, '*\\/').replace(/@/g, '\\@'), - ); - - if (escaped.length === 1) { - return `\n/** ${escaped[0]} */\n`; - } - - return `\n/**${escaped.map((desc) => `\n * ${desc}`).join('')}\n */\n`; -} +export function getDescriptions(def: { description?: string }): string[] { + const descs: string[] = []; + if (def.description) { + descs.push(def.description); + if (def.description.toLowerCase().startsWith('deprecated')) { + descs.push('@deprecated'); + } + } + return descs; +} + +export function writeJsdoc(descriptions: string[]) { + if (!descriptions.length) return ''; + + const escaped = descriptions.map((desc) => + desc.replace(/\*\//g, '*\\/').replace(/@/g, '\\@'), + ); + + if (escaped.length === 1) { + return `\n/** ${escaped[0]} */\n`; + } + + return `\n/**${escaped.map((desc) => `\n * ${desc}`).join('')}\n */\n`; +} diff --git a/packages/lex-cli/src/utils/formats.ts b/packages/lex-cli/src/utils/formats.ts index 30fccd8..3834f40 100644 --- a/packages/lex-cli/src/utils/formats.ts +++ b/packages/lex-cli/src/utils/formats.ts @@ -1,14 +1,14 @@ -export const TYPE_FORMATS = { - DID: 'At.DID', - CID: 'At.CID', - HANDLE: 'At.Handle', - URI: 'At.Uri', -} as const; - -export const IGNORED_FORMATS = new Set([ - 'at-identifier', - 'datetime', - 'language', - 'nsid', - 'uri', -]); +export const TYPE_FORMATS = { + DID: 'At.DID', + CID: 'At.CID', + HANDLE: 'At.Handle', + URI: 'At.Uri', +} as const; + +export const IGNORED_FORMATS = new Set([ + 'at-identifier', + 'datetime', + 'language', + 'nsid', + 'uri', +]); diff --git a/packages/lex-cli/src/utils/index.ts b/packages/lex-cli/src/utils/index.ts index bcaae1e..c73ad3c 100644 --- a/packages/lex-cli/src/utils/index.ts +++ b/packages/lex-cli/src/utils/index.ts @@ -1,5 +1,5 @@ -export { toNamespace, toUpper } from './cache.js'; -export { getDescriptions, writeJsdoc } from './docs.js'; -export { IGNORED_FORMATS, TYPE_FORMATS } from './formats.js'; -export { mainPrelude } from './prelude.js'; -export { sortDefinition, sortName, sortPropertyKeys } from './sort.js'; +export { toNamespace, toUpper } from './cache.js'; +export { getDescriptions, writeJsdoc } from './docs.js'; +export { IGNORED_FORMATS, TYPE_FORMATS } from './formats.js'; +export { mainPrelude } from './prelude.js'; +export { sortDefinition, sortName, sortPropertyKeys } from './sort.js'; diff --git a/packages/lex-cli/src/utils/prelude.ts b/packages/lex-cli/src/utils/prelude.ts index 3102284..045bf45 100644 --- a/packages/lex-cli/src/utils/prelude.ts +++ b/packages/lex-cli/src/utils/prelude.ts @@ -1,57 +1,57 @@ -export const mainPrelude = `/** Base type with optional type field */ -export interface TypedBase { - $type?: string; -} - -/** Base type for all record types */ -export interface RecordBase { - $type: string; -} - -/** Makes $type required and specific */ -export type Typed = Omit & { - $type: Type; -}; - -/** Creates a union of objects discriminated by $type */ -export type TypeUnion = T extends any ? Typed : never; - -/** Type guard for records */ -export function isRecord(value: unknown): value is RecordBase { - return typeof value === 'object' && value !== null && '$type' in value && typeof value.$type === 'string'; -} - -/** Base AT Protocol schema types */ -export declare namespace At { - /** CID string */ - type CID = string; - - /** DID of a user */ - type DID = \`did:\${string}\`; - - /** User handle */ - type Handle = string; - - /** URI string */ - type Uri = string; - - /** Object containing a CID string */ - interface CIDLink { - $link: CID; - } - - /** Object containing a base64-encoded bytes */ - interface Bytes { - $bytes: string; - } - - /** Blob interface */ - interface Blob extends RecordBase { - $type: 'blob'; - mimeType: T; - ref: { - $link: string; - }; - size: number; - } -}`; +export const mainPrelude = `/** Base type with optional type field */ +export interface TypedBase { + $type?: string; +} + +/** Base type for all record types */ +export interface RecordBase { + $type: string; +} + +/** Makes $type required and specific */ +export type Typed = Omit & { + $type: Type; +}; + +/** Creates a union of objects discriminated by $type */ +export type TypeUnion = T extends any ? Typed : never; + +/** Type guard for records */ +export function isRecord(value: unknown): value is RecordBase { + return typeof value === 'object' && value !== null && '$type' in value && typeof value.$type === 'string'; +} + +/** Base AT Protocol schema types */ +export declare namespace At { + /** CID string */ + type CID = string; + + /** DID of a user */ + type DID = \`did:\${string}\`; + + /** User handle */ + type Handle = string; + + /** URI string */ + type Uri = string; + + /** Object containing a CID string */ + interface CIDLink { + $link: CID; + } + + /** Object containing a base64-encoded bytes */ + interface Bytes { + $bytes: string; + } + + /** Blob interface */ + interface Blob extends RecordBase { + $type: 'blob'; + mimeType: T; + ref: { + $link: string; + }; + size: number; + } +}`; diff --git a/packages/lex-cli/src/utils/sort.ts b/packages/lex-cli/src/utils/sort.ts index 8f1bbc1..5a43e89 100644 --- a/packages/lex-cli/src/utils/sort.ts +++ b/packages/lex-cli/src/utils/sort.ts @@ -1,28 +1,28 @@ -const COLLATOR = new Intl.Collator('en-US'); -export function sortName(a: string, b: string): number { - return COLLATOR.compare(a, b); -} - -export function sortPropertyKeys( - keys: string[], - required?: string[], -): string[] { - return keys.sort((a, b) => { - const aIsOptional = !required || !required.includes(a); - const bIsOptional = !required || !required.includes(b); - if (aIsOptional === bIsOptional) { - return sortName(a, b); - } - return +aIsOptional - +bIsOptional; - }); -} - -export function sortDefinition(a: string, b: string) { - const aIsMain = a === 'main'; - const bIsMain = b === 'main'; - - if (aIsMain === bIsMain) { - return sortName(a, b); - } - return +bIsMain - +aIsMain; -} +const COLLATOR = new Intl.Collator('en-US'); +export function sortName(a: string, b: string): number { + return COLLATOR.compare(a, b); +} + +export function sortPropertyKeys( + keys: string[], + required?: string[], +): string[] { + return keys.sort((a, b) => { + const aIsOptional = !required || !required.includes(a); + const bIsOptional = !required || !required.includes(b); + if (aIsOptional === bIsOptional) { + return sortName(a, b); + } + return +aIsOptional - +bIsOptional; + }); +} + +export function sortDefinition(a: string, b: string) { + const aIsMain = a === 'main'; + const bIsMain = b === 'main'; + + if (aIsMain === bIsMain) { + return sortName(a, b); + } + return +bIsMain - +aIsMain; +} diff --git a/packages/lexicons/index.ts b/packages/lexicons/index.ts index ec5a8e3..4df73b3 100644 --- a/packages/lexicons/index.ts +++ b/packages/lexicons/index.ts @@ -1 +1 @@ -export * from './src/index.js'; +export * from './src/index.js'; diff --git a/packages/lexicons/scripts/check-version-change.ts b/packages/lexicons/scripts/check-version-change.ts index 59119c4..ebf6c23 100644 --- a/packages/lexicons/scripts/check-version-change.ts +++ b/packages/lexicons/scripts/check-version-change.ts @@ -1,31 +1,31 @@ -import { execSync } from 'node:child_process'; -import path from 'node:path'; -import process from 'node:process'; -import { fileURLToPath } from 'node:url'; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); - -const TYPES_OUTPUT_PATH = path.resolve(__dirname, '../src/lib/lexicons.ts'); - -async function main() { - try { - const command = [ - 'git diff --unified=0', - TYPES_OUTPUT_PATH, - '| grep -q "* Source:"', - '&& echo yes', - '|| echo no', - ].join(' '); - - execSync(command, { - stdio: 'inherit', - env: process.env, - }); - } catch (error) { - console.error('Error:', error); - process.exit(1); - } -} - -main(); +import { execSync } from 'node:child_process'; +import path from 'node:path'; +import process from 'node:process'; +import { fileURLToPath } from 'node:url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +const TYPES_OUTPUT_PATH = path.resolve(__dirname, '../src/lib/lexicons.ts'); + +async function main() { + try { + const command = [ + 'git diff --unified=0', + TYPES_OUTPUT_PATH, + '| grep -q "* Source:"', + '&& echo yes', + '|| echo no', + ].join(' '); + + execSync(command, { + stdio: 'inherit', + env: process.env, + }); + } catch (error) { + console.error('Error:', error); + process.exit(1); + } +} + +main(); diff --git a/packages/lexicons/scripts/generate-types.ts b/packages/lexicons/scripts/generate-types.ts index 8d3e112..881638a 100644 --- a/packages/lexicons/scripts/generate-types.ts +++ b/packages/lexicons/scripts/generate-types.ts @@ -1,118 +1,118 @@ -import { Buffer } from 'node:buffer'; -import { execSync } from 'node:child_process'; -import fs from 'node:fs/promises'; -import path from 'node:path'; -import process from 'node:process'; -import { fileURLToPath } from 'node:url'; -import glob from 'fast-glob'; -import * as tar from 'tar'; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); - -const LEXICONS_DIR = path.resolve(__dirname, '../lexicons'); -const TYPES_OUTPUT_PATH = path.resolve(__dirname, '../src/lib/lexicons.ts'); -const LEX_CLI_PATH = path.resolve(__dirname, '../../lex-cli/dist/index.js'); -const REPO = 'bluesky-social/atproto'; - -async function downloadLexicons() { - console.log('Getting latest lexicon commit...'); - const shaResponse = await fetch( - `https://api.github.com/repos/${REPO}/commits?path=lexicons/`, - ); - if (!shaResponse.ok) { - throw new Error(`Failed to get commit SHA: ${shaResponse.statusText}`); - } - - const commits = await shaResponse.json(); - const sha = commits[0]?.sha; - const version = - commits[0]?.commit?.message?.match(/Release v([\d.]+)/)?.[1] || 'main'; - - if (!sha) { - throw new Error('No commits found for lexicons'); - } - - console.log('Downloading lexicons from atproto...'); - const response = await fetch( - `https://github.com/${REPO}/archive/${sha}.tar.gz`, - ); - if (!response.ok) { - throw new Error(`Failed to download lexicons: ${response.statusText}`); - } - - const tarFile = path.join(LEXICONS_DIR, 'atproto.tar.gz'); - await fs.writeFile(tarFile, Buffer.from(await response.arrayBuffer())); - - await tar.x({ - file: tarFile, - cwd: LEXICONS_DIR, - filter: (path) => path.includes('/lexicons/'), - strip: 2, - }); - - await fs.unlink(tarFile); - - return { - sha, - version, - sourceUrl: `https://github.com/${REPO}/tree/${sha}/lexicons`, - }; -} - -async function main() { - try { - await fs.mkdir(LEXICONS_DIR, { recursive: true }); - await fs.mkdir(path.dirname(TYPES_OUTPUT_PATH), { recursive: true }); - - const metadata = await downloadLexicons(); - - const globPatterns = [ - 'app/bsky/**/*.json', - 'chat/bsky/**/*.json', - 'com/atproto/**/*.json', - 'tools/ozone/**/*.json', - ]; - - const lexiconFiles = await glob(globPatterns, { - cwd: LEXICONS_DIR, - absolute: true, - }); - - if (lexiconFiles.length === 0) { - throw new Error('No lexicon files found'); - } - - console.log('Building lex-cli...'); - execSync('pnpm --filter @tsky/lex-cli build', { - stdio: 'inherit', - env: process.env, - }); - - const command = [ - 'node', - LEX_CLI_PATH, - 'generate-types', - ...lexiconFiles, - '-o', - TYPES_OUTPUT_PATH, - '--description', - '"Contains type declarations for Bluesky lexicons"', - '--metadata', - JSON.stringify(JSON.stringify(metadata)), - ].join(' '); - - console.log('Running lex-cli to generate types...'); - execSync(command, { - stdio: 'inherit', - env: process.env, - }); - - console.log('Done! Types generated at', TYPES_OUTPUT_PATH); - } catch (error) { - console.error('Error:', error); - process.exit(1); - } -} - -main(); +import { Buffer } from 'node:buffer'; +import { execSync } from 'node:child_process'; +import fs from 'node:fs/promises'; +import path from 'node:path'; +import process from 'node:process'; +import { fileURLToPath } from 'node:url'; +import glob from 'fast-glob'; +import * as tar from 'tar'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +const LEXICONS_DIR = path.resolve(__dirname, '../lexicons'); +const TYPES_OUTPUT_PATH = path.resolve(__dirname, '../src/lib/lexicons.ts'); +const LEX_CLI_PATH = path.resolve(__dirname, '../../lex-cli/dist/index.js'); +const REPO = 'bluesky-social/atproto'; + +async function downloadLexicons() { + console.log('Getting latest lexicon commit...'); + const shaResponse = await fetch( + `https://api.github.com/repos/${REPO}/commits?path=lexicons/`, + ); + if (!shaResponse.ok) { + throw new Error(`Failed to get commit SHA: ${shaResponse.statusText}`); + } + + const commits = await shaResponse.json(); + const sha = commits[0]?.sha; + const version = + commits[0]?.commit?.message?.match(/Release v([\d.]+)/)?.[1] || 'main'; + + if (!sha) { + throw new Error('No commits found for lexicons'); + } + + console.log('Downloading lexicons from atproto...'); + const response = await fetch( + `https://github.com/${REPO}/archive/${sha}.tar.gz`, + ); + if (!response.ok) { + throw new Error(`Failed to download lexicons: ${response.statusText}`); + } + + const tarFile = path.join(LEXICONS_DIR, 'atproto.tar.gz'); + await fs.writeFile(tarFile, Buffer.from(await response.arrayBuffer())); + + await tar.x({ + file: tarFile, + cwd: LEXICONS_DIR, + filter: (path) => path.includes('/lexicons/'), + strip: 2, + }); + + await fs.unlink(tarFile); + + return { + sha, + version, + sourceUrl: `https://github.com/${REPO}/tree/${sha}/lexicons`, + }; +} + +async function main() { + try { + await fs.mkdir(LEXICONS_DIR, { recursive: true }); + await fs.mkdir(path.dirname(TYPES_OUTPUT_PATH), { recursive: true }); + + const metadata = await downloadLexicons(); + + const globPatterns = [ + 'app/bsky/**/*.json', + 'chat/bsky/**/*.json', + 'com/atproto/**/*.json', + 'tools/ozone/**/*.json', + ]; + + const lexiconFiles = await glob(globPatterns, { + cwd: LEXICONS_DIR, + absolute: true, + }); + + if (lexiconFiles.length === 0) { + throw new Error('No lexicon files found'); + } + + console.log('Building lex-cli...'); + execSync('pnpm --filter @tsky/lex-cli build', { + stdio: 'inherit', + env: process.env, + }); + + const command = [ + 'node', + LEX_CLI_PATH, + 'generate-types', + ...lexiconFiles, + '-o', + TYPES_OUTPUT_PATH, + '--description', + '"Contains type declarations for Bluesky lexicons"', + '--metadata', + JSON.stringify(JSON.stringify(metadata)), + ].join(' '); + + console.log('Running lex-cli to generate types...'); + execSync(command, { + stdio: 'inherit', + env: process.env, + }); + + console.log('Done! Types generated at', TYPES_OUTPUT_PATH); + } catch (error) { + console.error('Error:', error); + process.exit(1); + } +} + +main(); diff --git a/packages/lexicons/src/index.ts b/packages/lexicons/src/index.ts index 9a38160..77782bb 100644 --- a/packages/lexicons/src/index.ts +++ b/packages/lexicons/src/index.ts @@ -1,137 +1,137 @@ -import { - isRecord, - type Procedures, - type Queries, - type Records, -} from './lib/lexicons.js'; - -export * from './lib/lexicons.js'; - -// --- Core utility types --- -export type LexiconUnion = T[keyof T]; -export type NSIDOf = T extends { $type: infer U extends string } ? U : never; - -// --- NSID Patterns --- -export const APP_BSKY_PREFIX = 'app.bsky.' as const; -export const COM_ATPROTO_PREFIX = 'com.atproto.' as const; - -export type BskyNSID = `${typeof APP_BSKY_PREFIX}${string}`; -export type AtProtoNSID = `${typeof COM_ATPROTO_PREFIX}${string}`; -export type KnownNSID = BskyNSID | AtProtoNSID; - -// --- Record Types --- -export type RecordDefs = LexiconUnion; -export type BskyRecord = Extract; -export type AtProtoRecord = Extract; - -// --- Query Types --- -export type QueryDefs = LexiconUnion; -export type BskyQuery = { - [K in keyof Queries]: K extends BskyNSID ? Queries[K] : never; -}[keyof Queries]; - -export type QueryParams = Queries[T] extends { - params: infer P; -} - ? P - : never; - -export type QueryOutput = Queries[T]['output']; - -export type QueryErrors = Queries[T] extends { - errors: infer E; -} - ? E - : never; - -// --- Procedure Types --- -export type ProcedureDefs = LexiconUnion; -export type BskyProcedure = { - [K in keyof Procedures]: K extends BskyNSID ? Procedures[K] : never; -}[keyof Procedures]; - -export type ProcedureParams = - Procedures[T] extends { - params: infer P; - } - ? P - : never; - -export type ProcedureInput = Procedures[T] extends { - input: infer I; -} - ? I - : never; - -export type ProcedureOutput = - Procedures[T] extends { - output: infer O; - } - ? O - : never; - -export type ProcedureErrors = - Procedures[T] extends { - errors: infer E; - } - ? E - : never; - -// --- Common Bluesky Types --- -export type BskyPost = Extract< - BskyRecord, - { $type: `${typeof APP_BSKY_PREFIX}feed.post` } ->; -export type BskyProfile = Extract< - BskyRecord, - { $type: `${typeof APP_BSKY_PREFIX}actor.profile` } ->; -export type BskyLike = Extract< - BskyRecord, - { $type: `${typeof APP_BSKY_PREFIX}feed.like` } ->; -export type BskyFollow = Extract< - BskyRecord, - { $type: `${typeof APP_BSKY_PREFIX}graph.follow` } ->; - -// --- Type Guards --- -export function isBskyRecord(value: unknown): value is BskyRecord { - return isRecord(value) && value.$type.startsWith(APP_BSKY_PREFIX); -} - -export function isAtProtoRecord(value: unknown): value is AtProtoRecord { - return isRecord(value) && value.$type.startsWith(COM_ATPROTO_PREFIX); -} - -export function isBskyPost(value: unknown): value is BskyPost { - return ( - isBskyRecord(value) && value.$type.startsWith(`${APP_BSKY_PREFIX}feed.post`) - ); -} - -// --- Error Types and Guards --- -export interface BskyError { - error: string; - message: string; - statusCode?: number; -} - -export function isBskyError(value: unknown): value is BskyError { - return ( - typeof value === 'object' && - value !== null && - 'error' in value && - 'message' in value && - typeof (value as BskyError).error === 'string' && - typeof (value as BskyError).message === 'string' - ); -} - -// --- Helper Types --- -export type BskyQueryParams = T extends keyof Queries - ? QueryParams - : never; -export type BskyProcedureInput = T extends keyof Procedures - ? ProcedureInput - : never; +import { + isRecord, + type Procedures, + type Queries, + type Records, +} from './lib/lexicons.js'; + +export * from './lib/lexicons.js'; + +// --- Core utility types --- +export type LexiconUnion = T[keyof T]; +export type NSIDOf = T extends { $type: infer U extends string } ? U : never; + +// --- NSID Patterns --- +export const APP_BSKY_PREFIX = 'app.bsky.' as const; +export const COM_ATPROTO_PREFIX = 'com.atproto.' as const; + +export type BskyNSID = `${typeof APP_BSKY_PREFIX}${string}`; +export type AtProtoNSID = `${typeof COM_ATPROTO_PREFIX}${string}`; +export type KnownNSID = BskyNSID | AtProtoNSID; + +// --- Record Types --- +export type RecordDefs = LexiconUnion; +export type BskyRecord = Extract; +export type AtProtoRecord = Extract; + +// --- Query Types --- +export type QueryDefs = LexiconUnion; +export type BskyQuery = { + [K in keyof Queries]: K extends BskyNSID ? Queries[K] : never; +}[keyof Queries]; + +export type QueryParams = Queries[T] extends { + params: infer P; +} + ? P + : never; + +export type QueryOutput = Queries[T]['output']; + +export type QueryErrors = Queries[T] extends { + errors: infer E; +} + ? E + : never; + +// --- Procedure Types --- +export type ProcedureDefs = LexiconUnion; +export type BskyProcedure = { + [K in keyof Procedures]: K extends BskyNSID ? Procedures[K] : never; +}[keyof Procedures]; + +export type ProcedureParams = + Procedures[T] extends { + params: infer P; + } + ? P + : never; + +export type ProcedureInput = Procedures[T] extends { + input: infer I; +} + ? I + : never; + +export type ProcedureOutput = + Procedures[T] extends { + output: infer O; + } + ? O + : never; + +export type ProcedureErrors = + Procedures[T] extends { + errors: infer E; + } + ? E + : never; + +// --- Common Bluesky Types --- +export type BskyPost = Extract< + BskyRecord, + { $type: `${typeof APP_BSKY_PREFIX}feed.post` } +>; +export type BskyProfile = Extract< + BskyRecord, + { $type: `${typeof APP_BSKY_PREFIX}actor.profile` } +>; +export type BskyLike = Extract< + BskyRecord, + { $type: `${typeof APP_BSKY_PREFIX}feed.like` } +>; +export type BskyFollow = Extract< + BskyRecord, + { $type: `${typeof APP_BSKY_PREFIX}graph.follow` } +>; + +// --- Type Guards --- +export function isBskyRecord(value: unknown): value is BskyRecord { + return isRecord(value) && value.$type.startsWith(APP_BSKY_PREFIX); +} + +export function isAtProtoRecord(value: unknown): value is AtProtoRecord { + return isRecord(value) && value.$type.startsWith(COM_ATPROTO_PREFIX); +} + +export function isBskyPost(value: unknown): value is BskyPost { + return ( + isBskyRecord(value) && value.$type.startsWith(`${APP_BSKY_PREFIX}feed.post`) + ); +} + +// --- Error Types and Guards --- +export interface BskyError { + error: string; + message: string; + statusCode?: number; +} + +export function isBskyError(value: unknown): value is BskyError { + return ( + typeof value === 'object' && + value !== null && + 'error' in value && + 'message' in value && + typeof (value as BskyError).error === 'string' && + typeof (value as BskyError).message === 'string' + ); +} + +// --- Helper Types --- +export type BskyQueryParams = T extends keyof Queries + ? QueryParams + : never; +export type BskyProcedureInput = T extends keyof Procedures + ? ProcedureInput + : never; From f9f2f2518361dfd567d10093a83abe7c58abe481 Mon Sep 17 00:00:00 2001 From: TAKAHASHI Shuuji Date: Sun, 19 Jan 2025 22:20:18 +0900 Subject: [PATCH 3/3] chore: fix remaining CRLF error --- packages/client/src/bsky/index.test.ts | 158 ++++++++++++------------- 1 file changed, 79 insertions(+), 79 deletions(-) diff --git a/packages/client/src/bsky/index.test.ts b/packages/client/src/bsky/index.test.ts index 211ef01..17bc2a4 100644 --- a/packages/client/src/bsky/index.test.ts +++ b/packages/client/src/bsky/index.test.ts @@ -1,79 +1,79 @@ -import { CredentialManager } from '@atcute/client'; -import { describe, expect, it } from 'vitest'; -import { Tsky } from '~/index'; - -const formatSecret = (secret: string | undefined) => { - if (!secret) { - throw new Error('Secret is required'); - } - - return secret.replace(/^tsky /g, '').trim(); -}; - -const TEST_CREDENTIALS = { - alice: { - handle: 'alice.tsky.dev', - did: 'did:plc:jguhdmnjclquqf5lsvkyxqy3', - password: 'alice_and_bob', - }, - bob: { - handle: 'bob.tsky.dev', - did: 'did:plc:2ig7akkyfq256j42uxvc4g2h', - password: 'alice_and_bob', - }, -}; - -async function getAliceTsky() { - const manager = new CredentialManager({ service: 'https://bsky.social' }); - await manager.login({ - identifier: TEST_CREDENTIALS.alice.handle, - password: TEST_CREDENTIALS.alice.password, - }); - - return new Tsky(manager); -} - -describe('bsky', () => { - it('.profile()', async () => { - const tsky = await getAliceTsky(); - const profile = await tsky.bsky.profile(TEST_CREDENTIALS.alice.did); - - expect(profile).toBeDefined(); - expect(profile).toHaveProperty('handle', TEST_CREDENTIALS.alice.handle); - }); - - describe('feed', () => { - it('.timeline()', async () => { - const tsky = await getAliceTsky(); - - const paginator = await tsky.bsky.feed.getTimeline({ - limit: 30, - }); - - expect(paginator).toBeDefined(); - expect(paginator.values).toBeDefined(); - expect(paginator.values).toBeInstanceOf(Array); - expect(paginator.values.length).toBe(1); // we should get the first page from the paginator - expect(paginator.values[0].feed.length).toBeGreaterThan(0); // alice has some posts ;) - expect(paginator.values[0].feed[0]).toHaveProperty('post'); - }); - - it('.feed()', async () => { - const tsky = await getAliceTsky(); - - const paginator = await tsky.bsky.feed.getFeed({ - // "Birds! 🦉" custom feed - // - https://bsky.app/profile/daryllmarie.bsky.social/feed/aaagllxbcbsje - feed: 'at://did:plc:ffkgesg3jsv2j7aagkzrtcvt/app.bsky.feed.generator/aaagllxbcbsje', - limit: 30, - }); - - expect(paginator).toBeDefined(); - expect(paginator.values).toBeDefined(); - expect(paginator.values).toBeInstanceOf(Array); - expect(paginator.values.length).toBe(1); // we should get the first page from the paginator - expect(paginator.values[0].feed.length).toBeGreaterThan(0); // we found some birds posts ;) - expect(paginator.values[0].feed[0]).toHaveProperty('post'); - }); - }); -}); +import { CredentialManager } from '@atcute/client'; +import { describe, expect, it } from 'vitest'; +import { Tsky } from '~/index'; + +const formatSecret = (secret: string | undefined) => { + if (!secret) { + throw new Error('Secret is required'); + } + + return secret.replace(/^tsky /g, '').trim(); +}; + +const TEST_CREDENTIALS = { + alice: { + handle: 'alice.tsky.dev', + did: 'did:plc:jguhdmnjclquqf5lsvkyxqy3', + password: 'alice_and_bob', + }, + bob: { + handle: 'bob.tsky.dev', + did: 'did:plc:2ig7akkyfq256j42uxvc4g2h', + password: 'alice_and_bob', + }, +}; + +async function getAliceTsky() { + const manager = new CredentialManager({ service: 'https://bsky.social' }); + await manager.login({ + identifier: TEST_CREDENTIALS.alice.handle, + password: TEST_CREDENTIALS.alice.password, + }); + + return new Tsky(manager); +} + +describe('bsky', () => { + it('.profile()', async () => { + const tsky = await getAliceTsky(); + const profile = await tsky.bsky.profile(TEST_CREDENTIALS.alice.did); + + expect(profile).toBeDefined(); + expect(profile).toHaveProperty('handle', TEST_CREDENTIALS.alice.handle); + }); + + describe('feed', () => { + it('.timeline()', async () => { + const tsky = await getAliceTsky(); + + const paginator = await tsky.bsky.feed.getTimeline({ + limit: 30, + }); + + expect(paginator).toBeDefined(); + expect(paginator.values).toBeDefined(); + expect(paginator.values).toBeInstanceOf(Array); + expect(paginator.values.length).toBe(1); // we should get the first page from the paginator + expect(paginator.values[0].feed.length).toBeGreaterThan(0); // alice has some posts ;) + expect(paginator.values[0].feed[0]).toHaveProperty('post'); + }); + + it('.feed()', async () => { + const tsky = await getAliceTsky(); + + const paginator = await tsky.bsky.feed.getFeed({ + // "Birds! 🦉" custom feed + // - https://bsky.app/profile/daryllmarie.bsky.social/feed/aaagllxbcbsje + feed: 'at://did:plc:ffkgesg3jsv2j7aagkzrtcvt/app.bsky.feed.generator/aaagllxbcbsje', + limit: 30, + }); + + expect(paginator).toBeDefined(); + expect(paginator.values).toBeDefined(); + expect(paginator.values).toBeInstanceOf(Array); + expect(paginator.values.length).toBe(1); // we should get the first page from the paginator + expect(paginator.values[0].feed.length).toBeGreaterThan(0); // we found some birds posts ;) + expect(paginator.values[0].feed[0]).toHaveProperty('post'); + }); + }); +});