├── .circleci └── config.yml ├── .editorconfig ├── .env.example ├── .eslintignore ├── .eslintrc.json ├── .gitignore ├── .npmrc ├── .nycrc.json ├── .prettierrc.js ├── .tool-versions ├── CHANGELOG.md ├── LICENCE.md ├── README.md ├── docs └── examples │ └── vanilla │ ├── autosuggest │ ├── .env.example │ ├── README.md │ ├── esbuild │ │ ├── build.mjs │ │ ├── common.mjs │ │ └── serve.mjs │ ├── package-lock.json │ ├── package.json │ ├── public │ │ ├── index.html │ │ └── styles.css │ └── src │ │ └── what3words.js │ └── map │ ├── .env.example │ ├── README.md │ ├── esbuild │ ├── build.mjs │ ├── common.mjs │ └── serve.mjs │ ├── package-lock.json │ ├── package.json │ ├── public │ ├── index.html │ └── styles.css │ └── src │ ├── google.js │ └── what3words.js ├── package-lock.json ├── package.json ├── scripts └── languages ├── src ├── client │ ├── autosuggest.ts │ ├── available-languages.ts │ ├── convert-to-3wa.ts │ ├── convert-to-coordinates.ts │ ├── grid-section.ts │ ├── index.ts │ └── response.model.ts ├── index.ts ├── lib │ ├── client │ │ ├── abstract.ts │ │ ├── client.model.ts │ │ └── index.ts │ ├── constants.ts │ ├── index.ts │ ├── languages │ │ ├── index.ts │ │ └── language-codes.ts │ ├── serializer.ts │ ├── transport │ │ ├── axios.ts │ │ ├── error.ts │ │ ├── fetch.ts │ │ ├── index.ts │ │ └── model.ts │ └── validation.ts └── service.ts ├── tests ├── client │ ├── autosuggest.spec.ts │ ├── available-languages.spec.ts │ ├── convert-to-3wa.spec.ts │ ├── convert-to-coordinates.spec.ts │ └── grid-section.spec.ts ├── lib │ ├── languages.spec.ts │ ├── serializer.spec.ts │ ├── transport │ │ ├── axios.spec.ts │ │ ├── custom.spec.ts │ │ ├── fetch.browser.spec.ts │ │ └── fetch.node.spec.ts │ └── validation.spec.ts ├── service.spec.ts ├── tsconfig.json └── utils │ ├── fixtures.ts │ └── setup.tsx ├── tsconfig.json └── vitest.config.ts /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | orbs: 4 | node: circleci/node@5.2.0 5 | 6 | executors: 7 | build-executor: 8 | docker: 9 | - image: cimg/node:18.18.2 10 | resource_class: small 11 | 12 | commands: 13 | setup_environment: 14 | description: "Export pipeline number as env var" 15 | parameters: 16 | version_path: 17 | type: string 18 | default: . 19 | steps: 20 | - run: 21 | name: "Export pipeline number" 22 | command: | 23 | echo "export PIPELINE_NUM=<< pipeline.number >>" >> $BASH_ENV 24 | echo "export VERSION=$(cat << parameters.version_path >>/package.json | grep '\"version\":' | sed -e 's/ \"version\": \"//' | sed -e 's/\",//')" >> $BASH_ENV 25 | 26 | add_fingerprint: 27 | parameters: 28 | fingerprint: 29 | type: string 30 | steps: 31 | - add_ssh_keys: 32 | fingerprints: << parameters.fingerprint >> 33 | 34 | git_config: 35 | parameters: 36 | email: 37 | type: string 38 | author: 39 | type: string 40 | steps: 41 | - run: 42 | name: git configuration 43 | command: | 44 | git config --global user.email << parameters.email >> 45 | git config --global user.name << parameters.author >> 46 | ssh-keyscan github.com >> ~/.ssh/known_hosts 47 | git_tag: 48 | description: "Tag the project" 49 | parameters: 50 | tag: 51 | type: string 52 | when: 53 | type: string 54 | default: on_success 55 | steps: 56 | - run: 57 | name: Git tag 58 | command: | 59 | git tag -f << parameters.tag >> 60 | when: << parameters.when >> 61 | git_push_tags: 62 | description: "Push tags to remote repository" 63 | steps: 64 | - run: 65 | name: git push tags 66 | command: git push -f --tags 67 | 68 | jobs: 69 | build_test: 70 | executor: build-executor 71 | steps: 72 | - checkout 73 | - setup_environment 74 | - node/install-packages 75 | - run: npm run coverage 76 | - run: 77 | name: Inject version constant 78 | command: sed -ie "s/__VERSION__/${VERSION}/g" src/lib/constants.ts 79 | - run: npm run compile 80 | - store_test_results: 81 | path: ./coverage/junit-report.xml 82 | - persist_to_workspace: 83 | root: . 84 | paths: 85 | - .npmrc 86 | - ./dist 87 | - ./package-lock.json 88 | - ./package.json 89 | # - jobstatus 90 | publish: 91 | executor: build-executor 92 | steps: 93 | - attach_workspace: 94 | at: /tmp/workspace 95 | - setup_environment: 96 | version_path: /tmp/workspace 97 | - run: 98 | name: "publish npm" 99 | command: cd /tmp/workspace && npm publish 100 | # - jobstatus 101 | 102 | tag: 103 | executor: build-executor 104 | steps: 105 | - checkout 106 | - add_fingerprint: 107 | fingerprint: "81:f0:38:72:86:c7:e4:b2:92:6b:d8:b8:43:fb:24:6a" 108 | - setup_environment 109 | - git_config: 110 | email: circleci@what3words.com 111 | author: $CIRCLE_USERNAME 112 | - git_tag: 113 | tag: v$VERSION 114 | - git_push_tags 115 | 116 | workflows: 117 | build-and-deploy: 118 | jobs: 119 | - build_test 120 | - publish: 121 | context: org-global 122 | requires: 123 | - build_test 124 | filters: 125 | branches: 126 | only: master 127 | - tag: 128 | requires: 129 | - publish 130 | 131 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | indent_style = space 8 | indent_size = 2 9 | end_of_line = lf 10 | insert_final_newline = true 11 | trim_trailing_whitespace = true 12 | 13 | [*.md] 14 | insert_final_newline = false 15 | trim_trailing_whitespace = false 16 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | W3W_API_KEY="" # Required to fetch languages during build -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | .nyc_output/ 2 | coverage/ 3 | dist/ 4 | node_modules/ 5 | docs/ 6 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./node_modules/gts/", 3 | "rules": { 4 | "object-curly-spacing": ["warn", "always"] 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.toptal.com/developers/gitignore/api/node,macos,windows,visualstudiocode 3 | # Edit at https://www.toptal.com/developers/gitignore?templates=node,macos,windows,visualstudiocode 4 | 5 | ### macOS ### 6 | # General 7 | .DS_Store 8 | .AppleDouble 9 | .LSOverride 10 | 11 | # Icon must end with two \r 12 | Icon 13 | 14 | 15 | # Thumbnails 16 | ._* 17 | 18 | # Files that might appear in the root of a volume 19 | .DocumentRevisions-V100 20 | .fseventsd 21 | .Spotlight-V100 22 | .TemporaryItems 23 | .Trashes 24 | .VolumeIcon.icns 25 | .com.apple.timemachine.donotpresent 26 | 27 | # Directories potentially created on remote AFP share 28 | .AppleDB 29 | .AppleDesktop 30 | Network Trash Folder 31 | Temporary Items 32 | .apdisk 33 | 34 | ### Node ### 35 | # Logs 36 | logs 37 | *.log 38 | npm-debug.log* 39 | yarn-debug.log* 40 | yarn-error.log* 41 | lerna-debug.log* 42 | .pnpm-debug.log* 43 | 44 | # Diagnostic reports (https://nodejs.org/api/report.html) 45 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 46 | 47 | # Runtime data 48 | pids 49 | *.pid 50 | *.seed 51 | *.pid.lock 52 | 53 | # Directory for instrumented libs generated by jscoverage/JSCover 54 | lib-cov 55 | 56 | # Coverage directory used by tools like istanbul 57 | coverage 58 | *.lcov 59 | 60 | # nyc test coverage 61 | .nyc_output 62 | 63 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 64 | .grunt 65 | 66 | # Bower dependency directory (https://bower.io/) 67 | bower_components 68 | 69 | # node-waf configuration 70 | .lock-wscript 71 | 72 | # Compiled binary addons (https://nodejs.org/api/addons.html) 73 | build/Release 74 | 75 | # Dependency directories 76 | node_modules/ 77 | jspm_packages/ 78 | 79 | # Snowpack dependency directory (https://snowpack.dev/) 80 | web_modules/ 81 | 82 | # TypeScript cache 83 | *.tsbuildinfo 84 | 85 | # Optional npm cache directory 86 | .npm 87 | 88 | # Optional eslint cache 89 | .eslintcache 90 | 91 | # Microbundle cache 92 | .rpt2_cache/ 93 | .rts2_cache_cjs/ 94 | .rts2_cache_es/ 95 | .rts2_cache_umd/ 96 | 97 | # Optional REPL history 98 | .node_repl_history 99 | 100 | # Output of 'npm pack' 101 | *.tgz 102 | 103 | # Yarn Integrity file 104 | .yarn-integrity 105 | 106 | # dotenv environment variables file 107 | .env 108 | .env.test 109 | .env.production 110 | 111 | # parcel-bundler cache (https://parceljs.org/) 112 | .cache 113 | .parcel-cache 114 | 115 | # Next.js build output 116 | .next 117 | out 118 | 119 | # Nuxt.js build / generate output 120 | .nuxt 121 | dist 122 | 123 | # Gatsby files 124 | .cache/ 125 | # Comment in the public line in if your project uses Gatsby and not Next.js 126 | # https://nextjs.org/blog/next-9-1#public-directory-support 127 | # public 128 | 129 | # vuepress build output 130 | .vuepress/dist 131 | 132 | # Serverless directories 133 | .serverless/ 134 | 135 | # FuseBox cache 136 | .fusebox/ 137 | 138 | # DynamoDB Local files 139 | .dynamodb/ 140 | 141 | # TernJS port file 142 | .tern-port 143 | 144 | # Stores VSCode versions used for testing VSCode extensions 145 | .vscode-test 146 | 147 | # yarn v2 148 | .yarn/cache 149 | .yarn/unplugged 150 | .yarn/build-state.yml 151 | .yarn/install-state.gz 152 | .pnp.* 153 | 154 | ### Node Patch ### 155 | # Serverless Webpack directories 156 | .webpack/ 157 | 158 | # Optional stylelint cache 159 | .stylelintcache 160 | 161 | # SvelteKit build / generate output 162 | .svelte-kit 163 | 164 | ### VisualStudioCode ### 165 | .vscode/* 166 | !.vscode/settings.json 167 | !.vscode/tasks.json 168 | !.vscode/launch.json 169 | !.vscode/extensions.json 170 | *.code-workspace 171 | 172 | # Local History for Visual Studio Code 173 | .history/ 174 | 175 | ### VisualStudioCode Patch ### 176 | # Ignore all local history of files 177 | .history 178 | .ionide 179 | 180 | # Support for Project snippet scope 181 | !.vscode/*.code-snippets 182 | 183 | ### Windows ### 184 | # Windows thumbnail cache files 185 | Thumbs.db 186 | Thumbs.db:encryptable 187 | ehthumbs.db 188 | ehthumbs_vista.db 189 | 190 | # Dump file 191 | *.stackdump 192 | 193 | # Folder config file 194 | [Dd]esktop.ini 195 | 196 | # Recycle Bin used on file shares 197 | $RECYCLE.BIN/ 198 | 199 | # Windows Installer files 200 | *.cab 201 | *.msi 202 | *.msix 203 | *.msm 204 | *.msp 205 | 206 | # Windows shortcuts 207 | *.lnk 208 | 209 | # End of https://www.toptal.com/developers/gitignore/api/node,macos,windows,visualstudiocode 210 | 211 | # Snyk cacke 212 | .dccache 213 | 214 | # What3words 215 | pacts/ 216 | coverage*/ -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | //registry.npmjs.org/:_authToken=${NPM_TOKEN} 2 | -------------------------------------------------------------------------------- /.nycrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@istanbuljs/nyc-config-typescript", 3 | "all": false, 4 | "check-coverage": true, 5 | "reporter": ["text", "html"] 6 | } -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | ...require('gts/.prettierrc.json'), 3 | bracketSpacing: true, 4 | jsxBracketSameLine: true, 5 | } 6 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | nodejs 18.20.2 -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # v4.0.4 2 | * Switched `isomorphic-unfetch` for `cross-fetch` for better browser support without polyfills. 3 | 4 | # v4.0.3 5 | * Added GridSection Typescript types 6 | 7 | # v4.0.2 8 | * Added custom headers injection to client configuration 9 | 10 | # v4.0.1 11 | * Removed `pkginfo` for support in environments without a `package.json` 12 | 13 | # v4.0.0 14 | * Updated Typescript transpilation `module` to `commonJS` and `target` to `es5` 15 | * Added feature for client initialisation to either `browser` or `node` (defaults to `node`) 16 | * Added support for custom transport 17 | -------------------------------------------------------------------------------- /LICENCE.md: -------------------------------------------------------------------------------- 1 | The MIT License 2 | =============== 3 | 4 | Copyright (c) 2014 Lokku ltd , Copyright (c) 2016 what3words Ltd 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining 7 | a copy of this software and associated documentation files (the 8 | 'Software'), to deal in the Software without restriction, including 9 | without limitation the rights to use, copy, modify, merge, publish, 10 | distribute, sublicense, and/or sell copies of the Software, and to 11 | permit persons to whom the Software is furnished to do so, subject to 12 | the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be 15 | included in all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 18 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 20 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 21 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 22 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 23 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![what3words](https://what3words.com/assets/images/w3w_square_red.png)](https://developer.what3words.com) 2 | 3 | # what3words JavaScript API Wrapper 4 | 5 | [![CircleCI](https://circleci.com/gh/what3words/w3w-node-wrapper.svg?style=svg)](https://github.com/what3words/w3w-node-wrapper) 6 | 7 | A JavaScript library to make requests to the [what3words REST API][api]. **Now with better support for use within both browser and node based environments!** See the what3words public API [documentation](apidocs) for more information about how to use our REST API. 8 | 9 | ## Table of Contents 10 | 11 | - [Overview](#overview) 12 | - [Install](#install) 13 | - [Usage](#usage) 14 | - [JavaScript](#javascript) 15 | - [Typescript](#typescript) 16 | - [Documentation](#documentation) 17 | - [What3wordsService](#what3words-service) 18 | - [Clients](#clients) 19 | - [Transport](#transport) 20 | - Examples 21 | - [Vanilla Autosuggest](docs/examples/vanilla/autosuggest/README.md) 22 | - [Vanilla Map](docs/examples/vanilla/map/README.md) 23 | - [Examples](#examples) 24 | - [CustomTransport](#custom-transport) 25 | - [Autosuggest](#autosuggest) 26 | - [Convert to Coordinates](#convert-to-coordinates) 27 | - [Convert to Three Word Address](#convert-to-three-word-address) 28 | - [Available Languages](#available-languages) 29 | - [Grid Section](#grid-section) 30 | 31 | ## Overview 32 | 33 | The what3words JavaScript wrapper gives you programmatic access to: 34 | 35 | - [Convert a 3 word address to coordinates](https://developer.what3words.com/public-api/docs#convert-to-coords) 36 | - [Convert coordinates to a 3 word address](https://developer.what3words.com/public-api/docs#convert-to-3wa) 37 | - [Autosuggest functionality which takes a slightly incorrect 3 word address, and suggests a list of valid 3 word addresses](https://developer.what3words.com/public-api/docs#autosuggest) 38 | - [Obtain a section of the 3m x 3m what3words grid for a bounding box.](https://developer.what3words.com/public-api/docs#grid-section) 39 | - [Determine the currently support 3 word address languages.](https://developer.what3words.com/public-api/docs#available-languages) 40 | 41 | ## Install 42 | 43 | [npm][]: 44 | 45 | ```sh 46 | npm install @what3words/api 47 | ``` 48 | 49 | [yarn][]: 50 | 51 | ```sh 52 | yarn add @what3words/api 53 | ``` 54 | 55 | If you wish to use the built-in transports you will also need to install the peer dependencies for them. For more information on the default transports read the section on [Transports](#transport). 56 | 57 | ## Usage 58 | 59 | ### JavaScript 60 | 61 | ```javascript 62 | const what3words, 63 | { fetchTransport } = require('@what3words/api'); 64 | 65 | const apiKey = ''; 66 | const config = { 67 | host: 'https://api.what3words.com', 68 | apiVersion: 'v3', 69 | }; 70 | const transport = fetchTransport(); // or you can import 'axiosTransport' instead 71 | const w3wService = what3words(apiKey, config, { transport }); 72 | 73 | // you can uncomment the following lines to set your api key and config after instantiation of the w3w service 74 | // w3wService.setApiKey(apiKey); 75 | // w3wService.setConfig(config); 76 | ``` 77 | 78 | ### Typescript 79 | 80 | ```typescript 81 | import what3words, { 82 | ApiVersion, 83 | Transport, 84 | What3wordsService, 85 | axiosTransport, 86 | } from '@what3words/api'; 87 | 88 | const apiKey = ''; 89 | const config: { 90 | host: string; 91 | apiVersion: ApiVersion; 92 | } = { 93 | host: 'https://api.what3words.com', 94 | apiVersion: ApiVersion.Version3, 95 | }; 96 | const transport: Transport = axiosTransport(); 97 | const w3wService: What3wordsService = what3words(apiKey, config, { transport }); 98 | 99 | // code continues... 100 | ``` 101 | 102 | ## Documentation 103 | 104 | ### what3words Service 105 | 106 | The `What3wordsService` provides a quick and easy way to instantiate the API clients that can be used to make requests against the what3words API. It also provides helper functions for setting API configuration, such as host and API version and your API key across the what3words API clients. 107 | 108 | ### Clients 109 | 110 | The what3words API clients in this library are used to validate request options, serialise and handle request/response and errors against an API endpoint. Each client extends the abstract `ApiClient` class. 111 | 112 | There is a specific client for each request and you can use them independently of the `What3wordsService`. This can be particularly useful if you want to extend the client behaviour, minimise your code or, in a more extreme example, use a custom transport to handle requests differently in each client. 113 | 114 | Every client accepts the following parameters: 115 | 116 | | Parameter | Datatype | Default value | 117 | | --------- | ----------------- | ---------------------------- | 118 | | apiKey | string | `''` | 119 | | config | config.host | `https://api.what3words.com` | 120 | | | config.apiVersion | `v3` | 121 | 122 | ### Transport 123 | 124 | The transport is a function responsible for executing the request against the API. Given a `ClientRequest` the transport should return a promise that resolves to `TransportResponse`. 125 | 126 | A `ClientRequest` consists of the following properties: 127 | 128 | | Property | Datatype | 129 | | ------------ | ------------------------------------ | 130 | | **host**\* | `string` | 131 | | **url**\* | `string` | 132 | | **method**\* | `get` or `post` | 133 | | **query** | `object` | 134 | | **headers** | `object` | 135 | | **body** | `object` | 136 | | **format**\* | `json` or `geojson`. Default: `json` | 137 | 138 | A `TransportResponse` consists of the following properties: 139 | 140 | | Property | Datatype | 141 | | ---------------- | -------- | 142 | | **status**\* | `number` | 143 | | **statusText**\* | `string` | 144 | | **body**\* | `any` | 145 | | **headers** | `object` | 146 | 147 | There are two built-in transports available with this library that you can use; either [cross-fetch][] or [axios][]. By specifying which transport you would like to use on initialisation of the `What3wordsService` or a client, if you wish to instantiate a client for yourself. 148 | 149 | #### Built-ins 150 | 151 | There are two built-in transports available: 152 | 153 | - [Cross-fetch][cross-fetch] 154 | - [Axios][axios] 155 | 156 | In order to use either of these you will need install the peer dependency. By default [cross-fetch][cross-fetch] is assumed by the `What3wordsService` or any instantiated client where no override is provided. 157 | 158 | [npm][]: 159 | 160 | ```sh 161 | npm install cross-fetch 162 | ``` 163 | 164 | or 165 | 166 | ```sh 167 | npm install axios 168 | ``` 169 | 170 | [yarn][]: 171 | 172 | ```sh 173 | yarn add cross-fetch 174 | ``` 175 | 176 | or 177 | 178 | ```sh 179 | yarn add axios 180 | ``` 181 | 182 | #### Custom 183 | 184 | You can provide your own custom transport, if you wish to use another library for handling requests, which might be useful if you have other integrations or you are already using a http library elsewhere. 185 | 186 | In order to do so you need to define your own `Transport` and pass it into the `What3wordsService` or client to use it. 187 | 188 | The custom `Transport` you create should be a function that accepts a `ClientRequest` as an argument and returns a promise that resolves to a `TransportResponse`. 189 | 190 | ## Examples 191 | 192 | ### Custom Transport 193 | 194 | ```typescript 195 | import what3words, { ClientRequest, TransportResponse } from '@what3words/api'; 196 | import superagent from 'superagent'; 197 | 198 | const API_KEY = ''; 199 | const config = {}; // This will ensure we do not override the defaults 200 | 201 | function customTransport( 202 | request: ClientRequest 203 | ): Promise> { 204 | const { 205 | method, 206 | host, 207 | url, 208 | query = {}, 209 | headers = {}, 210 | body = {}, 211 | format, 212 | } = request; 213 | return new Promise(resolve => 214 | superagent[method](`${host}${url}`) 215 | .query({ ...query, format }) 216 | .send(body || {}) 217 | .set(headers) 218 | .end((err, res) => { 219 | if (err || !res) 220 | return resolve({ 221 | status: err.status || 500, 222 | statusText: err.response.text || 'Internal Server Error', 223 | headers: err.headers || {}, 224 | body: err.response.text || null, 225 | }); 226 | const response: TransportResponse = { 227 | status: res.status, 228 | statusText: res.text, 229 | headers: res.headers, 230 | body: res.body, 231 | }; 232 | resolve(response); 233 | }) 234 | ); 235 | } 236 | 237 | const service = what3words(API_KEY, config, { transport: customTransport }); 238 | service 239 | .availableLanguages() 240 | .then(({ languages }) => console.log('Available languages', languages)); 241 | ``` 242 | 243 | ### Autosuggest 244 | 245 | ```typescript 246 | import { 247 | AutosuggestClient, 248 | AutosuggestOptions, 249 | AutosuggestResponse, 250 | } from '@what3words/api'; 251 | 252 | const API_KEY = ''; 253 | const client: AutosuggestClient = AutosuggestClient.init(API_KEY); 254 | const options: AutosuggestOptions = { 255 | input: 'filled.count.s', 256 | }; 257 | client 258 | .run(options) 259 | .then((res: AutosuggestResponse) => 260 | console.log(`suggestions for "${options.input}"`, res) 261 | ); 262 | ``` 263 | 264 | ### Convert to Coordinates 265 | 266 | ```typescript 267 | import { 268 | ConvertToCoordinatesClient, 269 | ConvertToCoordinatesOptions, 270 | FeatureCollectionResponse, 271 | LocationGeoJsonResponse, 272 | LocationJsonResponse, 273 | } from '@what3words/api'; 274 | 275 | const API_KEY = ''; 276 | const client: ConvertToCoordinatesClient = 277 | ConvertToCoordinatesClient.init(API_KEY); 278 | const options: ConvertToCoordinatesOptions = { words: 'filled.count.soap' }; 279 | 280 | // If you want to retrieve the JSON response from our API 281 | client 282 | .run({ ...options, format: 'json' }) // { format: 'json' } is the default response 283 | .then((res: LocationJsonResponse) => 284 | console.log('Convert to coordinates', res) 285 | ); 286 | 287 | // If you want to retrieve the GeoJsonResponse from our API 288 | client 289 | .run({ ...options, format: 'geojson' }) 290 | .then((res: FeatureCollectionResponse) => 291 | console.log('Convert to coordinates', res) 292 | ); 293 | ``` 294 | 295 | ### Convert to Three Word Address 296 | 297 | ```typescript 298 | import { 299 | ConvertTo3waClient, 300 | ConvertTo3waOptions, 301 | FeatureCollectionResponse, 302 | LocationGeoJsonResponse, 303 | LocationJsonResponse, 304 | } from '@what3words/api'; 305 | 306 | const API_KEY = ''; 307 | const client: ConvertTo3waClient = ConvertTo3waClient.init(API_KEY); 308 | const options: ConvertTo3waOptions = { 309 | coordinates: { lat: 51.520847, lng: -0.195521 }, 310 | }; 311 | 312 | // If you want to retrieve the JSON response from our API 313 | client 314 | .run({ ...options, format: 'json' }) // { format: 'json' } is the default response 315 | .then((res: LocationJsonResponse) => console.log('Convert to 3wa', res)); 316 | 317 | // If you want to retrieve the GeoJsonResponse from our API 318 | client 319 | .run({ ...options, format: 'geojson' }) 320 | .then((res: FeatureCollectionResponse) => 321 | console.log('Convert to 3wa', res) 322 | ); 323 | ``` 324 | 325 | ### Available Languages 326 | 327 | ```typescript 328 | import { 329 | AvailableLanguagesClient, 330 | AvailableLanguagesResponse, 331 | } from '@what3words/api'; 332 | 333 | const API_KEY = ''; 334 | const client: AvailableLanguagesClient = AvailableLanguagesClient.init(API_KEY); 335 | client 336 | .run() 337 | .then((res: AvailableLanguagesResponse) => 338 | console.log('Available Languages', res) 339 | ); 340 | ``` 341 | 342 | ### Grid Section 343 | 344 | ```typescript 345 | import { 346 | GridSectionClient, 347 | GridSectionOptions, 348 | FeatureCollectionResponse, 349 | GridSectionGeoJsonResponse, 350 | GridSectionJsonResponse, 351 | } from '../src'; 352 | 353 | const API_KEY = ''; 354 | const client: GridSectionClient = GridSectionClient.init(API_KEY); 355 | const options: GridSectionOptions = { 356 | boundingBox: { 357 | southwest: { lat: 52.208867, lng: 0.11754 }, 358 | northeast: { lat: 52.207988, lng: 0.116126 }, 359 | }, 360 | }; 361 | 362 | // If you want to retrieve the JSON response from our API 363 | client 364 | .run({ ...options, format: 'json' }) // { format: 'json' } is the default response 365 | .then((res: GridSectionJsonResponse) => console.log('Grid Section', res)); 366 | 367 | // If you want to retrieve the JSON response from our API 368 | client 369 | .run({ ...options, format: 'geojson' }) // { format: 'json' } is the default response 370 | .then((res: FeatureCollectionResponse) => 371 | console.log('Grid Section', res) 372 | ); 373 | ``` 374 | 375 | > **The requested box must not exceed 4km from corner to corner, or a BadBoundingBoxTooBig error will be returned. Latitudes must be >= -90 and <= 90, but longitudes are allowed to wrap around 180. To specify a bounding-box that crosses the anti-meridian, use longitude greater than 180.** 376 | 377 | ### Input validation 378 | 379 | ```typescript 380 | import { 381 | GridSectionClient, 382 | GridSectionOptions, 383 | FeatureCollectionResponse, 384 | GridSectionGeoJsonResponse, 385 | GridSectionJsonResponse, 386 | } from '../src'; 387 | 388 | const API_KEY = ''; 389 | const client: GridSectionClient = GridSectionClient.init(API_KEY); 390 | const options: GridSectionOptions = { 391 | boundingBox: { 392 | southwest: { lat: 52.208867, lng: 0.11754 }, 393 | northeast: { lat: 52.207988, lng: 0.116126 }, 394 | }, 395 | }; 396 | 397 | // Search a string for any character sequences that could be three word addresses 398 | client.findPossible3wa('filled.count.soap'); // returns ['filled.count.soap'] 399 | client.findPossible3wa( 400 | 'this string contains a three word address substring: filled.count.soap' 401 | ); // returns ['filled.count.soap'] 402 | client.findPossible3wa('filled.count'); // returns [] 403 | 404 | // Search a string for any character sequences that could be three word addresses 405 | client.isPossible3wa('filled.count.soap'); // returns true 406 | client.isPossible3wa( 407 | 'this string contains a three word address substring: filled.count.soap' 408 | ); // returns false 409 | client.isPossible3wa('filled.count'); // returns false 410 | 411 | // Search a string for any character sequences that could be three word addresses 412 | client.isValid3wa('filled.count.soap'); // returns Promise 413 | client.isValid3wa( 414 | 'this string contains a three word address substring: filled.count.soap' 415 | ); // returns Promise 416 | client.isValid3wa('filled.count.negative'); // returns Promise 417 | ``` 418 | 419 | ## 420 | 421 | [npm]: https://www.npmjs.com/ 422 | [yarn]: https://yarnpkg.com/ 423 | [api]: https://developer.what3words.com/public-api/ 424 | [apidocs]: https://developer.what3words.com/public-api/docs 425 | [cross-fetch]: https://www.npmjs.com/package/cross-fetch 426 | [axios]: https://www.npmjs.com/package/axios 427 | -------------------------------------------------------------------------------- /docs/examples/vanilla/autosuggest/.env.example: -------------------------------------------------------------------------------- 1 | W3W_API_KEY="" 2 | W3W_API_ENDPOINT=https://api.what3words.com -------------------------------------------------------------------------------- /docs/examples/vanilla/autosuggest/README.md: -------------------------------------------------------------------------------- 1 | # Vanilla Autosuggest With Wrapper 2 | 3 | This project presents an example of how to make use of the `@what3words/api` js wrapper in a browser environment. 4 | 5 | ## Usage 6 | 7 | Edit the source file variable `W3W_API_KEY` located in `src/what3words.js` to a valid key from your [accounts dashboard](accounts.what3words.com/overview). Follow the commands below as needed to run or build the project. 8 | 9 | 1. To install the project dependencies, run the command `npm install`. 10 | 2. To build the project, run `npm run build`. 11 | 3. To serve the project using esbuild, run `npm run dev`. 12 | 13 | ## Development 14 | 15 | [esbuild](https://esbuild.github.io/) was used to build the project with a dependency on [esbuild-plugin-polyfill-node](https://www.npmjs.com/package/esbuild-plugin-polyfill-node) to shim node-specific features to successfully compile for browsers. You may choose to bundle your project with any appropriate tool of your choice, the only requirement as of `v5.0.1` is to polyfill the node `os` built-in. 16 | -------------------------------------------------------------------------------- /docs/examples/vanilla/autosuggest/esbuild/build.mjs: -------------------------------------------------------------------------------- 1 | import * as esbuild from 'esbuild'; 2 | import { buildOptions } from './common.mjs'; 3 | 4 | await esbuild 5 | .build(buildOptions) 6 | .then(() => console.log('⚡ Done')) 7 | .catch(() => process.exit(1)); 8 | -------------------------------------------------------------------------------- /docs/examples/vanilla/autosuggest/esbuild/common.mjs: -------------------------------------------------------------------------------- 1 | import { polyfillNode } from 'esbuild-plugin-polyfill-node'; 2 | 3 | const define = Object.fromEntries( 4 | Object.entries(process.env) 5 | .filter(([key]) => key.startsWith('W3W_')) 6 | .map(([key, value]) => [`process.env.${key}`, JSON.stringify(value)]) 7 | ); 8 | 9 | export const buildOptions = { 10 | entryPoints: ['src/what3words.js'], 11 | bundle: true, 12 | outfile: 'public/dist/bundle.js', 13 | plugins: [polyfillNode()], 14 | define, 15 | }; 16 | -------------------------------------------------------------------------------- /docs/examples/vanilla/autosuggest/esbuild/serve.mjs: -------------------------------------------------------------------------------- 1 | import * as esbuild from 'esbuild'; 2 | import { buildOptions } from './common.mjs'; 3 | 4 | const PORT = 3000; 5 | 6 | const ctx = await esbuild.context({ 7 | ...buildOptions, 8 | banner: { 9 | js: `new EventSource('/esbuild').addEventListener('change', () => location.reload());`, 10 | }, 11 | sourcemap: true, 12 | }); 13 | 14 | await ctx.watch(); 15 | 16 | const { host, port } = await ctx.serve({ 17 | port: PORT, 18 | servedir: 'public', 19 | }); 20 | 21 | console.log(`⚡ Serving app at http://${host}:${port}`); 22 | -------------------------------------------------------------------------------- /docs/examples/vanilla/autosuggest/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "autosuggest-using-sdk", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "build": "node esbuild/build.mjs", 9 | "dev": "rm -rf public/dist/** && node esbuild/serve.mjs" 10 | }, 11 | "author": "", 12 | "license": "ISC", 13 | "dependencies": { 14 | "@what3words/api": "file:../../../.." 15 | }, 16 | "devDependencies": { 17 | "esbuild": "^0.19.11", 18 | "esbuild-plugin-polyfill-node": "^0.3.0" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /docs/examples/vanilla/autosuggest/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Autosuggest - SDK 9 | 10 | 11 |
12 | 19 |
20 |
21 | 22 | 23 | -------------------------------------------------------------------------------- /docs/examples/vanilla/autosuggest/public/styles.css: -------------------------------------------------------------------------------- 1 | .component { 2 | display: flex; 3 | flex-direction: column; 4 | } 5 | 6 | .input { 7 | display: flex; 8 | flex-direction: column; 9 | padding: 10px 4px 10px 4px; 10 | } 11 | -------------------------------------------------------------------------------- /docs/examples/vanilla/autosuggest/src/what3words.js: -------------------------------------------------------------------------------- 1 | import what3words, { 2 | axiosTransport, 3 | ApiVersion, 4 | W3W_REGEX, 5 | } from '@what3words/api'; 6 | 7 | // SETUP 8 | const COMPONENT_SELECTOR = 'div#w3w-autosuggest'; 9 | const INPUT_SELECTOR = 'input#autosuggest'; 10 | const SUGGESTIONS_SELECTOR = 'div#suggestions'; 11 | const W3W_API_KEY = process.env.W3W_API_KEY; 12 | const W3W_API_ENDPOINT = process.env.W3W_API_ENDPOINT; 13 | 14 | // DECLARATIONS 15 | let component, input, selected; 16 | 17 | /** 18 | * Retrieves suggestions from the what3words API given a search string 19 | */ 20 | async function getSuggestions(search) { 21 | /** 22 | * @see {@link https://developer.what3words.com/tutorial/nodejs#usage|Node/Javascript Wrapper} 23 | */ 24 | const { suggestions } = await window.what3words 25 | .autosuggest({ 26 | input: search, 27 | }) 28 | .catch(err => { 29 | // you should do something with this 30 | console.error(err); 31 | 32 | // Return no suggestions 33 | return { suggestions: [] }; 34 | }); 35 | 36 | return suggestions; 37 | } 38 | 39 | /** 40 | * Clears the suggestions and removes the elements from the DOM. 41 | * TODO - this should probably tear down and remove event listeners that have been attached. 42 | */ 43 | function clearSuggestions() { 44 | const div = document.createElement('div'); 45 | document.querySelector(SUGGESTIONS_SELECTOR).remove(); 46 | div.id = 'suggestions'; 47 | component.append(div); 48 | } 49 | 50 | /** 51 | * Handler for when a suggestion is selected 52 | */ 53 | async function selectedSuggestion(suggestion) { 54 | // Inform our API which suggestion was selected. 55 | await window.what3words.autosuggestSelection(suggestion); 56 | // Update input with selected suggestion 57 | input.value = `///${suggestion.words}`; 58 | selected = suggestion; 59 | console.log(`Three word address selected: ${JSON.stringify(selected)}`); 60 | // Clear suggestions as a selection has been made 61 | clearSuggestions(); 62 | } 63 | 64 | /** 65 | * Handler for when suggestions are returned from the API 66 | */ 67 | function displaySuggestions(suggestions = []) { 68 | const parent = document.querySelector(SUGGESTIONS_SELECTOR); 69 | const div = document.createElement('div'); 70 | 71 | for (const suggestion of suggestions) { 72 | const s = document.createElement('div'); 73 | const address = document.createElement('div'); 74 | const nearest = document.createElement('div'); 75 | const hr = document.createElement('hr'); 76 | address.innerHTML = suggestion.words; 77 | nearest.innerHTML = `${suggestion.nearestPlace}`; 78 | s.append(address, nearest, hr); 79 | 80 | // Add an event listener when the suggestion is selected 81 | s.addEventListener('click', async () => { 82 | await selectedSuggestion(suggestion); 83 | }); 84 | 85 | // Append elements to DOM 86 | div.append(s); 87 | } 88 | if (parent) parent.append(div); 89 | } 90 | 91 | /** 92 | * Use an IIFE to load the app 93 | */ 94 | (function init() { 95 | if (!W3W_API_KEY) throw new Error('Invalid or missing what3words API key.'); 96 | 97 | input = document.querySelector(INPUT_SELECTOR); 98 | component = document.querySelector(COMPONENT_SELECTOR); 99 | 100 | /** 101 | * what3words is now available! 102 | */ 103 | window.what3words = what3words( 104 | W3W_API_KEY, 105 | { apiVersion: ApiVersion.Version3, host: W3W_API_ENDPOINT }, 106 | { transport: axiosTransport() } 107 | ); 108 | let timeout = null; 109 | 110 | // Alternatively, you can set the API key like this: 111 | // window.what3words.setApiKey(W3W_API_KEY); 112 | 113 | // Attach event listeners to target input element 114 | input.addEventListener('focus', ({ target }) => { 115 | const { value } = target; 116 | if (!value) { 117 | target.value = '///'; 118 | const cursorPosition = target.value.length; 119 | setTimeout( 120 | () => target.setSelectionRange(cursorPosition, cursorPosition), 121 | 10 122 | ); 123 | } 124 | }); 125 | input.addEventListener('input', ({ target }) => { 126 | const { value } = target; 127 | // Check if the search string is a 3wa-like string - if not do nothing. 128 | if (!W3W_REGEX.test(value)) return; 129 | if (timeout) clearTimeout(timeout); 130 | 131 | // Throttle the requests by having a timeout on last input before requesting suggestions 132 | timeout = setTimeout(async () => { 133 | const suggestions = await getSuggestions(value); 134 | displaySuggestions(suggestions); 135 | }, 300); 136 | }); 137 | input.addEventListener('blur', event => { 138 | const { 139 | target: { value }, 140 | } = event; 141 | event.stopPropagation(); 142 | setTimeout(() => { 143 | if (!W3W_REGEX.test(value)) { 144 | input.value = ''; 145 | } 146 | clearSuggestions(); 147 | }, 300); 148 | }); 149 | })(); 150 | -------------------------------------------------------------------------------- /docs/examples/vanilla/map/.env.example: -------------------------------------------------------------------------------- 1 | W3W_API_KEY="" 2 | W3W_API_ENDPOINT=https://api.what3words.com 3 | W3W_MAP_API_KEY="" -------------------------------------------------------------------------------- /docs/examples/vanilla/map/README.md: -------------------------------------------------------------------------------- 1 | # Vanilla Map With Wrapper 2 | 3 | This project presents an example of how to make use of the `@what3words/api` js wrapper in a browser environment. 4 | 5 | ## Usage 6 | 7 | Edit the source file variables `W3W_API_KEY` and `GOOGLE_API_KEY` located in `src/what3words.js` to a valid key from your respective dashboards ([what3words](accounts.what3words.com/overview), [google](https://console.cloud.google.com/project/_/google/maps-apis/credentials)). Follow the commands below as needed to run or build the project. 8 | 9 | 1. To install the project dependencies, run the command `npm install`. 10 | 2. To build the project, run `npm run build`. 11 | 3. To serve the project using esbuild, run `npm run dev`. 12 | 13 | ## Development 14 | 15 | [esbuild](https://esbuild.github.io/) was used to build the project with a dependency on [esbuild-plugin-polyfill-node](https://www.npmjs.com/package/esbuild-plugin-polyfill-node) to shim node-specific features to successfully compile for browsers. You may choose to bundle your project with any appropriate tool of your choice, the only requirement as of `v5.0.1` is to polyfill the node `os` built-in. 16 | -------------------------------------------------------------------------------- /docs/examples/vanilla/map/esbuild/build.mjs: -------------------------------------------------------------------------------- 1 | import * as esbuild from 'esbuild'; 2 | import { buildOptions } from './common.mjs'; 3 | 4 | await esbuild 5 | .build(buildOptions) 6 | .then(() => console.log('⚡ Done')) 7 | .catch(() => process.exit(1)); 8 | -------------------------------------------------------------------------------- /docs/examples/vanilla/map/esbuild/common.mjs: -------------------------------------------------------------------------------- 1 | import { polyfillNode } from 'esbuild-plugin-polyfill-node'; 2 | 3 | const define = Object.fromEntries( 4 | Object.entries(process.env) 5 | .filter(([key]) => key.startsWith('W3W_')) 6 | .map(([key, value]) => [`process.env.${key}`, JSON.stringify(value)]) 7 | ); 8 | 9 | export const buildOptions = { 10 | entryPoints: ['src/what3words.js'], 11 | bundle: true, 12 | outfile: 'public/dist/bundle.js', 13 | plugins: [polyfillNode()], 14 | define, 15 | }; 16 | -------------------------------------------------------------------------------- /docs/examples/vanilla/map/esbuild/serve.mjs: -------------------------------------------------------------------------------- 1 | import * as esbuild from 'esbuild'; 2 | import { buildOptions } from './common.mjs'; 3 | 4 | const PORT = 3000; 5 | 6 | const ctx = await esbuild.context({ 7 | ...buildOptions, 8 | banner: { 9 | js: `new EventSource('/esbuild').addEventListener('change', () => location.reload());`, 10 | }, 11 | sourcemap: true, 12 | }); 13 | 14 | await ctx.watch(); 15 | 16 | const { host, port } = await ctx.serve({ 17 | port: PORT, 18 | servedir: 'public', 19 | }); 20 | 21 | console.log(`⚡ Serving app at http://${host}:${port}`); 22 | -------------------------------------------------------------------------------- /docs/examples/vanilla/map/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "map-using-sdk", 3 | "version": "1.0.0", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "map-using-sdk", 9 | "version": "1.0.0", 10 | "license": "ISC", 11 | "dependencies": { 12 | "@what3words/api": "file:../../../.." 13 | }, 14 | "devDependencies": { 15 | "esbuild": "^0.19.11", 16 | "esbuild-plugin-polyfill-node": "^0.3.0" 17 | } 18 | }, 19 | "../../../..": { 20 | "version": "5.4.0", 21 | "devDependencies": { 22 | "@istanbuljs/nyc-config-typescript": "^1.0.1", 23 | "@testing-library/react": "^12.1.3", 24 | "@types/chance": "^1.1.3", 25 | "@types/jsdom": "^16.2.14", 26 | "@types/mocha": "^9.0.0", 27 | "@types/node": "^18.19.31", 28 | "@types/react": "^17.0.39", 29 | "@types/sinon": "^10.0.6", 30 | "@types/superagent": "^4.1.16", 31 | "@typescript-eslint/eslint-plugin": "^5.3.1", 32 | "@vitest/coverage-v8": "^1.5.0", 33 | "chance": "^1.1.8", 34 | "eslint-plugin-prettier": "^4.0.0", 35 | "gts": "^3.1.0", 36 | "jsdom": "^19.0.0", 37 | "nock": "^13.2.0", 38 | "react": "^17.0.2", 39 | "react-dom": "^17.0.2", 40 | "superagent": "^8.0.8", 41 | "ts-node": "^10.4.0", 42 | "typescript": "^5.0.0", 43 | "vite-plugin-commonjs": "^0.10.1", 44 | "vitest": "^1.5.0" 45 | }, 46 | "engines": { 47 | "node": ">=18" 48 | }, 49 | "peerDependencies": { 50 | "axios": ">=1", 51 | "cross-fetch": ">=3" 52 | }, 53 | "peerDependenciesMeta": { 54 | "axios": { 55 | "optional": true 56 | }, 57 | "cross-fetch": { 58 | "optional": true 59 | } 60 | } 61 | }, 62 | "node_modules/@esbuild/aix-ppc64": { 63 | "version": "0.19.11", 64 | "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.11.tgz", 65 | "integrity": "sha512-FnzU0LyE3ySQk7UntJO4+qIiQgI7KoODnZg5xzXIrFJlKd2P2gwHsHY4927xj9y5PJmJSzULiUCWmv7iWnNa7g==", 66 | "cpu": [ 67 | "ppc64" 68 | ], 69 | "dev": true, 70 | "optional": true, 71 | "os": [ 72 | "aix" 73 | ], 74 | "engines": { 75 | "node": ">=12" 76 | } 77 | }, 78 | "node_modules/@esbuild/android-arm": { 79 | "version": "0.19.11", 80 | "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.11.tgz", 81 | "integrity": "sha512-5OVapq0ClabvKvQ58Bws8+wkLCV+Rxg7tUVbo9xu034Nm536QTII4YzhaFriQ7rMrorfnFKUsArD2lqKbFY4vw==", 82 | "cpu": [ 83 | "arm" 84 | ], 85 | "dev": true, 86 | "optional": true, 87 | "os": [ 88 | "android" 89 | ], 90 | "engines": { 91 | "node": ">=12" 92 | } 93 | }, 94 | "node_modules/@esbuild/android-arm64": { 95 | "version": "0.19.11", 96 | "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.11.tgz", 97 | "integrity": "sha512-aiu7K/5JnLj//KOnOfEZ0D90obUkRzDMyqd/wNAUQ34m4YUPVhRZpnqKV9uqDGxT7cToSDnIHsGooyIczu9T+Q==", 98 | "cpu": [ 99 | "arm64" 100 | ], 101 | "dev": true, 102 | "optional": true, 103 | "os": [ 104 | "android" 105 | ], 106 | "engines": { 107 | "node": ">=12" 108 | } 109 | }, 110 | "node_modules/@esbuild/android-x64": { 111 | "version": "0.19.11", 112 | "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.11.tgz", 113 | "integrity": "sha512-eccxjlfGw43WYoY9QgB82SgGgDbibcqyDTlk3l3C0jOVHKxrjdc9CTwDUQd0vkvYg5um0OH+GpxYvp39r+IPOg==", 114 | "cpu": [ 115 | "x64" 116 | ], 117 | "dev": true, 118 | "optional": true, 119 | "os": [ 120 | "android" 121 | ], 122 | "engines": { 123 | "node": ">=12" 124 | } 125 | }, 126 | "node_modules/@esbuild/darwin-arm64": { 127 | "version": "0.19.11", 128 | "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.11.tgz", 129 | "integrity": "sha512-ETp87DRWuSt9KdDVkqSoKoLFHYTrkyz2+65fj9nfXsaV3bMhTCjtQfw3y+um88vGRKRiF7erPrh/ZuIdLUIVxQ==", 130 | "cpu": [ 131 | "arm64" 132 | ], 133 | "dev": true, 134 | "optional": true, 135 | "os": [ 136 | "darwin" 137 | ], 138 | "engines": { 139 | "node": ">=12" 140 | } 141 | }, 142 | "node_modules/@esbuild/darwin-x64": { 143 | "version": "0.19.11", 144 | "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.11.tgz", 145 | "integrity": "sha512-fkFUiS6IUK9WYUO/+22omwetaSNl5/A8giXvQlcinLIjVkxwTLSktbF5f/kJMftM2MJp9+fXqZ5ezS7+SALp4g==", 146 | "cpu": [ 147 | "x64" 148 | ], 149 | "dev": true, 150 | "optional": true, 151 | "os": [ 152 | "darwin" 153 | ], 154 | "engines": { 155 | "node": ">=12" 156 | } 157 | }, 158 | "node_modules/@esbuild/freebsd-arm64": { 159 | "version": "0.19.11", 160 | "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.11.tgz", 161 | "integrity": "sha512-lhoSp5K6bxKRNdXUtHoNc5HhbXVCS8V0iZmDvyWvYq9S5WSfTIHU2UGjcGt7UeS6iEYp9eeymIl5mJBn0yiuxA==", 162 | "cpu": [ 163 | "arm64" 164 | ], 165 | "dev": true, 166 | "optional": true, 167 | "os": [ 168 | "freebsd" 169 | ], 170 | "engines": { 171 | "node": ">=12" 172 | } 173 | }, 174 | "node_modules/@esbuild/freebsd-x64": { 175 | "version": "0.19.11", 176 | "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.11.tgz", 177 | "integrity": "sha512-JkUqn44AffGXitVI6/AbQdoYAq0TEullFdqcMY/PCUZ36xJ9ZJRtQabzMA+Vi7r78+25ZIBosLTOKnUXBSi1Kw==", 178 | "cpu": [ 179 | "x64" 180 | ], 181 | "dev": true, 182 | "optional": true, 183 | "os": [ 184 | "freebsd" 185 | ], 186 | "engines": { 187 | "node": ">=12" 188 | } 189 | }, 190 | "node_modules/@esbuild/linux-arm": { 191 | "version": "0.19.11", 192 | "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.11.tgz", 193 | "integrity": "sha512-3CRkr9+vCV2XJbjwgzjPtO8T0SZUmRZla+UL1jw+XqHZPkPgZiyWvbDvl9rqAN8Zl7qJF0O/9ycMtjU67HN9/Q==", 194 | "cpu": [ 195 | "arm" 196 | ], 197 | "dev": true, 198 | "optional": true, 199 | "os": [ 200 | "linux" 201 | ], 202 | "engines": { 203 | "node": ">=12" 204 | } 205 | }, 206 | "node_modules/@esbuild/linux-arm64": { 207 | "version": "0.19.11", 208 | "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.11.tgz", 209 | "integrity": "sha512-LneLg3ypEeveBSMuoa0kwMpCGmpu8XQUh+mL8XXwoYZ6Be2qBnVtcDI5azSvh7vioMDhoJFZzp9GWp9IWpYoUg==", 210 | "cpu": [ 211 | "arm64" 212 | ], 213 | "dev": true, 214 | "optional": true, 215 | "os": [ 216 | "linux" 217 | ], 218 | "engines": { 219 | "node": ">=12" 220 | } 221 | }, 222 | "node_modules/@esbuild/linux-ia32": { 223 | "version": "0.19.11", 224 | "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.11.tgz", 225 | "integrity": "sha512-caHy++CsD8Bgq2V5CodbJjFPEiDPq8JJmBdeyZ8GWVQMjRD0sU548nNdwPNvKjVpamYYVL40AORekgfIubwHoA==", 226 | "cpu": [ 227 | "ia32" 228 | ], 229 | "dev": true, 230 | "optional": true, 231 | "os": [ 232 | "linux" 233 | ], 234 | "engines": { 235 | "node": ">=12" 236 | } 237 | }, 238 | "node_modules/@esbuild/linux-loong64": { 239 | "version": "0.19.11", 240 | "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.11.tgz", 241 | "integrity": "sha512-ppZSSLVpPrwHccvC6nQVZaSHlFsvCQyjnvirnVjbKSHuE5N24Yl8F3UwYUUR1UEPaFObGD2tSvVKbvR+uT1Nrg==", 242 | "cpu": [ 243 | "loong64" 244 | ], 245 | "dev": true, 246 | "optional": true, 247 | "os": [ 248 | "linux" 249 | ], 250 | "engines": { 251 | "node": ">=12" 252 | } 253 | }, 254 | "node_modules/@esbuild/linux-mips64el": { 255 | "version": "0.19.11", 256 | "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.11.tgz", 257 | "integrity": "sha512-B5x9j0OgjG+v1dF2DkH34lr+7Gmv0kzX6/V0afF41FkPMMqaQ77pH7CrhWeR22aEeHKaeZVtZ6yFwlxOKPVFyg==", 258 | "cpu": [ 259 | "mips64el" 260 | ], 261 | "dev": true, 262 | "optional": true, 263 | "os": [ 264 | "linux" 265 | ], 266 | "engines": { 267 | "node": ">=12" 268 | } 269 | }, 270 | "node_modules/@esbuild/linux-ppc64": { 271 | "version": "0.19.11", 272 | "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.11.tgz", 273 | "integrity": "sha512-MHrZYLeCG8vXblMetWyttkdVRjQlQUb/oMgBNurVEnhj4YWOr4G5lmBfZjHYQHHN0g6yDmCAQRR8MUHldvvRDA==", 274 | "cpu": [ 275 | "ppc64" 276 | ], 277 | "dev": true, 278 | "optional": true, 279 | "os": [ 280 | "linux" 281 | ], 282 | "engines": { 283 | "node": ">=12" 284 | } 285 | }, 286 | "node_modules/@esbuild/linux-riscv64": { 287 | "version": "0.19.11", 288 | "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.11.tgz", 289 | "integrity": "sha512-f3DY++t94uVg141dozDu4CCUkYW+09rWtaWfnb3bqe4w5NqmZd6nPVBm+qbz7WaHZCoqXqHz5p6CM6qv3qnSSQ==", 290 | "cpu": [ 291 | "riscv64" 292 | ], 293 | "dev": true, 294 | "optional": true, 295 | "os": [ 296 | "linux" 297 | ], 298 | "engines": { 299 | "node": ">=12" 300 | } 301 | }, 302 | "node_modules/@esbuild/linux-s390x": { 303 | "version": "0.19.11", 304 | "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.11.tgz", 305 | "integrity": "sha512-A5xdUoyWJHMMlcSMcPGVLzYzpcY8QP1RtYzX5/bS4dvjBGVxdhuiYyFwp7z74ocV7WDc0n1harxmpq2ePOjI0Q==", 306 | "cpu": [ 307 | "s390x" 308 | ], 309 | "dev": true, 310 | "optional": true, 311 | "os": [ 312 | "linux" 313 | ], 314 | "engines": { 315 | "node": ">=12" 316 | } 317 | }, 318 | "node_modules/@esbuild/linux-x64": { 319 | "version": "0.19.11", 320 | "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.11.tgz", 321 | "integrity": "sha512-grbyMlVCvJSfxFQUndw5mCtWs5LO1gUlwP4CDi4iJBbVpZcqLVT29FxgGuBJGSzyOxotFG4LoO5X+M1350zmPA==", 322 | "cpu": [ 323 | "x64" 324 | ], 325 | "dev": true, 326 | "optional": true, 327 | "os": [ 328 | "linux" 329 | ], 330 | "engines": { 331 | "node": ">=12" 332 | } 333 | }, 334 | "node_modules/@esbuild/netbsd-x64": { 335 | "version": "0.19.11", 336 | "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.11.tgz", 337 | "integrity": "sha512-13jvrQZJc3P230OhU8xgwUnDeuC/9egsjTkXN49b3GcS5BKvJqZn86aGM8W9pd14Kd+u7HuFBMVtrNGhh6fHEQ==", 338 | "cpu": [ 339 | "x64" 340 | ], 341 | "dev": true, 342 | "optional": true, 343 | "os": [ 344 | "netbsd" 345 | ], 346 | "engines": { 347 | "node": ">=12" 348 | } 349 | }, 350 | "node_modules/@esbuild/openbsd-x64": { 351 | "version": "0.19.11", 352 | "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.11.tgz", 353 | "integrity": "sha512-ysyOGZuTp6SNKPE11INDUeFVVQFrhcNDVUgSQVDzqsqX38DjhPEPATpid04LCoUr2WXhQTEZ8ct/EgJCUDpyNw==", 354 | "cpu": [ 355 | "x64" 356 | ], 357 | "dev": true, 358 | "optional": true, 359 | "os": [ 360 | "openbsd" 361 | ], 362 | "engines": { 363 | "node": ">=12" 364 | } 365 | }, 366 | "node_modules/@esbuild/sunos-x64": { 367 | "version": "0.19.11", 368 | "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.11.tgz", 369 | "integrity": "sha512-Hf+Sad9nVwvtxy4DXCZQqLpgmRTQqyFyhT3bZ4F2XlJCjxGmRFF0Shwn9rzhOYRB61w9VMXUkxlBy56dk9JJiQ==", 370 | "cpu": [ 371 | "x64" 372 | ], 373 | "dev": true, 374 | "optional": true, 375 | "os": [ 376 | "sunos" 377 | ], 378 | "engines": { 379 | "node": ">=12" 380 | } 381 | }, 382 | "node_modules/@esbuild/win32-arm64": { 383 | "version": "0.19.11", 384 | "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.11.tgz", 385 | "integrity": "sha512-0P58Sbi0LctOMOQbpEOvOL44Ne0sqbS0XWHMvvrg6NE5jQ1xguCSSw9jQeUk2lfrXYsKDdOe6K+oZiwKPilYPQ==", 386 | "cpu": [ 387 | "arm64" 388 | ], 389 | "dev": true, 390 | "optional": true, 391 | "os": [ 392 | "win32" 393 | ], 394 | "engines": { 395 | "node": ">=12" 396 | } 397 | }, 398 | "node_modules/@esbuild/win32-ia32": { 399 | "version": "0.19.11", 400 | "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.11.tgz", 401 | "integrity": "sha512-6YOrWS+sDJDmshdBIQU+Uoyh7pQKrdykdefC1avn76ss5c+RN6gut3LZA4E2cH5xUEp5/cA0+YxRaVtRAb0xBg==", 402 | "cpu": [ 403 | "ia32" 404 | ], 405 | "dev": true, 406 | "optional": true, 407 | "os": [ 408 | "win32" 409 | ], 410 | "engines": { 411 | "node": ">=12" 412 | } 413 | }, 414 | "node_modules/@esbuild/win32-x64": { 415 | "version": "0.19.11", 416 | "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.11.tgz", 417 | "integrity": "sha512-vfkhltrjCAb603XaFhqhAF4LGDi2M4OrCRrFusyQ+iTLQ/o60QQXxc9cZC/FFpihBI9N1Grn6SMKVJ4KP7Fuiw==", 418 | "cpu": [ 419 | "x64" 420 | ], 421 | "dev": true, 422 | "optional": true, 423 | "os": [ 424 | "win32" 425 | ], 426 | "engines": { 427 | "node": ">=12" 428 | } 429 | }, 430 | "node_modules/@jspm/core": { 431 | "version": "2.0.1", 432 | "resolved": "https://registry.npmjs.org/@jspm/core/-/core-2.0.1.tgz", 433 | "integrity": "sha512-Lg3PnLp0QXpxwLIAuuJboLeRaIhrgJjeuh797QADg3xz8wGLugQOS5DpsE8A6i6Adgzf+bacllkKZG3J0tGfDw==", 434 | "dev": true 435 | }, 436 | "node_modules/@what3words/api": { 437 | "resolved": "../../../..", 438 | "link": true 439 | }, 440 | "node_modules/esbuild": { 441 | "version": "0.19.11", 442 | "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.11.tgz", 443 | "integrity": "sha512-HJ96Hev2hX/6i5cDVwcqiJBBtuo9+FeIJOtZ9W1kA5M6AMJRHUZlpYZ1/SbEwtO0ioNAW8rUooVpC/WehY2SfA==", 444 | "dev": true, 445 | "hasInstallScript": true, 446 | "bin": { 447 | "esbuild": "bin/esbuild" 448 | }, 449 | "engines": { 450 | "node": ">=12" 451 | }, 452 | "optionalDependencies": { 453 | "@esbuild/aix-ppc64": "0.19.11", 454 | "@esbuild/android-arm": "0.19.11", 455 | "@esbuild/android-arm64": "0.19.11", 456 | "@esbuild/android-x64": "0.19.11", 457 | "@esbuild/darwin-arm64": "0.19.11", 458 | "@esbuild/darwin-x64": "0.19.11", 459 | "@esbuild/freebsd-arm64": "0.19.11", 460 | "@esbuild/freebsd-x64": "0.19.11", 461 | "@esbuild/linux-arm": "0.19.11", 462 | "@esbuild/linux-arm64": "0.19.11", 463 | "@esbuild/linux-ia32": "0.19.11", 464 | "@esbuild/linux-loong64": "0.19.11", 465 | "@esbuild/linux-mips64el": "0.19.11", 466 | "@esbuild/linux-ppc64": "0.19.11", 467 | "@esbuild/linux-riscv64": "0.19.11", 468 | "@esbuild/linux-s390x": "0.19.11", 469 | "@esbuild/linux-x64": "0.19.11", 470 | "@esbuild/netbsd-x64": "0.19.11", 471 | "@esbuild/openbsd-x64": "0.19.11", 472 | "@esbuild/sunos-x64": "0.19.11", 473 | "@esbuild/win32-arm64": "0.19.11", 474 | "@esbuild/win32-ia32": "0.19.11", 475 | "@esbuild/win32-x64": "0.19.11" 476 | } 477 | }, 478 | "node_modules/esbuild-plugin-polyfill-node": { 479 | "version": "0.3.0", 480 | "resolved": "https://registry.npmjs.org/esbuild-plugin-polyfill-node/-/esbuild-plugin-polyfill-node-0.3.0.tgz", 481 | "integrity": "sha512-SHG6CKUfWfYyYXGpW143NEZtcVVn8S/WHcEOxk62LuDXnY4Zpmc+WmxJKN6GMTgTClXJXhEM5KQlxKY6YjbucQ==", 482 | "dev": true, 483 | "dependencies": { 484 | "@jspm/core": "^2.0.1", 485 | "import-meta-resolve": "^3.0.0" 486 | }, 487 | "peerDependencies": { 488 | "esbuild": "*" 489 | } 490 | }, 491 | "node_modules/import-meta-resolve": { 492 | "version": "3.1.1", 493 | "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-3.1.1.tgz", 494 | "integrity": "sha512-qeywsE/KC3w9Fd2ORrRDUw6nS/nLwZpXgfrOc2IILvZYnCaEMd+D56Vfg9k4G29gIeVi3XKql1RQatME8iYsiw==", 495 | "dev": true, 496 | "funding": { 497 | "type": "github", 498 | "url": "https://github.com/sponsors/wooorm" 499 | } 500 | } 501 | }, 502 | "dependencies": { 503 | "@esbuild/aix-ppc64": { 504 | "version": "0.19.11", 505 | "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.11.tgz", 506 | "integrity": "sha512-FnzU0LyE3ySQk7UntJO4+qIiQgI7KoODnZg5xzXIrFJlKd2P2gwHsHY4927xj9y5PJmJSzULiUCWmv7iWnNa7g==", 507 | "dev": true, 508 | "optional": true 509 | }, 510 | "@esbuild/android-arm": { 511 | "version": "0.19.11", 512 | "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.11.tgz", 513 | "integrity": "sha512-5OVapq0ClabvKvQ58Bws8+wkLCV+Rxg7tUVbo9xu034Nm536QTII4YzhaFriQ7rMrorfnFKUsArD2lqKbFY4vw==", 514 | "dev": true, 515 | "optional": true 516 | }, 517 | "@esbuild/android-arm64": { 518 | "version": "0.19.11", 519 | "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.11.tgz", 520 | "integrity": "sha512-aiu7K/5JnLj//KOnOfEZ0D90obUkRzDMyqd/wNAUQ34m4YUPVhRZpnqKV9uqDGxT7cToSDnIHsGooyIczu9T+Q==", 521 | "dev": true, 522 | "optional": true 523 | }, 524 | "@esbuild/android-x64": { 525 | "version": "0.19.11", 526 | "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.11.tgz", 527 | "integrity": "sha512-eccxjlfGw43WYoY9QgB82SgGgDbibcqyDTlk3l3C0jOVHKxrjdc9CTwDUQd0vkvYg5um0OH+GpxYvp39r+IPOg==", 528 | "dev": true, 529 | "optional": true 530 | }, 531 | "@esbuild/darwin-arm64": { 532 | "version": "0.19.11", 533 | "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.11.tgz", 534 | "integrity": "sha512-ETp87DRWuSt9KdDVkqSoKoLFHYTrkyz2+65fj9nfXsaV3bMhTCjtQfw3y+um88vGRKRiF7erPrh/ZuIdLUIVxQ==", 535 | "dev": true, 536 | "optional": true 537 | }, 538 | "@esbuild/darwin-x64": { 539 | "version": "0.19.11", 540 | "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.11.tgz", 541 | "integrity": "sha512-fkFUiS6IUK9WYUO/+22omwetaSNl5/A8giXvQlcinLIjVkxwTLSktbF5f/kJMftM2MJp9+fXqZ5ezS7+SALp4g==", 542 | "dev": true, 543 | "optional": true 544 | }, 545 | "@esbuild/freebsd-arm64": { 546 | "version": "0.19.11", 547 | "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.11.tgz", 548 | "integrity": "sha512-lhoSp5K6bxKRNdXUtHoNc5HhbXVCS8V0iZmDvyWvYq9S5WSfTIHU2UGjcGt7UeS6iEYp9eeymIl5mJBn0yiuxA==", 549 | "dev": true, 550 | "optional": true 551 | }, 552 | "@esbuild/freebsd-x64": { 553 | "version": "0.19.11", 554 | "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.11.tgz", 555 | "integrity": "sha512-JkUqn44AffGXitVI6/AbQdoYAq0TEullFdqcMY/PCUZ36xJ9ZJRtQabzMA+Vi7r78+25ZIBosLTOKnUXBSi1Kw==", 556 | "dev": true, 557 | "optional": true 558 | }, 559 | "@esbuild/linux-arm": { 560 | "version": "0.19.11", 561 | "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.11.tgz", 562 | "integrity": "sha512-3CRkr9+vCV2XJbjwgzjPtO8T0SZUmRZla+UL1jw+XqHZPkPgZiyWvbDvl9rqAN8Zl7qJF0O/9ycMtjU67HN9/Q==", 563 | "dev": true, 564 | "optional": true 565 | }, 566 | "@esbuild/linux-arm64": { 567 | "version": "0.19.11", 568 | "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.11.tgz", 569 | "integrity": "sha512-LneLg3ypEeveBSMuoa0kwMpCGmpu8XQUh+mL8XXwoYZ6Be2qBnVtcDI5azSvh7vioMDhoJFZzp9GWp9IWpYoUg==", 570 | "dev": true, 571 | "optional": true 572 | }, 573 | "@esbuild/linux-ia32": { 574 | "version": "0.19.11", 575 | "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.11.tgz", 576 | "integrity": "sha512-caHy++CsD8Bgq2V5CodbJjFPEiDPq8JJmBdeyZ8GWVQMjRD0sU548nNdwPNvKjVpamYYVL40AORekgfIubwHoA==", 577 | "dev": true, 578 | "optional": true 579 | }, 580 | "@esbuild/linux-loong64": { 581 | "version": "0.19.11", 582 | "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.11.tgz", 583 | "integrity": "sha512-ppZSSLVpPrwHccvC6nQVZaSHlFsvCQyjnvirnVjbKSHuE5N24Yl8F3UwYUUR1UEPaFObGD2tSvVKbvR+uT1Nrg==", 584 | "dev": true, 585 | "optional": true 586 | }, 587 | "@esbuild/linux-mips64el": { 588 | "version": "0.19.11", 589 | "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.11.tgz", 590 | "integrity": "sha512-B5x9j0OgjG+v1dF2DkH34lr+7Gmv0kzX6/V0afF41FkPMMqaQ77pH7CrhWeR22aEeHKaeZVtZ6yFwlxOKPVFyg==", 591 | "dev": true, 592 | "optional": true 593 | }, 594 | "@esbuild/linux-ppc64": { 595 | "version": "0.19.11", 596 | "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.11.tgz", 597 | "integrity": "sha512-MHrZYLeCG8vXblMetWyttkdVRjQlQUb/oMgBNurVEnhj4YWOr4G5lmBfZjHYQHHN0g6yDmCAQRR8MUHldvvRDA==", 598 | "dev": true, 599 | "optional": true 600 | }, 601 | "@esbuild/linux-riscv64": { 602 | "version": "0.19.11", 603 | "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.11.tgz", 604 | "integrity": "sha512-f3DY++t94uVg141dozDu4CCUkYW+09rWtaWfnb3bqe4w5NqmZd6nPVBm+qbz7WaHZCoqXqHz5p6CM6qv3qnSSQ==", 605 | "dev": true, 606 | "optional": true 607 | }, 608 | "@esbuild/linux-s390x": { 609 | "version": "0.19.11", 610 | "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.11.tgz", 611 | "integrity": "sha512-A5xdUoyWJHMMlcSMcPGVLzYzpcY8QP1RtYzX5/bS4dvjBGVxdhuiYyFwp7z74ocV7WDc0n1harxmpq2ePOjI0Q==", 612 | "dev": true, 613 | "optional": true 614 | }, 615 | "@esbuild/linux-x64": { 616 | "version": "0.19.11", 617 | "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.11.tgz", 618 | "integrity": "sha512-grbyMlVCvJSfxFQUndw5mCtWs5LO1gUlwP4CDi4iJBbVpZcqLVT29FxgGuBJGSzyOxotFG4LoO5X+M1350zmPA==", 619 | "dev": true, 620 | "optional": true 621 | }, 622 | "@esbuild/netbsd-x64": { 623 | "version": "0.19.11", 624 | "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.11.tgz", 625 | "integrity": "sha512-13jvrQZJc3P230OhU8xgwUnDeuC/9egsjTkXN49b3GcS5BKvJqZn86aGM8W9pd14Kd+u7HuFBMVtrNGhh6fHEQ==", 626 | "dev": true, 627 | "optional": true 628 | }, 629 | "@esbuild/openbsd-x64": { 630 | "version": "0.19.11", 631 | "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.11.tgz", 632 | "integrity": "sha512-ysyOGZuTp6SNKPE11INDUeFVVQFrhcNDVUgSQVDzqsqX38DjhPEPATpid04LCoUr2WXhQTEZ8ct/EgJCUDpyNw==", 633 | "dev": true, 634 | "optional": true 635 | }, 636 | "@esbuild/sunos-x64": { 637 | "version": "0.19.11", 638 | "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.11.tgz", 639 | "integrity": "sha512-Hf+Sad9nVwvtxy4DXCZQqLpgmRTQqyFyhT3bZ4F2XlJCjxGmRFF0Shwn9rzhOYRB61w9VMXUkxlBy56dk9JJiQ==", 640 | "dev": true, 641 | "optional": true 642 | }, 643 | "@esbuild/win32-arm64": { 644 | "version": "0.19.11", 645 | "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.11.tgz", 646 | "integrity": "sha512-0P58Sbi0LctOMOQbpEOvOL44Ne0sqbS0XWHMvvrg6NE5jQ1xguCSSw9jQeUk2lfrXYsKDdOe6K+oZiwKPilYPQ==", 647 | "dev": true, 648 | "optional": true 649 | }, 650 | "@esbuild/win32-ia32": { 651 | "version": "0.19.11", 652 | "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.11.tgz", 653 | "integrity": "sha512-6YOrWS+sDJDmshdBIQU+Uoyh7pQKrdykdefC1avn76ss5c+RN6gut3LZA4E2cH5xUEp5/cA0+YxRaVtRAb0xBg==", 654 | "dev": true, 655 | "optional": true 656 | }, 657 | "@esbuild/win32-x64": { 658 | "version": "0.19.11", 659 | "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.11.tgz", 660 | "integrity": "sha512-vfkhltrjCAb603XaFhqhAF4LGDi2M4OrCRrFusyQ+iTLQ/o60QQXxc9cZC/FFpihBI9N1Grn6SMKVJ4KP7Fuiw==", 661 | "dev": true, 662 | "optional": true 663 | }, 664 | "@jspm/core": { 665 | "version": "2.0.1", 666 | "resolved": "https://registry.npmjs.org/@jspm/core/-/core-2.0.1.tgz", 667 | "integrity": "sha512-Lg3PnLp0QXpxwLIAuuJboLeRaIhrgJjeuh797QADg3xz8wGLugQOS5DpsE8A6i6Adgzf+bacllkKZG3J0tGfDw==", 668 | "dev": true 669 | }, 670 | "@what3words/api": { 671 | "version": "file:../../../..", 672 | "requires": { 673 | "@istanbuljs/nyc-config-typescript": "^1.0.1", 674 | "@testing-library/react": "^12.1.3", 675 | "@types/chance": "^1.1.3", 676 | "@types/jsdom": "^16.2.14", 677 | "@types/mocha": "^9.0.0", 678 | "@types/node": "^18.19.31", 679 | "@types/react": "^17.0.39", 680 | "@types/sinon": "^10.0.6", 681 | "@types/superagent": "^4.1.16", 682 | "@typescript-eslint/eslint-plugin": "^5.3.1", 683 | "@vitest/coverage-v8": "^1.5.0", 684 | "chance": "^1.1.8", 685 | "eslint-plugin-prettier": "^4.0.0", 686 | "gts": "^3.1.0", 687 | "jsdom": "^19.0.0", 688 | "nock": "^13.2.0", 689 | "react": "^17.0.2", 690 | "react-dom": "^17.0.2", 691 | "superagent": "^8.0.8", 692 | "ts-node": "^10.4.0", 693 | "typescript": "^5.0.0", 694 | "vite-plugin-commonjs": "^0.10.1", 695 | "vitest": "^1.5.0" 696 | } 697 | }, 698 | "esbuild": { 699 | "version": "0.19.11", 700 | "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.11.tgz", 701 | "integrity": "sha512-HJ96Hev2hX/6i5cDVwcqiJBBtuo9+FeIJOtZ9W1kA5M6AMJRHUZlpYZ1/SbEwtO0ioNAW8rUooVpC/WehY2SfA==", 702 | "dev": true, 703 | "requires": { 704 | "@esbuild/aix-ppc64": "0.19.11", 705 | "@esbuild/android-arm": "0.19.11", 706 | "@esbuild/android-arm64": "0.19.11", 707 | "@esbuild/android-x64": "0.19.11", 708 | "@esbuild/darwin-arm64": "0.19.11", 709 | "@esbuild/darwin-x64": "0.19.11", 710 | "@esbuild/freebsd-arm64": "0.19.11", 711 | "@esbuild/freebsd-x64": "0.19.11", 712 | "@esbuild/linux-arm": "0.19.11", 713 | "@esbuild/linux-arm64": "0.19.11", 714 | "@esbuild/linux-ia32": "0.19.11", 715 | "@esbuild/linux-loong64": "0.19.11", 716 | "@esbuild/linux-mips64el": "0.19.11", 717 | "@esbuild/linux-ppc64": "0.19.11", 718 | "@esbuild/linux-riscv64": "0.19.11", 719 | "@esbuild/linux-s390x": "0.19.11", 720 | "@esbuild/linux-x64": "0.19.11", 721 | "@esbuild/netbsd-x64": "0.19.11", 722 | "@esbuild/openbsd-x64": "0.19.11", 723 | "@esbuild/sunos-x64": "0.19.11", 724 | "@esbuild/win32-arm64": "0.19.11", 725 | "@esbuild/win32-ia32": "0.19.11", 726 | "@esbuild/win32-x64": "0.19.11" 727 | } 728 | }, 729 | "esbuild-plugin-polyfill-node": { 730 | "version": "0.3.0", 731 | "resolved": "https://registry.npmjs.org/esbuild-plugin-polyfill-node/-/esbuild-plugin-polyfill-node-0.3.0.tgz", 732 | "integrity": "sha512-SHG6CKUfWfYyYXGpW143NEZtcVVn8S/WHcEOxk62LuDXnY4Zpmc+WmxJKN6GMTgTClXJXhEM5KQlxKY6YjbucQ==", 733 | "dev": true, 734 | "requires": { 735 | "@jspm/core": "^2.0.1", 736 | "import-meta-resolve": "^3.0.0" 737 | } 738 | }, 739 | "import-meta-resolve": { 740 | "version": "3.1.1", 741 | "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-3.1.1.tgz", 742 | "integrity": "sha512-qeywsE/KC3w9Fd2ORrRDUw6nS/nLwZpXgfrOc2IILvZYnCaEMd+D56Vfg9k4G29gIeVi3XKql1RQatME8iYsiw==", 743 | "dev": true 744 | } 745 | } 746 | } 747 | -------------------------------------------------------------------------------- /docs/examples/vanilla/map/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "map-using-sdk", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "build": "node esbuild/build.mjs", 9 | "dev": "rm -rf public/dist/** && node esbuild/serve.mjs" 10 | }, 11 | "author": "", 12 | "license": "ISC", 13 | "dependencies": { 14 | "@what3words/api": "file:../../../.." 15 | }, 16 | "devDependencies": { 17 | "esbuild": "^0.19.11", 18 | "esbuild-plugin-polyfill-node": "^0.3.0" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /docs/examples/vanilla/map/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Map - SDK 9 | 10 | 11 |
12 | 13 | 14 | -------------------------------------------------------------------------------- /docs/examples/vanilla/map/public/styles.css: -------------------------------------------------------------------------------- 1 | /* Always set the map height explicitly to define the size of the div element that contains the map. */ 2 | #w3w-map { 3 | height: 100%; 4 | } 5 | /* Optional: Makes the sample page fill the window. */ 6 | html, 7 | body { 8 | height: 100%; 9 | margin: 0; 10 | padding: 0; 11 | } 12 | -------------------------------------------------------------------------------- /docs/examples/vanilla/map/src/google.js: -------------------------------------------------------------------------------- 1 | export const initMap = ({ selector, center, zoom, ...opts }) => { 2 | if (!window.google) throw Error('Google API sdk not found'); 3 | 4 | const mapEl = document.querySelector(selector); 5 | return new window.google.maps.Map(mapEl, { 6 | center, 7 | zoom, 8 | ...opts, 9 | }); 10 | }; 11 | -------------------------------------------------------------------------------- /docs/examples/vanilla/map/src/what3words.js: -------------------------------------------------------------------------------- 1 | import what3words, { axiosTransport, ApiVersion } from '@what3words/api'; 2 | import { initMap } from './google'; 3 | 4 | // SETUP 5 | const DEFAULT_MAP_CENTER = { lat: 51.52086, lng: -0.195499 }; 6 | const MAP_SELECTOR = 'div#w3w-map'; 7 | const MAP_ZOOM = 20; 8 | const THREE_WORD_ADDRESS = 'filled.count.soap'; 9 | const W3W_API_KEY = process.env.W3W_API_KEY; 10 | const W3W_API_ENDPOINT = process.env.W3W_API_ENDPOINT; 11 | const GOOGLE_API_KEY = process.env.W3W_MAP_API_KEY; 12 | 13 | if (!GOOGLE_API_KEY) throw new Error('Missing Google Maps API key'); 14 | 15 | // Dynamically load google maps API - must attach to DOM after callback is defined 16 | const googleScript = document.createElement('script'); 17 | googleScript.src = `https://maps.googleapis.com/maps/api/js?key=${GOOGLE_API_KEY}&callback=init`; 18 | googleScript.async = true; 19 | 20 | let address, 21 | div, 22 | input, 23 | grid = [], 24 | languages, 25 | map, 26 | mapCenter; 27 | 28 | function getControls() { 29 | div = document.createElement('div'); 30 | input = document.createElement('input'); 31 | 32 | // Add element styling 33 | div.style.margin = '50px'; 34 | div.style.display = 'flex'; 35 | div.style.flexDirection = 'column'; 36 | div.style.flexGrow = 1; 37 | input.style.lineHeight = '25px'; 38 | input.style.minWidth = '250px'; 39 | 40 | // Add input attributes 41 | input.type = 'text'; 42 | input.index = 1; 43 | 44 | div.appendChild(input); 45 | return div; 46 | } 47 | 48 | function clearGrid() { 49 | if (grid) grid.forEach(g => g.setMap(null)); 50 | } 51 | 52 | function getGrid() { 53 | // Zoom level is high enough 54 | const ne = map.getBounds().getNorthEast(); 55 | const sw = map.getBounds().getSouthWest(); 56 | 57 | // Call the what3words Grid API to obtain the grid squares within the current visible bounding box 58 | return window.what3words 59 | .gridSection({ 60 | boundingBox: { 61 | southwest: { 62 | lat: sw.lat(), 63 | lng: sw.lng(), 64 | }, 65 | northeast: { 66 | lat: ne.lat(), 67 | lng: ne.lng(), 68 | }, 69 | }, 70 | }) 71 | .catch(err => { 72 | const { 73 | details: { code, message }, 74 | } = err; 75 | console.error(`Error retrieving grid sections - ${code}: ${message}`); 76 | }); 77 | } 78 | 79 | function onMapClick(e) { 80 | const latLng = e.latLng.toJSON(); 81 | // Convert a coordinate to a three word address 82 | window.what3words.convertTo3wa({ coordinates: latLng }).then(res => { 83 | plot3wa(res); 84 | if (map.getZoom() < 20) { 85 | map.panTo(res.coordinates); 86 | map.setZoom(20); 87 | } 88 | }); 89 | } 90 | 91 | function plot3wa(new3wa) { 92 | if (!new3wa) return; 93 | const { square, words } = new3wa; 94 | if (address) address.setMap(null); 95 | 96 | address = new google.maps.Rectangle({ 97 | bounds: new google.maps.LatLngBounds(square.southwest, square.northeast), 98 | strokeWeight: 1, 99 | fillColor: '#0A3049', 100 | strokeColor: '#0A3049', 101 | strokePosition: google.maps.StrokePosition.INSIDE, 102 | }); 103 | input.value = '///' + words; 104 | address.setMap(map); 105 | } 106 | 107 | function plotGrid(newGrid) { 108 | if (!grid) return; 109 | 110 | const { lines } = newGrid; 111 | lines.forEach(line => { 112 | const lineCoords = [ 113 | { lat: line.start.lat, lng: line.start.lng }, 114 | { lat: line.end.lat, lng: line.end.lng }, 115 | ]; 116 | const gridline = new google.maps.Polyline({ 117 | path: lineCoords, 118 | geodesic: false, 119 | strokeWeight: 1, 120 | strokeOpacity: 0.1, 121 | fillColor: '#0A3049', 122 | strokeColor: '#0A3049', 123 | strokePosition: google.maps.StrokePosition.CENTER, 124 | }); 125 | gridline.setMap(map); 126 | grid.push(gridline); 127 | }); 128 | } 129 | 130 | /** 131 | * Make use of google maps script callback functionality to load the app 132 | */ 133 | window.init = function () { 134 | if (!W3W_API_KEY) throw new Error('Invalid or missing what3words API key.'); 135 | 136 | window.what3words = what3words( 137 | W3W_API_KEY, 138 | { apiVersion: ApiVersion.Version3, host: W3W_API_ENDPOINT }, 139 | { transport: axiosTransport() } 140 | ); 141 | 142 | // Retrieve and print out supported languages 143 | window.what3words.availableLanguages().then(res => { 144 | languages = res.languages; 145 | }); 146 | 147 | // Retrieve coordinates for initial three word address 148 | window.what3words 149 | .convertToCoordinates({ words: THREE_WORD_ADDRESS }) 150 | .then(res => { 151 | mapCenter = res.coordinates; 152 | }) 153 | .catch(err => { 154 | const { 155 | details: { code, message }, 156 | } = err; 157 | console.error( 158 | `Error converting 3wa to coordinates - ${code}: ${message}` 159 | ); 160 | }) 161 | .finally(() => { 162 | // Initialise map instance 163 | map = initMap({ 164 | selector: MAP_SELECTOR, 165 | center: mapCenter || DEFAULT_MAP_CENTER, 166 | zoom: MAP_ZOOM, 167 | }); 168 | 169 | const controls = getControls(); 170 | map.controls[google.maps.ControlPosition.TOP_CENTER].push(controls); 171 | map.addListener('bounds_changed', e => { 172 | const zoom = map.getZoom(); 173 | clearGrid(); 174 | if (zoom > 15) getGrid(map).then(grid => grid && plotGrid(grid)); 175 | }); 176 | map.addListener('click', onMapClick); 177 | }); 178 | }; 179 | 180 | // Append google script to head 181 | document.head.appendChild(googleScript); 182 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": false, 3 | "name": "@what3words/api", 4 | "version": "5.4.0", 5 | "description": "what3words JavaScript API", 6 | "homepage": "https://github.com/what3words/w3w-node-wrapper#readme", 7 | "main": "dist/index.js", 8 | "browser": "dist/index.js", 9 | "module": "dist/index.js", 10 | "types": "dist/index.d.ts", 11 | "files": [ 12 | "dist", 13 | "README.md" 14 | ], 15 | "scripts": { 16 | "test": "vitest run", 17 | "test:watch": "vitest dev", 18 | "coverage": "vitest run --coverage", 19 | "lint": "gts lint", 20 | "clean": "gts clean", 21 | "compile": "tsc", 22 | "fix": "gts fix", 23 | "precoverage": "rm -rf coverage", 24 | "precompile": "npm rum clean; scripts/languages", 25 | "posttest": "npm run lint" 26 | }, 27 | "peerDependencies": { 28 | "axios": ">=1", 29 | "cross-fetch": ">=3" 30 | }, 31 | "peerDependenciesMeta": { 32 | "axios": { 33 | "optional": true 34 | }, 35 | "cross-fetch": { 36 | "optional": true 37 | } 38 | }, 39 | "devDependencies": { 40 | "@istanbuljs/nyc-config-typescript": "^1.0.1", 41 | "@testing-library/react": "^12.1.3", 42 | "@types/chance": "^1.1.3", 43 | "@types/jsdom": "^16.2.14", 44 | "@types/mocha": "^9.0.0", 45 | "@types/node": "^18.19.31", 46 | "@types/react": "^17.0.39", 47 | "@types/sinon": "^10.0.6", 48 | "@types/superagent": "^4.1.16", 49 | "@typescript-eslint/eslint-plugin": "^5.3.1", 50 | "@vitest/coverage-v8": "^1.5.0", 51 | "chance": "^1.1.8", 52 | "eslint-plugin-prettier": "^4.0.0", 53 | "gts": "^3.1.0", 54 | "jsdom": "^19.0.0", 55 | "nock": "^13.2.0", 56 | "react": "^17.0.2", 57 | "react-dom": "^17.0.2", 58 | "superagent": "^8.0.8", 59 | "ts-node": "^10.4.0", 60 | "typescript": "^5.0.0", 61 | "vite-plugin-commonjs": "^0.10.1", 62 | "vitest": "^1.5.0" 63 | }, 64 | "engines": { 65 | "node": ">=18" 66 | }, 67 | "author": { 68 | "name": "what3words" 69 | }, 70 | "bugs": { 71 | "url": "https://github.com/what3words/w3w-node-wrapper/issues" 72 | }, 73 | "keywords": [ 74 | "what3words", 75 | "api", 76 | "library", 77 | "utility", 78 | "geo", 79 | "maps", 80 | "geolocation" 81 | ] 82 | } 83 | -------------------------------------------------------------------------------- /scripts/languages: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /* eslint-disable no-console */ 3 | /* eslint-disable node/shebang */ 4 | 5 | /** 6 | * This script fetches all supported languages from the public API 7 | * and generates a typescript file which can be used by the library 8 | * to validate language codes without having to call the API every time. 9 | */ 10 | 11 | const { existsSync } = require('fs'); 12 | const { writeFile } = require('fs/promises'); 13 | const fetch = require('cross-fetch'); 14 | 15 | const LIB_PATH = 'src/lib/languages'; 16 | const GENERATED_FILE_COMMENT = '/** GENERATED FILE - DO NOT MODIFY */\n'; 17 | const GENERATED_FILE_NAME = 'language-codes.ts'; 18 | function tryPrettier(content) { 19 | const moduleExists = module => { 20 | try { 21 | return existsSync(require.resolve(module)); 22 | } catch (error) { 23 | return false; 24 | } 25 | }; 26 | // check if prettier is installed 27 | if (moduleExists('prettier')) { 28 | // default prettier config in case .prettierrc.js doesn't exist 29 | let prettierConfig = { 30 | bracketSpacing: false, 31 | singleQuote: true, 32 | trailingComma: 'es5', 33 | arrowParens: 'avoid', 34 | }; 35 | // override default prettier config if .prettierrc.js exists 36 | if (moduleExists('../.prettierrc.js')) { 37 | prettierConfig = require('../.prettierrc.js'); 38 | // remove deprecated property 39 | if (Object.hasOwn(prettierConfig, 'jsxBracketSameLine')) { 40 | delete prettierConfig.jsxBracketSameLine; 41 | } 42 | } 43 | // eslint-disable-next-line node/no-extraneous-require 44 | return require('prettier').format(content, { 45 | ...prettierConfig, 46 | parser: 'babel', 47 | }); 48 | } 49 | return content; 50 | } 51 | 52 | async function fetchSupportedLanguages() { 53 | /** 54 | * Fetch all supported language from the public API 55 | */ 56 | try { 57 | const api_key = process.env.W3W_API_KEY; 58 | if (!api_key) { 59 | throw new Error('W3W_API_KEY is not set'); 60 | } 61 | const res = await fetch( 62 | `https://api.what3words.com/v3/available-languages?key=${api_key}` 63 | ); 64 | if (res.status >= 400) { 65 | throw new Error('Bad response from server'); 66 | } 67 | const { languages } = (await res.json()) || []; 68 | return languages; 69 | } catch (err) { 70 | console.error( 71 | `\x1b[1m\x1b[31mERROR\x1b[0m:\x1b[0m Failed to fetch supported languages from the Public API - \x1b[33m${err.message}\x1b[0m` 72 | ); 73 | // eslint-disable-next-line no-process-exit 74 | process.exit(); 75 | } 76 | } 77 | 78 | (async () => { 79 | const languages = await fetchSupportedLanguages(); 80 | const languagesAndLocales = languages 81 | .flatMap(lang => 82 | lang.locales 83 | ? [`"${lang.code}"`, ...lang.locales.map(locale => `"${locale.code}"`)] 84 | : [`"${lang.code}"`] 85 | ) 86 | .sort() 87 | .join(','); 88 | const additionalComment = 89 | '/** Supported languages and locales from the what3words public api. See: https://api.what3words.com/v3/available-languages\n * This allows validation of language codes without having to call the API every single time\n */\n'; 90 | const languageContent = await tryPrettier( 91 | `${GENERATED_FILE_COMMENT}${additionalComment}export const languages = [${languagesAndLocales}];` 92 | ); 93 | try { 94 | await writeFile(`${LIB_PATH}/${GENERATED_FILE_NAME}`, languageContent); 95 | console.log( 96 | `\x1b[1m\x1b[32mSuccessfully\x1b[0m\x1b[0m generated \x1b[33m${GENERATED_FILE_NAME}\x1b[0m` 97 | ); 98 | } catch (err) { 99 | console.error( 100 | `\x1b[1m\x1b[31mERROR\x1b[0m:\x1b[0m Failed to generate \x1b[32m${GENERATED_FILE_NAME}\x1b[0m - \x1b[33m${err.message}\x1b[0m` 101 | ); 102 | // eslint-disable-next-line no-process-exit 103 | process.exit(); 104 | } 105 | })(); 106 | -------------------------------------------------------------------------------- /src/client/autosuggest.ts: -------------------------------------------------------------------------------- 1 | import type { ApiClientConfiguration, Transport } from '../lib'; 2 | import { 3 | ApiClient, 4 | W3W_POSSIBLE_REGEX, 5 | W3W_REGEX, 6 | arrayToString, 7 | boundsToString, 8 | coordinatesToString, 9 | validLanguage, 10 | } from '../lib'; 11 | 12 | import type { Bounds, Coordinates } from './response.model'; 13 | 14 | export interface AutosuggestSuggestion { 15 | country: string; 16 | nearestPlace: string; 17 | words: string; 18 | distanceToFocusKm: number; 19 | rank: number; 20 | language: string; 21 | } 22 | export interface AutosuggestResponse { 23 | suggestions: AutosuggestSuggestion[]; 24 | } 25 | export enum AutosuggestInputType { 26 | Text = 'text', 27 | VoconHybrid = 'vocon-hybrid', 28 | NMDP_ASR = 'nmdp-asr', 29 | GenericVoice = 'generic-voice', 30 | } 31 | 32 | export interface AutosuggestOptions { 33 | input: string; 34 | nResults?: number; 35 | focus?: Coordinates; 36 | nFocusResults?: number; 37 | clipToCountry?: string[]; 38 | clipToBoundingBox?: Bounds; 39 | clipToCircle?: { center: Coordinates; radius: number }; 40 | clipToPolygon?: Coordinates[]; 41 | inputType?: AutosuggestInputType; 42 | language?: string; 43 | preferLand?: boolean; 44 | } 45 | 46 | export class AutosuggestClient extends ApiClient< 47 | AutosuggestResponse, 48 | AutosuggestOptions 49 | > { 50 | private lastReqOpts: AutosuggestOptions = { input: '' }; 51 | protected readonly url = '/autosuggest'; 52 | protected readonly method = 'get'; 53 | 54 | public static init( 55 | apiKey?: string, 56 | config?: ApiClientConfiguration, 57 | transport?: Transport 58 | ): AutosuggestClient { 59 | return new AutosuggestClient(apiKey, config, transport); 60 | } 61 | 62 | protected query(options: AutosuggestOptions) { 63 | this.lastReqOpts = options; 64 | return { 65 | ...this.autosuggestOptionsToQuery(options), 66 | input: options.input, 67 | }; 68 | } 69 | 70 | protected async validate(options: AutosuggestOptions) { 71 | const textOptions = [AutosuggestInputType.Text]; 72 | const speechOptions = [ 73 | AutosuggestInputType.GenericVoice, 74 | AutosuggestInputType.NMDP_ASR, 75 | AutosuggestInputType.VoconHybrid, 76 | ]; 77 | let valid = !!options; 78 | let message: string | undefined = undefined; 79 | 80 | if (!valid) { 81 | message = 'You must provide at least options.input'; 82 | return { valid, message }; 83 | } 84 | if (options.input.length < 1) { 85 | valid = false; 86 | message = 'You must specify an input value'; 87 | } 88 | if (options.clipToCountry?.filter(country => country.length > 2)?.length) { 89 | valid = false; 90 | message = 91 | 'Invalid clip to country. All values must be an ISO 3166-1 alpha-2 country code'; 92 | } 93 | if ( 94 | options.clipToBoundingBox && 95 | (options.clipToBoundingBox.southwest.lat > 96 | options.clipToBoundingBox.northeast.lat || 97 | options.clipToBoundingBox.southwest.lng > 98 | options.clipToBoundingBox.northeast.lng) 99 | ) { 100 | valid = false; 101 | message = 102 | 'Southwest lat must be less than or equal to northeast lat and southwest lng must be less than or equal to northeast lng'; 103 | } 104 | if (options.clipToPolygon) { 105 | if ( 106 | !Array.isArray(options.clipToPolygon) || 107 | options.clipToPolygon.length < 4 || 108 | options.clipToPolygon.length > 25 109 | ) { 110 | valid = false; 111 | message = 112 | 'Invalid clip to polygon value. Array must contain at least 4 coordinates and no more than 25'; 113 | } 114 | const lastIndex = options.clipToPolygon.length - 1; 115 | if ( 116 | options.clipToPolygon[0].lat !== options.clipToPolygon[lastIndex].lat || 117 | options.clipToPolygon[0].lng !== options.clipToPolygon[lastIndex].lng 118 | ) { 119 | valid = false; 120 | message = 121 | 'Invalid clip to polygon value. The polygon bounds must be closed.'; 122 | } 123 | } 124 | if (options.inputType) { 125 | if (![...textOptions, ...speechOptions].includes(options.inputType)) { 126 | valid = false; 127 | message = 128 | 'Invalid input type provided. Must provide a valid input type.'; 129 | } 130 | if ( 131 | options.language === undefined && 132 | speechOptions.includes(options.inputType) 133 | ) { 134 | valid = false; 135 | message = 'You must provide language when using a speech input type'; 136 | } 137 | } 138 | if (options.language && !validLanguage(options.language)) { 139 | valid = false; 140 | message = `The language ${options.language} is not supported. Refer to our API for supported languages.`; 141 | } 142 | return { valid, message }; 143 | } 144 | 145 | private autosuggestOptionsToQuery(options: AutosuggestOptions): { 146 | [key: string]: string; 147 | } { 148 | const requestOptions: { [key: string]: string } = {}; 149 | if (options.nResults !== undefined) { 150 | requestOptions['n-results'] = options.nResults.toString(); 151 | } 152 | if (options.focus !== undefined) { 153 | requestOptions['focus'] = coordinatesToString(options.focus); 154 | } 155 | if (options.nFocusResults !== undefined) { 156 | requestOptions['n-focus-results'] = options.nFocusResults.toString(); 157 | } 158 | if ( 159 | options.clipToCountry !== undefined && 160 | Array.isArray(options.clipToCountry) && 161 | options.clipToCountry.length > 0 162 | ) { 163 | requestOptions['clip-to-country'] = arrayToString(options.clipToCountry); 164 | } 165 | if (options.clipToBoundingBox !== undefined) { 166 | requestOptions['clip-to-bounding-box'] = boundsToString( 167 | options.clipToBoundingBox 168 | ); 169 | } 170 | if (options.clipToCircle !== undefined) { 171 | requestOptions['clip-to-circle'] = `${coordinatesToString( 172 | options.clipToCircle.center 173 | )},${options.clipToCircle.radius}`; 174 | } 175 | if (options.clipToPolygon !== undefined) { 176 | requestOptions['clip-to-polygon'] = options.clipToPolygon 177 | .map(coord => coordinatesToString(coord)) 178 | .join(','); 179 | } 180 | if (options.inputType !== undefined) { 181 | requestOptions['input-type'] = options.inputType; 182 | } 183 | if (options.language !== undefined) { 184 | requestOptions['language'] = options.language; 185 | } 186 | if (options.preferLand !== undefined) { 187 | requestOptions['prefer-land'] = options.preferLand.toString(); 188 | } 189 | return requestOptions; 190 | } 191 | 192 | /** 193 | * An analytics handler to transmit successful autosuggest selections 194 | * @param {AutosuggestSuggestion} selected 195 | * @param {AutosuggestOptions} initialRequestOptions 196 | * @return {Promise} 197 | */ 198 | public async onSelected( 199 | selected: AutosuggestSuggestion, 200 | initialRequestOptions: AutosuggestOptions = this.lastReqOpts 201 | ): Promise { 202 | await this.makeClientRequest('get', '/autosuggest-selection', { 203 | query: { 204 | ...this.autosuggestOptionsToQuery(initialRequestOptions), 205 | 'raw-input': initialRequestOptions.input, 206 | selection: selected.words, 207 | rank: `${selected.rank}`, 208 | ...(!initialRequestOptions.inputType 209 | ? { 'source-api': 'text' } 210 | : { 211 | 'source-api': 212 | initialRequestOptions.inputType === AutosuggestInputType.Text 213 | ? 'text' 214 | : 'voice', 215 | }), 216 | }, 217 | }); 218 | } 219 | 220 | /** 221 | * Searches the string passed in for all substrings in the form of a three word address. This does not validate whther it is a real address as it will return x.x.x as a result 222 | * @param {string} text 223 | * @returns {string[]} 224 | * @since 5.1.1 225 | */ 226 | public findPossible3wa(text: string): string[] { 227 | return text.match(W3W_REGEX) || []; 228 | } 229 | 230 | /** 231 | * Determines of the string passed in is the form of a three word address. This does not validate whther it is a real address as it returns True for x.x.x 232 | * @param {string} text 233 | * @returns {boolean} 234 | * @since 5.1.1 235 | */ 236 | public isPossible3wa(text: string): boolean { 237 | return new RegExp(W3W_POSSIBLE_REGEX).test(text); 238 | } 239 | 240 | /** 241 | * Determines of the string passed in is a real three word address. It calls the API to verify it refers to an actual plac eon earth. 242 | * @param {string} text 243 | * @returns {boolean} 244 | * @since 5.1.1 245 | */ 246 | public async isValid3wa(text: string): Promise { 247 | if (this.isPossible3wa(text)) { 248 | const options: AutosuggestOptions = { 249 | input: text, 250 | nResults: 1, 251 | }; 252 | const { suggestions } = await this.run(options); 253 | if (suggestions.length > 0) { 254 | if (suggestions[0]['words'] === text) { 255 | return true; 256 | } 257 | } 258 | } 259 | return false; 260 | } 261 | } 262 | -------------------------------------------------------------------------------- /src/client/available-languages.ts: -------------------------------------------------------------------------------- 1 | import { ApiClient } from '../lib'; 2 | import type { ApiClientConfiguration, Transport } from '../lib'; 3 | 4 | export interface AvailableLanguagesResponse { 5 | languages: { 6 | code: string; 7 | name: string; 8 | nativeName: string; 9 | }[]; 10 | } 11 | 12 | export class AvailableLanguagesClient extends ApiClient { 13 | protected readonly method = 'get'; 14 | protected readonly url = '/available-languages'; 15 | public static init( 16 | apiKey?: string, 17 | config?: ApiClientConfiguration, 18 | transport?: Transport 19 | ): AvailableLanguagesClient { 20 | return new AvailableLanguagesClient(apiKey, config, transport); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/client/convert-to-3wa.ts: -------------------------------------------------------------------------------- 1 | import { ApiClient } from '../lib'; 2 | import type { ApiClientConfiguration, Transport } from '../lib'; 3 | import type { 4 | Coordinates, 5 | FeatureCollectionResponse, 6 | LocationGeoJsonResponse, 7 | LocationJsonResponse, 8 | } from './response.model'; 9 | 10 | export type ConvertTo3waOptions = { 11 | coordinates: Coordinates; 12 | language?: string; 13 | format?: 'json' | 'geojson'; 14 | }; 15 | 16 | export class ConvertTo3waClient extends ApiClient< 17 | LocationJsonResponse, 18 | ConvertTo3waOptions, 19 | FeatureCollectionResponse 20 | > { 21 | protected readonly method = 'get'; 22 | protected readonly url = '/convert-to-3wa'; 23 | 24 | public static init( 25 | apiKey?: string, 26 | config?: ApiClientConfiguration, 27 | transport?: Transport 28 | ): ConvertTo3waClient { 29 | return new ConvertTo3waClient(apiKey, config, transport); 30 | } 31 | 32 | protected query(options: ConvertTo3waOptions) { 33 | return { 34 | coordinates: `${options.coordinates.lat},${options.coordinates.lng}`, 35 | language: options.language || 'en', 36 | format: options.format || 'json', 37 | }; 38 | } 39 | 40 | protected async validate(options: ConvertTo3waOptions) { 41 | if (!options?.coordinates) { 42 | return { valid: false, message: 'No coordinates provided' }; 43 | } 44 | return { valid: true }; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/client/convert-to-coordinates.ts: -------------------------------------------------------------------------------- 1 | import { ApiClient } from '../lib'; 2 | import type { ApiClientConfiguration, Transport } from '../lib'; 3 | import type { 4 | FeatureCollectionResponse, 5 | LocationGeoJsonResponse, 6 | LocationJsonResponse, 7 | } from './response.model'; 8 | 9 | export type ConvertToCoordinatesOptions = { 10 | words: string; 11 | format?: 'json' | 'geojson'; 12 | }; 13 | 14 | export class ConvertToCoordinatesClient extends ApiClient< 15 | LocationJsonResponse, 16 | ConvertToCoordinatesOptions, 17 | FeatureCollectionResponse 18 | > { 19 | protected readonly method = 'get'; 20 | protected readonly url = '/convert-to-coordinates'; 21 | 22 | public static init( 23 | apiKey?: string, 24 | config?: ApiClientConfiguration, 25 | transport?: Transport 26 | ): ConvertToCoordinatesClient { 27 | return new ConvertToCoordinatesClient(apiKey, config, transport); 28 | } 29 | 30 | protected query(options: ConvertToCoordinatesOptions) { 31 | return { 32 | words: options.words, 33 | format: options.format || 'json', 34 | }; 35 | } 36 | 37 | protected async validate(options: ConvertToCoordinatesOptions) { 38 | if (!options?.words) { 39 | return { 40 | valid: false, 41 | message: 'You must specify the words to convert to coordinates', 42 | }; 43 | } 44 | return { valid: true }; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/client/grid-section.ts: -------------------------------------------------------------------------------- 1 | import { ApiClient, boundsToString } from '../lib'; 2 | import type { ApiClientConfiguration, Transport } from '../lib'; 3 | import type { Coordinates, FeatureCollectionResponse } from './response.model'; 4 | 5 | export interface GridSectionJsonResponse { 6 | lines: { 7 | start: Coordinates; 8 | end: Coordinates; 9 | }[]; 10 | } 11 | export interface GridSectionGeoJsonResponse { 12 | geometry: { 13 | coordinates: [number, number][][]; 14 | type: 'MultiLineString'; 15 | }; 16 | type: 'Feature'; 17 | properties: {}; 18 | } 19 | 20 | export type GridSectionOptions = { 21 | boundingBox: { southwest: Coordinates; northeast: Coordinates }; 22 | format?: 'json' | 'geojson'; 23 | }; 24 | 25 | export class GridSectionClient extends ApiClient< 26 | GridSectionJsonResponse, 27 | GridSectionOptions, 28 | FeatureCollectionResponse 29 | > { 30 | protected readonly method = 'get'; 31 | protected readonly url = '/grid-section'; 32 | 33 | public static init( 34 | apiKey?: string, 35 | config?: ApiClientConfiguration, 36 | transport?: Transport 37 | ): GridSectionClient { 38 | return new GridSectionClient(apiKey, config, transport); 39 | } 40 | 41 | protected query(options: GridSectionOptions) { 42 | return { 43 | 'bounding-box': boundsToString(options.boundingBox, false), 44 | format: options.format || 'json', 45 | }; 46 | } 47 | 48 | protected async validate(options: GridSectionOptions) { 49 | if (!options?.boundingBox) { 50 | return { valid: false, message: 'No bounding box specified' }; 51 | } 52 | if ( 53 | options.boundingBox.northeast.lat < -90 || 54 | options.boundingBox.northeast.lat > 90 || 55 | options.boundingBox.southwest.lat < -90 || 56 | options.boundingBox.southwest.lat > 90 57 | ) { 58 | return { 59 | valid: false, 60 | message: 'Invalid latitude provided. Latitude must be >= -90 and <= 90', 61 | }; 62 | } 63 | return { valid: true }; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/client/index.ts: -------------------------------------------------------------------------------- 1 | export * from './autosuggest'; 2 | export * from './available-languages'; 3 | export * from './convert-to-3wa'; 4 | export * from './convert-to-coordinates'; 5 | export * from './grid-section'; 6 | export * from './response.model'; 7 | -------------------------------------------------------------------------------- /src/client/response.model.ts: -------------------------------------------------------------------------------- 1 | export interface ErrorResponse { 2 | error: { 3 | code: string; 4 | message: string; 5 | }; 6 | } 7 | export type ResponseFormat = 'json' | 'geojson'; 8 | export interface Coordinates { 9 | lat: number; 10 | lng: number; 11 | } 12 | export interface Bounds { 13 | southwest: Coordinates; 14 | northeast: Coordinates; 15 | } 16 | export interface LocationProperties { 17 | country: string; 18 | nearestPlace: string; 19 | words: string; 20 | language: string; 21 | map: string; 22 | } 23 | export interface LocationJsonResponse extends LocationProperties { 24 | coordinates: Coordinates; 25 | square: Bounds; 26 | } 27 | export interface LocationGeoJsonResponse { 28 | bbox: [number, number, number, number]; 29 | geometry: { 30 | coordinates: number[]; 31 | type: string; 32 | }; 33 | type: string; 34 | properties: LocationProperties; 35 | } 36 | export interface RequestOptions { 37 | [x: string]: string; 38 | } 39 | 40 | export interface FeatureCollectionResponse { 41 | features: LocationResponse[]; 42 | type: 'FeatureCollection'; 43 | } 44 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { what3words, What3wordsService } from './service'; 2 | 3 | export * from './client'; 4 | export * from './lib'; 5 | export default what3words; 6 | export { What3wordsService }; 7 | -------------------------------------------------------------------------------- /src/lib/client/abstract.ts: -------------------------------------------------------------------------------- 1 | import { ApiClientConfiguration, ApiVersion } from './client.model'; 2 | import { 3 | errorHandler, 4 | fetchTransport, 5 | Transport, 6 | TransportResponse, 7 | } from '../transport'; 8 | import { HEADERS } from '../constants'; 9 | 10 | export abstract class ApiClient< 11 | JsonResponse, 12 | Params = undefined, 13 | GeoJsonResponse = JsonResponse 14 | > { 15 | protected abstract url: string; 16 | protected abstract method: 'get' | 'post'; 17 | protected _config: ApiClientConfiguration; 18 | private static DEFAULT_CONFIG = { 19 | host: 'https://api.what3words.com', 20 | apiVersion: ApiVersion.Version3, 21 | }; 22 | private transport: Transport; 23 | 24 | constructor( 25 | private _apiKey: string = '', 26 | config: ApiClientConfiguration = {}, 27 | transport: Transport = fetchTransport() 28 | ) { 29 | this._config = Object.assign({}, ApiClient.DEFAULT_CONFIG, config); 30 | this.transport = transport; 31 | } 32 | 33 | public apiKey(apiKey?: string): this | string { 34 | if (apiKey !== undefined) { 35 | this._apiKey = apiKey; 36 | return this; 37 | } 38 | return this._apiKey; 39 | } 40 | 41 | public config( 42 | config?: ApiClientConfiguration 43 | ): this | ApiClientConfiguration { 44 | if (config !== undefined) { 45 | this._config = Object.assign({}, this._config, config); 46 | return this; 47 | } 48 | return this._config; 49 | } 50 | 51 | public async run( 52 | options?: Params | (Params & { format?: 'json' }) 53 | ): Promise; 54 | public async run( 55 | options: Params & { format: 'geojson' } 56 | ): Promise; 57 | public async run(options?: Params): Promise { 58 | const validation = await this.validate(options); 59 | if (!validation.valid) { 60 | throw new Error( 61 | validation.message || 62 | 'There was a problem validating your request options' 63 | ); 64 | } 65 | const params = { 66 | headers: { ...this.headers(), ...(this._config.headers || {}) }, 67 | body: this.body(options), 68 | query: this.query(options), 69 | }; 70 | const response = await this.makeClientRequest< 71 | JsonResponse | GeoJsonResponse 72 | >(this.method, this.url, params); 73 | if (!response.body) throw new Error('No response body set'); 74 | return response.body; 75 | } 76 | 77 | protected async makeClientRequest( 78 | method: 'get' | 'post', 79 | url: string, 80 | params?: { 81 | headers?: { [key: string]: string }; 82 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 83 | body?: { [key: string]: any } | null; 84 | query?: { [key: string]: string }; 85 | } 86 | ): Promise> { 87 | const clientRequest = this.getClientRequest(method, url, params); 88 | const response = await this.transport(clientRequest); 89 | return errorHandler(response); 90 | } 91 | 92 | protected getClientRequest( 93 | method: 'get' | 'post', 94 | url: string, 95 | params?: { 96 | headers?: { [key: string]: string }; 97 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 98 | body?: { [key: string]: any } | null; 99 | query?: { [key: string]: string }; 100 | } 101 | ) { 102 | return { 103 | method, 104 | host: `${this._config.host?.replace(/\/$/, '')}/${ 105 | this._config.apiVersion 106 | }`, 107 | url, 108 | query: { 109 | ...(params?.query || {}), 110 | key: this._apiKey, 111 | }, 112 | headers: { 113 | ...(params?.headers || {}), 114 | 'X-Api-Key': this._apiKey, 115 | ...HEADERS, 116 | }, 117 | body: params?.body || null, 118 | }; 119 | } 120 | 121 | protected async validate( 122 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 123 | options?: Params 124 | ): Promise<{ valid: boolean; message?: string }> { 125 | return { valid: true, message: undefined }; 126 | } 127 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 128 | protected headers(options?: Params) { 129 | return {}; 130 | } 131 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 132 | protected body(options?: Params) { 133 | return null; 134 | } 135 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 136 | protected query(options?: Params) { 137 | return {}; 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/lib/client/client.model.ts: -------------------------------------------------------------------------------- 1 | export enum ApiVersion { 2 | Version1 = 'v1', 3 | Version2 = 'v2', 4 | Version3 = 'v3', 5 | } 6 | export interface ApiClientConfiguration { 7 | apiVersion?: ApiVersion; 8 | host?: string; 9 | headers?: { [key: string]: string }; 10 | } 11 | export interface ClientRequest { 12 | method: 'get' | 'post'; 13 | host: string; 14 | url: string; 15 | query?: { [key: string]: string }; 16 | headers?: { [key: string]: string }; 17 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 18 | body?: { [key: string]: any } | null; 19 | format?: 'json' | 'geojson'; 20 | } 21 | export type ExecFnResponse = [ 22 | 'get' | 'post', 23 | string, 24 | { 25 | headers?: { [key: string]: string }; 26 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 27 | body?: { [key: string]: any }; 28 | query?: { [key: string]: string }; 29 | }? 30 | ]; 31 | -------------------------------------------------------------------------------- /src/lib/client/index.ts: -------------------------------------------------------------------------------- 1 | export * from './abstract'; 2 | export * from './client.model'; 3 | -------------------------------------------------------------------------------- /src/lib/constants.ts: -------------------------------------------------------------------------------- 1 | import { platform, release } from 'os'; 2 | import { getPlatform } from './serializer'; 3 | 4 | /** 5 | * @constant {RegExp} 6 | * Regex pattern derived from https://github.com/what3words/w3w-python-wrapper/blob/master/what3words/what3words.py#L284 7 | */ 8 | export const W3W_REGEX = 9 | /[^0-9`~!@#$%^&*()+\-_=[{\]}\\|'<,.>?/";:£§º©®\s]{1,}[.。。・・︒។։။۔።।][^0-9`~!@#$%^&*()+\-_=[{\]}\\|'<,.>?/";:£§º©®\s]{1,}[.。。・・︒។։။۔።।][^0-9`~!@#$%^&*()+\-_=[{\]}\\|'<,.>?/";:£§º©®\s]{1,}/gi; 10 | 11 | /** 12 | * @constant {RegExp} 13 | * Regex pattern derived from https://github.com/what3words/w3w-python-wrapper/blob/master/what3words/what3words.py#L298 14 | */ 15 | export const W3W_POSSIBLE_REGEX = 16 | /^\/*(?:[^0-9`~!@#$%^&*()+\-_=[{\]}\\|'<,.>?/";:£§º©®\s]{1,}[.。。・・︒។։။۔።।][^0-9`~!@#$%^&*()+\-_=[{\]}\\|'<,.>?/";:£§º©®\s]{1,}[.。。・・︒។։။۔።।][^0-9`~!@#$%^&*()+\-_=[{\]}\\|'<,.>?/";:£§º©®\s]{1,}|'<,.>?\/";:£§º©®\s]+[.。。・・︒។։။۔።।][^0-9`~!@#$%^&*()+\-_=[{\]}\\|'<,.>?/";:£§º©®\s]+|[^0-9`~!@#$%^&*()+\-_=[{\]}\\|'<,.>?/";:£§º©®\s]+([\u0020\u00A0][^0-9`~!@#$%^&*()+\-_=[{\]}\\|'<,.>?/";:£§º©®\s]+){1,3}[.。。・・︒។։။۔።।][^0-9`~!@#$%^&*()+\-_=[{\]}\\|'<,.>?/";:£§º©®\s]+([\u0020\u00A0][^0-9`~!@#$%^&*()+\-_=[{\]}\\|'<,.>?/";:£§º©®\s]+){1,3}[.。。・・︒។։။۔።।][^0-9`~!@#$%^&*()+\-_=[{\]}\\|'<,.>?/";:£§º©®\s]+([\u0020\u00A0][^0-9`~!@#$%^&*()+\-_=[{\]}\\|'<,.>?/";:£§º©®\s]+){1,3})$/gi; 17 | export const VERSION = '__VERSION__'; 18 | export const HEADERS = { 19 | 'X-W3W-Wrapper': 20 | typeof window !== 'undefined' 21 | ? `what3words-JavaScript/${VERSION}` 22 | : `what3words-Node/${VERSION} (Node ${process.version}; ${getPlatform( 23 | platform() 24 | )} ${release()})`, 25 | }; 26 | -------------------------------------------------------------------------------- /src/lib/index.ts: -------------------------------------------------------------------------------- 1 | export * from './client'; 2 | export * from './constants'; 3 | export * from './serializer'; 4 | export * from './transport'; 5 | export * from './validation'; 6 | export * from './languages'; 7 | -------------------------------------------------------------------------------- /src/lib/languages/index.ts: -------------------------------------------------------------------------------- 1 | import { languages } from './language-codes'; 2 | /** 3 | * Retrieves the base language code that the public API supports for any given ISO 639-1 language code. 4 | * i.e.: zh-TW -> zh, pt-BR -> pt, en-GB -> en 5 | * 6 | * Note: this ignores cyrillic and latin languages (_cy and _la) 7 | * @param languageCode The language code to validate. 8 | * @returns the supported language code or undefined if not supported or if the language has invalid format (i.e. 'xxxx'). 9 | * 10 | * @example 11 | * baseLanguageCodeForISO6391('en-GB'); // 'en' 12 | * baseLanguageCodeForISO6391('zh-CN'); // 'zh' 13 | * baseLanguageCodeForISO6391('abcde'); // Error: 'Invalid language code - xxxx.' 14 | */ 15 | export function baseLanguageCodeForISO6391(languageCode: string) { 16 | // Undefined if the language code is invalid. 17 | if (!/^[a-z]{2}(-[a-z]{2})?$/i.test(languageCode)) { 18 | return; 19 | } 20 | const language = languageCode.split('-').shift()?.toLowerCase(); 21 | return languages.find((code: string) => language === code.toLowerCase()); 22 | } 23 | -------------------------------------------------------------------------------- /src/lib/languages/language-codes.ts: -------------------------------------------------------------------------------- 1 | /** GENERATED FILE - DO NOT MODIFY */ 2 | /** Supported languages and locales from the what3words public api. See: https://api.what3words.com/v3/available-languages 3 | * This allows validation of language codes without having to call the API every single time 4 | */ 5 | export const languages = [ 6 | 'af', 7 | 'am', 8 | 'ar', 9 | 'bg', 10 | 'bn', 11 | 'ca', 12 | 'cs', 13 | 'cy', 14 | 'da', 15 | 'de', 16 | 'el', 17 | 'en', 18 | 'es', 19 | 'et', 20 | 'fa', 21 | 'fi', 22 | 'fr', 23 | 'gu', 24 | 'he', 25 | 'hi', 26 | 'hu', 27 | 'id', 28 | 'it', 29 | 'ja', 30 | 'kk', 31 | 'kk_cy', 32 | 'kk_la', 33 | 'km', 34 | 'kn', 35 | 'ko', 36 | 'lo', 37 | 'ml', 38 | 'mn', 39 | 'mn_cy', 40 | 'mn_la', 41 | 'mr', 42 | 'ms', 43 | 'ne', 44 | 'nl', 45 | 'no', 46 | 'oo', 47 | 'oo_cy', 48 | 'oo_la', 49 | 'or', 50 | 'pa', 51 | 'pl', 52 | 'pt', 53 | 'ro', 54 | 'ru', 55 | 'si', 56 | 'sk', 57 | 'sl', 58 | 'sv', 59 | 'sw', 60 | 'ta', 61 | 'te', 62 | 'th', 63 | 'tr', 64 | 'uk', 65 | 'ur', 66 | 'vi', 67 | 'xh', 68 | 'zh', 69 | 'zh_si', 70 | 'zh_tr', 71 | 'zu', 72 | ]; 73 | -------------------------------------------------------------------------------- /src/lib/serializer.ts: -------------------------------------------------------------------------------- 1 | import type { Coordinates, Bounds } from '../client'; 2 | 3 | export function searchParams(data: { 4 | [x: string]: string | boolean | number; 5 | }): string { 6 | return Object.keys(data) 7 | .map(key => `${encodeURIComponent(key)}=${encodeURIComponent(data[key])}`) 8 | .join('&'); 9 | } 10 | export function coordinatesToString( 11 | coordinates: Coordinates, 12 | ordered = false 13 | ): string { 14 | if (ordered && coordinates.lat < coordinates.lng) { 15 | return `${coordinates.lng},${coordinates.lat}`; 16 | } 17 | return `${coordinates.lat},${coordinates.lng}`; 18 | } 19 | export function boundsToString(bounds: Bounds, ordered = true): string { 20 | return `${coordinatesToString( 21 | bounds.southwest, 22 | ordered 23 | )},${coordinatesToString(bounds.northeast, ordered)}`; 24 | } 25 | export function arrayToString(array: Array): string { 26 | return array.join(','); 27 | } 28 | export function getPlatform(platform: string) { 29 | switch (platform) { 30 | case 'darwin': 31 | return 'Mac OS X'; 32 | case 'win32': 33 | return 'Windows'; 34 | case 'linux': 35 | return 'Linux'; 36 | default: 37 | return ''; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/lib/transport/axios.ts: -------------------------------------------------------------------------------- 1 | import { AxiosResponse } from 'axios'; 2 | import type { Transport, TransportResponse } from './model'; 3 | import { ClientRequest } from '../client'; 4 | import { errorHandler } from './error'; 5 | 6 | export function axiosTransport(): Transport { 7 | // axios v0.x.x returns a function when imported via require (which is the expected behaviour) 8 | // while axios v1.x.x returns an object when imported via require 9 | const axios = require('axios'); 10 | const transporter = axios.default || axios; 11 | return async function axiosTransport( 12 | req: ClientRequest 13 | ): Promise> { 14 | const params = req.query || {}; 15 | if (req.format) params.format = req.format; 16 | const options = { 17 | ...req, 18 | baseURL: req.host, 19 | url: req.url, 20 | params, 21 | }; 22 | return await transporter(options) 23 | .then((res: AxiosResponse) => { 24 | const response = errorHandler({ 25 | status: res.status, 26 | statusText: res.statusText, 27 | body: res.data, 28 | headers: res.headers as Record, 29 | }); 30 | return response; 31 | }) 32 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 33 | .catch((err: any) => { 34 | if (err.isAxiosError) 35 | errorHandler({ 36 | status: err.response?.status || err.status || 500, 37 | statusText: err.response?.statusText || err.statusText, 38 | headers: err.response?.headers, 39 | body: err.response?.data, 40 | }); 41 | throw err; 42 | }); 43 | }; 44 | } 45 | -------------------------------------------------------------------------------- /src/lib/transport/error.ts: -------------------------------------------------------------------------------- 1 | import type { TransportResponse } from './model'; 2 | 3 | export class TransportError extends Error { 4 | constructor( 5 | message: string, 6 | public readonly status: number, 7 | public readonly details?: T 8 | ) { 9 | super(message); 10 | } 11 | } 12 | export class BadRequestError extends TransportError { 13 | constructor(message?: string, details?: T) { 14 | super(message || 'Bad Request', 400, details); 15 | } 16 | } 17 | export class UnauthorizedError extends TransportError { 18 | constructor(message?: string, details?: T) { 19 | super(message || 'Unauthorized', 401, details); 20 | } 21 | } 22 | 23 | export class PaymentRequiredError extends TransportError { 24 | constructor(message?: string, details?: T) { 25 | super(message || 'Payment Required', 402, details); 26 | } 27 | } 28 | 29 | export class ForbiddenError extends TransportError { 30 | constructor(message?: string, details?: T) { 31 | super(message || 'Forbidden', 403, details); 32 | } 33 | } 34 | export class NotFoundError extends TransportError { 35 | constructor(message?: string, details?: T) { 36 | super(message || 'Not Found', 404, details); 37 | } 38 | } 39 | export class InternalServerError extends TransportError { 40 | constructor(message?: string, details?: T) { 41 | super(message || 'Internal Server Error', 500, details); 42 | } 43 | } 44 | export class BadGatewayError extends TransportError { 45 | constructor(message?: string, details?: T) { 46 | super(message || 'Bad Gateway', 502, details); 47 | } 48 | } 49 | export class ServiceUnavailableError extends TransportError { 50 | constructor(message?: string, details?: T) { 51 | super(message || 'Service Unavailable', 503, details); 52 | } 53 | } 54 | export class GatewayTimeoutError extends TransportError { 55 | constructor(message?: string, details?: T) { 56 | super(message || 'Gateway Timeout', 504, details); 57 | } 58 | } 59 | 60 | export function errorHandler( 61 | response: TransportResponse 62 | ): TransportResponse { 63 | if (!response) return { status: 204, statusText: 'No Content' }; 64 | const { status, statusText: message, body } = response; 65 | const details = 66 | body && typeof body === 'object' && 'error' in body ? body.error : body; 67 | if (status >= 400) 68 | switch (status) { 69 | case 400: 70 | throw new BadRequestError(message, details); 71 | case 401: 72 | throw new UnauthorizedError(message, details); 73 | case 402: 74 | throw new PaymentRequiredError(message, details); 75 | case 403: 76 | throw new ForbiddenError(message, details); 77 | case 404: 78 | throw new NotFoundError(message, details); 79 | case 500: 80 | throw new InternalServerError(message, details); 81 | case 502: 82 | throw new BadGatewayError(message, details); 83 | case 503: 84 | throw new ServiceUnavailableError(message, details); 85 | case 504: 86 | throw new GatewayTimeoutError(message, details); 87 | default: 88 | throw new TransportError(message || 'Transport Error', status, details); 89 | } 90 | return response; 91 | } 92 | -------------------------------------------------------------------------------- /src/lib/transport/fetch.ts: -------------------------------------------------------------------------------- 1 | import type { Transport, TransportResponse } from './model'; 2 | import { ClientRequest } from '../client'; 3 | import { errorHandler } from './error'; 4 | import { searchParams } from '../serializer'; 5 | 6 | export function fetchTransport(): Transport { 7 | const transporter = require('cross-fetch'); 8 | return async function fetchTransport( 9 | req: ClientRequest 10 | ): Promise> { 11 | const url = `${req.host}${req.url}`; 12 | const query = req.query || {}; 13 | if (req.format) query.format = req.format; 14 | const queryParams = searchParams(query); 15 | const fullPath = `${url}${queryParams.length > 0 ? `?${queryParams}` : ''}`; 16 | const response = await transporter(fullPath, { 17 | method: req.method, 18 | headers: req.headers, 19 | body: req.body ? JSON.stringify(req.body) : null, 20 | }); 21 | const result = errorHandler(await toTransportResponse(response)); 22 | return result; 23 | }; 24 | } 25 | 26 | async function toTransportResponse( 27 | res: Response 28 | ): Promise> { 29 | const headers: { [key: string]: string } = {}; 30 | res.headers.forEach((value, key) => { 31 | headers[key] = value; 32 | }); 33 | return { 34 | status: res.status, 35 | statusText: res.statusText, 36 | body: res.headers.get('content-type')?.includes('application/json') 37 | ? await res.json() 38 | : await res.text(), 39 | headers, 40 | }; 41 | } 42 | -------------------------------------------------------------------------------- /src/lib/transport/index.ts: -------------------------------------------------------------------------------- 1 | export * from './axios'; 2 | export * from './error'; 3 | export * from './fetch'; 4 | export * from './model'; 5 | -------------------------------------------------------------------------------- /src/lib/transport/model.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AutosuggestResponse, 3 | AvailableLanguagesResponse, 4 | FeatureCollectionResponse, 5 | GridSectionGeoJsonResponse, 6 | GridSectionJsonResponse, 7 | LocationGeoJsonResponse, 8 | LocationJsonResponse, 9 | } from '../../client'; 10 | import { ClientRequest } from '../client'; 11 | 12 | export type Transport = < 13 | T = 14 | | AutosuggestResponse 15 | | AvailableLanguagesResponse 16 | | LocationJsonResponse 17 | | GridSectionJsonResponse 18 | | FeatureCollectionResponse 19 | | FeatureCollectionResponse 20 | | string 21 | | null 22 | >( 23 | req: ClientRequest 24 | ) => Promise>; 25 | export interface TransportResponse { 26 | status: number; 27 | statusText?: string; 28 | body?: T | null; 29 | headers?: Record; 30 | } 31 | -------------------------------------------------------------------------------- /src/lib/validation.ts: -------------------------------------------------------------------------------- 1 | import { languages } from './languages/language-codes'; 2 | import { W3W_REGEX } from './constants'; 3 | 4 | /** 5 | * @deprecated since version 5.1.1. Use `Autosuggest.isValid3wa` instead 6 | */ 7 | export function valid3wa(value: string): boolean { 8 | return W3W_REGEX.test(value); 9 | } 10 | 11 | export function validLanguage(languageCode: string): boolean { 12 | return languages.includes(languageCode.toLowerCase()); 13 | } 14 | -------------------------------------------------------------------------------- /src/service.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AutosuggestClient, 3 | AvailableLanguagesClient, 4 | ConvertTo3waClient, 5 | ConvertToCoordinatesClient, 6 | FeatureCollectionResponse, 7 | GridSectionClient, 8 | } from './client'; 9 | import type { 10 | AutosuggestResponse, 11 | AutosuggestSuggestion, 12 | AutosuggestOptions, 13 | AvailableLanguagesResponse, 14 | ConvertTo3waOptions, 15 | ConvertToCoordinatesOptions, 16 | GridSectionOptions, 17 | GridSectionJsonResponse, 18 | GridSectionGeoJsonResponse, 19 | LocationGeoJsonResponse, 20 | LocationJsonResponse, 21 | } from './client'; 22 | import { ApiClientConfiguration } from './lib'; 23 | import type { Transport } from './lib'; 24 | 25 | export interface What3wordsService { 26 | clients: { 27 | autosuggest: AutosuggestClient; 28 | availableLanguages: AvailableLanguagesClient; 29 | convertTo3wa: ConvertTo3waClient; 30 | convertToCoordinates: ConvertToCoordinatesClient; 31 | gridSection: GridSectionClient; 32 | }; 33 | setApiKey(key: string): void; 34 | setConfig(config: ApiClientConfiguration): void; 35 | autosuggest(options: AutosuggestOptions): Promise; 36 | autosuggestSelection(options: AutosuggestSuggestion): Promise; 37 | availableLanguages(): Promise; 38 | convertTo3wa(options: ConvertTo3waOptions): Promise; 39 | convertTo3wa( 40 | options: ConvertTo3waOptions & { format?: 'json' } 41 | ): Promise; 42 | convertTo3wa( 43 | options: ConvertTo3waOptions & { format: 'geojson' } 44 | ): Promise>; 45 | convertToCoordinates( 46 | options: ConvertToCoordinatesOptions 47 | ): Promise; 48 | convertToCoordinates( 49 | options: ConvertToCoordinatesOptions & { format?: 'json' } 50 | ): Promise; 51 | convertToCoordinates( 52 | options: ConvertToCoordinatesOptions & { format: 'geojson' } 53 | ): Promise>; 54 | gridSection(options: GridSectionOptions): Promise; 55 | gridSection( 56 | options: GridSectionOptions & { format?: 'json' } 57 | ): Promise; 58 | gridSection( 59 | options: GridSectionOptions & { format: 'geojson' } 60 | ): Promise>; 61 | } 62 | 63 | export function what3words( 64 | apiKey?: string, 65 | config?: ApiClientConfiguration, 66 | opts?: { transport: Transport } 67 | ): What3wordsService { 68 | const transport = opts?.transport || require('./lib').fetchTransport(); 69 | const autosuggestClient = new AutosuggestClient(apiKey, config, transport); 70 | const availableLanguagesClient = new AvailableLanguagesClient( 71 | apiKey, 72 | config, 73 | transport 74 | ); 75 | const convertTo3waClient = new ConvertTo3waClient(apiKey, config, transport); 76 | const convertToCoordinatesClient = new ConvertToCoordinatesClient( 77 | apiKey, 78 | config, 79 | transport 80 | ); 81 | const gridSectionClient = new GridSectionClient(apiKey, config, transport); 82 | const service = { 83 | clients: { 84 | autosuggest: autosuggestClient, 85 | availableLanguages: availableLanguagesClient, 86 | convertTo3wa: convertTo3waClient, 87 | convertToCoordinates: convertToCoordinatesClient, 88 | gridSection: gridSectionClient, 89 | }, 90 | setApiKey: (apiKey: string) => { 91 | autosuggestClient.apiKey(apiKey); 92 | availableLanguagesClient.apiKey(apiKey); 93 | convertTo3waClient.apiKey(apiKey); 94 | convertToCoordinatesClient.apiKey(apiKey); 95 | gridSectionClient.apiKey(apiKey); 96 | }, 97 | setConfig: (config: ApiClientConfiguration) => { 98 | autosuggestClient.config(config); 99 | availableLanguagesClient.config(config); 100 | convertTo3waClient.config(config); 101 | convertToCoordinatesClient.config(config); 102 | gridSectionClient.config(config); 103 | }, 104 | autosuggest: autosuggestClient.run.bind(autosuggestClient), 105 | autosuggestSelection: autosuggestClient.onSelected.bind(autosuggestClient), 106 | availableLanguages: availableLanguagesClient.run.bind( 107 | availableLanguagesClient 108 | ), 109 | convertTo3wa: convertTo3waClient.run.bind(convertTo3waClient), 110 | convertToCoordinates: convertToCoordinatesClient.run.bind( 111 | convertToCoordinatesClient 112 | ), 113 | gridSection: gridSectionClient.run.bind(gridSectionClient), 114 | }; 115 | 116 | return service; 117 | } 118 | -------------------------------------------------------------------------------- /tests/client/autosuggest.spec.ts: -------------------------------------------------------------------------------- 1 | import { Mock } from 'vitest'; 2 | import nock from 'nock'; 3 | import { Chance } from 'chance'; 4 | import { 5 | ApiClientConfiguration, 6 | ApiVersion, 7 | AutosuggestClient, 8 | AutosuggestInputType, 9 | HEADERS, 10 | Transport, 11 | } from '@/.'; 12 | import { 13 | generateAutosuggestSuggestion, 14 | generateCoordinate, 15 | } from '@utils/fixtures'; 16 | import { languages } from '@/lib/languages/language-codes'; 17 | 18 | const CHANCE = new Chance(); 19 | 20 | describe('Autosuggest Client', () => { 21 | describe('init()', () => { 22 | let apiKey: string; 23 | let apiVersion: ApiVersion; 24 | let host: string; 25 | let config: ApiClientConfiguration; 26 | let transportSpy: Mock; 27 | let transport: Transport; 28 | let client: AutosuggestClient; 29 | 30 | beforeEach(() => { 31 | apiKey = CHANCE.string({ length: 8 }); 32 | apiVersion = ApiVersion.Version1; 33 | host = CHANCE.url({ path: '' }); 34 | config = { host, apiVersion, headers: {} }; 35 | transportSpy = vi.fn(); 36 | transport = async (...args) => { 37 | transportSpy(...args); 38 | return { 39 | status: 200, 40 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 41 | body: {} as any, 42 | }; 43 | }; 44 | client = AutosuggestClient.init(apiKey, config, transport); 45 | }); 46 | 47 | it('should instantiate an Autosuggest Client instance', () => { 48 | expect(client).toBeInstanceOf(AutosuggestClient); 49 | expect(client).toHaveProperty('_apiKey'); 50 | expect(client).toHaveProperty('apiKey'); 51 | expect(client).toHaveProperty('_config'); 52 | expect(client).toHaveProperty('config'); 53 | expect(client).toHaveProperty('lastReqOpts'); 54 | expect(client).toHaveProperty('onSelected'); 55 | expect(client).toHaveProperty('run'); 56 | expect(client).toHaveProperty('transport'); 57 | expectTypeOf(client['_apiKey']).toBeString(); 58 | expect(client['_apiKey']).toEqual(apiKey); 59 | expectTypeOf(client['_config']).toBeObject(); 60 | expect(client['_config']).toEqual(config); 61 | expectTypeOf(client['lastReqOpts']).toBeObject(); 62 | expect(client['lastReqOpts']).toEqual({ input: '' }); 63 | expectTypeOf(client.apiKey).toBeFunction(); 64 | expectTypeOf(client.config).toBeFunction(); 65 | expectTypeOf(client.onSelected).toBeFunction(); 66 | }); 67 | }); 68 | 69 | describe('apiKey()', () => { 70 | let apiKey: string; 71 | let apiVersion: ApiVersion; 72 | let host: string; 73 | let config: ApiClientConfiguration; 74 | let transportSpy: Mock; 75 | let transport: Transport; 76 | let client: AutosuggestClient; 77 | 78 | beforeEach(() => { 79 | apiKey = CHANCE.string({ length: 8 }); 80 | apiVersion = ApiVersion.Version1; 81 | host = CHANCE.url({ path: '' }); 82 | config = { host, apiVersion, headers: {} }; 83 | transportSpy = vi.fn(); 84 | transport = async (...args) => { 85 | transportSpy(...args); 86 | return { 87 | status: 200, 88 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 89 | body: {} as any, 90 | }; 91 | }; 92 | client = AutosuggestClient.init(apiKey, config, transport); 93 | }); 94 | 95 | it('should return the api key when called with no parameter', () => { 96 | expect(client.apiKey()).toEqual(apiKey); 97 | }); 98 | 99 | it('should set the api key when called with value', () => { 100 | const _apiKey = CHANCE.string({ length: 8 }); 101 | expect(client.apiKey()).toEqual(apiKey); 102 | expect(client.apiKey(_apiKey)).toEqual(client); 103 | expect(client.apiKey()).toEqual(_apiKey); 104 | }); 105 | }); 106 | 107 | describe('config()', () => { 108 | let apiKey: string; 109 | let apiVersion: ApiVersion; 110 | let host: string; 111 | let config: ApiClientConfiguration; 112 | let transportSpy: Mock; 113 | let transport: Transport; 114 | let client: AutosuggestClient; 115 | 116 | beforeEach(() => { 117 | apiKey = CHANCE.string({ length: 8 }); 118 | apiVersion = ApiVersion.Version1; 119 | host = CHANCE.url({ path: '' }); 120 | config = { host, apiVersion, headers: {} }; 121 | transportSpy = vi.fn(); 122 | transport = async (...args) => { 123 | transportSpy(...args); 124 | return { 125 | status: 200, 126 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 127 | body: {} as any, 128 | }; 129 | }; 130 | client = AutosuggestClient.init(apiKey, config, transport); 131 | }); 132 | 133 | it('should return the config when called with no parameter', () => { 134 | expect(client.config()).toEqual(config); 135 | }); 136 | 137 | it('should set the config when called with value', () => { 138 | const defaultConfig = { host, apiVersion, headers: {} }; 139 | const config = { 140 | host: CHANCE.url(), 141 | apiVersion: CHANCE.pickone([ApiVersion.Version2, ApiVersion.Version3]), 142 | headers: {}, 143 | }; 144 | expect(client.config()).toEqual(defaultConfig); 145 | expect(client.config(config)).toEqual(client); 146 | expect(client.config()).toEqual(config); 147 | }); 148 | }); 149 | 150 | describe('onSelected()', () => { 151 | let apiKey: string; 152 | let apiVersion: ApiVersion; 153 | let host: string; 154 | let config: ApiClientConfiguration; 155 | let transportSpy: Mock; 156 | let transport: Transport; 157 | let client: AutosuggestClient; 158 | 159 | beforeEach(() => { 160 | apiKey = CHANCE.string({ length: 8 }); 161 | apiVersion = ApiVersion.Version1; 162 | host = CHANCE.url({ path: '' }); 163 | config = { host, apiVersion, headers: {} }; 164 | transportSpy = vi.fn(); 165 | transport = async (...args) => { 166 | transportSpy(...args); 167 | return { 168 | status: 200, 169 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 170 | body: {} as any, 171 | }; 172 | }; 173 | client = AutosuggestClient.init(apiKey, config, transport); 174 | }); 175 | 176 | it('should call /autosuggest-selection with selected suggestion', async () => { 177 | const selected = generateAutosuggestSuggestion(); 178 | const transportArguments = { 179 | method: 'get', 180 | host: `${host.replace(/\/$/, '')}/${apiVersion}`, 181 | url: '/autosuggest-selection', 182 | query: { 183 | 'raw-input': '', 184 | selection: selected.words, 185 | rank: `${selected.rank}`, 186 | 'source-api': 'text', 187 | key: apiKey, 188 | }, 189 | headers: { 'X-Api-Key': apiKey, ...HEADERS }, 190 | body: null, 191 | }; 192 | 193 | expect(client.onSelected(selected)).resolves.toBeUndefined(); 194 | expect(transportSpy).toHaveBeenNthCalledWith(1, transportArguments); 195 | }); 196 | 197 | describe('should call /autosuggest-selection with initial request options override', () => { 198 | const input = `${CHANCE.word()}.${CHANCE.word()}.${CHANCE.letter()}`; 199 | const nResults = CHANCE.natural(); 200 | const nFocusResults = CHANCE.natural(); 201 | const focus = generateCoordinate(); 202 | const clipToBoundingBox = { 203 | southwest: { lat: 71.2, lng: -0.123 }, 204 | northeast: { lat: 91, lng: -0.12 }, 205 | }; 206 | const clipToCircle = { 207 | center: generateCoordinate(), 208 | radius: CHANCE.natural(), 209 | }; 210 | const clipToCountry = [CHANCE.locale()]; 211 | const startEndCoordinate = generateCoordinate(); 212 | const clipToPolygon = [ 213 | startEndCoordinate, 214 | generateCoordinate(), 215 | generateCoordinate(), 216 | generateCoordinate(), 217 | startEndCoordinate, 218 | ]; 219 | const language = CHANCE.locale(); 220 | const preferLand = CHANCE.bool(); 221 | const selected = generateAutosuggestSuggestion(); 222 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 223 | let transportArguments: { [key: string]: any }; 224 | 225 | beforeEach(() => { 226 | transportArguments = { 227 | method: 'get', 228 | host: `${host.replace(/\/$/, '')}/${apiVersion}`, 229 | url: '/autosuggest-selection', 230 | query: { 231 | key: apiKey, 232 | 'raw-input': input, 233 | selection: selected.words, 234 | rank: `${selected.rank}`, 235 | 'n-results': `${nResults}`, 236 | focus: `${focus.lat},${focus.lng}`, 237 | 'n-focus-results': `${nFocusResults}`, 238 | 'clip-to-bounding-box': `${clipToBoundingBox.southwest.lat},${clipToBoundingBox.southwest.lng},${clipToBoundingBox.northeast.lat},${clipToBoundingBox.northeast.lng}`, 239 | 'clip-to-circle': `${clipToCircle.center.lat},${clipToCircle.center.lng},${clipToCircle.radius}`, 240 | 'clip-to-country': clipToCountry.join(','), 241 | 'clip-to-polygon': clipToPolygon 242 | .map(coord => `${coord.lat},${coord.lng}`) 243 | .join(','), 244 | language, 245 | 'prefer-land': `${preferLand}`, 246 | }, 247 | headers: { 'X-Api-Key': apiKey, ...HEADERS }, 248 | body: null, 249 | }; 250 | }); 251 | 252 | it('text input type', async () => { 253 | const inputType = AutosuggestInputType.Text; 254 | transportArguments.query['input-type'] = inputType; 255 | transportArguments.query['source-api'] = 'text'; 256 | 257 | expect( 258 | client.onSelected(selected, { 259 | input, 260 | inputType, 261 | nResults, 262 | nFocusResults, 263 | focus, 264 | clipToBoundingBox, 265 | clipToCircle, 266 | clipToCountry, 267 | clipToPolygon, 268 | language, 269 | preferLand, 270 | }) 271 | ).resolves.toBeUndefined(); 272 | expect(transportSpy).toHaveBeenNthCalledWith(1, transportArguments); 273 | }); 274 | it('voice input type', async () => { 275 | const inputType = AutosuggestInputType.GenericVoice; 276 | transportArguments.query['input-type'] = inputType; 277 | transportArguments.query['source-api'] = 'voice'; 278 | 279 | expect( 280 | client.onSelected(selected, { 281 | input, 282 | inputType, 283 | nResults, 284 | nFocusResults, 285 | focus, 286 | clipToBoundingBox, 287 | clipToCircle, 288 | clipToCountry, 289 | clipToPolygon, 290 | language, 291 | preferLand, 292 | }) 293 | ).resolves.toBeUndefined(); 294 | expect(transportSpy).toHaveBeenNthCalledWith(1, transportArguments); 295 | }); 296 | }); 297 | }); 298 | 299 | describe('run()', () => { 300 | let apiKey: string; 301 | let apiVersion: ApiVersion; 302 | let host: string; 303 | let config: ApiClientConfiguration; 304 | let transportSpy: Mock; 305 | let transport: Transport; 306 | let client: AutosuggestClient; 307 | 308 | beforeEach(() => { 309 | apiKey = CHANCE.string({ length: 8 }); 310 | apiVersion = ApiVersion.Version1; 311 | host = CHANCE.url({ path: '' }); 312 | config = { host, apiVersion, headers: {} }; 313 | transportSpy = vi.fn(); 314 | transport = async (...args) => { 315 | transportSpy(...args); 316 | return { 317 | status: 200, 318 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 319 | body: {} as any, 320 | }; 321 | }; 322 | client = AutosuggestClient.init(apiKey, config, transport); 323 | }); 324 | 325 | it('should call /autosuggest', async () => { 326 | const input = `${CHANCE.word()}.${CHANCE.word()}.${CHANCE.letter()}`; 327 | const inputType = AutosuggestInputType.Text; 328 | const nResults = CHANCE.natural(); 329 | const nFocusResults = CHANCE.natural(); 330 | const focus = generateCoordinate(); 331 | const clipToBoundingBox = { 332 | southwest: { lat: 71.2, lng: -0.123 }, 333 | northeast: { lat: 91, lng: -0.12 }, 334 | }; 335 | const clipToCircle = { 336 | center: generateCoordinate(), 337 | radius: CHANCE.natural(), 338 | }; 339 | const clipToCountry = [CHANCE.locale()]; 340 | const startEndCoordinate = generateCoordinate(); 341 | const clipToPolygon = [ 342 | startEndCoordinate, 343 | generateCoordinate(), 344 | generateCoordinate(), 345 | generateCoordinate(), 346 | startEndCoordinate, 347 | ]; 348 | const language = CHANCE.pickone(languages); 349 | const preferLand = CHANCE.bool(); 350 | const transportArguments = { 351 | method: 'get', 352 | host: `${host.replace(/\/$/, '')}/${apiVersion}`, 353 | url: '/autosuggest', 354 | query: { 355 | key: apiKey, 356 | input, 357 | 'n-results': `${nResults}`, 358 | focus: `${focus.lat},${focus.lng}`, 359 | 'n-focus-results': `${nFocusResults}`, 360 | 'clip-to-bounding-box': `${clipToBoundingBox.southwest.lat},${clipToBoundingBox.southwest.lng},${clipToBoundingBox.northeast.lat},${clipToBoundingBox.northeast.lng}`, 361 | 'clip-to-circle': `${clipToCircle.center.lat},${clipToCircle.center.lng},${clipToCircle.radius}`, 362 | 'clip-to-country': clipToCountry.join(','), 363 | 'clip-to-polygon': clipToPolygon 364 | .map(coord => `${coord.lat},${coord.lng}`) 365 | .join(','), 366 | language, 367 | 'prefer-land': `${preferLand}`, 368 | 'input-type': inputType, 369 | }, 370 | headers: { 'X-Api-Key': apiKey, ...HEADERS }, 371 | body: null, 372 | }; 373 | 374 | await client.run({ 375 | input, 376 | inputType, 377 | nResults, 378 | nFocusResults, 379 | focus, 380 | clipToBoundingBox, 381 | clipToCircle, 382 | clipToCountry, 383 | clipToPolygon, 384 | language, 385 | preferLand, 386 | }); 387 | expect(transportSpy).toHaveBeenNthCalledWith(1, transportArguments); 388 | }); 389 | 390 | it('should call /autosuggest with voice input type', async () => { 391 | const input = `${CHANCE.word()}.${CHANCE.word()}.${CHANCE.letter()}`; 392 | const inputType = CHANCE.pickone([ 393 | AutosuggestInputType.VoconHybrid, 394 | AutosuggestInputType.NMDP_ASR, 395 | AutosuggestInputType.GenericVoice, 396 | ]); 397 | const language = CHANCE.pickone(languages); 398 | const transportArguments = { 399 | method: 'get', 400 | host: `${host.replace(/\/$/, '')}/${apiVersion}`, 401 | url: '/autosuggest', 402 | query: { 403 | key: apiKey, 404 | input, 405 | 'input-type': inputType, 406 | language, 407 | }, 408 | headers: { 'X-Api-Key': apiKey, ...HEADERS }, 409 | body: null, 410 | }; 411 | 412 | await client.run({ 413 | input, 414 | inputType, 415 | language, 416 | }); 417 | expect(transportSpy).toHaveBeenNthCalledWith(1, transportArguments); 418 | }); 419 | 420 | it('should throw error when no language provided with voice input type', async () => { 421 | const input = `${CHANCE.word()}.${CHANCE.word()}.${CHANCE.letter()}`; 422 | const inputType = CHANCE.pickone([ 423 | AutosuggestInputType.VoconHybrid, 424 | AutosuggestInputType.NMDP_ASR, 425 | AutosuggestInputType.GenericVoice, 426 | ]); 427 | 428 | try { 429 | await client.run({ 430 | input, 431 | inputType, 432 | }); 433 | } catch (err) { 434 | expect(err.message).toEqual( 435 | 'You must provide language when using a speech input type' 436 | ); 437 | } finally { 438 | expect(transportSpy).not.toHaveBeenCalled(); 439 | } 440 | }); 441 | 442 | it('should throw error if no options provided', async () => { 443 | try { 444 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 445 | await client.run(undefined as any); 446 | } catch (err) { 447 | expect(err.message).toEqual('You must provide at least options.input'); 448 | } finally { 449 | expect(transportSpy).not.toHaveBeenCalled(); 450 | } 451 | }); 452 | 453 | it('should throw error if input is empty', async () => { 454 | const input = ''; 455 | 456 | try { 457 | await client.run({ input }); 458 | } catch (err) { 459 | expect(err.message).toEqual('You must specify an input value'); 460 | } finally { 461 | expect(transportSpy).not.toHaveBeenCalled(); 462 | } 463 | }); 464 | 465 | it('should throw error if clipToBoundingBox has southwest lat > northeast lat', async () => { 466 | const input = `${CHANCE.word()}.${CHANCE.word()}.${CHANCE.letter()}`; 467 | const clipToBoundingBox = { 468 | southwest: { lat: 2, lng: 3 }, 469 | northeast: { lat: 1, lng: 5 }, 470 | }; 471 | 472 | try { 473 | await client.run({ input, clipToBoundingBox }); 474 | } catch (err) { 475 | expect(err.message).toEqual( 476 | 'Southwest lat must be less than or equal to northeast lat and southwest lng must be less than or equal to northeast lng' 477 | ); 478 | } finally { 479 | expect(transportSpy).not.toHaveBeenCalled(); 480 | } 481 | }); 482 | 483 | it('should throw error if clipToBoundingBox has southwest lng > northeast lng', async () => { 484 | const input = `${CHANCE.word()}.${CHANCE.word()}.${CHANCE.letter()}`; 485 | const clipToBoundingBox = { 486 | southwest: { lat: 1, lng: 9 }, 487 | northeast: { lat: 1, lng: 5 }, 488 | }; 489 | 490 | try { 491 | await client.run({ input, clipToBoundingBox }); 492 | } catch (err) { 493 | expect(err.message).toEqual( 494 | 'Southwest lat must be less than or equal to northeast lat and southwest lng must be less than or equal to northeast lng' 495 | ); 496 | } finally { 497 | expect(transportSpy).not.toHaveBeenCalled(); 498 | } 499 | }); 500 | 501 | it('should throw error if clipToCountry has incorrect value', async () => { 502 | const input = `${CHANCE.word()}.${CHANCE.word()}.${CHANCE.letter()}`; 503 | const clipToCountry = [ 504 | CHANCE.locale(), 505 | CHANCE.string({ length: 3 }), 506 | CHANCE.locale(), 507 | ]; 508 | 509 | try { 510 | await client.run({ input, clipToCountry }); 511 | } catch (err) { 512 | expect(err.message).toEqual( 513 | 'Invalid clip to country. All values must be an ISO 3166-1 alpha-2 country code' 514 | ); 515 | } finally { 516 | expect(transportSpy).not.toHaveBeenCalled(); 517 | } 518 | }); 519 | 520 | it('should throw error if clipToPolygon has less than 4 entries', async () => { 521 | const input = `${CHANCE.word()}.${CHANCE.word()}.${CHANCE.letter()}`; 522 | const firstLastCoord = generateCoordinate(); 523 | const clipToPolygon = [ 524 | firstLastCoord, 525 | generateCoordinate(), 526 | firstLastCoord, 527 | ]; 528 | 529 | try { 530 | await client.run({ input, clipToPolygon }); 531 | } catch (err) { 532 | expect(err.message).toEqual( 533 | 'Invalid clip to polygon value. Array must contain at least 4 coordinates and no more than 25' 534 | ); 535 | } finally { 536 | expect(transportSpy).not.toHaveBeenCalled(); 537 | } 538 | }); 539 | 540 | it('should throw error if clipToPolygon is not closed', async () => { 541 | const input = `${CHANCE.word()}.${CHANCE.word()}.${CHANCE.letter()}`; 542 | const clipToPolygon = [ 543 | generateCoordinate(), 544 | generateCoordinate(), 545 | generateCoordinate(), 546 | generateCoordinate(), 547 | ]; 548 | 549 | try { 550 | await client.run({ input, clipToPolygon }); 551 | } catch (err) { 552 | expect(err.message).toEqual( 553 | 'Invalid clip to polygon value. The polygon bounds must be closed.' 554 | ); 555 | } finally { 556 | expect(transportSpy).not.toHaveBeenCalled(); 557 | } 558 | }); 559 | 560 | it('should throw error if inputType is not valid', async () => { 561 | const input = `${CHANCE.word()}.${CHANCE.word()}.${CHANCE.letter()}`; 562 | const inputType = 'abc' as AutosuggestInputType; 563 | 564 | try { 565 | await client.run({ input, inputType }); 566 | } catch (err) { 567 | expect(err.message).toEqual( 568 | 'Invalid input type provided. Must provide a valid input type.' 569 | ); 570 | } finally { 571 | expect(transportSpy).not.toHaveBeenCalled(); 572 | } 573 | }); 574 | 575 | it('should throw error if language is not valid', async () => { 576 | const input = `${CHANCE.word()}.${CHANCE.word()}.${CHANCE.letter()}`; 577 | const language = 'abc'; 578 | 579 | try { 580 | await client.run({ input, language }); 581 | } catch (err) { 582 | expect(err.message).toEqual( 583 | `The language ${language} is not supported. Refer to our API for supported languages.` 584 | ); 585 | } finally { 586 | expect(transportSpy).not.toHaveBeenCalled(); 587 | } 588 | }); 589 | 590 | const voiceInputTypes = [ 591 | AutosuggestInputType.GenericVoice, 592 | AutosuggestInputType.NMDP_ASR, 593 | AutosuggestInputType.VoconHybrid, 594 | ]; 595 | 596 | voiceInputTypes.forEach(inputType => { 597 | it(`should throw error if voice inputType ${inputType} is specified but no language provided`, async () => { 598 | const input = `${CHANCE.word()}.${CHANCE.word()}.${CHANCE.letter()}`; 599 | 600 | try { 601 | await client.run({ input, inputType }); 602 | } catch (err) { 603 | expect(err.message).toEqual( 604 | 'You must provide language when using a speech input type' 605 | ); 606 | } finally { 607 | expect(transportSpy).not.toHaveBeenCalled(); 608 | } 609 | }); 610 | }); 611 | }); 612 | 613 | describe('regex validation', () => { 614 | let apiKey: string; 615 | let apiVersion: ApiVersion; 616 | let host: string; 617 | let config: ApiClientConfiguration; 618 | let client: AutosuggestClient; 619 | const invalidStrings = [ 620 | // a 621 | CHANCE.letter(), 622 | // word 623 | CHANCE.word(), 624 | // 1 625 | `${CHANCE.natural()}`, 626 | // 1.2.3 627 | `${CHANCE.natural()}.${CHANCE.natural()}.${CHANCE.natural()}`, 628 | // word.a 629 | `${CHANCE.word()}.${CHANCE.letter()}`, 630 | // word.1 631 | `${CHANCE.word()}.${CHANCE.natural()}`, 632 | // word.word2.1 633 | `${CHANCE.word()}.${CHANCE.word()}.${CHANCE.natural()}`, 634 | // word;a 635 | `${CHANCE.word()}${CHANCE.character({ 636 | symbols: true, 637 | })}${CHANCE.letter()}`, 638 | // word;1 639 | `${CHANCE.word()}${CHANCE.character({ 640 | symbols: true, 641 | })}${CHANCE.natural()}`, 642 | // word?word2;1 643 | `${CHANCE.word()}${CHANCE.character({ 644 | symbols: true, 645 | })}${CHANCE.word()}${CHANCE.character({ 646 | symbols: true, 647 | })}${CHANCE.natural()}`, 648 | ]; 649 | const validStrings = [ 650 | // a.b.c 651 | `${CHANCE.letter()}.${CHANCE.letter()}.${CHANCE.letter()}`, 652 | // word.word2.word3 653 | `${CHANCE.word()}.${CHANCE.word()}.${CHANCE.word()}`, 654 | ]; 655 | 656 | beforeEach(() => { 657 | apiKey = CHANCE.string({ length: 8 }); 658 | apiVersion = ApiVersion.Version1; 659 | host = 'https://api.dev.non-production.w3w.io'; 660 | config = { host, apiVersion, headers: {} }; 661 | client = AutosuggestClient.init(apiKey, config); 662 | }); 663 | 664 | describe('findPossible3wa()', () => { 665 | it('should return an empty array if empty string is provided', async () => { 666 | expect(client.findPossible3wa('')).toHaveLength(0); 667 | }); 668 | 669 | describe('invalid value', () => { 670 | const invalidSubstrings = invalidStrings.map( 671 | v => `text with invalid three word address: ${v}` 672 | ); 673 | 674 | invalidStrings.forEach(invalidString => { 675 | it(`should return an empty array if "${invalidString}" is provided`, async () => { 676 | expect(client.findPossible3wa(invalidString)).toHaveLength(0); 677 | }); 678 | }); 679 | 680 | invalidSubstrings.forEach(invalidSubstring => { 681 | it(`should return an empty array if "${invalidSubstring}" is provided`, async () => { 682 | expect(client.findPossible3wa(invalidSubstring)).toHaveLength(0); 683 | }); 684 | }); 685 | }); 686 | 687 | describe('valid value', () => { 688 | const validSubstrings = validStrings.map( 689 | v => `text with valid three word address: ${v}` 690 | ); 691 | 692 | validStrings.forEach(validString => { 693 | it(`should return a match if "${validString}" is provided`, async () => { 694 | expect(client.findPossible3wa(validString)).toContainEqual( 695 | validString 696 | ); 697 | }); 698 | }); 699 | 700 | validSubstrings.forEach(substring => { 701 | it(`should return a match if "${substring}" is provided`, async () => { 702 | expect( 703 | client 704 | .findPossible3wa(substring) 705 | .every(res => validStrings.includes(res)) 706 | ).toBeTruthy(); 707 | }); 708 | }); 709 | }); 710 | }); 711 | 712 | describe('isPossible3wa()', () => { 713 | it('should return false if empty string is provided', async () => { 714 | expect(client.isPossible3wa('')).toBeFalsy(); 715 | }); 716 | invalidStrings.forEach(invalidString => { 717 | it(`should return false if "${invalidString}" is provided`, async () => { 718 | expect(client.isPossible3wa(invalidString)).toBeFalsy(); 719 | }); 720 | }); 721 | validStrings.forEach(validString => { 722 | it(`should return true if "${validString}" is provided`, async () => { 723 | expect(client.isPossible3wa(validString)).toBeTruthy(); 724 | }); 725 | }); 726 | }); 727 | 728 | describe('isValid3wa()', () => { 729 | it('should return false if empty string is provided', async () => { 730 | const input = ''; 731 | const isValid = client.isValid3wa(input); 732 | expect(isValid).toBeInstanceOf(Promise); 733 | expect(isValid).resolves.toBeFalsy(); 734 | }); 735 | it('should return false if invalid value is provided', async () => { 736 | const input = invalidStrings[0]; 737 | const isValid = client.isValid3wa(input); 738 | expect(isValid).toBeInstanceOf(Promise); 739 | expect(isValid).resolves.toBeFalsy(); 740 | }); 741 | describe('valid values', () => { 742 | validStrings.forEach(input => { 743 | it(`should return true if valid value "${input}" is provided`, async () => { 744 | nock(host, { 745 | allowUnmocked: false, 746 | }) 747 | .get('/v1/autosuggest') 748 | .query(true) // exclude queries from url matching 749 | .reply(200, { suggestions: [{ words: input }] }); 750 | 751 | const isValid = await client.isValid3wa(input); 752 | expect(isValid).toBeTruthy(); 753 | nock.cleanAll(); 754 | }); 755 | }); 756 | }); 757 | }); 758 | }); 759 | }); 760 | -------------------------------------------------------------------------------- /tests/client/available-languages.spec.ts: -------------------------------------------------------------------------------- 1 | import { Chance } from 'chance'; 2 | import { 3 | ApiClientConfiguration, 4 | ApiVersion, 5 | AvailableLanguagesClient, 6 | HEADERS, 7 | Transport, 8 | } from '@/.'; 9 | import type { Mock } from 'vitest'; 10 | 11 | const CHANCE = new Chance(); 12 | 13 | describe('Available Languages Client', () => { 14 | let apiKey: string; 15 | let apiVersion: ApiVersion; 16 | let host: string; 17 | let config: ApiClientConfiguration; 18 | let transportSpy: Mock; 19 | let transport: Transport; 20 | let client: AvailableLanguagesClient; 21 | 22 | beforeEach(() => { 23 | apiKey = CHANCE.string({ length: 8 }); 24 | apiVersion = ApiVersion.Version1; 25 | host = CHANCE.url({ path: '' }); 26 | config = { host, apiVersion }; 27 | transportSpy = vi.fn(); 28 | transport = async (...args) => { 29 | transportSpy(...args); 30 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 31 | return { status: 200, body: {} as any }; 32 | }; 33 | client = AvailableLanguagesClient.init(apiKey, config, transport); 34 | }); 35 | 36 | it('should return instantiate an Available Languages Client instance', () => { 37 | expect(client).toBeInstanceOf(AvailableLanguagesClient); 38 | expect(client).toHaveProperty('_apiKey'); 39 | expect(client).toHaveProperty('apiKey'); 40 | expect(client).toHaveProperty('_config'); 41 | expect(client).toHaveProperty('config'); 42 | expect(client).toHaveProperty('run'); 43 | expect(client).toHaveProperty('transport'); 44 | expectTypeOf(client['_apiKey']).toBeString(); 45 | expect(client['_apiKey']).toEqual(apiKey); 46 | expectTypeOf(client['_config']).toBeObject(); 47 | expect(client['_config']).toEqual(config); 48 | expectTypeOf(client.apiKey).toBeFunction(); 49 | expectTypeOf(client.config).toBeFunction(); 50 | }); 51 | it('should return the api key when apiKey function is called with no parameter', () => { 52 | expect(client.apiKey()).toEqual(apiKey); 53 | }); 54 | it('should set the api key when apiKey function is called with value', () => { 55 | const _apiKey = CHANCE.string({ length: 8 }); 56 | expect(client.apiKey()).toEqual(apiKey); 57 | expect(client.apiKey(_apiKey)).toEqual(client); 58 | expect(client.apiKey()).toEqual(_apiKey); 59 | }); 60 | it('should return the config when config function is called with no parameter', () => { 61 | expect(client.config()).toEqual(config); 62 | }); 63 | it('should set the config when config function is called with value', () => { 64 | const defaultConfig = { host, apiVersion }; 65 | const config = { 66 | host: CHANCE.url(), 67 | apiVersion: CHANCE.pickone([ApiVersion.Version2, ApiVersion.Version3]), 68 | headers: {}, 69 | }; 70 | expect(client.config()).toEqual(defaultConfig); 71 | expect(client.config(config)).toEqual(client); 72 | expect(client.config()).toEqual(config); 73 | }); 74 | it('should call /available-languages when run is called', async () => { 75 | const transportArguments = { 76 | method: 'get', 77 | host: `${host.replace(/\/$/, '')}/${apiVersion}`, 78 | url: '/available-languages', 79 | query: { key: apiKey }, 80 | headers: { 'X-Api-Key': apiKey, ...HEADERS }, 81 | body: null, 82 | }; 83 | await client.run(); 84 | expect(transportSpy).toHaveBeenNthCalledWith(1, transportArguments); 85 | }); 86 | }); 87 | -------------------------------------------------------------------------------- /tests/client/convert-to-3wa.spec.ts: -------------------------------------------------------------------------------- 1 | import { Mock } from 'vitest'; 2 | import { Chance } from 'chance'; 3 | import { 4 | ApiClientConfiguration, 5 | ApiVersion, 6 | ConvertTo3waClient, 7 | HEADERS, 8 | Transport, 9 | } from '@/.'; 10 | import { generateCoordinate } from '@utils/fixtures'; 11 | 12 | const CHANCE = new Chance(); 13 | 14 | describe('Convert to 3wa Client', () => { 15 | let apiKey: string; 16 | let apiVersion: ApiVersion; 17 | let host: string; 18 | let config: ApiClientConfiguration; 19 | let transportSpy: Mock; 20 | let transport: Transport; 21 | let client: ConvertTo3waClient; 22 | 23 | beforeEach(() => { 24 | apiKey = CHANCE.string({ length: 8 }); 25 | apiVersion = ApiVersion.Version1; 26 | host = CHANCE.url({ path: '' }); 27 | config = { host, apiVersion }; 28 | transportSpy = vi.fn(); 29 | transport = async (...args) => { 30 | transportSpy(...args); 31 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 32 | return { status: 200, body: {} as any }; 33 | }; 34 | client = ConvertTo3waClient.init(apiKey, config, transport); 35 | }); 36 | 37 | it('should return instantiate an Convert to 3wa Client instance', () => { 38 | expect(client).toBeInstanceOf(ConvertTo3waClient); 39 | expect(client).toHaveProperty('_apiKey'); 40 | expect(client).toHaveProperty('apiKey'); 41 | expect(client).toHaveProperty('_config'); 42 | expect(client).toHaveProperty('config'); 43 | expect(client).toHaveProperty('run'); 44 | expect(client).toHaveProperty('transport'); 45 | expectTypeOf(client['_apiKey']).toBeString(); 46 | expect(client['_apiKey']).toEqual(apiKey); 47 | expectTypeOf(client['_config']).toBeObject(); 48 | expect(client['_config']).toEqual(config); 49 | expectTypeOf(client.apiKey).toBeFunction(); 50 | expectTypeOf(client.config).toBeFunction(); 51 | }); 52 | it('should return the api key when apiKey function is called with no parameter', () => { 53 | expect(client.apiKey()).toEqual(apiKey); 54 | }); 55 | it('should set the api key when apiKey function is called with value', () => { 56 | const _apiKey = CHANCE.string({ length: 8 }); 57 | expect(client.apiKey()).toEqual(apiKey); 58 | expect(client.apiKey(_apiKey)).toEqual(client); 59 | expect(client.apiKey()).toEqual(_apiKey); 60 | }); 61 | it('should return the config when config function is called with no parameter', () => { 62 | expect(client.config()).toEqual(config); 63 | }); 64 | it('should set the config when config function is called with value', () => { 65 | const defaultConfig = { host, apiVersion }; 66 | const config = { 67 | host: CHANCE.url(), 68 | apiVersion: CHANCE.pickone([ApiVersion.Version2, ApiVersion.Version3]), 69 | headers: {}, 70 | }; 71 | expect(client.config()).toEqual(defaultConfig); 72 | expect(client.config(config)).toEqual(client); 73 | expect(client.config()).toEqual(config); 74 | }); 75 | it('should call /convert-to-3wa when run is called', async () => { 76 | const coordinates = generateCoordinate(); 77 | const language = CHANCE.locale(); 78 | const format = 'json'; 79 | const transportArguments = { 80 | method: 'get', 81 | host: `${host.replace(/\/$/, '')}/${apiVersion}`, 82 | url: '/convert-to-3wa', 83 | query: { 84 | key: apiKey, 85 | language, 86 | coordinates: `${coordinates.lat},${coordinates.lng}`, 87 | format, 88 | }, 89 | headers: { 'X-Api-Key': apiKey, ...HEADERS }, 90 | body: null, 91 | }; 92 | await client.run({ coordinates, language, format }); 93 | expect(transportSpy).toHaveBeenNthCalledWith(1, transportArguments); 94 | }); 95 | it('should call /convert-to-3wa when run is called (no format/language)', async () => { 96 | const coordinates = generateCoordinate(); 97 | const transportArguments = { 98 | method: 'get', 99 | host: `${host.replace(/\/$/, '')}/${apiVersion}`, 100 | url: '/convert-to-3wa', 101 | query: { 102 | key: apiKey, 103 | language: 'en', 104 | coordinates: `${coordinates.lat},${coordinates.lng}`, 105 | format: 'json', 106 | }, 107 | headers: { 'X-Api-Key': apiKey, ...HEADERS }, 108 | body: null, 109 | }; 110 | await client.run({ coordinates }); 111 | expect(transportSpy).toHaveBeenNthCalledWith(1, transportArguments); 112 | }); 113 | it('should throw error when no coordinates are provided', async () => { 114 | try { 115 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 116 | await client.run(undefined as any); 117 | } catch (err) { 118 | expect(err.message).toEqual('No coordinates provided'); 119 | } finally { 120 | expect(transportSpy).not.toHaveBeenCalled(); 121 | } 122 | }); 123 | }); 124 | -------------------------------------------------------------------------------- /tests/client/convert-to-coordinates.spec.ts: -------------------------------------------------------------------------------- 1 | import { Mock } from 'vitest'; 2 | import { Chance } from 'chance'; 3 | import { 4 | ApiClientConfiguration, 5 | ApiVersion, 6 | ConvertToCoordinatesClient, 7 | HEADERS, 8 | Transport, 9 | } from '@/.'; 10 | 11 | const CHANCE = new Chance(); 12 | 13 | describe('Convert to Coordinates Client', () => { 14 | let apiKey: string; 15 | let apiVersion: ApiVersion; 16 | let host: string; 17 | let config: ApiClientConfiguration; 18 | let transportSpy: Mock; 19 | let transport: Transport; 20 | let client: ConvertToCoordinatesClient; 21 | 22 | beforeEach(() => { 23 | apiKey = CHANCE.string({ length: 8 }); 24 | apiVersion = ApiVersion.Version1; 25 | host = CHANCE.url({ path: '' }); 26 | config = { host, apiVersion }; 27 | transportSpy = vi.fn(); 28 | transport = async (...args) => { 29 | transportSpy(...args); 30 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 31 | return { status: 200, body: {} as any }; 32 | }; 33 | client = ConvertToCoordinatesClient.init(apiKey, config, transport); 34 | }); 35 | 36 | it('should return instantiate an Convert to Coordinates Client instance', () => { 37 | expect(client).toBeInstanceOf(ConvertToCoordinatesClient); 38 | expect(client).toHaveProperty('_apiKey'); 39 | expect(client).toHaveProperty('apiKey'); 40 | expect(client).toHaveProperty('_config'); 41 | expect(client).toHaveProperty('config'); 42 | expect(client).toHaveProperty('run'); 43 | expect(client).toHaveProperty('transport'); 44 | 45 | expectTypeOf(client['_apiKey']).toBeString(); 46 | expect(client['_apiKey']).toEqual(apiKey); 47 | expectTypeOf(client['_config']).toBeObject(); 48 | expect(client['_config']).toEqual(config); 49 | expectTypeOf(client.apiKey).toBeFunction(); 50 | expectTypeOf(client.config).toBeFunction(); 51 | }); 52 | it('should return the api key when apiKey function is called with no parameter', () => { 53 | expect(client.apiKey()).toEqual(apiKey); 54 | }); 55 | it('should set the api key when apiKey function is called with value', () => { 56 | const _apiKey = CHANCE.string({ length: 8 }); 57 | expect(client.apiKey()).toEqual(apiKey); 58 | expect(client.apiKey(_apiKey)).toEqual(client); 59 | expect(client.apiKey()).toEqual(_apiKey); 60 | }); 61 | it('should return the config when config function is called with no parameter', () => { 62 | expect(client.config()).toEqual(config); 63 | }); 64 | it('should set the config when config function is called with value', () => { 65 | const defaultConfig = { host, apiVersion }; 66 | const config = { 67 | host: CHANCE.url(), 68 | apiVersion: CHANCE.pickone([ApiVersion.Version2, ApiVersion.Version3]), 69 | headers: {}, 70 | }; 71 | expect(client.config()).toEqual(defaultConfig); 72 | expect(client.config(config)).toEqual(client); 73 | expect(client.config()).toEqual(config); 74 | }); 75 | it('should call /convert-to-coordinates when run is called', async () => { 76 | const words = `${CHANCE.word()}.${CHANCE.word()}.${CHANCE.word()}`; 77 | const transportArguments = { 78 | method: 'get', 79 | host: `${host.replace(/\/$/, '')}/${apiVersion}`, 80 | url: '/convert-to-coordinates', 81 | query: { 82 | key: apiKey, 83 | words, 84 | format: 'json', 85 | }, 86 | headers: { 'X-Api-Key': apiKey, ...HEADERS }, 87 | body: null, 88 | }; 89 | await client.run({ words }); 90 | expect(transportSpy).toHaveBeenNthCalledWith(1, transportArguments); 91 | }); 92 | it('should throw error if no words provided', async () => { 93 | try { 94 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 95 | await client.run(undefined as any); 96 | } catch (err) { 97 | expect(err.message).toEqual( 98 | 'You must specify the words to convert to coordinates' 99 | ); 100 | } finally { 101 | expect(transportSpy).not.toHaveBeenCalled(); 102 | } 103 | }); 104 | }); 105 | -------------------------------------------------------------------------------- /tests/client/grid-section.spec.ts: -------------------------------------------------------------------------------- 1 | import { Mock } from 'vitest'; 2 | import { Chance } from 'chance'; 3 | import { 4 | ApiClientConfiguration, 5 | ApiVersion, 6 | GridSectionClient, 7 | HEADERS, 8 | Transport, 9 | } from '@/.'; 10 | import { generateCoordinate } from '@utils/fixtures'; 11 | 12 | const CHANCE = new Chance(); 13 | 14 | describe('Grid Section Client', () => { 15 | let apiKey: string; 16 | let apiVersion: ApiVersion; 17 | let host: string; 18 | let config: ApiClientConfiguration; 19 | let transportSpy: Mock; 20 | let transport: Transport; 21 | let client: GridSectionClient; 22 | 23 | beforeEach(() => { 24 | apiKey = CHANCE.string({ length: 8 }); 25 | apiVersion = ApiVersion.Version1; 26 | host = CHANCE.url({ path: '' }); 27 | config = { host, apiVersion }; 28 | transportSpy = vi.fn(); 29 | transport = async (...args) => { 30 | transportSpy(...args); 31 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 32 | return { status: 200, body: {} as any }; 33 | }; 34 | client = GridSectionClient.init(apiKey, config, transport); 35 | }); 36 | 37 | it('should return instantiate an Grid Section Client instance', () => { 38 | expect(client).toBeInstanceOf(GridSectionClient); 39 | expect(client).toHaveProperty('_apiKey'); 40 | expect(client).toHaveProperty('apiKey'); 41 | expect(client).toHaveProperty('_config'); 42 | expect(client).toHaveProperty('config'); 43 | expect(client).toHaveProperty('run'); 44 | expect(client).toHaveProperty('transport'); 45 | 46 | expectTypeOf(client['_apiKey']).toBeString(); 47 | expect(client['_apiKey']).toEqual(apiKey); 48 | expectTypeOf(client['_config']).toBeObject(); 49 | expect(client['_config']).toEqual(config); 50 | expectTypeOf(client.apiKey).toBeFunction(); 51 | expectTypeOf(client.config).toBeFunction(); 52 | }); 53 | it('should return the api key when apiKey function is called with no parameter', () => { 54 | expect(client.apiKey()).toEqual(apiKey); 55 | }); 56 | it('should set the api key when apiKey function is called with value', () => { 57 | const _apiKey = CHANCE.string({ length: 8 }); 58 | expect(client.apiKey()).toEqual(apiKey); 59 | expect(client.apiKey(_apiKey)).toEqual(client); 60 | expect(client.apiKey()).toEqual(_apiKey); 61 | }); 62 | it('should return the config when config function is called with no parameter', () => { 63 | expect(client.config()).toEqual(config); 64 | }); 65 | it('should set the config when config function is called with value', () => { 66 | const defaultConfig = { host, apiVersion }; 67 | const config = { 68 | host: CHANCE.url(), 69 | apiVersion: CHANCE.pickone([ApiVersion.Version2, ApiVersion.Version3]), 70 | headers: {}, 71 | }; 72 | expect(client.config()).toEqual(defaultConfig); 73 | expect(client.config(config)).toEqual(client); 74 | expect(client.config()).toEqual(config); 75 | }); 76 | it('should call /grid-section when run is called', async () => { 77 | const boundingBox = { 78 | southwest: generateCoordinate(), 79 | northeast: generateCoordinate(), 80 | }; 81 | const transportArguments = { 82 | method: 'get', 83 | host: `${host.replace(/\/$/, '')}/${apiVersion}`, 84 | url: '/grid-section', 85 | query: { 86 | key: apiKey, 87 | 'bounding-box': `${boundingBox.southwest.lat},${boundingBox.southwest.lng},${boundingBox.northeast.lat},${boundingBox.northeast.lng}`, 88 | format: 'json', 89 | }, 90 | headers: { 'X-Api-Key': apiKey, ...HEADERS }, 91 | body: null, 92 | }; 93 | await client.run({ boundingBox }); 94 | expect(transportSpy).toHaveBeenNthCalledWith(1, transportArguments); 95 | }); 96 | it('should throw error if no bounding box is specified', async () => { 97 | try { 98 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 99 | await client.run(undefined as any); 100 | } catch (err) { 101 | expect(err.message).toEqual('No bounding box specified'); 102 | } finally { 103 | expect(transportSpy).not.toHaveBeenCalled(); 104 | } 105 | }); 106 | it('should throw error if bounding box latitudes are invalid', async () => { 107 | const boundingBox = { 108 | northeast: { lat: 95, lng: -2 }, 109 | southwest: generateCoordinate(), 110 | }; 111 | try { 112 | await client.run({ boundingBox }); 113 | } catch (err) { 114 | expect(err.message).toEqual( 115 | 'Invalid latitude provided. Latitude must be >= -90 and <= 90' 116 | ); 117 | } finally { 118 | expect(transportSpy).not.toHaveBeenCalled(); 119 | } 120 | }); 121 | }); 122 | -------------------------------------------------------------------------------- /tests/lib/languages.spec.ts: -------------------------------------------------------------------------------- 1 | import { Chance } from 'chance'; 2 | import { baseLanguageCodeForISO6391 } from '@/.'; 3 | 4 | const CHANCE = new Chance(); 5 | 6 | const AVAILABLE_LANGUAGES = ['en-GB', 'de-DE', 'it-IT', 'es-ES', 'fr-FR']; 7 | 8 | describe('baseLanguageCodeForISO6391', () => { 9 | it('should return base language code for valid ISO 639-1 language code', () => { 10 | const languageCode = CHANCE.pickone(AVAILABLE_LANGUAGES); 11 | expect(baseLanguageCodeForISO6391(languageCode)).toEqual( 12 | languageCode.substring(0, 2) 13 | ); 14 | }); 15 | 16 | it('should allow case insensitive language code', () => { 17 | const languageCode = CHANCE.pickone(AVAILABLE_LANGUAGES); 18 | expect(baseLanguageCodeForISO6391(languageCode.toUpperCase())).toEqual( 19 | languageCode.substring(0, 2) 20 | ); 21 | }); 22 | 23 | it('should return undefined if invalid language code is provided', () => { 24 | const invalidLanguageCode = CHANCE.string({ length: 8 }); 25 | expect(baseLanguageCodeForISO6391(invalidLanguageCode)).toBeUndefined(); 26 | }); 27 | 28 | it('should return undefined if language code is valid but not supported by the public API', () => { 29 | const unsupportedLanguageCode = 'xx-XX'; 30 | expect(baseLanguageCodeForISO6391(unsupportedLanguageCode)).toBeUndefined(); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /tests/lib/serializer.spec.ts: -------------------------------------------------------------------------------- 1 | import { Chance } from 'chance'; 2 | import { 3 | searchParams, 4 | coordinatesToString, 5 | boundsToString, 6 | arrayToString, 7 | getPlatform, 8 | } from '@/.'; 9 | import { generateCoordinate } from '@utils/fixtures'; 10 | 11 | const CHANCE = new Chance(); 12 | 13 | describe('searchParams()', () => { 14 | it('should serialize parameters', () => { 15 | const params = { 16 | foo: CHANCE.string(), 17 | bar: CHANCE.bool(), 18 | x: CHANCE.natural(), 19 | }; 20 | expect(searchParams(params)).toEqual( 21 | `foo=${encodeURIComponent(params.foo)}&bar=${params.bar}&x=${params.x}` 22 | ); 23 | }); 24 | }); 25 | 26 | describe('coordinateToString()', () => { 27 | it('should serialize coordinates to a string (ordered)', () => { 28 | const coordinates = generateCoordinate(); 29 | const ordered = true; 30 | expect(coordinatesToString(coordinates, ordered)).toEqual( 31 | coordinates.lat < coordinates.lng 32 | ? `${coordinates.lng},${coordinates.lat}` 33 | : `${coordinates.lat},${coordinates.lng}` 34 | ); 35 | }); 36 | it('should serialize coordinates to a string (ordered) lat < lng', () => { 37 | const coordinates = { lat: 1, lng: 10 }; 38 | const ordered = true; 39 | expect(coordinatesToString(coordinates, ordered)).toEqual( 40 | `${coordinates.lng},${coordinates.lat}` 41 | ); 42 | }); 43 | it('should serialize coordinates to a string (unordered)', () => { 44 | const coordinates = generateCoordinate(); 45 | const ordered = false; 46 | expect(coordinatesToString(coordinates, ordered)).toEqual( 47 | `${coordinates.lat},${coordinates.lng}` 48 | ); 49 | }); 50 | }); 51 | 52 | describe('arrayToString()', () => { 53 | it('should serialize an array to string', () => { 54 | const array = [CHANCE.word(), CHANCE.letter(), CHANCE.natural()]; 55 | expect(arrayToString(array)).toEqual(`${array[0]},${array[1]},${array[2]}`); 56 | }); 57 | }); 58 | 59 | describe('getPlatform()', () => { 60 | it('should serialize platform for darwin', () => { 61 | const platform = 'darwin'; 62 | expect(getPlatform(platform)).toEqual('Mac OS X'); 63 | }); 64 | it('should serialize platform for win32', () => { 65 | const platform = 'win32'; 66 | expect(getPlatform(platform)).toEqual('Windows'); 67 | }); 68 | it('should serialize platform for linux', () => { 69 | const platform = 'linux'; 70 | expect(getPlatform(platform)).toEqual('Linux'); 71 | }); 72 | it('should serialize platform for non-matched value', () => { 73 | const platform = CHANCE.word(); 74 | expect(getPlatform(platform)).toEqual(''); 75 | }); 76 | }); 77 | 78 | describe('boundsToString()', () => { 79 | it('should serialize a bound to string (unordered)', () => { 80 | const bounds = { 81 | southwest: generateCoordinate(), 82 | northeast: generateCoordinate(), 83 | }; 84 | const ordered = false; 85 | expect(boundsToString(bounds, ordered)).toEqual( 86 | `${bounds.southwest.lat},${bounds.southwest.lng},${bounds.northeast.lat},${bounds.northeast.lng}` 87 | ); 88 | }); 89 | it('should serialize a bound to string (ordered)', () => { 90 | const bounds = { 91 | southwest: { lat: 1, lng: 5 }, 92 | northeast: { lat: 12, lng: 4 }, 93 | }; 94 | const ordered = true; 95 | expect(boundsToString(bounds, ordered)).toEqual( 96 | `${bounds.southwest.lng},${bounds.southwest.lat},${bounds.northeast.lat},${bounds.northeast.lng}` 97 | ); 98 | }); 99 | }); 100 | -------------------------------------------------------------------------------- /tests/lib/transport/axios.spec.ts: -------------------------------------------------------------------------------- 1 | import nock from 'nock'; 2 | import { Chance } from 'chance'; 3 | import { axiosTransport, HEADERS, searchParams } from '@/.'; 4 | 5 | const CHANCE = new Chance(); 6 | const MOCK_RESPONSE = { foo: 'bar' }; 7 | const MOCK_ERROR_RESPONSE = 'My custom error response message'; 8 | 9 | describe('Axios Transport', () => { 10 | const query = { 11 | example: 'params', 12 | random: 'value', 13 | }; 14 | let host: string; 15 | let url: string; 16 | let method: 'get' | 'post'; 17 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 18 | let request: any; 19 | 20 | beforeEach(() => { 21 | method = CHANCE.pickone(['get', 'post']); 22 | host = CHANCE.url(); 23 | url = '/foo/bar'; 24 | request = { 25 | method, 26 | host, 27 | url, 28 | query, 29 | headers: { 30 | 'X-Custom-Header': 'my-random-header-value', 31 | ...HEADERS, 32 | }, 33 | body: null, 34 | }; 35 | }); 36 | 37 | afterEach(() => { 38 | nock.cleanAll(); 39 | }); 40 | 41 | it('should make a request given a ClientRequest', async () => { 42 | nock(host) 43 | [method](`${url}?${searchParams(request.query)}`) 44 | .reply(200, MOCK_RESPONSE, { 45 | 'Content-Type': 'application/json;charset=utf-8', 46 | }); 47 | 48 | expect(await axiosTransport()(request)).toMatchObject({ 49 | status: 200, 50 | statusText: null, 51 | headers: { 52 | 'content-type': 'application/json;charset=utf-8', 53 | }, 54 | body: MOCK_RESPONSE, 55 | }); 56 | }); 57 | 58 | describe('Errors', () => { 59 | const errorStatuses = [ 60 | { status: 400, message: 'Bad Request' }, 61 | { status: 401, message: 'Unauthorized' }, 62 | { 63 | status: 402, 64 | message: 'Payment Required', 65 | details: { error: { code: 'QuotaExceeded' } }, 66 | }, 67 | { status: 403, message: 'Forbidden' }, 68 | { status: 404, message: 'Not Found' }, 69 | { status: 500, message: 'Internal Server Error' }, 70 | { status: 502, message: 'Bad Gateway' }, 71 | { status: 503, message: 'Service Unavailable' }, 72 | { status: 504, message: 'Gateway Timeout' }, 73 | ]; 74 | errorStatuses.forEach(({ status, message, details }) => { 75 | it(`should handle ${status} errors`, async () => { 76 | nock(host) 77 | [method](`${url}?${searchParams(query)}`) 78 | .reply(status, details || MOCK_ERROR_RESPONSE); 79 | 80 | try { 81 | expect(await axiosTransport()(request)).toEqual( 82 | details || MOCK_ERROR_RESPONSE 83 | ); 84 | } catch (err) { 85 | expect(err).toHaveProperty('message'); 86 | expect(err).toHaveProperty('status'); 87 | expect(err.message).toEqual(message); 88 | expect(err.status).toEqual(status); 89 | if (details?.error) expect(err.details).toEqual(details?.error); 90 | } 91 | }); 92 | }); 93 | }); 94 | }); 95 | -------------------------------------------------------------------------------- /tests/lib/transport/custom.spec.ts: -------------------------------------------------------------------------------- 1 | import nock from 'nock'; 2 | import Chance from 'chance'; 3 | import what3words, { 4 | ClientRequest, 5 | TransportResponse, 6 | HEADERS, 7 | What3wordsService, 8 | ApiVersion, 9 | } from '@/.'; 10 | import superagent from 'superagent'; 11 | 12 | function customTransport( 13 | request: ClientRequest 14 | ): Promise> { 15 | const { 16 | method, 17 | host, 18 | url, 19 | query = {}, 20 | headers = {}, 21 | body = {}, 22 | format, 23 | } = request; 24 | return new Promise(resolve => 25 | superagent[method](`${host}${url}`) 26 | .query({ ...query, format }) 27 | .send(body || {}) 28 | .set(headers) 29 | .end((err, res) => { 30 | if (err || !res) 31 | return resolve({ 32 | status: err.status || 500, 33 | statusText: err.response.text || 'Internal Server Error', 34 | headers: err.headers || {}, 35 | body: err.response.text || null, 36 | }); 37 | const response: TransportResponse = { 38 | status: res.status, 39 | statusText: res.text, 40 | headers: res.headers, 41 | body: res.body, 42 | }; 43 | resolve(response); 44 | }) 45 | ); 46 | } 47 | 48 | const CHANCE = new Chance(); 49 | const MOCK_RESPONSE = { foo: 'bar' }; 50 | const MOCK_ERROR_RESPONSE = 'My custom error response message'; 51 | 52 | describe('Custom Transport', () => { 53 | const query = { 54 | example: 'params', 55 | random: 'value', 56 | }; 57 | let host: string; 58 | let url: string; 59 | let method: 'get' | 'post'; 60 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 61 | let request: any; 62 | let service: What3wordsService; 63 | let api_key: string; 64 | 65 | beforeEach(() => { 66 | method = CHANCE.pickone(['get', 'post']); 67 | host = `http://${CHANCE.domain({})}`; 68 | url = '/foo/bar'; 69 | request = { 70 | method, 71 | host, 72 | url, 73 | query, 74 | headers: { 75 | 'X-Custom-Header': 'my-random-header-value', 76 | ...HEADERS, 77 | }, 78 | body: null, 79 | }; 80 | api_key = CHANCE.string({ length: 8, symbols: false }); 81 | service = what3words(api_key, { host }, { transport: customTransport }); 82 | }); 83 | 84 | afterEach(() => { 85 | nock.cleanAll(); 86 | }); 87 | 88 | it('should make request using custom transport and return 200', async () => { 89 | nock(host, { allowUnmocked: false }) 90 | [method](url) 91 | .query(request.query) 92 | .reply(200, MOCK_RESPONSE, { 93 | 'Content-Type': 'application/json;charset=utf-8', 94 | }); 95 | 96 | expect(await customTransport(request)).toEqual({ 97 | status: 200, 98 | statusText: JSON.stringify(MOCK_RESPONSE), 99 | headers: { 'content-type': 'application/json;charset=utf-8' }, 100 | body: MOCK_RESPONSE, 101 | }); 102 | }); 103 | 104 | it('should make request using custom transport and return 500', async () => { 105 | nock(host, { allowUnmocked: false }) 106 | [method](url) 107 | .query(request.query) 108 | .reply(500, MOCK_ERROR_RESPONSE); 109 | 110 | expect(await customTransport(request)).toEqual({ 111 | status: 500, 112 | statusText: MOCK_ERROR_RESPONSE, 113 | headers: {}, 114 | body: MOCK_ERROR_RESPONSE, 115 | }); 116 | }); 117 | 118 | it('should call autosuggest and return results', async () => { 119 | const input = `${CHANCE.word()}.${CHANCE.word()}.${CHANCE.letter()}`; 120 | const options = { input }; 121 | const mock_response = { suggestions: [] }; 122 | 123 | nock(`${host}`, { 124 | reqheaders: { 125 | 'X-Api-Key': api_key, 126 | ...HEADERS, 127 | }, 128 | allowUnmocked: false, 129 | }) 130 | .get(`/${ApiVersion.Version3}/autosuggest`) 131 | .query({ ...options, key: api_key }) 132 | .reply(200, mock_response, { 133 | 'Content-Type': 'application/json;charset=utf-8', 134 | }); 135 | 136 | expect(await service.autosuggest({ input })).toEqual(mock_response); 137 | }); 138 | 139 | it('should call available-languages and return results', async () => { 140 | const mock_response = { languages: [] }; 141 | nock(`${host}`, { 142 | reqheaders: { 143 | 'X-Api-Key': api_key, 144 | ...HEADERS, 145 | }, 146 | allowUnmocked: false, 147 | }) 148 | .get(`/${ApiVersion.Version3}/available-languages`) 149 | .query({ key: api_key }) 150 | .reply(200, mock_response, { 151 | 'Content-Type': 'application/json;charset=utf-8', 152 | }); 153 | 154 | expect(await service.availableLanguages()).toEqual(mock_response); 155 | }); 156 | 157 | it('should call convert-to-3wa and return json results', async () => { 158 | const mock_response = { languages: [] }; 159 | const options = { 160 | coordinates: { 161 | lat: parseFloat(CHANCE.coordinates().split(', ')[0]), 162 | lng: parseFloat(CHANCE.coordinates().split(', ')[1]), 163 | }, 164 | language: CHANCE.locale(), 165 | }; 166 | 167 | nock(`${host}`, { 168 | reqheaders: { 169 | 'X-Api-Key': api_key, 170 | ...HEADERS, 171 | }, 172 | allowUnmocked: false, 173 | }) 174 | .get(`/${ApiVersion.Version3}/convert-to-3wa`) 175 | .query({ 176 | coordinates: `${options.coordinates.lat},${options.coordinates.lng}`, 177 | language: options.language, 178 | key: api_key, 179 | }) 180 | .reply(200, mock_response, { 181 | 'Content-Type': 'application/json;charset=utf-8', 182 | }); 183 | 184 | expect(await service.convertTo3wa(options)).toEqual(mock_response); 185 | }); 186 | }); 187 | -------------------------------------------------------------------------------- /tests/lib/transport/fetch.browser.spec.ts: -------------------------------------------------------------------------------- 1 | import nock from 'nock'; 2 | import { Chance } from 'chance'; 3 | import { useEffect } from 'react'; 4 | import { renderComponent, setup } from '@utils/setup'; 5 | import { fetchTransport, HEADERS, searchParams } from '@/.'; 6 | 7 | const CHANCE = new Chance(); 8 | 9 | describe('Fetch Transport - Browser ', () => { 10 | const query = { 11 | example: 'params', 12 | random: 'value', 13 | }; 14 | let host: string; 15 | let url: string; 16 | let method: 'get' | 'post'; 17 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 18 | let request: any; 19 | 20 | beforeEach(() => { 21 | method = CHANCE.pickone(['get', 'post']); 22 | host = CHANCE.url(); 23 | url = '/foo/bar'; 24 | request = { 25 | method, 26 | host, 27 | url, 28 | query, 29 | headers: { 30 | 'X-Custom-Header': 'my-random-header-value', 31 | ...HEADERS, 32 | }, 33 | body: null, 34 | }; 35 | setup(); 36 | }); 37 | 38 | afterEach(() => { 39 | nock.cleanAll(); 40 | }); 41 | 42 | it('should make a request given a ClientRequest and return JSON', async () => { 43 | const response = { foo: 'bar' }; 44 | nock(host) 45 | [method](`${url}?${searchParams(request.query)}`) 46 | .reply(200, response, { 47 | 'Content-Type': 'application/json;charset=utf-8', 48 | }); 49 | 50 | await renderComponent(res => { 51 | useEffect(() => { 52 | fetchTransport()(request).then(result => { 53 | window.result = result; 54 | res(null); 55 | }); 56 | }, []); 57 | }); 58 | 59 | expect(window.result).toEqual({ 60 | status: 200, 61 | statusText: 'OK', 62 | body: response, 63 | headers: { 'content-type': 'application/json;charset=utf-8' }, 64 | }); 65 | }); 66 | 67 | it('should make a request given a ClientRequest and return string', async () => { 68 | const response = CHANCE.sentence(); 69 | nock(host) 70 | [method](`${url}?${searchParams(request.query)}`) 71 | .reply(200, response); 72 | 73 | await renderComponent(res => { 74 | useEffect(() => { 75 | fetchTransport()(request).then(result => { 76 | window.result = result; 77 | res(null); 78 | }); 79 | }, []); 80 | }); 81 | 82 | expect(window.result).toEqual({ 83 | status: 200, 84 | statusText: 'OK', 85 | body: response, 86 | headers: {}, 87 | }); 88 | }); 89 | 90 | it('should make a request given a ClientRequest (no query params)', async () => { 91 | const response = CHANCE.sentence(); 92 | delete request.query; 93 | nock(host)[method](url).reply(200, response); 94 | 95 | await renderComponent(res => { 96 | useEffect(() => { 97 | fetchTransport()(request).then(result => { 98 | window.result = result; 99 | res(null); 100 | }); 101 | }, []); 102 | }); 103 | 104 | expect(window.result).toEqual({ 105 | status: 200, 106 | statusText: 'OK', 107 | body: response, 108 | headers: {}, 109 | }); 110 | }); 111 | 112 | describe('Errors', () => { 113 | const errorStatuses = [ 114 | { status: 400, message: 'Bad Request' }, 115 | { status: 401, message: 'Unauthorized' }, 116 | { 117 | status: 402, 118 | message: 'Payment Required', 119 | details: { error: { code: 'QuotaExceeded' } }, 120 | }, 121 | { status: 403, message: 'Forbidden' }, 122 | { status: 404, message: 'Not Found' }, 123 | { status: 500, message: 'Internal Server Error' }, 124 | { status: 502, message: 'Bad Gateway' }, 125 | { status: 503, message: 'Service Unavailable' }, 126 | { status: 504, message: 'Gateway Timeout' }, 127 | ]; 128 | errorStatuses.forEach(({ status, message, details }) => { 129 | it(`should handle ${status} errors`, async () => { 130 | nock(host) 131 | [method](`${url}?${searchParams(query)}`) 132 | .reply(status, details); 133 | 134 | await renderComponent(res => { 135 | useEffect(() => { 136 | fetchTransport()(request).catch(error => { 137 | window.result = { 138 | message: error.message, 139 | status: error.status, 140 | details: error.details, 141 | }; 142 | res(null); 143 | }); 144 | }, []); 145 | }); 146 | 147 | expect(window.result.message).toEqual(message); 148 | expect(window.result.status).toEqual(status); 149 | if (details?.error) 150 | expect(window.result.details).toEqual(details?.error); 151 | }); 152 | }); 153 | }); 154 | }); 155 | -------------------------------------------------------------------------------- /tests/lib/transport/fetch.node.spec.ts: -------------------------------------------------------------------------------- 1 | import nock from 'nock'; 2 | import { Chance } from 'chance'; 3 | import { fetchTransport, HEADERS, searchParams } from '@/.'; 4 | 5 | const CHANCE = new Chance(); 6 | const MOCK_ERROR_RESPONSE = { response: 'My custom error response message' }; 7 | 8 | describe('Fetch Transport - Node', () => { 9 | const query = { 10 | example: 'params', 11 | random: 'value', 12 | }; 13 | let host: string; 14 | let url: string; 15 | let method: 'get' | 'post'; 16 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 17 | let request: any; 18 | 19 | beforeEach(() => { 20 | method = CHANCE.pickone(['get', 'post']); 21 | host = CHANCE.url(); 22 | url = '/foo/bar'; 23 | request = { 24 | method, 25 | host, 26 | url, 27 | query, 28 | headers: { 29 | 'X-Custom-Header': 'my-random-header-value', 30 | ...HEADERS, 31 | }, 32 | body: null, 33 | }; 34 | }); 35 | 36 | afterEach(() => { 37 | nock.cleanAll(); 38 | }); 39 | 40 | it('should make a request given a ClientRequest and return JSON', async () => { 41 | const response = { foo: 'bar' }; 42 | nock(host) 43 | [method](`${url}?${searchParams(request.query)}`) 44 | .reply(200, response, { 45 | 'Content-Type': 'application/json;charset=utf-8', 46 | }); 47 | expect(await fetchTransport()(request)).toEqual({ 48 | status: 200, 49 | statusText: 'OK', 50 | body: response, 51 | headers: { 'content-type': 'application/json;charset=utf-8' }, 52 | }); 53 | }); 54 | 55 | it('should make a request given a ClientRequest and return string', async () => { 56 | const response = CHANCE.sentence(); 57 | nock(host) 58 | [method](`${url}?${searchParams(request.query)}`) 59 | .reply(200, response); 60 | expect(await fetchTransport()(request)).toEqual({ 61 | status: 200, 62 | statusText: 'OK', 63 | body: response, 64 | headers: {}, 65 | }); 66 | }); 67 | 68 | it('should make a request given a ClientRequest (no query params)', async () => { 69 | const response = CHANCE.sentence(); 70 | delete request.query; 71 | nock(host)[method](url).reply(200, response); 72 | expect(await fetchTransport()(request)).toEqual({ 73 | status: 200, 74 | statusText: 'OK', 75 | body: response, 76 | headers: {}, 77 | }); 78 | }); 79 | 80 | describe('Errors', () => { 81 | const errorStatuses = [ 82 | { status: 400, message: 'Bad Request' }, 83 | { status: 401, message: 'Unauthorized' }, 84 | { 85 | status: 402, 86 | message: 'Payment Required', 87 | details: { error: { code: 'QuotaExceeded' } }, 88 | }, 89 | { status: 403, message: 'Forbidden' }, 90 | { status: 404, message: 'Not Found' }, 91 | { status: 500, message: 'Internal Server Error' }, 92 | { status: 502, message: 'Bad Gateway' }, 93 | { status: 503, message: 'Service Unavailable' }, 94 | { status: 504, message: 'Gateway Timeout' }, 95 | ]; 96 | errorStatuses.forEach(({ status, message, details }) => { 97 | it(`should handle ${status} errors`, async () => { 98 | nock(host) 99 | [method](`${url}?${searchParams(query)}`) 100 | .reply(status, details || MOCK_ERROR_RESPONSE); 101 | try { 102 | expect(await fetchTransport()(request)).toEqual( 103 | details || MOCK_ERROR_RESPONSE 104 | ); 105 | } catch (err) { 106 | expect(err).toHaveProperty('message'); 107 | expect(err).toHaveProperty('status'); 108 | expect(err.message).toEqual(message); 109 | expect(err.status).toEqual(status); 110 | if (details?.error) expect(err.details).toEqual(details?.error); 111 | } 112 | }); 113 | }); 114 | }); 115 | }); 116 | -------------------------------------------------------------------------------- /tests/lib/validation.spec.ts: -------------------------------------------------------------------------------- 1 | import { Chance } from 'chance'; 2 | import { valid3wa, validLanguage } from '@/.'; 3 | import { languages } from '@/lib/languages/language-codes'; 4 | 5 | const CHANCE = new Chance(); 6 | 7 | describe('validation', () => { 8 | describe('valid3wa', () => { 9 | it('should return true if valid 3wa is provided', () => { 10 | const words = `${CHANCE.word()}.${CHANCE.word()}.${CHANCE.letter()}`; 11 | expect(valid3wa(words)).toBeTruthy(); 12 | }); 13 | it.skip('should return true if valid 3wa is provided', () => { 14 | const words = `${CHANCE.word()}.${CHANCE.word()}.${CHANCE.word()}`; 15 | expect(valid3wa(words)).toBeTruthy(); 16 | }); 17 | it('should return false if invalid 3wa is provided', () => { 18 | const words = `${CHANCE.word()}`; 19 | expect(valid3wa(words)).toBeFalsy(); 20 | }); 21 | it('should return false if invalid 3wa is provided', () => { 22 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 23 | const words = CHANCE.bool() as any; 24 | expect(valid3wa(words)).toBeFalsy(); 25 | }); 26 | }); 27 | describe('validLanguage', () => { 28 | it('should return true if valid language code is provided', () => { 29 | const languageCode = CHANCE.pickone(languages); 30 | expect(validLanguage(languageCode)).toBeTruthy(); 31 | }); 32 | it('should return false if invalid language code is provided', () => { 33 | const languageCode = 'xx'; 34 | expect(validLanguage(languageCode)).toBeFalsy(); 35 | }); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /tests/service.spec.ts: -------------------------------------------------------------------------------- 1 | import nock from 'nock'; 2 | import { Chance } from 'chance'; 3 | import what3words, { 4 | ApiClientConfiguration, 5 | ApiVersion, 6 | searchParams, 7 | axiosTransport, 8 | } from '@/.'; 9 | import { What3wordsService } from '@/service'; 10 | 11 | const CHANCE = new Chance(); 12 | const MOCK_AUTOSUGGEST_RESPONSE = { suggestions: [] }; 13 | const MOCK_AVAILABLE_LANGUAGES_RESPONSE = { languages: [] }; 14 | const MOCK_C23WA_RESPONSE = {}; 15 | const MOCK_C2C_RESPONSE = {}; 16 | const MOCK_GRID_SECTION_RESPONSE = {}; 17 | 18 | describe('what3words', () => { 19 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 20 | let service: What3wordsService; 21 | let apiVersion: ApiVersion; 22 | let apiKey: string; 23 | let config: ApiClientConfiguration; 24 | 25 | beforeEach(() => { 26 | apiKey = CHANCE.string({ length: 8 }); 27 | apiVersion = CHANCE.pickone([ 28 | ApiVersion.Version1, 29 | ApiVersion.Version2, 30 | ApiVersion.Version3, 31 | ]); 32 | config = { 33 | host: CHANCE.url(), 34 | apiVersion, 35 | headers: {}, 36 | }; 37 | }); 38 | 39 | describe('Service', () => { 40 | beforeEach(() => { 41 | service = what3words(apiKey, config); 42 | }); 43 | 44 | it('should instantiate the what3words service instance', () => { 45 | expect(service).toHaveProperty('clients'); 46 | expect(service).toHaveProperty('setApiKey'); 47 | expect(service).toHaveProperty('setConfig'); 48 | expect(service).toHaveProperty('autosuggest'); 49 | expect(service).toHaveProperty('autosuggestSelection'); 50 | expect(service).toHaveProperty('availableLanguages'); 51 | expect(service).toHaveProperty('convertTo3wa'); 52 | expect(service).toHaveProperty('convertToCoordinates'); 53 | expect(service).toHaveProperty('gridSection'); 54 | expectTypeOf(service.clients).toBeObject(); 55 | expect(service.clients).toHaveProperty('autosuggest'); 56 | expect(service.clients).toHaveProperty('availableLanguages'); 57 | expect(service.clients).toHaveProperty('convertTo3wa'); 58 | expect(service.clients).toHaveProperty('convertToCoordinates'); 59 | expect(service.clients).toHaveProperty('gridSection'); 60 | expectTypeOf(service.setApiKey).toBeFunction(); 61 | expectTypeOf(service.setConfig).toBeFunction(); 62 | expectTypeOf(service.autosuggest).toBeFunction(); 63 | expectTypeOf(service.autosuggestSelection).toBeFunction(); 64 | expectTypeOf(service.availableLanguages).toBeFunction(); 65 | expectTypeOf(service.convertTo3wa).toBeFunction(); 66 | expectTypeOf(service.convertToCoordinates).toBeFunction(); 67 | expectTypeOf(service.gridSection).toBeFunction(); 68 | }); 69 | it('should set the api key for all clients', () => { 70 | const newApiKey = CHANCE.string({ length: 8 }); 71 | 72 | expect(service.clients.autosuggest.apiKey()).toEqual(apiKey); 73 | expect(service.clients.availableLanguages.apiKey()).toEqual(apiKey); 74 | expect(service.clients.convertTo3wa.apiKey()).toEqual(apiKey); 75 | expect(service.clients.convertToCoordinates.apiKey()).toEqual(apiKey); 76 | expect(service.clients.gridSection.apiKey()).toEqual(apiKey); 77 | 78 | service.setApiKey(newApiKey); 79 | 80 | expect(service.clients.autosuggest.apiKey()).toEqual(newApiKey); 81 | expect(service.clients.availableLanguages.apiKey()).toEqual(newApiKey); 82 | expect(service.clients.convertTo3wa.apiKey()).toEqual(newApiKey); 83 | expect(service.clients.convertToCoordinates.apiKey()).toEqual(newApiKey); 84 | expect(service.clients.gridSection.apiKey()).toEqual(newApiKey); 85 | }); 86 | it('should set the config for all clients', () => { 87 | const newConfig = { 88 | host: CHANCE.url(), 89 | apiVersion: CHANCE.pickone( 90 | [ 91 | ApiVersion.Version1, 92 | ApiVersion.Version2, 93 | ApiVersion.Version3, 94 | ].filter(v => v !== apiVersion) 95 | ), 96 | headers: {}, 97 | }; 98 | 99 | expect(service.clients.autosuggest.config()).toEqual(config); 100 | expect(service.clients.availableLanguages.config()).toEqual(config); 101 | expect(service.clients.convertTo3wa.config()).toEqual(config); 102 | expect(service.clients.convertToCoordinates.config()).toEqual(config); 103 | expect(service.clients.gridSection.config()).toEqual(config); 104 | 105 | service.setConfig(newConfig); 106 | 107 | expect(service.clients.autosuggest.config()).toEqual(newConfig); 108 | expect(service.clients.availableLanguages.config()).toEqual(newConfig); 109 | expect(service.clients.convertTo3wa.config()).toEqual(newConfig); 110 | expect(service.clients.convertToCoordinates.config()).toEqual(newConfig); 111 | expect(service.clients.gridSection.config()).toEqual(newConfig); 112 | }); 113 | }); 114 | 115 | describe('Axios Transport', () => { 116 | let input: string; 117 | beforeEach(() => { 118 | service = what3words(apiKey, config, { transport: axiosTransport() }); 119 | input = CHANCE.string(); 120 | nock(`${config.host!}/${config.apiVersion}`) 121 | .get(`/autosuggest?${searchParams({ input, key: apiKey })}`) 122 | .reply(200, MOCK_AUTOSUGGEST_RESPONSE) 123 | .get('/available-languages') 124 | .reply(200, MOCK_AVAILABLE_LANGUAGES_RESPONSE) 125 | .get('/convert-to-3wa') 126 | .reply(200, MOCK_C2C_RESPONSE) 127 | .get('/convert-to-coordinates') 128 | .reply(200, MOCK_C23WA_RESPONSE) 129 | .get('/grid-section') 130 | .reply(200, MOCK_GRID_SECTION_RESPONSE); 131 | }); 132 | 133 | afterEach(() => { 134 | nock.cleanAll(); 135 | }); 136 | 137 | it('should use the axios transport', () => { 138 | expect(service.clients.autosuggest['transport'].name).toEqual( 139 | 'axiosTransport2' 140 | ); 141 | expect(service.clients.availableLanguages['transport'].name).toEqual( 142 | 'axiosTransport2' 143 | ); 144 | expect(service.clients.convertTo3wa['transport'].name).toEqual( 145 | 'axiosTransport2' 146 | ); 147 | expect(service.clients.convertToCoordinates['transport'].name).toEqual( 148 | 'axiosTransport2' 149 | ); 150 | expect(service.clients.gridSection['transport'].name).toEqual( 151 | 'axiosTransport2' 152 | ); 153 | }); 154 | it('should use the axios transport to make autosuggest requests', async () => { 155 | const response = await service.autosuggest({ input }); 156 | expect(response).toEqual(MOCK_AUTOSUGGEST_RESPONSE); 157 | }); 158 | }); 159 | }); 160 | -------------------------------------------------------------------------------- /tests/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "useUnknownInCatchVariables": false, 5 | "paths": { 6 | "@/*": ["*"], 7 | "@utils/*": ["../tests/utils/*"] 8 | }, 9 | "types": ["vitest/globals", "node"] 10 | }, 11 | "include": ["**/*.spec.ts"] 12 | } 13 | -------------------------------------------------------------------------------- /tests/utils/fixtures.ts: -------------------------------------------------------------------------------- 1 | import { Chance } from 'chance'; 2 | 3 | const CHANCE = new Chance(); 4 | 5 | export function generateAutosuggestSuggestion() { 6 | return { 7 | words: `${CHANCE.word()}.${CHANCE.word()}.${CHANCE.word()}`, 8 | country: CHANCE.country(), 9 | nearestPlace: CHANCE.address(), 10 | language: CHANCE.locale(), 11 | rank: 2, 12 | distanceToFocusKm: CHANCE.natural(), 13 | }; 14 | } 15 | 16 | export function generateCoordinate() { 17 | return { lat: CHANCE.latitude(), lng: CHANCE.longitude() }; 18 | } 19 | -------------------------------------------------------------------------------- /tests/utils/setup.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { JSDOM } from 'jsdom'; 3 | import { render } from '@testing-library/react'; 4 | 5 | declare global { 6 | interface Window { 7 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 8 | result?: any; 9 | } 10 | } 11 | 12 | export function setup(html?: string) { 13 | const HTML = html || ''; 14 | const dom = new JSDOM(HTML, { 15 | resources: 'usable', 16 | runScripts: 'dangerously', 17 | }); 18 | 19 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 20 | global.window = dom.window as any; 21 | global.document = dom.window.document; 22 | global.navigator = dom.window.navigator; 23 | } 24 | 25 | export function renderComponent( 26 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 27 | callback?: (res: (value: any) => void, rej: (reason?: any) => void) => void 28 | ) { 29 | return new Promise((res, rej) => { 30 | const Component = () => { 31 | callback ? callback(res, rej) : res(null); 32 | return null; 33 | }; 34 | return render(); 35 | }); 36 | } 37 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./node_modules/gts/tsconfig-google.json", 3 | "compilerOptions": { 4 | "baseUrl": "src/", 5 | "declaration": true, 6 | "esModuleInterop": true, 7 | "jsx": "react", 8 | "lib": [ 9 | "dom" 10 | ], 11 | "moduleResolution": "node", 12 | "module": "commonjs", 13 | "outDir": "dist", 14 | "resolveJsonModule": true, 15 | "skipLibCheck": true, 16 | "strict": true, 17 | "target": "es5" 18 | }, 19 | "include": [ 20 | "src/**/*" 21 | ], 22 | "exclude": ["test"] 23 | } 24 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config'; 2 | import commonjs from 'vite-plugin-commonjs'; 3 | 4 | const exclude = [ 5 | 'test/**', 6 | 'tests/mocks/**', 7 | 'docs/**', 8 | '.prettierrc.js', 9 | 'coverage/**', 10 | ]; 11 | 12 | const path = (path: string) => new URL(path, import.meta.url).pathname; 13 | 14 | export default defineConfig({ 15 | plugins: [commonjs()], 16 | test: { 17 | reporters: ['junit', 'verbose'], 18 | exclude, 19 | include: ['tests/**/*.spec.ts'], 20 | globals: true, 21 | coverage: { 22 | exclude, 23 | reportsDirectory: path('./coverage/'), 24 | }, 25 | alias: { 26 | '@/': path('./src/'), 27 | '@utils/': path('./tests/utils/'), 28 | }, 29 | outputFile: { 30 | junit: path('./coverage/junit-report.xml'), 31 | }, 32 | }, 33 | }); 34 | --------------------------------------------------------------------------------