From 3ea414f5dd3297f653ff9f49b87a07d21ccaaae7 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 7 Jan 2025 13:18:38 -0500 Subject: [PATCH] release: 0.25.0 (#467) Co-authored-by: stainless-bot Co-authored-by: stainless-app[bot] <142633134+stainless-app[bot]@users.noreply.github.com> --- .release-please-manifest.json | 2 +- .stats.yml | 2 +- CHANGELOG.md | 30 +++++ CONTRIBUTING.md | 8 +- LICENSE | 2 +- jsr.json | 2 +- package.json | 4 +- scripts/build-deno | 2 + scripts/utils/check-is-in-git-install.sh | 2 +- scripts/utils/convert-jsr-readme.cjs | 140 +++++++++++++++++++++++ scripts/utils/git-swap.sh | 13 +++ src/core.ts | 32 +++--- src/error.ts | 64 ++++------- src/version.ts | 2 +- tests/index.test.ts | 19 ++- yarn.lock | 6 +- 16 files changed, 260 insertions(+), 70 deletions(-) create mode 100644 scripts/utils/convert-jsr-readme.cjs create mode 100755 scripts/utils/git-swap.sh diff --git a/.release-please-manifest.json b/.release-please-manifest.json index fc5553b07..945fbaf29 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.24.0" + ".": "0.25.0" } diff --git a/.stats.yml b/.stats.yml index 8fb5c325f..0c641b87f 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,2 +1,2 @@ configured_endpoints: 26 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/val-town%2Fval-town-a2ff8d1d2f7e6375a8d422e67adf9ff5958e010c3ad152d837cf19f79d9a533e.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/val-town%2Fval-town-f02ed5fb5a61e227c08148c44c8c65de2890bab8732d0ab2888714c8f0513e1a.yml diff --git a/CHANGELOG.md b/CHANGELOG.md index 2135edf8e..6d4fa45ec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,35 @@ # Changelog +## 0.25.0 (2025-01-07) + +Full Changelog: [v0.24.0...v0.25.0](https://github.com/val-town/sdk/compare/v0.24.0...v0.25.0) + +### Features + +* **api:** api update ([#477](https://github.com/val-town/sdk/issues/477)) ([7126ec7](https://github.com/val-town/sdk/commit/7126ec7aef2b7f5b6a9c99a6dfe44b18a3f1b92e)) +* **internal:** make git install file structure match npm ([#466](https://github.com/val-town/sdk/issues/466)) ([2921499](https://github.com/val-town/sdk/commit/2921499332800264d819b16704980802a746d27a)) + + +### Bug Fixes + +* **client:** normalize method ([#475](https://github.com/val-town/sdk/issues/475)) ([00cd997](https://github.com/val-town/sdk/commit/00cd997fc0763ba2fe2e8c83ae9ce28c2ce7ec4b)) + + +### Chores + +* **internal:** codegen related update ([#468](https://github.com/val-town/sdk/issues/468)) ([89670f1](https://github.com/val-town/sdk/commit/89670f1110f52d75764d5bd0ef6f93904dcd0b7d)) +* **internal:** codegen related update ([#469](https://github.com/val-town/sdk/issues/469)) ([87f8db8](https://github.com/val-town/sdk/commit/87f8db8117438438374f760f0cf95ab9cbbdfdd0)) +* **internal:** codegen related update ([#470](https://github.com/val-town/sdk/issues/470)) ([032bec5](https://github.com/val-town/sdk/commit/032bec5c8a9c118c68ee432e6a91d4fa5107e3a1)) +* **internal:** codegen related update ([#473](https://github.com/val-town/sdk/issues/473)) ([84a52e1](https://github.com/val-town/sdk/commit/84a52e177691cc284856541d940b7bb2d1c840e6)) +* **internal:** codegen related update ([#474](https://github.com/val-town/sdk/issues/474)) ([1db3338](https://github.com/val-town/sdk/commit/1db333806833f23172d2e1045eab28493e07152f)) +* **internal:** fix some typos ([#472](https://github.com/val-town/sdk/issues/472)) ([8214c09](https://github.com/val-town/sdk/commit/8214c096e44e4c5295fb2008047d16f3cc80ccf5)) +* **internal:** update isAbsoluteURL ([#471](https://github.com/val-town/sdk/issues/471)) ([0cb8e25](https://github.com/val-town/sdk/commit/0cb8e252c0e874660526d28caabf12c5fb648d70)) + + +### Documentation + +* minor formatting changes ([#476](https://github.com/val-town/sdk/issues/476)) ([43ce72f](https://github.com/val-town/sdk/commit/43ce72f81519b4d51e230b200e0e78929ad760f3)) + ## 0.24.0 (2024-11-21) Full Changelog: [v0.23.0...v0.24.0](https://github.com/val-town/sdk/compare/v0.23.0...v0.24.0) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8f8f9e1f3..d89a12ab7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,6 +1,6 @@ ## Setting up the environment -This repository uses [`yarn@v1`](https://classic.yarnpkg.com/lang/en/docs/install/#mac-stable). +This repository uses [`yarn@v1`](https://classic.yarnpkg.com/lang/en/docs/install). Other package managers may work but are not officially supported for development. To set up the repository, run: @@ -29,10 +29,10 @@ All files in the `examples/` directory are not modified by the generator and can … ``` -``` -chmod +x examples/.ts +```sh +$ chmod +x examples/.ts # run the example against your api -yarn tsn -T examples/.ts +$ yarn tsn -T examples/.ts ``` ## Using the repository from source diff --git a/LICENSE b/LICENSE index 052886e12..1ad25a415 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright 2024 val-town +Copyright 2025 val-town Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: diff --git a/jsr.json b/jsr.json index c13e55bc9..92fc6d9aa 100644 --- a/jsr.json +++ b/jsr.json @@ -1,6 +1,6 @@ { "name": "@valtown/sdk", - "version": "0.24.0", + "version": "0.25.0", "exports": "./index.ts", "publish": { "exclude": [ diff --git a/package.json b/package.json index 97c57efb6..a1da8044e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@valtown/sdk", - "version": "0.24.0", + "version": "0.25.0", "description": "The official TypeScript library for the Val Town API", "author": "Val Town ", "types": "dist/index.d.ts", @@ -18,7 +18,7 @@ "build": "./scripts/build", "prepublishOnly": "echo 'to publish, run yarn build && (cd dist; yarn publish)' && exit 1", "format": "prettier --write --cache --cache-strategy metadata . !dist", - "prepare": "if ./scripts/utils/check-is-in-git-install.sh; then ./scripts/build; fi", + "prepare": "if ./scripts/utils/check-is-in-git-install.sh; then ./scripts/build && ./scripts/utils/git-swap.sh; fi", "tsn": "ts-node -r tsconfig-paths/register", "lint": "./scripts/lint", "fix": "./scripts/format" diff --git a/scripts/build-deno b/scripts/build-deno index 4a2000a66..858c99d16 100755 --- a/scripts/build-deno +++ b/scripts/build-deno @@ -17,3 +17,5 @@ done for file in README.md LICENSE CHANGELOG.md; do if [ -e "${file}" ]; then cp "${file}" dist-deno; fi done + +node scripts/utils/convert-jsr-readme.cjs ./dist-deno/README.md diff --git a/scripts/utils/check-is-in-git-install.sh b/scripts/utils/check-is-in-git-install.sh index 36bcedc20..1354eb432 100755 --- a/scripts/utils/check-is-in-git-install.sh +++ b/scripts/utils/check-is-in-git-install.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # Check if you happen to call prepare for a repository that's already in node_modules. [ "$(basename "$(dirname "$PWD")")" = 'node_modules' ] || # The name of the containing directory that 'npm` uses, which looks like diff --git a/scripts/utils/convert-jsr-readme.cjs b/scripts/utils/convert-jsr-readme.cjs new file mode 100644 index 000000000..dd9f718b9 --- /dev/null +++ b/scripts/utils/convert-jsr-readme.cjs @@ -0,0 +1,140 @@ +const fs = require('fs'); +const { parse } = require('@typescript-eslint/parser'); +const { TSError } = require('@typescript-eslint/typescript-estree'); + +/** + * Quick and dirty AST traversal + */ +function traverse(node, visitor) { + if (!node || typeof node.type !== 'string') return; + visitor.node?.(node); + visitor[node.type]?.(node); + for (const key in node) { + const value = node[key]; + if (Array.isArray(value)) { + for (const elem of value) traverse(elem, visitor); + } else if (value instanceof Object) { + traverse(value, visitor); + } + } +} + +/** + * Helper method for replacing arbitrary ranges of text in input code. + */ +function replaceRanges(code, replacer) { + const replacements = []; + replacer({ replace: (range, replacement) => replacements.push({ range, replacement }) }); + + if (!replacements.length) return code; + replacements.sort((a, b) => a.range[0] - b.range[0]); + const overlapIndex = replacements.findIndex( + (r, index) => index > 0 && replacements[index - 1].range[1] > r.range[0], + ); + if (overlapIndex >= 0) { + throw new Error( + `replacements overlap: ${JSON.stringify(replacements[overlapIndex - 1])} and ${JSON.stringify( + replacements[overlapIndex], + )}`, + ); + } + + const parts = []; + let end = 0; + for (const { + range: [from, to], + replacement, + } of replacements) { + if (from > end) parts.push(code.substring(end, from)); + parts.push(replacement); + end = to; + } + if (end < code.length) parts.push(code.substring(end)); + return parts.join(''); +} + +function replaceProcessEnv(content) { + // Replace process.env['KEY'] and process.env.KEY with Deno.env.get('KEY') + return content.replace(/process\.env(?:\.|\[['"])(.+?)(?:['"]\])/g, "Deno.env.get('$1')"); +} + +function replaceProcessStdout(content) { + return content.replace(/process\.stdout.write\(([^)]+)\)/g, 'Deno.stdout.writeSync($1)'); +} + +function replaceInstallationDirections(content) { + // Remove npm installation section + return content.replace(/```sh\nnpm install.*?\n```.*### Installation from JSR\n\n/s, ''); +} + +/** + * Maps over module paths in imports and exports + */ +function replaceImports(code, config) { + try { + const ast = parse(code, { sourceType: 'module', range: true }); + return replaceRanges(code, ({ replace }) => + traverse(ast, { + node(node) { + switch (node.type) { + case 'ImportDeclaration': + case 'ExportNamedDeclaration': + case 'ExportAllDeclaration': + case 'ImportExpression': + if (node.source) { + const { range, value } = node.source; + if (value.startsWith(config.npm)) { + replace(range, JSON.stringify(value.replace(config.npm, config.jsr))); + } + } + } + }, + }), + ); + } catch (e) { + if (e instanceof TSError) { + // This can error if the code block is not valid TS, in this case give up trying to transform the imports. + console.warn(`Original codeblock could not be parsed, replace import skipped: ${e}\n\n${code}`); + return code; + } + throw e; + } +} + +function processReadme(config, file) { + try { + let readmeContent = fs.readFileSync(file, 'utf8'); + + // First replace installation directions + readmeContent = replaceInstallationDirections(readmeContent); + + // Replace content in all code blocks with a single regex + readmeContent = readmeContent.replaceAll( + /```(?:typescript|ts|javascript|js)\n([\s\S]*?)```/g, + (match, codeBlock) => { + try { + let transformedCode = codeBlock.trim(); + transformedCode = replaceImports(transformedCode, config); + transformedCode = replaceProcessEnv(transformedCode); + transformedCode = replaceProcessStdout(transformedCode); + return '```typescript\n' + transformedCode + '\n```'; + } catch (error) { + console.warn(`Failed to transform code block: ${error}\n\n${codeBlock}`); + return match; // Return original code block if transformation fails + } + }, + ); + + fs.writeFileSync(file, readmeContent); + } catch (error) { + console.error('Error processing README:', error); + throw error; + } +} + +const config = { + npm: '@valtown/sdk', + jsr: '@valtown/sdk', +}; + +processReadme(config, process.argv[2]); diff --git a/scripts/utils/git-swap.sh b/scripts/utils/git-swap.sh new file mode 100755 index 000000000..79d1888eb --- /dev/null +++ b/scripts/utils/git-swap.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash +set -exuo pipefail +# the package is published to NPM from ./dist +# we want the final file structure for git installs to match the npm installs, so we + +# delete everything except ./dist and ./node_modules +find . -maxdepth 1 -mindepth 1 ! -name 'dist' ! -name 'node_modules' -exec rm -rf '{}' + + +# move everything from ./dist to . +mv dist/* . + +# delete the now-empty ./dist +rmdir dist diff --git a/src/core.ts b/src/core.ts index 6b4427169..fa4737241 100644 --- a/src/core.ts +++ b/src/core.ts @@ -163,7 +163,7 @@ export abstract class APIClient { maxRetries = 2, timeout = 60000, // 1 minute httpAgent, - fetch: overridenFetch, + fetch: overriddenFetch, }: { baseURL: string; maxRetries?: number | undefined; @@ -176,7 +176,7 @@ export abstract class APIClient { this.timeout = validatePositiveInteger('timeout', timeout); this.httpAgent = httpAgent; - this.fetch = overridenFetch ?? fetch; + this.fetch = overriddenFetch ?? fetch; } protected authHeaders(opts: FinalRequestOptions): Headers { @@ -522,20 +522,24 @@ export abstract class APIClient { const timeout = setTimeout(() => controller.abort(), ms); + const fetchOptions = { + signal: controller.signal as any, + ...options, + }; + if (fetchOptions.method) { + // Custom methods like 'patch' need to be uppercased + // See https://github.com/nodejs/undici/issues/2294 + fetchOptions.method = fetchOptions.method.toUpperCase(); + } + return ( - this.getRequestClient() - // use undefined this binding; fetch errors if bound to something else in browser/cloudflare - .fetch.call(undefined, url, { signal: controller.signal as any, ...options }) - .finally(() => { - clearTimeout(timeout); - }) + // use undefined this binding; fetch errors if bound to something else in browser/cloudflare + this.fetch.call(undefined, url, fetchOptions).finally(() => { + clearTimeout(timeout); + }) ); } - protected getRequestClient(): RequestClient { - return { fetch: this.fetch }; - } - private shouldRetry(response: Response): boolean { // Note this is not a standard header. const shouldRetryHeader = response.headers.get('x-should-retry'); @@ -976,8 +980,8 @@ export const safeJSON = (text: string) => { } }; -// https://stackoverflow.com/a/19709846 -const startsWithSchemeRegexp = new RegExp('^(?:[a-z]+:)?//', 'i'); +// https://url.spec.whatwg.org/#url-scheme-string +const startsWithSchemeRegexp = /^[a-z][a-z0-9+.-]*:/i; const isAbsoluteURL = (url: string): boolean => { return startsWithSchemeRegexp.test(url); }; diff --git a/src/error.ts b/src/error.ts index 4c1f05725..0153f7aee 100644 --- a/src/error.ts +++ b/src/error.ts @@ -4,17 +4,19 @@ import { castToError, Headers } from './core'; export class ValTownError extends Error {} -export class APIError extends ValTownError { - readonly status: number | undefined; - readonly headers: Headers | undefined; - readonly error: Object | undefined; - - constructor( - status: number | undefined, - error: Object | undefined, - message: string | undefined, - headers: Headers | undefined, - ) { +export class APIError< + TStatus extends number | undefined = number | undefined, + THeaders extends Headers | undefined = Headers | undefined, + TError extends Object | undefined = Object | undefined, +> extends ValTownError { + /** HTTP status for the response that caused the error */ + readonly status: TStatus; + /** HTTP headers for the response that caused the error */ + readonly headers: THeaders; + /** JSON body of the response that caused the error */ + readonly error: TError; + + constructor(status: TStatus, error: TError, message: string | undefined, headers: THeaders) { super(`${APIError.makeMessage(status, error, message)}`); this.status = status; this.headers = headers; @@ -48,7 +50,7 @@ export class APIError extends ValTownError { message: string | undefined, headers: Headers | undefined, ): APIError { - if (!status) { + if (!status || !headers) { return new APIConnectionError({ message, cause: castToError(errorResponse) }); } @@ -90,17 +92,13 @@ export class APIError extends ValTownError { } } -export class APIUserAbortError extends APIError { - override readonly status: undefined = undefined; - +export class APIUserAbortError extends APIError { constructor({ message }: { message?: string } = {}) { super(undefined, undefined, message || 'Request was aborted.', undefined); } } -export class APIConnectionError extends APIError { - override readonly status: undefined = undefined; - +export class APIConnectionError extends APIError { constructor({ message, cause }: { message?: string | undefined; cause?: Error | undefined }) { super(undefined, undefined, message || 'Connection error.', undefined); // in some environments the 'cause' property is already declared @@ -115,32 +113,18 @@ export class APIConnectionTimeoutError extends APIConnectionError { } } -export class BadRequestError extends APIError { - override readonly status: 400 = 400; -} +export class BadRequestError extends APIError<400, Headers> {} -export class AuthenticationError extends APIError { - override readonly status: 401 = 401; -} +export class AuthenticationError extends APIError<401, Headers> {} -export class PermissionDeniedError extends APIError { - override readonly status: 403 = 403; -} +export class PermissionDeniedError extends APIError<403, Headers> {} -export class NotFoundError extends APIError { - override readonly status: 404 = 404; -} +export class NotFoundError extends APIError<404, Headers> {} -export class ConflictError extends APIError { - override readonly status: 409 = 409; -} +export class ConflictError extends APIError<409, Headers> {} -export class UnprocessableEntityError extends APIError { - override readonly status: 422 = 422; -} +export class UnprocessableEntityError extends APIError<422, Headers> {} -export class RateLimitError extends APIError { - override readonly status: 429 = 429; -} +export class RateLimitError extends APIError<429, Headers> {} -export class InternalServerError extends APIError {} +export class InternalServerError extends APIError {} diff --git a/src/version.ts b/src/version.ts index 7434729b5..4931d4f69 100644 --- a/src/version.ts +++ b/src/version.ts @@ -1 +1 @@ -export const VERSION = '0.24.0'; // x-release-please-version +export const VERSION = '0.25.0'; // x-release-please-version diff --git a/tests/index.test.ts b/tests/index.test.ts index c1c710233..3836ac726 100644 --- a/tests/index.test.ts +++ b/tests/index.test.ts @@ -122,6 +122,23 @@ describe('instantiate client', () => { expect(spy).toHaveBeenCalledTimes(1); }); + test('normalized method', async () => { + let capturedRequest: RequestInit | undefined; + const testFetch = async (url: RequestInfo, init: RequestInit = {}): Promise => { + capturedRequest = init; + return new Response(JSON.stringify({}), { headers: { 'Content-Type': 'application/json' } }); + }; + + const client = new ValTown({ + baseURL: 'http://localhost:5000/', + bearerToken: 'My Bearer Token', + fetch: testFetch, + }); + + await client.patch('/foo'); + expect(capturedRequest?.method).toEqual('PATCH'); + }); + describe('baseUrl', () => { test('trailing slash', () => { const client = new ValTown({ @@ -183,7 +200,7 @@ describe('instantiate client', () => { expect(client.bearerToken).toBe('My Bearer Token'); }); - test('with overriden environment variable arguments', () => { + test('with overridden environment variable arguments', () => { // set options via env var process.env['VAL_TOWN_API_KEY'] = 'another My Bearer Token'; const client = new ValTown({ bearerToken: 'My Bearer Token' }); diff --git a/yarn.lock b/yarn.lock index bfd47d8da..bb1794207 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1370,9 +1370,9 @@ create-require@^1.1.0: integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== cross-spawn@^7.0.2, cross-spawn@^7.0.3: - version "7.0.3" - resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" - integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== + version "7.0.6" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f" + integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA== dependencies: path-key "^3.1.0" shebang-command "^2.0.0"