diff --git a/.copilot/README.md b/.copilot/README.md new file mode 100644 index 0000000..aee49fa --- /dev/null +++ b/.copilot/README.md @@ -0,0 +1,19 @@ +# Copilot Suggestions + +This directory contains GitHub Copilot-specific configuration to help maintain consistent code quality and style. + +## Features + +- Pattern-based file filtering +- TypeScript-specific suggestions +- Naming convention enforcement +- Best practices suggestions + +## Configuration + +The `settings.json` file contains: + +- File patterns to include/exclude +- Language-specific rules +- Coding style preferences +- Type checking preferences diff --git a/.copilot/instructions.md b/.copilot/instructions.md new file mode 100644 index 0000000..7339dca --- /dev/null +++ b/.copilot/instructions.md @@ -0,0 +1,42 @@ +# Standing Instructions for GitHub Copilot + +- Follow the project's coding standards and style guides. +- Use TypeScript for all new code. +- Use descriptive variable and function names. +- Avoid using any deprecated APIs or libraries. +- Use the projects linting rules, don't suggest code breaking them. +- Always have a namespace when creating a type, even if empty +- For all exported `type` and `interface` declarations provide `type`, `is` and `flaw` definitions. + +# Coding Standards + +- Functions only return in one place. +- Use the name `result` for what the function returns even when it is modified on the last line. +- Never use abbreviations except "UI", "Id", "max", "min". +- Prefer fewer lines of code over shorter lines. +- Prefer expressions over statements. +- Never use unnecessary braces. +- Rely on type system and only use `===` and `!==` when strictly necessary. +- In tests, always import the top level export from the packages index file like this: `import { isoly } from "../index"` with adjustments for path. +- In tests, prefer using `it.each` where possible. +- Don't use braces in lambda function when not required. +- Prefer single word identifier names. +- Only use single letter identifiers if usage is kept within a maximum of 3 lines. +- Make test descriptions short, use function names and single words describing test. +- No blank lines between test cases in test files. + +# Test File Structure Example + +```typescript +describe("isoly.Something", () => { + it.each([ + ["input1", "output1"], + ["input2", "output2"], + ])("test description %s", (input, output) => expect(something(input)).toBe(output)) + it.each([ + ["input3", "output3"], + ["input4", "output4"], + ])("another test %s", (input, output) => expect(something(input)).toBe(output)) + it("single test", () => expect(something()).toBe(true)) +}) +``` diff --git a/.copilot/settings.json b/.copilot/settings.json new file mode 100644 index 0000000..1bdc935 --- /dev/null +++ b/.copilot/settings.json @@ -0,0 +1,28 @@ +{ + "patterns": [ + "**/*.ts", + "**/*.json", + "**/*.md", + "!**/node_modules/**", + "!**/dist/**" + ], + "suggestions": { + "typescript": { + "description": "TypeScript suggestions:", + "rules": { + "identifier-pattern": { + "interfaces": "PascalCase", + "types": "PascalCase", + "variables": "camelCase", + "functions": "camelCase", + "enum-members": "PascalCase" + }, + "no-duplicate-generics": true, + "no-implicit-any": true, + "no-unused-variables": true, + "prefer-const": true, + "strict-boolean-expressions": true + } + } + } +} diff --git a/.eslintrc b/.eslintrc index a6f139a..2fb6820 100644 --- a/.eslintrc +++ b/.eslintrc @@ -10,6 +10,7 @@ "eslint:recommended", "plugin:@typescript-eslint/eslint-recommended", "plugin:@typescript-eslint/recommended", + "plugin:@typescript-eslint/recommended-requiring-type-checking", "plugin:prettierx/default" ], "rules": { @@ -21,9 +22,9 @@ ], "@typescript-eslint/no-explicit-any": "off", "@typescript-eslint/no-namespace": "off", - "prefer-const": 1, + "prefer-const": "warn", "@typescript-eslint/no-unused-vars": [ - 2, + "error", { "vars": "all", "args": "none", @@ -31,8 +32,18 @@ } ], "@typescript-eslint/explicit-module-boundary-types": "off", + "@typescript-eslint/no-non-null-assertion": "error", + "@typescript-eslint/no-floating-promises": "error", + "@typescript-eslint/naming-convention": [ + "error", + { + "selector": "interface", + "format": ["PascalCase"] + } + ], "no-case-declarations": "off", "no-inner-declarations": "off", + "no-console": "warn", "sort-imports": "off", "simple-import-sort/imports": [ "error", diff --git a/.github/workflows/workflows/bump-alpha.yml b/.github/workflows/workflows/bump-alpha.yml new file mode 100644 index 0000000..38577ec --- /dev/null +++ b/.github/workflows/workflows/bump-alpha.yml @@ -0,0 +1,36 @@ +name: "Bump Alpha" + +on: + push: + branches: + - "master-alpha" + - "master-1" +jobs: + bump-version: + name: "Bump Alpha Version" + timeout-minutes: 60 + runs-on: ubuntu-latest + if: "!contains(github.event.head_commit.message, 'ci: version bump to ')" + + steps: + - name: "Checkout source code" + uses: "actions/checkout@v4" + with: + ref: ${{ github.ref }} + token: ${{ secrets.ADMIN_TOKEN }} + - name: "Setup Node" + uses: "actions/setup-node@v3" + with: + node-version: current + cache: "npm" + - name: "Version Bump" + id: version-bump + uses: "phips28/gh-action-bump-version@master" + with: + default: prerelease + version-type: "prerelease" + preid: alpha + rc-wording: "" + tag-prefix: "release-" + env: + GITHUB_TOKEN: ${{ secrets.ADMIN_TOKEN }} diff --git a/.github/workflows/workflows/bump.yml b/.github/workflows/workflows/bump.yml new file mode 100644 index 0000000..4151e4a --- /dev/null +++ b/.github/workflows/workflows/bump.yml @@ -0,0 +1,33 @@ +name: "Bump" + +on: + push: + branches: + - "master" + - "master-0" +jobs: + bump-version: + name: "Bump version on master" + timeout-minutes: 60 + runs-on: ubuntu-latest + if: "!contains(github.event.head_commit.message, 'ci: version bump to ')" + + steps: + - name: "Checkout source code" + uses: "actions/checkout@v3" + with: + token: ${{ secrets.ADMIN_TOKEN }} + - name: "Setup Node" + uses: "actions/setup-node@v3" + with: + node-version: current + cache: 'npm' + - name: Update NPM + run: npm install -g npm + - name: "Version Bump" + id: version-bump + uses: "phips28/gh-action-bump-version@master" + with: + tag-prefix: 'release-' + env: + GITHUB_TOKEN: ${{ secrets.ADMIN_TOKEN }} diff --git a/.github/workflows/workflows/ci.yml b/.github/workflows/workflows/ci.yml new file mode 100644 index 0000000..6c27c8d --- /dev/null +++ b/.github/workflows/workflows/ci.yml @@ -0,0 +1,49 @@ +name: CI + +on: + pull_request: + +jobs: + build: + name: Build + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: 20.10.0 + cache: 'npm' + - run: npm install + - run: npm run build + test: + name: Test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: 20.10.0 + cache: 'npm' + - run: npm install + - run: npm run test + lint: + name: Lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: 20.10.0 + cache: 'npm' + - run: npm install + - run: npm run lint + audit: + name: Audit + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: current + cache: 'npm' + - run: npm audit diff --git a/.github/workflows/workflows/publish.yml b/.github/workflows/workflows/publish.yml new file mode 100644 index 0000000..2d64921 --- /dev/null +++ b/.github/workflows/workflows/publish.yml @@ -0,0 +1,39 @@ +name: "Publish" + +on: + push: + tags: + - "release-*" +jobs: + publish: + name: "Publish" + timeout-minutes: 60 + runs-on: ubuntu-latest + steps: + - name: "Checkout source code" + uses: "actions/checkout@v3" + - name: "Setup Node" + uses: "actions/setup-node@v3" + with: + node-version: current + cache: "npm" + - uses: actions/cache@v3 + with: + path: "**/node_modules" + key: node_modules-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }} + - name: Install + run: npm install + - name: Build + run: npm run build + - name: Publish + run: | + echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > .npmrc + VERSION=$(node -p "require('./package.json').version") + if [[ "$VERSION" == *-* ]]; then + npm publish --access public --tag prerelease + else + npm publish --access public + fi + shell: bash + env: + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.prettierrc b/.prettierrc index bb69252..55f1430 100644 --- a/.prettierrc +++ b/.prettierrc @@ -4,8 +4,9 @@ "semi": false, "singleQuote": false, "bracketSpacing": true, - "jsxBracketSameLine": true, + "bracketSameLine": true, "arrowParens": "avoid", "endOfLine": "lf", - "breakBeforeStatement": "always" -} + "proseWrap": "preserve", + "htmlWhitespaceSensitivity": "strict" +} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..938f3fe --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,205 @@ +# Contributing to `cloudly-http` + +This document outlines the coding standards and guidelines for contributing to the cloudly-http project. + +## Project Overview + +`cloudly-http` is a TypeScript library providing improved handling of HTTP Requests and Responses. The project follows strict coding conventions to maintain consistency and quality across the utily ecosystem. + +## Project Setup + +1. Install dependencies: + +```bash +npm install +``` + +2. Build the project: + +```bash +npm run build +``` + +3. Run tests: + +```bash +npm test +``` + +## Coding Standards + +### TypeScript & Types + +1. All code must be written in TypeScript +2. Keep interfaces and types minimal and focused +3. Use generics when a type needs to be flexible (e.g., `Client`) +4. Prefer interfaces for public APIs +5. Use type guards when runtime type checking is needed + +Example: + +```typescript +export interface Request { + url: string + method: Method + header?: Request.Header + body?: any +} + +export namespace Request { + export interface Header { + contentType?: string + authorization?: string + // ...other header fields + } + + export function create(request: Partial): Request { + return { + url: request.url ?? "", + method: request.method ?? "GET", + header: request.header, + body: request.body, + } + } +} +``` + +### Code Structure + +1. Functions should have single return points +2. Use `result` as the variable name for function return values +3. Prefer fewer lines of code over shorter lines +4. Prefer expressions over statements +5. Avoid unnecessary braces +6. Use strict equality (`===` and `!==`) only when necessary +7. Rely on TypeScript's type system for type checking + +### File Organization & Structure + +1. Main components: + + - Keep HTTP-related functionality in root directory (`Client.ts`, `Request.ts`, etc.) + - Group related functionality in subdirectories (e.g., `Request/`, `Response/`, `Socket/`) + - Use index files to re-export functionality + - Keep directory structures shallow (max 2 levels deep) + +2. Implementation files: + + - Aim to keep files under 150 lines of code + - Each file should have a single responsibility (e.g., `Client.ts` handles HTTP client operations) + - Split complex components into submodules (e.g., `Request/Header.ts`) + - Keep class and interface definitions focused + +3. Test files: + - Place test files next to implementation files with `.spec.ts` extension + - Keep test cases focused on a single behavior + - Use descriptive test names that explain the expected behavior + - Group related test cases using `describe` blocks + +### Naming Conventions + +1. No abbreviations except: + + - "UI" (uppercase because it's a two-letter multi-word abbreviation) + - "Id" (regular casing) + - "max" + - "min" + +2. When using abbreviations: + + - Multi-word abbreviations of 1-2 letters stay uppercase (e.g., "UI") + - All other abbreviations follow normal casing rules regardless of word count: + - In PascalCase: "Id", "Utf", "Iso", etc. + - In camelCase: "id", "utf", "iso", etc. + +3. Prefer single word identifiers + +4. Single letter identifiers only allowed if usage is within 3 lines + +5. Use descriptive and clear names for variables and functions + +### Testing + +1. Always import from the package's index file: + + ```typescript + import { http } from "../index" + ``` + +2. Test both success and error cases: + + ```typescript + describe("Client", () => { + it("should handle successful responses", async () => { + const client = new Client("https://api.example.com") + const response = await client.get("/data") + expect(response).toBeDefined() + }) + + it("should handle authorization errors", async () => { + const client = new Client("https://api.example.com") + const response = await client.get("/protected") + expect(response.status).toBe(401) + }) + }) + ``` + +3. Keep test descriptions focused on behavior +4. Test files should match implementation files with `.spec.ts` extension +5. Use meaningful test data that represents real use cases + +### Code Formatting + +The project uses ESLint and Prettier with the following configuration: + +1. Print width: 120 characters +2. Use tabs for indentation +3. No semicolons +4. Double quotes for strings +5. LF line endings + +### Import Order + +1. Import order is enforced by eslint-plugin-simple-import-sort +2. Imports are grouped in the following order: + - Core/framework imports + - External packages + - Internal modules + - Relative imports + +## Pull Request Process + +1. Create a branch for your feature/fix +2. Ensure code passes all tests: `npm test` +3. Ensure code passes linting: `npm run lint` +4. Run the verification script: `npm run verify` +5. Update documentation as needed +6. Create a pull request with a clear description + +## Development Workflow + +1. Build the project: + + ```bash + npm run build + ``` + +2. Run tests: + + ```bash + npm test + ``` + +3. Check and fix linting: + + ```bash + npm run lint + npm run fix + ``` + +4. Before submitting changes: + - Ensure all tests pass + - Check that changes follow the coding style + - Update documentation if needed + - Update README.md if adding new features + - Create or update examples if appropriate diff --git a/README.md b/README.md index 1bc2a97..b823747 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,94 @@ # Cloudly HTTP -Improved handling of HTTP Requests and Responses. +A TypeScript library for improved handling of HTTP Requests and Responses. This library provides a robust, type-safe way to make HTTP requests with features like automatic content-type handling, response parsing, and WebSocket support. -## Breaking changes +## Features -### 0.1.0 +- 🔒 Type-safe HTTP client with generic error handling +- 🔄 Automatic request/response content-type parsing +- 🎯 Customizable request preprocessing and response postprocessing +- 🔑 Built-in authorization header support +- 🌐 WebSocket support with JSON, string, and ArrayBuffer messaging +- 📝 Form data and URL-encoded data handling +- 🚦 Configurable error and unauthorized request handling -- Client constructor takes named callbacks. -- `path`-parameter now should be prefixed with `/`. +## Installation + +```bash +npm install cloudly-http +``` + +## Basic Usage + +```typescript +import { Client } from "cloudly-http" + +// Create a client +const client = new Client("https://api.example.com", "your-api-key") + +// Make requests +const data = await client.get("/users") +const user = await client.post("/users", { name: "John" }) +``` + +## Advanced Features + +### Custom Authorization + +```typescript +const client = new Client("https://api.example.com", undefined, { + getHeader: async request => ({ + authorization: `Bearer ${await getToken()}`, + }), +}) +``` + +### Handle Unauthorized Requests + +```typescript +const client = new Client("https://api.example.com", "initial-token", { + onUnauthorized: async client => { + const newToken = await refreshToken() + client.key = newToken + return true // retry the request + }, +}) +``` + +### WebSocket Support + +```typescript +import { Socket } from "cloudly-http" + +const socket = new Socket.Json(websocket) +socket.send({ type: "message", content: "Hello!" }) +``` + +## API Documentation + +### Client + +The main class for making HTTP requests: + +- `get(path: string, header?: Request.Header): Promise` +- `post(path: string, request: any, header?: Request.Header): Promise` +- `put(path: string, request: any, header?: Request.Header): Promise` +- `patch(path: string, request: any, header?: Request.Header): Promise` +- `delete(path: string, header?: Request.Header): Promise` + +### Request/Response Processing + +- Automatic content-type handling for common types: + - JSON (`application/json`) + - Form data (`multipart/form-data`) + - URL-encoded data (`application/x-www-form-urlencoded`) + - Plain text (`text/plain`, `text/html`) + - PDF (`application/pdf`) + +## Contributing + +See [CONTRIBUTING.md](./CONTRIBUTING.md) for development guidelines. + +## License + +Released under the [MIT License](./LICENSE). diff --git a/package.json b/package.json index 23ec484..efa8fef 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "cloudly-http", "version": "0.1.7", "description": "Improved handling of HTTP Requests and Responses.", - "author": "Simon Mika ", + "author": "Cloudly Contributors", "license": "MIT", "repository": "https://github.com/utily/cloudly-http", "bugs": { diff --git a/tsconfig.json b/tsconfig.json index b6782b4..46d54f6 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,14 +2,19 @@ "extends": "./tsconfig.test.json", "compilerOptions": { "declaration": true, + "declarationMap": true, "sourceMap": true, "sourceRoot": "../", "outDir": "dist", "baseUrl": ".", - "allowSyntheticDefaultImports":true, + "allowSyntheticDefaultImports": true, + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noUncheckedIndexedAccess": true, "types": [] }, "files": [ - "index.ts", + "index.ts" ] } diff --git a/tsconfig.test.json b/tsconfig.test.json index e61294f..154e94b 100644 --- a/tsconfig.test.json +++ b/tsconfig.test.json @@ -1,9 +1,9 @@ { "compilerOptions": { - "target": "es2021", - "module": "es2020", + "target": "es2022", + "module": "es2022", "lib": [ - "es2021", + "es2022", "webworker" ], "allowJs": true,