diff --git a/packages/content-type-text/.eslintrc.cjs b/packages/content-type-text/.eslintrc.cjs new file mode 100644 index 0000000..d46b01d --- /dev/null +++ b/packages/content-type-text/.eslintrc.cjs @@ -0,0 +1,7 @@ +module.exports = { + root: true, + extends: ["custom"], + parserOptions: { + project: "./tsconfig.eslint.json", + }, +}; diff --git a/packages/content-type-text/LICENSE b/packages/content-type-text/LICENSE new file mode 100644 index 0000000..ae6695a --- /dev/null +++ b/packages/content-type-text/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 XMTP (xmtp.org) + +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: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/content-type-text/README.md b/packages/content-type-text/README.md new file mode 100644 index 0000000..8a994e3 --- /dev/null +++ b/packages/content-type-text/README.md @@ -0,0 +1,45 @@ +# Text content type + +This package provides an XMTP content type to support text messages. + +## Install the package + +```bash +# npm +npm i @xmtp/content-type-text + +# yarn +yarn add @xmtp/content-type-text + +# pnpm +pnpm i @xmtp/content-type-text +``` + +## Send a text message + +Use a string to send a text message. It's not required to specify a content type in the send options for text messages. + +```tsx +await conversation.send("gm"); +``` + +## Developing + +Run `yarn dev` to build the content type and watch for changes, which will trigger a rebuild. + +## Testing + +Before running unit tests, start the required Docker container at the root of this repository. For more info, see [Running tests](../../README.md#running-tests). + +## Useful commands + +- `yarn build`: Builds the content type +- `yarn clean`: Removes `node_modules`, `dist`, and `.turbo` folders +- `yarn dev`: Builds the content type and watches for changes, which will trigger a rebuild +- `yarn format`: Runs prettier format and write changes +- `yarn format:check`: Runs prettier format check +- `yarn lint`: Runs ESLint +- `yarn test:setup`: Starts a necessary docker container for testing +- `yarn test:teardown`: Stops docker container for testing +- `yarn test`: Runs all unit tests +- `yarn typecheck`: Runs `tsc` diff --git a/packages/content-type-text/package.json b/packages/content-type-text/package.json new file mode 100644 index 0000000..9f2cd33 --- /dev/null +++ b/packages/content-type-text/package.json @@ -0,0 +1,98 @@ +{ + "name": "@xmtp/content-type-text", + "version": "1.0.0", + "description": "An XMTP content type to support text messages", + "keywords": [ + "xmtp", + "messaging", + "web3", + "js", + "ts", + "javascript", + "typescript", + "content-types" + ], + "homepage": "https://github.com/xmtp/xmtp-js-content-types", + "bugs": { + "url": "https://github.com/xmtp/xmtp-js-content-types/issues" + }, + "repository": { + "type": "git", + "url": "git+ssh://git@github.com/xmtp/xmtp-js-content-types.git", + "directory": "packages/content-type-text" + }, + "license": "MIT", + "author": "XMTP Labs ", + "sideEffects": false, + "type": "module", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "browser": "./dist/browser/index.js", + "import": "./dist/index.js", + "require": "./dist/index.cjs" + } + }, + "main": "dist/index.cjs", + "module": "dist/index.js", + "browser": "dist/browser/index.js", + "types": "dist/index.d.ts", + "files": [ + "dist" + ], + "scripts": { + "build": "yarn clean:dist && yarn rollup -c", + "clean": "rimraf .turbo node_modules && yarn clean:dist", + "clean:dist": "rimraf dist", + "dev": "yarn clean:dist && yarn rollup -c --watch", + "format": "yarn format:base -w .", + "format:base": "prettier --ignore-path ../../.gitignore", + "format:check": "yarn format:base -c .", + "lint": "eslint . --ignore-path ../../.gitignore", + "test": "yarn test:node && yarn test:jsdom", + "test:jsdom": "NODE_TLS_REJECT_UNAUTHORIZED=0 vitest run --environment happy-dom", + "test:node": "NODE_TLS_REJECT_UNAUTHORIZED=0 vitest run --environment node", + "typecheck": "tsc --noEmit" + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 3 chrome versions", + "last 3 firefox versions", + "last 3 safari versions" + ] + }, + "dependencies": { + "@xmtp/content-type-primitives": "^1.0.1" + }, + "devDependencies": { + "@ianvs/prettier-plugin-sort-imports": "^4.2.1", + "@rollup/plugin-terser": "^0.4.4", + "@rollup/plugin-typescript": "^11.1.6", + "@types/node": "^18.19.22", + "@xmtp/xmtp-js": "^11.6.3", + "buffer": "^6.0.3", + "eslint": "^8.57.0", + "eslint-config-custom": "workspace:*", + "ethers": "^6.11.1", + "happy-dom": "^13.7.3", + "prettier": "^3.2.5", + "prettier-plugin-packagejson": "^2.4.12", + "rimraf": "^5.0.5", + "rollup": "^4.12.1", + "rollup-plugin-dts": "^6.1.0", + "rollup-plugin-filesize": "^10.0.0", + "typescript": "^5.4.2", + "vite": "^5.1.6", + "vitest": "^1.3.1" + }, + "publishConfig": { + "access": "public", + "provenance": true, + "registry": "https://registry.npmjs.org/" + } +} diff --git a/packages/content-type-text/rollup.config.js b/packages/content-type-text/rollup.config.js new file mode 100644 index 0000000..b6b0494 --- /dev/null +++ b/packages/content-type-text/rollup.config.js @@ -0,0 +1,58 @@ +import terser from "@rollup/plugin-terser"; +import typescript from "@rollup/plugin-typescript"; +import { defineConfig } from "rollup"; +import { dts } from "rollup-plugin-dts"; +import filesize from "rollup-plugin-filesize"; + +const plugins = [ + typescript({ + declaration: false, + declarationMap: false, + }), + filesize({ + showMinifiedSize: false, + }), +]; + +const external = ["@xmtp/xmtp-js"]; + +export default defineConfig([ + { + input: "src/index.ts", + output: { + file: "dist/index.js", + format: "es", + sourcemap: true, + }, + plugins, + external, + }, + { + input: "src/index.ts", + output: { + file: "dist/browser/index.js", + format: "es", + sourcemap: true, + }, + plugins: [...plugins, terser()], + external, + }, + { + input: "src/index.ts", + output: { + file: "dist/index.cjs", + format: "cjs", + sourcemap: true, + }, + plugins, + external, + }, + { + input: "src/index.ts", + output: { + file: "dist/index.d.ts", + format: "es", + }, + plugins: [dts()], + }, +]); diff --git a/packages/content-type-text/src/Text.test.ts b/packages/content-type-text/src/Text.test.ts new file mode 100644 index 0000000..8f98d7d --- /dev/null +++ b/packages/content-type-text/src/Text.test.ts @@ -0,0 +1,44 @@ +import { describe, expect, it } from "vitest"; +import { ContentTypeText, Encoding, TextCodec } from "./Text"; + +describe("ContentTypeText", () => { + it("can encode/decode text", () => { + const text = "Hey"; + const codec = new TextCodec(); + const ec = codec.encode(text); + expect(ec.type.sameAs(ContentTypeText)).toBe(true); + expect(ec.parameters.encoding).toEqual(Encoding.utf8); + const text2 = codec.decode(ec); + expect(text2).toEqual(text); + }); + + it("defaults to utf-8", () => { + const text = "Hey"; + const codec = new TextCodec(); + const ec = codec.encode(text); + expect(ec.type.sameAs(ContentTypeText)).toBe(true); + expect(ec.parameters.encoding).toEqual(Encoding.utf8); + const text2 = codec.decode(ec); + expect(text2).toEqual(text); + }); + + it("throws on invalid input", () => { + const codec = new TextCodec(); + const ec = { + type: ContentTypeText, + parameters: {}, + content: {} as Uint8Array, + }; + expect(() => codec.decode(ec)).toThrow(); + }); + + it("throws on unknown encoding", () => { + const codec = new TextCodec(); + const ec = { + type: ContentTypeText, + parameters: { encoding: "UTF-16" }, + content: new Uint8Array(0), + }; + expect(() => codec.decode(ec)).toThrow("unrecognized encoding UTF-16"); + }); +}); diff --git a/packages/content-type-text/src/Text.ts b/packages/content-type-text/src/Text.ts new file mode 100644 index 0000000..12f7471 --- /dev/null +++ b/packages/content-type-text/src/Text.ts @@ -0,0 +1,46 @@ +import { + ContentTypeId, + type ContentCodec, + type EncodedContent, +} from "@xmtp/content-type-primitives"; + +export const ContentTypeText = new ContentTypeId({ + authorityId: "xmtp.org", + typeId: "text", + versionMajor: 1, + versionMinor: 0, +}); + +export enum Encoding { + utf8 = "UTF-8", +} + +export class TextCodec implements ContentCodec { + get contentType(): ContentTypeId { + return ContentTypeText; + } + + encode(content: string): EncodedContent { + return { + type: ContentTypeText, + parameters: { encoding: Encoding.utf8 }, + content: new TextEncoder().encode(content), + }; + } + + decode(content: EncodedContent) { + const { encoding } = content.parameters; + if ((encoding as Encoding) !== Encoding.utf8) { + throw new Error(`unrecognized encoding ${encoding}`); + } + return new TextDecoder().decode(content.content); + } + + fallback() { + return undefined; + } + + shouldPush() { + return true; + } +} diff --git a/packages/content-type-text/src/index.ts b/packages/content-type-text/src/index.ts new file mode 100644 index 0000000..41b613e --- /dev/null +++ b/packages/content-type-text/src/index.ts @@ -0,0 +1 @@ +export { ContentTypeText, Encoding, TextCodec } from "./Text"; diff --git a/packages/content-type-text/tsconfig.eslint.json b/packages/content-type-text/tsconfig.eslint.json new file mode 100644 index 0000000..5ee5f6f --- /dev/null +++ b/packages/content-type-text/tsconfig.eslint.json @@ -0,0 +1,5 @@ +{ + "extends": "./tsconfig.json", + "include": [".", ".eslintrc.cjs", "rollup.config.js"], + "exclude": ["dist", "node_modules"] +} diff --git a/packages/content-type-text/tsconfig.json b/packages/content-type-text/tsconfig.json new file mode 100644 index 0000000..69f4541 --- /dev/null +++ b/packages/content-type-text/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "tsconfig/build.json", + "include": ["src"], + "exclude": ["dist", "node_modules"] +} diff --git a/yarn.lock b/yarn.lock index 8788dd6..1dcbe8a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2084,6 +2084,33 @@ __metadata: languageName: unknown linkType: soft +"@xmtp/content-type-text@workspace:packages/content-type-text": + version: 0.0.0-use.local + resolution: "@xmtp/content-type-text@workspace:packages/content-type-text" + dependencies: + "@ianvs/prettier-plugin-sort-imports": "npm:^4.2.1" + "@rollup/plugin-terser": "npm:^0.4.4" + "@rollup/plugin-typescript": "npm:^11.1.6" + "@types/node": "npm:^18.19.22" + "@xmtp/content-type-primitives": "npm:^1.0.1" + "@xmtp/xmtp-js": "npm:^11.6.3" + buffer: "npm:^6.0.3" + eslint: "npm:^8.57.0" + eslint-config-custom: "workspace:*" + ethers: "npm:^6.11.1" + happy-dom: "npm:^13.7.3" + prettier: "npm:^3.2.5" + prettier-plugin-packagejson: "npm:^2.4.12" + rimraf: "npm:^5.0.5" + rollup: "npm:^4.12.1" + rollup-plugin-dts: "npm:^6.1.0" + rollup-plugin-filesize: "npm:^10.0.0" + typescript: "npm:^5.4.2" + vite: "npm:^5.1.6" + vitest: "npm:^1.3.1" + languageName: unknown + linkType: soft + "@xmtp/content-type-transaction-reference@workspace:packages/content-type-transaction-reference": version: 0.0.0-use.local resolution: "@xmtp/content-type-transaction-reference@workspace:packages/content-type-transaction-reference"