From 31f935711a5d377642c5a39e278b2d734f4405b0 Mon Sep 17 00:00:00 2001 From: tmm Date: Sun, 7 Jun 2026 14:32:07 -0400 Subject: [PATCH] fix: type createClient in published package --- .changeset/fresh-buckets-matter.md | 5 ++ cli/package.json | 30 ++++++++-- cli/src/client.ts | 6 +- cli/src/shims.d.ts | 5 ++ cli/test/package-types.test.ts | 93 ++++++++++++++++++++++++++++++ cli/tsconfig.pkg.api.json | 13 +++++ cli/tsconfig.pkg.json | 5 +- plugins/amp/src/plugin.test.ts | 20 +------ plugins/amp/src/plugin.ts | 3 +- pnpm-lock.yaml | 30 +++++----- pnpm-workspace.yaml | 2 +- scripts/formatPackage.ts | 31 +++++++++- scripts/preconstruct.ts | 11 ++++ test/vitest.config.ts | 15 +++++ 14 files changed, 226 insertions(+), 43 deletions(-) create mode 100644 .changeset/fresh-buckets-matter.md create mode 100644 cli/src/shims.d.ts create mode 100644 cli/test/package-types.test.ts create mode 100644 cli/tsconfig.pkg.api.json diff --git a/.changeset/fresh-buckets-matter.md b/.changeset/fresh-buckets-matter.md new file mode 100644 index 00000000..aea27a17 --- /dev/null +++ b/.changeset/fresh-buckets-matter.md @@ -0,0 +1,5 @@ +--- +'curl.md': patch +--- + +Fixed published TypeScript SDK declarations so `createClient` retained its route-aware types for package consumers. diff --git a/cli/package.json b/cli/package.json index 7ee42031..3270f261 100644 --- a/cli/package.json +++ b/cli/package.json @@ -37,24 +37,46 @@ }, "files": [ "CHANGELOG.md", - "dist" + "dist", + "src/cf-env.d.ts", + "src/shims.d.ts" ], "sideEffects": false, "type": "module", "types": "./dist/exports/index.d.ts", "imports": { + "#api.ts": { + "types": "./dist/src/api.ts", + "default": "./dist/src/api.ts" + }, + "#db/client.ts": { + "types": "./dist/db/client.ts", + "default": "./dist/db/client.ts" + }, + "#db/*.ts": { + "types": "./dist/db/*.ts", + "default": "./dist/db/*.ts" + }, "#db/*": { "types": "./dist/db/*", - "default": "../db/*" + "default": "./dist/db/*" + }, + "#*.ts": { + "types": "./dist/src/*.ts", + "default": "./dist/src/*.ts" + }, + "#*.tsx": { + "types": "./dist/src/*.tsx", + "default": "./dist/src/*.tsx" }, "#*": { "types": "./dist/src/*", - "default": "../src/*" + "default": "./dist/src/*" }, "#test/*": "../test/*" }, "scripts": { - "build": "rm -rf dist && tsgo --noCheck -p tsconfig.build.json && tsgo --noCheck -p tsconfig.pkg.json && mkdir -p dist/exports dist/cli/src && cp dist/cli/src/exports/index.d.ts dist/exports/index.d.ts && cp dist/cli/src/exports/internal.d.ts dist/exports/internal.d.ts && cp src/cf-env.d.ts dist/cli/src/cf-env.d.ts && find ../src ../db -name '*.js' -delete", + "build": "rm -rf dist && tsgo --noCheck -p tsconfig.build.json && tsgo --noCheck -p tsconfig.pkg.json && tsgo --noCheck -p tsconfig.pkg.api.json && find ../src ../db -name '*.js' -delete", "check:types": "tsgo --noEmit", "gen:types": "incur gen" }, diff --git a/cli/src/client.ts b/cli/src/client.ts index bd51e7bf..a59e9529 100644 --- a/cli/src/client.ts +++ b/cli/src/client.ts @@ -1,8 +1,8 @@ // oxlint-disable-next-line typescript-eslint/triple-slash-reference -- ambient worker shims for published type output -/// +/// import { hc, type ClientRequestOptions } from 'hono/client' -import type { api } from '../../src/api.ts' +import type { api } from '#api.ts' export const defaultBaseUrl = 'https://curl.md' @@ -78,7 +78,7 @@ export type Client = Omit & { type RpcClient = ReturnType> type Api = RpcClient['api'] -type PublicApi = Omit +type PublicApi = Omit type Fetch = RpcClient['api'][':url{.+}']['$get'] type FetchQuery = Pick< NonNullable[0]>['query']>, diff --git a/cli/src/shims.d.ts b/cli/src/shims.d.ts new file mode 100644 index 00000000..b6012e3f --- /dev/null +++ b/cli/src/shims.d.ts @@ -0,0 +1,5 @@ +// Minimal internal type shims used by published SDK declarations so consumers do not need app/database types. + +export declare const api: unknown + +export type Database = unknown diff --git a/cli/test/package-types.test.ts b/cli/test/package-types.test.ts new file mode 100644 index 00000000..29e93e87 --- /dev/null +++ b/cli/test/package-types.test.ts @@ -0,0 +1,93 @@ +import { execFile } from 'node:child_process' +import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join, resolve } from 'node:path' +import { expect, test } from 'vitest' + +const root = resolve(import.meta.dirname, '../..') + +test('published package exposes createClient types', async () => { + const dir = await mkdtemp(join(tmpdir(), 'curl-md-package-types-')) + let formatted = false + + try { + await exec('pnpm', ['--filter', 'curl.md', 'build'], { cwd: root, timeout: 120_000 }) + await exec('node', ['--experimental-strip-types', 'scripts/formatPackage.ts'], { cwd: root }) + formatted = true + + const packed = await exec('pnpm', ['--filter', 'curl.md', 'pack', '--pack-destination', dir], { + cwd: root, + }) + const tarball = packed.stdout.trim().split('\n').at(-1) + expect(tarball).toBeTruthy() + + const consumer = join(dir, 'consumer') + await mkdir(consumer) + await writeFile(join(consumer, 'package.json'), '{"type":"module"}\n') + await writeFile( + join(consumer, 'tsconfig.json'), + `${JSON.stringify( + { + compilerOptions: { + module: 'nodenext', + moduleResolution: 'nodenext', + noEmit: true, + strict: true, + target: 'es2022', + }, + }, + null, + 2, + )}\n`, + ) + await writeFile( + join(consumer, 'index.ts'), + `import { createClient, type Client } from 'curl.md' +import { Auth } from 'curl.md/internal' + +const client = createClient() +const res = await client.fetch('example.com', { + fresh: true, + keywords: ['example'], + mode: 'rush', + objective: 'example domain', +}) + +const typed: Client = client +// @ts-expect-error internal Sentry tunnel is not SDK surface +client.api.tunnel +void Auth +void res +void typed +`, + ) + + await exec('pnpm', ['--dir', consumer, 'add', tarball!], { cwd: root, timeout: 120_000 }) + await exec('pnpm', ['exec', 'tsgo', '-p', join(consumer, 'tsconfig.json')], { + cwd: root, + timeout: 120_000, + }) + } finally { + if (formatted) + await exec('node', ['--experimental-strip-types', 'scripts/restorePackage.ts'], { cwd: root }) + await rm(dir, { force: true, recursive: true }) + } +}, 180_000) + +function exec( + cmd: string, + args: string[], + options: { cwd: string; timeout?: number }, +): Promise<{ stderr: string; stdout: string }> { + return new Promise((resolve, reject) => { + execFile( + cmd, + args, + { cwd: options.cwd, timeout: options.timeout ?? 30_000 }, + (error, stdout, stderr) => { + if (error) reject(new Error(stderr.trim() || stdout.trim() || error.message)) + else resolve({ stderr, stdout }) + }, + ) + }) +} diff --git a/cli/tsconfig.pkg.api.json b/cli/tsconfig.pkg.api.json new file mode 100644 index 00000000..88b7764d --- /dev/null +++ b/cli/tsconfig.pkg.api.json @@ -0,0 +1,13 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "declaration": true, + "emitDeclarationOnly": true, + "noEmit": false, + "outDir": "dist", + "rootDir": "..", + "rewriteRelativeImportExtensions": true + }, + "include": ["../src/api.ts"], + "exclude": ["src/**/*.test.*", "test"] +} diff --git a/cli/tsconfig.pkg.json b/cli/tsconfig.pkg.json index bf9076ec..badff7a1 100644 --- a/cli/tsconfig.pkg.json +++ b/cli/tsconfig.pkg.json @@ -5,7 +5,10 @@ "emitDeclarationOnly": true, "noEmit": false, "outDir": "dist", - "rootDir": "..", + "paths": { + "#api.ts": ["./src/shims.d.ts"] + }, + "rootDir": "src", "rewriteRelativeImportExtensions": true }, "include": ["src/exports/index.ts", "src/exports/internal.ts", "src/cf-env.d.ts"], diff --git a/plugins/amp/src/plugin.test.ts b/plugins/amp/src/plugin.test.ts index 53ff7a29..542a899f 100644 --- a/plugins/amp/src/plugin.test.ts +++ b/plugins/amp/src/plugin.test.ts @@ -202,7 +202,7 @@ test('preserves URL fragments', async () => { } as any) expect(requests[0]?.url).toContain('anchor=section') - expect((result as any).url).toBe('https://example.com/docs?q=1#section') + expect(result).toBe('# Fragment') }) // --- Anonymous fetch --- @@ -244,20 +244,7 @@ test('fetches anonymously and returns expected shape', async () => { expect(requests[0]?.url).toContain(`${defaultBaseUrl}/api/https://example.com/docs`) expect(requests[0]?.url).toContain('anchor=intro') - expect(result).toEqual({ - auth: 'anon', - cache: 'HIT', - credits_remaining: 42, - fresh: true, - keywords: ['a'], - markdown: '# Example\n\n---\n\nPowered by [curl.md](https://curl.md)', - mode: 'rush', - objective: 'test', - request_id: 'req_abc', - tokens_count: 100, - tokens_saved: 50, - url: 'https://example.com/docs#intro', - }) + expect(result).toBe('# Example\n\n---\n\nPowered by [curl.md](https://curl.md)') }) // --- API key auth --- @@ -411,8 +398,7 @@ test('retries once on session 401 with forced auth refresh', async () => { 'Bearer access-token-stale', 'Bearer access-token-fresh', ]) - expect((result as any).auth).toBe('session') - expect((result as any).markdown).toBe('# Retried') + expect(result).toBe('# Retried') Session.delete() }) diff --git a/plugins/amp/src/plugin.ts b/plugins/amp/src/plugin.ts index af53a763..1a974b46 100644 --- a/plugins/amp/src/plugin.ts +++ b/plugins/amp/src/plugin.ts @@ -74,13 +74,14 @@ export default function (amp: PluginAPI) { required: ['url'], }, async execute(input) { - return fetchPage({ + const result = await fetchPage({ fresh: input.fresh as boolean | undefined, keywords: input.keywords as string[] | undefined, mode: input.mode as 'rush' | 'smart' | undefined, objective: input.objective as string | undefined, url: input.url as string, }) + return result.markdown }, }) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 55a55e05..ff2d4480 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7,8 +7,8 @@ settings: catalogs: default: hono: - specifier: ^4.12.16 - version: 4.12.18 + specifier: ^4.12.21 + version: 4.12.23 vite-plus: specifier: 0.1.20 version: 0.1.20 @@ -53,7 +53,7 @@ importers: version: 5.2.8 '@hono/zod-validator': specifier: ^0.7.6 - version: 0.7.6(hono@4.12.18)(zod@4.3.6) + version: 0.7.6(hono@4.12.23)(zod@4.3.6) '@octokit/core': specifier: ^7.0.6 version: 7.0.6 @@ -89,7 +89,7 @@ importers: version: 1.167.35(@tanstack/react-router@1.169.2(patch_hash=53ce3be0f6f7b146142e16bfe785fc470bf9dcc679b9b3b701a2b3cb4668a198)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@voidzero-dev/vite-plus-core@0.1.20) hono: specifier: 'catalog:' - version: 4.12.18 + version: 4.12.23 kysely: specifier: ^0.28.17 version: 0.28.17 @@ -207,7 +207,7 @@ importers: version: 5.2.0(@voidzero-dev/vite-plus-core@0.1.20) emulate: specifier: 0.5.0 - version: 0.5.0(hono@4.12.18) + version: 0.5.0(hono@4.12.23) knip: specifier: ^6.7.0 version: 6.7.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) @@ -273,7 +273,7 @@ importers: dependencies: hono: specifier: 'catalog:' - version: 4.12.18 + version: 4.12.23 incur: specifier: 0.4.5 version: 0.4.5 @@ -4680,8 +4680,8 @@ packages: highlight.js@10.7.3: resolution: {integrity: sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==} - hono@4.12.18: - resolution: {integrity: sha512-RWzP96k/yv0PQfyXnWjs6zot20TqfpfsNXhOnev8d1InAxubW93L11/oNUc3tQqn2G0bSdAOBpX+2uDFHV7kdQ==} + hono@4.12.23: + resolution: {integrity: sha512-eIaZ9qDgu7XV0pxOCrg7/WhnQ6Ivm22UcxhXx/A3dcbqbbYgBEkc6e/J/s7j2tS96zoB0S9VBdLwQNCWwUo4LA==} engines: {node: '>=16.9.0'} hosted-git-info@9.0.2: @@ -7799,13 +7799,13 @@ snapshots: protobufjs: 7.5.8 yargs: 17.7.2 - '@hono/node-server@1.19.13(hono@4.12.18)': + '@hono/node-server@1.19.13(hono@4.12.23)': dependencies: - hono: 4.12.18 + hono: 4.12.23 - '@hono/zod-validator@0.7.6(hono@4.12.18)(zod@4.3.6)': + '@hono/zod-validator@0.7.6(hono@4.12.23)(zod@4.3.6)': dependencies: - hono: 4.12.18 + hono: 4.12.23 zod: 4.3.6 '@iconify/json@2.2.467': @@ -10837,9 +10837,9 @@ snapshots: emoji-regex@9.2.2: {} - emulate@0.5.0(hono@4.12.18): + emulate@0.5.0(hono@4.12.23): dependencies: - '@hono/node-server': 1.19.13(hono@4.12.18) + '@hono/node-server': 1.19.13(hono@4.12.23) commander: 14.0.3 picocolors: 1.1.1 yaml: 2.8.3 @@ -11401,7 +11401,7 @@ snapshots: highlight.js@10.7.3: {} - hono@4.12.18: {} + hono@4.12.23: {} hosted-git-info@9.0.2: dependencies: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index f093560b..34e9479a 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -18,7 +18,7 @@ auditConfig: - GHSA-6cpc-mj5c-m9rq blockExoticSubdeps: true catalog: - hono: ^4.12.16 + hono: ^4.12.21 # Vite+ dependency aliases. vite: npm:@voidzero-dev/vite-plus-core@0.1.20 vite-plus: 0.1.20 diff --git a/scripts/formatPackage.ts b/scripts/formatPackage.ts index 3939fd6f..3a6a511f 100644 --- a/scripts/formatPackage.ts +++ b/scripts/formatPackage.ts @@ -29,11 +29,40 @@ for (const dir of packageDirs) { ) // Remove dev-only fields - const { devDependencies: _d, imports: _i, scripts: _s, ...rest } = packageJson + const { devDependencies: _d, scripts: _s, ...rest } = packageJson if (rest.bin) for (const key of Object.keys(rest.bin)) if (key.endsWith('.src')) delete rest.bin[key] + if (rest.name === 'curl.md' && isObject(rest.imports)) + rest.imports = Object.fromEntries( + Object.entries(rest.imports) + .filter(([key]) => !key.startsWith('#test/')) + .map(([key, value]) => [key, formatImport(key, value)]), + ) await fs.writeFile(packagePath, `${JSON.stringify(rest, undefined, 2)}\n`, 'utf-8') } console.log('Done.') + +function formatImport(key: string, value: unknown) { + if (!isObject(value)) return value + const types = formatImportTarget(key, value.types) + if (!types) return value + return { ...value, default: types, types } +} + +function formatImportTarget(key: string, value: unknown) { + if (key === '#db/client.ts') return './src/shims.d.ts' + if (typeof value !== 'string') return undefined + return value + .replace(/^\.\/dist\/src\/(.*)\.tsx?$/, './dist/src/$1.d.ts') + .replace(/^\.\/dist\/db\/(.*)\.ts$/, './dist/db/$1.d.ts') + .replace(/^\.\.\/src\/(.*)\.tsx?$/, './dist/src/$1.d.ts') + .replace(/^\.\.\/db\/(.*)\.ts$/, './dist/db/$1.d.ts') + .replace(/^\.\.\/src\/(.*)$/, './dist/src/$1') + .replace(/^\.\.\/db\/(.*)$/, './dist/db/$1') +} + +function isObject(value: unknown): value is Record { + return typeof value === 'object' && value !== null && !Array.isArray(value) +} diff --git a/scripts/preconstruct.ts b/scripts/preconstruct.ts index d895e453..80651b71 100644 --- a/scripts/preconstruct.ts +++ b/scripts/preconstruct.ts @@ -21,10 +21,21 @@ for (const dir of packageDirs) { await linkExport(dir, key, value.default) await linkExport(dir, key, value.types) } + + if (packageJson.name === 'curl.md') { + await linkDir(path.join(dir, 'dist/src'), 'src') + await linkDir(path.join(dir, 'dist/db'), 'db') + } } console.log('Done.') +async function linkDir(outputPath: string, sourcePath: string) { + await fs.rm(outputPath, { force: true, recursive: true }) + await fs.mkdir(path.dirname(outputPath), { recursive: true }) + await fs.symlink(path.resolve(sourcePath), outputPath, 'dir') +} + async function linkExport(dir: string, key: string, exportPath: string | undefined) { if (!exportPath) return diff --git a/test/vitest.config.ts b/test/vitest.config.ts index 9e6f865b..55c75039 100644 --- a/test/vitest.config.ts +++ b/test/vitest.config.ts @@ -42,6 +42,13 @@ export default defineConfig({ }, }, { + resolve: { + alias: [ + { find: /^#db\/(.*)$/, replacement: path.resolve(root, 'db/$1') }, + { find: /^#test\/(.*)$/, replacement: path.resolve(root, 'test/$1') }, + { find: /^#(.*)$/, replacement: path.resolve(root, 'src/$1') }, + ], + }, test: { name: 'cli', globalSetup: ['cli/test/global.setup.ts'], @@ -52,6 +59,14 @@ export default defineConfig({ testTimeout: 30_000, }, }, + { + test: { + name: 'sdk', + include: ['cli/test/package-types.test.ts'], + root, + testTimeout: 180_000, + }, + }, { define: { __HOST__: JSON.stringify('curl.local'),