diff --git a/.github/workflows/ci-common.yml b/.github/workflows/ci-common.yml new file mode 100644 index 0000000..b1f2adc --- /dev/null +++ b/.github/workflows/ci-common.yml @@ -0,0 +1,66 @@ +name: CI checks for common workspace + +on: + pull_request: + branches: ['main'] + paths: ['common/**/*', '!**/*.{txt,md}'] + push: + branches: ['main'] + paths: ['common/**/*', '!**/*.{txt,md}'] + +defaults: + run: + working-directory: 'common' +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + cache: 'yarn' + - name: Install yarn + run: npm install -g yarn + - name: Install dependencies + run: yarn + - name: Run tests + run: yarn test + eslint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + cache: 'yarn' + - name: Install yarn + run: npm install -g yarn + - name: Install dependencies + run: yarn + - name: Run ESLint + run: yarn eslint . + prettier: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + cache: 'yarn' + - name: Install yarn + run: npm install -g yarn + - name: Install dependencies + run: yarn + - name: Run Prettier + run: yarn prettier . --check --ignore-unknown + typecheck: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + cache: 'yarn' + - name: Install yarn + run: npm install -g yarn + - name: Install dependencies + run: yarn + - name: Run tsc to check types + run: yarn tsc --noEmit --strict diff --git a/common/eslint.config.mjs b/common/eslint.config.mjs new file mode 100644 index 0000000..0c9cd26 --- /dev/null +++ b/common/eslint.config.mjs @@ -0,0 +1,17 @@ +import globals from 'globals' +import pluginJs from '@eslint/js' +import tseslint from 'typescript-eslint' + +/** @type {import('eslint').Linter.Config[]} */ +export default [ + { files: ['**/*.{js,mjs,cjs,ts}'] }, + { languageOptions: { globals: { ...globals.browser, ...globals.node } } }, + pluginJs.configs.recommended, + ...tseslint.configs.recommended, + { + rules: { + '@typescript-eslint/no-explicit-any': 'off', // disable this particular rule for now, since the `any` annotations are very tricky to get rid of + '@typescript-eslint/no-unused-expressions': 'off', // turn this off too for now, otherwise ESLint says "Cannot read properties of undefined" + }, + }, +] diff --git a/common/jest.config.js b/common/jest.config.js index 22b1bda..8f9b097 100644 --- a/common/jest.config.js +++ b/common/jest.config.js @@ -1,11 +1,11 @@ /** @type {import('ts-jest').JestConfigWithTsJest} */ -const { pathsToModuleNameMapper } = require('ts-jest') -const { compilerOptions } = require('./tsconfig') +import { pathsToModuleNameMapper } from 'ts-jest' +import tsconfig from './tsconfig.js' -module.exports = { +export default { preset: 'ts-jest', testEnvironment: 'node', - moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths, { + moduleNameMapper: pathsToModuleNameMapper(tsconfig.compilerOptions.paths, { prefix: '/', }), testMatch: ['**/*.test.ts'], diff --git a/common/package.json b/common/package.json index b17f1df..490402c 100644 --- a/common/package.json +++ b/common/package.json @@ -1,5 +1,6 @@ { "name": "common", + "type": "module", "version": "1.0.0", "private": true, "scripts": { @@ -25,11 +26,15 @@ "zod": "3.21.4" }, "devDependencies": { + "@eslint/js": "9.32.0", "@types/jest": "29.2.4", "@types/lodash": "4.14.178", "@types/string-similarity": "4.0.0", + "eslint": "9.32.0", + "globals": "16.3.0", "jest": "29.3.1", "supabase": "2.15.8", - "ts-jest": "29.0.3" + "ts-jest": "29.0.3", + "typescript-eslint": "8.39.0" } } diff --git a/common/src/api/schema.ts b/common/src/api/schema.ts index d0374b0..01a6dcd 100644 --- a/common/src/api/schema.ts +++ b/common/src/api/schema.ts @@ -36,9 +36,9 @@ type APIGenericSchema = { cache?: string } -let _apiTypeCheck: { [x: string]: APIGenericSchema } +type apiTypeCheck = { [x: string]: APIGenericSchema } -export const API = (_apiTypeCheck = { +export const API: apiTypeCheck = { health: { method: 'GET', authed: false, @@ -476,7 +476,7 @@ export const API = (_apiTypeCheck = { radius: z.number().min(1).max(500), }), }, -} as const) +} as const export type APIPath = keyof typeof API export type APISchema = (typeof API)[N] diff --git a/common/src/api/websocket-client.ts b/common/src/api/websocket-client.ts index 74ee83e..ae30565 100644 --- a/common/src/api/websocket-client.ts +++ b/common/src/api/websocket-client.ts @@ -86,7 +86,7 @@ export class APIRealtimeClient { // to connect, so we need to turn on our reconnect in that case this.waitAndReconnect() } - this.ws.onopen = (_ev) => { + this.ws.onopen = () => { if (VERBOSE_LOGGING) { console.info('API websocket opened.') } diff --git a/common/src/api/zod-types.ts b/common/src/api/zod-types.ts index c3cab69..853266c 100644 --- a/common/src/api/zod-types.ts +++ b/common/src/api/zod-types.ts @@ -8,7 +8,6 @@ export const arraybeSchema = z .or(z.string()) .transform(arrify) -// @ts-ignore export const contentSchema: z.ZodType = z.lazy(() => z.intersection( z.record(z.any()), diff --git a/common/src/envs/is-prod.ts b/common/src/envs/is-prod.ts index 76d98ff..251d620 100644 --- a/common/src/envs/is-prod.ts +++ b/common/src/envs/is-prod.ts @@ -1,3 +1,5 @@ +import admin from 'firebase-admin' + export const isProd = () => { // For cloud run API service if (process.env.ENVIRONMENT) { @@ -7,8 +9,6 @@ export const isProd = () => { return process.env.NEXT_PUBLIC_FIREBASE_ENV == 'PROD' } else { // For local scripts and cloud functions - // eslint-disable-next-line @typescript-eslint/no-var-requires - const admin = require('firebase-admin') return admin.app().options.projectId === 'polylove' } } diff --git a/common/src/love/lover.ts b/common/src/love/lover.ts index f334178..cf2da97 100644 --- a/common/src/love/lover.ts +++ b/common/src/love/lover.ts @@ -1,6 +1,6 @@ import { Row, run, SupabaseClient } from 'common/supabase/utils' import { User } from 'common/user' -import { Database } from 'common/supabase/schema' +//import { Database } from 'common/supabase/schema' export type LoverRow = Row<'lovers'> export type Lover = LoverRow & { user: User } diff --git a/common/src/secrets.ts b/common/src/secrets.ts index fe85a2d..c6dc647 100644 --- a/common/src/secrets.ts +++ b/common/src/secrets.ts @@ -91,7 +91,7 @@ export const getServiceAccountCredentials = (env: 'PROD' | 'DEV') => { try { return JSON.parse(readFileSync(keyPath, { encoding: 'utf8' })) - } catch (e) { + } catch { throw new Error(`Failed to load service account key from ${keyPath}.`) } } diff --git a/common/src/socials.test.ts b/common/src/socials.test.ts index e1bfbb2..ce84c8e 100644 --- a/common/src/socials.test.ts +++ b/common/src/socials.test.ts @@ -23,7 +23,9 @@ describe('strip', () => { describe('instagram', () => { it('should strip instagram URLs', () => { - expect(strip('instagram', 'https://instagram.com/username')).toBe('username') + expect(strip('instagram', 'https://instagram.com/username')).toBe( + 'username' + ) expect(strip('instagram', 'instagram.com/username')).toBe('username') expect(strip('instagram', '@username')).toBe('username') expect(strip('instagram', 'username')).toBe('username') @@ -32,7 +34,9 @@ describe('strip', () => { describe('bluesky', () => { it('should strip bluesky URLs', () => { - expect(strip('bluesky', 'https://bsky.app/profile/username')).toBe('username') + expect(strip('bluesky', 'https://bsky.app/profile/username')).toBe( + 'username' + ) expect(strip('bluesky', 'bsky.app/profile/username')).toBe('username') expect(strip('bluesky', '@username')).toBe('username') expect(strip('bluesky', 'username')).toBe('username') @@ -41,16 +45,24 @@ describe('strip', () => { describe('mastodon', () => { it('should handle mastodon handles', () => { - expect(strip('mastodon', '@user@instance.social')).toBe('user@instance.social') - expect(strip('mastodon', 'user@instance.social')).toBe('user@instance.social') + expect(strip('mastodon', '@user@instance.social')).toBe( + 'user@instance.social' + ) + expect(strip('mastodon', 'user@instance.social')).toBe( + 'user@instance.social' + ) }) }) describe('linkedin', () => { it('should strip linkedin URLs', () => { - expect(strip('linkedin', 'https://linkedin.com/in/username')).toBe('username') + expect(strip('linkedin', 'https://linkedin.com/in/username')).toBe( + 'username' + ) expect(strip('linkedin', 'linkedin.com/in/username')).toBe('username') - expect(strip('linkedin', 'https://linkedin.com/company/companyname')).toBe('companyname') + expect( + strip('linkedin', 'https://linkedin.com/company/companyname') + ).toBe('companyname') expect(strip('linkedin', 'username')).toBe('username') }) }) @@ -59,22 +71,42 @@ describe('strip', () => { describe('getSocialUrl', () => { it('should generate correct URLs for each platform', () => { expect(getSocialUrl('x', 'username')).toBe('https://x.com/username') - expect(getSocialUrl('github', 'username')).toBe('https://github.com/username') - expect(getSocialUrl('instagram', 'username')).toBe('https://instagram.com/username') - expect(getSocialUrl('bluesky', 'username')).toBe('https://bsky.app/profile/username') - expect(getSocialUrl('mastodon', 'user@instance.social')).toBe('https://instance.social/@user') - expect(getSocialUrl('linkedin', 'username')).toBe('https://linkedin.com/in/username') - expect(getSocialUrl('facebook', 'username')).toBe('https://facebook.com/username') - expect(getSocialUrl('spotify', 'username')).toBe('https://open.spotify.com/user/username') + expect(getSocialUrl('github', 'username')).toBe( + 'https://github.com/username' + ) + expect(getSocialUrl('instagram', 'username')).toBe( + 'https://instagram.com/username' + ) + expect(getSocialUrl('bluesky', 'username')).toBe( + 'https://bsky.app/profile/username' + ) + expect(getSocialUrl('mastodon', 'user@instance.social')).toBe( + 'https://instance.social/@user' + ) + expect(getSocialUrl('linkedin', 'username')).toBe( + 'https://linkedin.com/in/username' + ) + expect(getSocialUrl('facebook', 'username')).toBe( + 'https://facebook.com/username' + ) + expect(getSocialUrl('spotify', 'username')).toBe( + 'https://open.spotify.com/user/username' + ) }) it('should handle custom website URLs', () => { expect(getSocialUrl('site', 'example.com')).toBe('https://example.com') - expect(getSocialUrl('site', 'https://example.com')).toBe('https://example.com') + expect(getSocialUrl('site', 'https://example.com')).toBe( + 'https://example.com' + ) }) it('should handle discord user IDs and default invite', () => { - expect(getSocialUrl('discord', '123456789012345678')).toBe('https://discord.com/users/123456789012345678') - expect(getSocialUrl('discord', 'not-an-id')).toBe('https://discord.com/invite/AYDw8dbrGS') + expect(getSocialUrl('discord', '123456789012345678')).toBe( + 'https://discord.com/users/123456789012345678' + ) + expect(getSocialUrl('discord', 'not-an-id')).toBe( + 'https://discord.com/invite/AYDw8dbrGS' + ) }) -}) \ No newline at end of file +}) diff --git a/common/src/util/algos.ts b/common/src/util/algos.ts index 7fe2118..95b7a98 100644 --- a/common/src/util/algos.ts +++ b/common/src/util/algos.ts @@ -5,7 +5,7 @@ export function binarySearch( ) { let mid = 0 let i = 0 - while (true) { + while (i <= 100000) { mid = min + (max - min) / 2 // Break once we've reached max precision. diff --git a/common/src/util/api.ts b/common/src/util/api.ts index 9005cd3..4849f6c 100644 --- a/common/src/util/api.ts +++ b/common/src/util/api.ts @@ -15,7 +15,7 @@ export const typedAPICall =

( ) => { // parse any params that should part of the path (like market/:id) const newParams: any = {} - let url = getApiUrl(path) + let url: string = getApiUrl(String(path)) // `path` could be a string or number, we don't know forEach(params, (v, k) => { if (url.includes(`:${k}`)) { url = url.replace(`:${k}`, v + '') diff --git a/common/src/util/json.ts b/common/src/util/json.ts index bbdf56e..e772069 100644 --- a/common/src/util/json.ts +++ b/common/src/util/json.ts @@ -1,7 +1,7 @@ export const safeJsonParse = (json: string | undefined | null) => { try { - return JSON.parse(json ?? '') - } catch (e) { + return JSON.parse(json ?? '') + } catch { return null } } diff --git a/common/src/util/parse.ts b/common/src/util/parse.ts index 74713b1..11a7922 100644 --- a/common/src/util/parse.ts +++ b/common/src/util/parse.ts @@ -2,7 +2,7 @@ import { getText, getSchema, getTextSerializersFromSchema, - Node, + //Node, JSONContent, } from '@tiptap/core' import { Node as ProseMirrorNode } from '@tiptap/pm/model' diff --git a/common/src/util/random.ts b/common/src/util/random.ts index b9c34cc..6162281 100644 --- a/common/src/util/random.ts +++ b/common/src/util/random.ts @@ -51,4 +51,4 @@ export const shuffle = (array: unknown[], rand: () => number) => { const swapIndex = i + Math.floor(rand() * (array.length - i)) ;[array[i], array[swapIndex]] = [array[swapIndex], array[i]] } -} \ No newline at end of file +} diff --git a/common/src/util/string.ts b/common/src/util/string.ts index 4dae265..23e1c73 100644 --- a/common/src/util/string.ts +++ b/common/src/util/string.ts @@ -4,4 +4,4 @@ export function removeEmojis(str: string) { /[\u{1F600}-\u{1F64F}\u{1F300}-\u{1F5FF}\u{1F680}-\u{1F6FF}\u{1F700}-\u{1F77F}\u{1F780}-\u{1F7FF}\u{1F800}-\u{1F8FF}\u{1F900}-\u{1F9FF}\u{1FA00}-\u{1FA6F}\u{1FA70}-\u{1FAFF}\u{2600}-\u{26FF}\u{2700}-\u{27BF}\u{2300}-\u{23FF}\u{2B50}\u{2B55}\u{2934}\u{2935}\u{2B05}\u{2B06}\u{2B07}\u{2B1B}\u{2B1C}\u{2B50}\u{2B55}\u{3030}\u{303D}\u{3297}\u{3299}\u{FE0F}]/gu, '' ) -} \ No newline at end of file +} diff --git a/common/tsconfig.js b/common/tsconfig.js new file mode 100644 index 0000000..e9bb67a --- /dev/null +++ b/common/tsconfig.js @@ -0,0 +1,22 @@ +// hacky workaround for not being able to import tsconfig.json directly +export default { + compilerOptions: { + rootDir: 'src', + composite: true, + module: 'commonjs', + moduleResolution: 'node', + noImplicitReturns: true, + outDir: 'lib', + tsBuildInfoFile: 'lib/tsconfig.tsbuildinfo', + sourceMap: true, + strict: true, + target: 'es2022', + skipLibCheck: true, + paths: { + 'common/*': ['./src/*', '../lib/*'], + }, + resolveJsonModule: true, + esModuleInterop: true, + }, + include: ['src/**/*.ts'], +} diff --git a/common/tsconfig.json b/common/tsconfig.json index 2506d8d..09eab2a 100644 --- a/common/tsconfig.json +++ b/common/tsconfig.json @@ -13,7 +13,9 @@ "skipLibCheck": true, "paths": { "common/*": ["./src/*", "../lib/*"] - } + }, + "resolveJsonModule": true, + "esModuleInterop": true }, "include": ["src/**/*.ts"] }