diff --git a/.env.dist b/.env.dist index 2df1931b..8ab1b022 100644 --- a/.env.dist +++ b/.env.dist @@ -1,6 +1,6 @@ TRACKER_MATOMO_ID_SITE= TRACKER_MATOMO_URL=https://matomo.fabrique.social.gouv.fr -# matomo | posthog | noop (noop = no tracker) | debug | delegating +# matomo | posthog | noop (noop = no tracker) | debug | delegating / default=noop # delegating = "delegating:,,...," = multiple tracker TRACKER_PROVIDER=noop TRACKER_POSTHOG_API_KEY= @@ -11,3 +11,5 @@ SENTRY_ORG=incubateur SENTRY_URL=https://sentry.fabrique.social.gouv.fr # if "prodcution" build, skip uploading sourcemaps to Sentry SKIP_SENTRY_UPLOAD=1 +# file | localdb | inmemory / default=file +CACHE_PROVIDER=file diff --git a/.github/workflows/branch-test-setup.yml b/.github/workflows/branch-test-setup.yml index d437f2be..40bd6f56 100644 --- a/.github/workflows/branch-test-setup.yml +++ b/.github/workflows/branch-test-setup.yml @@ -58,6 +58,7 @@ jobs: SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} SENTRY_ORG: ${{ secrets.SENTRY_ORG }} SENTRY_URL: ${{ secrets.SENTRY_URL }} + CACHE_PROVIDER: file - name: Archive dist uses: actions/upload-artifact@v2 with: diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index cb50249e..aebd54c7 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -35,6 +35,7 @@ jobs: SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} SENTRY_ORG: ${{ secrets.SENTRY_ORG }} SENTRY_URL: ${{ secrets.SENTRY_URL }} + CACHE_PROVIDER: file - name: Archive dist uses: actions/upload-artifact@v2 diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index f1c77ef5..60db2f18 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -35,6 +35,7 @@ jobs: SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} SENTRY_ORG: ${{ secrets.SENTRY_ORG }} SENTRY_URL: ${{ secrets.SENTRY_URL }} + CACHE_PROVIDER: file - name: Lint run: yarn lint && yarn lint:test diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f8e8714e..f860522b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -105,6 +105,7 @@ jobs: SENTRY_DSN: ${{ secrets.SENTRY_DSN }} SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} SENTRY_URL: ${{ secrets.SENTRY_URL }} + CACHE_PROVIDER: localdb - name: Archive dist uses: actions/upload-artifact@v2 with: diff --git a/TODO.md b/TODO.md index 79ecaebc..14069c2d 100644 --- a/TODO.md +++ b/TODO.md @@ -12,7 +12,7 @@ - [x] coverage - [x] generate build ## Services -- [ ] Logger (winston + sentry + console + "renderer transport to main") +- [x] Logger (winston + sentry + console + "renderer transport to main") - [x] Tracker (posthog) - [x] global and user config (shared over ipc) - [x] i18n (i18next + react-i18next (start [here](./src/common/i18n/))) @@ -46,5 +46,5 @@ - [ ] pst extractor indexes => descriptorId - [ ] eml export parallel ops - [x] test csv export -- [ ] windows installer appId shortcut regedit -- [ ] windows open pst error (db?) +- [x] windows installer appId shortcut regedit +- [x] windows open pst error (db?) diff --git a/global.d.ts b/global.d.ts index f870730b..5556d6e7 100644 --- a/global.d.ts +++ b/global.d.ts @@ -24,6 +24,7 @@ declare const __static: string; declare namespace NodeJS { interface ProcessEnv { + CACHE_PROVIDER: string; SENTRY_DSN: string; SENTRY_ORG: string; SENTRY_URL: string; diff --git a/package.json b/package.json index beb9f0ec..d2180dc0 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "scripts": { "dev": "electron-webpack dev", "debug": "cross-env 'ELECTRON_ARGS=[\"--inspect-brk=9229\"]' yarn dev", - "compile": "electron-webpack", + "compile": "electron-webpack && webpack --config webpack.workers.config.js", "dist:win": "electron-builder --x64 --win portable msi nsis", "dist:mac": "electron-builder --mac dmg zip", "dist:mac-local": "CSC_IDENTITY_AUTO_DISCOVERY=false electron-builder --mac dmg", @@ -55,16 +55,19 @@ "@nivo/core", "@nivo/generators", "@sentry/electron", - "@socialgouv/archimail-pst-extractor", "d3", + "electron-log", "electron-store", "i18next", "lodash", + "normalize.css", "posthog-js", + "react", + "react-dom", "react-dropzone", "react-i18next", "source-map-support", - "tarn", + "uuid", "zustand" ] }, @@ -98,6 +101,13 @@ "filter": [ "**/*" ] + }, + { + "from": "node_modules", + "to": "natives/", + "filter": [ + "**/*.node" + ] } ], "directories": { @@ -189,6 +199,7 @@ "sass": "^1.49.0", "sass-loader": "10", "semantic-release": "^19.0.3", + "string-replace-loader": "^2", "stylelint": "^14.9.1", "stylelint-config-sass-guidelines": "^9.0.1", "stylelint-config-standard": "^26.0.0", @@ -207,14 +218,15 @@ "@nivo/generators": "^0.79.0", "@sentry/electron": "^3.0.7", "@socialgouv/archimail-pst-extractor": "^0.2.0", + "classic-level": "^1.2.0", "d3": "^7.3.0", + "electron-log": "^4.4.8", "electron-store": "^8.0.2", "electron-updater": "^5.0.5", "electron-util": "^0.17.2", "fs-extra": "^10.1.0", "i18next": "^21.8.10", "json2csv": "^5.0.6", - "level": "^8.0.0", "lodash": "^4.17.21", "normalize.css": "^8.0.1", "posthog-js": "~1.24.0", diff --git a/src/common/config.ts b/src/common/config.ts index f05c91b1..b6c0be04 100644 --- a/src/common/config.ts +++ b/src/common/config.ts @@ -15,6 +15,7 @@ const { app, ipcMain, ipcRenderer } = IS_WORKER export interface WorkerConfig { APP_CACHE: string; + APP_DATA: string; IS_DEV: boolean; IS_DIST_MODE: boolean; IS_E2E: boolean; @@ -51,6 +52,7 @@ export const IS_MAC = localWorkerConfig.IS_MAC ?? process.platform === "darwin"; export const IS_WIN = localWorkerConfig.IS_WIN ?? process.platform === "win32"; const IS_PACKAGE_EVENT = "config.IS_PACKAGED"; const APP_CACHE_EVENT = "config.APP_CACHE"; +const APP_DATA_EVENT = "config.APP_DATA"; if (IS_MAIN && !IS_WORKER) { ipcMain.on(IS_PACKAGE_EVENT, (event) => { event.returnValue = app.isPackaged; @@ -58,6 +60,9 @@ if (IS_MAIN && !IS_WORKER) { ipcMain.on(APP_CACHE_EVENT, (event) => { event.returnValue = APP_CACHE(); }); + ipcMain.on(APP_DATA_EVENT, (event) => { + event.returnValue = APP_DATA(); + }); } if (IS_DEV && IS_MAIN && !IS_WORKER) { @@ -87,6 +92,13 @@ export const APP_CACHE = (): string => { } else return ipcRenderer.sendSync(APP_CACHE_EVENT) as string; }; +export const APP_DATA = (): string => { + if (IS_WORKER) return localWorkerConfig.APP_DATA!; + if (IS_MAIN) { + return app.getPath("userData"); + } else return ipcRenderer.sendSync(APP_DATA_EVENT) as string; +}; + export const IS_DIST_MODE = localWorkerConfig.IS_DIST_MODE ?? (!IS_PACKAGED() && !process.env.ELECTRON_WEBPACK_WDS_PORT); @@ -100,6 +112,7 @@ export const STATIC_PATH = export const workerConfig: WorkerConfig = { APP_CACHE: APP_CACHE(), + APP_DATA: APP_DATA(), IS_DEV, IS_DIST_MODE, IS_E2E, diff --git a/src/common/lib/ModuleManager.ts b/src/common/lib/ModuleManager.ts index 2798b3e2..486e77ac 100644 --- a/src/common/lib/ModuleManager.ts +++ b/src/common/lib/ModuleManager.ts @@ -1,3 +1,4 @@ +import { logger } from "../logger"; import type { Module } from "../modules/Module"; import { ModuleError } from "../modules/Module"; import { AppError } from "./error/AppError"; @@ -8,7 +9,7 @@ import { AppError } from "./error/AppError"; export const loadModules = async (...mods: Module[]): pvoid => { await Promise.all( mods.map(async (mod) => { - console.log(` ${mod.constructor.name} loading !`); + logger.log(` ${mod.constructor.name} loading !`); await mod.init().catch((error) => { throw new ModuleError( ` ${mod.constructor.name} failed init.`, @@ -27,7 +28,7 @@ export const loadModules = async (...mods: Module[]): pvoid => { export const unloadModules = async (...mods: Module[]): pvoid => { await Promise.all( mods.map(async (mod) => { - console.warn( + logger.warn( ` ${mod.constructor.name} unloading !` ); await mod.uninit().catch((error) => { diff --git a/src/common/lib/event/PubSub.ts b/src/common/lib/event/PubSub.ts index 044b586d..b6c0a5a3 100644 --- a/src/common/lib/event/PubSub.ts +++ b/src/common/lib/event/PubSub.ts @@ -2,6 +2,7 @@ import { noop } from "lodash"; import { v4 as randomUuid } from "uuid"; import { IS_MAIN } from "../../config"; +import { logger } from "../../logger"; import { IsomorphicService } from "../../modules/ContainerModule"; import { ipcMain, ipcRenderer } from "../ipc"; import type { @@ -51,7 +52,7 @@ export class PubSub extends IsomorphicService { // the trigger with an ipc event (PUBSUB_TRIGGER_EVENT) // also save the "unsubscriber" returned function for later usage ipcMain.on(PUBSUB_SUBSCRIBE_EVENT, (ipcEvent, id, uuid) => { - console.log("[pubsub] renderer asks for subscribe", { + logger.log("[pubsub] renderer asks for subscribe", { id, uuid, }); @@ -68,7 +69,7 @@ export class PubSub extends IsomorphicService { // get and trigger "unsubscriber" functions (which call the // PUBSUB_UNSUBSCRIBE_EVENT back to ipc) and delete them ipcMain.on(PUBSUB_UNSUBSCRIBE_EVENT, (event, uuid) => { - console.log("[pubsub] renderer asks for unsub", { uuid }); + logger.log("[pubsub] renderer asks for unsub", { uuid }); this.unsubscribersInRenderer.get(uuid)?.(); event.returnValue = this.unsubscribersInRenderer.delete(uuid); }); @@ -77,7 +78,7 @@ export class PubSub extends IsomorphicService { // - basically when a publish is done on main - propagate the // trigger to registered renderer listeners ipcRenderer.on(PUBSUB_TRIGGER_EVENT, (_, id, event) => { - console.log("[pubsub] main triggered an event", { + logger.log("[pubsub] main triggered an event", { event, id, }); @@ -98,7 +99,7 @@ export class PubSub extends IsomorphicService { */ public async uninit(): pvoid { this.unsubscribersInRenderer.forEach((unsubscribe, uuid) => { - console.log("[pubsub] call unsubscribe for ", uuid); + logger.log("[pubsub] call unsubscribe for ", uuid); unsubscribe(); }); this.unsubscribersInRenderer.clear(); @@ -141,7 +142,7 @@ export class PubSub extends IsomorphicService { PUBSUB_UNSUBSCRIBE_EVENT, uuid ); - console.log("[pubsub] unsubscribe in main !", { done }); + logger.log("[pubsub] unsubscribe in main !", { done }); }; this.unsubscribersInRenderer.set(uuid, unsubscribe); } diff --git a/src/common/logger.ts b/src/common/logger.ts new file mode 100644 index 00000000..1e43e1b3 --- /dev/null +++ b/src/common/logger.ts @@ -0,0 +1,26 @@ +import { create } from "electron-log"; +import path from "path"; + +import { APP_DATA, IS_DEV, IS_MAIN, IS_TEST, IS_WORKER } from "./config"; +import { name } from "./utils/package"; + +const logger = create(name); +if (!IS_TEST) { + logger.transports.file.resolvePath = ({ fileName }) => + path.resolve(APP_DATA(), "logs", fileName ?? "default.log"); + + if (!IS_MAIN && !IS_WORKER) { + // renderer only config + if (logger.transports.ipc) logger.transports.ipc.level = false; + } + + if (IS_DEV) { + logger.transports.file.level = false; + } + + if (IS_MAIN && !IS_WORKER) { + // main only config + } +} + +export { logger }; diff --git a/src/common/modules/ContainerModule.ts b/src/common/modules/ContainerModule.ts index 8f9bc1fb..354c4f47 100644 --- a/src/common/modules/ContainerModule.ts +++ b/src/common/modules/ContainerModule.ts @@ -2,6 +2,7 @@ import { useEffect, useState } from "react"; import { IS_PACKAGED } from "../config"; import { AppError } from "../lib/error/AppError"; +import { logger } from "../logger"; import type { UnknownMapping } from "../utils/type"; import type { ReturnServiceType, @@ -80,7 +81,7 @@ class ContainerModule extends IsomorphicModule { await Promise.all( [...isomorphicServiceMap.values(), ...serviceMap.values()] .map((service) => { - console.info( + logger.info( `[ContainerModule] ${ (service as Service).name } loading !` @@ -102,7 +103,7 @@ class ContainerModule extends IsomorphicModule { await Promise.all( [...isomorphicServiceMap.values(), ...serviceMap.values()] .map((service) => { - console.warn( + logger.warn( `[ContainerModule] ${ (service as Service).name } unloading !` diff --git a/src/common/modules/I18nModule.ts b/src/common/modules/I18nModule.ts index 541097e6..3cd632c3 100644 --- a/src/common/modules/I18nModule.ts +++ b/src/common/modules/I18nModule.ts @@ -17,6 +17,7 @@ import { } from "../i18n/raw"; import type { PubSub } from "../lib/event/PubSub"; import type { Event, Unsubscriber } from "../lib/event/type"; +import { logger } from "../logger"; import { WaitableTrait } from "../utils/WaitableTrait"; import { IsomorphicService } from "./ContainerModule"; import { IsomorphicModule } from "./Module"; @@ -139,7 +140,7 @@ export class I18nModule extends IsomorphicModule { this.unsubscriberConfigUpdate = this.pubSub.subscribe( "event.userconfig.updated", (event) => { - console.log( + logger.log( "[i18nModule] pubsub trigger", this.userConfigChangedFromHere, event @@ -157,7 +158,7 @@ export class I18nModule extends IsomorphicModule { } public async uninit(): pvoid { - console.info("[I18nModule] uninit ", this.unsubscriberConfigUpdate); + logger.info("[I18nModule] uninit ", this.unsubscriberConfigUpdate); this.unsubscriberConfigUpdate?.(); return Promise.resolve(); } @@ -209,7 +210,7 @@ export class I18nModule extends IsomorphicModule { ipcMain.handle( I18N_CHANGE_LANGUAGE_EVENT, async (_evt, lng: Locale) => { - console.log("[I18nModule] change asked from renderer", lng); + logger.log("[I18nModule] change asked from renderer", lng); const oldLng = i18next.language; await i18next.changeLanguage(lng); this.userConfigChangedFromHere = true; @@ -231,7 +232,7 @@ export class I18nModule extends IsomorphicModule { ipcRenderer.on( I18N_CHANGE_LANGUAGE_CALLBACK_EVENT, async (_evt, lng: Locale) => { - console.log("[I18nModule] change triggered by main", lng); + logger.log("[I18nModule] change triggered by main", lng); const oldLng = i18next.language; await i18next.changeLanguage(lng); this.triggerLanguageChangedListeners(lng, oldLng as Locale); diff --git a/src/common/modules/TrackerModule.ts b/src/common/modules/TrackerModule.ts index bf72fdfc..8d3233e7 100644 --- a/src/common/modules/TrackerModule.ts +++ b/src/common/modules/TrackerModule.ts @@ -1,4 +1,6 @@ +import { IS_DEV } from "../config"; import type { PubSub } from "../lib/event/PubSub"; +import { DebugProvider } from "../tracker/provider/DebugProvider"; import { DelegatingProvider } from "../tracker/provider/DelegatingProvider"; import { NoopProvider } from "../tracker/provider/NoopProvider"; import type { TrackerProvider } from "../tracker/provider/TrackerProvider"; @@ -70,8 +72,9 @@ export class TrackerModule extends IsomorphicModule { ); return new DelegatingProvider(appId, disabled, foundProviders); } - return new (providers.find((p) => p.trackerName === name) ?? - NoopProvider)(appId, disabled) as TrackerProvider; + return new (providers.find((p) => p.trackerName === name) ?? IS_DEV + ? DebugProvider + : NoopProvider)(appId, disabled) as TrackerProvider; } get service(): TrackerService { diff --git a/src/common/modules/exporters/CsvExporter.ts b/src/common/modules/exporters/CsvExporter.ts index c2335ba0..bde753bb 100644 --- a/src/common/modules/exporters/CsvExporter.ts +++ b/src/common/modules/exporters/CsvExporter.ts @@ -1,6 +1,7 @@ import { writeFile } from "fs/promises"; import { Parser } from "json2csv"; +import { logger } from "../../logger"; import type { SimpleObject } from "../../utils/type"; import type { JsonExporter } from "./Exporter"; // eslint-disable-next-line unused-imports/no-unused-imports @@ -11,16 +12,16 @@ import { xlsxExporter } from "./XslxExporter"; */ export const csvExporter: JsonExporter = { async export(obj: T[], dest: string) { - console.log("Generate CSV..."); + logger.info("[CsvExporter] Generate CSV..."); const parser = new Parser({ excelStrings: true, }); const data = parser.parse(obj); - console.log("Export..."); + logger.info("[CsvExporter] Export..."); await writeFile(dest, data, { encoding: "utf-8", }); - console.info("Done!"); + logger.info("[CsvExporter] Done!"); }, }; diff --git a/src/common/modules/exporters/XslxExporter.ts b/src/common/modules/exporters/XslxExporter.ts index 3e4558e8..ff2fa298 100644 --- a/src/common/modules/exporters/XslxExporter.ts +++ b/src/common/modules/exporters/XslxExporter.ts @@ -2,6 +2,7 @@ import type { WorkBook } from "xlsx"; import { utils, writeFile } from "xlsx"; +import { logger } from "../../logger"; import { chunkString } from "../../utils"; import type { SimpleObject } from "../../utils/type"; import type { JsonExporter } from "./Exporter"; @@ -13,7 +14,7 @@ const MAX_CELL_LENGTH = 32767; */ export const xlsxExporter: JsonExporter = { async export(obj: T[], dest: string) { - console.log("Generate XLSX"); + logger.info("[XlsxExporter] Generate XLSX"); const sheet = utils.json_to_sheet(sanitize(obj), { WTF: true, cellDates: true, @@ -27,7 +28,7 @@ export const xlsxExporter: JsonExporter = { Sheets, }; - console.log("And write"); + logger.info("[XlsxExporter] And write"); await writeFile(book, dest); }, }; diff --git a/src/common/tracker/matomo/MatomoClient.ts b/src/common/tracker/matomo/MatomoClient.ts index 888e9390..adfbb85d 100644 --- a/src/common/tracker/matomo/MatomoClient.ts +++ b/src/common/tracker/matomo/MatomoClient.ts @@ -1,5 +1,7 @@ import axios from "axios"; +import { logger } from "../../logger"; + /** * @see https://developer.matomo.org/api-reference/tracking-api */ @@ -110,7 +112,7 @@ export class MatomoClient { }, }) .then((res) => { - console.log( + logger.log( "MATOMO RESPONSE", res.status, res.statusText, diff --git a/src/common/tracker/provider/DebugProvider.ts b/src/common/tracker/provider/DebugProvider.ts index b0af6fa2..5463c7c9 100644 --- a/src/common/tracker/provider/DebugProvider.ts +++ b/src/common/tracker/provider/DebugProvider.ts @@ -1,3 +1,4 @@ +import { logger } from "../../logger"; import type { TrackEvent } from "../type"; import type { TrackArgs } from "./TrackerProvider"; import { TrackerProvider } from "./TrackerProvider"; @@ -16,7 +17,7 @@ export class DebugProvider extends TrackerProvider { public track(...args: TrackArgs): void { const [event, props] = args; - console.info("[DebugTracker] Track", { event, props }); + logger.debug("[Tracker][DebugProvider] Track", { event, props }); } public enable(): void { diff --git a/src/common/tracker/provider/DelegatingProvider.ts b/src/common/tracker/provider/DelegatingProvider.ts index 7f18e6fa..43213364 100644 --- a/src/common/tracker/provider/DelegatingProvider.ts +++ b/src/common/tracker/provider/DelegatingProvider.ts @@ -1,5 +1,6 @@ import type { Integration } from "@sentry/types"; +import { logger } from "../../logger"; import type { Split, UnionConcat } from "../../utils/type"; import type { TrackAppId, TrackEvent } from "../type"; import type { TrackArgs } from "./TrackerProvider"; @@ -19,7 +20,7 @@ export class DelegatingProvider extends TrackerProvider { disabled: boolean, private readonly providers: TrackerProvider[] ) { - console.log("[Tracker][DelegatingProvider]", { providers }); + logger.debug("[Tracker][DelegatingProvider]", { providers }); super(appId, disabled); } diff --git a/src/common/tracker/provider/MatomoProvider.ts b/src/common/tracker/provider/MatomoProvider.ts index 68df7327..def5460a 100644 --- a/src/common/tracker/provider/MatomoProvider.ts +++ b/src/common/tracker/provider/MatomoProvider.ts @@ -1,6 +1,7 @@ import type MatomoTracker from "@datapunt/matomo-tracker-js"; import { IS_MAIN } from "../../config"; +import { logger } from "../../logger"; import { MatomoClient } from "../matomo/MatomoClient"; import type { TrackEvent } from "../type"; import { eventCategoryMap } from "../type"; @@ -22,7 +23,7 @@ export class MatomoProvider extends TrackerProvider< async init(): pvoid { if (this.inited) { - console.warn("[MatomoProvider] Already inited."); + logger.warn("[MatomoProvider] Already inited."); } if (IS_MAIN) { this.tracker = new MatomoClient( diff --git a/src/common/tracker/provider/NoopProvider.ts b/src/common/tracker/provider/NoopProvider.ts index 7563dd6d..79829a05 100644 --- a/src/common/tracker/provider/NoopProvider.ts +++ b/src/common/tracker/provider/NoopProvider.ts @@ -1,3 +1,4 @@ +import { logger } from "../../logger"; import type { TrackEvent } from "../type"; import type { TrackArgs } from "./TrackerProvider"; import { TrackerProvider } from "./TrackerProvider"; @@ -31,8 +32,7 @@ export class NoopProvider extends TrackerProvider { private warn() { if (!this.flagConsole) { - // eslint-disable-next-line no-console - console.warn( + logger.warn( `[Tracker] No tracker set or found (${process.env.TRACKER_PROVIDER})` ); this.flagConsole = true; diff --git a/src/common/tracker/provider/PosthogProvider.ts b/src/common/tracker/provider/PosthogProvider.ts index 7426b8a1..af2f8671 100644 --- a/src/common/tracker/provider/PosthogProvider.ts +++ b/src/common/tracker/provider/PosthogProvider.ts @@ -3,6 +3,7 @@ import type FrontPostHog from "posthog-js"; import type NodeJsPostHog from "posthog-node"; import { IS_MAIN } from "../../config"; +import { logger } from "../../logger"; import type { TrackEvent } from "../type"; import type { TrackArgs } from "./TrackerProvider"; import { TrackerProvider } from "./TrackerProvider"; @@ -32,7 +33,7 @@ export class PosthogProvider extends TrackerProvider< public async init(): pvoid { if (this.inited) { - console.warn("[PosthogProvider] Already inited.", this.disabled); + logger.warn("[PosthogProvider] Already inited.", this.disabled); } if (IS_MAIN) { this.tracker = new (await import("posthog-node")).default( @@ -80,7 +81,7 @@ export class PosthogProvider extends TrackerProvider< } public async uninit(): pvoid { - console.info("[Tracker][PosthogProvider] Shutdown posthog"); + logger.info("[Tracker][PosthogProvider] Shutdown posthog"); if (this.isMain(this.tracker)) { this.tracker.shutdown(); } diff --git a/src/main/index.ts b/src/main/index.ts index fb01bdb6..662728e6 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -1,10 +1,10 @@ -// import "@common/utils/electron"; import "@common/utils/overload"; import { IS_DIST_MODE, IS_E2E, IS_PACKAGED } from "@common/config"; import { getIsomorphicModules } from "@common/lib/core/isomorphic"; import { AppError } from "@common/lib/error/AppError"; import { loadModules, unloadModules } from "@common/lib/ModuleManager"; +import { logger } from "@common/logger"; import { containerModule } from "@common/modules/ContainerModule"; import type { Module } from "@common/modules/Module"; import { setupSentry } from "@common/monitoring/sentry"; @@ -14,11 +14,11 @@ import { app, BrowserWindow, Menu } from "electron"; import path from "path"; import { AppModule } from "./modules/AppModule"; +import { CacheModule } from "./modules/CacheModule"; import { DevToolsModule } from "./modules/DevToolsModule"; import { MenuModule } from "./modules/MenuModule"; import { PstExporterModule } from "./modules/PstExporterModule"; import { PstExtractorModule } from "./modules/PstExtractorModule"; -import { consoleToRendererService } from "./services/ConsoleToRendererService"; export type MainWindowRetriever = () => Promise; @@ -61,6 +61,7 @@ let mainWindow: BrowserWindow | null = null; */ const createMainWindow = async () => { mainWindow = new BrowserWindow({ + show: false, webPreferences: { contextIsolation: false, defaultEncoding: "UTF-8", @@ -71,6 +72,10 @@ const createMainWindow = async () => { }, }); + mainWindow.once("ready-to-show", () => { + mainWindow?.show(); + }); + if (!IS_PACKAGED() && !IS_E2E) mainWindow.webContents.openDevTools(); await mainWindow.loadURL(INDEX_URL); @@ -97,27 +102,26 @@ const mainWindowRetriever: MainWindowRetriever = async () => app.on("ready", async () => { try { // load shared/common modules - const isomorphicModules = getIsomorphicModules( - ["consoleToRendererService", consoleToRendererService], - ["mainWindowRetriever", mainWindowRetriever] - ); + const isomorphicModules = getIsomorphicModules([ + "mainWindowRetriever", + mainWindowRetriever, + ]); const trackerService = containerModule.get("trackerService"); const modules: Module[] = [ ...isomorphicModules, new AppModule( mainWindowRetriever, - consoleToRendererService, containerModule.get("i18nService"), containerModule.get("userConfigService"), trackerService ), new DevToolsModule(), + new CacheModule(), new PstExtractorModule( containerModule.get("userConfigService"), - consoleToRendererService + containerModule.get("pstCacheMainService") ), new MenuModule( - consoleToRendererService, containerModule.get("pstExtractorMainService"), containerModule.get("i18nService"), containerModule.get("fileExporterService"), @@ -148,7 +152,7 @@ app.on("ready", async () => { app.emit(MAIN_WINDOW_CREATED_APP_EVENT); } catch (error: unknown) { if (error instanceof AppError) { - console.error("Error during app lauching"); + logger.error("Error during app lauching"); if (IS_PACKAGED()) Sentry.addBreadcrumb({ data: { @@ -157,7 +161,7 @@ app.on("ready", async () => { }, type: "info", }); - else console.error(error.appErrorStack()); + else logger.error(error.appErrorStack()); } throw error; } diff --git a/src/main/modules/AppModule.ts b/src/main/modules/AppModule.ts index ae33f846..9c8bf371 100644 --- a/src/main/modules/AppModule.ts +++ b/src/main/modules/AppModule.ts @@ -6,7 +6,6 @@ import { version } from "@common/utils/package"; import { dialog } from "electron"; import type { MainWindowRetriever } from ".."; -import type { ConsoleToRendererService } from "../services/ConsoleToRendererService"; import { isQuitingForUpdate, setupAutoUpdate } from "./app/autoUpdate"; import { MainModule } from "./MainModule"; @@ -16,7 +15,6 @@ import { MainModule } from "./MainModule"; export class AppModule extends MainModule { constructor( private readonly mainWindowRetriever: MainWindowRetriever, - private readonly consoleToRendererService: ConsoleToRendererService, private readonly i18nService: I18nService, private readonly userConfigService: UserConfigService, private readonly trackerService: TrackerService @@ -91,21 +89,7 @@ export class AppModule extends MainModule { }); } - const log = this.consoleToRendererService.log.bind( - this.consoleToRendererService, - mainWindow - ); - setupAutoUpdate( - this.trackerService, - { - debug: log, - error: log, - info: log, - log, - warn: log, - }, - this.i18nService.i18next.t - ); + setupAutoUpdate(this.trackerService, this.i18nService.i18next.t); }); return Promise.resolve(); diff --git a/src/main/modules/CacheModule.ts b/src/main/modules/CacheModule.ts new file mode 100644 index 00000000..3011955a --- /dev/null +++ b/src/main/modules/CacheModule.ts @@ -0,0 +1,73 @@ +import { logger } from "@common/logger"; +import type { Service } from "@common/modules/container/type"; +import { containerModule } from "@common/modules/ContainerModule"; + +import type { AbstractPstCache } from "./cache/AbstractPstCache"; +import { ElectronStorePstCache } from "./cache/ElectronStorePstCache"; +import { InMemoryPstCache } from "./cache/InMemoryPstCache"; +import { LevelPstCache } from "./cache/LevelPstCache"; +import { MainModule } from "./MainModule"; + +interface CacheType { + file: typeof ElectronStorePstCache; + /** @deprecated */ + inmemory: typeof InMemoryPstCache; + localdb: typeof LevelPstCache; +} + +/** + * Module responsible of handling and extracting datas from given PST files. + * + * It will load a worker to extract the PST without blocking the main thread. + */ +export class CacheModule extends MainModule { + private static cacheService: PstCacheMainService | null = null; + + constructor() { + super(); + containerModule.registerServices([ + "pstCacheMainService", + CacheModule.getCacheService(), + ]); + } + + public static getCacheService(): PstCacheMainService { + if (!this.cacheService) { + const cacheType = + /* eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- handle empty var */ + (process.env.CACHE_PROVIDER as keyof CacheType) || "file"; + + logger.info(`[CacheModule] Initialize cache of type ${cacheType}`); + + this.cacheService = new (class CacheService + extends (cacheType === "localdb" + ? LevelPstCache + : cacheType === "inmemory" + ? InMemoryPstCache + : ElectronStorePstCache) + implements Service + { + public name = "PstCacheMainService"; + + /** @override */ + public async init(this: PstCacheMainService): pvoid { + await this.close(); + } + + /** @override */ + public async uninit(this: PstCacheMainService): pvoid { + await this.close(); + } + })() as PstCacheMainService; + + logger.debug(this.cacheService); + } + return this.cacheService; + } + + public async init(): pvoid { + return Promise.resolve(); + } +} + +export interface PstCacheMainService extends Service, AbstractPstCache {} diff --git a/src/main/modules/MenuModule.ts b/src/main/modules/MenuModule.ts index 792d3177..acfc9d05 100644 --- a/src/main/modules/MenuModule.ts +++ b/src/main/modules/MenuModule.ts @@ -7,7 +7,6 @@ import type { MenuItem } from "electron"; import { Menu } from "electron"; import { t } from "i18next"; -import type { ConsoleToRendererService } from "../services/ConsoleToRendererService"; import { MainModule } from "./MainModule"; import { DebugMenu } from "./menu/DebugMenu"; import type { PstExtractorMainService } from "./PstExtractorModule"; @@ -49,7 +48,6 @@ export class MenuModule extends MainModule { private debugMenu?: DebugMenu; constructor( - private readonly consoleToRendererService: ConsoleToRendererService, private readonly pstExtractorMainService: PstExtractorMainService, private readonly i18nService: I18nService, private readonly fileExporterService: FileExporterService, @@ -62,7 +60,6 @@ export class MenuModule extends MainModule { await this.i18nService.wait(); await this.userConfigService.wait(); this.debugMenu = new DebugMenu( - this.consoleToRendererService, this.pstExtractorMainService, this.i18nService, this.fileExporterService, diff --git a/src/main/modules/PstExporterModule.ts b/src/main/modules/PstExporterModule.ts index 1c3b5749..c48549fd 100644 --- a/src/main/modules/PstExporterModule.ts +++ b/src/main/modules/PstExporterModule.ts @@ -20,11 +20,9 @@ import { import type { SimpleObject } from "@common/utils/type"; import path from "path"; +import type { PstCacheMainService } from "./CacheModule"; import { MainModule } from "./MainModule"; -import type { - PstCacheMainService, - PstExtractorMainService, -} from "./PstExtractorModule"; +import type { PstExtractorMainService } from "./PstExtractorModule"; export class PstExporterModule extends MainModule { private inited = false; diff --git a/src/main/modules/PstExtractorModule.ts b/src/main/modules/PstExtractorModule.ts index 7d8b9480..4e53a9d6 100644 --- a/src/main/modules/PstExtractorModule.ts +++ b/src/main/modules/PstExtractorModule.ts @@ -1,6 +1,7 @@ import { AppError } from "@common/lib/error/AppError"; import { ipcMain } from "@common/lib/ipc"; import type { DualIpcConfigExtractReply } from "@common/lib/ipc/event"; +import { logger } from "@common/logger"; import type { Service } from "@common/modules/container/type"; import { containerModule } from "@common/modules/ContainerModule"; import type { @@ -10,14 +11,12 @@ import type { PstProgressState, } from "@common/modules/pst-extractor/type"; import type { UserConfigService } from "@common/modules/UserConfigModule"; -import { BrowserWindow } from "electron"; -import type { ConsoleToRendererService } from "../services/ConsoleToRendererService"; import { WorkerClient } from "../workers/WorkerClient"; +import type { PstCacheMainService } from "./CacheModule"; import { MainModule } from "./MainModule"; import type { FetchWorkerConfig } from "./pst-extractor/pst-email-fetcher.worker"; import type { ExtractorWorkerConfig } from "./pst-extractor/pst-extractor.worker"; -import { PstCache } from "./pst-extractor/PstCache"; export class PstExtractorError extends AppError {} @@ -54,13 +53,13 @@ export class PstExtractorModule extends MainModule { constructor( private readonly userConfigService: UserConfigService, - private readonly consoleToRendererService: ConsoleToRendererService + private readonly cacheService: PstCacheMainService ) { super(); - containerModule.registerServices( - ["pstExtractorMainService", this.extractorService], - ["pstCacheMainService", this.cacheService] - ); + containerModule.registerServices([ + "pstExtractorMainService", + this.extractorService, + ]); } public async init(): pvoid { @@ -103,20 +102,21 @@ export class PstExtractorModule extends MainModule { ); }); this.extractorWorker.addEventListener("error", (error) => { - console.log("[PstExtractorModule] extractorWorker Error"); - console.error(error); // TODO handle error + logger.log("[PstExtractorModule] extractorWorker Error"); + logger.error(error); // TODO handle error }); this.fetchWorker.addEventListener("error", (error) => { - console.log("[PstExtractorModule] fetchWorker Error"); - console.error(error); // TODO handle error + logger.log("[PstExtractorModule] fetchWorker Error"); + logger.error(error); // TODO handle error + }); + + this.extractorWorker.addEventListener("log", (message) => { + logger.log(`[FROM EXTRACTWORKER] ${message}`); }); this.fetchWorker.addEventListener("log", (message) => { - this.consoleToRendererService.log( - BrowserWindow.getAllWindows()[0]!, - `[FROM FETCHWORKER] ${message}` - ); + logger.log(`[FROM FETCHWORKER] ${message}`); }); this.inited = true; @@ -147,7 +147,7 @@ export class PstExtractorModule extends MainModule { delete this.lastPstExtractDatas; this.lastPath = options.pstFilePath; - console.info("Start extracting..."); + logger.info("[PstExtractorModule] Start extracting..."); await Promise.all([ this.fetchWorker.command("open", { pstFilePath: this.lastPath, @@ -156,6 +156,7 @@ export class PstExtractorModule extends MainModule { pstFilePath: this.lastPath, }), ]); + logger.info("[PstExtractorModule] Pst opened"); await this.extractorWorker.command("extract", { progressInterval: this.userConfigService.get( @@ -163,15 +164,17 @@ export class PstExtractorModule extends MainModule { ), viewConfigs: this.userConfigService.get("viewConfigs"), }); + logger.info("[PstExtractorModule] Worker extract done"); // TODO: hash instead this.cacheService.openForPst(this.lastPath); + logger.info("[PstExtractorModule] Cache opened"); const attachments = await this.cacheService.getAttachments(); const indexes = await this.cacheService.getPstMailIndexes(); const groups = await this.cacheService.getAllGroups(); const additionalDatas = await this.cacheService.getAllAddtionalData(); - this.consoleToRendererService.log(BrowserWindow.getAllWindows()[0]!, { + logger.debug({ additionalDatas, attachments, groups, @@ -185,7 +188,7 @@ export class PstExtractorModule extends MainModule { }; this.working = false; - console.info("Extract done."); + logger.info("[PstExtractorModule] Extract done."); return this.lastPstExtractDatas; } @@ -193,13 +196,13 @@ export class PstExtractorModule extends MainModule { if (this.working) { throw new PstExtractorError("Extractor already working."); } - console.info("Start fetching emails..."); + logger.info("[PstExtractorModule] Start fetching emails..."); const cacheKey = await this.fetchWorker.query("fetch", { emailIndexes, }); const emails = await this.cacheService.getTempEmails(cacheKey); - console.log("Fetching done"); + logger.log("[PstExtractorModule] Fetching done"); if (!emails.length) { throw new Error("Emails not found from given indexes."); } @@ -227,29 +230,9 @@ export class PstExtractorModule extends MainModule { name: "PstExtractorMainService", }; } - - public get cacheService(): PstCacheMainService { - return cacheService; - } } -const cacheService = new (class extends PstCache implements PstCacheMainService { - public name = "PstCacheMainService"; - - /** @override */ - public async init(): pvoid { - await this.db.close(); - } - - /** @override */ - public async uninit(): pvoid { - await this.db.close(); - } -})(); - export interface PstExtractorMainService extends Service { extract: typeof PstExtractorModule.prototype["extract"]; getEmails: typeof PstExtractorModule.prototype["getEmails"]; } - -export interface PstCacheMainService extends Service, PstCache {} diff --git a/src/main/modules/app/autoUpdate.ts b/src/main/modules/app/autoUpdate.ts index b09d1711..332f25d0 100644 --- a/src/main/modules/app/autoUpdate.ts +++ b/src/main/modules/app/autoUpdate.ts @@ -1,10 +1,11 @@ import { PRODUCT_CHANNEL } from "@common/config"; import { ipcMain } from "@common/lib/ipc"; +import { logger } from "@common/logger"; import type { AutoUpdateCheckIpcConfig } from "@common/modules/app/ipc"; import type { TrackerService } from "@common/modules/TrackerModule"; import { version } from "@common/utils/package"; import { dialog } from "electron"; -import type { Logger, ProgressInfo, UpdateInfo } from "electron-updater"; +import type { ProgressInfo, UpdateInfo } from "electron-updater"; import { autoUpdater } from "electron-updater"; import type { TFunction } from "i18next"; @@ -19,7 +20,6 @@ export const isQuitingForUpdate = (): boolean => quitForUpdate; let setup = false; export const setupAutoUpdate = ( trackerService: TrackerService, - logger: Logger & { error: Console["error"]; log: Console["log"] }, t: TFunction ): void => { if (setup) return; diff --git a/src/main/modules/cache/AbstractPstCache.ts b/src/main/modules/cache/AbstractPstCache.ts new file mode 100644 index 00000000..54963117 --- /dev/null +++ b/src/main/modules/cache/AbstractPstCache.ts @@ -0,0 +1,59 @@ +import { APP_CACHE } from "@common/config"; +import { logger } from "@common/logger"; +import type { + AdditionalDatas, + GroupType, + PstAttachment, + PstEmail, + PstMailIndex, +} from "@common/modules/pst-extractor/type"; + +export const ROOT_KEY = "_index_"; +export const ATTACHMENTS_KEY = "_attachments_"; +export const GROUPS_DB_PREFIX = "_groups_"; +export const ADDITIONNAL_DATAS_DB_PREFIX = "_additionalDatas_"; +export const PST_FETCH_CACHE_PREFIX = "_pstFetchCache_"; + +export abstract class AbstractPstCache { + protected currentPstID?: string; + + constructor(protected readonly cachePath = APP_CACHE()) { + logger.log(`[${this.constructor.name}] constructor`); + logger.debug(`[${this.constructor.name}]`, { cachePath }); + } + + public openForPst(pstId: string): void { + this.currentPstID = pstId; + } + + abstract close(): pvoid; + + abstract getAddtionalDatas( + name: T + ): Promise; + + abstract getAllAddtionalData(): Promise; + + abstract getAllGroups(): Promise>>; + + abstract getAttachments(): Promise>; + + abstract getGroup(name: GroupType): Promise>; + + abstract getPstMailIndexes(): Promise>; + + abstract getTempEmails(cacheKey: string): Promise; + + abstract setAddtionalDatas( + name: T, + addtionalDatas: AdditionalDatas[T] + ): pvoid; + + abstract setAttachments(attachments: Map): pvoid; + + abstract setGroup(name: GroupType, ids: Map): pvoid; + + abstract setPstMailIndexes(indexes: Map): pvoid; + + abstract setTempEmails(cacheKey: string, emails: PstEmail[]): pvoid; +} diff --git a/src/main/modules/cache/ElectronStorePstCache.ts b/src/main/modules/cache/ElectronStorePstCache.ts new file mode 100644 index 00000000..5b585d4a --- /dev/null +++ b/src/main/modules/cache/ElectronStorePstCache.ts @@ -0,0 +1,168 @@ +import { logger } from "@common/logger"; +import type { + AdditionalDatas, + GroupType, + PstAttachment, + PstAttachmentEntries, + PstEmail, + PstMailIndex, + PstMailIndexEntries, +} from "@common/modules/pst-extractor/type"; +import { Object } from "@common/utils/overload"; +import type { SimpleObject } from "@common/utils/type"; +import Store from "electron-store"; + +import { + AbstractPstCache, + ADDITIONNAL_DATAS_DB_PREFIX, + ATTACHMENTS_KEY, + GROUPS_DB_PREFIX, + PST_FETCH_CACHE_PREFIX, + ROOT_KEY, +} from "./AbstractPstCache"; + +type CacheStore = SimpleObject; + +export class ElectronStorePstCache extends AbstractPstCache { + private readonly store = new Store({ + accessPropertiesByDotNotation: false, + cwd: this.cachePath, + name: "file-db", + }); + + constructor(cachePath?: string) { + super(cachePath); + + logger.debug("[ElectronStorePstCache]", { store: this.store }); + } + + async setPstMailIndexes(indexes: Map): pvoid { + this.setValue(ROOT_KEY, [...indexes.entries()]); + return Promise.resolve(); + } + + async getPstMailIndexes(): Promise> { + const rawIndexes = this.getValue(ROOT_KEY) as PstMailIndexEntries; + return Promise.resolve(new Map(rawIndexes)); + } + + async setAttachments(attachments: Map): pvoid { + this.setValue(ATTACHMENTS_KEY, [...attachments.entries()]); + return Promise.resolve(); + } + + async getAttachments(): Promise> { + const rawAttachments = this.getValue( + ATTACHMENTS_KEY + ) as PstAttachmentEntries; + return Promise.resolve(new Map(rawAttachments)); + } + + async setGroup(name: GroupType, ids: Map): pvoid { + this.setValue(`${GROUPS_DB_PREFIX}${name}`, [...ids.entries()]); + return Promise.resolve(); + } + + async getGroup(name: GroupType): Promise> { + const rawIds = this.getValue(`${GROUPS_DB_PREFIX}${name}`) as [ + string, + string[] + ][]; + return Promise.resolve(new Map(rawIds)); + } + + async getAllGroups(): Promise>> { + const currentDb = this.getCurrentPstDb(); + const entries = Object.entries(currentDb) + .filter(([key]) => key.startsWith(GROUPS_DB_PREFIX)) + .map(([key, value]) => [ + key.replace(GROUPS_DB_PREFIX, ""), + value, + ]) as [string, unknown][]; + + return Promise.resolve( + entries.reduce( + (acc, [k, v]) => ({ + ...acc, + [k]: new Map(v as [string, string[]][]), + }), + {} + ) as Record> + ); + } + + async setAddtionalDatas( + name: T, + addtionalDatas: AdditionalDatas[T] + ): pvoid { + this.setValue(`${ADDITIONNAL_DATAS_DB_PREFIX}${name}`, addtionalDatas); + return Promise.resolve(); + } + + async getAddtionalDatas( + name: T + ): Promise { + return Promise.resolve( + this.getValue( + `${ADDITIONNAL_DATAS_DB_PREFIX}${name}` + ) as AdditionalDatas[T] + ); + } + + async getAllAddtionalData(): Promise { + const currentDb = this.getCurrentPstDb(); + const entries = Object.entries(currentDb) + .filter(([key]) => key.startsWith(ADDITIONNAL_DATAS_DB_PREFIX)) + .map(([key, value]) => [ + key.replace(ADDITIONNAL_DATAS_DB_PREFIX, ""), + value, + ]); + + return Promise.resolve( + Object.fromEntries(entries) as unknown as AdditionalDatas + ); + } + + async setTempEmails(cacheKey: string, emails: PstEmail[]): pvoid { + this.setValue(`${PST_FETCH_CACHE_PREFIX}${cacheKey}`, emails); + return Promise.resolve(); + } + + async getTempEmails(cacheKey: string): Promise { + const ret = this.getValue( + `${PST_FETCH_CACHE_PREFIX}${cacheKey}`, + true + ) as PstEmail[]; + return Promise.resolve(ret); + } + + public async close(): pvoid { + logger.log(`[ElectronStorePstCache] close (useless)`); + return Promise.resolve(); + } + + private getCurrentPstDb() { + if (!this.currentPstID) throw new Error("No PST cache opened yet."); + if (!this.store.has(this.currentPstID)) { + this.store.set(this.currentPstID, {}); + } + return this.store.get(this.currentPstID)!; + } + + private getValue(key: string, deleteValue?: true) { + const cache = this.getCurrentPstDb(); + const ret = cache[key]; + if (deleteValue) { + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete + delete cache[key]; + this.store.set(this.currentPstID!, cache); + } + return ret; + } + + private setValue(key: string, value: unknown) { + const cache = this.getCurrentPstDb(); + cache[key] = value; + this.store.set(this.currentPstID!, cache); + } +} diff --git a/src/main/modules/cache/InMemoryPstCache.ts b/src/main/modules/cache/InMemoryPstCache.ts new file mode 100644 index 00000000..2670fe2c --- /dev/null +++ b/src/main/modules/cache/InMemoryPstCache.ts @@ -0,0 +1,187 @@ +import { logger } from "@common/logger"; +import type { + AdditionalDatas, + GroupType, + PstAttachment, + PstAttachmentEntries, + PstEmail, + PstMailIndex, + PstMailIndexEntries, +} from "@common/modules/pst-extractor/type"; +import type { AnyFunction, MethodNames } from "@common/utils/type"; + +import { + AbstractPstCache, + ADDITIONNAL_DATAS_DB_PREFIX, + ATTACHMENTS_KEY, + GROUPS_DB_PREFIX, + PST_FETCH_CACHE_PREFIX, + ROOT_KEY, +} from "./AbstractPstCache"; + +type PstFileName = string; +type PstCacheKey = string; + +// TODO make it work with shared memory between workers and main process +const cache: Map> = new Map(); + +const LogOp = < + TProp extends MethodNames, + TMeth extends InMemoryPstCache[TProp], + TParams extends Parameters +>( + _proto: InMemoryPstCache, + _property: TProp, + descriptor: TypedPropertyDescriptor +) => { + const originalMethod = _proto[_property]; + descriptor.value = async function ( + this: InMemoryPstCache, + ...args: TParams + ) { + logger.log(`[InMemoryPstCache][LogOp][${_property}] before exec`, [ + ...(([...cache][0] ?? [])[1] ?? []), + ]); + const ret = await (originalMethod as AnyFunction).apply(this, args); + logger.log(`[InMemoryPstCache][LogOp][${_property}] after exec`, { + ret, + }); + return ret; + } as TMeth; +}; + +/** + * @deprecated - has to be shared between worker and main before (with SharedArraybuffer maybe) + */ +export class InMemoryPstCache extends AbstractPstCache { + @LogOp + async getPstMailIndexes(): Promise> { + const currentDb = this.getCurrentPstDb(); + const rawIndexes = currentDb.get(ROOT_KEY) as PstMailIndexEntries; + return Promise.resolve(new Map(rawIndexes)); + } + + @LogOp + async setPstMailIndexes(indexes: Map): pvoid { + const currentDb = this.getCurrentPstDb(); + currentDb.set(ROOT_KEY, [...indexes.entries()]); + return Promise.resolve(); + } + + @LogOp + async setAttachments(attachments: Map): pvoid { + const currentDb = this.getCurrentPstDb(); + currentDb.set(ATTACHMENTS_KEY, [...attachments.entries()]); + return Promise.resolve(); + } + + @LogOp + async getAttachments(): Promise> { + const currentDb = this.getCurrentPstDb(); + const rawAttachments = currentDb.get( + ATTACHMENTS_KEY + ) as PstAttachmentEntries; + return Promise.resolve(new Map(rawAttachments)); + } + + @LogOp + async setGroup(name: GroupType, ids: Map): pvoid { + const currentDb = this.getCurrentPstDb(); + currentDb.set(`${GROUPS_DB_PREFIX}${name}`, [...ids.entries()]); + return Promise.resolve(); + } + + @LogOp + async getGroup(name: GroupType): Promise> { + const currentDb = this.getCurrentPstDb(); + const rawIds = currentDb.get(`${GROUPS_DB_PREFIX}${name}`) as [ + string, + string[] + ][]; + return Promise.resolve(new Map(rawIds)); + } + + @LogOp + async getAllGroups(): Promise>> { + const currentDb = this.getCurrentPstDb(); + const entries = [...currentDb.entries()].filter(([key]) => + key.startsWith(GROUPS_DB_PREFIX) + ); + + return Promise.resolve( + entries.reduce( + (acc, [k, v]) => ({ + ...acc, + [k]: new Map(v as [string, string[]][]), + }), + {} + ) as Record> + ); + } + + @LogOp + async setAddtionalDatas( + name: T, + addtionalDatas: AdditionalDatas[T] + ): pvoid { + const currentDb = this.getCurrentPstDb(); + currentDb.set(`${ADDITIONNAL_DATAS_DB_PREFIX}${name}`, addtionalDatas); + return Promise.resolve(); + } + + @LogOp + async getAddtionalDatas( + name: T + ): Promise { + const currentDb = this.getCurrentPstDb(); + return Promise.resolve(currentDb.get(name) as AdditionalDatas[T]); + } + + @LogOp + async getAllAddtionalData(): Promise { + const currentDb = this.getCurrentPstDb(); + const entries = [...currentDb.entries()].filter(([key]) => + key.startsWith(ADDITIONNAL_DATAS_DB_PREFIX) + ); + + return Promise.resolve( + entries.reduce( + (acc, [k, v]) => ({ + ...acc, + [k]: new Map(v as [string, unknown][]), + }), + {} + ) as AdditionalDatas + ); + } + + @LogOp + async setTempEmails(cacheKey: string, emails: PstEmail[]): pvoid { + const currentDb = this.getCurrentPstDb(); + currentDb.set(`${PST_FETCH_CACHE_PREFIX}${cacheKey}`, emails); + return Promise.resolve(); + } + + @LogOp + async getTempEmails(cacheKey: string): Promise { + const currentDb = this.getCurrentPstDb(); + const ret = (await currentDb.get( + `${PST_FETCH_CACHE_PREFIX}${cacheKey}` + )) as PstEmail[]; + currentDb.delete(`${PST_FETCH_CACHE_PREFIX}${cacheKey}`); + return Promise.resolve(ret); + } + + public async close(): pvoid { + logger.log(`[InMemoryPstCache] close (useless)`); + return Promise.resolve(); + } + + private getCurrentPstDb() { + if (!this.currentPstID) throw new Error("No PST cache opened yet."); + if (!cache.has(this.currentPstID)) { + cache.set(this.currentPstID, new Map()); + } + return cache.get(this.currentPstID)!; + } +} diff --git a/src/main/modules/pst-extractor/PstCache.ts b/src/main/modules/cache/LevelPstCache.ts similarity index 76% rename from src/main/modules/pst-extractor/PstCache.ts rename to src/main/modules/cache/LevelPstCache.ts index 80c11fa9..e9103763 100644 --- a/src/main/modules/pst-extractor/PstCache.ts +++ b/src/main/modules/cache/LevelPstCache.ts @@ -1,4 +1,4 @@ -import { APP_CACHE } from "@common/config"; +import { logger } from "@common/logger"; import type { AdditionalDatas, GroupType, @@ -11,14 +11,18 @@ import type { } from "@common/modules/pst-extractor/type"; import type { ViewType } from "@common/modules/views/setup"; import type { AnyFunction, MethodNames } from "@common/utils/type"; -import { Level } from "level"; +import { ClassicLevel as Level } from "classic-level"; import path from "path"; -const ROOT_KEY = "_index_"; -const ATTACHMENTS_KEY = "_attachments_"; -const GROUPS_DB_PREFIX = "_groups_"; -const ADDITIONNAL_DATES_DB_PREFIX = "_additionalDatas_"; -const PST_FETCH_CACHE_PREFIX = "_pstFetchCache_"; +import { + AbstractPstCache, + ADDITIONNAL_DATAS_DB_PREFIX, + ATTACHMENTS_KEY, + GROUPS_DB_PREFIX, + PST_FETCH_CACHE_PREFIX, + ROOT_KEY, +} from "./AbstractPstCache"; + const CACHE_FOLDER_NAME = "archimail-db"; const defaultDbOptions = { @@ -26,35 +30,49 @@ const defaultDbOptions = { }; const SoftLockDb = < - TProp extends MethodNames, - TMeth extends PstCache[TProp], + TProp extends MethodNames, + TMeth extends LevelPstCache[TProp], TParams extends Parameters >( - _proto: PstCache, + _proto: LevelPstCache, _property: TProp, descriptor: TypedPropertyDescriptor ) => { const originalMethod = _proto[_property]; - descriptor.value = async function (this: PstCache, ...args: TParams) { + descriptor.value = async function (this: LevelPstCache, ...args: TParams) { + logger.debug(`[PstCache][SoftLockDb][${_property}] before open`); await this.db.open(); + logger.debug( + `[PstCache][SoftLockDb][${_property}] after open`, + this.db + ); const ret = await (originalMethod as AnyFunction).apply(this, args); + logger.debug(`[PstCache][SoftLockDb][${_property}] after exec`, { + ret, + }); await this.db.close(); + logger.debug( + `[PstCache][SoftLockDb][${_property}] after close`, + this.db + ); return ret; } as TMeth; }; -export class PstCache { +export class LevelPstCache extends AbstractPstCache { public readonly db: Level; - private currrentPstID?: string; - - constructor( - private readonly cachePath = path.resolve( - APP_CACHE(), - CACHE_FOLDER_NAME - ) - ) { - this.db = new Level(this.cachePath, defaultDbOptions); + constructor(cachePath?: string) { + super(cachePath); + logger.debug("[PstCache] constructor", { cachePath }); + this.db = new Level( + path.resolve(this.cachePath, CACHE_FOLDER_NAME), + defaultDbOptions + ); + logger.debug("[PstCache] DB", { + db: this.db, + status: this.db.status, + }); } @SoftLockDb @@ -155,14 +173,14 @@ export class PstCache { return ret; } - public openForPst(pstId: string): void { - this.currrentPstID = pstId; + public async close(): pvoid { + return this.db.close(); } private getCurrentPstDb() { - if (!this.currrentPstID) throw new Error("No PST cache opened yet."); + if (!this.currentPstID) throw new Error("No PST cache opened yet."); return this.db.sublevel( - this.currrentPstID, + this.currentPstID, defaultDbOptions ); } @@ -176,7 +194,7 @@ export class PstCache { private getCurrentAdditionalDatasDb() { return this.getCurrentPstDb().sublevel( - ADDITIONNAL_DATES_DB_PREFIX, + ADDITIONNAL_DATAS_DB_PREFIX, defaultDbOptions ); } diff --git a/src/main/modules/menu/DebugMenu.ts b/src/main/modules/menu/DebugMenu.ts index 8eda8e4c..a0da5408 100644 --- a/src/main/modules/menu/DebugMenu.ts +++ b/src/main/modules/menu/DebugMenu.ts @@ -1,5 +1,6 @@ import { IS_PACKAGED } from "@common/config"; import { SupportedLocales } from "@common/i18n/raw"; +import { logger } from "@common/logger"; import type { ExporterType, FileExporterService, @@ -10,7 +11,6 @@ import type { BrowserWindow, MenuItemConstructorOptions } from "electron"; import { dialog, MenuItem } from "electron"; import { t } from "i18next"; -import type { ConsoleToRendererService } from "../../services/ConsoleToRendererService"; // eslint-disable-next-line unused-imports/no-unused-imports -- MenuModule used in doc import type { ArchifiltreMailsMenu, MenuModule } from "../MenuModule"; import type { PstExtractorMainService } from "../PstExtractorModule"; @@ -33,7 +33,6 @@ export class DebugMenu implements ArchifiltreMailsMenu { private lastPstFilePath = ""; constructor( - private readonly consoleToRendererService: ConsoleToRendererService, private readonly pstExtractorMainService: PstExtractorMainService, private readonly i18nService: I18nService, private readonly fileExporterService: FileExporterService, @@ -103,11 +102,7 @@ export class DebugMenu implements ArchifiltreMailsMenu { } private readonly onClickOpenLogPST: MenuItemConstructorOptions["click"] = - async (_menuItem, browserWindow, _event) => { - if (!browserWindow) { - return; - } - + async () => { const dialogReturn = await dialog.showOpenDialog({ filters: [ { @@ -127,7 +122,7 @@ export class DebugMenu implements ArchifiltreMailsMenu { if (pstFilePath) { disableMenus(this.id); - await this.extractAndLogPst(browserWindow, pstFilePath); + await this.extractAndLogPst(pstFilePath); enableMenus( this.id, EXPORT_LAST_PST_MENU_ID, @@ -137,27 +132,18 @@ export class DebugMenu implements ArchifiltreMailsMenu { }; private readonly onClickOpenLogLastPST: MenuItemConstructorOptions["click"] = - async (_menuItem, browserWindow, _event) => { - if (this.lastPstFilePath && browserWindow) { - this.consoleToRendererService.log( - browserWindow, - `Open last PST file: ${this.lastPstFilePath}` - ); - await this.extractAndLogPst( - browserWindow, - this.lastPstFilePath - ); + async () => { + if (this.lastPstFilePath) { + logger.log(`Open last PST file: ${this.lastPstFilePath}`); + await this.extractAndLogPst(this.lastPstFilePath); } }; - private async extractAndLogPst( - browserWindow: BrowserWindow, - pstFilePath: string - ): pvoid { + private async extractAndLogPst(pstFilePath: string): pvoid { const extractDatas = await this.pstExtractorMainService.extract({ pstFilePath, }); - this.consoleToRendererService.log(browserWindow, extractDatas); + logger.log(extractDatas); } private async exportLast( @@ -182,7 +168,7 @@ export class DebugMenu implements ArchifiltreMailsMenu { } disableMenus(this.id); - const extractDatas = await this.pstExtractorMainService.extract({ + const _extractDatas = await this.pstExtractorMainService.extract({ pstFilePath: this.lastPstFilePath, }); @@ -193,6 +179,6 @@ export class DebugMenu implements ArchifiltreMailsMenu { // emails, dialogReturn.filePath ); - console.info("MENU EXPORT DONE"); + logger.info("MENU EXPORT DONE"); } } diff --git a/src/main/modules/menu/utils.ts b/src/main/modules/menu/utils.ts index b4a9a9ad..b5237adf 100644 --- a/src/main/modules/menu/utils.ts +++ b/src/main/modules/menu/utils.ts @@ -1,10 +1,11 @@ +import { logger } from "@common/logger"; import { Menu } from "electron"; const switchEnableMenus = (enable: boolean, ...ids: string[]): void => { for (const id of ids) { const menu = Menu.getApplicationMenu()?.getMenuItemById(id); if (menu) { - console.log(enable ? "enable" : "disable", id); + logger.log(enable ? "enable" : "disable", id); menu.enabled = enable; } } diff --git a/src/main/modules/pst-extractor/pst-email-fetcher.worker.ts b/src/main/modules/pst-extractor/pst-email-fetcher.worker.ts index d8ed0d50..2024eeaa 100644 --- a/src/main/modules/pst-extractor/pst-email-fetcher.worker.ts +++ b/src/main/modules/pst-extractor/pst-email-fetcher.worker.ts @@ -1,3 +1,4 @@ +import { logger } from "@common/logger"; import type { PstAttachment as PstAttachment, PstEmail, @@ -22,7 +23,7 @@ import type { } from "../../workers/type"; import { Ack } from "../../workers/type"; import { WorkerServer } from "../../workers/WorkerServer"; -import { PstCache } from "./PstCache"; +import { CacheModule } from "../CacheModule"; type Commands = WorkerCommandsBuilder<{ open: { @@ -50,9 +51,9 @@ export type FetchWorkerConfig = WorkerConfigBuilder<{ queries: Queries; }>; -const pstCache = new PstCache(); -void pstCache.db.close(); -const server = new WorkerServer(); +const pstCache = CacheModule.getCacheService(); +void pstCache.close(); +const server = WorkerServer.getInstance(); let pool: Pool | null = null; let buf: Buffer | null = null; @@ -122,6 +123,7 @@ const parallelIndexesProcess = async ( if (!pool) { throw new Error("No pst file opened yet."); } + // eslint-disable-next-line no-console console.time("==FETCH MAILS"); const emails = await Promise.all( emailIndexes.map(async (emailIndex) => { @@ -135,12 +137,13 @@ const parallelIndexesProcess = async ( return email; } catch (error: unknown) { if (error instanceof TimeoutError) { - console.error("Pool timeout error mega fail..."); + logger.error("Pool timeout error mega fail..."); } throw error; } }) ); + // eslint-disable-next-line no-console console.timeEnd("==FETCH MAILS"); return emails; diff --git a/src/main/modules/pst-extractor/pst-extractor.worker.ts b/src/main/modules/pst-extractor/pst-extractor.worker.ts index 9c75a704..3218f643 100644 --- a/src/main/modules/pst-extractor/pst-extractor.worker.ts +++ b/src/main/modules/pst-extractor/pst-extractor.worker.ts @@ -23,7 +23,7 @@ import type { } from "../../workers/type"; import { Ack } from "../../workers/type"; import { WorkerServer } from "../../workers/WorkerServer"; -import { PstCache } from "./PstCache"; +import { CacheModule } from "../CacheModule"; // import { randomUUID } from "crypto"; let ID = 0; @@ -62,9 +62,9 @@ export type ExtractorWorkerConfig = WorkerConfigBuilder<{ eventListeners: EventListeners; }>; -const pstCache = new PstCache(); -void pstCache.db.close(); -const server = new WorkerServer(); +const pstCache = CacheModule.getCacheService(); +void pstCache.close(); +const server = WorkerServer.getInstance(); let pstFile: PSTFile | null = null; server.onCommand("open", async ({ pstFilePath }) => { @@ -78,9 +78,9 @@ server.onCommand("open", async ({ pstFilePath }) => { return Ack.Resolve(); }); -let stop = false; +let _stop = false; server.onCommand("stop", async () => { - stop = true; + _stop = true; return Ack.Resolve(); }); diff --git a/src/main/preload.ts b/src/main/preload.ts index 5536504e..c4bc1820 100644 --- a/src/main/preload.ts +++ b/src/main/preload.ts @@ -6,4 +6,5 @@ sourceMapSupport.install(); module.hot?.accept(); +// eslint-disable-next-line no-console console.info("[Preload] Inited"); diff --git a/src/main/services/ConsoleToRendererService.ts b/src/main/services/ConsoleToRendererService.ts deleted file mode 100644 index 40fdcd4c..00000000 --- a/src/main/services/ConsoleToRendererService.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { CONSOLE_LOG_EVENT } from "@common/constant/event"; -import type { Service } from "@common/modules/container/type"; -import type { BrowserWindow } from "electron"; - -export interface ConsoleToRendererService extends Service { - /** - * Log in the renderer process via the given browser window - */ - log: (browserWindow: BrowserWindow, ...args: unknown[]) => void; -} - -/** - * Simple back service that send console.log's to renderer. - */ -export const consoleToRendererService: ConsoleToRendererService = { - log(browserWindow: BrowserWindow, ...args) { - browserWindow.webContents.send(CONSOLE_LOG_EVENT, ...args); - }, - name: "ConsoleToRendererService", -}; diff --git a/src/main/services/serviceType.d.ts b/src/main/services/serviceType.d.ts index 88b36b59..c7934912 100644 --- a/src/main/services/serviceType.d.ts +++ b/src/main/services/serviceType.d.ts @@ -1,14 +1,10 @@ import type { MainWindowRetriever } from ".."; -import type { - PstCacheMainService, - PstExtractorMainService, -} from "../modules/PstExtractorModule"; -import type { ConsoleToRendererService } from "./ConsoleToRendererService"; +import type { PstCacheMainService } from "../modules/CacheModule"; +import type { PstExtractorMainService } from "../modules/PstExtractorModule"; // add "main-only" services for autocomplete declare module "@common/modules/container/type" { interface ServicesKeyType { - consoleToRendererService: ConsoleToRendererService; mainWindowRetriever: MainWindowRetriever; pstCacheMainService: PstCacheMainService; pstExtractorMainService: PstExtractorMainService; diff --git a/src/main/workers/WorkerClient.ts b/src/main/workers/WorkerClient.ts index 24369388..6f71bcc6 100644 --- a/src/main/workers/WorkerClient.ts +++ b/src/main/workers/WorkerClient.ts @@ -1,4 +1,5 @@ import { WORKER_CONFIG_TOKEN, workerConfig } from "@common/config"; +import { logger } from "@common/logger"; import type { SimpleObject, StringKeyOf, @@ -79,6 +80,11 @@ export class WorkerClient { ...(workerData ?? {}), [WORKER_CONFIG_TOKEN]: workerConfig, }; + logger.info( + "[WorkerClient] Create worker", + this.workerPath, + this.workerData + ); this.worker = new TSWorker(workerPath, { stderr: true, workerData: this.workerData, @@ -105,6 +111,10 @@ export class WorkerClient { } } ); + + this.worker.on("error", (err) => { + logger.error("[WorkerClient] Internal worker error", err); + }); } public command: CommandFunction> = diff --git a/src/main/workers/WorkerServer.ts b/src/main/workers/WorkerServer.ts index bdb8e43e..39ba8f3a 100644 --- a/src/main/workers/WorkerServer.ts +++ b/src/main/workers/WorkerServer.ts @@ -49,13 +49,15 @@ type TriggerFunction = < ) => void; export class WorkerServer { + private static INSTANCE: WorkerServer | null = null; + public workerData = workerData as TWorkerConfig["data"] & WorkerAppConfig; private readonly callbackPool = new Map(); private readonly subscribersPool = new Map(); - constructor() { + private constructor() { if (isMainThread) { throw new Error("Worker server should not be in main thread."); } @@ -97,6 +99,7 @@ export class WorkerServer { }, }); } catch (error: unknown) { + // eslint-disable-next-line no-console console.log(`[WorkerServer] Error in ${type}`, { data: error, event: "error", @@ -105,6 +108,7 @@ export class WorkerServer { type, }, }); + // eslint-disable-next-line no-console console.error(error); parentPort?.postMessage({ data: error, @@ -120,6 +124,19 @@ export class WorkerServer { ); } + /** + * Get a single server instance per worker. + */ + public static getInstance< + TWorkerConfig extends WorkerConfig + >(): WorkerServer { + if (!this.INSTANCE) { + this.INSTANCE = new WorkerServer(); + } + + return this.INSTANCE as unknown as WorkerServer; + } + public onCommand: OnCommandFunction< NonNullable > = (name, callback) => { diff --git a/src/renderer/index.tsx b/src/renderer/index.tsx index 9157a3d6..689df7fd 100644 --- a/src/renderer/index.tsx +++ b/src/renderer/index.tsx @@ -1,17 +1,16 @@ import "@common/utils/overload"; +import "@common/logger"; import { IS_DEV, PRODUCT_CHANNEL } from "@common/config"; import { getIsomorphicModules } from "@common/lib/core/isomorphic"; import { ipcRenderer } from "@common/lib/ipc"; import { loadModules, unloadModules } from "@common/lib/ModuleManager"; -import type { Module } from "@common/modules/Module"; import { setupSentry } from "@common/monitoring/sentry"; import { version } from "@common/utils/package"; import React from "react"; import { render } from "react-dom"; import { App } from "./app"; -import { ConsoleFromMainModule } from "./modules/ConsoleFromMainModule"; import { pstExporterService } from "./services/PstExporterService"; import { pstExtractorService } from "./services/PstExtractorService"; @@ -26,14 +25,10 @@ void (async () => { ["pstExtractorService", pstExtractorService], ["pstExporterService", pstExporterService] ); - const modules: Module[] = [ - ...isomorphicModules, - new ConsoleFromMainModule(), - ]; window.addEventListener("beforeunload", async () => - unloadModules(...modules) + unloadModules(...isomorphicModules) ); - await loadModules(...modules); + await loadModules(...isomorphicModules); setupSentryIntegrations(); render(, document.querySelector("#app")); @@ -41,8 +36,7 @@ void (async () => { if (IS_DEV) { window["_archifiltre-debug"] = { ipcRenderer, - pstExporterService, - pstExtractorService, + modules: isomorphicModules, }; } })(); diff --git a/src/renderer/modules/ConsoleFromMainModule.ts b/src/renderer/modules/ConsoleFromMainModule.ts deleted file mode 100644 index a13e3bb5..00000000 --- a/src/renderer/modules/ConsoleFromMainModule.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { CONSOLE_LOG_EVENT } from "@common/constant/event"; -import type { Module } from "@common/modules/Module"; -import { ipcRenderer } from "electron"; - -/** - * Expose an ipc channel to give a way to properly console log from main into renderer. - */ -export class ConsoleFromMainModule implements Module { - public async init(): pvoid { - ipcRenderer.on(CONSOLE_LOG_EVENT, (_event, ...args: unknown[]) => { - console.log(...args); - }); - - return Promise.resolve(); - } - - public async uninit(): pvoid { - return Promise.resolve(); - } -} diff --git a/tests/integration/mock/electron.ts b/tests/integration/mock/electron.ts index 0cac4324..8d400166 100644 --- a/tests/integration/mock/electron.ts +++ b/tests/integration/mock/electron.ts @@ -1,3 +1,4 @@ +import type { ElectronLog } from "electron-log"; import path from "path"; process.env.NODE_ENV = "test-jest"; @@ -22,3 +23,18 @@ jest.mock( virtual: true, } ); + +jest.mock( + "electron-log", + (): Partial => { + return { + create(_name) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return console as Any; + }, + }; + }, + { + virtual: true, + } +); diff --git a/webpack.common.config.js b/webpack.common.config.js index c203e935..a06544ed 100644 --- a/webpack.common.config.js +++ b/webpack.common.config.js @@ -23,6 +23,7 @@ module.exports = if (isProd) { config.plugins.push( new webpack.EnvironmentPlugin([ + "CACHE_PROVIDER", "TRACKER_MATOMO_ID_SITE", "TRACKER_MATOMO_URL", "TRACKER_PROVIDER", diff --git a/webpack.main.config.js b/webpack.main.config.js index be0899b9..90dcfcc6 100644 --- a/webpack.main.config.js +++ b/webpack.main.config.js @@ -1,45 +1,31 @@ -const path = require("path"); -const glob = require("glob"); const webpack = require("webpack"); const webpackCommonConfig = require("./webpack.common.config"); +const { workers } = require("./webpack.workers.config"); const HtmlWebpackPlugin = require("html-webpack-plugin"); require("dotenv").config(); module.exports = /** @param {import("webpack").Configuration} config */ function (config) { - const workers = glob - .sync("./src/main/**/*.worker.ts") - .map((filePath) => { - const name = path.basename(filePath, path.extname(filePath)); - return [ - name, - path.dirname(filePath).split("src/main/")[1], - filePath, - ]; - }) - .reduce((acc, [name, directoryPath, filePath]) => { - acc[path.join(directoryPath, name)] = filePath; - return acc; - }, {}); - if (config.mode === "production") { for (const plugin of config.plugins) { if (plugin instanceof webpack.BannerPlugin) { - plugin.options.exclude = /(preload|\.worker)\.js$/i; + plugin.options.exclude = /preload\.js$/i; } } - } - for (const plugin of config.plugins) { - if (plugin instanceof HtmlWebpackPlugin) { - plugin.options?.excludeChunks?.push(...Object.keys(workers)); + } else { + // add workers in dev + for (const plugin of config.plugins) { + if (plugin instanceof HtmlWebpackPlugin) { + plugin.options?.excludeChunks?.push(...Object.keys(workers)); + } } - } - if (config.entry) { - config.entry = { - ...config.entry, - ...workers, - }; + if (config.entry) { + config.entry = { + ...config.entry, + ...workers, + }; + } } return webpackCommonConfig(config); diff --git a/webpack.workers.config.js b/webpack.workers.config.js new file mode 100644 index 00000000..fd506b00 --- /dev/null +++ b/webpack.workers.config.js @@ -0,0 +1,82 @@ +const webpackMain = require("electron-webpack/webpack.main.config"); +const webpack = require("webpack"); +const webpackCommonConfig = require("./webpack.common.config"); +const { glob } = require("glob"); +const path = require("path"); +const HtmlWebpackPlugin = require("html-webpack-plugin"); + +const workers = glob + .sync("./src/main/**/*.worker.ts") + .map((filePath) => { + const name = path.basename(filePath, path.extname(filePath)); + return [name, path.dirname(filePath).split("src/main/")[1], filePath]; + }) + .reduce((acc, [name, directoryPath, filePath]) => { + console.log({ directoryPath, filePath, name }); + acc[path.join(directoryPath, name)] = filePath; + return acc; + }, {}); + +module.exports = async ( + /** @type {import("electron-webpack/out/core").ConfigurationEnv} */ env = {} +) => { + env.production = true; + env.autoClean = false; + const config = await webpackMain(env); + if (!config) { + throw new Error("No main config found for workers."); + } + + config.mode = "production"; + + for (const plugin of config.plugins) { + if (plugin instanceof webpack.BannerPlugin) { + plugin.options.exclude = /(\.worker)\.js$/i; + } + if (plugin instanceof HtmlWebpackPlugin) { + plugin.options?.excludeChunks?.push(...Object.keys(workers)); + } + } + + config.module.rules.push({ + loader: "string-replace-loader", + options: { + replace: `(() => { +const path = require("path"); +const IS_PACKAGED = __dirname.indexOf(path.join("resources/workers/")) > -1; + +return path.resolve(process.cwd(), IS_PACKAGED ? "resources/natives/" : "node_modules/", "classic-level/"); + })()`, + search: "__dirname", + }, + test: /node_modules\/classic-level\/binding\.js$/, + }); + + const shouldNotBeExternal = [ + "@lsagetlethias/tstrait", + "@socialgouv/archimail-pst-extractor", + "classic-level", + "electron-log", + "electron-store", + "electron-util", + "i18next", + "lodash", + "posthog-js", + "posthog-node", + "source-map-support", + "tarn", + "uuid", + ]; + + if (Array.isArray(config.externals)) { + config.externals = config.externals.filter( + (name) => !shouldNotBeExternal.includes(name) + ); + } + + config.entry = workers; + + return webpackCommonConfig(config); +}; + +module.exports.workers = workers; diff --git a/yarn.lock b/yarn.lock index acfe9496..b9c1ff9a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4459,16 +4459,6 @@ brorand@^1.0.1, brorand@^1.1.0: resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f" integrity sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8= -browser-level@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/browser-level/-/browser-level-1.0.1.tgz#36e8c3183d0fe1c405239792faaab5f315871011" - integrity sha512-XECYKJ+Dbzw0lbydyQuJzwNXtOpbMSq737qxJN11sIRTErOMShvDpbzTlgju7orJKvx4epULolZAuJGLzCmWRQ== - dependencies: - abstract-level "^1.0.2" - catering "^2.1.1" - module-error "^1.0.2" - run-parallel-limit "^1.1.0" - browser-process-hrtime@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz#3c9b4b7d782c8121e56f10106d84c0d0ffc94626" @@ -4865,7 +4855,7 @@ cardinal@^2.1.1: ansicolors "~0.3.2" redeyed "~2.1.0" -catering@^2.1.0, catering@^2.1.1: +catering@^2.1.0: version "2.1.1" resolved "https://registry.yarnpkg.com/catering/-/catering-2.1.1.tgz#66acba06ed5ee28d5286133982a927de9a04b510" integrity sha512-K7Qy8O9p76sL3/3m7/zLKbRkyOlSZAgzEaLhyj2mXS8PsCud2Eo4hAb8aLtZqHh0QGqLcb9dlJSu6lHRVENm1w== @@ -6601,6 +6591,11 @@ electron-is-dev@^1.1.0: resolved "https://registry.yarnpkg.com/electron-is-dev/-/electron-is-dev-1.2.0.tgz#2e5cea0a1b3ccf1c86f577cee77363ef55deb05e" integrity sha512-R1oD5gMBPS7PVU8gJwH6CtT0e6VSoD0+SzSnYpNm+dBkcijgA+K7VAMHDfnRq/lkKPZArpzplTW6jfiMYosdzw== +electron-log@^4.4.8: + version "4.4.8" + resolved "https://registry.yarnpkg.com/electron-log/-/electron-log-4.4.8.tgz#fcb9f714dbcaefb6ac7984c4683912c74730248a" + integrity sha512-QQ4GvrXO+HkgqqEOYbi+DHL7hj5JM+nHi/j+qrN9zeeXVKy8ZABgbu4CnG+BBqDZ2+tbeq9tUC4DZfIWFU5AZA== + electron-notarize@^1.1.1: version "1.2.1" resolved "https://registry.yarnpkg.com/electron-notarize/-/electron-notarize-1.2.1.tgz#347c18eca8e29dddadadee511b870c13d4008baf" @@ -10200,14 +10195,6 @@ level-transcoder@^1.0.1: buffer "^6.0.3" module-error "^1.0.1" -level@^8.0.0: - version "8.0.0" - resolved "https://registry.yarnpkg.com/level/-/level-8.0.0.tgz#41b4c515dabe28212a3e881b61c161ffead14394" - integrity sha512-ypf0jjAk2BWI33yzEaaotpq7fkOPALKAgDBxggO6Q9HGX2MRXn0wbP1Jn/tJv1gtL867+YOjOB49WaUF3UoJNQ== - dependencies: - browser-level "^1.0.1" - classic-level "^1.2.0" - leven@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2" @@ -11006,7 +10993,7 @@ modify-values@^1.0.0: resolved "https://registry.yarnpkg.com/modify-values/-/modify-values-1.0.1.tgz#b3939fa605546474e3e3e3c63d64bd43b4ee6022" integrity sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw== -module-error@^1.0.1, module-error@^1.0.2: +module-error@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/module-error/-/module-error-1.0.2.tgz#8d1a48897ca883f47a45816d4fb3e3c6ba404d86" integrity sha512-0yuvsqSCv8LbaOKhnsQ/T5JhyFlCYLPXK3U2sgV10zoKQwzs/MyfuQUOZQ1V/6OCOJsK/TRgNVrPuPDqtdMFtA== @@ -13204,13 +13191,6 @@ rrweb-snapshot@^1.1.14: resolved "https://registry.yarnpkg.com/rrweb-snapshot/-/rrweb-snapshot-1.1.14.tgz#9d4d9be54a28a893373428ee4393ec7e5bd83fcc" integrity sha512-eP5pirNjP5+GewQfcOQY4uBiDnpqxNRc65yKPW0eSoU1XamDfc4M8oqpXGMyUyvLyxFDB0q0+DChuxxiU2FXBQ== -run-parallel-limit@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/run-parallel-limit/-/run-parallel-limit-1.1.0.tgz#be80e936f5768623a38a963262d6bef8ff11e7ba" - integrity sha512-jJA7irRNM91jaKc3Hcl1npHsFLOXOoTkPCUL1JEa1R82O2miplXXRaGdjW/KM/98YQWDhJLiSs793CnXfblJUw== - dependencies: - queue-microtask "^1.2.2" - run-parallel@^1.1.9: version "1.2.0" resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" @@ -13970,6 +13950,14 @@ string-length@^4.0.1: char-regex "^1.0.2" strip-ansi "^6.0.0" +string-replace-loader@^2: + version "2.3.0" + resolved "https://registry.yarnpkg.com/string-replace-loader/-/string-replace-loader-2.3.0.tgz#7f29be7d73c94dd92eccd5c5a15651181d7ecd3d" + integrity sha512-HYBIHStViMKLZC/Lehxy42OuwsBaPzX/LjcF5mkJlE2SnHXmW6SW6eiHABTXnY8ZCm/REbdJ8qnA0ptmIzN0Ng== + dependencies: + loader-utils "^1.2.3" + schema-utils "^2.6.5" + string-width@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3"