From d07e7b834831230b53860d0919a68edc2d36193d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krist=C3=B3f=20Marussy?= Date: Sat, 8 Jan 2022 21:36:43 +0100 Subject: build: Eslint fixes for multi-module project MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Kristóf Marussy --- .eslintignore | 1 + .eslintrc.cjs | 126 ++++++++++ .eslintrc.json | 191 --------------- .gitlab-ci.yml | 35 +-- .yarnrc.yml | 23 +- README.md | 8 +- config/buildConstants.js | 4 +- config/esbuildConfig.js | 40 ---- config/fileURLToDirname.js | 10 + config/getEsbuildConfig.js | 40 ++++ config/jest.config.base.js | 4 +- config/jestEsbuildTransform.js | 23 -- config/jestEsbuildTransformer.js | 28 +++ config/tsconfig.base.json | 19 ++ config/utils.js | 10 - jest.config.js | 3 +- package.json | 16 +- packages/main/.eslintrc.cjs | 6 + packages/main/.eslintrc.json | 6 - packages/main/esbuild.config.js | 8 +- packages/main/package.json | 2 +- packages/main/src/compositionRoot.ts | 38 --- .../main/src/controllers/__tests__/config.spec.ts | 185 --------------- .../src/controllers/__tests__/initConfig.spec.ts | 185 +++++++++++++++ .../controllers/__tests__/initNativeTheme.spec.ts | 71 ++++++ .../src/controllers/__tests__/nativeTheme.spec.ts | 71 ------ packages/main/src/controllers/config.ts | 90 ------- packages/main/src/controllers/initConfig.ts | 90 +++++++ packages/main/src/controllers/initNativeTheme.ts | 50 ++++ packages/main/src/controllers/nativeTheme.ts | 50 ---- packages/main/src/devTools.ts | 7 +- packages/main/src/index.ts | 109 +++++---- packages/main/src/init.ts | 38 +++ .../main/src/services/ConfigPersistenceService.ts | 6 +- .../services/impl/ConfigPersistenceServiceImpl.ts | 20 +- packages/main/src/stores/Config.ts | 2 +- packages/main/src/stores/MainStore.ts | 2 +- packages/main/src/stores/SharedStore.ts | 2 +- packages/main/src/utils/Disposer.ts | 25 ++ packages/main/src/utils/disposer.ts | 23 -- packages/main/src/utils/log.ts | 66 ++++++ packages/main/src/utils/logging.ts | 62 ----- packages/main/tsconfig.json | 10 +- packages/preload/.eslintrc.cjs | 6 + packages/preload/esbuild.config.js | 6 +- packages/preload/jest.config.js | 2 +- packages/preload/package.json | 6 +- .../src/contextBridge/SophieRendererImpl.ts | 96 -------- .../__tests__/SophieRendererImpl.spec.ts | 258 --------------------- .../__tests__/createSophieRenderer.spec.ts | 258 +++++++++++++++++++++ .../src/contextBridge/createSophieRenderer.ts | 96 ++++++++ packages/preload/src/index.ts | 2 +- packages/preload/tsconfig.json | 8 +- packages/renderer/.eslinrc.cjs | 11 + packages/renderer/.eslintrc.json | 5 - packages/renderer/package.json | 10 +- packages/renderer/src/components/App.tsx | 6 +- .../src/components/BrowserViewPlaceholder.tsx | 14 +- packages/renderer/src/components/Sidebar.tsx | 4 +- packages/renderer/src/components/StoreProvider.tsx | 2 +- packages/renderer/src/components/ThemeProvider.tsx | 8 +- .../src/components/ToggleDarkModeButton.tsx | 9 +- packages/renderer/src/devTools.ts | 21 +- packages/renderer/src/index.tsx | 13 +- packages/renderer/src/stores/RendererEnv.ts | 4 +- packages/renderer/src/stores/RendererStore.ts | 21 +- packages/renderer/src/utils/log.ts | 50 ++++ packages/renderer/tsconfig.json | 8 +- packages/renderer/vite.config.js | 5 +- packages/service-inject/.eslintrc.cjs | 6 + packages/service-inject/esbuild.config.js | 6 +- packages/service-inject/package.json | 6 +- packages/service-inject/tsconfig.json | 7 +- packages/service-preload/.eslintrc.cjs | 6 + packages/service-preload/esbuild.config.js | 6 +- packages/service-preload/package.json | 10 +- packages/service-preload/src/index.ts | 12 +- packages/service-preload/src/utils/log.ts | 49 ++++ packages/service-preload/tsconfig.json | 8 +- packages/service-preload/types/importMeta.d.ts | 7 + packages/service-shared/.eslintrc.cjs | 7 + packages/service-shared/esbuild.config.js | 6 +- packages/service-shared/package.json | 7 +- packages/service-shared/src/index.ts | 2 +- packages/service-shared/src/ipc.ts | 3 + packages/service-shared/tsconfig.build.json | 12 + packages/service-shared/tsconfig.json | 14 +- packages/shared/.eslintrc.cjs | 7 + packages/shared/esbuild.config.js | 6 +- packages/shared/package.json | 7 +- .../shared/src/contextBridge/SophieRenderer.ts | 7 +- packages/shared/src/index.ts | 5 +- packages/shared/tsconfig.build.json | 12 + packages/shared/tsconfig.json | 14 +- scripts/build.js | 6 +- scripts/update-electron-vendors.js | 14 +- scripts/watch.js | 52 +++-- tsconfig.json | 27 +-- yarn.lock | 202 ++++++++++++++-- 99 files changed, 1813 insertions(+), 1454 deletions(-) create mode 100644 .eslintrc.cjs delete mode 100644 .eslintrc.json delete mode 100644 config/esbuildConfig.js create mode 100644 config/fileURLToDirname.js create mode 100644 config/getEsbuildConfig.js delete mode 100644 config/jestEsbuildTransform.js create mode 100644 config/jestEsbuildTransformer.js create mode 100644 config/tsconfig.base.json delete mode 100644 config/utils.js create mode 100644 packages/main/.eslintrc.cjs delete mode 100644 packages/main/.eslintrc.json delete mode 100644 packages/main/src/compositionRoot.ts delete mode 100644 packages/main/src/controllers/__tests__/config.spec.ts create mode 100644 packages/main/src/controllers/__tests__/initConfig.spec.ts create mode 100644 packages/main/src/controllers/__tests__/initNativeTheme.spec.ts delete mode 100644 packages/main/src/controllers/__tests__/nativeTheme.spec.ts delete mode 100644 packages/main/src/controllers/config.ts create mode 100644 packages/main/src/controllers/initConfig.ts create mode 100644 packages/main/src/controllers/initNativeTheme.ts delete mode 100644 packages/main/src/controllers/nativeTheme.ts create mode 100644 packages/main/src/init.ts create mode 100644 packages/main/src/utils/Disposer.ts delete mode 100644 packages/main/src/utils/disposer.ts create mode 100644 packages/main/src/utils/log.ts delete mode 100644 packages/main/src/utils/logging.ts create mode 100644 packages/preload/.eslintrc.cjs delete mode 100644 packages/preload/src/contextBridge/SophieRendererImpl.ts delete mode 100644 packages/preload/src/contextBridge/__tests__/SophieRendererImpl.spec.ts create mode 100644 packages/preload/src/contextBridge/__tests__/createSophieRenderer.spec.ts create mode 100644 packages/preload/src/contextBridge/createSophieRenderer.ts create mode 100644 packages/renderer/.eslinrc.cjs delete mode 100644 packages/renderer/.eslintrc.json create mode 100644 packages/renderer/src/utils/log.ts create mode 100644 packages/service-inject/.eslintrc.cjs create mode 100644 packages/service-preload/.eslintrc.cjs create mode 100644 packages/service-preload/src/utils/log.ts create mode 100644 packages/service-preload/types/importMeta.d.ts create mode 100644 packages/service-shared/.eslintrc.cjs create mode 100644 packages/service-shared/tsconfig.build.json create mode 100644 packages/shared/.eslintrc.cjs create mode 100644 packages/shared/tsconfig.build.json diff --git a/.eslintignore b/.eslintignore index cf31955..01b8bcb 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,4 +1,5 @@ # Logs +.log/ logs *.log diff --git a/.eslintrc.cjs b/.eslintrc.cjs new file mode 100644 index 0000000..92af738 --- /dev/null +++ b/.eslintrc.cjs @@ -0,0 +1,126 @@ +const project = [ + './tsconfig.json', + './packages/*/tsconfig.json', +]; + +module.exports = { + root: true, + plugins: [ + '@typescript-eslint', + ], + extends: [ + 'airbnb-base', + 'airbnb-typescript/base', + 'plugin:@typescript-eslint/recommended', + 'plugin:@typescript-eslint/recommended-requiring-type-checking', + ], + env: { + es6: true, + node: true, + browser: false, + jest: true, + }, + parser: '@typescript-eslint/parser', + parserOptions: { + sourceType: 'module', + project, + allowAutomaticSingleRunInference: true, + tsconfigRootDir: __dirname, + warnOnUnsupportedTypeScriptVersion: false, + }, + settings: { + 'import/parsers': { + '@typescript-eslint/parser': ['.ts', '.tsx'], + }, + 'import/resolver': { + typescript: { + alwaysTryTypes: true, + project, + }, + }, + }, + rules: { + 'import/no-unresolved': 'error', + 'import/order': [ + 'error', + { + alphabetize: { + order: 'asc', + }, + 'newlines-between': 'always', + }, + ], + }, + overrides: [ + { + files: [ + '**/stores/**/*.ts', + ], + rules: { + // In a mobx-state-tree action, we assign to the properties of `self` to update the store. + 'no-param-reassign': 'off', + // mobx-state-tree uses empty interfaces to speed up typechecking. + '@typescript-eslint/no-empty-interface': 'off', + }, + }, + { + files: [ + '**/__tests__/*.{ts,tsx}', + '**/*.{spec,test}.{ts,tsx}', + ], + rules: { + // If a non-null assertion fails in a test, the test will also fail anyways. + '@typescript-eslint/no-non-null-assertion': 'off', + // Jest mocks use unbound method references. + '@typescript-eslint/unbound-method': 'off', + }, + }, + { + files: [ + '**/*.js', + ], + rules: { + // ESM requires extensions for imports. + 'import/extensions': [ + 'error', + 'ignorePackages', + ], + }, + }, + { + files: [ + '.electron-builder.config.cjs', + 'config/**/*.{cjs,js}', + 'jest.config.js', + 'scripts/**/*.js', + 'packages/*/*.config.js', + ], + env: { + // Config files are never run in a browser (even in frontend projects). + browser: false, + node: true, + jest: false, + }, + rules: { + // Config files and build scripts are allowed to use devDependencies. + 'import/no-extraneous-dependencies': [ + 'error', + { + devDependencies: true, + }, + ], + // Allow reporting progress to the console from scripts. + 'no-console': 'off', + }, + }, + { + files: [ + 'packages/*/*.config.js', + ], + rules: { + // Allow relative imports of config files from the root package. + 'import/no-relative-packages': 'off', + }, + }, + ], +}; diff --git a/.eslintrc.json b/.eslintrc.json deleted file mode 100644 index 82f5d58..0000000 --- a/.eslintrc.json +++ /dev/null @@ -1,191 +0,0 @@ -{ - "root": true, - "env": { - "node": true, - "browser": true, - "es2021": true - }, - "extends": [ - "eslint-config-airbnb-typescript", - "plugin:import/recommended", - "plugin:import/typescript" - ], - "parser": "@typescript-eslint/parser", - "parserOptions": { - "ecmaFeatures": { - "jsx": true - }, - "ecmaVersion": 2021, - "project": "./tsconfig.json" - }, - "plugins": [ - "react", - "@typescript-eslint" - ], - "rules": { - "indent": [ - 2, - 2, - { - "SwitchCase": 1 - } - ], - "quotes": [ - "error", - "single" - ], - "linebreak-style": [ - "error", - "unix" - ], - "semi": [ - "error", - "always" - ], - "import/extensions": [ - "error", - "ignorePackages", - { - "js": "always", - "jsx": "always", - "json": "never", - "ts": "never", - "tsx": "never" - } - ], - "import/no-unresolved": [ - "error", - { - "caseSensitive": false - } - ], - "import/no-extraneous-dependencies": [ - "error", - { - // "devDependencies": true, - // "optionalDependencies": true, - // "peerDependencies": true, - "bundledDependencies": true - } - ], - // Best practices - "block-scoped-var": 1, - "complexity": [ - 1, - 4 - ], - "consistent-return": 1, - "curly": 1, - "default-case": 1, - "dot-location": [ - 1, - "property" - ], - "dot-notation": 1, - "eqeqeq": 2, - "guard-for-in": 1, - "no-alert": 2, - "no-caller": 2, - "no-case-declarations": 2, - "no-console": 0, - "no-div-regex": 1, - "no-else-return": 0, - "no-empty": 0, - "no-empty-pattern": 2, - "no-eq-null": 2, - "no-eval": 2, - "no-extend-native": 1, - "no-extra-bind": 1, - "no-fallthrough": 1, - "no-floating-decimal": 1, - "no-implicit-coercion": 1, - "no-implied-eval": 1, - "no-invalid-this": 2, - "no-iterator": 2, - "no-labels": 1, - "no-lone-blocks": 1, - "no-loop-func": 2, - "no-magic-numbers": [ - "error", - { - "ignore": [ - -1, - 0, - 1, - 2, - 100, - 200, - 422, - 3600000, - 1453449120000, - 1453445460000 - ] - } - ], - "no-multi-spaces": 1, - "no-multi-str": 1, - "no-native-reassign": 1, - "no-new-func": 2, - "no-new-wrappers": 2, - "no-new": 1, - "no-octal-escape": 1, - "no-octal": 1, - "no-param-reassign": 1, - "no-process-env": 2, - "no-proto": 2, - "no-redeclare": 1, - "no-return-assign": 2, - "no-script-url": 2, - "no-self-compare": 1, - "no-sequences": 1, - "no-throw-literal": 2, - "no-unused-expressions": [ - 1, - { - "allowTernary": true - } - ], - "no-useless-call": 2, - "no-useless-concat": 1, - "no-void": 2, - "no-warning-comments": 0, - "no-with": 2, - "radix": 1, - "vars-on-top": 0, - "wrap-iife": 2, - "yoda": 0, - "strict": 1, - // Variables - "init-declarations": 0, - "no-catch-shadow": 2, - "no-delete-var": 2, - "no-label-var": 2, - "no-shadow-restricted-names": 2, - "no-shadow": 2, - "no-undef-init": 1, - "no-undef": 2, - "no-undefined": 0, - "no-unused-vars": 2, - "no-use-before-define": 2 - }, - "globals": { - "__dirname": false - }, - "overrides": [ - { - "files": [ - "**/__tests__/*" - ], - "globals": { - "after": false, - "afterEach": false, - "beforeAll": false, - "beforeEach": false, - "describe": false, - "Electron": false, - "expect": false, - "it": false - } - } - ] -} diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index bf9461e..c522036 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -2,35 +2,38 @@ image: node:16.13.1 cache: paths: - - node_modules/ - - .yarn/cache + - .yarn/cache/ stages: + - code-quality - test - - lint - build -Run tests: - stage: test +default: before_script: - - yarn install + - yarn install --immutable + +lint: + stage: code-quality script: - - yarn test + - yarn lint --format gitlab + artifacts: + reports: + codequality: gl-codequality.json -Run linter and static analyzer: - stage: lint - before_script: - - yarn install +typecheck: + stage: code-quality script: - - yarn dlx @yarnpkg/doctor - yarn typecheck - - yarn run lint + +test: + stage: test + script: + - yarn test # TODO: GitlabCI free runners are only for linux - need to investigate for macos and windows artifacts -Build the app: +build: stage: build - before_script: - - yarn install script: - yarn compile # TODO: Need to publish the built distributable file diff --git a/.yarnrc.yml b/.yarnrc.yml index f83ee48..e8caff1 100644 --- a/.yarnrc.yml +++ b/.yarnrc.yml @@ -1,13 +1,6 @@ -plugins: - - path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs - spec: "@yarnpkg/plugin-interactive-tools" - - path: .yarn/plugins/@yarnpkg/plugin-workspace-tools.cjs - spec: "@yarnpkg/plugin-workspace-tools" - - path: .yarn/plugins/@yarnpkg/plugin-typescript.cjs - spec: "@yarnpkg/plugin-typescript" +enableTelemetry: 0 logFilters: - # Discard incompatible package architecture warnings. - code: YN0076 level: discard @@ -15,4 +8,18 @@ nmMode: hardlinks-local nodeLinker: node-modules +packageExtensions: + eslint-config-airbnb-typescript@*: + peerDependencies: + eslint: "*" + eslint-plugin-import: "*" + +plugins: + - path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs + spec: "@yarnpkg/plugin-interactive-tools" + - path: .yarn/plugins/@yarnpkg/plugin-workspace-tools.cjs + spec: "@yarnpkg/plugin-workspace-tools" + - path: .yarn/plugins/@yarnpkg/plugin-typescript.cjs + spec: "@yarnpkg/plugin-typescript" + yarnPath: .yarn/releases/yarn-3.1.1.cjs diff --git a/README.md b/README.md index 6a2eba0..28f9ec5 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,12 @@ To start working, install all dependencies with yarn install ``` +If TypeScript complains about missing type definitions, run + +```sh +yarn types +``` + To start a development instance of Sophie, which will reload on source changes, run ``` sh @@ -49,7 +55,7 @@ To build the application in release mode, run yarn compile ``` -If TypeScript complains about missing type definitions, or you want to typecheck the project, run +To typecheck the project, run ```sh yarn typecheck diff --git a/config/buildConstants.js b/config/buildConstants.js index 627c895..9083d78 100644 --- a/config/buildConstants.js +++ b/config/buildConstants.js @@ -1,7 +1,7 @@ import { readFileSync } from 'fs'; import { join } from 'path'; -import { fileURLToDirname } from './utils.js'; +import fileURLToDirname from './fileURLToDirname.js'; const thisDir = fileURLToDirname(import.meta.url); @@ -9,6 +9,8 @@ const thisDir = fileURLToDirname(import.meta.url); // so we have to use the synchronous filesystem API. const electronVendorsJson = readFileSync(join(thisDir, '../.electron-vendors.cache.json'), 'utf8'); +/** @type {{ chrome: number; node: number; }} */ +// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const { chrome: chromeVersion, node: nodeVersion } = JSON.parse(electronVendorsJson); /** @type {string} */ diff --git a/config/esbuildConfig.js b/config/esbuildConfig.js deleted file mode 100644 index 93419fb..0000000 --- a/config/esbuildConfig.js +++ /dev/null @@ -1,40 +0,0 @@ -/* eslint-disable no-process-env */ -import { banner } from './buildConstants.js'; - -/** @type {string} */ -const mode = process.env.MODE || 'development'; - -/** @type {boolean} */ -const isDevelopment = mode === 'development'; - -/** @type {string} */ -const modeString = JSON.stringify(mode); - -/** - * @param {import('esbuild').BuildOptions} config - * @param {Record} [extraMetaEnvVars] - * @returns {import('esbuild').BuildOptions} - */ -export function getConfig(config, extraMetaEnvVars) { - return { - logLevel: 'info', - bundle: true, - treeShaking: !isDevelopment, - minify: !isDevelopment, - banner: { - js: banner, - }, - ...config, - sourcemap: isDevelopment ? (config.sourcemap || true) : false, - define: { - 'process.env.NODE_ENV': modeString, - 'process.env.MODE': modeString, - 'import.meta.env': JSON.stringify({ - DEV: isDevelopment, - MODE: mode, - PROD: !isDevelopment, - ...extraMetaEnvVars, - }), - }, - }; -} diff --git a/config/fileURLToDirname.js b/config/fileURLToDirname.js new file mode 100644 index 0000000..70654cb --- /dev/null +++ b/config/fileURLToDirname.js @@ -0,0 +1,10 @@ +import { dirname } from 'path'; +import { fileURLToPath } from 'url'; + +/** + * @param {string} url + * @returns {string} + */ +export default function fileURLToDirname(url) { + return dirname(fileURLToPath(url)); +} diff --git a/config/getEsbuildConfig.js b/config/getEsbuildConfig.js new file mode 100644 index 0000000..b338d68 --- /dev/null +++ b/config/getEsbuildConfig.js @@ -0,0 +1,40 @@ +/* eslint-disable no-process-env */ +import { banner } from './buildConstants.js'; + +/** @type {string} */ +const mode = process.env.MODE || 'development'; + +/** @type {boolean} */ +const isDevelopment = mode === 'development'; + +/** @type {string} */ +const modeString = JSON.stringify(mode); + +/** + * @param {import('esbuild').BuildOptions} config + * @param {Record} [extraMetaEnvVars] + * @returns {import('esbuild').BuildOptions} + */ +export default function getEsbuildConfig(config, extraMetaEnvVars) { + return { + logLevel: 'info', + bundle: true, + treeShaking: !isDevelopment, + minify: !isDevelopment, + banner: { + js: banner, + }, + ...config, + sourcemap: isDevelopment ? (config.sourcemap || true) : false, + define: { + 'process.env.NODE_ENV': modeString, + 'process.env.MODE': modeString, + 'import.meta.env': JSON.stringify({ + DEV: isDevelopment, + MODE: mode, + PROD: !isDevelopment, + ...extraMetaEnvVars, + }), + }, + }; +} diff --git a/config/jest.config.base.js b/config/jest.config.base.js index 2ca021b..463e498 100644 --- a/config/jest.config.base.js +++ b/config/jest.config.base.js @@ -1,13 +1,13 @@ import { join } from 'path'; -import { fileURLToDirname } from './utils.js'; +import fileURLToDirname from './fileURLToDirname.js'; const thisDir = fileURLToDirname(import.meta.url); /** @type {import('@jest/types').Config.InitialOptions} */ export default { transform: { - '\\.tsx?$': join(thisDir, 'jestEsbuildTransform.js'), + '\\.tsx?$': join(thisDir, 'jestEsbuildTransformer.js'), }, extensionsToTreatAsEsm: [ '.ts', diff --git a/config/jestEsbuildTransform.js b/config/jestEsbuildTransform.js deleted file mode 100644 index 4bf49e6..0000000 --- a/config/jestEsbuildTransform.js +++ /dev/null @@ -1,23 +0,0 @@ -import { transform } from 'esbuild'; - -import { node } from './buildConstants.js'; - -/** @type {import('@jest/transform').AsyncTransformer} */ -const transformer = { - canInstrument: false, - async processAsync(source, filePath) { - const { code, map } = await transform(source, { - loader: filePath.endsWith('tsx') ? 'tsx' : 'ts', - sourcefile: filePath, - format: 'esm', - target: node, - sourcemap: true, - }); - return { - code, - map, - }; - }, -}; - -export default transformer; diff --git a/config/jestEsbuildTransformer.js b/config/jestEsbuildTransformer.js new file mode 100644 index 0000000..b6f2fc3 --- /dev/null +++ b/config/jestEsbuildTransformer.js @@ -0,0 +1,28 @@ +import { transform } from 'esbuild'; + +import { node } from './buildConstants.js'; + +/** + * @param {string} source + * @param {import('@jest/types').Config.Path} filePath + * @return {Promise} + */ +async function processAsync(source, filePath) { + const { code, map } = await transform(source, { + loader: filePath.endsWith('tsx') ? 'tsx' : 'ts', + sourcefile: filePath, + format: 'esm', + target: node, + sourcemap: true, + }); + return { + code, + map, + }; +} + +/** @type {import('@jest/transform').AsyncTransformer} */ +export default { + canInstrument: false, + processAsync, +}; diff --git a/config/tsconfig.base.json b/config/tsconfig.base.json new file mode 100644 index 0000000..255f334 --- /dev/null +++ b/config/tsconfig.base.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "module": "esnext", + "target": "esnext", + "moduleResolution": "node", + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "noImplicitOverride": true, + "noImplicitReturns": true, + "exactOptionalPropertyTypes": true, + "isolatedModules": true, + "skipLibCheck": true, + "checkJs": true, + "lib": [ + "esnext" + ] + } +} diff --git a/config/utils.js b/config/utils.js deleted file mode 100644 index d3e13d9..0000000 --- a/config/utils.js +++ /dev/null @@ -1,10 +0,0 @@ -import { dirname } from 'path'; -import { fileURLToPath } from 'url'; - -/** - * @param {string} url - * @returns {string} - */ -export function fileURLToDirname(url) { - return dirname(fileURLToPath(url)); -} diff --git a/jest.config.js b/jest.config.js index 74e18b7..c631fdd 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,8 +1,9 @@ -/** @type {import('ts-jest').InitialOptionsTsJest} */ +/** @type {import('@jest/types').Config.InitialOptions} */ export default { projects: [ '/packages/*', ], + /** @type {'babel' | 'v8'} */ coverageProvider: 'v8', collectCoverageFrom: [ 'src/**/*.{ts,tsx}', diff --git a/package.json b/package.json index 99cc7a2..f632a8c 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "type": "module", "main": "packages/main/dist/index.cjs", "scripts": { - "clean": "rimraf dist packages/*/dist packages/*/tsconfig.tsbuildinfo .vite", + "clean": "rimraf coverage dist packages/*/dist packages/*/*.tsbuildinfo .vite", "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js", "build": "node scripts/build.js", "precompile": "cross-env MODE=production yarn run build", @@ -27,8 +27,12 @@ "compile:electron-builder": "electron-builder build --config .electron-builder.config.cjs --dir", "watch": "node scripts/watch.js", "watch:test": "yarn test --watch", - "typecheck": "yarn workspaces foreach -vpt run typecheck", - "lint": "eslint \"{config,packages,scripts}/**/*.{js,jsx,ts,tsx}\" --quiet", + "lint": "yarn types && eslint .", + "typecheck": "yarn types && yarn workspaces foreach -vp run typecheck:workspace", + "typecheck:workspace": "yarn g:typecheck", + "g:typecheck": "cd $INIT_CWD && tsc", + "types": "yarn workspaces foreach -vpt run types", + "g:types": "cd $INIT_CWD && tsc -b tsconfig.build.json", "update-electron-vendors": "node scripts/update-electron-vendors.js", "main": "yarn workspace @sophie/main", "preload": "yarn workspace @sophie/preload", @@ -53,9 +57,15 @@ "electron-builder": "^22.14.12", "esbuild": "^0.14.11", "eslint": "^8.6.0", + "eslint-config-airbnb": "^19.0.4", + "eslint-config-airbnb-base": "^15.0.0", "eslint-config-airbnb-typescript": "^16.1.0", + "eslint-formatter-gitlab": "^3.0.0", + "eslint-import-resolver-typescript": "^2.5.0", "eslint-plugin-import": "^2.25.4", + "eslint-plugin-jsx-a11y": "^6.5.1", "eslint-plugin-react": "^7.28.0", + "eslint-plugin-react-hooks": "^4.3.0", "git-repo-info": "^2.1.1", "jest": "^27.4.7", "rimraf": "^3.0.2", diff --git a/packages/main/.eslintrc.cjs b/packages/main/.eslintrc.cjs new file mode 100644 index 0000000..548ea34 --- /dev/null +++ b/packages/main/.eslintrc.cjs @@ -0,0 +1,6 @@ +module.exports = { + env: { + node: true, + browser: false, + }, +}; diff --git a/packages/main/.eslintrc.json b/packages/main/.eslintrc.json deleted file mode 100644 index 6b736e2..0000000 --- a/packages/main/.eslintrc.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "globals": { - "NodeJS": false, - "require": false - } -} diff --git a/packages/main/esbuild.config.js b/packages/main/esbuild.config.js index c24d6e1..49fba6b 100644 --- a/packages/main/esbuild.config.js +++ b/packages/main/esbuild.config.js @@ -1,8 +1,8 @@ -/* eslint-disable no-process-env */ import getRepoInfo from 'git-repo-info'; + import { node } from '../../config/buildConstants.js'; -import { getConfig } from '../../config/esbuildConfig.js'; -import { fileURLToDirname } from '../../config/utils.js'; +import fileURLToDirname from '../../config/fileURLToDirname.js'; +import getEsbuildConfig from '../../config/getEsbuildConfig.js'; const externalPackages = ['electron']; @@ -12,7 +12,7 @@ if (process.env.MODE !== 'development') { const gitInfo = getRepoInfo(); -export default getConfig({ +export default getEsbuildConfig({ absWorkingDir: fileURLToDirname(import.meta.url), entryPoints: [ 'src/index.ts', diff --git a/packages/main/package.json b/packages/main/package.json index e1b3f49..d9abf51 100644 --- a/packages/main/package.json +++ b/packages/main/package.json @@ -5,7 +5,7 @@ "type": "module", "types": "dist-types/index.d.ts", "scripts": { - "typecheck": "tsc" + "typecheck:workspace": "yarn g:typecheck" }, "dependencies": { "@sophie/service-shared": "workspace:*", diff --git a/packages/main/src/compositionRoot.ts b/packages/main/src/compositionRoot.ts deleted file mode 100644 index 76835a1..0000000 --- a/packages/main/src/compositionRoot.ts +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright (C) 2021-2022 Kristóf Marussy - * - * This file is part of Sophie. - * - * Sophie is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, version 3. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { app } from 'electron'; - -import { initConfig } from './controllers/config'; -import { initNativeTheme } from './controllers/nativeTheme'; -import { ConfigPersistenceServiceImpl } from './services/impl/ConfigPersistenceServiceImpl'; -import { MainStore } from './stores/MainStore'; -import { Disposer } from './utils/disposer'; - -export async function init(store: MainStore): Promise { - const configPersistenceService = new ConfigPersistenceServiceImpl(app.getPath('userData')); - const disposeConfigController = await initConfig(store.config, configPersistenceService); - const disposeNativeThemeController = initNativeTheme(store); - - return () => { - disposeNativeThemeController(); - disposeConfigController(); - }; -} diff --git a/packages/main/src/controllers/__tests__/config.spec.ts b/packages/main/src/controllers/__tests__/config.spec.ts deleted file mode 100644 index eb67df0..0000000 --- a/packages/main/src/controllers/__tests__/config.spec.ts +++ /dev/null @@ -1,185 +0,0 @@ -/* - * Copyright (C) 2021-2022 Kristóf Marussy - * - * This file is part of Sophie. - * - * Sophie is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, version 3. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { jest } from '@jest/globals'; -import { mocked } from 'jest-mock'; -import ms from 'ms'; - -import { initConfig } from '../config'; -import type { ConfigPersistenceService } from '../../services/ConfigPersistenceService'; -import { Config, config as configModel } from '../../stores/Config'; -import { Disposer } from '../../utils/disposer'; -import { silenceLogger } from '../../utils/logging'; - -let config: Config; -let persistenceService: ConfigPersistenceService = { - readConfig: jest.fn(), - writeConfig: jest.fn(), - watchConfig: jest.fn(), -}; -let lessThanThrottleMs = ms('0.1s'); -let throttleMs = ms('1s'); - -beforeAll(() => { - jest.useFakeTimers(); - silenceLogger(); -}); - -beforeEach(() => { - config = configModel.create(); -}); - -describe('when initializing', () => { - describe('when there is no config file', () => { - beforeEach(() => { - mocked(persistenceService.readConfig).mockResolvedValueOnce({ - found: false, - }); - }); - - it('should create a new config file', async () => { - await initConfig(config, persistenceService); - expect(persistenceService.writeConfig).toBeCalledTimes(1); - }); - - it('should bail if there is an an error creating the config file', async () => { - mocked(persistenceService.writeConfig).mockRejectedValue(new Error('boo')); - await expect(() => initConfig(config, persistenceService)).rejects.toBeInstanceOf(Error); - }); - }); - - describe('when there is a valid config file', () => { - beforeEach(() => { - mocked(persistenceService.readConfig).mockResolvedValueOnce({ - found: true, - data: { - themeSource: 'dark', - }, - }); - }); - - it('should read the existing config file is there is one', async () => { - await initConfig(config, persistenceService); - expect(persistenceService.writeConfig).not.toBeCalled(); - expect(config.themeSource).toBe('dark'); - }); - - it('should bail if it cannot set up a watcher', async () => { - mocked(persistenceService.watchConfig).mockImplementationOnce(() => { - throw new Error('boo'); - }); - await expect(() => initConfig(config, persistenceService)).rejects.toBeInstanceOf(Error); - }); - }); - - it('should not apply an invalid config file', async () => { - mocked(persistenceService.readConfig).mockResolvedValueOnce({ - found: true, - data: { - themeSource: -1, - }, - }); - await initConfig(config, persistenceService); - expect(config.themeSource).not.toBe(-1); - }); - - it('should bail if it cannot determine whether there is a config file', async () => { - mocked(persistenceService.readConfig).mockRejectedValue(new Error('boo')); - await expect(() => initConfig(config, persistenceService)).rejects.toBeInstanceOf(Error); - }); -}); - -describe('when it has loaded the config', () => { - let sutDisposer: Disposer; - let watcherDisposer: Disposer = jest.fn(); - let configChangedCallback: () => Promise; - - beforeEach(async () => { - mocked(persistenceService.readConfig).mockResolvedValueOnce({ - found: true, - data: {}, - }); - mocked(persistenceService.watchConfig).mockReturnValueOnce(watcherDisposer); - sutDisposer = await initConfig(config, persistenceService, throttleMs); - configChangedCallback = mocked(persistenceService.watchConfig).mock.calls[0][0]; - jest.resetAllMocks(); - }); - - it('should throttle saving changes to the config file', () => { - mocked(persistenceService.writeConfig).mockResolvedValue(undefined); - config.setThemeSource('dark'); - jest.advanceTimersByTime(lessThanThrottleMs); - config.setThemeSource('light'); - jest.advanceTimersByTime(throttleMs); - expect(persistenceService.writeConfig).toBeCalledTimes(1); - }); - - it('should handle config writing errors gracefully', () => { - mocked(persistenceService.writeConfig).mockRejectedValue(new Error('boo')); - config.setThemeSource('dark'); - jest.advanceTimersByTime(throttleMs); - expect(persistenceService.writeConfig).toBeCalledTimes(1); - }); - - it('should read the config file when it has changed', async () => { - mocked(persistenceService.readConfig).mockResolvedValueOnce({ - found: true, - data: { - themeSource: 'dark', - }, - }); - await configChangedCallback(); - // Do not write back the changes we have just read. - expect(persistenceService.writeConfig).not.toBeCalled(); - expect(config.themeSource).toBe('dark'); - }); - - it('should not apply an invalid config file when it has changed', async () => { - mocked(persistenceService.readConfig).mockResolvedValueOnce({ - found: true, - data: { - themeSource: -1, - }, - }); - await configChangedCallback(); - expect(config.themeSource).not.toBe(-1); - }); - - it('should handle config writing errors gracefully', async () => { - mocked(persistenceService.readConfig).mockRejectedValue(new Error('boo')); - await configChangedCallback(); - }); - - describe('when it was disposed', () => { - beforeEach(() => { - sutDisposer(); - }); - - it('should dispose the watcher', () => { - expect(watcherDisposer).toBeCalled(); - }); - - it('should not listen to store changes any more', () => { - config.setThemeSource('dark'); - jest.advanceTimersByTime(2 * throttleMs); - expect(persistenceService.writeConfig).not.toBeCalled(); - }); - }); -}); diff --git a/packages/main/src/controllers/__tests__/initConfig.spec.ts b/packages/main/src/controllers/__tests__/initConfig.spec.ts new file mode 100644 index 0000000..e386a07 --- /dev/null +++ b/packages/main/src/controllers/__tests__/initConfig.spec.ts @@ -0,0 +1,185 @@ +/* + * Copyright (C) 2021-2022 Kristóf Marussy + * + * This file is part of Sophie. + * + * Sophie is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { jest } from '@jest/globals'; +import { mocked } from 'jest-mock'; +import ms from 'ms'; + +import type ConfigPersistenceService from '../../services/ConfigPersistenceService'; +import { Config, config as configModel } from '../../stores/Config'; +import type Disposer from '../../utils/Disposer'; +import { silenceLogger } from '../../utils/log'; +import initConfig from '../initConfig'; + +let config: Config; +const persistenceService: ConfigPersistenceService = { + readConfig: jest.fn(), + writeConfig: jest.fn(), + watchConfig: jest.fn(), +}; +const lessThanThrottleMs = ms('0.1s'); +const throttleMs = ms('1s'); + +beforeAll(() => { + jest.useFakeTimers(); + silenceLogger(); +}); + +beforeEach(() => { + config = configModel.create(); +}); + +describe('when initializing', () => { + describe('when there is no config file', () => { + beforeEach(() => { + mocked(persistenceService.readConfig).mockResolvedValueOnce({ + found: false, + }); + }); + + it('should create a new config file', async () => { + await initConfig(config, persistenceService); + expect(persistenceService.writeConfig).toBeCalledTimes(1); + }); + + it('should bail if there is an an error creating the config file', async () => { + mocked(persistenceService.writeConfig).mockRejectedValue(new Error('boo')); + await expect(() => initConfig(config, persistenceService)).rejects.toBeInstanceOf(Error); + }); + }); + + describe('when there is a valid config file', () => { + beforeEach(() => { + mocked(persistenceService.readConfig).mockResolvedValueOnce({ + found: true, + data: { + themeSource: 'dark', + }, + }); + }); + + it('should read the existing config file is there is one', async () => { + await initConfig(config, persistenceService); + expect(persistenceService.writeConfig).not.toBeCalled(); + expect(config.themeSource).toBe('dark'); + }); + + it('should bail if it cannot set up a watcher', async () => { + mocked(persistenceService.watchConfig).mockImplementationOnce(() => { + throw new Error('boo'); + }); + await expect(() => initConfig(config, persistenceService)).rejects.toBeInstanceOf(Error); + }); + }); + + it('should not apply an invalid config file', async () => { + mocked(persistenceService.readConfig).mockResolvedValueOnce({ + found: true, + data: { + themeSource: -1, + }, + }); + await initConfig(config, persistenceService); + expect(config.themeSource).not.toBe(-1); + }); + + it('should bail if it cannot determine whether there is a config file', async () => { + mocked(persistenceService.readConfig).mockRejectedValue(new Error('boo')); + await expect(() => initConfig(config, persistenceService)).rejects.toBeInstanceOf(Error); + }); +}); + +describe('when it has loaded the config', () => { + let sutDisposer: Disposer; + const watcherDisposer: Disposer = jest.fn(); + let configChangedCallback: () => Promise; + + beforeEach(async () => { + mocked(persistenceService.readConfig).mockResolvedValueOnce({ + found: true, + data: {}, + }); + mocked(persistenceService.watchConfig).mockReturnValueOnce(watcherDisposer); + sutDisposer = await initConfig(config, persistenceService, throttleMs); + [[configChangedCallback]] = mocked(persistenceService.watchConfig).mock.calls; + jest.resetAllMocks(); + }); + + it('should throttle saving changes to the config file', () => { + mocked(persistenceService.writeConfig).mockResolvedValue(undefined); + config.setThemeSource('dark'); + jest.advanceTimersByTime(lessThanThrottleMs); + config.setThemeSource('light'); + jest.advanceTimersByTime(throttleMs); + expect(persistenceService.writeConfig).toBeCalledTimes(1); + }); + + it('should handle config writing errors gracefully', () => { + mocked(persistenceService.writeConfig).mockRejectedValue(new Error('boo')); + config.setThemeSource('dark'); + jest.advanceTimersByTime(throttleMs); + expect(persistenceService.writeConfig).toBeCalledTimes(1); + }); + + it('should read the config file when it has changed', async () => { + mocked(persistenceService.readConfig).mockResolvedValueOnce({ + found: true, + data: { + themeSource: 'dark', + }, + }); + await configChangedCallback(); + // Do not write back the changes we have just read. + expect(persistenceService.writeConfig).not.toBeCalled(); + expect(config.themeSource).toBe('dark'); + }); + + it('should not apply an invalid config file when it has changed', async () => { + mocked(persistenceService.readConfig).mockResolvedValueOnce({ + found: true, + data: { + themeSource: -1, + }, + }); + await configChangedCallback(); + expect(config.themeSource).not.toBe(-1); + }); + + it('should handle config writing errors gracefully', async () => { + mocked(persistenceService.readConfig).mockRejectedValue(new Error('boo')); + await configChangedCallback(); + }); + + describe('when it was disposed', () => { + beforeEach(() => { + sutDisposer(); + }); + + it('should dispose the watcher', () => { + expect(watcherDisposer).toBeCalled(); + }); + + it('should not listen to store changes any more', () => { + config.setThemeSource('dark'); + jest.advanceTimersByTime(2 * throttleMs); + expect(persistenceService.writeConfig).not.toBeCalled(); + }); + }); +}); diff --git a/packages/main/src/controllers/__tests__/initNativeTheme.spec.ts b/packages/main/src/controllers/__tests__/initNativeTheme.spec.ts new file mode 100644 index 0000000..bd33f48 --- /dev/null +++ b/packages/main/src/controllers/__tests__/initNativeTheme.spec.ts @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2021-2022 Kristóf Marussy + * + * This file is part of Sophie. + * + * Sophie is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { jest } from '@jest/globals'; +import { mocked } from 'jest-mock'; + +import { createMainStore, MainStore } from '../../stores/MainStore'; +import type Disposer from '../../utils/Disposer'; + +let shouldUseDarkColors = false; + +jest.unstable_mockModule('electron', () => ({ + nativeTheme: { + themeSource: 'system', + get shouldUseDarkColors() { + return shouldUseDarkColors; + }, + on: jest.fn(), + off: jest.fn(), + }, +})); + +const { nativeTheme } = await import('electron'); +const { default: initNativeTheme } = await import('../initNativeTheme'); + +let store: MainStore; +let disposeSut: Disposer; + +beforeEach(() => { + store = createMainStore(); + disposeSut = initNativeTheme(store); +}); + +it('should register a nativeTheme updated listener', () => { + expect(nativeTheme.on).toBeCalledWith('updated', expect.anything()); +}); + +it('should synchronize themeSource changes to the nativeTheme', () => { + store.config.setThemeSource('dark'); + expect(nativeTheme.themeSource).toBe('dark'); +}); + +it('should synchronize shouldUseDarkColors changes to the store', () => { + const listener = mocked(nativeTheme.on).mock.calls.find(([event]) => event === 'updated')![1]; + shouldUseDarkColors = true; + listener(); + expect(store.shared.shouldUseDarkColors).toBe(true); +}); + +it('should remove the listener on dispose', () => { + const listener = mocked(nativeTheme.on).mock.calls.find(([event]) => event === 'updated')![1]; + disposeSut(); + expect(nativeTheme.off).toBeCalledWith('updated', listener); +}); diff --git a/packages/main/src/controllers/__tests__/nativeTheme.spec.ts b/packages/main/src/controllers/__tests__/nativeTheme.spec.ts deleted file mode 100644 index 85d6dd2..0000000 --- a/packages/main/src/controllers/__tests__/nativeTheme.spec.ts +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright (C) 2021-2022 Kristóf Marussy - * - * This file is part of Sophie. - * - * Sophie is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, version 3. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { jest } from '@jest/globals'; -import { mocked } from 'jest-mock'; - -import { createMainStore, MainStore } from '../../stores/MainStore'; -import { Disposer } from '../../utils/disposer'; - -let shouldUseDarkColors = false; - -jest.unstable_mockModule('electron', () => ({ - nativeTheme: { - themeSource: 'system', - get shouldUseDarkColors() { - return shouldUseDarkColors; - }, - on: jest.fn(), - off: jest.fn(), - }, -})); - -const { nativeTheme } = await import('electron'); -const { initNativeTheme } = await import('../nativeTheme'); - -let store: MainStore; -let disposeSut: Disposer; - -beforeEach(() => { - store = createMainStore(); - disposeSut = initNativeTheme(store); -}); - -it('should register a nativeTheme updated listener', () => { - expect(nativeTheme.on).toBeCalledWith('updated', expect.anything()); -}); - -it('should synchronize themeSource changes to the nativeTheme', () => { - store.config.setThemeSource('dark'); - expect(nativeTheme.themeSource).toBe('dark'); -}); - -it('should synchronize shouldUseDarkColors changes to the store', () => { - const listener = mocked(nativeTheme.on).mock.calls.find(([event]) => event === 'updated')![1]; - shouldUseDarkColors = true; - listener(); - expect(store.shared.shouldUseDarkColors).toBe(true); -}); - -it('should remove the listener on dispose', () => { - const listener = mocked(nativeTheme.on).mock.calls.find(([event]) => event === 'updated')![1]; - disposeSut(); - expect(nativeTheme.off).toBeCalledWith('updated', listener); -}); diff --git a/packages/main/src/controllers/config.ts b/packages/main/src/controllers/config.ts deleted file mode 100644 index deaeac2..0000000 --- a/packages/main/src/controllers/config.ts +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Copyright (C) 2021-2022 Kristóf Marussy - * - * This file is part of Sophie. - * - * Sophie is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, version 3. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { debounce } from 'lodash-es'; -import ms from 'ms'; -import { applySnapshot, getSnapshot, onSnapshot } from 'mobx-state-tree'; - -import type { ConfigPersistenceService } from '../services/ConfigPersistenceService.js'; -import type { Config, ConfigSnapshotOut } from '../stores/Config.js'; -import { Disposer } from '../utils/disposer'; -import { getLogger } from '../utils/logging'; - -const DEFAULT_CONFIG_DEBOUNCE_TIME = ms('1s'); - -const log = getLogger('config'); - -export async function initConfig( - config: Config, - persistenceService: ConfigPersistenceService, - debounceTime: number = DEFAULT_CONFIG_DEBOUNCE_TIME, -): Promise { - log.trace('Initializing config controller'); - - let lastSnapshotOnDisk: ConfigSnapshotOut | null = null; - - async function readConfig(): Promise { - const result = await persistenceService.readConfig(); - if (result.found) { - try { - applySnapshot(config, result.data); - lastSnapshotOnDisk = getSnapshot(config); - } catch (err) { - log.error('Failed to apply config snapshot', result.data, err); - } - } - return result.found; - } - - async function writeConfig(): Promise { - const snapshot = getSnapshot(config); - await persistenceService.writeConfig(snapshot); - lastSnapshotOnDisk = snapshot; - } - - if (!await readConfig()) { - log.info('Config file was not found'); - await writeConfig(); - log.info('Created config file'); - } - - const disposeOnSnapshot = onSnapshot(config, debounce((snapshot) => { - // We can compare snapshots by reference, since it is only recreated on store changes. - if (lastSnapshotOnDisk !== snapshot) { - writeConfig().catch((err) => { - log.error('Failed to write config on config change', err); - }); - } - }, debounceTime)); - - const disposeWatcher = persistenceService.watchConfig(async () => { - try { - await readConfig(); - } catch (err) { - log.error('Failed to read config', err); - } - }, debounceTime); - - return () => { - log.trace('Disposing config controller'); - disposeWatcher(); - disposeOnSnapshot(); - }; -} diff --git a/packages/main/src/controllers/initConfig.ts b/packages/main/src/controllers/initConfig.ts new file mode 100644 index 0000000..1d40762 --- /dev/null +++ b/packages/main/src/controllers/initConfig.ts @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2021-2022 Kristóf Marussy + * + * This file is part of Sophie. + * + * Sophie is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { debounce } from 'lodash-es'; +import { applySnapshot, getSnapshot, onSnapshot } from 'mobx-state-tree'; +import ms from 'ms'; + +import type ConfigPersistenceService from '../services/ConfigPersistenceService'; +import type { Config, ConfigSnapshotOut } from '../stores/Config'; +import type Disposer from '../utils/Disposer'; +import { getLogger } from '../utils/log'; + +const DEFAULT_CONFIG_DEBOUNCE_TIME = ms('1s'); + +const log = getLogger('config'); + +export default async function initConfig( + config: Config, + persistenceService: ConfigPersistenceService, + debounceTime: number = DEFAULT_CONFIG_DEBOUNCE_TIME, +): Promise { + log.trace('Initializing config controller'); + + let lastSnapshotOnDisk: ConfigSnapshotOut | null = null; + + async function readConfig(): Promise { + const result = await persistenceService.readConfig(); + if (result.found) { + try { + applySnapshot(config, result.data); + lastSnapshotOnDisk = getSnapshot(config); + } catch (err) { + log.error('Failed to apply config snapshot', result.data, err); + } + } + return result.found; + } + + async function writeConfig(): Promise { + const snapshot = getSnapshot(config); + await persistenceService.writeConfig(snapshot); + lastSnapshotOnDisk = snapshot; + } + + if (!await readConfig()) { + log.info('Config file was not found'); + await writeConfig(); + log.info('Created config file'); + } + + const disposeOnSnapshot = onSnapshot(config, debounce((snapshot) => { + // We can compare snapshots by reference, since it is only recreated on store changes. + if (lastSnapshotOnDisk !== snapshot) { + writeConfig().catch((err) => { + log.error('Failed to write config on config change', err); + }); + } + }, debounceTime)); + + const disposeWatcher = persistenceService.watchConfig(async () => { + try { + await readConfig(); + } catch (err) { + log.error('Failed to read config', err); + } + }, debounceTime); + + return () => { + log.trace('Disposing config controller'); + disposeWatcher(); + disposeOnSnapshot(); + }; +} diff --git a/packages/main/src/controllers/initNativeTheme.ts b/packages/main/src/controllers/initNativeTheme.ts new file mode 100644 index 0000000..d2074ab --- /dev/null +++ b/packages/main/src/controllers/initNativeTheme.ts @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2021-2022 Kristóf Marussy + * + * This file is part of Sophie. + * + * Sophie is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { nativeTheme } from 'electron'; +import { autorun } from 'mobx'; + +import type { MainStore } from '../stores/MainStore'; +import type Disposer from '../utils/Disposer'; +import { getLogger } from '../utils/log'; + +const log = getLogger('nativeTheme'); + +export default function initNativeTheme(store: MainStore): Disposer { + log.trace('Initializing nativeTheme controller'); + + const disposeThemeSourceReaction = autorun(() => { + nativeTheme.themeSource = store.config.themeSource; + log.debug('Set theme source:', store.config.themeSource); + }); + + store.setShouldUseDarkColors(nativeTheme.shouldUseDarkColors); + const shouldUseDarkColorsListener = () => { + store.setShouldUseDarkColors(nativeTheme.shouldUseDarkColors); + log.debug('Set should use dark colors:', nativeTheme.shouldUseDarkColors); + }; + nativeTheme.on('updated', shouldUseDarkColorsListener); + + return () => { + log.trace('Disposing nativeTheme controller'); + nativeTheme.off('updated', shouldUseDarkColorsListener); + disposeThemeSourceReaction(); + }; +} diff --git a/packages/main/src/controllers/nativeTheme.ts b/packages/main/src/controllers/nativeTheme.ts deleted file mode 100644 index ccd12d8..0000000 --- a/packages/main/src/controllers/nativeTheme.ts +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright (C) 2021-2022 Kristóf Marussy - * - * This file is part of Sophie. - * - * Sophie is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, version 3. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { nativeTheme } from 'electron'; -import { autorun } from 'mobx'; - -import type { MainStore } from '../stores/MainStore.js'; -import { Disposer } from '../utils/disposer'; -import { getLogger } from '../utils/logging'; - -const log = getLogger('nativeTheme'); - -export function initNativeTheme(store: MainStore): Disposer { - log.trace('Initializing nativeTheme controller'); - - const disposeThemeSourceReaction = autorun(() => { - nativeTheme.themeSource = store.config.themeSource; - log.debug('Set theme source:', store.config.themeSource); - }); - - store.setShouldUseDarkColors(nativeTheme.shouldUseDarkColors); - const shouldUseDarkColorsListener = () => { - store.setShouldUseDarkColors(nativeTheme.shouldUseDarkColors); - log.debug('Set should use dark colors:', nativeTheme.shouldUseDarkColors); - }; - nativeTheme.on('updated', shouldUseDarkColorsListener); - - return () => { - log.trace('Disposing nativeTheme controller'); - nativeTheme.off('updated', shouldUseDarkColorsListener); - disposeThemeSourceReaction(); - }; -} diff --git a/packages/main/src/devTools.ts b/packages/main/src/devTools.ts index 398904c..0486c36 100644 --- a/packages/main/src/devTools.ts +++ b/packages/main/src/devTools.ts @@ -46,7 +46,12 @@ export async function installDevToolsExtensions(): Promise { default: installExtension, REACT_DEVELOPER_TOOLS, REDUX_DEVTOOLS, - } = require('electron-devtools-installer'); + /* eslint-disable-next-line + import/no-extraneous-dependencies, + global-require, + @typescript-eslint/no-var-requires + */ + } = require('electron-devtools-installer') as typeof import('electron-devtools-installer'); await installExtension( [ REACT_DEVELOPER_TOOLS, diff --git a/packages/main/src/index.ts b/packages/main/src/index.ts index d0191b7..bc10b4c 100644 --- a/packages/main/src/index.ts +++ b/packages/main/src/index.ts @@ -19,18 +19,10 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { - app, - BrowserView, - BrowserWindow, - ipcMain, -} from 'electron'; import { arch } from 'os'; -import osName from 'os-name'; -import { ensureDirSync, readFile, readFileSync } from 'fs-extra'; -import { autorun } from 'mobx'; -import { getSnapshot, onPatch } from 'mobx-state-tree'; import { join } from 'path'; +import { URL } from 'url'; + import { ServiceToMainIpcMessage, unreadCount, @@ -41,18 +33,30 @@ import { MainToRendererIpcMessage, RendererToMainIpcMessage, } from '@sophie/shared'; -import { URL } from 'url'; +import { + app, + BrowserView, + BrowserWindow, + ipcMain, +} from 'electron'; +import { ensureDirSync, readFile, readFileSync } from 'fs-extra'; +import { autorun } from 'mobx'; +import { getSnapshot, onPatch } from 'mobx-state-tree'; +import osName from 'os-name'; -import { init } from './compositionRoot'; import { DEVMODE_ALLOWED_URL_PREFIXES, installDevToolsExtensions, openDevToolsWhenReady, } from './devTools'; +import init from './init'; import { createMainStore } from './stores/MainStore'; +import { getLogger } from './utils/log'; const isDevelopment = import.meta.env.MODE === 'development'; +const log = getLogger('index'); + // Always enable sandboxing. app.enableSandbox(); @@ -93,7 +97,7 @@ app.setAboutPanelOptions({ `Node.js: ${process.versions.node}`, `Platform: ${osName()}`, `Arch: ${arch()}`, - `Build date: ${new Date(Number(import.meta.env.BUILD_DATE))}`, + `Build date: ${new Date(Number(import.meta.env.BUILD_DATE)).toLocaleString()}`, `Git SHA: ${import.meta.env.GIT_SHA}`, `Git branch: ${import.meta.env.GIT_BRANCH}`, ].join('\n'), @@ -109,9 +113,9 @@ function getResourceUrl(relativePath: string): string { return new URL(relativePath, baseUrl).toString(); } -let serviceInjectRelativePath = '../../service-inject/dist/index.js'; -let serviceInjectPath = getResourcePath(serviceInjectRelativePath); -let serviceInject: WebSource = { +const serviceInjectRelativePath = '../../service-inject/dist/index.js'; +const serviceInjectPath = getResourcePath(serviceInjectRelativePath); +const serviceInject: WebSource = { code: readFileSync(serviceInjectPath, 'utf8'), url: getResourceUrl(serviceInjectRelativePath), }; @@ -122,7 +126,7 @@ const store = createMainStore(); init(store).then((disposeCompositionRoot) => { app.on('will-quit', disposeCompositionRoot); }).catch((err) => { - console.log('Failed to initialize application', err); + log.log('Failed to initialize application', err); }); const rendererBaseUrl = getResourceUrl('../renderer/'); @@ -211,7 +215,7 @@ async function createWindow(): Promise { ipcMain.handle(RendererToMainIpcMessage.GetSharedStoreSnapshot, (event) => { if (event.sender.id !== webContents.id) { - console.warn( + log.warn( 'Unexpected', RendererToMainIpcMessage.GetSharedStoreSnapshot, 'from webContents', @@ -224,7 +228,7 @@ async function createWindow(): Promise { ipcMain.on(RendererToMainIpcMessage.DispatchAction, (event, rawAction) => { if (event.sender.id !== webContents.id) { - console.warn( + log.warn( 'Unexpected', RendererToMainIpcMessage.DispatchAction, 'from webContents', @@ -242,17 +246,26 @@ async function createWindow(): Promise { store.config.setThemeSource(actionToDispatch.themeSource); break; case 'reload-all-services': - readFile(serviceInjectPath, 'utf8').then((data) => { - serviceInject.code = data; - }).catch((err) => { - console.error('Error while reloading', serviceInjectPath, err); - }).then(() => { - browserView.webContents.reload(); - }); + readFile(serviceInjectPath, 'utf8') + .then((data) => { + serviceInject.code = data; + }) + .catch((err) => { + log.error('Error while reloading', serviceInjectPath, err); + }) + .then(() => { + browserView.webContents.reload(); + }) + .catch((err) => { + log.error('Failed to reload browserView', err); + }); + break; + default: + log.error('Unexpected action from UI renderer:', actionToDispatch); break; } } catch (err) { - console.error('Error while dispatching renderer action', rawAction, err); + log.error('Error while dispatching renderer action', rawAction, err); } }); @@ -260,11 +273,10 @@ async function createWindow(): Promise { webContents.send(MainToRendererIpcMessage.SharedStorePatch, patch); }); - ipcMain.handle(ServiceToMainIpcMessage.ApiExposedInMainWorld, (event) => { - return event.sender.id === browserView.webContents.id - ? serviceInject - : null; - }); + ipcMain.handle( + ServiceToMainIpcMessage.ApiExposedInMainWorld, + (event) => (event.sender.id === browserView.webContents.id ? serviceInject : null), + ); browserView.webContents.on('ipc-message', (_event, channel, ...args) => { try { @@ -274,14 +286,14 @@ async function createWindow(): Promise { // otherwise electron emits a no handler registered warning. break; case ServiceToMainIpcMessage.SetUnreadCount: - console.log('Unread count:', unreadCount.parse(args[0])); + log.log('Unread count:', unreadCount.parse(args[0])); break; default: - console.error('Unknown IPC message:', channel, args); + log.error('Unknown IPC message:', channel, args); break; } } catch (err) { - console.error('Error while processing IPC message:', channel, args, err); + log.error('Error while processing IPC message:', channel, args, err); } }); @@ -291,17 +303,22 @@ async function createWindow(): Promise { }, ); - browserView.webContents.session.webRequest.onBeforeSendHeaders(({ url, requestHeaders }, callback) => { - if (url.match(/^[^:]+:\/\/accounts\.google\.[^.\/]+\//)) { - requestHeaders['User-Agent'] = chromelessUserAgent; - } else { - requestHeaders['User-Agent'] = userAgent; - } - callback({ requestHeaders }); - }); + browserView.webContents.session.webRequest.onBeforeSendHeaders( + ({ url, requestHeaders }, callback) => { + const requestUserAgent = url.match(/^[^:]+:\/\/accounts\.google\.[^./]+\//) + ? chromelessUserAgent + : userAgent; + callback({ + requestHeaders: { + ...requestHeaders, + 'User-Agent': requestUserAgent, + }, + }); + }, + ); browserView.webContents.loadURL('https://gitlab.com/say-hi-to-sophie/sophie').catch((err) => { - console.error('Failed to load browser', err); + log.error('Failed to load browser', err); }); return mainWindow.loadURL(pageUrl); @@ -330,12 +347,12 @@ app.whenReady().then(async () => { try { await installDevToolsExtensions(); } catch (err) { - console.error('Failed to install devtools extensions', err); + log.error('Failed to install devtools extensions', err); } } return createWindow(); }).catch((err) => { - console.error('Failed to create window', err); + log.error('Failed to create window', err); process.exit(1); }); diff --git a/packages/main/src/init.ts b/packages/main/src/init.ts new file mode 100644 index 0000000..4487cc4 --- /dev/null +++ b/packages/main/src/init.ts @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2021-2022 Kristóf Marussy + * + * This file is part of Sophie. + * + * Sophie is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { app } from 'electron'; + +import initConfig from './controllers/initConfig'; +import initNativeTheme from './controllers/initNativeTheme'; +import ConfigPersistenceServiceImpl from './services/impl/ConfigPersistenceServiceImpl'; +import { MainStore } from './stores/MainStore'; +import type Disposer from './utils/Disposer'; + +export default async function init(store: MainStore): Promise { + const configPersistenceService = new ConfigPersistenceServiceImpl(app.getPath('userData')); + const disposeConfigController = await initConfig(store.config, configPersistenceService); + const disposeNativeThemeController = initNativeTheme(store); + + return () => { + disposeNativeThemeController(); + disposeConfigController(); + }; +} diff --git a/packages/main/src/services/ConfigPersistenceService.ts b/packages/main/src/services/ConfigPersistenceService.ts index aed0ba3..7d508c5 100644 --- a/packages/main/src/services/ConfigPersistenceService.ts +++ b/packages/main/src/services/ConfigPersistenceService.ts @@ -18,12 +18,12 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import type { ConfigSnapshotOut } from '../stores/Config'; -import { Disposer } from '../utils/disposer'; +import type { ConfigSnapshotOut } from '../stores/Config'; +import type Disposer from '../utils/Disposer'; export type ReadConfigResult = { found: true; data: unknown; } | { found: false; }; -export interface ConfigPersistenceService { +export default interface ConfigPersistenceService { readConfig(): Promise; writeConfig(configSnapshot: ConfigSnapshotOut): Promise; diff --git a/packages/main/src/services/impl/ConfigPersistenceServiceImpl.ts b/packages/main/src/services/impl/ConfigPersistenceServiceImpl.ts index 2d19632..df8c807 100644 --- a/packages/main/src/services/impl/ConfigPersistenceServiceImpl.ts +++ b/packages/main/src/services/impl/ConfigPersistenceServiceImpl.ts @@ -19,18 +19,20 @@ */ import { watch } from 'fs'; import { readFile, stat, writeFile } from 'fs/promises'; -import JSON5 from 'json5'; -import { throttle } from 'lodash-es'; import { join } from 'path'; -import type { ConfigPersistenceService, ReadConfigResult } from '../ConfigPersistenceService.js'; -import type { ConfigSnapshotOut } from '../../stores/Config.js'; -import { Disposer } from '../../utils/disposer'; -import { getLogger } from '../../utils/logging'; +import JSON5 from 'json5'; +import throttle from 'lodash-es/throttle'; + +import type { ConfigSnapshotOut } from '../../stores/Config'; +import type Disposer from '../../utils/Disposer'; +import { getLogger } from '../../utils/log'; +import type ConfigPersistenceService from '../ConfigPersistenceService'; +import type { ReadConfigResult } from '../ConfigPersistenceService'; const log = getLogger('configPersistence'); -export class ConfigPersistenceServiceImpl implements ConfigPersistenceService { +export default class ConfigPersistenceServiceImpl implements ConfigPersistenceService { private readonly configFilePath: string; private writingConfig = false; @@ -103,7 +105,7 @@ export class ConfigPersistenceServiceImpl implements ConfigPersistenceService { 'whish is newer than last written', this.timeLastWritten, ); - return callback(); + await callback(); } }, throttleMs); @@ -115,7 +117,7 @@ export class ConfigPersistenceServiceImpl implements ConfigPersistenceService { if (eventType === 'change' && (filename === this.configFileName || filename === null)) { configChanged()?.catch((err) => { - console.log('Unhandled error while listening for config changes', err); + log.error('Unhandled error while listening for config changes', err); }); } }); diff --git a/packages/main/src/stores/Config.ts b/packages/main/src/stores/Config.ts index 7d1168f..06dbdeb 100644 --- a/packages/main/src/stores/Config.ts +++ b/packages/main/src/stores/Config.ts @@ -18,13 +18,13 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { Instance } from 'mobx-state-tree'; import { config as originalConfig, ConfigSnapshotIn, ConfigSnapshotOut, ThemeSource, } from '@sophie/shared'; +import { Instance } from 'mobx-state-tree'; export const config = originalConfig.actions((self) => ({ setThemeSource(mode: ThemeSource) { diff --git a/packages/main/src/stores/MainStore.ts b/packages/main/src/stores/MainStore.ts index f8a09d6..7b26c52 100644 --- a/packages/main/src/stores/MainStore.ts +++ b/packages/main/src/stores/MainStore.ts @@ -18,8 +18,8 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { applySnapshot, Instance, types } from 'mobx-state-tree'; import { BrowserViewBounds } from '@sophie/shared'; +import { applySnapshot, Instance, types } from 'mobx-state-tree'; import type { Config } from './Config.js'; import { sharedStore } from './SharedStore'; diff --git a/packages/main/src/stores/SharedStore.ts b/packages/main/src/stores/SharedStore.ts index e20150d..c023fc7 100644 --- a/packages/main/src/stores/SharedStore.ts +++ b/packages/main/src/stores/SharedStore.ts @@ -18,8 +18,8 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { Instance, types } from 'mobx-state-tree'; import { sharedStore as originalSharedStore } from '@sophie/shared'; +import { Instance, types } from 'mobx-state-tree'; import { config } from './Config'; diff --git a/packages/main/src/utils/Disposer.ts b/packages/main/src/utils/Disposer.ts new file mode 100644 index 0000000..2e0ca25 --- /dev/null +++ b/packages/main/src/utils/Disposer.ts @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2021-2022 Kristóf Marussy + * + * This file is part of Sophie. + * + * Sophie is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { IDisposer } from 'mobx-state-tree'; + +type Disposer = IDisposer; + +export default Disposer; diff --git a/packages/main/src/utils/disposer.ts b/packages/main/src/utils/disposer.ts deleted file mode 100644 index 0d469dd..0000000 --- a/packages/main/src/utils/disposer.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright (C) 2021-2022 Kristóf Marussy - * - * This file is part of Sophie. - * - * Sophie is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, version 3. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { IDisposer } from 'mobx-state-tree'; - -export type Disposer = IDisposer; diff --git a/packages/main/src/utils/log.ts b/packages/main/src/utils/log.ts new file mode 100644 index 0000000..c704797 --- /dev/null +++ b/packages/main/src/utils/log.ts @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2021-2022 Kristóf Marussy + * + * This file is part of Sophie. + * + * Sophie is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import chalk, { ChalkInstance } from 'chalk'; +import loglevel, { Logger } from 'loglevel'; +import prefix from 'loglevel-plugin-prefix'; + +if (import.meta.env?.DEV) { + loglevel.setLevel('debug'); +} else { + loglevel.setLevel('info'); +} + +const COLORS: Partial> = { + TRACE: chalk.magenta, + DEBUG: chalk.cyan, + INFO: chalk.blue, + WARN: chalk.yellow, + ERROR: chalk.red, + CRITICAL: chalk.red, +}; + +function getColor(level: string): ChalkInstance { + return COLORS[level] ?? chalk.gray; +} + +prefix.reg(loglevel); +prefix.apply(loglevel, { + format(level, name, timestamp) { + const levelColor = getColor(level); + const timeStr = timestamp.toString(); + const nameStr = typeof name === 'undefined' + ? levelColor(':') + : ` ${chalk.green(`${name}:`)}`; + return `${chalk.gray(`[${timeStr}]`)} ${levelColor(level)}${nameStr}`; + }, +}); + +export function getLogger(loggerName: string): Logger { + return loglevel.getLogger(loggerName); +} + +export function silenceLogger(): void { + loglevel.disableAll(); + const loggers = loglevel.getLoggers(); + Object.keys(loggers).forEach((loggerName) => { + loggers[loggerName].disableAll(); + }); +} diff --git a/packages/main/src/utils/logging.ts b/packages/main/src/utils/logging.ts deleted file mode 100644 index f703749..0000000 --- a/packages/main/src/utils/logging.ts +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright (C) 2021-2022 Kristóf Marussy - * - * This file is part of Sophie. - * - * Sophie is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, version 3. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import chalk, { ChalkInstance } from 'chalk'; -import loglevel, { Logger } from 'loglevel'; -import prefix from 'loglevel-plugin-prefix'; - -if (import.meta.env?.DEV) { - loglevel.setLevel('debug'); -} else { - loglevel.setLevel('info'); -} - -const COLORS: Partial> = { - TRACE: chalk.magenta, - DEBUG: chalk.cyan, - INFO: chalk.blue, - WARN: chalk.yellow, - ERROR: chalk.red, - CRITICAL: chalk.red, -}; - -function getColor(level: string): ChalkInstance { - return COLORS[level] ?? chalk.gray; -} - -prefix.reg(loglevel); -prefix.apply(loglevel, { - format(level, name, timestamp) { - const levelColor = getColor(level); - return `${chalk.gray(`[${timestamp}]`)} ${levelColor(level)} ${chalk.green(`${name}:`)}`; - }, -}); - -export function getLogger(loggerName: string): Logger { - return loglevel.getLogger(loggerName); -} - -export function silenceLogger(): void { - loglevel.disableAll(); - const loggers = loglevel.getLoggers(); - for (const loggerName of Object.keys(loggers)) { - loggers[loggerName].disableAll(); - } -} diff --git a/packages/main/tsconfig.json b/packages/main/tsconfig.json index 1401445..00a1985 100644 --- a/packages/main/tsconfig.json +++ b/packages/main/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../tsconfig.json", + "extends": "../../config/tsconfig.base.json", "compilerOptions": { "noEmit": true, "types": [ @@ -9,14 +9,16 @@ }, "references": [ { - "path": "../service-shared" + "path": "../service-shared/tsconfig.build.json" }, { - "path": "../shared" + "path": "../shared/tsconfig.build.json" } ], "include": [ "src/**/*.ts", - "types/**/*.d.ts" + "types/**/*.d.ts", + "esbuild.config.js", + "jest.config.js" ] } diff --git a/packages/preload/.eslintrc.cjs b/packages/preload/.eslintrc.cjs new file mode 100644 index 0000000..02fab21 --- /dev/null +++ b/packages/preload/.eslintrc.cjs @@ -0,0 +1,6 @@ +module.exports = { + env: { + node: true, + browser: true, + }, +}; diff --git a/packages/preload/esbuild.config.js b/packages/preload/esbuild.config.js index b73a071..66f5e84 100644 --- a/packages/preload/esbuild.config.js +++ b/packages/preload/esbuild.config.js @@ -1,8 +1,8 @@ import { chrome } from '../../config/buildConstants.js'; -import { getConfig } from '../../config/esbuildConfig.js'; -import { fileURLToDirname } from '../../config/utils.js'; +import fileURLToDirname from '../../config/fileURLToDirname.js'; +import getEsbuildConfig from '../../config/getEsbuildConfig.js'; -export default getConfig({ +export default getEsbuildConfig({ absWorkingDir: fileURLToDirname(import.meta.url), entryPoints: [ 'src/index.ts', diff --git a/packages/preload/jest.config.js b/packages/preload/jest.config.js index e474c4c..27af475 100644 --- a/packages/preload/jest.config.js +++ b/packages/preload/jest.config.js @@ -1,6 +1,6 @@ import rootConfig from '../../config/jest.config.base.js'; -/** @type {import('ts-jest').InitialOptionsTsJest} */ +/** @type {import('@jest/types').Config.InitialOptions} */ export default { ...rootConfig, testEnvironment: 'jsdom', diff --git a/packages/preload/package.json b/packages/preload/package.json index 0957aaf..a03d7d9 100644 --- a/packages/preload/package.json +++ b/packages/preload/package.json @@ -6,7 +6,7 @@ "type": "module", "types": "dist-types/index.d.ts", "scripts": { - "typecheck": "tsc" + "typecheck:workspace": "yarn g:typecheck" }, "dependencies": { "@sophie/shared": "workspace:*", @@ -20,8 +20,6 @@ "@types/jest": "^27.4.0", "jest": "^27.4.7", "jest-mock": "^27.4.6", - "jsdom": "^19.0.0", - "rimraf": "^3.0.2", - "typescript": "^4.5.4" + "jsdom": "^19.0.0" } } diff --git a/packages/preload/src/contextBridge/SophieRendererImpl.ts b/packages/preload/src/contextBridge/SophieRendererImpl.ts deleted file mode 100644 index f3c07c5..0000000 --- a/packages/preload/src/contextBridge/SophieRendererImpl.ts +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Copyright (C) 2021-2022 Kristóf Marussy - * - * This file is part of Sophie. - * - * Sophie is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, version 3. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { ipcRenderer } from 'electron'; -import log from 'loglevel'; -import type { IJsonPatch } from 'mobx-state-tree'; -import { - Action, - action, - MainToRendererIpcMessage, - RendererToMainIpcMessage, - sharedStore, - SharedStoreListener, - SophieRenderer, -} from '@sophie/shared'; - -class SophieRendererImpl implements SophieRenderer { - private onSharedStoreChangeCalled: boolean = false; - - private listener: SharedStoreListener | null = null; - - constructor(private readonly allowReplaceListener: boolean) { - ipcRenderer.on(MainToRendererIpcMessage.SharedStorePatch, (_event, patch) => { - try { - // `mobx-state-tree` will validate the patch, so we can safely cast here. - this.listener?.onPatch(patch as IJsonPatch); - } catch (err) { - log.error('Shared store listener onPatch failed', err); - this.listener = null; - } - }); - } - - async onSharedStoreChange(listener: SharedStoreListener): Promise { - if (this.onSharedStoreChangeCalled && !this.allowReplaceListener) { - throw new Error('Shared store change listener was already set'); - } - this.onSharedStoreChangeCalled = true; - let success = false; - let snapshot: unknown | null = null; - try { - snapshot = await ipcRenderer.invoke(RendererToMainIpcMessage.GetSharedStoreSnapshot); - success = true; - } catch (err) { - log.error('Failed to get initial shared store snapshot', err); - } - if (success) { - if (sharedStore.is(snapshot)) { - listener.onSnapshot(snapshot); - this.listener = listener; - return; - } - log.error('Got invalid initial shared store snapshot', snapshot); - } - throw new Error('Failed to connect to shared store'); - } - - dispatchAction(actionToDispatch: Action): void { - // Let the full zod parse error bubble up to the main world, - // since all data it may contain was provided from the main world. - const parsedAction = action.parse(actionToDispatch); - try { - ipcRenderer.send(RendererToMainIpcMessage.DispatchAction, parsedAction); - } catch (err) { - // Do not leak IPC failure details into the main world. - const message = 'Failed to dispatch action'; - log.error(message, actionToDispatch, err); - throw new Error(message); - } - } -} - -export function createSophieRenderer(allowReplaceListener: boolean): SophieRenderer { - const impl = new SophieRendererImpl(allowReplaceListener); - return { - onSharedStoreChange: impl.onSharedStoreChange.bind(impl), - dispatchAction: impl.dispatchAction.bind(impl), - }; -} diff --git a/packages/preload/src/contextBridge/__tests__/SophieRendererImpl.spec.ts b/packages/preload/src/contextBridge/__tests__/SophieRendererImpl.spec.ts deleted file mode 100644 index ff77a63..0000000 --- a/packages/preload/src/contextBridge/__tests__/SophieRendererImpl.spec.ts +++ /dev/null @@ -1,258 +0,0 @@ -/* - * Copyright (C) 2021-2022 Kristóf Marussy - * - * This file is part of Sophie. - * - * Sophie is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, version 3. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { jest } from '@jest/globals'; -import { mocked } from 'jest-mock'; -import log from 'loglevel'; -import type { IJsonPatch } from 'mobx-state-tree'; -import { - Action, - MainToRendererIpcMessage, - RendererToMainIpcMessage, - SharedStoreSnapshotIn, - SophieRenderer, -} from '@sophie/shared'; - -jest.unstable_mockModule('electron', () => ({ - ipcRenderer: { - invoke: jest.fn(), - on: jest.fn(), - send: jest.fn(), - }, -})); - -const { ipcRenderer } = await import('electron'); - -const { createSophieRenderer } = await import('../SophieRendererImpl'); - -const event: Electron.IpcRendererEvent = null as unknown as Electron.IpcRendererEvent; - -const snapshot: SharedStoreSnapshotIn = { - shouldUseDarkColors: true, -}; - -const invalidSnapshot = { - shouldUseDarkColors: -1, -} as unknown as SharedStoreSnapshotIn; - -const patch: IJsonPatch = { - op: 'replace', - path: 'foo', - value: 'bar', -}; - -const action: Action = { - action: 'set-theme-source', - themeSource: 'dark', -}; - -const invalidAction = { - action: 'not-a-valid-action', -} as unknown as Action; - -beforeAll(() => { - log.disableAll(); -}); - -describe('createSophieRenderer', () => { - it('registers a shared store patch listener', () => { - createSophieRenderer(false); - expect(ipcRenderer.on).toHaveBeenCalledWith( - MainToRendererIpcMessage.SharedStorePatch, - expect.anything(), - ); - }); -}); - -describe('SophieRendererImpl', () => { - let sut: SophieRenderer; - let onSharedStorePatch: (event1: Electron.IpcRendererEvent, patch1: unknown) => void; - let listener = { - // eslint-disable-next-line @typescript-eslint/no-unused-vars, no-unused-vars - onSnapshot: jest.fn((_snapshot: SharedStoreSnapshotIn) => {}), - // eslint-disable-next-line @typescript-eslint/no-unused-vars, no-unused-vars - onPatch: jest.fn((_patch: IJsonPatch) => {}), - }; - - beforeEach(() => { - sut = createSophieRenderer(false); - onSharedStorePatch = mocked(ipcRenderer.on).mock.calls.find(([channel]) => { - return channel === MainToRendererIpcMessage.SharedStorePatch; - })?.[1]!; - }); - - describe('onSharedStoreChange', () => { - it('should request a snapshot from the main process', async () => { - mocked(ipcRenderer.invoke).mockResolvedValueOnce(snapshot); - await sut.onSharedStoreChange(listener); - expect(ipcRenderer.invoke).toBeCalledWith(RendererToMainIpcMessage.GetSharedStoreSnapshot); - expect(listener.onSnapshot).toBeCalledWith(snapshot); - }); - - it('should catch IPC errors without exposing them', async () => { - mocked(ipcRenderer.invoke).mockRejectedValue(new Error('s3cr3t')); - await expect(sut.onSharedStoreChange(listener)).rejects.not.toHaveProperty( - 'message', - expect.stringMatching(/s3cr3t/), - ); - expect(listener.onSnapshot).not.toBeCalled(); - }); - - it('should not pass on invalid snapshots', async () => { - mocked(ipcRenderer.invoke).mockResolvedValueOnce(invalidSnapshot); - await expect(sut.onSharedStoreChange(listener)).rejects.toBeInstanceOf(Error); - expect(listener.onSnapshot).not.toBeCalled(); - }); - }); - - describe('dispatchAction', () => { - it('should dispatch valid actions', () => { - sut.dispatchAction(action); - expect(ipcRenderer.send).toBeCalledWith(RendererToMainIpcMessage.DispatchAction, action); - }); - - it('should not dispatch invalid actions', () => { - expect(() => sut.dispatchAction(invalidAction)).toThrowError(); - expect(ipcRenderer.send).not.toBeCalled(); - }); - }); - - describe('when no listener is registered', () => { - it('should discard the received patch without any error', () => { - onSharedStorePatch(event, patch); - }); - }); - - function itRefusesToRegisterAnotherListener() { - it('should refuse to register another listener', async () => { - await expect(sut.onSharedStoreChange(listener)).rejects.toBeInstanceOf(Error); - }); - } - - function itDoesNotPassPatchesToTheListener( - name: string = 'should not pass patches to the listener', - ) { - it(name, () => { - onSharedStorePatch(event, patch); - expect(listener.onPatch).not.toBeCalled(); - }); - } - - describe('when a listener registered successfully', () => { - beforeEach(async () => { - mocked(ipcRenderer.invoke).mockResolvedValueOnce(snapshot); - await sut.onSharedStoreChange(listener); - }); - - it('should pass patches to the listener', () => { - onSharedStorePatch(event, patch); - expect(listener.onPatch).toBeCalledWith(patch); - }); - - it('should catch listener errors', () => { - mocked(listener.onPatch).mockImplementation(() => { throw new Error(); }); - onSharedStorePatch(event, patch); - }); - - itRefusesToRegisterAnotherListener(); - - describe('after the listener threw in onPatch', () => { - beforeEach(() => { - mocked(listener.onPatch).mockImplementation(() => { throw new Error(); }); - onSharedStorePatch(event, patch); - listener.onPatch.mockRestore(); - }); - - itDoesNotPassPatchesToTheListener('should not pass on patches any more'); - }); - }); - - describe('when a listener failed to register due to IPC error', () => { - beforeEach(async () => { - mocked(ipcRenderer.invoke).mockRejectedValue(new Error()); - try { - await sut.onSharedStoreChange(listener); - } catch { - // Ignore error. - } - }); - - itRefusesToRegisterAnotherListener(); - - itDoesNotPassPatchesToTheListener(); - }); - - describe('when a listener failed to register due to an invalid snapshot', () => { - beforeEach(async () => { - mocked(ipcRenderer.invoke).mockResolvedValueOnce(invalidSnapshot); - try { - await sut.onSharedStoreChange(listener); - } catch { - // Ignore error. - } - }); - - itRefusesToRegisterAnotherListener(); - - itDoesNotPassPatchesToTheListener(); - }); - - describe('when a listener failed to register due to listener error', () => { - beforeEach(async () => { - mocked(ipcRenderer.invoke).mockResolvedValueOnce(snapshot); - mocked(listener.onSnapshot).mockImplementation(() => { throw new Error(); }); - try { - await sut.onSharedStoreChange(listener); - } catch { - // Ignore error. - } - }); - - itRefusesToRegisterAnotherListener(); - - itDoesNotPassPatchesToTheListener(); - }); - - describe('when it is allowed to replace listeners', () => { - const snapshot2 = { - shouldUseDarkColors: false, - }; - const listener2 = { - // eslint-disable-next-line @typescript-eslint/no-unused-vars, no-unused-vars - onSnapshot: jest.fn((_snapshot: SharedStoreSnapshotIn) => { }), - // eslint-disable-next-line @typescript-eslint/no-unused-vars, no-unused-vars - onPatch: jest.fn((_patch: IJsonPatch) => { }), - }; - - it('should fetch a second snapshot', async () => { - mocked(ipcRenderer.invoke).mockResolvedValueOnce(snapshot2); - await sut.onSharedStoreChange(listener2); - expect(ipcRenderer.invoke).toBeCalledWith(RendererToMainIpcMessage.GetSharedStoreSnapshot); - expect(listener2.onSnapshot).toBeCalledWith(snapshot2); - }); - - it('should pass the second snapshot to the new listener', async () => { - mocked(ipcRenderer.invoke).mockResolvedValueOnce(snapshot2); - await sut.onSharedStoreChange(listener2); - onSharedStorePatch(event, patch); - expect(listener2.onPatch).toBeCalledWith(patch); - }); - }); -}); diff --git a/packages/preload/src/contextBridge/__tests__/createSophieRenderer.spec.ts b/packages/preload/src/contextBridge/__tests__/createSophieRenderer.spec.ts new file mode 100644 index 0000000..a38dbac --- /dev/null +++ b/packages/preload/src/contextBridge/__tests__/createSophieRenderer.spec.ts @@ -0,0 +1,258 @@ +/* + * Copyright (C) 2021-2022 Kristóf Marussy + * + * This file is part of Sophie. + * + * Sophie is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { jest } from '@jest/globals'; +import { + Action, + MainToRendererIpcMessage, + RendererToMainIpcMessage, + SharedStoreSnapshotIn, + SophieRenderer, +} from '@sophie/shared'; +import { mocked } from 'jest-mock'; +import log from 'loglevel'; +import type { IJsonPatch } from 'mobx-state-tree'; + +jest.unstable_mockModule('electron', () => ({ + ipcRenderer: { + invoke: jest.fn(), + on: jest.fn(), + send: jest.fn(), + }, +})); + +const { ipcRenderer } = await import('electron'); + +const { default: createSophieRenderer } = await import('../createSophieRenderer'); + +const event: Electron.IpcRendererEvent = null as unknown as Electron.IpcRendererEvent; + +const snapshot: SharedStoreSnapshotIn = { + shouldUseDarkColors: true, +}; + +const invalidSnapshot = { + shouldUseDarkColors: -1, +} as unknown as SharedStoreSnapshotIn; + +const patch: IJsonPatch = { + op: 'replace', + path: 'foo', + value: 'bar', +}; + +const action: Action = { + action: 'set-theme-source', + themeSource: 'dark', +}; + +const invalidAction = { + action: 'not-a-valid-action', +} as unknown as Action; + +beforeAll(() => { + log.disableAll(); +}); + +describe('createSophieRenderer', () => { + it('registers a shared store patch listener', () => { + createSophieRenderer(false); + expect(ipcRenderer.on).toHaveBeenCalledWith( + MainToRendererIpcMessage.SharedStorePatch, + expect.anything(), + ); + }); +}); + +describe('SharedStoreConnector', () => { + let sut: SophieRenderer; + let onSharedStorePatch: (eventArg: Electron.IpcRendererEvent, patchArg: unknown) => void; + const listener = { + // eslint-disable-next-line @typescript-eslint/no-unused-vars, no-unused-vars + onSnapshot: jest.fn((_snapshot: SharedStoreSnapshotIn) => {}), + // eslint-disable-next-line @typescript-eslint/no-unused-vars, no-unused-vars + onPatch: jest.fn((_patch: IJsonPatch) => {}), + }; + + beforeEach(() => { + sut = createSophieRenderer(false); + [, onSharedStorePatch] = mocked(ipcRenderer.on).mock.calls.find( + ([channel]) => channel === MainToRendererIpcMessage.SharedStorePatch, + )!; + }); + + describe('onSharedStoreChange', () => { + it('should request a snapshot from the main process', async () => { + mocked(ipcRenderer.invoke).mockResolvedValueOnce(snapshot); + await sut.onSharedStoreChange(listener); + expect(ipcRenderer.invoke).toBeCalledWith(RendererToMainIpcMessage.GetSharedStoreSnapshot); + expect(listener.onSnapshot).toBeCalledWith(snapshot); + }); + + it('should catch IPC errors without exposing them', async () => { + mocked(ipcRenderer.invoke).mockRejectedValue(new Error('s3cr3t')); + await expect(sut.onSharedStoreChange(listener)).rejects.not.toHaveProperty( + 'message', + expect.stringMatching(/s3cr3t/), + ); + expect(listener.onSnapshot).not.toBeCalled(); + }); + + it('should not pass on invalid snapshots', async () => { + mocked(ipcRenderer.invoke).mockResolvedValueOnce(invalidSnapshot); + await expect(sut.onSharedStoreChange(listener)).rejects.toBeInstanceOf(Error); + expect(listener.onSnapshot).not.toBeCalled(); + }); + }); + + describe('dispatchAction', () => { + it('should dispatch valid actions', () => { + sut.dispatchAction(action); + expect(ipcRenderer.send).toBeCalledWith(RendererToMainIpcMessage.DispatchAction, action); + }); + + it('should not dispatch invalid actions', () => { + expect(() => sut.dispatchAction(invalidAction)).toThrowError(); + expect(ipcRenderer.send).not.toBeCalled(); + }); + }); + + describe('when no listener is registered', () => { + it('should discard the received patch without any error', () => { + onSharedStorePatch(event, patch); + }); + }); + + function itRefusesToRegisterAnotherListener(): void { + it('should refuse to register another listener', async () => { + await expect(sut.onSharedStoreChange(listener)).rejects.toBeInstanceOf(Error); + }); + } + + function itDoesNotPassPatchesToTheListener( + name = 'should not pass patches to the listener', + ): void { + it(name, () => { + onSharedStorePatch(event, patch); + expect(listener.onPatch).not.toBeCalled(); + }); + } + + describe('when a listener registered successfully', () => { + beforeEach(async () => { + mocked(ipcRenderer.invoke).mockResolvedValueOnce(snapshot); + await sut.onSharedStoreChange(listener); + }); + + it('should pass patches to the listener', () => { + onSharedStorePatch(event, patch); + expect(listener.onPatch).toBeCalledWith(patch); + }); + + it('should catch listener errors', () => { + mocked(listener.onPatch).mockImplementation(() => { throw new Error(); }); + onSharedStorePatch(event, patch); + }); + + itRefusesToRegisterAnotherListener(); + + describe('after the listener threw in onPatch', () => { + beforeEach(() => { + mocked(listener.onPatch).mockImplementation(() => { throw new Error(); }); + onSharedStorePatch(event, patch); + listener.onPatch.mockRestore(); + }); + + itDoesNotPassPatchesToTheListener('should not pass on patches any more'); + }); + }); + + describe('when a listener failed to register due to IPC error', () => { + beforeEach(async () => { + mocked(ipcRenderer.invoke).mockRejectedValue(new Error()); + try { + await sut.onSharedStoreChange(listener); + } catch { + // Ignore error. + } + }); + + itRefusesToRegisterAnotherListener(); + + itDoesNotPassPatchesToTheListener(); + }); + + describe('when a listener failed to register due to an invalid snapshot', () => { + beforeEach(async () => { + mocked(ipcRenderer.invoke).mockResolvedValueOnce(invalidSnapshot); + try { + await sut.onSharedStoreChange(listener); + } catch { + // Ignore error. + } + }); + + itRefusesToRegisterAnotherListener(); + + itDoesNotPassPatchesToTheListener(); + }); + + describe('when a listener failed to register due to listener error', () => { + beforeEach(async () => { + mocked(ipcRenderer.invoke).mockResolvedValueOnce(snapshot); + mocked(listener.onSnapshot).mockImplementation(() => { throw new Error(); }); + try { + await sut.onSharedStoreChange(listener); + } catch { + // Ignore error. + } + }); + + itRefusesToRegisterAnotherListener(); + + itDoesNotPassPatchesToTheListener(); + }); + + describe('when it is allowed to replace listeners', () => { + const snapshot2 = { + shouldUseDarkColors: false, + }; + const listener2 = { + // eslint-disable-next-line @typescript-eslint/no-unused-vars, no-unused-vars + onSnapshot: jest.fn((_snapshot: SharedStoreSnapshotIn) => { }), + // eslint-disable-next-line @typescript-eslint/no-unused-vars, no-unused-vars + onPatch: jest.fn((_patch: IJsonPatch) => { }), + }; + + it('should fetch a second snapshot', async () => { + mocked(ipcRenderer.invoke).mockResolvedValueOnce(snapshot2); + await sut.onSharedStoreChange(listener2); + expect(ipcRenderer.invoke).toBeCalledWith(RendererToMainIpcMessage.GetSharedStoreSnapshot); + expect(listener2.onSnapshot).toBeCalledWith(snapshot2); + }); + + it('should pass the second snapshot to the new listener', async () => { + mocked(ipcRenderer.invoke).mockResolvedValueOnce(snapshot2); + await sut.onSharedStoreChange(listener2); + onSharedStorePatch(event, patch); + expect(listener2.onPatch).toBeCalledWith(patch); + }); + }); +}); diff --git a/packages/preload/src/contextBridge/createSophieRenderer.ts b/packages/preload/src/contextBridge/createSophieRenderer.ts new file mode 100644 index 0000000..2055080 --- /dev/null +++ b/packages/preload/src/contextBridge/createSophieRenderer.ts @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2021-2022 Kristóf Marussy + * + * This file is part of Sophie. + * + * Sophie is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { + Action, + action, + MainToRendererIpcMessage, + RendererToMainIpcMessage, + sharedStore, + SharedStoreListener, + SophieRenderer, +} from '@sophie/shared'; +import { ipcRenderer } from 'electron'; +import log from 'loglevel'; +import type { IJsonPatch } from 'mobx-state-tree'; + +class SharedStoreConnector { + private onSharedStoreChangeCalled = false; + + private listener: SharedStoreListener | null = null; + + constructor(private readonly allowReplaceListener: boolean) { + ipcRenderer.on(MainToRendererIpcMessage.SharedStorePatch, (_event, patch) => { + try { + // `mobx-state-tree` will validate the patch, so we can safely cast here. + this.listener?.onPatch(patch as IJsonPatch); + } catch (err) { + log.error('Shared store listener onPatch failed', err); + this.listener = null; + } + }); + } + + async onSharedStoreChange(listener: SharedStoreListener): Promise { + if (this.onSharedStoreChangeCalled && !this.allowReplaceListener) { + throw new Error('Shared store change listener was already set'); + } + this.onSharedStoreChangeCalled = true; + let success = false; + let snapshot: unknown | null = null; + try { + snapshot = await ipcRenderer.invoke(RendererToMainIpcMessage.GetSharedStoreSnapshot); + success = true; + } catch (err) { + log.error('Failed to get initial shared store snapshot', err); + } + if (success) { + if (sharedStore.is(snapshot)) { + listener.onSnapshot(snapshot); + this.listener = listener; + return; + } + log.error('Got invalid initial shared store snapshot', snapshot); + } + throw new Error('Failed to connect to shared store'); + } +} + +function dispatchAction(actionToDispatch: Action): void { + // Let the full zod parse error bubble up to the main world, + // since all data it may contain was provided from the main world. + const parsedAction = action.parse(actionToDispatch); + try { + ipcRenderer.send(RendererToMainIpcMessage.DispatchAction, parsedAction); + } catch (err) { + // Do not leak IPC failure details into the main world. + const message = 'Failed to dispatch action'; + log.error(message, actionToDispatch, err); + throw new Error(message); + } +} + +export default function createSophieRenderer(allowReplaceListener: boolean): SophieRenderer { + const connector = new SharedStoreConnector(allowReplaceListener); + return { + onSharedStoreChange: connector.onSharedStoreChange.bind(connector), + dispatchAction, + }; +} diff --git a/packages/preload/src/index.ts b/packages/preload/src/index.ts index de91742..f13220c 100644 --- a/packages/preload/src/index.ts +++ b/packages/preload/src/index.ts @@ -20,7 +20,7 @@ import { contextBridge } from 'electron'; -import { createSophieRenderer } from './contextBridge/SophieRendererImpl'; +import createSophieRenderer from './contextBridge/createSophieRenderer'; const isDevelopment = import.meta.env.MODE === 'development'; diff --git a/packages/preload/tsconfig.json b/packages/preload/tsconfig.json index 741d435..ff49538 100644 --- a/packages/preload/tsconfig.json +++ b/packages/preload/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../tsconfig.json", + "extends": "../../config/tsconfig.base.json", "compilerOptions": { "noEmit": true, "lib": [ @@ -13,11 +13,13 @@ }, "references": [ { - "path": "../shared" + "path": "../shared/tsconfig.build.json" } ], "include": [ "src/**/*.ts", - "types/**/*.d.ts" + "types/**/*.d.ts", + "esbuild.config.js", + "jest.config.js" ] } diff --git a/packages/renderer/.eslinrc.cjs b/packages/renderer/.eslinrc.cjs new file mode 100644 index 0000000..3385ac5 --- /dev/null +++ b/packages/renderer/.eslinrc.cjs @@ -0,0 +1,11 @@ +module.exports = { + extends: [ + 'airbnb', + 'airbnb/hooks', + 'airbnb-typescript', + ], + env: { + node: false, + browser: true, + }, +}; diff --git a/packages/renderer/.eslintrc.json b/packages/renderer/.eslintrc.json deleted file mode 100644 index a28aec9..0000000 --- a/packages/renderer/.eslintrc.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "globals": { - "JSX": false - } -} diff --git a/packages/renderer/package.json b/packages/renderer/package.json index df15abb..fde4c28 100644 --- a/packages/renderer/package.json +++ b/packages/renderer/package.json @@ -5,7 +5,7 @@ "type": "module", "types": "dist-types/index.d.ts", "scripts": { - "typecheck": "tsc" + "typecheck:workspace": "yarn g:typecheck" }, "dependencies": { "@emotion/react": "^11.7.1", @@ -14,7 +14,9 @@ "@mui/icons-material": "^5.2.5", "@mui/material": "^5.2.7", "@sophie/shared": "workspace:*", - "lodash": "^4.17.21", + "lodash-es": "^4.17.21", + "loglevel": "^1.8.0", + "loglevel-plugin-prefix": "^0.8.4", "mobx": "^6.3.12", "mobx-react-lite": "^3.2.3", "mobx-state-tree": "^5.1.0", @@ -22,14 +24,12 @@ "react-dom": "^17.0.2" }, "devDependencies": { - "@types/lodash": "^4.14.178", + "@types/lodash-es": "^4.14.178", "@types/react": "^17.0.38", "@types/react-dom": "^17.0.11", "@vitejs/plugin-react": "^1.1.4", "mst-middlewares": "^5.1.0", "remotedev": "^0.2.9", - "rimraf": "^3.0.2", - "typescript": "^4.5.4", "vite": "^2.7.10" } } diff --git a/packages/renderer/src/components/App.tsx b/packages/renderer/src/components/App.tsx index 8bd3dd8..1174bbb 100644 --- a/packages/renderer/src/components/App.tsx +++ b/packages/renderer/src/components/App.tsx @@ -21,10 +21,10 @@ import Box from '@mui/material/Box'; import React from 'react'; -import { BrowserViewPlaceholder } from './BrowserViewPlaceholder'; -import { Sidebar } from './Sidebar'; +import BrowserViewPlaceholder from './BrowserViewPlaceholder'; +import Sidebar from './Sidebar'; -export function App(): JSX.Element { +export default function App(): JSX.Element { return ( { + const store = useStore(); const onResize = useCallback(throttle(([entry]: ResizeObserverEntry[]) => { if (entry) { @@ -38,14 +36,14 @@ export const BrowserViewPlaceholder = observer(function BrowserViewPlaceholder() width, height, } = entry.target.getBoundingClientRect(); - setBrowserViewBounds({ + store.setBrowserViewBounds({ x, y, width, height, }); } - }, 100), [setBrowserViewBounds]); + }, 100), [store]); const resizeObserverRef = useRef(null); diff --git a/packages/renderer/src/components/Sidebar.tsx b/packages/renderer/src/components/Sidebar.tsx index 6c79932..44a47b0 100644 --- a/packages/renderer/src/components/Sidebar.tsx +++ b/packages/renderer/src/components/Sidebar.tsx @@ -21,9 +21,9 @@ import Box from '@mui/material/Box'; import React from 'react'; -import { ToggleDarkModeButton } from './ToggleDarkModeButton'; +import ToggleDarkModeButton from './ToggleDarkModeButton'; -export function Sidebar(): JSX.Element { +export default function Sidebar(): JSX.Element { return ( ({ diff --git a/packages/renderer/src/components/StoreProvider.tsx b/packages/renderer/src/components/StoreProvider.tsx index da1e699..cde6a31 100644 --- a/packages/renderer/src/components/StoreProvider.tsx +++ b/packages/renderer/src/components/StoreProvider.tsx @@ -32,7 +32,7 @@ export function useStore(): RendererStore { return store; } -export function StoreProvider({ children, store }: { +export default function StoreProvider({ children, store }: { children: JSX.Element | JSX.Element[], store: RendererStore, }): JSX.Element { diff --git a/packages/renderer/src/components/ThemeProvider.tsx b/packages/renderer/src/components/ThemeProvider.tsx index 9215f5c..eacaa52 100644 --- a/packages/renderer/src/components/ThemeProvider.tsx +++ b/packages/renderer/src/components/ThemeProvider.tsx @@ -18,18 +18,18 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { observer } from 'mobx-react-lite'; import { unstable_createMuiStrictModeTheme as createTheme, ThemeProvider as MuiThemeProvider, } from '@mui/material/styles'; +import { observer } from 'mobx-react-lite'; import React from 'react'; import { useStore } from './StoreProvider'; -export const ThemeProvider = observer(function ThemeProvider({ children }: { - children: JSX.Element | JSX.Element[], -}) { +export default observer(({ children }: { + children: JSX.Element | JSX.Element[]; +}) => { const { shared: { shouldUseDarkColors } } = useStore(); const theme = createTheme({ diff --git a/packages/renderer/src/components/ToggleDarkModeButton.tsx b/packages/renderer/src/components/ToggleDarkModeButton.tsx index 1b6757e..c8ffdf0 100644 --- a/packages/renderer/src/components/ToggleDarkModeButton.tsx +++ b/packages/renderer/src/components/ToggleDarkModeButton.tsx @@ -18,21 +18,22 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { observer } from 'mobx-react-lite'; import DarkModeIcon from '@mui/icons-material/DarkMode'; import LightModeIcon from '@mui/icons-material/LightMode'; import IconButton from '@mui/material/IconButton'; +import { observer } from 'mobx-react-lite'; import React from 'react'; import { useStore } from './StoreProvider'; -export const ToggleDarkModeButton = observer(function ToggleDarkModeButton() { - const { shared: { shouldUseDarkColors }, toggleDarkMode } = useStore(); +export default observer(() => { + const store = useStore(); + const { shared: { shouldUseDarkColors } } = store; return ( toggleDarkMode()} + onClick={() => store.toggleDarkMode()} > {shouldUseDarkColors ? : } diff --git a/packages/renderer/src/devTools.ts b/packages/renderer/src/devTools.ts index 3ec66aa..3d3ba99 100644 --- a/packages/renderer/src/devTools.ts +++ b/packages/renderer/src/devTools.ts @@ -32,30 +32,23 @@ import type { IAnyStateTreeNode } from 'mobx-state-tree'; * However, we don't bundle `remotedev` in production, so the call would fail anyways. * * @param model The store to connect to the redux devtools. + * @return A promise that resolves when the store was exposed to the devtools. * @see https://github.com/SocketCluster/socketcluster-client/issues/118#issuecomment-469064682 */ -async function exposeToReduxDevtoolsAsync(model: IAnyStateTreeNode): Promise { +export async function exposeToReduxDevtools(model: IAnyStateTreeNode): Promise { (window as { global?: unknown }).global = window; + // Hack to load dev dependencies on demand. const [remotedev, { connectReduxDevtools }] = await Promise.all([ - // @ts-ignore - import('remotedev'), + // @ts-expect-error `remotedev` has no typings. + // eslint-disable-next-line import/no-extraneous-dependencies + import('remotedev') as unknown, + // eslint-disable-next-line import/no-extraneous-dependencies import('mst-middlewares'), ]); connectReduxDevtools(remotedev, model); } -/** - * Connects the `model` to the redux devtools extension. - * - * @param model The store to connect to the redux devtools. - */ -export function exposeToReduxDevtools(model: IAnyStateTreeNode): void { - exposeToReduxDevtoolsAsync(model).catch((err) => { - console.error('Could not connect to Redux devtools', err); - }); -} - /** * Sends a message to the main process to reload all services when * `build/watch.js` sends a reload event on bundle write. diff --git a/packages/renderer/src/index.tsx b/packages/renderer/src/index.tsx index 1626bef..d900e50 100644 --- a/packages/renderer/src/index.tsx +++ b/packages/renderer/src/index.tsx @@ -26,14 +26,17 @@ import CssBaseline from '@mui/material/CssBaseline'; import React from 'react'; import { render } from 'react-dom'; -import { App } from './components/App'; -import { StoreProvider } from './components/StoreProvider'; -import { ThemeProvider } from './components/ThemeProvider'; +import App from './components/App'; +import StoreProvider from './components/StoreProvider'; +import ThemeProvider from './components/ThemeProvider'; import { exposeToReduxDevtools, hotReloadServices } from './devTools'; import { createAndConnectRendererStore } from './stores/RendererStore'; +import { getLogger } from './utils/log'; const isDevelopment = import.meta.env.MODE === 'development'; +const log = getLogger('index'); + if (isDevelopment) { hotReloadServices(); document.title = `[dev] ${document.title}`; @@ -42,7 +45,9 @@ if (isDevelopment) { const store = createAndConnectRendererStore(window.sophieRenderer); if (isDevelopment) { - exposeToReduxDevtools(store); + exposeToReduxDevtools(store).catch((err) => { + log.error('Cannot initialize redux devtools', err); + }); } function Root(): JSX.Element { diff --git a/packages/renderer/src/stores/RendererEnv.ts b/packages/renderer/src/stores/RendererEnv.ts index d687738..f0a5a51 100644 --- a/packages/renderer/src/stores/RendererEnv.ts +++ b/packages/renderer/src/stores/RendererEnv.ts @@ -18,10 +18,10 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { getEnv as getAnyEnv, IAnyStateTreeNode } from 'mobx-state-tree'; import type { Action } from '@sophie/shared'; +import { getEnv as getAnyEnv, IAnyStateTreeNode } from 'mobx-state-tree'; -export interface RendererEnv { +export default interface RendererEnv { dispatchMainAction(action: Action): void; } diff --git a/packages/renderer/src/stores/RendererStore.ts b/packages/renderer/src/stores/RendererStore.ts index 037b212..e684759 100644 --- a/packages/renderer/src/stores/RendererStore.ts +++ b/packages/renderer/src/stores/RendererStore.ts @@ -18,20 +18,25 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { - applySnapshot, - applyPatch, - Instance, - types, -} from 'mobx-state-tree'; import { BrowserViewBounds, sharedStore, SophieRenderer, ThemeSource, } from '@sophie/shared'; +import { + applySnapshot, + applyPatch, + Instance, + types, +} from 'mobx-state-tree'; + +import { getLogger } from '../utils/log'; + +import type RendererEnv from './RendererEnv'; +import { getEnv } from './RendererEnv'; -import { getEnv, RendererEnv } from './RendererEnv'; +const log = getLogger('RendererStore'); export const rendererStore = types.model('RendererStore', { shared: types.optional(sharedStore, {}), @@ -81,7 +86,7 @@ export function createAndConnectRendererStore(ipc: SophieRenderer): RendererStor applyPatch(store.shared, patch); }, }).catch((err) => { - console.error('Failed to connect to shared store', err); + log.error('Failed to connect to shared store', err); }); return store; diff --git a/packages/renderer/src/utils/log.ts b/packages/renderer/src/utils/log.ts new file mode 100644 index 0000000..c17fc2a --- /dev/null +++ b/packages/renderer/src/utils/log.ts @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2022 Kristóf Marussy + * + * This file is part of Sophie. + * + * Sophie is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import loglevel, { Logger } from 'loglevel'; +import prefix from 'loglevel-plugin-prefix'; + +if (import.meta.env?.DEV) { + loglevel.setLevel('debug'); +} else { + // No devtools in production, so there's not point to log anything. + loglevel.disableAll(); +} + +prefix.reg(loglevel); +prefix.apply(loglevel, { + format(level, name, timestamp) { + const timeStr = timestamp.toString(); + const nameStr = typeof name === 'undefined' ? '' : ` ${name}`; + return `[${timeStr}] ${level}${nameStr}:`; + }, +}); + +export function getLogger(loggerName: string): Logger { + return loglevel.getLogger(loggerName); +} + +export function silenceLogger(): void { + loglevel.disableAll(); + const loggers = loglevel.getLoggers(); + Object.keys(loggers).forEach((loggerName) => { + loggers[loggerName].disableAll(); + }); +} diff --git a/packages/renderer/tsconfig.json b/packages/renderer/tsconfig.json index 8746462..14c3e0c 100644 --- a/packages/renderer/tsconfig.json +++ b/packages/renderer/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../tsconfig.json", + "extends": "../../config/tsconfig.base.json", "compilerOptions": { "noEmit": true, "jsx": "react", @@ -14,12 +14,14 @@ }, "references": [ { - "path": "../shared" + "path": "../shared/tsconfig.build.json" } ], "include": [ "src/**/*.ts", "src/**/*.tsx", - "types/**/*.d.ts" + "types/**/*.d.ts", + ".eslintrc.cjs", + "vite.config.js" ] } diff --git a/packages/renderer/vite.config.js b/packages/renderer/vite.config.js index bcd1975..6440ead 100644 --- a/packages/renderer/vite.config.js +++ b/packages/renderer/vite.config.js @@ -3,10 +3,11 @@ import { builtinModules } from 'module'; import { join } from 'path'; + import react from '@vitejs/plugin-react'; import { banner, chrome } from '../../config/buildConstants.js'; -import { fileURLToDirname } from '../../config/utils.js'; +import fileURLToDirname from '../../config/fileURLToDirname.js'; const thisDir = fileURLToDirname(import.meta.url); @@ -45,7 +46,7 @@ export default { preserveSymlinks: true, }, optimizeDeps: { - link: [ + exclude: [ '@sophie/shared', ], }, diff --git a/packages/service-inject/.eslintrc.cjs b/packages/service-inject/.eslintrc.cjs new file mode 100644 index 0000000..6ae3faf --- /dev/null +++ b/packages/service-inject/.eslintrc.cjs @@ -0,0 +1,6 @@ +module.exports = { + env: { + node: false, + browser: true, + }, +}; diff --git a/packages/service-inject/esbuild.config.js b/packages/service-inject/esbuild.config.js index 2169c8e..d0b04bb 100644 --- a/packages/service-inject/esbuild.config.js +++ b/packages/service-inject/esbuild.config.js @@ -1,8 +1,8 @@ import { chrome } from '../../config/buildConstants.js'; -import { getConfig } from '../../config/esbuildConfig.js'; -import { fileURLToDirname } from '../../config/utils.js'; +import fileURLToDirname from '../../config/fileURLToDirname.js'; +import getEsbuildConfig from '../../config/getEsbuildConfig.js'; -export default getConfig({ +export default getEsbuildConfig({ absWorkingDir: fileURLToDirname(import.meta.url), entryPoints: [ 'src/index.ts', diff --git a/packages/service-inject/package.json b/packages/service-inject/package.json index 7c496fd..c045500 100644 --- a/packages/service-inject/package.json +++ b/packages/service-inject/package.json @@ -6,13 +6,9 @@ "type": "module", "types": "dist-types/index.d.ts", "scripts": { - "typecheck": "tsc" + "typecheck:workspace": "yarn g:typecheck" }, "dependencies": { "@sophie/service-shared": "workspace:*" - }, - "devDependencies": { - "rimraf": "^3.0.2", - "typescript": "^4.5.4" } } diff --git a/packages/service-inject/tsconfig.json b/packages/service-inject/tsconfig.json index 638690b..cc61d63 100644 --- a/packages/service-inject/tsconfig.json +++ b/packages/service-inject/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../tsconfig.json", + "extends": "../../config/tsconfig.base.json", "compilerOptions": { "noEmit": true, "lib": [ @@ -10,10 +10,11 @@ }, "references": [ { - "path": "../service-shared" + "path": "../service-shared/tsconfig.build.json" } ], "include": [ - "src/**/*.ts" + "src/**/*.ts", + "esbuild.config.js" ] } diff --git a/packages/service-preload/.eslintrc.cjs b/packages/service-preload/.eslintrc.cjs new file mode 100644 index 0000000..02fab21 --- /dev/null +++ b/packages/service-preload/.eslintrc.cjs @@ -0,0 +1,6 @@ +module.exports = { + env: { + node: true, + browser: true, + }, +}; diff --git a/packages/service-preload/esbuild.config.js b/packages/service-preload/esbuild.config.js index b73a071..66f5e84 100644 --- a/packages/service-preload/esbuild.config.js +++ b/packages/service-preload/esbuild.config.js @@ -1,8 +1,8 @@ import { chrome } from '../../config/buildConstants.js'; -import { getConfig } from '../../config/esbuildConfig.js'; -import { fileURLToDirname } from '../../config/utils.js'; +import fileURLToDirname from '../../config/fileURLToDirname.js'; +import getEsbuildConfig from '../../config/getEsbuildConfig.js'; -export default getConfig({ +export default getEsbuildConfig({ absWorkingDir: fileURLToDirname(import.meta.url), entryPoints: [ 'src/index.ts', diff --git a/packages/service-preload/package.json b/packages/service-preload/package.json index 26215a3..14717f8 100644 --- a/packages/service-preload/package.json +++ b/packages/service-preload/package.json @@ -5,14 +5,12 @@ "type": "module", "types": "dist-types/index.d.ts", "scripts": { - "typecheck": "tsc" + "typecheck:workspace": "yarn g:typecheck" }, "dependencies": { "@sophie/service-shared": "workspace:*", - "electron": "16.0.6" - }, - "devDependencies": { - "rimraf": "^3.0.2", - "typescript": "^4.5.4" + "electron": "16.0.6", + "loglevel": "^1.8.0", + "loglevel-plugin-prefix": "^0.8.4" } } diff --git a/packages/service-preload/src/index.ts b/packages/service-preload/src/index.ts index d1ea13c..2bbfefd 100644 --- a/packages/service-preload/src/index.ts +++ b/packages/service-preload/src/index.ts @@ -18,8 +18,12 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { ipcRenderer, webFrame } from 'electron'; import { ServiceToMainIpcMessage, webSource } from '@sophie/service-shared'; +import { ipcRenderer, webFrame } from 'electron'; + +import { getLogger } from './utils/log'; + +const log = getLogger('index'); if (webFrame.parent === null) { // Inject CSS to simulate `browserView.setBackgroundColor`. @@ -49,14 +53,14 @@ if (webFrame.parent === null) { * @see https://www.electronjs.org/docs/latest/api/web-contents#contentsexecutejavascriptinisolatedworldworldid-scripts-usergesture */ async function fetchAndExecuteInjectScript(): Promise { - const apiExposedResponse = await ipcRenderer.invoke( + const apiExposedResponse: unknown = await ipcRenderer.invoke( ServiceToMainIpcMessage.ApiExposedInMainWorld, ); const injectSource = webSource.parse(apiExposedResponse); // Isolated world 0 is the main world. - return webFrame.executeJavaScriptInIsolatedWorld(0, [injectSource]); + await webFrame.executeJavaScriptInIsolatedWorld(0, [injectSource]); } fetchAndExecuteInjectScript().catch((err) => { - console.log('Failed to fetch inject source:', err); + log.error('Failed to fetch inject source:', err); }); diff --git a/packages/service-preload/src/utils/log.ts b/packages/service-preload/src/utils/log.ts new file mode 100644 index 0000000..0c35319 --- /dev/null +++ b/packages/service-preload/src/utils/log.ts @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2022 Kristóf Marussy + * + * This file is part of Sophie. + * + * Sophie is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import loglevel, { Logger } from 'loglevel'; +import prefix from 'loglevel-plugin-prefix'; + +if (import.meta.env?.DEV) { + loglevel.setLevel('debug'); +} else { + loglevel.setLevel('info'); +} + +prefix.reg(loglevel); +prefix.apply(loglevel, { + format(level, name, timestamp) { + const timeStr = timestamp.toString(); + const nameStr = typeof name === 'undefined' ? '' : ` ${name}`; + return `[${timeStr}] ${level}${nameStr}:`; + }, +}); + +export function getLogger(loggerName: string): Logger { + return loglevel.getLogger(loggerName); +} + +export function silenceLogger(): void { + loglevel.disableAll(); + const loggers = loglevel.getLoggers(); + Object.keys(loggers).forEach((loggerName) => { + loggers[loggerName].disableAll(); + }); +} diff --git a/packages/service-preload/tsconfig.json b/packages/service-preload/tsconfig.json index 638690b..0372dde 100644 --- a/packages/service-preload/tsconfig.json +++ b/packages/service-preload/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../tsconfig.json", + "extends": "../../config/tsconfig.base.json", "compilerOptions": { "noEmit": true, "lib": [ @@ -10,10 +10,12 @@ }, "references": [ { - "path": "../service-shared" + "path": "../service-shared/tsconfig.build.json" } ], "include": [ - "src/**/*.ts" + "src/**/*.ts", + "types/**/*.ts", + "esbuild.config.js" ] } diff --git a/packages/service-preload/types/importMeta.d.ts b/packages/service-preload/types/importMeta.d.ts new file mode 100644 index 0000000..9b73170 --- /dev/null +++ b/packages/service-preload/types/importMeta.d.ts @@ -0,0 +1,7 @@ +interface ImportMeta { + env: { + DEV: boolean; + MODE: string; + PROD: boolean; + } +} diff --git a/packages/service-shared/.eslintrc.cjs b/packages/service-shared/.eslintrc.cjs new file mode 100644 index 0000000..71d6ec4 --- /dev/null +++ b/packages/service-shared/.eslintrc.cjs @@ -0,0 +1,7 @@ +module.exports = { + env: { + // We must run in both node and browser, so we can't depend on either of them. + node: false, + browser: false, + }, +}; diff --git a/packages/service-shared/esbuild.config.js b/packages/service-shared/esbuild.config.js index 08941a4..ccee72c 100644 --- a/packages/service-shared/esbuild.config.js +++ b/packages/service-shared/esbuild.config.js @@ -1,8 +1,8 @@ import { chrome, node } from '../../config/buildConstants.js'; -import { getConfig } from '../../config/esbuildConfig.js'; -import { fileURLToDirname } from '../../config/utils.js'; +import fileURLToDirname from '../../config/fileURLToDirname.js'; +import getEsbuildConfig from '../../config/getEsbuildConfig.js'; -export default getConfig({ +export default getEsbuildConfig({ absWorkingDir: fileURLToDirname(import.meta.url), entryPoints: [ 'src/index.ts', diff --git a/packages/service-shared/package.json b/packages/service-shared/package.json index 9d75fc8..5338c8c 100644 --- a/packages/service-shared/package.json +++ b/packages/service-shared/package.json @@ -7,13 +7,10 @@ "exports": "./dist/index.mjs", "types": "dist/index.d.ts", "scripts": { - "typecheck": "tsc" + "typecheck:workspace": "yarn g:typecheck", + "types": "yarn g:types" }, "dependencies": { "zod": "^3.11.6" - }, - "devDependencies": { - "rimraf": "^3.0.2", - "typescript": "^4.5.4" } } diff --git a/packages/service-shared/src/index.ts b/packages/service-shared/src/index.ts index 564ebe8..e111347 100644 --- a/packages/service-shared/src/index.ts +++ b/packages/service-shared/src/index.ts @@ -18,7 +18,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -export { ServiceToMainIpcMessage } from './ipc'; +export { MainToServiceIpcMessage, ServiceToMainIpcMessage } from './ipc'; export type { UnreadCount, diff --git a/packages/service-shared/src/ipc.ts b/packages/service-shared/src/ipc.ts index 4f991c5..c0dab11 100644 --- a/packages/service-shared/src/ipc.ts +++ b/packages/service-shared/src/ipc.ts @@ -18,6 +18,9 @@ * SPDX-License-Identifier: AGPL-3.0-only */ +export enum MainToServiceIpcMessage { +} + export enum ServiceToMainIpcMessage { ApiExposedInMainWorld = 'sophie-service-to-main:api-exposed-in-main-world', SetUnreadCount = 'sophie-service-to-main:set-unread-count', diff --git a/packages/service-shared/tsconfig.build.json b/packages/service-shared/tsconfig.build.json new file mode 100644 index 0000000..9a0c835 --- /dev/null +++ b/packages/service-shared/tsconfig.build.json @@ -0,0 +1,12 @@ +{ + "extends": "../../config/tsconfig.base.json", + "compilerOptions": { + "composite": true, + "declarationDir": "dist", + "emitDeclarationOnly": true, + "rootDir": "src" + }, + "include": [ + "src/**/*.ts" + ] +} diff --git a/packages/service-shared/tsconfig.json b/packages/service-shared/tsconfig.json index ff5a29b..79889d2 100644 --- a/packages/service-shared/tsconfig.json +++ b/packages/service-shared/tsconfig.json @@ -1,12 +1,14 @@ { - "extends": "../../tsconfig.json", + "extends": "./tsconfig.build.json", "compilerOptions": { - "composite": true, - "declarationDir": "dist", - "emitDeclarationOnly": true, - "rootDir": "src" + "composite": false, + "emitDeclarationOnly": false, + "declarationDir": null, + "noEmit": true, + "rootDir": null }, "include": [ - "src/**/*.ts" + "src/**/*.ts", + "esbuild.config.js" ] } diff --git a/packages/shared/.eslintrc.cjs b/packages/shared/.eslintrc.cjs new file mode 100644 index 0000000..71d6ec4 --- /dev/null +++ b/packages/shared/.eslintrc.cjs @@ -0,0 +1,7 @@ +module.exports = { + env: { + // We must run in both node and browser, so we can't depend on either of them. + node: false, + browser: false, + }, +}; diff --git a/packages/shared/esbuild.config.js b/packages/shared/esbuild.config.js index 66d6658..78249ab 100644 --- a/packages/shared/esbuild.config.js +++ b/packages/shared/esbuild.config.js @@ -1,8 +1,8 @@ import { chrome, node } from '../../config/buildConstants.js'; -import { getConfig } from '../../config/esbuildConfig.js'; -import { fileURLToDirname } from '../../config/utils.js'; +import fileURLToDirname from '../../config/fileURLToDirname.js'; +import getEsbuildConfig from '../../config/getEsbuildConfig.js'; -export default getConfig({ +export default getEsbuildConfig({ absWorkingDir: fileURLToDirname(import.meta.url), entryPoints: [ 'src/index.ts', diff --git a/packages/shared/package.json b/packages/shared/package.json index 0c06643..d77261d 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -7,15 +7,12 @@ "exports": "./dist/index.mjs", "types": "dist/index.d.ts", "scripts": { - "typecheck": "tsc" + "typecheck:workspace": "yarn g:typecheck", + "types": "yarn g:types" }, "dependencies": { "mobx": "^6.3.12", "mobx-state-tree": "^5.1.0", "zod": "^3.11.6" - }, - "devDependencies": { - "rimraf": "^3.0.2", - "typescript": "^4.5.4" } } diff --git a/packages/shared/src/contextBridge/SophieRenderer.ts b/packages/shared/src/contextBridge/SophieRenderer.ts index fc43b6e..9858aa9 100644 --- a/packages/shared/src/contextBridge/SophieRenderer.ts +++ b/packages/shared/src/contextBridge/SophieRenderer.ts @@ -18,12 +18,11 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { SharedStoreListener } from '../stores/SharedStore'; - import { Action } from '../schemas'; +import { SharedStoreListener } from '../stores/SharedStore'; export interface SophieRenderer { - onSharedStoreChange(listener: SharedStoreListener): Promise; + onSharedStoreChange(this: void, listener: SharedStoreListener): Promise; - dispatchAction(action: Action): void; + dispatchAction(this: void, action: Action): void; } diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index 2f7146c..9828ec4 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -20,10 +20,7 @@ export type { SophieRenderer } from './contextBridge/SophieRenderer'; -export { - MainToRendererIpcMessage, - RendererToMainIpcMessage, -} from './ipc'; +export { MainToRendererIpcMessage, RendererToMainIpcMessage } from './ipc'; export type { Action, diff --git a/packages/shared/tsconfig.build.json b/packages/shared/tsconfig.build.json new file mode 100644 index 0000000..9a0c835 --- /dev/null +++ b/packages/shared/tsconfig.build.json @@ -0,0 +1,12 @@ +{ + "extends": "../../config/tsconfig.base.json", + "compilerOptions": { + "composite": true, + "declarationDir": "dist", + "emitDeclarationOnly": true, + "rootDir": "src" + }, + "include": [ + "src/**/*.ts" + ] +} diff --git a/packages/shared/tsconfig.json b/packages/shared/tsconfig.json index ff5a29b..79889d2 100644 --- a/packages/shared/tsconfig.json +++ b/packages/shared/tsconfig.json @@ -1,12 +1,14 @@ { - "extends": "../../tsconfig.json", + "extends": "./tsconfig.build.json", "compilerOptions": { - "composite": true, - "declarationDir": "dist", - "emitDeclarationOnly": true, - "rootDir": "src" + "composite": false, + "emitDeclarationOnly": false, + "declarationDir": null, + "noEmit": true, + "rootDir": null }, "include": [ - "src/**/*.ts" + "src/**/*.ts", + "esbuild.config.js" ] } diff --git a/scripts/build.js b/scripts/build.js index ef2b998..1236a6c 100644 --- a/scripts/build.js +++ b/scripts/build.js @@ -1,8 +1,9 @@ -import { build as esbuildBuild } from 'esbuild'; import { join } from 'path'; + +import { build as esbuildBuild } from 'esbuild'; import { build as viteBuild } from 'vite'; -import { fileURLToDirname } from '../config/utils.js'; +import fileURLToDirname from '../config/fileURLToDirname.js'; const thisDir = fileURLToDirname(import.meta.url); @@ -12,6 +13,7 @@ const thisDir = fileURLToDirname(import.meta.url); */ async function buildPackageEsbuild(packageName) { /** @type {{ default: import('esbuild').BuildOptions }} */ + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- Read untyped config file. const { default: config } = await import(`../packages/${packageName}/esbuild.config.js`); return esbuildBuild(config); } diff --git a/scripts/update-electron-vendors.js b/scripts/update-electron-vendors.js index 70d3afc..91cdb61 100644 --- a/scripts/update-electron-vendors.js +++ b/scripts/update-electron-vendors.js @@ -1,9 +1,10 @@ import { execSync } from 'child_process'; -import electronPath from 'electron'; import { writeFile } from 'fs/promises'; import { join } from 'path'; -import { fileURLToDirname } from '../config/utils.js'; +import electronPath from 'electron'; + +import fileURLToDirname from '../config/fileURLToDirname.js'; const thisDir = fileURLToDirname(import.meta.url); @@ -15,11 +16,12 @@ const thisDir = fileURLToDirname(import.meta.url); * @returns {NodeJS.ProcessVersions} */ function getVendors() { - const output = execSync(`${electronPath} -p "JSON.stringify(process.versions)"`, { - env: { 'ELECTRON_RUN_AS_NODE': '1' }, + const output = execSync(`${electronPath.toString()} -p "JSON.stringify(process.versions)"`, { + env: { ELECTRON_RUN_AS_NODE: '1' }, encoding: 'utf-8', }); + // eslint-disable-next-line @typescript-eslint/no-unsafe-return -- Read untyped output. return JSON.parse(output); } @@ -39,10 +41,10 @@ function updateVendors() { return Promise.all([ writeFile( join(thisDir, '../.electron-vendors.cache.json'), - JSON.stringify({ + `${JSON.stringify({ chrome: chromeMajorVersion, node: nodeMajorVersion, - }, null, 2) + '\n', + }, null, 2)}\n`, ), writeFile(browserslistrcPath, `Chrome ${chromeMajorVersion}\n`, 'utf8'), diff --git a/scripts/watch.js b/scripts/watch.js index a61d3c8..1345a0f 100644 --- a/scripts/watch.js +++ b/scripts/watch.js @@ -1,11 +1,12 @@ -import { build as esbuildBuild } from 'esbuild'; import { spawn } from 'child_process'; +import { join } from 'path'; + import { watch } from 'chokidar'; import electronPath from 'electron'; -import { join } from 'path'; +import { build as esbuildBuild } from 'esbuild'; import { createServer } from 'vite'; -import { fileURLToDirname } from '../config/utils.js'; +import fileURLToDirname from '../config/fileURLToDirname.js'; /** @type {string} */ const thisDir = fileURLToDirname(import.meta.url); @@ -35,6 +36,7 @@ const stderrIgnorePatterns = [ */ async function setupEsbuildWatcher(packageName, extraPaths, callback) { /** @type {{ default: import('esbuild').BuildOptions }} */ + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- Read untyped config file. const { default: config } = await import(`../packages/${packageName}/esbuild.config.js`); config.logLevel = 'info'; config.incremental = true; @@ -45,7 +47,7 @@ async function setupEsbuildWatcher(packageName, extraPaths, callback) { ...(extraPaths || []), ]; const watcher = watch(paths, { - ignored: /(^|[\/\\])\.|__(tests|mocks)__|\.(spec|test)\.[jt]sx?$/, + ignored: /(^|[/\\])\.|__(tests|mocks)__|\.(spec|test)\.[jt]sx?$/, ignoreInitial: true, persistent: true, }); @@ -59,13 +61,26 @@ async function setupEsbuildWatcher(packageName, extraPaths, callback) { callback(); } }).catch((err) => { - const errCount = err.errors.length; + if (typeof err === 'object' && 'errors' in err) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- We just checked. + const { errors } = err; + if (Array.isArray(errors)) { + const errCount = errors.length; + console.error( + '\ud83d\udd25', + errCount, + errCount > 1 ? 'errors' : 'error', + 'while rebuilding package', + packageName, + ); + return; + } + } console.error( '\ud83d\udd25', - errCount, - errCount > 1 ? 'errors' : 'error', - 'while rebuilding package', + 'error while rebuilding package', packageName, + err, ); }); }); @@ -121,13 +136,14 @@ function setupServicePackageWatcher(packageName, sendEvent) { */ function setupMainPackageWatcher(viteDevServer) { // Write a value to an environment variable to pass it to the main process. - const protocol = `http${viteDevServer.config.server.https ? 's' : ''}:`; - const host = viteDevServer.config.server.host || 'localhost'; - const port = viteDevServer.config.server.port; - const path = '/'; - process.env.VITE_DEV_SERVER_URL = `${protocol}//${host}:${port}${path}`; - - /** @type {import('child_process').ChildProcessByStdio | null} */ + const { config: { server: { port, https, host } } } = viteDevServer; + const protocol = `http${https ? 's' : ''}:`; + const hostOrDefault = typeof host === 'string' ? host : 'localhost'; + const portOrDefault = port || 3000; + process.env.VITE_DEV_SERVER_URL = `${protocol}//${hostOrDefault}:${portOrDefault}/`; + + /** @type {import('child_process').ChildProcessByStdio + | null} */ let spawnProcess = null; return setupEsbuildWatcher( @@ -146,8 +162,8 @@ function setupMainPackageWatcher(viteDevServer) { stdio: ['inherit', 'inherit', 'pipe'], }); - spawnProcess.stderr.on('data', (data) => { - const stderrString = data.toString('utf-8').trimRight(); + spawnProcess.stderr.on('data', (/** @type {Buffer} */ data) => { + const stderrString = data.toString('utf-8').trimEnd(); if (!stderrIgnorePatterns.some((r) => r.test(stderrString))) { console.error(stderrString); } @@ -193,7 +209,7 @@ async function setupDevEnvironment() { } console.log('\ud83c\udf80 Sophie is starting up'); - return setupMainPackageWatcher(viteDevServer); + await setupMainPackageWatcher(viteDevServer); } setupDevEnvironment().catch((err) => { diff --git a/tsconfig.json b/tsconfig.json index 255f334..627bed2 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,19 +1,14 @@ { + "extends": "./config/tsconfig.base.json", "compilerOptions": { - "module": "esnext", - "target": "esnext", - "moduleResolution": "node", - "esModuleInterop": true, - "allowSyntheticDefaultImports": true, - "strict": true, - "noImplicitOverride": true, - "noImplicitReturns": true, - "exactOptionalPropertyTypes": true, - "isolatedModules": true, - "skipLibCheck": true, - "checkJs": true, - "lib": [ - "esnext" - ] - } + "noEmit": true + }, + "include": [ + "config/**/*.js", + "config/**/*.cjs", + "scripts/**/*.js", + ".electron-builder.config.cjs", + ".eslintrc.cjs", + "jest.config.js" + ] } diff --git a/yarn.lock b/yarn.lock index 86fe235..55deb08 100644 --- a/yarn.lock +++ b/yarn.lock @@ -420,12 +420,22 @@ __metadata: languageName: node linkType: hard -"@babel/runtime@npm:^7.13.10, @babel/runtime@npm:^7.16.3, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.7.2, @babel/runtime@npm:^7.8.7": - version: 7.16.5 - resolution: "@babel/runtime@npm:7.16.5" +"@babel/runtime-corejs3@npm:^7.10.2": + version: 7.16.7 + resolution: "@babel/runtime-corejs3@npm:7.16.7" dependencies: + core-js-pure: ^3.19.0 regenerator-runtime: ^0.13.4 - checksum: b96e67280efe581c6147b4fe984dfe08a8fbea048934a092f3cbf4dcf61725f6b221cb0c879b6e6e98671f83a104c9e8cfbd24c683e5ebcc886a731aa8984ad0 + checksum: c40cabaead64e4843a24b064cdeeabf87780bf06567146234eca94a64acb760225a9f31151eec1913c91f6f4c86afad325c5fec9262a5434e8b0a3ea905d51cf + languageName: node + linkType: hard + +"@babel/runtime@npm:^7.10.2, @babel/runtime@npm:^7.13.10, @babel/runtime@npm:^7.16.3, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.7.2, @babel/runtime@npm:^7.8.7": + version: 7.16.7 + resolution: "@babel/runtime@npm:7.16.7" + dependencies: + regenerator-runtime: ^0.13.4 + checksum: 47912f0aaacd1cab2e2552aaf3e6eaffbcaf2d5ac9b07a89a12ac0d42029cb92c070b0d16f825e4277c4a34677c54d8ffe85e1f7c6feb57de58f700eec67ce2f languageName: node linkType: hard @@ -1266,8 +1276,6 @@ __metadata: loglevel: ^1.8.0 mobx: ^6.3.12 mobx-state-tree: ^5.1.0 - rimraf: ^3.0.2 - typescript: ^4.5.4 languageName: unknown linkType: soft @@ -1281,11 +1289,13 @@ __metadata: "@mui/icons-material": ^5.2.5 "@mui/material": ^5.2.7 "@sophie/shared": "workspace:*" - "@types/lodash": ^4.14.178 + "@types/lodash-es": ^4.14.178 "@types/react": ^17.0.38 "@types/react-dom": ^17.0.11 "@vitejs/plugin-react": ^1.1.4 - lodash: ^4.17.21 + lodash-es: ^4.17.21 + loglevel: ^1.8.0 + loglevel-plugin-prefix: ^0.8.4 mobx: ^6.3.12 mobx-react-lite: ^3.2.3 mobx-state-tree: ^5.1.0 @@ -1293,8 +1303,6 @@ __metadata: react: ^17.0.2 react-dom: ^17.0.2 remotedev: ^0.2.9 - rimraf: ^3.0.2 - typescript: ^4.5.4 vite: ^2.7.10 languageName: unknown linkType: soft @@ -1304,8 +1312,6 @@ __metadata: resolution: "@sophie/service-inject@workspace:packages/service-inject" dependencies: "@sophie/service-shared": "workspace:*" - rimraf: ^3.0.2 - typescript: ^4.5.4 languageName: unknown linkType: soft @@ -1315,8 +1321,8 @@ __metadata: dependencies: "@sophie/service-shared": "workspace:*" electron: 16.0.6 - rimraf: ^3.0.2 - typescript: ^4.5.4 + loglevel: ^1.8.0 + loglevel-plugin-prefix: ^0.8.4 languageName: unknown linkType: soft @@ -1324,8 +1330,6 @@ __metadata: version: 0.0.0-use.local resolution: "@sophie/service-shared@workspace:packages/service-shared" dependencies: - rimraf: ^3.0.2 - typescript: ^4.5.4 zod: ^3.11.6 languageName: unknown linkType: soft @@ -1336,8 +1340,6 @@ __metadata: dependencies: mobx: ^6.3.12 mobx-state-tree: ^5.1.0 - rimraf: ^3.0.2 - typescript: ^4.5.4 zod: ^3.11.6 languageName: unknown linkType: soft @@ -1508,7 +1510,7 @@ __metadata: languageName: node linkType: hard -"@types/lodash-es@npm:^4.17.5": +"@types/lodash-es@npm:^4.14.178, @types/lodash-es@npm:^4.17.5": version: 4.17.5 resolution: "@types/lodash-es@npm:4.17.5" dependencies: @@ -1517,7 +1519,7 @@ __metadata: languageName: node linkType: hard -"@types/lodash@npm:*, @types/lodash@npm:^4.14.178": +"@types/lodash@npm:*": version: 4.14.178 resolution: "@types/lodash@npm:4.14.178" checksum: a69a04a60bfc5257c3130a554b4efa0c383f0141b7b3db8ab7cf07ad2a46ea085fce66d0242da41da7e5647b133d5dfb2c15add9cbed8d7fef955e4a1e5b3128 @@ -2058,6 +2060,16 @@ __metadata: languageName: node linkType: hard +"aria-query@npm:^4.2.2": + version: 4.2.2 + resolution: "aria-query@npm:4.2.2" + dependencies: + "@babel/runtime": ^7.10.2 + "@babel/runtime-corejs3": ^7.10.2 + checksum: 38401a9a400f26f3dcc24b84997461a16b32869a9893d323602bed8da40a8bcc0243b8d2880e942249a1496cea7a7de769e93d21c0baa439f01e1ee936fed665 + languageName: node + linkType: hard + "array-includes@npm:^3.1.3, array-includes@npm:^3.1.4": version: 3.1.4 resolution: "array-includes@npm:3.1.4" @@ -2125,6 +2137,13 @@ __metadata: languageName: node linkType: hard +"ast-types-flow@npm:^0.0.7": + version: 0.0.7 + resolution: "ast-types-flow@npm:0.0.7" + checksum: a26dcc2182ffee111cad7c471759b0bda22d3b7ebacf27c348b22c55f16896b18ab0a4d03b85b4020dce7f3e634b8f00b593888f622915096ea1927fa51866c4 + languageName: node + linkType: hard + "astral-regex@npm:^2.0.0": version: 2.0.0 resolution: "astral-regex@npm:2.0.0" @@ -2167,6 +2186,20 @@ __metadata: languageName: node linkType: hard +"axe-core@npm:^4.3.5": + version: 4.3.5 + resolution: "axe-core@npm:4.3.5" + checksum: 973c6a80f0aaa663820b209d4202de7a0c240a2dea2f3cff168b09c0f221b27179b1f0988f00ad11ed63cbc50535920f8ca779de1c60dc82090ab2d275f71fdd + languageName: node + linkType: hard + +"axobject-query@npm:^2.2.0": + version: 2.2.0 + resolution: "axobject-query@npm:2.2.0" + checksum: 96b8c7d807ca525f41ad9b286186e2089b561ba63a6d36c3e7d73dc08150714660995c7ad19cda05784458446a0793b45246db45894631e13853f48c1aa3117f + languageName: node + linkType: hard + "babel-jest@npm:^27.4.6": version: 27.4.6 resolution: "babel-jest@npm:27.4.6" @@ -2855,6 +2888,13 @@ __metadata: languageName: node linkType: hard +"core-js-pure@npm:^3.19.0": + version: 3.20.2 + resolution: "core-js-pure@npm:3.20.2" + checksum: d6b3f6782e3f2fc27eb2335917d5c5d0e7621e424c25da67429e9b48b7708b76fdc4a178b245421eeb8342c0ea9b0ca636ece002db3d0e68246a9d395d461ca7 + languageName: node + linkType: hard + "core-util-is@npm:1.0.2": version: 1.0.2 resolution: "core-util-is@npm:1.0.2" @@ -2958,6 +2998,13 @@ __metadata: languageName: node linkType: hard +"damerau-levenshtein@npm:^1.0.7": + version: 1.0.7 + resolution: "damerau-levenshtein@npm:1.0.7" + checksum: ec8161cb381523e0db9b5c9b64863736da3197808b6fdc4a3a2ca764c0b4357e9232a4c5592220fb18755a91240b8fee7b13ab1b269fbbdc5f68c36f0053aceb + languageName: node + linkType: hard + "data-urls@npm:^2.0.0": version: 2.0.0 resolution: "data-urls@npm:2.0.0" @@ -3373,6 +3420,13 @@ __metadata: languageName: node linkType: hard +"emoji-regex@npm:^9.2.2": + version: 9.2.2 + resolution: "emoji-regex@npm:9.2.2" + checksum: 8487182da74aabd810ac6d6f1994111dfc0e331b01271ae01ec1eb0ad7b5ecc2bbbbd2f053c05cb55a1ac30449527d819bbfbf0e3de1023db308cbcb47f86601 + languageName: node + linkType: hard + "encodeurl@npm:^1.0.2": version: 1.0.2 resolution: "encodeurl@npm:1.0.2" @@ -3929,6 +3983,35 @@ __metadata: languageName: node linkType: hard +"eslint-config-airbnb@npm:^19.0.4": + version: 19.0.4 + resolution: "eslint-config-airbnb@npm:19.0.4" + dependencies: + eslint-config-airbnb-base: ^15.0.0 + object.assign: ^4.1.2 + object.entries: ^1.1.5 + peerDependencies: + eslint: ^7.32.0 || ^8.2.0 + eslint-plugin-import: ^2.25.3 + eslint-plugin-jsx-a11y: ^6.5.1 + eslint-plugin-react: ^7.28.0 + eslint-plugin-react-hooks: ^4.3.0 + checksum: 253178689c3c80eef2567e3aaf0612e18973bc9cf51d9be36074b5dd58210e8b6942200a424bcccbb81ac884e41303479ab09f251a2a97addc2de61efdc9576c + languageName: node + linkType: hard + +"eslint-formatter-gitlab@npm:^3.0.0": + version: 3.0.0 + resolution: "eslint-formatter-gitlab@npm:3.0.0" + dependencies: + chalk: ^4.0.0 + js-yaml: ^4.0.0 + peerDependencies: + eslint: ^5 || ^6 || ^7 || ^8 + checksum: 7730a003e5e9b2bde7a8cc473d8b377ac198be33a9e0f1dbc7e5ddf489232dc683b8aedd2726c0539e0d2e73b9769b2fcaf089ad90b75fc8564a0b4a9511d041 + languageName: node + linkType: hard + "eslint-import-resolver-node@npm:^0.3.6": version: 0.3.6 resolution: "eslint-import-resolver-node@npm:0.3.6" @@ -3939,6 +4022,22 @@ __metadata: languageName: node linkType: hard +"eslint-import-resolver-typescript@npm:^2.5.0": + version: 2.5.0 + resolution: "eslint-import-resolver-typescript@npm:2.5.0" + dependencies: + debug: ^4.3.1 + glob: ^7.1.7 + is-glob: ^4.0.1 + resolve: ^1.20.0 + tsconfig-paths: ^3.9.0 + peerDependencies: + eslint: "*" + eslint-plugin-import: "*" + checksum: e507a0cb46a05f136b1416664c7cbe1b1178001417421ce5621f147e88c8973b5c9ee1554dbf0b79ae93f760d69f2796e1a880d562356a080e9e4ac1058206a3 + languageName: node + linkType: hard + "eslint-module-utils@npm:^2.7.2": version: 2.7.2 resolution: "eslint-module-utils@npm:2.7.2" @@ -3972,6 +4071,37 @@ __metadata: languageName: node linkType: hard +"eslint-plugin-jsx-a11y@npm:^6.5.1": + version: 6.5.1 + resolution: "eslint-plugin-jsx-a11y@npm:6.5.1" + dependencies: + "@babel/runtime": ^7.16.3 + aria-query: ^4.2.2 + array-includes: ^3.1.4 + ast-types-flow: ^0.0.7 + axe-core: ^4.3.5 + axobject-query: ^2.2.0 + damerau-levenshtein: ^1.0.7 + emoji-regex: ^9.2.2 + has: ^1.0.3 + jsx-ast-utils: ^3.2.1 + language-tags: ^1.0.5 + minimatch: ^3.0.4 + peerDependencies: + eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 + checksum: 311ab993ed982d0cc7cb0ba02fbc4b36c4a94e9434f31e97f13c4d67e8ecb8aec36baecfd759ff70498846e7e11d7a197eb04c39ad64934baf3354712fd0bc9d + languageName: node + linkType: hard + +"eslint-plugin-react-hooks@npm:^4.3.0": + version: 4.3.0 + resolution: "eslint-plugin-react-hooks@npm:4.3.0" + peerDependencies: + eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 + checksum: 0ba1566ba0780bbc75a5921f49188edf232db2085ab32c8d3889592f0db9d6fadc97fcf639775e0101dec6b5409ca3c803ec44213b90c8bacaf0bdf921871c2e + languageName: node + linkType: hard + "eslint-plugin-react@npm:^7.28.0": version: 7.28.0 resolution: "eslint-plugin-react@npm:7.28.0" @@ -4558,7 +4688,7 @@ __metadata: languageName: node linkType: hard -"glob@npm:^7.1.1, glob@npm:^7.1.2, glob@npm:^7.1.3, glob@npm:^7.1.4, glob@npm:^7.1.6": +"glob@npm:^7.1.1, glob@npm:^7.1.2, glob@npm:^7.1.3, glob@npm:^7.1.4, glob@npm:^7.1.6, glob@npm:^7.1.7": version: 7.2.0 resolution: "glob@npm:7.2.0" dependencies: @@ -5857,7 +5987,7 @@ __metadata: languageName: node linkType: hard -"js-yaml@npm:^4.1.0": +"js-yaml@npm:^4.0.0, js-yaml@npm:^4.1.0": version: 4.1.0 resolution: "js-yaml@npm:4.1.0" dependencies: @@ -6053,7 +6183,7 @@ __metadata: languageName: node linkType: hard -"jsx-ast-utils@npm:^2.4.1 || ^3.0.0": +"jsx-ast-utils@npm:^2.4.1 || ^3.0.0, jsx-ast-utils@npm:^3.2.1": version: 3.2.1 resolution: "jsx-ast-utils@npm:3.2.1" dependencies: @@ -6091,6 +6221,22 @@ __metadata: languageName: node linkType: hard +"language-subtag-registry@npm:~0.3.2": + version: 0.3.21 + resolution: "language-subtag-registry@npm:0.3.21" + checksum: 5f794525a5bfcefeea155a681af1c03365b60e115b688952a53c6e0b9532b09163f57f1fcb69d6150e0e805ec0350644a4cb35da98f4902562915be9f89572a1 + languageName: node + linkType: hard + +"language-tags@npm:^1.0.5": + version: 1.0.5 + resolution: "language-tags@npm:1.0.5" + dependencies: + language-subtag-registry: ~0.3.2 + checksum: c81b5d8b9f5f9cfd06ee71ada6ddfe1cf83044dd5eeefcd1e420ad491944da8957688db4a0a9bc562df4afdc2783425cbbdfd152c01d93179cf86888903123cf + languageName: node + linkType: hard + "latest-version@npm:^5.1.0": version: 5.1.0 resolution: "latest-version@npm:5.1.0" @@ -6190,7 +6336,7 @@ __metadata: languageName: node linkType: hard -"lodash@npm:^4.17.10, lodash@npm:^4.17.15, lodash@npm:^4.17.21, lodash@npm:^4.7.0": +"lodash@npm:^4.17.10, lodash@npm:^4.17.15, lodash@npm:^4.7.0": version: 4.17.21 resolution: "lodash@npm:4.17.21" checksum: eb835a2e51d381e561e508ce932ea50a8e5a68f4ebdd771ea240d3048244a8d13658acbd502cd4829768c56f2e16bdd4340b9ea141297d472517b83868e677f7 @@ -7789,9 +7935,15 @@ __metadata: electron-builder: ^22.14.12 esbuild: ^0.14.11 eslint: ^8.6.0 + eslint-config-airbnb: ^19.0.4 + eslint-config-airbnb-base: ^15.0.0 eslint-config-airbnb-typescript: ^16.1.0 + eslint-formatter-gitlab: ^3.0.0 + eslint-import-resolver-typescript: ^2.5.0 eslint-plugin-import: ^2.25.4 + eslint-plugin-jsx-a11y: ^6.5.1 eslint-plugin-react: ^7.28.0 + eslint-plugin-react-hooks: ^4.3.0 git-repo-info: ^2.1.1 jest: ^27.4.7 preload: ^0.1.0 @@ -8224,7 +8376,7 @@ __metadata: languageName: node linkType: hard -"tsconfig-paths@npm:^3.12.0": +"tsconfig-paths@npm:^3.12.0, tsconfig-paths@npm:^3.9.0": version: 3.12.0 resolution: "tsconfig-paths@npm:3.12.0" dependencies: -- cgit v1.2.3