From f5809006093af1a7e83b8091bb5006d810fcb71c Mon Sep 17 00:00:00 2001 From: marcus-sa Date: Fri, 12 Jan 2024 12:00:56 +0100 Subject: [PATCH] feat: add initial featurebase integration TODO: - [x] Allow client integrations to be lazy loaded - [ ] Allow server integrations to be lazy loaded --- .eslintrc.json | 3 +- apps/client/jest.config.ts | 22 ------- apps/client/project.json | 7 +- apps/client/src/app/config.ts | 4 +- .../integrations/integrations.module.spec.ts | 27 ++++++++ .../app/integrations/integrations.module.ts | 23 +++++-- apps/client/src/test-setup.ts | 24 ++++--- apps/client/tsconfig.app.json | 4 +- apps/client/tsconfig.editor.json | 2 +- apps/client/tsconfig.spec.json | 6 +- apps/client/vite.config.ts | 58 ++++++++++++----- .../src/integrations/integrations.module.ts | 9 +++ libs/api/shared/src/lib/entities/user.ts | 1 + libs/client/src/lib/config.ts | 5 +- libs/client/src/lib/dynamic-module.ts | 16 +++-- libs/integrations/featurebase/README.md | 1 + .../featurebase/client/.eslintrc.json | 33 ++++++++++ .../featurebase/client/jest.config.ts | 11 ++++ .../featurebase/client/ng-packagr.json | 0 .../featurebase/client/package.json | 8 +++ .../featurebase/client/project.json | 34 ++++++++++ .../featurebase/client/src/index.ts | 2 + .../featurebase/client/src/lib/config.ts | 29 +++++++++ .../client/src/lib/featurebase.module.ts | 17 +++++ .../client/src/lib/featurebase.service.ts | 65 +++++++++++++++++++ .../featurebase/client/tsconfig.json | 13 ++++ .../featurebase/client/tsconfig.lib.json | 10 +++ .../featurebase/client/tsconfig.spec.json | 14 ++++ .../featurebase/server/.eslintrc.json | 33 ++++++++++ .../featurebase/server/jest.config.ts | 11 ++++ .../featurebase/server/package.json | 10 +++ .../featurebase/server/project.json | 34 ++++++++++ .../featurebase/server/src/index.ts | 3 + .../featurebase/server/src/lib/config.ts | 3 + .../server/src/lib/featurebase.controller.ts | 28 ++++++++ .../server/src/lib/featurebase.module.ts | 10 +++ .../featurebase/server/tsconfig.json | 13 ++++ .../featurebase/server/tsconfig.lib.json | 10 +++ .../featurebase/server/tsconfig.spec.json | 14 ++++ libs/integrations/supabase/README.md | 2 +- package.json | 2 + pnpm-lock.yaml | 57 ++++++++++++++++ tsconfig.base.json | 2 + 43 files changed, 606 insertions(+), 74 deletions(-) delete mode 100644 apps/client/jest.config.ts create mode 100644 apps/client/src/app/integrations/integrations.module.spec.ts create mode 100644 libs/integrations/featurebase/README.md create mode 100644 libs/integrations/featurebase/client/.eslintrc.json create mode 100644 libs/integrations/featurebase/client/jest.config.ts create mode 100644 libs/integrations/featurebase/client/ng-packagr.json create mode 100644 libs/integrations/featurebase/client/package.json create mode 100644 libs/integrations/featurebase/client/project.json create mode 100644 libs/integrations/featurebase/client/src/index.ts create mode 100644 libs/integrations/featurebase/client/src/lib/config.ts create mode 100644 libs/integrations/featurebase/client/src/lib/featurebase.module.ts create mode 100644 libs/integrations/featurebase/client/src/lib/featurebase.service.ts create mode 100644 libs/integrations/featurebase/client/tsconfig.json create mode 100644 libs/integrations/featurebase/client/tsconfig.lib.json create mode 100644 libs/integrations/featurebase/client/tsconfig.spec.json create mode 100644 libs/integrations/featurebase/server/.eslintrc.json create mode 100644 libs/integrations/featurebase/server/jest.config.ts create mode 100644 libs/integrations/featurebase/server/package.json create mode 100644 libs/integrations/featurebase/server/project.json create mode 100644 libs/integrations/featurebase/server/src/index.ts create mode 100644 libs/integrations/featurebase/server/src/lib/config.ts create mode 100644 libs/integrations/featurebase/server/src/lib/featurebase.controller.ts create mode 100644 libs/integrations/featurebase/server/src/lib/featurebase.module.ts create mode 100644 libs/integrations/featurebase/server/tsconfig.json create mode 100644 libs/integrations/featurebase/server/tsconfig.lib.json create mode 100644 libs/integrations/featurebase/server/tsconfig.spec.json diff --git a/.eslintrc.json b/.eslintrc.json index 1c44a0d..dc3ffe3 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -84,7 +84,6 @@ "@typescript-eslint/no-unsafe-call": "warn", "@typescript-eslint/no-unsafe-assignment": "warn", "@typescript-eslint/no-unsafe-return": "warn", - "@typescript-eslint/no-extraneous-class": "off", "@typescript-eslint/require-await": "warn", "@typescript-eslint/no-misused-promises": [ @@ -93,7 +92,7 @@ "checksVoidReturn": false } ], - "@typescript-eslint/await-thenable": ["error"], + "@typescript-eslint/await-thenable": ["off"], "@typescript-eslint/no-non-null-assertion": ["off"], "@typescript-eslint/no-inferrable-types": ["off"], "@typescript-eslint/explicit-module-boundary-types": ["warn"], diff --git a/apps/client/jest.config.ts b/apps/client/jest.config.ts deleted file mode 100644 index cdc42dd..0000000 --- a/apps/client/jest.config.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* eslint-disable */ -export default { - displayName: 'client', - preset: '../../jest.preset.js', - setupFilesAfterEnv: ['/src/test-setup.ts'], - coverageDirectory: '../../coverage/apps/client', - transform: { - '^.+\\.(ts|mjs|js|html)$': [ - 'jest-preset-angular', - { - tsconfig: '/tsconfig.spec.json', - stringifyContentPathRegex: '\\.(html|svg)$', - }, - ], - }, - transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)'], - snapshotSerializers: [ - 'jest-preset-angular/build/serializers/no-ng-attributes', - 'jest-preset-angular/build/serializers/ng-snapshot', - 'jest-preset-angular/build/serializers/html-comment', - ], -}; diff --git a/apps/client/project.json b/apps/client/project.json index 103f990..fd8ce03 100644 --- a/apps/client/project.json +++ b/apps/client/project.json @@ -74,10 +74,11 @@ "outputs": ["{options.outputFile}"] }, "test": { - "executor": "@nx/jest:jest", - "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], + "executor": "@nx/vite:test", + "outputs": ["{options.reportsDirectory}"], "options": { - "jestConfig": "apps/client/jest.config.ts" + "passWithNoTests": true, + "reportsDirectory": "../../coverage/{projectRoot}" } } } diff --git a/apps/client/src/app/config.ts b/apps/client/src/app/config.ts index 2a9ece2..a5bdfd5 100644 --- a/apps/client/src/app/config.ts +++ b/apps/client/src/app/config.ts @@ -30,13 +30,13 @@ export const appConfig: ApplicationConfig = { provide: ApexClientConfig, useValue: clientConfig, }, - importProvidersDynamicallyFrom( + await importProvidersDynamicallyFrom( new IntegrationsModule(clientConfig.integrations), ), { provide: APP_INITIALIZER, deps: [AuthService], - useFactory: (auth: AuthService) => () => auth.initialize(), + useFactory: (auth: AuthService) => async () => auth.initialize(), multi: true, }, ], diff --git a/apps/client/src/app/integrations/integrations.module.spec.ts b/apps/client/src/app/integrations/integrations.module.spec.ts new file mode 100644 index 0000000..9b8e20d --- /dev/null +++ b/apps/client/src/app/integrations/integrations.module.spec.ts @@ -0,0 +1,27 @@ +import { test, vitest } from 'vitest'; +import { TestBed } from '@angular/core/testing'; +import { NgModule } from '@angular/core'; + +import { IntegrationsModule } from './integrations.module'; + +test('register', async () => { + // @ts-expect-error unnecessary + const integrations = new IntegrationsModule(); + integrations.process = vitest.fn(); + + @NgModule({}) + class TestModule {} + + class TestConfig {} + + const config = new TestConfig(); + + await integrations.register(config, () => TestModule); + + const testBed = TestBed.configureTestingModule({ + imports: [await integrations.configure()], + }); + + const config2 = testBed.inject(TestConfig); + expect(config2).toBe(config); +}); diff --git a/apps/client/src/app/integrations/integrations.module.ts b/apps/client/src/app/integrations/integrations.module.ts index e829e1d..be3e57f 100644 --- a/apps/client/src/app/integrations/integrations.module.ts +++ b/apps/client/src/app/integrations/integrations.module.ts @@ -1,16 +1,18 @@ import { ClassType } from '@deepkit/core'; import { DynamicNgModule, IntegrationsConfig } from '@apex/client'; -import { SupabaseModule } from '@apex/integrations/supabase/client'; export class IntegrationsModule extends DynamicNgModule { constructor(private readonly config: IntegrationsConfig) { super(); } - private register(config: any, module: ClassType): void { + async register( + config: any, + loadModule: () => Promise | ClassType, + ): Promise { if (config) { - this.addImport(module); + this.addImport(await loadModule()); this.addProvider({ provide: config.constructor, useValue: config, @@ -18,7 +20,18 @@ export class IntegrationsModule extends DynamicNgModule { } } - process(): void { - this.register(this.config.supabase, SupabaseModule); + async process(): Promise { + await Promise.all([ + this.register(this.config.supabase, async () => + import('@apex/integrations/supabase/client').then( + m => m.SupabaseModule, + ), + ), + this.register(this.config.featurebase, async () => + import('@apex/integrations/featurebase/client').then( + m => m.FeaturebaseModule, + ), + ), + ]); } } diff --git a/apps/client/src/test-setup.ts b/apps/client/src/test-setup.ts index ab1eeeb..f955a4c 100644 --- a/apps/client/src/test-setup.ts +++ b/apps/client/src/test-setup.ts @@ -1,8 +1,16 @@ -// @ts-expect-error https://thymikee.github.io/jest-preset-angular/docs/getting-started/test-environment -globalThis.ngJest = { - testEnvironmentOptions: { - errorOnUnknownElements: true, - errorOnUnknownProperties: true, - }, -}; -import 'jest-preset-angular/setup-jest'; +import '@analogjs/vite-plugin-angular/setup-vitest'; +import '@testing-library/jest-dom/vitest'; + +/** + * Initialize TestBed for all tests + */ +import { TestBed } from '@angular/core/testing'; +import { + BrowserDynamicTestingModule, + platformBrowserDynamicTesting, +} from '@angular/platform-browser-dynamic/testing'; + +TestBed.initTestEnvironment( + BrowserDynamicTestingModule, + platformBrowserDynamicTesting(), +); diff --git a/apps/client/tsconfig.app.json b/apps/client/tsconfig.app.json index fff4a41..0904f33 100644 --- a/apps/client/tsconfig.app.json +++ b/apps/client/tsconfig.app.json @@ -2,9 +2,9 @@ "extends": "./tsconfig.json", "compilerOptions": { "outDir": "../../dist/out-tsc", - "types": [] + "types": ["vite/client"] }, "files": ["src/main.ts"], "include": ["src/**/*.d.ts"], - "exclude": ["jest.config.ts", "src/**/*.test.ts", "src/**/*.spec.ts"] + "exclude": ["vite.config.ts", "src/**/*.test.ts", "src/**/*.spec.ts"] } diff --git a/apps/client/tsconfig.editor.json b/apps/client/tsconfig.editor.json index 8ae117d..2f59fc4 100644 --- a/apps/client/tsconfig.editor.json +++ b/apps/client/tsconfig.editor.json @@ -2,6 +2,6 @@ "extends": "./tsconfig.json", "include": ["src/**/*.ts"], "compilerOptions": { - "types": ["jest", "node"] + "types": ["vitest/client", "vitest/globals", "node"] } } diff --git a/apps/client/tsconfig.spec.json b/apps/client/tsconfig.spec.json index 53fbfcd..ab662e9 100644 --- a/apps/client/tsconfig.spec.json +++ b/apps/client/tsconfig.spec.json @@ -2,13 +2,11 @@ "extends": "./tsconfig.json", "compilerOptions": { "outDir": "../../dist/out-tsc", - "module": "commonjs", - "target": "es2016", - "types": ["jest", "node"] + "types": ["vitest/globals", "node"] }, "files": ["src/test-setup.ts"], "include": [ - "jest.config.ts", + "vite.config.ts", "src/**/*.test.ts", "src/**/*.spec.ts", "src/**/*.d.ts" diff --git a/apps/client/vite.config.ts b/apps/client/vite.config.ts index b8712ee..08f825c 100644 --- a/apps/client/vite.config.ts +++ b/apps/client/vite.config.ts @@ -1,25 +1,49 @@ -import { join } from 'path'; +import { join } from 'node:path'; import { defineConfig } from 'vite'; import { deepkitType } from '@deepkit/vite'; import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; +import { angular } from '@analogjs/vite-plugin-angular/src/lib/angular-vite-plugin'; -export default defineConfig({ - optimizeDeps: { - exclude: ['@deepkit/rpc'], - esbuildOptions: { +export default defineConfig(({ mode }) => { + const isTestMode = mode === 'test'; + const tsConfig = isTestMode ? join(__dirname, 'tsconfig.spec.json') : join(__dirname, 'tsconfig.app.json'); + + return { + optimizeDeps: { + exclude: ['@deepkit/rpc'], + esbuildOptions: { + target: 'esnext', + }, + }, + build: { target: 'esnext', }, - }, - build: { - target: 'esnext', - }, - plugins: [ - nxViteTsPaths(), - deepkitType({ - compilerOptions: { - sourceMap: true, + plugins: [ + nxViteTsPaths(), + isTestMode && angular({ tsconfig: tsConfig }), + deepkitType({ + compilerOptions: { + sourceMap: true, + }, + tsConfig, + }), + ], + test: { + globals: true, + cache: { + dir: '../../node_modules/.vitest', }, - tsConfig: join(__dirname, 'tsconfig.app.json'), - }), - ], + reporters: ['default'], + environment: 'jsdom', + setupFiles: ['src/test-setup.ts'], + include: ['src/**/*.spec.ts'], + deps: { + optimizer: { + web: { + exclude: ['rxjs', '@deepkit/rpc'], + }, + } + }, + }, + }; }); diff --git a/apps/server/src/integrations/integrations.module.ts b/apps/server/src/integrations/integrations.module.ts index 69ba840..0c42c15 100644 --- a/apps/server/src/integrations/integrations.module.ts +++ b/apps/server/src/integrations/integrations.module.ts @@ -4,9 +4,14 @@ import { SupabaseConfig, SupabaseModule, } from '@apex/integrations/supabase/server'; +import { + FeaturebaseConfig, + FeaturebaseModule, +} from '@apex/integrations/featurebase/server'; export class IntegrationsConfig { readonly supabase?: SupabaseConfig; + readonly featurebase?: FeaturebaseConfig; readonly thirdweb?: unknown; } @@ -18,5 +23,9 @@ export class IntegrationsModule extends createModule({ if (this.config.supabase) { this.addImport(new SupabaseModule(this.config.supabase)); } + + if (this.config.featurebase) { + this.addImport(new FeaturebaseModule(this.config.featurebase)); + } } } diff --git a/libs/api/shared/src/lib/entities/user.ts b/libs/api/shared/src/lib/entities/user.ts index 93b343d..211830c 100644 --- a/libs/api/shared/src/lib/entities/user.ts +++ b/libs/api/shared/src/lib/entities/user.ts @@ -22,6 +22,7 @@ export class User { readonly inventory: Inventory & BackReference = new Inventory(this); readonly rooms: readonly Room[] & BackReference = []; readonly username: string & Unique; + readonly email?: string & Unique; readonly look: string; readonly online: boolean = false; readonly friends: readonly Friend[] & diff --git a/libs/client/src/lib/config.ts b/libs/client/src/lib/config.ts index 81a873e..cd481d5 100644 --- a/libs/client/src/lib/config.ts +++ b/libs/client/src/lib/config.ts @@ -2,10 +2,13 @@ import { deserialize } from '@deepkit/type'; // FIXME: circular dependency // import { SupabaseConfig } from '@apex/integrations/supabase/client'; +// import { FeaturebaseConfig } from '@apex/integrations/featurebase/client'; +// import { ThirdWebConfig } from '@apex/integrations/thirdweb/client'; export class IntegrationsConfig { readonly supabase?: unknown; // SupabaseConfig - readonly thirdweb?: unknown; + readonly featurebase?: unknown; // Featurebase + readonly thirdweb?: unknown; // ThirdWebConfig } export class ApexClientConfig { diff --git a/libs/client/src/lib/dynamic-module.ts b/libs/client/src/lib/dynamic-module.ts index 2f73e45..15480d3 100644 --- a/libs/client/src/lib/dynamic-module.ts +++ b/libs/client/src/lib/dynamic-module.ts @@ -10,10 +10,14 @@ import { setFactoryDef, setInjectorDef, setNgModuleDef } from './utils'; export type NgModuleProvider = Provider | EnvironmentProviders; -export function importProvidersDynamicallyFrom( +export async function importProvidersDynamicallyFrom( ...modules: readonly DynamicNgModule[] -): EnvironmentProviders { - return importProvidersFrom(...modules.map(module => module.configure())); +): Promise { + let configuredModules: readonly ModuleWithProviders[] = []; + for (const module of modules) { + configuredModules = [...configuredModules, await module.configure()]; + } + return importProvidersFrom(...configuredModules); } export abstract class DynamicNgModule { @@ -28,10 +32,10 @@ export abstract class DynamicNgModule { this.imports.add(module); } - protected abstract process(): void; + protected abstract process(): Promise | void; - configure(): ModuleWithProviders { - this.process(); + async configure(): Promise> { + await this.process(); setNgModuleDef(this.constructor, { type: this.constructor, diff --git a/libs/integrations/featurebase/README.md b/libs/integrations/featurebase/README.md new file mode 100644 index 0000000..487a28a --- /dev/null +++ b/libs/integrations/featurebase/README.md @@ -0,0 +1 @@ +# [Featurebase](https://featurebase.app) Integration diff --git a/libs/integrations/featurebase/client/.eslintrc.json b/libs/integrations/featurebase/client/.eslintrc.json new file mode 100644 index 0000000..5e8178c --- /dev/null +++ b/libs/integrations/featurebase/client/.eslintrc.json @@ -0,0 +1,33 @@ +{ + "extends": ["../../../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "parserOptions": { + "project": ["libs/integrations/featurebase/client/tsconfig.*?.json"] + }, + "rules": {} + }, + { + "files": ["*.ts", "*.tsx"], + "rules": {} + }, + { + "files": ["*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["*.json"], + "parser": "jsonc-eslint-parser", + "rules": { + "@nx/dependency-checks": [ + "error", + { + "ignoredFiles": ["{projectRoot}/rollup.config.{js,ts,mjs,mts}"] + } + ] + } + } + ] +} diff --git a/libs/integrations/featurebase/client/jest.config.ts b/libs/integrations/featurebase/client/jest.config.ts new file mode 100644 index 0000000..020cf8f --- /dev/null +++ b/libs/integrations/featurebase/client/jest.config.ts @@ -0,0 +1,11 @@ +/* eslint-disable */ +export default { + displayName: 'integrations-featurebase-client', + preset: '../../../../jest.preset.js', + transform: { + '^.+\\.[tj]s$': ['ts-jest', { tsconfig: '/tsconfig.spec.json' }], + }, + moduleFileExtensions: ['ts', 'js', 'html'], + testEnvironment: 'node', + coverageDirectory: '../../../../coverage/libs/integrations/featurebase/client', +}; diff --git a/libs/integrations/featurebase/client/ng-packagr.json b/libs/integrations/featurebase/client/ng-packagr.json new file mode 100644 index 0000000..e69de29 diff --git a/libs/integrations/featurebase/client/package.json b/libs/integrations/featurebase/client/package.json new file mode 100644 index 0000000..569144f --- /dev/null +++ b/libs/integrations/featurebase/client/package.json @@ -0,0 +1,8 @@ +{ + "name": "@apex/integrations/featurebase/client", + "version": "0.0.1", + "dependencies": {}, + "type": "module", + "module": "./index.js", + "reflection": true +} diff --git a/libs/integrations/featurebase/client/project.json b/libs/integrations/featurebase/client/project.json new file mode 100644 index 0000000..2d427ab --- /dev/null +++ b/libs/integrations/featurebase/client/project.json @@ -0,0 +1,34 @@ +{ + "name": "integrations-featurebase-client", + "$schema": "../../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/integrations/featurebase/client/src", + "projectType": "library", + "targets": { + "build": { + "executor": "@nx/rollup:rollup", + "outputs": ["{options.outputPath}"], + "options": { + "outputPath": "dist/{projectRoot}", + "main": "{projectRoot}/src/index.ts", + "tsConfig": "{projectRoot}/tsconfig.lib.json", + "assets": [], + "project": "{projectRoot}/package.json", + "compiler": "tsc", + "external": "all", + "format": ["esm"] + } + }, + "lint": { + "executor": "@nx/eslint:lint", + "outputs": ["{options.outputFile}"] + }, + "test": { + "executor": "@nx/jest:jest", + "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], + "options": { + "jestConfig": "{projectRoot}/jest.config.ts" + } + } + }, + "tags": [] +} diff --git a/libs/integrations/featurebase/client/src/index.ts b/libs/integrations/featurebase/client/src/index.ts new file mode 100644 index 0000000..e138b92 --- /dev/null +++ b/libs/integrations/featurebase/client/src/index.ts @@ -0,0 +1,2 @@ +export * from './lib/config'; +export * from './lib/featurebase.module'; diff --git a/libs/integrations/featurebase/client/src/lib/config.ts b/libs/integrations/featurebase/client/src/lib/config.ts new file mode 100644 index 0000000..bb5bf76 --- /dev/null +++ b/libs/integrations/featurebase/client/src/lib/config.ts @@ -0,0 +1,29 @@ +export class FeaturebaseChangelogConfig { + readonly fullscreenPopup?: boolean; + readonly usersName?: string; +} + +export class FeaturebasePortalConfig { + readonly fullScreen: boolean; + readonly initialPage: + | 'MainView' + | 'RoadmapView' + | 'CreatePost' + | 'PostsView' + | 'ChangelogView'; +} + +export class FeaturebaseFeedbackConfig { + readonly fullscreenPopup?: boolean; + readonly email?: string; +} + +export class FeaturebaseConfig { + readonly widget: 'changelog' | 'portal' | 'feedback'; + readonly organization: string; + readonly theme: 'dark' | 'light' = 'dark'; + readonly placement?: 'right' | 'left' | 'top' | 'bottom'; + readonly changelog?: FeaturebaseChangelogConfig; + readonly portal?: FeaturebasePortalConfig; + readonly feedback?: FeaturebaseFeedbackConfig; +} diff --git a/libs/integrations/featurebase/client/src/lib/featurebase.module.ts b/libs/integrations/featurebase/client/src/lib/featurebase.module.ts new file mode 100644 index 0000000..d13a6f6 --- /dev/null +++ b/libs/integrations/featurebase/client/src/lib/featurebase.module.ts @@ -0,0 +1,17 @@ +import { APP_INITIALIZER, NgModule } from '@angular/core'; + +import { FeaturebaseService } from './featurebase.service'; + +@NgModule({ + providers: [ + FeaturebaseService, + { + provide: APP_INITIALIZER, + deps: [FeaturebaseService], + useFactory: (featurebase: FeaturebaseService) => async () => + featurebase.initialize(), + multi: true, + }, + ], +}) +export class FeaturebaseModule {} diff --git a/libs/integrations/featurebase/client/src/lib/featurebase.service.ts b/libs/integrations/featurebase/client/src/lib/featurebase.service.ts new file mode 100644 index 0000000..029aa4a --- /dev/null +++ b/libs/integrations/featurebase/client/src/lib/featurebase.service.ts @@ -0,0 +1,65 @@ +import { Inject, Injectable } from '@angular/core'; +import { DOCUMENT } from '@angular/common'; +import { asyncOperation } from '@deepkit/core'; + +import { + FeaturebaseChangelogConfig, + FeaturebaseConfig, + FeaturebaseFeedbackConfig, + FeaturebasePortalConfig, +} from './config'; + +declare global { + interface Window { + Featurebase( + widget: 'initialize_portal_widget', + options: FeaturebasePortalConfig, + ): void; + Featurebase( + widget: 'initialize_feedback_widget', + options: FeaturebaseFeedbackConfig, + ): void; + Featurebase( + widget: 'initialize_changelog_widget', + options: FeaturebaseChangelogConfig, + ): void; + } +} + +@Injectable() +export class FeaturebaseService { + constructor( + private readonly config: FeaturebaseConfig, + @Inject(DOCUMENT) private readonly document: Document, + ) {} + + async initialize(): Promise { + const scriptContent = + '!(function(e,t){const a="featurebase-sdk";function n(){if(!t.getElementById(a)){var e=t.createElement("script");(e.id=a),(e.src="https://do.featurebase.app/js/sdk.js"),t.getElementsByTagName("script")[0].parentNode.insertBefore(e,t.getElementsByTagName("script")[0])}}"function"!=typeof e.Featurebase&&(e.Featurebase=function(){(e.Featurebase.q=e.Featurebase.q||[]).push(arguments)}),"complete"===t.readyState||"interactive"===t.readyState?n():t.addEventListener("DOMContentLoaded",n)})(window,document);'; + + const script = this.document.createElement('script'); + script.async = true; + + await asyncOperation((resolve, reject) => { + script.onload = resolve; + script.onerror = reject; + }); + + if (this.config.widget === 'changelog') { + if (!this.config.changelog) { + throw new Error('Missing changelog config'); + } + window.Featurebase('initialize_changelog_widget', this.config.changelog); + } else if (this.config.widget === 'feedback') { + if (!this.config.feedback) { + throw new Error('Missing feedback config'); + } + window.Featurebase('initialize_feedback_widget', this.config.feedback); + } else if (this.config.widget === 'portal') { + if (!this.config.portal) { + throw new Error('Missing portal config'); + } + window.Featurebase('initialize_portal_widget', this.config.portal); + } + } +} diff --git a/libs/integrations/featurebase/client/tsconfig.json b/libs/integrations/featurebase/client/tsconfig.json new file mode 100644 index 0000000..26b7b4a --- /dev/null +++ b/libs/integrations/featurebase/client/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../../../../tsconfig.base.json", + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ] +} diff --git a/libs/integrations/featurebase/client/tsconfig.lib.json b/libs/integrations/featurebase/client/tsconfig.lib.json new file mode 100644 index 0000000..33eca2c --- /dev/null +++ b/libs/integrations/featurebase/client/tsconfig.lib.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "declaration": true, + "types": ["node"] + }, + "include": ["src/**/*.ts"], + "exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"] +} diff --git a/libs/integrations/featurebase/client/tsconfig.spec.json b/libs/integrations/featurebase/client/tsconfig.spec.json new file mode 100644 index 0000000..69a251f --- /dev/null +++ b/libs/integrations/featurebase/client/tsconfig.spec.json @@ -0,0 +1,14 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "module": "commonjs", + "types": ["jest", "node"] + }, + "include": [ + "jest.config.ts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.d.ts" + ] +} diff --git a/libs/integrations/featurebase/server/.eslintrc.json b/libs/integrations/featurebase/server/.eslintrc.json new file mode 100644 index 0000000..2949776 --- /dev/null +++ b/libs/integrations/featurebase/server/.eslintrc.json @@ -0,0 +1,33 @@ +{ + "extends": ["../../../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "parserOptions": { + "project": ["libs/integrations/featurebase/server/tsconfig.*?.json"] + }, + "rules": {} + }, + { + "files": ["*.ts", "*.tsx"], + "rules": {} + }, + { + "files": ["*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["*.json"], + "parser": "jsonc-eslint-parser", + "rules": { + "@nx/dependency-checks": [ + "error", + { + "ignoredFiles": ["{projectRoot}/rollup.config.{js,ts,mjs,mts}"] + } + ] + } + } + ] +} diff --git a/libs/integrations/featurebase/server/jest.config.ts b/libs/integrations/featurebase/server/jest.config.ts new file mode 100644 index 0000000..1fe064a --- /dev/null +++ b/libs/integrations/featurebase/server/jest.config.ts @@ -0,0 +1,11 @@ +/* eslint-disable */ +export default { + displayName: 'integrations-featurebase-server', + preset: '../../../../jest.preset.js', + transform: { + '^.+\\.[tj]s$': ['ts-jest', { tsconfig: '/tsconfig.spec.json' }], + }, + moduleFileExtensions: ['ts', 'js', 'html'], + testEnvironment: 'node', + coverageDirectory: '../../../../coverage/libs/integrations/featurebase/server', +}; diff --git a/libs/integrations/featurebase/server/package.json b/libs/integrations/featurebase/server/package.json new file mode 100644 index 0000000..ed22503 --- /dev/null +++ b/libs/integrations/featurebase/server/package.json @@ -0,0 +1,10 @@ +{ + "name": "@apex/integrations/featurebase/server", + "version": "0.0.1", + "type": "module", + "module": "./index.js", + "dependencies": { + "jose": "5.2.0" + }, + "reflection": true +} diff --git a/libs/integrations/featurebase/server/project.json b/libs/integrations/featurebase/server/project.json new file mode 100644 index 0000000..dd56293 --- /dev/null +++ b/libs/integrations/featurebase/server/project.json @@ -0,0 +1,34 @@ +{ + "name": "integrations-featurebase-server", + "$schema": "../../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/integrations/featurebase/server/src", + "projectType": "library", + "targets": { + "build": { + "executor": "@nx/rollup:rollup", + "outputs": ["{options.outputPath}"], + "options": { + "outputPath": "dist/{projectRoot}", + "main": "{projectRoot}/src/index.ts", + "tsConfig": "{projectRoot}/tsconfig.lib.json", + "assets": [], + "project": "{projectRoot}/package.json", + "compiler": "tsc", + "external": "all", + "format": ["esm"] + } + }, + "lint": { + "executor": "@nx/eslint:lint", + "outputs": ["{options.outputFile}"] + }, + "test": { + "executor": "@nx/jest:jest", + "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], + "options": { + "jestConfig": "{projectRoot}/jest.config.ts" + } + } + }, + "tags": [] +} diff --git a/libs/integrations/featurebase/server/src/index.ts b/libs/integrations/featurebase/server/src/index.ts new file mode 100644 index 0000000..93353f2 --- /dev/null +++ b/libs/integrations/featurebase/server/src/index.ts @@ -0,0 +1,3 @@ +export * from './lib/config'; +export * from './lib/featurebase.module'; +export * as type from './lib/featurebase.controller'; diff --git a/libs/integrations/featurebase/server/src/lib/config.ts b/libs/integrations/featurebase/server/src/lib/config.ts new file mode 100644 index 0000000..8507448 --- /dev/null +++ b/libs/integrations/featurebase/server/src/lib/config.ts @@ -0,0 +1,3 @@ +export class FeaturebaseConfig { + readonly ssoKey: string; +} diff --git a/libs/integrations/featurebase/server/src/lib/featurebase.controller.ts b/libs/integrations/featurebase/server/src/lib/featurebase.controller.ts new file mode 100644 index 0000000..d36fcc2 --- /dev/null +++ b/libs/integrations/featurebase/server/src/lib/featurebase.controller.ts @@ -0,0 +1,28 @@ +import { rpc } from '@deepkit/rpc'; +import { uuid } from '@deepkit/type'; +import { SignJWT } from 'jose'; + +import { User } from '@apex/api/shared'; + +import { FeaturebaseConfig } from './config'; + +@rpc.controller() +export class FeaturebaseController { + constructor( + private readonly config: FeaturebaseConfig, + private readonly user: User, + ) {} + + @rpc.action() + async generateJwtToken(): Promise { + const key = new TextEncoder().encode(this.config.ssoKey); + + return await new SignJWT({ + email: this.user.email, + name: this.user.username, + jti: uuid(), + }) + .setProtectedHeader({ alg: 'HS256' }) + .sign(key); + } +} diff --git a/libs/integrations/featurebase/server/src/lib/featurebase.module.ts b/libs/integrations/featurebase/server/src/lib/featurebase.module.ts new file mode 100644 index 0000000..346b66e --- /dev/null +++ b/libs/integrations/featurebase/server/src/lib/featurebase.module.ts @@ -0,0 +1,10 @@ +import { createModule } from '@deepkit/app'; + +import { FeaturebaseConfig } from './config'; +import { FeaturebaseController } from './featurebase.controller'; + +export class FeaturebaseModule extends createModule({ + config: FeaturebaseConfig, + controllers: [FeaturebaseController], + forRoot: true, +}) {} diff --git a/libs/integrations/featurebase/server/tsconfig.json b/libs/integrations/featurebase/server/tsconfig.json new file mode 100644 index 0000000..26b7b4a --- /dev/null +++ b/libs/integrations/featurebase/server/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../../../../tsconfig.base.json", + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ] +} diff --git a/libs/integrations/featurebase/server/tsconfig.lib.json b/libs/integrations/featurebase/server/tsconfig.lib.json new file mode 100644 index 0000000..33eca2c --- /dev/null +++ b/libs/integrations/featurebase/server/tsconfig.lib.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "declaration": true, + "types": ["node"] + }, + "include": ["src/**/*.ts"], + "exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"] +} diff --git a/libs/integrations/featurebase/server/tsconfig.spec.json b/libs/integrations/featurebase/server/tsconfig.spec.json new file mode 100644 index 0000000..69a251f --- /dev/null +++ b/libs/integrations/featurebase/server/tsconfig.spec.json @@ -0,0 +1,14 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "module": "commonjs", + "types": ["jest", "node"] + }, + "include": [ + "jest.config.ts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.d.ts" + ] +} diff --git a/libs/integrations/supabase/README.md b/libs/integrations/supabase/README.md index d45b4b1..dff996a 100644 --- a/libs/integrations/supabase/README.md +++ b/libs/integrations/supabase/README.md @@ -1 +1 @@ -# Supabase Auth Integration +# [Supabase Auth](https://supabase.com/auth) Integration diff --git a/package.json b/package.json index b7adf13..3c71aac 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "class-variance-authority": "^0.7.0", "glob": "10.3.10", "gsap": "^3.12.4", + "jose": "^5.2.0", "pixi-stats": "^1.2.2", "pixi.js": "^7.2.4", "rxjs": "7.8.1", @@ -93,6 +94,7 @@ "@swc/core": "1.3.85", "@swc/jest": "0.2.20", "@testing-library/angular": "^15.1.0", + "@testing-library/jest-dom": "^6.2.0", "@types/jest": "^29.4.0", "@types/node": "18.16.9", "@types/seedrandom": "3.0.8", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fbe1043..c910220 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -133,6 +133,9 @@ dependencies: gsap: specifier: ^3.12.4 version: 3.12.4 + jose: + specifier: ^5.2.0 + version: 5.2.0 pixi-stats: specifier: ^1.2.2 version: 1.2.2(pixi.js@7.3.2) @@ -264,6 +267,9 @@ devDependencies: '@testing-library/angular': specifier: ^15.1.0 version: 15.1.0(@angular/common@17.0.3)(@angular/core@17.0.3)(@angular/platform-browser@17.0.3)(@angular/router@17.0.3) + '@testing-library/jest-dom': + specifier: ^6.2.0 + version: 6.2.0(@types/jest@29.5.11)(jest@29.7.0)(vitest@1.0.4) '@types/jest': specifier: ^29.4.0 version: 29.5.11 @@ -7114,6 +7120,37 @@ packages: pretty-format: 27.5.1 dev: true + /@testing-library/jest-dom@6.2.0(@types/jest@29.5.11)(jest@29.7.0)(vitest@1.0.4): + resolution: {integrity: sha512-+BVQlJ9cmEn5RDMUS8c2+TU6giLvzaHZ8sU/x0Jj7fk+6/46wPdwlgOPcpxS17CjcanBi/3VmGMqVr2rmbUmNw==} + engines: {node: '>=14', npm: '>=6', yarn: '>=1'} + peerDependencies: + '@jest/globals': '>= 28' + '@types/jest': '>= 28' + jest: '>= 28' + vitest: '>= 0.32' + peerDependenciesMeta: + '@jest/globals': + optional: true + '@types/jest': + optional: true + jest: + optional: true + vitest: + optional: true + dependencies: + '@adobe/css-tools': 4.3.2 + '@babel/runtime': 7.23.6 + '@types/jest': 29.5.11 + aria-query: 5.3.0 + chalk: 3.0.0 + css.escape: 1.5.1 + dom-accessibility-api: 0.6.3 + jest: 29.7.0(@types/node@18.16.9)(ts-node@10.9.1) + lodash: 4.17.21 + redent: 3.0.0 + vitest: 1.0.4(@types/node@18.16.9)(@vitest/ui@0.34.7)(jsdom@22.1.0)(less@4.1.3)(stylus@0.59.0) + dev: true + /@tokenizer/token@0.3.0: resolution: {integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==} @@ -8965,6 +9002,14 @@ packages: escape-string-regexp: 1.0.5 supports-color: 5.5.0 + /chalk@3.0.0: + resolution: {integrity: sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==} + engines: {node: '>=8'} + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + dev: true + /chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} @@ -9575,6 +9620,10 @@ packages: resolution: {integrity: sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==} engines: {node: '>= 6'} + /css.escape@1.5.1: + resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==} + dev: true + /cssesc@3.0.0: resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} engines: {node: '>=4'} @@ -10034,6 +10083,10 @@ packages: resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} dev: true + /dom-accessibility-api@0.6.3: + resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==} + dev: true + /dom-serializer@1.4.1: resolution: {integrity: sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==} dependencies: @@ -12840,6 +12893,10 @@ packages: resolution: {integrity: sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==} dev: true + /jose@5.2.0: + resolution: {integrity: sha512-oW3PCnvyrcm1HMvGTzqjxxfnEs9EoFOFWi2HsEGhlFVOXxTE3K9GKWVMFoFw06yPUqwpvEWic1BmtUZBI/tIjw==} + dev: false + /jpeg-js@0.4.4: resolution: {integrity: sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==} dev: false diff --git a/tsconfig.base.json b/tsconfig.base.json index 6271cb1..a9ab0df 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -30,6 +30,8 @@ "@apex/client": ["libs/client/src/index.ts"], "@apex/integrations/supabase/server": ["libs/integrations/supabase/server/src/index.ts"], "@apex/integrations/supabase/client": ["libs/integrations/supabase/client/src/index.ts"], + "@apex/integrations/featurebase/server": ["libs/integrations/featurebase/server/src/index.ts"], + "@apex/integrations/featurebase/client": ["libs/integrations/featurebase/client/src/index.ts"], "@apex/scuti-renderer": ["libs/scuti-renderer/src/src/index.ts"], "@apex/server": ["libs/server/src/index.ts"], "@apex/ui": ["libs/ui/src/index.ts"]