;
8 | promise: Promise;
9 | }
--------------------------------------------------------------------------------
/src/util/encodingUtils.ts:
--------------------------------------------------------------------------------
1 | import {Base64} from "js-base64";
2 |
3 | /**
4 | * Encodes the passed ArrayBuffer to a base64 string
5 | */
6 | export function encodeBase64(value: ArrayBuffer): string {
7 | return Base64.fromUint8Array(new Uint8Array(value));
8 | }
9 |
10 | /**
11 | * Decodes a base64 string to an ArrayBuffer
12 | */
13 | export function decodeBase64(value: string): ArrayBuffer {
14 | return Base64.toUint8Array(value);
15 | }
16 |
17 | /**
18 | * Encodes an ArrayBuffer to a hex string
19 | */
20 | //https://stackoverflow.com/a/40031979
21 | export function arrayBufferToHex(data: ArrayBuffer): string {
22 | return Array.prototype.map.call(new Uint8Array(data), (x: number) => ("00" + x.toString(16)).slice(-2)).join("");
23 | }
--------------------------------------------------------------------------------
/src/util/encryptionUtils.ts:
--------------------------------------------------------------------------------
1 | //Creating the constants
2 | const saltLen = 8; //8 bytes
3 | const ivLen = 12; //12 bytes (instead of 16 because of GCM)
4 | const algorithm = "PBKDF2";
5 | const hash = "SHA-256";
6 | const cipherTransformation = "AES-GCM";
7 | const keyIterationCount = 10000;
8 | const keyLength = 128; //128 bits
9 |
10 | //Whether a request has been put in to initialize the crypto password, even if undefined
11 | let cryptoPasswordSet = false;
12 | let userKey: CryptoKey | undefined;
13 |
14 | /**
15 | * Sets the password to use for future cryptographic operations
16 | */
17 | export async function setCryptoPassword(password: string | undefined) {
18 | cryptoPasswordSet = true;
19 |
20 | if(password == undefined) {
21 | userKey = undefined;
22 | } else {
23 | userKey = await crypto.subtle.importKey("raw", new TextEncoder().encode(password), "PBKDF2", false, ["deriveKey"]);
24 | }
25 | }
26 |
27 | /**
28 | * Gets whether {@link setCryptoPassword} has been called once
29 | * (even if the password as undefined)
30 | */
31 | export function isCryptoPasswordSet() {
32 | return cryptoPasswordSet;
33 | }
34 |
35 | /**
36 | * Gets if a valid crypto password is available to use
37 | */
38 | export function isCryptoPasswordAvailable() {
39 | return userKey !== undefined;
40 | }
41 |
42 | /**
43 | * Encrypts the provided ArrayBuffer with the crypto password
44 | */
45 | export async function encryptData(inData: ArrayBuffer): Promise {
46 | //Generating random data
47 | const salt = new Uint8Array(saltLen);
48 | crypto.getRandomValues(salt);
49 | const iv = new Uint8Array(ivLen);
50 | crypto.getRandomValues(iv);
51 |
52 | //Creating the key
53 | const derivedKey = await crypto.subtle.deriveKey({name: algorithm, salt: salt, iterations: keyIterationCount, hash: hash},
54 | userKey!,
55 | {name: cipherTransformation, length: keyLength},
56 | false,
57 | ["encrypt"]);
58 |
59 | //Encrypting the data
60 | const encrypted = await crypto.subtle.encrypt({name: cipherTransformation, iv: iv}, derivedKey, inData);
61 |
62 | //Returning the data
63 | const returnData = new Uint8Array(saltLen + ivLen + encrypted.byteLength);
64 | returnData.set(salt, 0);
65 | returnData.set(iv, saltLen);
66 | returnData.set(new Uint8Array(encrypted), saltLen + ivLen);
67 | return returnData.buffer;
68 | }
69 |
70 | /**
71 | * Decrypts the provided ArrayBuffer with the crypto password
72 | */
73 | export async function decryptData(inData: ArrayBuffer): Promise {
74 | //Reading the data
75 | const salt = inData.slice(0, saltLen);
76 | const iv = inData.slice(saltLen, saltLen + ivLen);
77 | const data = inData.slice(saltLen + ivLen);
78 |
79 | //Creating the key
80 | const derivedKey = await crypto.subtle.deriveKey({name: algorithm, salt: salt, iterations: keyIterationCount, hash: hash},
81 | userKey!,
82 | {name: cipherTransformation, length: keyLength},
83 | false,
84 | ["decrypt"]);
85 |
86 | //Decrypting the data
87 | return await crypto.subtle.decrypt({name: cipherTransformation, iv: iv}, derivedKey, data);
88 | }
--------------------------------------------------------------------------------
/src/util/eventEmitter.ts:
--------------------------------------------------------------------------------
1 | import UnsubscribeCallback from "shared/data/unsubscribeCallback";
2 |
3 | /**
4 | * A listener that receives updates from an EventEmitter
5 | */
6 | export interface EventEmitterListener {
7 | (event: T): void;
8 | }
9 |
10 | /**
11 | * A stream of events that can be notified or subscribed to
12 | */
13 | export default class EventEmitter {
14 | private readonly listeners: EventEmitterListener[] = [];
15 |
16 | /**
17 | * Subscribes to updates from this EventEmitter
18 | * @param listener The listener to subscribe to this emitter
19 | * @param unsubscribeConsumer An optional callback function that
20 | * will receive an instance of this subscription's unsubscribe callback
21 | */
22 | public subscribe(listener: EventEmitterListener, unsubscribeConsumer?: (callback: UnsubscribeCallback) => void): UnsubscribeCallback {
23 | this.listeners.push(listener);
24 |
25 | const unsubscribeCallback = () => this.unsubscribe(listener);
26 | unsubscribeConsumer?.(unsubscribeCallback);
27 | return unsubscribeCallback;
28 | }
29 |
30 | /**
31 | * Unsubscribes a listener from this event emitter
32 | */
33 | public unsubscribe(listener: EventEmitterListener) {
34 | const index = this.listeners.indexOf(listener, 0);
35 | if(index !== -1) this.listeners.splice(index, 1);
36 | }
37 |
38 | /**
39 | * Notifies all registered listeners of a new event
40 | */
41 | public notify(event: T) {
42 | for(const listener of this.listeners) listener(event);
43 | }
44 | }
45 |
46 | /**
47 | * An {@link EventEmitter} that automatically emits the last
48 | * item on subscribe
49 | */
50 | export class CachedEventEmitter extends EventEmitter {
51 | private lastEvent: T | null = null;
52 |
53 | constructor(lastEvent: T | null = null) {
54 | super();
55 | this.lastEvent = lastEvent;
56 | }
57 |
58 | public override subscribe(listener: EventEmitterListener): UnsubscribeCallback {
59 | super.subscribe(listener);
60 | if(this.lastEvent !== null) {
61 | listener(this.lastEvent);
62 | }
63 | return () => this.unsubscribe(listener);
64 | }
65 |
66 | public override notify(event: T) {
67 | super.notify(event);
68 | this.lastEvent = event;
69 | }
70 | }
--------------------------------------------------------------------------------
/src/util/hashUtils.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Generates a hashcode from a string
3 | */
4 | //https://werxltd.com/wp/2010/05/13/javascript-implementation-of-javas-string-hashcode-method/
5 | export function hashString(input: string): number {
6 | let hash = 0;
7 | for(let i = 0; i < input.length; i++) {
8 | const chr = input.charCodeAt(i);
9 | hash = ((hash << 5) - hash) + chr;
10 | hash |= 0; // Convert to 32bit integer
11 | }
12 |
13 | return hash;
14 | }
--------------------------------------------------------------------------------
/src/util/installationUtils.ts:
--------------------------------------------------------------------------------
1 | import {v4 as uuidv4} from "uuid";
2 |
3 | export enum StorageKey {
4 | InstallationID = "installationID"
5 | }
6 |
7 | /**
8 | * Gets the installation ID of this instance
9 | */
10 | export function getInstallationID(): string {
11 | const installationID = localStorage.getItem(StorageKey.InstallationID);
12 | //Just return the installation ID value if we already have one
13 | if(installationID) {
14 | return installationID;
15 | } else {
16 | //Generating a new installation ID
17 | const installationID = uuidv4();
18 |
19 | //Saving the installation ID to local storage
20 | localStorage.setItem(StorageKey.InstallationID, installationID);
21 |
22 | //Returning the installation ID
23 | return installationID;
24 | }
25 | }
--------------------------------------------------------------------------------
/src/util/messageFlow.ts:
--------------------------------------------------------------------------------
1 | import PaletteSpecifier from "shared/data/paletteSpecifier";
2 |
3 | /**
4 | * A message's position in the thread in accordance with other nearby messages
5 | */
6 | export interface MessageFlow {
7 | //Whether this message should be anchored to the message above
8 | anchorTop: boolean;
9 |
10 | //Whether this message should be anchored to the message below
11 | anchorBottom: boolean;
12 |
13 | //Whether this message should have a divider between it and the message below
14 | showDivider: boolean;
15 | }
16 |
17 | export interface MessagePartFlow {
18 | //Whether this message is outgoing
19 | isOutgoing: boolean;
20 |
21 | //Whether this message is unconfirmed, and should be rendered as such
22 | isUnconfirmed: boolean;
23 |
24 | color: PaletteSpecifier; //Text and action button colors
25 | backgroundColor: PaletteSpecifier; //Message background color
26 |
27 | //Whether this message should be anchored to the message above
28 | anchorTop: boolean;
29 |
30 | //Whether this message should be anchored to the message below
31 | anchorBottom: boolean;
32 | }
33 |
34 | const radiusLinked = "4px";
35 | const radiusUnlinked = "16.5px";
36 |
37 | /**
38 | * Generates a CSS border radius string from the provided flow
39 | */
40 | export function getFlowBorderRadius(flow: MessagePartFlow): string {
41 | const radiusTop = flow.anchorTop ? radiusLinked : radiusUnlinked;
42 | const radiusBottom = flow.anchorBottom ? radiusLinked : radiusUnlinked;
43 |
44 | if(flow.isOutgoing) {
45 | return `${radiusUnlinked} ${radiusTop} ${radiusBottom} ${radiusUnlinked}`;
46 | } else {
47 | return `${radiusTop} ${radiusUnlinked} ${radiusUnlinked} ${radiusBottom}`;
48 | }
49 | }
50 |
51 | const opacityUnconfirmed = 0.5;
52 |
53 | /**
54 | * Generates a CSS opacity radius value from the provided flow
55 | */
56 | export function getFlowOpacity(flow: MessagePartFlow): number | undefined {
57 | if(flow.isUnconfirmed) {
58 | return opacityUnconfirmed;
59 | } else {
60 | return undefined;
61 | }
62 | }
63 |
64 | const spacingLinked = 0.25;
65 | const spacingUnlinked = 1;
66 |
67 | /**
68 | * Gets the spacing value to use between message bubbles
69 | * @param linked Whether the message is linked to the adjacent message
70 | */
71 | export function getBubbleSpacing(linked: boolean): number {
72 | if(linked) {
73 | return spacingLinked;
74 | } else {
75 | return spacingUnlinked;
76 | }
77 | }
--------------------------------------------------------------------------------
/src/util/promiseTimeout.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Wraps a promise, and returns a new promise that rejects after the
3 | * specified amount of time
4 | * @param timeout The promise timeout in milliseconds
5 | * @param timeoutReason The reason to use when rejecting the promise
6 | * @param promise The promise to wrap
7 | */
8 | export default function promiseTimeout(timeout: number, timeoutReason: any | undefined, promise: Promise): Promise {
9 | // Create a promise that rejects in milliseconds
10 | const timeoutPromise = new Promise((resolve, reject) => {
11 | const id = setTimeout(() => {
12 | clearTimeout(id);
13 | reject(timeoutReason);
14 | }, timeout);
15 | });
16 |
17 | return Promise.race([promise, timeoutPromise]);
18 | }
--------------------------------------------------------------------------------
/src/util/resolveablePromise.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * A promise wrapper that can be resolved or rejected from outside the object
3 | */
4 | export default class ResolveablePromise {
5 | public readonly promise: Promise;
6 | private promiseResolve!: (value: T | PromiseLike) => void;
7 | private promiseReject!: (reason?: any) => void;
8 |
9 | constructor() {
10 | this.promise = new Promise((resolve, reject) => {
11 | this.promiseResolve = resolve;
12 | this.promiseReject = reject;
13 | });
14 | }
15 |
16 | resolve(value: T | PromiseLike): void {
17 | this.promiseResolve(value);
18 | }
19 |
20 | reject(reason?: any): void {
21 | this.promiseReject(reason);
22 | }
23 | }
--------------------------------------------------------------------------------
/src/util/resolveablePromiseTimeout.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * A promise wrapper that can be resolved or rejected from outside the object
3 | */
4 | import ResolveablePromise from "shared/util/resolveablePromise";
5 |
6 | /**
7 | * A ResolveablePromise that can also be set to time out with a specified error
8 | */
9 | export default class ResolveablePromiseTimeout extends ResolveablePromise {
10 | private timeoutID: any | undefined = undefined;
11 |
12 | /**
13 | * Sets this promise to time out after duration with reason
14 | */
15 | timeout(duration: number, reason?: any): void {
16 | //Clear any existing timeouts
17 | if(this.timeoutID !== undefined) {
18 | clearTimeout(this.timeoutID);
19 | }
20 |
21 | //Set the timeout
22 | this.timeoutID = setTimeout(() => {
23 | this.reject(reason);
24 | }, duration);
25 | }
26 |
27 | /**
28 | * Clears the current timeout on this promise
29 | */
30 | clearTimeout() {
31 | //Ignore if there is no timeout
32 | if(this.timeoutID === undefined) return;
33 |
34 | //Cancel the timeout
35 | clearTimeout(this.timeoutID);
36 | this.timeoutID = undefined;
37 | }
38 |
39 | resolve(value: PromiseLike | T) {
40 | this.clearTimeout();
41 | super.resolve(value);
42 | }
43 |
44 | reject(reason?: any) {
45 | this.clearTimeout();
46 | super.reject(reason);
47 | }
48 | }
--------------------------------------------------------------------------------
/src/util/secureStorageUtils.ts:
--------------------------------------------------------------------------------
1 | import * as secrets from "../secrets";
2 | import {decodeBase64, encodeBase64} from "shared/util/encodingUtils";
3 |
4 | const ivLen = 12;
5 |
6 | export enum SecureStorageKey {
7 | ServerPassword = "serverPassword",
8 | GoogleRefreshToken = "googleRefreshToken"
9 | }
10 |
11 | const cryptoKey: Promise = crypto.subtle.importKey(
12 | "jwk",
13 | secrets.jwkLocalEncryption,
14 | "AES-GCM",
15 | false,
16 | ["encrypt", "decrypt"]
17 | );
18 |
19 | function concatBuffers(buffer1: ArrayBuffer, buffer2: ArrayBuffer): ArrayBuffer {
20 | const tmp = new Uint8Array(buffer1.byteLength + buffer2.byteLength);
21 | tmp.set(new Uint8Array(buffer1), 0);
22 | tmp.set(new Uint8Array(buffer2), buffer1.byteLength);
23 | return tmp;
24 | }
25 |
26 | async function encrypt(inData: ArrayBuffer, generateIV: boolean): Promise {
27 | if(generateIV) {
28 | const iv = window.crypto.getRandomValues(new Uint8Array(ivLen));
29 | const encrypted = await crypto.subtle.encrypt({name: "AES-GCM", iv: iv}, await cryptoKey, inData);
30 | return concatBuffers(iv, encrypted);
31 | } else {
32 | return crypto.subtle.encrypt({name: "AES-GCM", iv: new Uint8Array(ivLen)}, await cryptoKey, inData);
33 | }
34 | }
35 |
36 | async function decrypt(inData: ArrayBuffer, useIV: boolean): Promise {
37 | if(useIV) {
38 | const iv = inData.slice(0, ivLen);
39 | const data = inData.slice(ivLen);
40 | return crypto.subtle.decrypt({name: "AES-GCM", iv: iv}, await cryptoKey, data);
41 | } else {
42 | return crypto.subtle.decrypt({name: "AES-GCM", iv: new Int8Array(ivLen)}, await cryptoKey, inData);
43 | }
44 | }
45 |
46 | /**
47 | * Encrypts a string and returns it in base64 form
48 | */
49 | async function encryptString(value: string, generateIV: boolean): Promise {
50 | return encodeBase64(await encrypt(new TextEncoder().encode(value), generateIV));
51 | }
52 |
53 | /**
54 | * Decrypts a string from its base64 form
55 | */
56 | async function decryptString(value: string, useIV: boolean): Promise {
57 | return new TextDecoder().decode(await decrypt(decodeBase64(value), useIV));
58 | }
59 |
60 | /**
61 | * Stores a value in secure storage
62 | * @param key The storage key to use
63 | * @param value The value to use, or undefined to remove
64 | */
65 | export async function setSecureLS(key: SecureStorageKey, value: string | undefined) {
66 | const encryptedKey = await encryptString(key, false);
67 |
68 | if(value === undefined) {
69 | localStorage.removeItem(encryptedKey);
70 | } else {
71 | value = await encryptString(value, true);
72 | localStorage.setItem(encryptedKey, value);
73 | }
74 | }
75 |
76 | /**
77 | * Reads a value from secure storage
78 | * @param key The storage key to read from
79 | */
80 | export async function getSecureLS(key: SecureStorageKey): Promise {
81 | const value = localStorage.getItem(await encryptString(key, false));
82 | if(value === null) {
83 | return undefined;
84 | } else {
85 | return decryptString(value, true);
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/src/util/soundUtils.ts:
--------------------------------------------------------------------------------
1 | import soundNotification from "shared/resources/audio/notification.wav";
2 | import soundMessageIn from "shared/resources/audio/message_in.wav";
3 | import soundMessageOut from "shared/resources/audio/message_out.wav";
4 | import soundTapback from "shared/resources/audio/tapback.wav";
5 |
6 | /**
7 | * Plays the audio sound for an incoming notification
8 | */
9 | export function playSoundNotification() {
10 | new Audio(soundNotification).play()?.catch((reason) => {
11 | console.log("Failed to play notification audio: " + reason);
12 | });
13 | }
14 |
15 | /**
16 | * Plays the audio sound for an incoming message
17 | */
18 | export function playSoundMessageIn() {
19 | new Audio(soundMessageIn).play()?.catch((reason) => {
20 | console.log("Failed to play incoming message audio: " + reason);
21 | });
22 | }
23 |
24 | /**
25 | * Plays the audio sound for an outgoing message
26 | */
27 | export function playSoundMessageOut() {
28 | new Audio(soundMessageOut).play()?.catch((reason) => {
29 | console.log("Failed to play outgoing message audio: " + reason);
30 | });
31 | }
32 |
33 | /**
34 | * Plays the audio sound for a new tapback
35 | */
36 | export function playSoundTapback() {
37 | new Audio(soundTapback).play()?.catch((reason) => {
38 | console.log("Failed to play tapback audio: " + reason);
39 | });
40 | }
--------------------------------------------------------------------------------
/src/util/taskQueue.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * TaskQueue enqueues promises, ensuring that all promises
3 | * complete in the order they were enqueued in
4 | */
5 | export default class TaskQueue {
6 | private previousTask: Promise | undefined;
7 |
8 | /**
9 | * Enqueues a new promise.
10 | * The generator is only called when the previous promise in the queue completes.
11 | * The returned promise completes when itself and all of the promises before it have completed.
12 | */
13 | public enqueue(value: () => Promise): void {
14 | if(this.previousTask === undefined) {
15 | this.previousTask = value();
16 | } else {
17 | this.previousTask = this.previousTask.then(value);
18 | }
19 | }
20 | }
--------------------------------------------------------------------------------
/src/util/versionUtils.ts:
--------------------------------------------------------------------------------
1 | /* Compares 2 version lists and returns which one is larger
2 | -1: version 1 is smaller
3 | 0: versions are equal
4 | 1: version 1 is greater
5 | */
6 | export function compareVersions(version1: number[], version2: number[]): number {
7 | for(let i = 0; i < Math.max(version1.length, version2.length); i++) {
8 | //Get the version codes, defaulting to 0 if the length exceeds the code
9 | const code1 = version1[i] ?? 0;
10 | const code2 = version2[i] ?? 0;
11 |
12 | //Compare the codes
13 | const comparison = code1 - code2;
14 |
15 | if(comparison != 0) {
16 | return comparison;
17 | }
18 | }
19 |
20 | //All version codes are the same, no difference
21 | return 0;
22 | }
--------------------------------------------------------------------------------
/test/util/arrayUtils.test.ts:
--------------------------------------------------------------------------------
1 | import {arrayContainsAll, groupArray} from "../../src/util/arrayUtils";
2 |
3 | type ValueHolder = {value: T};
4 | type KeyValueHolder = {key: K, value: V};
5 |
6 | function compareTypes(a: unknown, b: unknown): number {
7 | const typeA = typeof a;
8 | const typeB = typeof b;
9 |
10 | if(typeA < typeB) return -1;
11 | else if(typeA > typeB) return 1;
12 | else return 0;
13 | }
14 |
15 | describe("arrayContainsAll", () => {
16 | test("empty arrays should match", () => {
17 | const array: unknown[] = [];
18 | expect(arrayContainsAll(array, array)).toBe(true);
19 | });
20 |
21 | test("matching arrays should match", () => {
22 | const array = [0, 1, "string", undefined, null];
23 | expect(arrayContainsAll(array, array)).toBe(true);
24 | });
25 |
26 | test("matching arrays in different order should match", () => {
27 | const array1 = [0, 1, "string", undefined, null];
28 | const array2 = [...array1].reverse();
29 | expect(arrayContainsAll(array1, array2)).toBe(true);
30 | });
31 |
32 | test("matching arrays with a custom mapper should match", () => {
33 | const array1 = [0, 1, "string", undefined, null].map((it): ValueHolder => ({value: it}));
34 | const array2 = [...array1].reverse();
35 | expect(arrayContainsAll(array1, array2, (it) => it.value)).toBe(true);
36 | });
37 |
38 | test("matching arrays with a custom matcher should match", () => {
39 | const array1 = [0, "1", 2];
40 | const array2 = ["3", 4, 5];
41 | expect(arrayContainsAll(array1, array2, undefined, compareTypes)).toBe(true);
42 | });
43 |
44 | test("arrays of differing lengths should not match", () => {
45 | const array1 = [0, 1, "string", undefined, null];
46 | const array2 = [0, 1, "string", undefined, null, null];
47 | expect(arrayContainsAll(array1, array2)).toBe(false);
48 | });
49 |
50 | test("arrays of differing values should not match", () => {
51 | const array1 = [0, 1, "string", undefined, null];
52 | const array2 = [0, 1, 2, undefined, null];
53 | expect(arrayContainsAll(array1, array2)).toBe(false);
54 | });
55 |
56 | test("arrays of differing values with a custom mapper should not match", () => {
57 | const array1 = [0, 1, "string", undefined, null].map((it): ValueHolder => ({value: it}));
58 | const array2 = [0, 1, 2, undefined, null].map((it): ValueHolder => ({value: it}));
59 | expect(arrayContainsAll(array1, array2, (it) => it.value)).toBe(false);
60 | });
61 |
62 | test("non-matching arrays with a custom matcher should not match", () => {
63 | const array1 = [0, 1, 2];
64 | const array2 = ["3", 4, 5];
65 | expect(arrayContainsAll(array1, array2, undefined, compareTypes)).toBe(false);
66 | });
67 | });
68 |
69 | describe("groupArray", () => {
70 | test("an empty array should produce an empty map", () => {
71 | expect(groupArray([], () => undefined).size).toBe(0);
72 | });
73 |
74 | test("a map with proper keys should be produced", () => {
75 | const redThings = ["apple", "lava", "ruby"].map((it): KeyValueHolder => ({key: "red", value: it}));
76 | const greenThings = ["grass", "leaf"].map((it): KeyValueHolder => ({key: "green", value: it}));
77 | const blueThings = ["water"].map((it): KeyValueHolder => ({key: "blue", value: it}));
78 |
79 | const array = [...redThings, ...greenThings, ...blueThings];
80 |
81 | const groupedMap = groupArray(array, (it) => it.key);
82 |
83 | expect(groupedMap.get("red")).toEqual(expect.arrayContaining(redThings));
84 | expect(groupedMap.get("green")).toEqual(expect.arrayContaining(greenThings));
85 | expect(groupedMap.get("blue")).toEqual(expect.arrayContaining(blueThings));
86 | });
87 | });
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": ".",
4 | "outDir": "dist",
5 | "target": "ES2021",
6 | "lib": [
7 | "es5",
8 | "es6",
9 | "es2017",
10 | "es2018",
11 | "es2019",
12 | "es2020",
13 | "es2021",
14 | "dom",
15 | "dom.iterable"
16 | ],
17 | "skipLibCheck": true,
18 | "esModuleInterop": true,
19 | "allowSyntheticDefaultImports": true,
20 | "strict": true,
21 | "forceConsistentCasingInFileNames": true,
22 | "moduleResolution": "node",
23 | "resolveJsonModule": true,
24 | "isolatedModules": false,
25 | "noImplicitThis": true,
26 | "noImplicitAny": true,
27 | "strictNullChecks": true,
28 | "jsx": "react",
29 | "noFallthroughCasesInSwitch": true,
30 | "paths": {
31 | "shared/*": ["./src/*"],
32 | "platform-components/*": ["./browser/*"]
33 | },
34 | "sourceMap": true,
35 | "inlineSources": true
36 | },
37 | "include": [
38 | "src",
39 | "browser",
40 | "windows/web",
41 | "index.d.ts",
42 | "window.ts"
43 | ]
44 | }
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | /* eslint-env node */
2 | const webpack = require("webpack");
3 | const path = require("path");
4 | const fs = require("fs");
5 | const ForkTsCheckerWebpackPlugin = require("fork-ts-checker-webpack-plugin");
6 | // const ESLintPlugin = require("eslint-webpack-plugin");
7 | const CopyPlugin = require("copy-webpack-plugin");
8 | const WorkboxPlugin = require("workbox-webpack-plugin");
9 |
10 | module.exports = (env) => ({
11 | entry: "./src/index.tsx",
12 | target: "web",
13 | mode: env.WEBPACK_SERVE ? "development" : "production",
14 | devtool: env.WEBPACK_SERVE ? "cheap-source-map" : "source-map",
15 | devServer: {
16 | static: {
17 | directory: path.join(__dirname, "public")
18 | },
19 | port: 8080,
20 | https: env.secure ? {
21 | key: fs.readFileSync("webpack.key"),
22 | cert: fs.readFileSync("webpack.crt"),
23 | } : undefined
24 | },
25 | output: {
26 | path: path.resolve(__dirname, "build"),
27 | filename: "index.js",
28 | assetModuleFilename: "res/[hash][ext][query]",
29 | publicPath: "",
30 | clean: true
31 | },
32 | module: {
33 | rules: [
34 | {
35 | test: /\.ts(x)?$/,
36 | loader: "ts-loader",
37 | exclude: /node_modules/,
38 | options: {
39 | transpileOnly: true
40 | }
41 | },
42 | {
43 | enforce: "pre",
44 | test: /\.js$/,
45 | loader: "source-map-loader"
46 | },
47 | {
48 | test: /\.css$/,
49 | use: [
50 | "style-loader",
51 | "css-loader"
52 | ],
53 | exclude: /\.module\.css$/
54 | },
55 | {
56 | test: /\.css$/,
57 | use: [
58 | "style-loader",
59 | {
60 | loader: "css-loader",
61 | options: {
62 | importLoaders: 1,
63 | modules: true
64 | }
65 | }
66 | ],
67 | include: /\.module\.css$/
68 | },
69 | {
70 | test: /\.(svg)|(wav)$/,
71 | type: "asset/resource"
72 | },
73 | {
74 | test: /\.md$/,
75 | type: "asset/source"
76 | }
77 | ]
78 | },
79 | resolve: {
80 | extensions: [
81 | ".tsx",
82 | ".ts",
83 | ".js"
84 | ],
85 | alias: {
86 | "shared": path.resolve(__dirname, "src")
87 | }
88 | },
89 | optimization: {
90 | usedExports: true
91 | },
92 | plugins: [
93 | new ForkTsCheckerWebpackPlugin(),
94 | /* new ESLintPlugin({
95 | files: ["src", "browser", "electron-main", "electron-renderer"],
96 | extensions: ["js", "jsx", "ts", "tsx"]
97 | }), */
98 | new CopyPlugin({
99 | patterns: [
100 | {from: "public"}
101 | ]
102 | }),
103 | new webpack.DefinePlugin({
104 | "WPEnv.ENVIRONMENT": JSON.stringify(env.WEBPACK_SERVE ? "development" : "production"),
105 | "WPEnv.PACKAGE_VERSION": JSON.stringify(process.env.npm_package_version),
106 | "WPEnv.RELEASE_HASH": "\"undefined\"",
107 | "WPEnv.BUILD_DATE": Date.now()
108 | }),
109 | ].concat(!env.WEBPACK_SERVE ? new WorkboxPlugin.GenerateSW() : [])
110 | });
--------------------------------------------------------------------------------