diff --git a/.env b/.env index 476f4fd..56f3187 100644 --- a/.env +++ b/.env @@ -1,2 +1 @@ -IS_PRODUCTION=true -FOO=bar \ No newline at end of file +IS_PRODUCTION=true \ No newline at end of file diff --git a/.env.development b/.env.development index 92a32ed..0efcec4 100644 --- a/.env.development +++ b/.env.development @@ -1,2 +1 @@ -IS_PRODUCTION=false -FOO=bar \ No newline at end of file +IS_PRODUCTION=false \ No newline at end of file diff --git a/.eslintignore b/.eslintignore index 6bd947b..76add87 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,31 +1,2 @@ -{ - "root": true, - "parser": "@typescript-eslint/parser", - "parserOptions": { - "project": "./tsconfig.json", - "sourceType": "module" - }, - "plugins": ["@typescript-eslint"], - "extends": [ - "eslint:recommended", - "plugin:@typescript-eslint/eslint-recommended", - "plugin:@typescript-eslint/recommended", - "plugin:@typescript-eslint/recommended-requiring-type-checking", - "plugin:prettier/recommended" - ], - "rules": { - "max-len": ["error", {"code": 120, "ignoreComments": true, "ignoreTemplateLiterals": true}], - "no-console": "error", - "semi": [ "error", "always" ], - "sort-imports": [ - "error", - { - "ignoreCase": false, - "ignoreDeclarationSort": false, - "ignoreMemberSort": false, - "memberSyntaxSortOrder": ["none", "all", "multiple", "single"], - "allowSeparatedGroups": false - } - ] - } -} \ No newline at end of file +node_modules +dist \ No newline at end of file diff --git a/.eslintrc b/.eslintrc index 6bd947b..d69eaef 100644 --- a/.eslintrc +++ b/.eslintrc @@ -15,6 +15,7 @@ ], "rules": { "max-len": ["error", {"code": 120, "ignoreComments": true, "ignoreTemplateLiterals": true}], + "@typescript-eslint/no-unsafe-return": "off", "no-console": "error", "semi": [ "error", "always" ], "sort-imports": [ diff --git a/.gitignore b/.gitignore index 62d067b..3003f82 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ node_modules dist -.parcel-cache \ No newline at end of file +.parcel-cache + +*.iml \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index fc8a71a..bc90323 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,9 @@ "name": "typescript-parcel-base", "version": "0.0.1", "devDependencies": { + "@parcel/packager-ts": "^2.8.3", "@parcel/transformer-typescript-tsc": "^2.8.3", + "@parcel/transformer-typescript-types": "^2.8.3", "@types/node": "^18.11.18", "@typescript-eslint/eslint-plugin": "^5.48.2", "@typescript-eslint/parser": "^5.48.2", @@ -1018,6 +1020,23 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/@parcel/packager-ts": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/@parcel/packager-ts/-/packager-ts-2.8.3.tgz", + "integrity": "sha512-8JooYHjKntHnQywLT7LAnfoGiAQ1fUu0N2DtuM0PxpgQqYJ4KE9TZS+SZq7hpe24cZkD0A4A+1kBlYAyvuanrg==", + "dev": true, + "dependencies": { + "@parcel/plugin": "2.8.3" + }, + "engines": { + "node": ">= 12.0.0", + "parcel": "^2.8.3" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, "node_modules/@parcel/plugin": { "version": "2.8.3", "resolved": "https://registry.npmjs.org/@parcel/plugin/-/plugin-2.8.3.tgz", @@ -1447,6 +1466,31 @@ "typescript": ">=3.0.0" } }, + "node_modules/@parcel/transformer-typescript-types": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/@parcel/transformer-typescript-types/-/transformer-typescript-types-2.8.3.tgz", + "integrity": "sha512-zjsJsgecjw4X1nt5R7A61uWwzwCce0usKKPqnE5tQpYtF4FfK5X69r0l5JLovlyaT2uwoe+hvhu2AELA0kKRQA==", + "dev": true, + "dependencies": { + "@parcel/diagnostic": "2.8.3", + "@parcel/plugin": "2.8.3", + "@parcel/source-map": "^2.1.1", + "@parcel/ts-utils": "2.8.3", + "@parcel/utils": "2.8.3", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">= 12.0.0", + "parcel": "^2.8.3" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "peerDependencies": { + "typescript": ">=3.0.0" + } + }, "node_modules/@parcel/ts-utils": { "version": "2.8.3", "resolved": "https://registry.npmjs.org/@parcel/ts-utils/-/ts-utils-2.8.3.tgz", @@ -5050,6 +5094,15 @@ "posthtml": "^0.16.4" } }, + "@parcel/packager-ts": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/@parcel/packager-ts/-/packager-ts-2.8.3.tgz", + "integrity": "sha512-8JooYHjKntHnQywLT7LAnfoGiAQ1fUu0N2DtuM0PxpgQqYJ4KE9TZS+SZq7hpe24cZkD0A4A+1kBlYAyvuanrg==", + "dev": true, + "requires": { + "@parcel/plugin": "2.8.3" + } + }, "@parcel/plugin": { "version": "2.8.3", "resolved": "https://registry.npmjs.org/@parcel/plugin/-/plugin-2.8.3.tgz", @@ -5312,6 +5365,20 @@ "@parcel/ts-utils": "2.8.3" } }, + "@parcel/transformer-typescript-types": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/@parcel/transformer-typescript-types/-/transformer-typescript-types-2.8.3.tgz", + "integrity": "sha512-zjsJsgecjw4X1nt5R7A61uWwzwCce0usKKPqnE5tQpYtF4FfK5X69r0l5JLovlyaT2uwoe+hvhu2AELA0kKRQA==", + "dev": true, + "requires": { + "@parcel/diagnostic": "2.8.3", + "@parcel/plugin": "2.8.3", + "@parcel/source-map": "^2.1.1", + "@parcel/ts-utils": "2.8.3", + "@parcel/utils": "2.8.3", + "nullthrows": "^1.1.1" + } + }, "@parcel/ts-utils": { "version": "2.8.3", "resolved": "https://registry.npmjs.org/@parcel/ts-utils/-/ts-utils-2.8.3.tgz", diff --git a/package.json b/package.json index 24f3e52..2445929 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,11 @@ { "name": "typescript-parcel-base", "version": "0.0.1", + "source": "src/fetch.service.ts", + "module": "dist/module.js", + "types": "dist/types.d.ts", "scripts": { - "dev": "parcel src/index.html", - "prod": "parcel build src/index.html", + "build": "parcel build", "lint": "eslint --ext .ts src/", "lint:fix": "eslint --ext .ts,.tsx src/ --fix" }, @@ -11,7 +13,9 @@ "lint" ], "devDependencies": { + "@parcel/packager-ts": "^2.8.3", "@parcel/transformer-typescript-tsc": "^2.8.3", + "@parcel/transformer-typescript-types": "^2.8.3", "@types/node": "^18.11.18", "@typescript-eslint/eslint-plugin": "^5.48.2", "@typescript-eslint/parser": "^5.48.2", diff --git a/src/config.ts b/src/config.ts index f8eafb4..9c8abbf 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,4 +1,3 @@ export const Config = { - isProduction: process.env.IS_PRODUCTION === 'true', - foo: process.env.FOO + isProduction: process.env.IS_PRODUCTION === 'true' }; diff --git a/src/debug.log.ts b/src/debug.log.ts new file mode 100644 index 0000000..2c1a614 --- /dev/null +++ b/src/debug.log.ts @@ -0,0 +1,8 @@ +import { Config } from './config'; + +export const debugLog = (...args: any[]) => { + if (!Config.isProduction) { + //eslint-disable-next-line no-console, @typescript-eslint/no-unsafe-argument + console.log(...args); + } +}; diff --git a/src/fetch.model.ts b/src/fetch.model.ts new file mode 100644 index 0000000..3ba5bc8 --- /dev/null +++ b/src/fetch.model.ts @@ -0,0 +1,56 @@ +/* + * MIT License + * Copyright (c) 2023 Michal Szczepanski + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +export type FetchResponseType = 'JSON' | 'TEXT' | 'BLOB'; + +export type FetchRequestMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD'; + +export type FetchHeaders = { [key: string]: string }; + +export interface FetchRefreshToken { + url: string; + key: string; + value: string; + method: FetchRequestMethod; +} + +export interface FetchAuthenticate { + refreshToken: FetchRefreshToken; + successCallback?: (data: any, headers?: FetchHeaders) => void; + errorCallback?: (error: Error) => void; +} + +export interface FetchParams { + method?: FetchRequestMethod; + type?: FetchResponseType; + data?: T; + timeout?: number; + headers?: FetchHeaders; +} + +export interface FetchResponse { + url: string; + ok: boolean; + status: number; + res: T; + type: FetchResponseType; +} diff --git a/src/fetch.service.ts b/src/fetch.service.ts new file mode 100644 index 0000000..26974c8 --- /dev/null +++ b/src/fetch.service.ts @@ -0,0 +1,139 @@ +/* + * MIT License + * Copyright (c) 2023 Michal Szczepanski + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +import { FetchAuthenticate, FetchParams, FetchResponse, FetchResponseType } from './fetch.model'; +import { debugLog } from './debug.log'; + +export class FetchService { + private static assignDefaultParams(params: FetchParams) { + if (!params.method) params.method = 'GET'; + if (!params.type) params.type = 'JSON'; + if (!params.timeout) params.timeout = 15000; + } + + static async fetch(url: string, params: FetchParams, auth?: FetchAuthenticate): Promise> { + this.assignDefaultParams(params); + return new Promise((resolve, reject) => { + if (auth) { + this.refetch(url, params, auth) + .then((res) => resolve(res)) + .catch((e) => reject(e)); + } else { + this._fetch(url, params, resolve, reject); + } + }); + } + + private static _fetch = ( + url: string, + params: FetchParams, + resolve: (value: any, ok: boolean, status: number) => void, + reject: (error: Error) => void + ) => { + const headers = this.applyDefaultHeaders(params.headers); + // timeout + const timeout = setTimeout(() => { + debugLog('FetchService->timeout', url); + reject(new Error(`Timeout ${url}`)); + }, params.timeout); + fetch(url, { + method: params.method, + headers + }) + .then((req) => { + this.getResponse(req, params.type!) + .then((res) => { + clearTimeout(timeout); + resolve(res, req.ok, req.status); + }) + .catch((e: Error) => reject(e)); + }) + .catch((e: Error) => reject(e)); + }; + + private static refetch = async ( + url: string, + params: FetchParams, + auth: FetchAuthenticate + ): Promise> => { + return new Promise((resolve, reject) => { + try { + auth.refreshToken; + this._fetch( + url, + params, + (res, ok) => { + //eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + if (!ok && res[auth.refreshToken.key] === auth.refreshToken.value) { + this.refreshToken(params, auth) + .then(() => { + this._fetch(url, params, resolve, reject); + }) + .catch((e) => reject(e)); + } + }, + (error) => { + reject(error); + } + ); + } catch (e) { + debugLog('Error FetchService->refetch', e); + } + }); + }; + + private static refreshToken(params: FetchParams, auth: FetchAuthenticate): Promise { + debugLog('FetchService->refreshToken', auth.refreshToken.url); + const headers = this.applyDefaultHeaders(params.headers); + this._fetch( + auth.refreshToken.url, + { + method: auth.refreshToken.method, + headers, + timeout: params.timeout + }, + (value, ok, status) => { + if (ok && auth.successCallback) auth.successCallback(value, params.headers); + }, + (error) => { + if (auth.errorCallback) auth.errorCallback(error); + } + ); + } + + private static applyDefaultHeaders(headers?: { [key: string]: string }): { [key: string]: string } { + if (!headers) headers = {}; + Object.assign(headers, { + 'Content-Type': 'application/json' + }); + return headers; + } + + private static getResponse = async (req: Response, type: FetchResponseType): Promise => { + if (type === 'BLOB') { + return await req.blob(); + } else if (type === 'JSON') { + return await req.json(); + } + return await req.text(); + }; +} diff --git a/src/index.html b/src/index.html deleted file mode 100644 index 6086181..0000000 --- a/src/index.html +++ /dev/null @@ -1,10 +0,0 @@ - - - - - Hello World - - - - - \ No newline at end of file diff --git a/src/index.ts b/src/index.ts deleted file mode 100644 index 825e89a..0000000 --- a/src/index.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Config } from './config'; -const h1 = document.createElement('h1'); -document.body.appendChild(h1); -h1.innerText = `is production ${Config.isProduction.toString()}, foo value from .env: ${Config.foo || ''}`; - -// Hot reloading -if (!Config.isProduction) { - const ws = new WebSocket('ws://localhost:1234'); - ws.onmessage = () => { - window.location.reload(); - }; -}