From eb989eae8e662473c212e295cb46dee18b961cd5 Mon Sep 17 00:00:00 2001 From: Tilman <147151040+TilmanHaupt@users.noreply.github.com> Date: Fri, 8 Nov 2024 12:03:35 +0100 Subject: [PATCH] refactor(url-state-provider): refactor to typescript (#587) * refactor(url-state-provider): refactor to tss * chore(url-state-provider): bump version * chore(url-state-provider): reducing any and exporting types --------- Co-authored-by: Andreas Pfau --- .changeset/grumpy-spies-hunt.md | 5 + package-lock.json | 8 +- packages/url-state-provider/eslint.config.mjs | 17 +++- packages/url-state-provider/package.json | 6 +- .../src/{index.test.js => index.test.ts} | 63 ++++-------- .../src/{index.js => index.ts} | 95 +++++++++++-------- ...{superstate.test.js => superstate.test.ts} | 77 ++++++++------- .../src/{superstate.js => superstate.ts} | 72 +++++++------- packages/url-state-provider/tsconfig.json | 23 +++++ packages/url-state-provider/types/global.d.ts | 12 +++ packages/url-state-provider/vite.config.ts | 10 +- 11 files changed, 229 insertions(+), 159 deletions(-) create mode 100644 .changeset/grumpy-spies-hunt.md rename packages/url-state-provider/src/{index.test.js => index.test.ts} (84%) rename packages/url-state-provider/src/{index.js => index.ts} (77%) rename packages/url-state-provider/src/{superstate.test.js => superstate.test.ts} (89%) rename packages/url-state-provider/src/{superstate.js => superstate.ts} (80%) create mode 100644 packages/url-state-provider/tsconfig.json create mode 100644 packages/url-state-provider/types/global.d.ts diff --git a/.changeset/grumpy-spies-hunt.md b/.changeset/grumpy-spies-hunt.md new file mode 100644 index 000000000..20d695219 --- /dev/null +++ b/.changeset/grumpy-spies-hunt.md @@ -0,0 +1,5 @@ +--- +"@cloudoperators/juno-url-state-provider": minor +--- + +Migrate to url-state-provider to typescript. diff --git a/package-lock.json b/package-lock.json index 8c987dbbb..b5855438c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -681,7 +681,7 @@ }, "apps/greenhouse": { "name": "@cloudoperators/juno-app-greenhouse", - "version": "0.3.2", + "version": "0.3.3", "license": "Apache-2.0", "dependencies": { "@cloudoperators/juno-app-doop": "*", @@ -826,7 +826,7 @@ }, "apps/heureka": { "name": "@cloudoperators/juno-app-heureka", - "version": "2.9.1", + "version": "2.9.2", "license": "Apache-2.0", "dependencies": { "@cloudoperators/juno-communicator": "*", @@ -29600,7 +29600,7 @@ }, "packages/ui-components": { "name": "@cloudoperators/juno-ui-components", - "version": "2.27.0", + "version": "2.28.0", "license": "Apache-2.0", "devDependencies": { "@babel/plugin-transform-parameters": "^7.22.15", @@ -29821,7 +29821,9 @@ "jsdom": "^18.0.0", "juri-cutlery": "^1.0.0", "lz-string": "^1.4.4", + "typescript": "^5.5.4", "vite": "^5.4.8", + "vite-plugin-dts": "^4.0.3", "vitest": "^2.1.1" }, "engines": { diff --git a/packages/url-state-provider/eslint.config.mjs b/packages/url-state-provider/eslint.config.mjs index e830a2b22..e3e8060b0 100644 --- a/packages/url-state-provider/eslint.config.mjs +++ b/packages/url-state-provider/eslint.config.mjs @@ -3,14 +3,25 @@ * SPDX-License-Identifier: Apache-2.0 */ -import junoConfigs from "@cloudoperators/juno-config/eslint/juno.mjs" +import junoConfigs from "@cloudoperators/juno-config/eslint/juno-typescript.mjs" export default [ ...junoConfigs, { - files: ["**/*.test.js"], + files: ["**/*.ts"], languageOptions: { - sourceType: "module", + parserOptions: { + project: ["./tsconfig.json"], // Ensure this points to your tsconfig.json + }, }, + // TODO: We need to make all of this checks on again, step by step + rules: { + "@typescript-eslint/no-unsafe-assignment": "off", + "@typescript-eslint/no-unsafe-call": "off", + "@typescript-eslint/no-unsafe-argument": "off", + "@typescript-eslint/no-unsafe-member-access": "off", + "@typescript-eslint/no-explicit-any": "off", + }, + ignores: ["vitest.config.ts", "vite.config.ts"], }, ] diff --git a/packages/url-state-provider/package.json b/packages/url-state-provider/package.json index c58ebad21..3ba048868 100644 --- a/packages/url-state-provider/package.json +++ b/packages/url-state-provider/package.json @@ -14,6 +14,7 @@ "source": "src/index.js", "main": "build/index.js", "module": "build/index.js", + "types": "build/index.d.ts", "files": [ "build" ], @@ -27,6 +28,7 @@ "dev": "vite", "lint": "eslint", "test": "vitest run", + "typecheck": "tsc --noEmit", "clean": "rm -rf build && rm -rf node_modules && rm -rf .turbo", "clean:cache": "rm -rf .turbo" }, @@ -35,8 +37,10 @@ "jsdom": "^18.0.0", "juri-cutlery": "^1.0.0", "lz-string": "^1.4.4", + "typescript": "^5.5.4", "vite": "^5.4.8", - "vitest": "^2.1.1" + "vitest": "^2.1.1", + "vite-plugin-dts": "^4.0.3" }, "babel": { "presets": [ diff --git a/packages/url-state-provider/src/index.test.js b/packages/url-state-provider/src/index.test.ts similarity index 84% rename from packages/url-state-provider/src/index.test.js rename to packages/url-state-provider/src/index.test.ts index 27304abf4..feaae6d40 100644 --- a/packages/url-state-provider/src/index.test.js +++ b/packages/url-state-provider/src/index.test.ts @@ -9,6 +9,7 @@ * @jest-environment jsdom */ import * as provider from "./index" +import { describe, it, vi, expect } from "vitest" Object.defineProperty(window, "location", { value: { @@ -110,7 +111,7 @@ describe("currentState", () => { }) it("should modify state search param in URL", () => { - var newState = provider.encode({ + const newState = provider.encode({ consumer1: { p: "/about", o: { tab: 1 } }, consumer2: { p: "/items/10", o: { tab: 2 } }, }) @@ -119,7 +120,7 @@ describe("currentState", () => { }) it("should not modify other search params in URL", () => { - var newState = provider.encode({ + const newState = provider.encode({ consumer1: { p: "/about", o: { tab: 1 } }, consumer2: { p: "/items/10", o: { tab: 2 } }, }) @@ -136,7 +137,7 @@ describe("currentState", () => { }) it("should add ?", () => { - var newState = provider.encode({ + const newState = provider.encode({ consumer1: { p: "/about" }, }) @@ -145,12 +146,12 @@ describe("currentState", () => { }) it("support 50 states with a path length of 1000 characters", () => { - let stateCount = 50 - let pathLength = 1000 - let states = {} + const stateCount = 50 + const pathLength = 1000 + const states: { [key: string]: any } = {} for (let i = 0; i < stateCount; i++) { - var key = "consumer" + i - var state = { + const key = "consumer" + i + const state = { p: new Array(pathLength + 1).join("x"), o: { tab: 2, option1: "test", option2: "test " }, } @@ -158,11 +159,11 @@ describe("currentState", () => { states[key] = state } - var urlState = new URL(window.location.href).searchParams.get("__s") + const urlState = new URL(window.location.href).searchParams.get("__s") // The browsers allow 2040 characters long URLs. // If we stay below 1500 characters, we can still support 50 different states with // a length of up to 1000 characters per path. - expect(urlState.length < 1500).toEqual(true) + expect(urlState && urlState.length < 1500).toEqual(true) }) }) @@ -194,7 +195,7 @@ describe("currentState", () => { }) it("should modify state search param in URL", () => { - var newState = provider.encode({ + const newState = provider.encode({ consumer1: { p: "/about", o: { tab: 1 } }, consumer2: { p: "/items/10", o: { tab: 2 } }, }) @@ -203,7 +204,7 @@ describe("currentState", () => { }) it("should not modify other search params in URL", () => { - var newState = provider.encode({ + const newState = provider.encode({ consumer1: { p: "/about", o: { tab: 1 } }, consumer2: { p: "/items/10", o: { tab: 2 } }, }) @@ -213,13 +214,13 @@ describe("currentState", () => { describe("search params are empty", () => { beforeAll(() => { vi.resetModules() - delete window["__url_state_provider"] + delete window.__url_state_provider window.location.href = "http://localhost" provider.replace("consumer1", { p: "/about" }) }) it("should add ?", () => { - var newState = provider.encode({ + const newState = provider.encode({ consumer1: { p: "/about" }, }) @@ -229,7 +230,7 @@ describe("currentState", () => { }) describe("addOnChangeListener", () => { - var listener + let listener: () => void beforeAll(() => { vi.resetModules() listener = vi.fn(() => null) @@ -261,7 +262,7 @@ describe("currentState", () => { }) describe("removeOnChangeListener", () => { - var listener + let listener: () => void beforeAll(() => { vi.resetModules() listener = vi.fn(() => null) @@ -275,34 +276,4 @@ describe("currentState", () => { expect(listener).not.toHaveBeenCalled() }) }) - - describe("registerConsumer", () => { - it("should be a function", () => { - expect(typeof provider.registerConsumer === "function").toEqual(true) - }) - - describe("consumer properties", () => { - var consumer = provider.registerConsumer("key1") - - it("should return an object", () => { - expect(typeof consumer === "object").toEqual(true) - }) - - it("responds to currentState", () => { - expect(typeof consumer.currentState === "function").toEqual(true) - }) - - it("responds to push", () => { - expect(typeof consumer.push === "function").toEqual(true) - }) - - it("responds to replace", () => { - expect(typeof consumer.replace === "function").toEqual(true) - }) - - it("responds to onChange", () => { - expect(typeof consumer.onChange === "function").toEqual(true) - }) - }) - }) }) diff --git a/packages/url-state-provider/src/index.js b/packages/url-state-provider/src/index.ts similarity index 77% rename from packages/url-state-provider/src/index.js rename to packages/url-state-provider/src/index.ts index ad94d1180..f14e3e26d 100644 --- a/packages/url-state-provider/src/index.js +++ b/packages/url-state-provider/src/index.ts @@ -6,6 +6,19 @@ import LZString from "lz-string" import superstate from "./superstate" +// Define the options type +type EncodeOptions = { + mode?: "humanize" | "auto" +} +type UpdateStateOptions = { + merge?: boolean +} +type HistoryOptions = { + state?: boolean + title?: string + replace?: boolean +} + const jsonURLSerializer = superstate() const SEARCH_KEY = "__s" @@ -13,8 +26,8 @@ const regex = new RegExp(SEARCH_KEY + "=([^&]+)") /** * Variable where to host listeners for history changes */ -var onHistoryChangeListeners = {} -var onGlobalChangeListeners = [] +const onHistoryChangeListeners: { [key: string]: any } = {} +const onGlobalChangeListeners: any[] = [] /** * Encode json data using json-url or lz-string. It automatically detects the best encoding. @@ -22,7 +35,10 @@ var onGlobalChangeListeners = [] * @param {Object} options options for the encoding. Possible values: mode: "auto" or "humanize" * @returns encoded string */ -function encode(json, options = {}) { +function encode(json: object, options?: EncodeOptions): string { + // Set options to an empty object if it's undefined + options = options || {} + try { let urlState = jsonURLSerializer.encode(json) @@ -44,10 +60,10 @@ function encode(json, options = {}) { * @param {string} string to be decoded * @returns json */ -function decode(string) { +function decode(string: string): object { try { // try to decode using jsonURLSerializer - let json = jsonURLSerializer.decode(string) + const json = jsonURLSerializer.decode(string) // if parsed value is an object, return it if (json && typeof json === "object") return json @@ -58,7 +74,8 @@ function decode(string) { try { // try to decode as lz-string - return JSON.parse(LZString.decompressFromEncodedURIComponent(string)) + const json: object = JSON.parse(LZString.decompressFromEncodedURIComponent(string)) + return json } catch (e) { console.warn("URL State Provider: Could not decode string: ", string, e) return {} @@ -66,11 +83,10 @@ function decode(string) { } /** - * find search param by key and convert it to json - * @param {string} searchKey + * find search param by key (regex) and convert it to json * @returns json */ -function URLToState() { +function URLToState(): { [key: string]: any } { const match = window.location.href.match(regex) if (!match) return {} try { @@ -87,8 +103,8 @@ function URLToState() { * @param {JSON} state data * @returns new query param string with encoded data */ -function stateToQueryParam(state) { - var encodedState = encode(state) +function stateToQueryParam(state: object): string { + const encodedState = encode(state) return SEARCH_KEY + "=" + encodedState } @@ -97,10 +113,10 @@ function stateToQueryParam(state) { * @param {JSON} state data * @returns new url string with encoded data */ -function stateToURL(state) { - var encodedState = encode(state) +function stateToURL(state: object): string { + const encodedState = encode(state) // get current url - var newUrl = new URL(window.location.href) + const newUrl = new URL(window.location.href) // set new search params newUrl.searchParams.set(SEARCH_KEY, encodedState) // return new url string, decodeURIComponent is needed to convert url encoded characters @@ -112,8 +128,8 @@ function stateToURL(state) { * @param {string} stateID * @returns state for stateID */ -function currentState(stateID) { - return URLToState()[stateID] +function currentState(stateID: string): object { + return URLToState()[stateID] as object } /** @@ -122,17 +138,17 @@ function currentState(stateID) { * @param {JSON} state * @returns new url string */ -function updateState(stateID, state, options = {}) { +function updateState(stateID: string, state: object, options: UpdateStateOptions = {}) { // get current state from URL - const newState = URLToState() + const newState: { [key: string]: any } = URLToState() // URLToState should return an object, if not return empty object if ("string" === typeof newState) return {} // change state, overwrite or merge if "merge" option is true newState[stateID] = options?.merge ? { ...newState[stateID], ...state } : state // inform listeners for all changes in the url + // eslint-disable-next-line @typescript-eslint/no-unsafe-return onGlobalChangeListeners.forEach((listener) => listener(newState)) - // convert to url return stateToURL(newState) } @@ -152,16 +168,15 @@ function updateState(stateID, state, options = {}) { * changes to the method. * @param {boolean} historyOptions.replace - If true it replaces the last state in the window history (default false). */ -function updateStateAndHistory(stateID, state, merge, historyOptions) { - historyOptions = historyOptions || {} +function updateStateAndHistory(stateID: string, state: object, merge: boolean, historyOptions: HistoryOptions = {}) { const newUrl = updateState(stateID, state, { merge }) const historyState = historyOptions.state || "" const historyTitle = historyOptions.title || "" if (historyOptions?.replace) { - window.history.replaceState(historyState, historyTitle, newUrl) + window.history.replaceState(historyState, historyTitle, newUrl as string) } else { - window.history.pushState(historyState, historyTitle, newUrl) + window.history.pushState(historyState, historyTitle, newUrl as string) } } @@ -179,7 +194,8 @@ function updateStateAndHistory(stateID, state, merge, historyOptions) { * changes to the method. * @param {boolean} historyOptions.replace - If true it replaces the last state in the window history (default false). */ -function push(stateID, state, historyOptions) { +function push(stateID: string, state: object, historyOptions?: HistoryOptions) { + historyOptions = historyOptions || {} updateStateAndHistory(stateID, state, true, historyOptions) } @@ -197,7 +213,9 @@ function push(stateID, state, historyOptions) { * changes to the method. * @param {boolean} historyOptions.replace - If true it replaces the last state in the window history (default false). */ -function replace(stateID, state, historyOptions) { +function replace(stateID: string, state: object, historyOptions?: HistoryOptions) { + // Set options to an empty object if it's undefined + historyOptions = historyOptions || {} updateStateAndHistory(stateID, state, false, historyOptions) } @@ -206,7 +224,7 @@ function replace(stateID, state, historyOptions) { * @param {string} stateID * @param {function} listener */ -function addOnChangeListener(stateID, listener) { +function addOnChangeListener(stateID: string, listener = () => {}) { onHistoryChangeListeners[stateID] = listener } @@ -214,7 +232,7 @@ function addOnChangeListener(stateID, listener) { * Remove listener for stateID * @param {string} stateID */ -function removeOnChangeListener(stateID) { +function removeOnChangeListener(stateID: string) { delete onHistoryChangeListeners[stateID] } @@ -223,7 +241,7 @@ function removeOnChangeListener(stateID) { * @param {function} listener * @returns function to remove the listener */ -function addOnGlobalChangeListener(listener) { +function addOnGlobalChangeListener(listener = () => {}) { onGlobalChangeListeners.push(listener) return () => { const index = onGlobalChangeListeners.indexOf(listener) @@ -242,8 +260,8 @@ function addOnGlobalChangeListener(listener) { * @param {object} oldState optional * @returns */ -function informListener(stateID, newState, oldState) { - var listener = onHistoryChangeListeners[stateID] +function informListener(stateID: string, newState: object, oldState: object) { + const listener = onHistoryChangeListeners[stateID] if (!listener) return if (oldState) { @@ -255,7 +273,7 @@ function informListener(stateID, newState, oldState) { listener(newState) } -function onGlobalChange(callback) { +function onGlobalChange(callback: () => void) { return addOnGlobalChangeListener(callback) } @@ -265,30 +283,29 @@ function onGlobalChange(callback) { * If the event is fired we update th global state to the state in URL and we inform * every listener but only if the individual state has changed. */ -window.addEventListener("popstate", function (event) { - const newUrl = event.target.location.href - const state = URLToState(newUrl) +window.addEventListener("popstate", function () { + const state = URLToState() Object.keys(state).forEach((stateID) => { - informListener(stateID, state[stateID]) + informListener(stateID, state[stateID], {}) }) }) -function registerConsumer(stateID) { +function registerConsumer(stateID: string): object { return { currentState: function () { return currentState(stateID) }, - onChange: function (callback) { + onChange: function (callback: () => void) { addOnChangeListener(stateID, callback) return function () { removeOnChangeListener(stateID) } }, onGlobalChange, - push: function (state, historyOptions) { + push: function (state: object, historyOptions: HistoryOptions) { push(stateID, state, historyOptions) }, - replace: function (state, historyOptions) { + replace: function (state: object, historyOptions: HistoryOptions) { replace(stateID, state, historyOptions) }, } diff --git a/packages/url-state-provider/src/superstate.test.js b/packages/url-state-provider/src/superstate.test.ts similarity index 89% rename from packages/url-state-provider/src/superstate.test.js rename to packages/url-state-provider/src/superstate.test.ts index 11dbc141a..d999fc445 100644 --- a/packages/url-state-provider/src/superstate.test.js +++ b/packages/url-state-provider/src/superstate.test.ts @@ -9,7 +9,7 @@ describe("encoding", () => { it("encodes a string", () => { const humanURI = superstate() const string = "hallo peter wie geht es ñ ~ dsfesf%&/834294239477788 {}()" - let urlState = humanURI.encode(string) + const urlState = humanURI.encode(string) expect(urlState).toBe("hallo+peter+wie+geht+es+%C3%B1+~A+dsfesf~B~Q~G834294239477788+~H~I~J~K") const decoded = humanURI.decode(urlState) expect(decoded).toStrictEqual(string) @@ -17,7 +17,7 @@ describe("encoding", () => { it("encodes null", () => { const humanURI = superstate() const data = null - let urlState = humanURI.encode(data) + const urlState = humanURI.encode(data) expect(urlState).toBe("*A") const decoded = humanURI.decode(urlState) expect(decoded).toStrictEqual(data) @@ -26,7 +26,7 @@ describe("encoding", () => { it("encodes undefined", () => { const humanURI = superstate() const data = undefined - let urlState = humanURI.encode(data) + const urlState = humanURI.encode(data) expect(urlState).toBe("*B") const decoded = humanURI.decode(urlState) expect(decoded).toStrictEqual(data) @@ -34,7 +34,7 @@ describe("encoding", () => { it("encodes boolean", () => { const humanURI = superstate() const data = true - let urlState = humanURI.encode(data) + const urlState = humanURI.encode(data) expect(urlState).toBe("*C") const decoded = humanURI.decode(urlState) expect(decoded).toStrictEqual(data) @@ -42,7 +42,7 @@ describe("encoding", () => { it("encodes integer", () => { const humanURI = superstate() const data = 12345 - let urlState = humanURI.encode(data) + const urlState = humanURI.encode(data) expect(urlState).toBe("*12345") const decoded = humanURI.decode(urlState) expect(decoded).toStrictEqual(data) @@ -50,7 +50,7 @@ describe("encoding", () => { it("encodes NaN", () => { const humanURI = superstate() const data = NaN - let urlState = humanURI.encode(data) + const urlState = humanURI.encode(data) expect(urlState).toBe("*E") const decoded = humanURI.decode(urlState) expect(decoded).toStrictEqual(data) @@ -58,7 +58,7 @@ describe("encoding", () => { it("encodes Infinity", () => { const humanURI = superstate() const data = Infinity - let urlState = humanURI.encode(data) + const urlState = humanURI.encode(data) expect(urlState).toBe("*F") const decoded = humanURI.decode(urlState) expect(decoded).toStrictEqual(data) @@ -66,7 +66,7 @@ describe("encoding", () => { it("encodes float", () => { const humanURI = superstate() const data = 123.45678 - let urlState = humanURI.encode(data) + const urlState = humanURI.encode(data) expect(urlState).toBe("*123.45678") const decoded = humanURI.decode(urlState) expect(decoded).toStrictEqual(data) @@ -74,7 +74,7 @@ describe("encoding", () => { it("encodes negative float", () => { const humanURI = superstate() const data = -123.45678 - let urlState = humanURI.encode(data) + const urlState = humanURI.encode(data) expect(urlState).toBe("~123.45678") const decoded = humanURI.decode(urlState) expect(decoded).toStrictEqual(data) @@ -110,7 +110,7 @@ describe("encoding", () => { id: 123456789, }, } - let urlState = humanURI.encode(data) + const urlState = humanURI.encode(data) expect(urlState).toBe( "(company:(name:Example,continent:Europe,city:*B,tools:*A,a:(),b:(*),c:(*2,(~)),d:(a:*1,b:*C,c:*D,d:*E,e:*F,f:*G,g:(((~)),(regex:*Rab~Lc*Ri*R))),workers:(name:John+Doe,title:CEO,term:*2,working_hours:(*12,*13,*14,*15,(other:(~)))),id:*123456789))" ) @@ -120,7 +120,7 @@ describe("encoding", () => { it("encodes simple JSON", () => { const humanURI = superstate() const data = { a: 1, b: null, c: -3 } - let urlState = humanURI.encode(data) + const urlState = humanURI.encode(data) expect(urlState).toBe("(a:*1,b:*A,c:~3)") const decoded = humanURI.decode(urlState) expect(decoded).toStrictEqual(data) @@ -137,7 +137,7 @@ describe("encoding", () => { }, c: "-3", } - let urlState = humanURI.encode(data) + const urlState = humanURI.encode(data) expect(urlState).toBe("(a:*B,b:(k:(1,*3),y:(),z:*2300000000),c:-3)") const decoded = humanURI.decode(urlState) expect(decoded).toStrictEqual(data) @@ -146,15 +146,15 @@ describe("encoding", () => { it("encodes array", () => { const humanURI = superstate() const data = ["a", "b", "c"] - let urlState = humanURI.encode(data) + const urlState = humanURI.encode(data) expect(urlState).toBe("(a,b,c)") const decoded = humanURI.decode(urlState) expect(decoded).toStrictEqual(data) }) it("encodes empty array", () => { const humanURI = superstate() - const data = [] - let urlState = humanURI.encode(data) + const data: any[] = [] + const urlState = humanURI.encode(data) expect(urlState).toBe("(~)") const decoded = humanURI.decode(urlState) expect(decoded).toStrictEqual(data) @@ -162,7 +162,7 @@ describe("encoding", () => { it("array with empty string", () => { const humanURI = superstate() const data = [""] - let urlState = humanURI.encode(data) + const urlState = humanURI.encode(data) expect(urlState).toBe("(*)") const decoded = humanURI.decode(urlState) expect(decoded).toStrictEqual(data) @@ -171,7 +171,7 @@ describe("encoding", () => { it("array with quotes in a string", () => { const humanURI = superstate() const data = "´´´'''" - let urlState = humanURI.encode(data) + const urlState = humanURI.encode(data) expect(urlState).toBe("%C2%B4%C2%B4%C2%B4'''") const decoded = humanURI.decode(urlState) expect(decoded).toStrictEqual(data) @@ -180,7 +180,7 @@ describe("encoding", () => { it("encodes regex", () => { const humanURI = superstate() const data = /ab+c/i - let urlState = humanURI.encode(data) + const urlState = humanURI.encode(data) expect(urlState).toBe("*Rab~Lc*Ri*R") const decoded = humanURI.decode(urlState) expect(decoded).toStrictEqual(data) @@ -190,7 +190,8 @@ describe("encoding", () => { const humanURI = superstate() const data = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/ - let urlState = humanURI.encode(data) + + const urlState = humanURI.encode(data) expect(urlState).toBe( "*R%5E~J~J~S%5E%3C%3E~J~K~S~F~T~F~F.~W~V%3A~Fs~O%22~T~L~J~F.~S%5E%3C%3E~J~K~S~F~T~F~F.~W~V%3A~Fs~O%22~T~L~K~U~K%7C~J%22.~L%22~K~K~O~J~J~F~S~S0-9~T~H1~W3~I~F.~S0-9~T~H1~W3~I~F.~S0-9~T~H1~W3~I~F.~S0-9~T~H1~W3~I~T~K%7C~J~J~Sa-zA-Z~F-0-9~T~L~F.~K~L~Sa-zA-Z~T~H2~W~I~K~K~N*R*R" ) @@ -213,7 +214,7 @@ describe("base 64", () => { it("encodes a string", () => { const humanURI = superstate() const string = "hallo peter wie geht es ñ ~ dsfesf%&/834294239477788 {}()" - let urlState = humanURI.encodeB64(string) + const urlState = humanURI.encodeB64(string) expect(urlState).toBe( "aGFsbG8rcGV0ZXIrd2llK2dlaHQrZXMrJUMzJUIxK35BK2RzZmVzZn5CflF+RzgzNDI5NDIzOTQ3Nzc4OCt+SH5Jfkp+Sw==" ) @@ -231,7 +232,7 @@ describe("base 64", () => { id: 123456789, }, } - let urlState = humanURI.encodeB64(data) + const urlState = humanURI.encodeB64(data) expect(urlState).toBe( "KGNvbXBhbnk6KG5hbWU6RXhhbXBsZSxjb250aW5lbnQ6RXVyb3BlLHdvcmtlcnM6KG5hbWU6Sm9obitEb2UsdGl0bGU6Q0VPLHRlcm06KjIpLGlkOioxMjM0NTY3ODkpKQ==" ) @@ -244,7 +245,7 @@ describe("lz-string compressed", () => { it("encodes and compress a string", () => { const humanURI = superstate() const string = "hallo peter wie geht es ñ ~ dsfesf%&/834294239477788 {}()" - let urlState = humanURI.encodeLZ(string) + const urlState = humanURI.encodeLZ(string) expect(urlState).toBe( "BYQwNmD2DUAOCmAXeAnaB3AlvaBzewi08AztAKQDCAzOQEICM0AfgILQAmJAZqd83WYBFZgHEAHNQAsAJgCcs6goDsq8eJYAJZgElmAKWYBpIA" ) @@ -262,7 +263,7 @@ describe("lz-string compressed", () => { string: "Lorem ipsum dolor sit amet, consectetur adipisici elit. Wie: 1234567890", }, } - let urlState = humanURI.encodeLZ(data) + const urlState = humanURI.encodeLZ(data) expect(urlState).toBe( "BQYw9gtgDghgdgTwFzDjCBTJBRAHuqAGwwBpw4AXASzg0pwFcAnMKUgdzCYGsMmBnFGkxIAUmAAWcANQARMKWoViSAMLYA8iQp8ISAFQAmAJQkqAEwMBGQwGYALAFYAbAHYAHAE4S-CkxoA5kgAMlwYENJUUPwMEeZghFzS-FQU0ugYFAB+AOrS5PwYIDoUzOnmUVQpIFTSGISpAHTSOVQYAKS2AILSNg4uHp4ADMbGQA" ) @@ -281,7 +282,7 @@ describe("lz-string compressed", () => { id: 123456789, }, } - let urlState = humanURI.encodeLZ(data) + const urlState = humanURI.encodeLZ(data) expect(urlState).toBe( "BQYw9gtgDghgdgTwFzDjCBTJBRAHuqAGwwBpw4AXASzg0pwFcAnMKUgdzCYGsMmBnFGkxIAUmAAWcANQARMKWoViSAMLYA8iQp8ISAFQAmAJQkqAEwMBGQwGYALAFYAbAHYAHAE5jxoA" ) @@ -302,7 +303,7 @@ describe("base 64 with null on error", () => { string: "Lorem ipsum dolor sit amet, consectetur adipisici elit. Wie: 1234567890", }, } - let urlState = humanURI.encodeB64(data) + const urlState = humanURI.encodeB64(data) const decoded = humanURI.decodeB64NullOnError(urlState) expect(decoded).toStrictEqual(data) }) @@ -326,7 +327,7 @@ describe("compressed with null on error", () => { string: "Lorem ipsum dolor sit amet, consectetur adipisici elit. Wie: 1234567890", }, } - let urlState = humanURI.encodeLZ(data) + const urlState = humanURI.encodeLZ(data) const decoded = humanURI.decodeLZNullOnError(urlState) expect(decoded).toStrictEqual(data) }) @@ -358,8 +359,10 @@ describe("broken LZ decompression", () => { try { humanURI.decodeLZ(data) - } catch (error) { - err = error + } catch (error: unknown) { + if (error instanceof Error) { + err = error + } } expect(err.message).toBe("URI malformed") @@ -375,8 +378,10 @@ describe("broken B64 decomopression", () => { try { humanURI.decodeB64(data) - } catch (error) { - err = error + } catch (error: unknown) { + if (error instanceof Error) { + err = error + } } expect(err.message).toBe("URI malformed") @@ -392,8 +397,10 @@ describe("broken decoding because of not closed obj", () => { try { humanURI.decode(data) - } catch (error) { - err = error + } catch (error: unknown) { + if (error instanceof Error) { + err = error + } } expect(err.message).toBe("JSON object not closed correctly") @@ -404,8 +411,10 @@ describe("broken decoding because of not closed obj", () => { const data = "%(324" try { humanURI.decode(data) - } catch (error) { - err = error + } catch (error: unknown) { + if (error instanceof Error) { + err = error + } } expect(err.message).toBe("URI malformed") diff --git a/packages/url-state-provider/src/superstate.js b/packages/url-state-provider/src/superstate.ts similarity index 80% rename from packages/url-state-provider/src/superstate.js rename to packages/url-state-provider/src/superstate.ts index 16e93983c..3b212027d 100644 --- a/packages/url-state-provider/src/superstate.js +++ b/packages/url-state-provider/src/superstate.ts @@ -21,12 +21,18 @@ import lzstring from "lz-string" +type SupportedTypes = string | object | number | boolean | undefined | null | RegExp | any[] + +type Indexable = { + [key: string]: SupportedTypes +} + export default function () { const nonURIsafe = "~%\t\n\r\\/{}()+#$@?&=[]*;," const keys = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-!_" // standard function for encoding and decoding - function encode(value) { + function encode(value: SupportedTypes): string { switch (typeof value) { case "object": if (value === null) { @@ -58,7 +64,7 @@ export default function () { } } - function decode(value) { + function decode(value: string): SupportedTypes { if (!value) return "" if (value[0] === "*") { @@ -84,7 +90,7 @@ export default function () { } } - // if value[0] is ~ and value[1] is a number + // test if value[0] is ~ and value[1] is a number if (/^~\d/.test(value)) { return decodeNumber(value) } @@ -98,7 +104,7 @@ export default function () { return decodeString(value) } // obj - function encodeString(value) { + function encodeString(value: string): string { return value .split("") .map((char) => { @@ -114,12 +120,12 @@ export default function () { .join("") } - function decodeString(value) { + function decodeString(value: string): string { let result = "" value = decodeURIComponent(value) for (let i = 0; i < value.length; i++) { - let char = value[i] + const char = value[i] if (char === "+") { result += " " } else if (char === "~" && keys.includes(value[i + 1])) { @@ -132,20 +138,22 @@ export default function () { return result } - function encodeRegex(value) { + function encodeRegex(value: RegExp): string { // stringfy the regex and add *R to the beginning - let source = encode(value?.source.toString()) - let flags = encode(value?.flags.toString()) + const source = encodeString(value?.source.toString()) + const flags = encodeString(value?.flags.toString()) return `*R${source}*R${flags}*R` } - function decodeRegex(value) { - let regex = value.slice(2, -2) - regex = regex.split("*R").map((v) => decode(v)) + function decodeRegex(value: string): RegExp { + const regex = value + .slice(2, -2) + .split("*R") + .map((v) => decodeString(v)) return new RegExp(regex[0], regex[1]) } - function encodeObject(value) { + function encodeObject(value: object | Array): string { if (Array.isArray(value)) { return encodeArray(value) } @@ -157,7 +165,7 @@ export default function () { return "(" + entries.join(",") + ")" } - function decodeObject(value) { + function decodeObject(value: string): object | Array { if (value === "(~)") { return [] } @@ -175,7 +183,7 @@ export default function () { } } - function decodeJSON(value) { + function decodeJSON(value: string): object { value = value.slice(1, -1) const entries = [] @@ -211,7 +219,7 @@ export default function () { if (key) entries.push([key, currentEntry.trim()]) - const result = {} + const result: Indexable = {} entries.forEach(([key, encodedValue]) => { result[key] = decode(encodedValue) }) @@ -219,18 +227,18 @@ export default function () { return result } - function encodeArray(value) { + function encodeArray(value: Array): string { if (value.length === 0) { return "(~)" // Special case for empty arrays } - let encoded = "(" + value.map(encode).join(",") + ")" + const encoded = "(" + value.map(encode).join(",") + ")" if (encoded === "()") { return "(*)" } return encoded } - function decodeArray(value) { + function decodeArray(value: string): SupportedTypes[] { // remove the brackets value = value.slice(1, -1) const entries = [] @@ -259,7 +267,7 @@ export default function () { entries.push(currentEntry.trim()) - const result = [] + const result: SupportedTypes[] = [] entries.forEach((encodedValue) => { result.push(decode(encodedValue)) }) @@ -267,7 +275,7 @@ export default function () { return result } - function encodeNumber(value) { + function encodeNumber(value: number): string { if (value < 0) { // delete - through ~ return "~" + -value @@ -275,18 +283,18 @@ export default function () { return "*" + value } } - function decodeNumber(value) { + function decodeNumber(value: string): number { if (value[0] === "~") { return -value.slice(1) } return +value.slice(1) } - function isEncodedJSON(value) { + function isEncodedJSON(value: string): boolean { let isObject = false let depth = 0 - for (let char of value) { + for (const char of value) { if (char === "(") depth++ if (char === ")") depth-- @@ -298,11 +306,11 @@ export default function () { } /// base64 - function encodeB64(value) { + function encodeB64(value: SupportedTypes): string { return btoa(encode(value)) } - function decodeB64(value) { + function decodeB64(value: string): SupportedTypes { try { return decode(atob(value)) } catch (_error) { @@ -311,25 +319,25 @@ export default function () { } // compressed - function encodeLZ(value) { + function encodeLZ(value: SupportedTypes): string { return lzstring.compressToEncodedURIComponent(encode(value)) } - function decodeLZ(value) { + function decodeLZ(value: string): SupportedTypes { try { const result = decode(lzstring.decompressFromEncodedURIComponent(value)) if (result === "") { throw new Error("URI malformed") } return result - } catch (error) { + } catch (error: any) { throw new Error(error.message) } } /// base64 with null on error - function decodeB64NullOnError(value) { + function decodeB64NullOnError(value: string): SupportedTypes { try { return decode(atob(value)) } catch (_error) { @@ -337,7 +345,7 @@ export default function () { } } - function decodeLZNullOnError(value) { + function decodeLZNullOnError(value: string): SupportedTypes { try { const result = decode(lzstring.decompressFromEncodedURIComponent(value)) if (result === "" && value !== "") { @@ -349,7 +357,7 @@ export default function () { } } - function decodeNullOnError(value) { + function decodeNullOnError(value: string): SupportedTypes { try { return decode(value) } catch (_error) { diff --git a/packages/url-state-provider/tsconfig.json b/packages/url-state-provider/tsconfig.json new file mode 100644 index 000000000..af97ca56e --- /dev/null +++ b/packages/url-state-provider/tsconfig.json @@ -0,0 +1,23 @@ +{ + "extends": "@cloudoperators/juno-config/typescript/base.json", + "compilerOptions": { + "outDir": "./build", + "target": "ES6", + "module": "ES2022", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "types": ["vitest/globals"], + "resolveJsonModule": true + }, + "include": [ + "./src/**/*.ts", + "./__tests__/**/*.test.ts", + "./types/**/*.ts", + "./src/**/*.tsx", + "vitest.setup.ts", + "src/superstate.js" + ], + "exclude": ["node_modules", "build"] +} diff --git a/packages/url-state-provider/types/global.d.ts b/packages/url-state-provider/types/global.d.ts new file mode 100644 index 000000000..0f1e42980 --- /dev/null +++ b/packages/url-state-provider/types/global.d.ts @@ -0,0 +1,12 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +/* eslint-disable no-unused-vars */ +declare global { + interface Window { + __url_state_provider?: string + } +} +export {} diff --git a/packages/url-state-provider/vite.config.ts b/packages/url-state-provider/vite.config.ts index 3a412f116..a3afc7972 100644 --- a/packages/url-state-provider/vite.config.ts +++ b/packages/url-state-provider/vite.config.ts @@ -4,15 +4,23 @@ */ import { defineConfig } from "vite" +import dts from "vite-plugin-dts" export default defineConfig({ build: { lib: { - entry: "src/index.js", // or 'src/main.ts' if TypeScript + entry: "src/index.ts", // or 'src/main.ts' if TypeScript name: "url-state-provider", // Replace with your library's global name formats: ["es"], // Output formats: ESM and CommonJS fileName: () => `index.js`, // Output file names }, outDir: "build", }, + plugins: [ + dts({ + exclude: ["./__tests__/**/*.test.ts", "vitest.setup.ts"], + insertTypesEntry: true, // Ensure types are properly exported + outDir: "build/types", // Specify where to output the types + }), + ], })