From 9dcfbce1a82230a17fa75e22f4a94bc76f2ec2cb Mon Sep 17 00:00:00 2001 From: Marcin Cichocki Date: Thu, 16 Sep 2021 15:21:44 +0200 Subject: [PATCH] fix(client-electron): include every 3rd party license fixes #205 --- configs/common.ts | 108 +++++--- configs/webpack.config.main.ts | 31 +-- configs/webpack.config.preload.ts | 31 +-- configs/webpack.config.renderer.ts | 35 ++- configs/webpack.config.worker.ts | 47 ++-- package-lock.json | 241 ++++++++++-------- package.json | 11 +- src/electron/common/common.ts | 10 + src/electron/main/main.ts | 77 +++++- src/electron/renderer/app.tsx | 2 + src/electron/renderer/common.ts | 13 + src/electron/renderer/components/Dialog.tsx | 24 +- .../components/ReleaseNotesDialog.tsx | 59 +---- .../components/ThirdPartyLicensesDialog.tsx | 68 +++++ src/electron/renderer/components/TitleBar.tsx | 1 + src/electron/renderer/components/index.ts | 1 + src/electron/renderer/preload/ipc.ts | 1 + src/electron/renderer/preload/shell.ts | 16 +- src/electron/worker/dialog.ts | 2 +- src/electron/worker/worker.ts | 10 +- 20 files changed, 478 insertions(+), 310 deletions(-) create mode 100644 src/electron/renderer/components/ThirdPartyLicensesDialog.tsx diff --git a/configs/common.ts b/configs/common.ts index edff4017..56fb9f45 100644 --- a/configs/common.ts +++ b/configs/common.ts @@ -1,6 +1,7 @@ import { execSync } from 'child_process'; -import { LicenseWebpackPlugin } from 'license-webpack-plugin'; -import { DefinePlugin, RuleSetRule, WebpackPluginInstance } from 'webpack'; +import { join, resolve } from 'path'; +import TsconfigPathsPlugin from 'tsconfig-paths-webpack-plugin'; +import { Configuration, DefinePlugin } from 'webpack'; function git(command: string) { return execSync(`git ${command}`, { encoding: 'utf-8' }).trim(); @@ -8,39 +9,18 @@ function git(command: string) { const pkg = require('../package.json'); -export const commonPlugins: WebpackPluginInstance[] = [ - new DefinePlugin({ - GIT_COMMIT_DATE: JSON.stringify( - git('log -1 --format=%cd --date=iso-strict') - ), - GIT_COMMIT_SHA: JSON.stringify(git('rev-parse HEAD')), - VERSION: JSON.stringify(pkg.version), - HOMEPAGE_URL: JSON.stringify(pkg.homepage), - BUGS_URL: JSON.stringify(pkg.bugs), - PRODUCT_NAME: JSON.stringify(pkg.build.productName), - APP_ID: JSON.stringify(pkg.build.appId), - BUILD_PLATFORM: JSON.stringify(process.platform), - }), - new LicenseWebpackPlugin({ - outputFilename: 'THIRD_PARTY_LICENSES.txt', - }) as any, -]; - -export const commonRules: RuleSetRule[] = [ - { - test: /\.tsx?$/, - exclude: /node_modules/, - loader: 'ts-loader', - }, - { - test: /\.(ttf|svg|png)$/, - type: 'asset/resource', - }, - { - test: /\.css$/, - use: ['style-loader', 'css-loader'], - }, -]; +export const defineConstantsPlugin = new DefinePlugin({ + GIT_COMMIT_DATE: JSON.stringify(git('log -1 --format=%cd --date=iso-strict')), + GIT_COMMIT_SHA: JSON.stringify(git('rev-parse HEAD')), + VERSION: JSON.stringify(pkg.version), + HOMEPAGE_URL: JSON.stringify(pkg.homepage), + BUGS_URL: JSON.stringify(pkg.bugs), + PRODUCT_NAME: JSON.stringify(pkg.build.productName), + APP_ID: JSON.stringify(pkg.build.appId), + BUILD_PLATFORM: JSON.stringify(process.platform), +}); + +export const root = resolve(__dirname, '..'); export function getCSPMetaTagConfig(content: string) { return { @@ -48,3 +28,61 @@ export function getCSPMetaTagConfig(content: string) { content, }; } + +type ConfigurationCallback = ( + isProduction: boolean, + env: any, + options: any +) => Configuration; + +export function getConfig( + configOrCallback: Configuration | ConfigurationCallback +) { + return (env: any, options: any) => { + const defaultConfig: Configuration = { + mode: 'development', + context: join(root, './src/electron'), + output: { + path: join(root, 'dist'), + filename: '[name].js', + }, + resolve: { + extensions: ['.js', '.jsx', '.ts', '.tsx', '.json'], + plugins: [new TsconfigPathsPlugin()], + }, + module: { + rules: [ + { + test: /\.tsx?$/, + exclude: /node_modules/, + loader: 'ts-loader', + }, + { + test: /\.(ttf|svg|png)$/, + type: 'asset/resource', + }, + { + test: /\.css$/, + use: ['style-loader', 'css-loader'], + }, + ], + }, + optimization: { + // NOTE: This allow WebpackLicensePlugin to work with ESNext module, + // which is required for treeshaking. + concatenateModules: false, + }, + }; + + const isProduction = options.mode === 'production'; + const config = + typeof configOrCallback === 'function' + ? configOrCallback(isProduction, env, options) + : configOrCallback; + + return { + ...defaultConfig, + ...config, + }; + }; +} diff --git a/configs/webpack.config.main.ts b/configs/webpack.config.main.ts index fbf0b6b8..e315c535 100644 --- a/configs/webpack.config.main.ts +++ b/configs/webpack.config.main.ts @@ -1,22 +1,15 @@ -import { join } from 'path'; -import { TsconfigPathsPlugin } from 'tsconfig-paths-webpack-plugin'; -import webpack from 'webpack'; -import { commonPlugins, commonRules } from './common'; +import WebpackLicensePlugin from 'webpack-license-plugin'; +import { defineConstantsPlugin, getConfig } from './common'; -export const config: webpack.Configuration = { - mode: 'development', - entry: join(__dirname, '../src/electron/main/index.ts'), +export const config = getConfig({ target: 'electron-main', - resolve: { - extensions: ['.js', '.jsx', '.ts', '.tsx', '.json'], - plugins: [new TsconfigPathsPlugin()], + entry: { + main: './main/index.ts', }, - output: { - path: join(__dirname, '../dist'), - filename: 'main.js', - }, - module: { - rules: [...commonRules], - }, - plugins: [...commonPlugins], -}; + plugins: [ + defineConstantsPlugin, + new WebpackLicensePlugin({ + outputFilename: 'main-licenses.json', + }), + ], +}); diff --git a/configs/webpack.config.preload.ts b/configs/webpack.config.preload.ts index b2e1fab5..33bd17a0 100644 --- a/configs/webpack.config.preload.ts +++ b/configs/webpack.config.preload.ts @@ -1,22 +1,15 @@ -import { join } from 'path'; -import { TsconfigPathsPlugin } from 'tsconfig-paths-webpack-plugin'; -import webpack from 'webpack'; -import { commonPlugins, commonRules } from './common'; +import WebpackLicensePlugin from 'webpack-license-plugin'; +import { defineConstantsPlugin, getConfig } from './common'; -export const config: webpack.Configuration = { - mode: 'development', - entry: join(__dirname, '../src/electron/renderer/preload/index.ts'), +export const config = getConfig({ target: 'electron-preload', - output: { - path: join(__dirname, '../dist'), - filename: 'preload.js', + entry: { + preload: './renderer/preload/index.ts', }, - resolve: { - extensions: ['.js', '.jsx', '.ts', '.tsx', '.json'], - plugins: [new TsconfigPathsPlugin()], - }, - module: { - rules: [...commonRules], - }, - plugins: [...commonPlugins], -}; + plugins: [ + defineConstantsPlugin, + new WebpackLicensePlugin({ + outputFilename: 'preload-licenses.json', + }), + ], +}); diff --git a/configs/webpack.config.renderer.ts b/configs/webpack.config.renderer.ts index 17f1a95a..694861e1 100644 --- a/configs/webpack.config.renderer.ts +++ b/configs/webpack.config.renderer.ts @@ -1,35 +1,30 @@ import HtmlWebpackPlugin from 'html-webpack-plugin'; -import { join } from 'path'; -import { TsconfigPathsPlugin } from 'tsconfig-paths-webpack-plugin'; -import webpack from 'webpack'; -import { commonPlugins, commonRules, getCSPMetaTagConfig } from './common'; +import WebpackLicensePlugin from 'webpack-license-plugin'; +import { + defineConstantsPlugin, + getConfig, + getCSPMetaTagConfig, +} from './common'; const pkg = require('../package.json'); const allowedSources = "default-src 'none'; img-src data:; script-src 'self' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; connect-src 'self' https:; font-src 'self';"; const csp = getCSPMetaTagConfig(allowedSources); -export const config: webpack.Configuration = { - mode: 'development', - entry: join(__dirname, '../src/electron/renderer/index.tsx'), +export const config = getConfig({ target: 'electron-renderer', - output: { - path: join(__dirname, '../dist'), - filename: 'renderer.js', - }, - resolve: { - extensions: ['.js', '.jsx', '.ts', '.tsx', '.json'], - plugins: [new TsconfigPathsPlugin()], - }, - module: { - rules: [...commonRules], + entry: { + renderer: './renderer/index.tsx', }, plugins: [ - ...commonPlugins, + defineConstantsPlugin, + new WebpackLicensePlugin({ + outputFilename: 'renderer-licenses.json', + }), new HtmlWebpackPlugin({ - filename: 'renderer.html', + filename: '[name].html', title: pkg.build.productName, meta: { csp }, }), ], -}; +}); diff --git a/configs/webpack.config.worker.ts b/configs/webpack.config.worker.ts index 46d9497b..433c5719 100644 --- a/configs/webpack.config.worker.ts +++ b/configs/webpack.config.worker.ts @@ -1,33 +1,32 @@ import HtmlWebpackPlugin from 'html-webpack-plugin'; import { join } from 'path'; -import { TsconfigPathsPlugin } from 'tsconfig-paths-webpack-plugin'; -import webpack from 'webpack'; -import { commonPlugins, commonRules } from './common'; +import WebpackLicensePlugin from 'webpack-license-plugin'; +import { defineConstantsPlugin, getConfig, root } from './common'; -export const config: webpack.Configuration = { - mode: 'development', - entry: join(__dirname, '../src/electron/worker/index.ts'), +const externalPackages = ['sharp', 'tesseract.js', 'screenshot-desktop']; +const externalEnteries = externalPackages.map((n) => [n, `commonjs ${n}`]); +const externals = Object.fromEntries(externalEnteries); + +function getAbsolutePathToExternalPackage(name: string) { + return join(root, 'node_modules', name); +} + +export const config = getConfig({ target: 'electron-renderer', - output: { - path: join(__dirname, '../dist'), - filename: 'worker.js', - }, - resolve: { - extensions: ['.js', '.jsx', '.ts', '.tsx', '.json'], - plugins: [new TsconfigPathsPlugin()], - }, - module: { - rules: [...commonRules], - }, - externals: { - sharp: 'commonjs sharp', - 'tesseract.js': 'commonjs tesseract.js', - 'screenshot-desktop': 'commonjs screenshot-desktop', + entry: { + worker: './worker/index.ts', }, + externals, plugins: [ - ...commonPlugins, + defineConstantsPlugin, + new WebpackLicensePlugin({ + outputFilename: 'worker-licenses.json', + includePackages() { + return externalPackages.map(getAbsolutePathToExternalPackage); + }, + }), new HtmlWebpackPlugin({ - filename: 'worker.html', + filename: '[name].html', }), ], -}; +}); diff --git a/package-lock.json b/package-lock.json index 42c93ed6..c60b9041 100644 --- a/package-lock.json +++ b/package-lock.json @@ -41,14 +41,12 @@ "@types/styled-components": "^5.1.9", "@types/tar": "^4.0.4", "@types/uuid": "^8.3.0", - "@types/webpack": "^5.28.0", "css-loader": "^5.2.4", "electron": "^13.1.2", "electron-builder": "^22.11.7", "fs-extra": "^9.1.0", "html-webpack-plugin": "^5.3.1", "jest": "^26.6.3", - "license-webpack-plugin": "^2.3.19", "patch-package": "^6.4.7", "standard-version": "^9.3.1", "style-loader": "^2.0.0", @@ -59,7 +57,8 @@ "tslib": "^2.1.0", "typescript": "^4.3.2", "webpack": "^5.35.1", - "webpack-cli": "^4.6.0" + "webpack-cli": "^4.6.0", + "webpack-license-plugin": "^4.2.0" } }, "node_modules/@babel/code-frame": { @@ -1369,12 +1368,6 @@ "@types/node": "*" } }, - "node_modules/@types/source-list-map": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/@types/source-list-map/-/source-list-map-0.1.2.tgz", - "integrity": "sha512-K5K+yml8LTo9bWJI/rECfIPrGgxdpeNbj+d53lwN4QjW1MCwlkhUms+gtdzigTeUyBr09+u8BwOIY3MXvHdcsA==", - "dev": true - }, "node_modules/@types/stack-utils": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.0.tgz", @@ -1421,28 +1414,6 @@ "dev": true, "optional": true }, - "node_modules/@types/webpack": { - "version": "5.28.0", - "resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-5.28.0.tgz", - "integrity": "sha512-8cP0CzcxUiFuA9xGJkfeVpqmWTk9nx6CWwamRGCj95ph1SmlRRk9KlCZ6avhCbZd4L68LvYT6l1kpdEnQXrF8w==", - "dev": true, - "dependencies": { - "@types/node": "*", - "tapable": "^2.2.0", - "webpack": "^5" - } - }, - "node_modules/@types/webpack-sources": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/@types/webpack-sources/-/webpack-sources-0.1.8.tgz", - "integrity": "sha512-JHB2/xZlXOjzjBB6fMOpH1eQAfsrpqVVIbneE0Rok16WXwFaznaI5vfg75U5WgGJm7V9W1c4xeRQDjX/zwvghA==", - "dev": true, - "dependencies": { - "@types/node": "*", - "@types/source-list-map": "*", - "source-map": "^0.6.1" - } - }, "node_modules/@types/yargs": { "version": "15.0.12", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.12.tgz", @@ -6125,6 +6096,18 @@ "node": "6.* || 8.* || >= 10.*" } }, + "node_modules/get-npm-tarball-url": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/get-npm-tarball-url/-/get-npm-tarball-url-2.0.2.tgz", + "integrity": "sha512-2dPhgT0K4pVyciTqdS0gr9nEwyCQwt9ql1/t5MCUMvcjWjAysjGJgT7Sx4n6oq3tFBjBN238mxX4RfTjT3838Q==", + "dev": true, + "dependencies": { + "normalize-registry-url": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/get-package-type": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", @@ -8535,31 +8518,6 @@ "node": ">= 0.8.0" } }, - "node_modules/license-webpack-plugin": { - "version": "2.3.19", - "resolved": "https://registry.npmjs.org/license-webpack-plugin/-/license-webpack-plugin-2.3.19.tgz", - "integrity": "sha512-z/izhwFRYHs1sCrDgrTUsNJpd+Xsd06OcFWSwHz/TiZygm5ucweVZi1Hu14Rf6tOj/XAl1Ebyc7GW6ZyyINyWA==", - "dev": true, - "dependencies": { - "@types/webpack-sources": "^0.1.5", - "webpack-sources": "^1.2.0" - }, - "peerDependenciesMeta": { - "webpack": { - "optional": true - } - } - }, - "node_modules/license-webpack-plugin/node_modules/webpack-sources": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.4.3.tgz", - "integrity": "sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==", - "dev": true, - "dependencies": { - "source-list-map": "^2.0.0", - "source-map": "~0.6.1" - } - }, "node_modules/lines-and-columns": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz", @@ -9177,6 +9135,32 @@ "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", "dev": true }, + "node_modules/needle": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/needle/-/needle-2.9.1.tgz", + "integrity": "sha512-6R9fqJ5Zcmf+uYaFgdIHmLwNldn5HbK8L5ybn7Uz+ylX/rnOsSp1AHcvQSrCaFN+qNM1wpymHqD7mVasEOlHGQ==", + "dev": true, + "dependencies": { + "debug": "^3.2.6", + "iconv-lite": "^0.4.4", + "sax": "^1.2.4" + }, + "bin": { + "needle": "bin/needle" + }, + "engines": { + "node": ">= 4.4.x" + } + }, + "node_modules/needle/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, "node_modules/neo-async": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", @@ -9309,6 +9293,12 @@ "node": ">=0.10.0" } }, + "node_modules/normalize-registry-url": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/normalize-registry-url/-/normalize-registry-url-1.0.0.tgz", + "integrity": "sha512-0v6T4851b72ykk5zEtFoN4QX/Fqyk7pouIj9xZyAvAe9jlDhAwT4z6FlwsoQCHjeuK2EGUoAwy/F4y4B1uZq9A==", + "dev": true + }, "node_modules/normalize-url": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.0.tgz", @@ -11680,6 +11670,15 @@ "spdx-license-ids": "^3.0.0" } }, + "node_modules/spdx-expression-validate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/spdx-expression-validate/-/spdx-expression-validate-2.0.0.tgz", + "integrity": "sha512-b3wydZLM+Tc6CFvaRDBOF9d76oGIHNCLYFeHbftFXUWjnfZWganmDmvtM5sm1cRwJc/VDBMLyGGrsLFd1vOxbg==", + "dev": true, + "dependencies": { + "spdx-expression-parse": "^3.0.0" + } + }, "node_modules/spdx-license-ids": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.7.tgz", @@ -13539,6 +13538,26 @@ "node": ">= 8" } }, + "node_modules/webpack-license-plugin": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/webpack-license-plugin/-/webpack-license-plugin-4.2.0.tgz", + "integrity": "sha512-uWcHEK6lQk6w5NcRWQIktlO30OMnHnp4JonwMcaHKAR+qPgjJ/SxKBiyQpmJE4nR6kqJADYAutSV8zkK/wLR3g==", + "dev": true, + "dependencies": { + "chalk": "^4.1.0", + "get-npm-tarball-url": "^2.0.1", + "lodash": "^4.17.20", + "needle": "^2.2.4", + "spdx-expression-validate": "^2.0.0", + "webpack-sources": "^2.0.0" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "webpack": ">=4.0.0 < 6.0.0" + } + }, "node_modules/webpack-merge": { "version": "5.7.3", "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.7.3.tgz", @@ -15045,12 +15064,6 @@ "@types/node": "*" } }, - "@types/source-list-map": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/@types/source-list-map/-/source-list-map-0.1.2.tgz", - "integrity": "sha512-K5K+yml8LTo9bWJI/rECfIPrGgxdpeNbj+d53lwN4QjW1MCwlkhUms+gtdzigTeUyBr09+u8BwOIY3MXvHdcsA==", - "dev": true - }, "@types/stack-utils": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.0.tgz", @@ -15097,28 +15110,6 @@ "dev": true, "optional": true }, - "@types/webpack": { - "version": "5.28.0", - "resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-5.28.0.tgz", - "integrity": "sha512-8cP0CzcxUiFuA9xGJkfeVpqmWTk9nx6CWwamRGCj95ph1SmlRRk9KlCZ6avhCbZd4L68LvYT6l1kpdEnQXrF8w==", - "dev": true, - "requires": { - "@types/node": "*", - "tapable": "^2.2.0", - "webpack": "^5" - } - }, - "@types/webpack-sources": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/@types/webpack-sources/-/webpack-sources-0.1.8.tgz", - "integrity": "sha512-JHB2/xZlXOjzjBB6fMOpH1eQAfsrpqVVIbneE0Rok16WXwFaznaI5vfg75U5WgGJm7V9W1c4xeRQDjX/zwvghA==", - "dev": true, - "requires": { - "@types/node": "*", - "@types/source-list-map": "*", - "source-map": "^0.6.1" - } - }, "@types/yargs": { "version": "15.0.12", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.12.tgz", @@ -18816,6 +18807,15 @@ "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "dev": true }, + "get-npm-tarball-url": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/get-npm-tarball-url/-/get-npm-tarball-url-2.0.2.tgz", + "integrity": "sha512-2dPhgT0K4pVyciTqdS0gr9nEwyCQwt9ql1/t5MCUMvcjWjAysjGJgT7Sx4n6oq3tFBjBN238mxX4RfTjT3838Q==", + "dev": true, + "requires": { + "normalize-registry-url": "^1.0.0" + } + }, "get-package-type": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", @@ -20694,28 +20694,6 @@ "type-check": "~0.3.2" } }, - "license-webpack-plugin": { - "version": "2.3.19", - "resolved": "https://registry.npmjs.org/license-webpack-plugin/-/license-webpack-plugin-2.3.19.tgz", - "integrity": "sha512-z/izhwFRYHs1sCrDgrTUsNJpd+Xsd06OcFWSwHz/TiZygm5ucweVZi1Hu14Rf6tOj/XAl1Ebyc7GW6ZyyINyWA==", - "dev": true, - "requires": { - "@types/webpack-sources": "^0.1.5", - "webpack-sources": "^1.2.0" - }, - "dependencies": { - "webpack-sources": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.4.3.tgz", - "integrity": "sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==", - "dev": true, - "requires": { - "source-list-map": "^2.0.0", - "source-map": "~0.6.1" - } - } - } - }, "lines-and-columns": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz", @@ -21186,6 +21164,28 @@ "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", "dev": true }, + "needle": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/needle/-/needle-2.9.1.tgz", + "integrity": "sha512-6R9fqJ5Zcmf+uYaFgdIHmLwNldn5HbK8L5ybn7Uz+ylX/rnOsSp1AHcvQSrCaFN+qNM1wpymHqD7mVasEOlHGQ==", + "dev": true, + "requires": { + "debug": "^3.2.6", + "iconv-lite": "^0.4.4", + "sax": "^1.2.4" + }, + "dependencies": { + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + } + } + }, "neo-async": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", @@ -21299,6 +21299,12 @@ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", "dev": true }, + "normalize-registry-url": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/normalize-registry-url/-/normalize-registry-url-1.0.0.tgz", + "integrity": "sha512-0v6T4851b72ykk5zEtFoN4QX/Fqyk7pouIj9xZyAvAe9jlDhAwT4z6FlwsoQCHjeuK2EGUoAwy/F4y4B1uZq9A==", + "dev": true + }, "normalize-url": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.0.tgz", @@ -23178,6 +23184,15 @@ "spdx-license-ids": "^3.0.0" } }, + "spdx-expression-validate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/spdx-expression-validate/-/spdx-expression-validate-2.0.0.tgz", + "integrity": "sha512-b3wydZLM+Tc6CFvaRDBOF9d76oGIHNCLYFeHbftFXUWjnfZWganmDmvtM5sm1cRwJc/VDBMLyGGrsLFd1vOxbg==", + "dev": true, + "requires": { + "spdx-expression-parse": "^3.0.0" + } + }, "spdx-license-ids": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.7.tgz", @@ -24591,6 +24606,20 @@ } } }, + "webpack-license-plugin": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/webpack-license-plugin/-/webpack-license-plugin-4.2.0.tgz", + "integrity": "sha512-uWcHEK6lQk6w5NcRWQIktlO30OMnHnp4JonwMcaHKAR+qPgjJ/SxKBiyQpmJE4nR6kqJADYAutSV8zkK/wLR3g==", + "dev": true, + "requires": { + "chalk": "^4.1.0", + "get-npm-tarball-url": "^2.0.1", + "lodash": "^4.17.20", + "needle": "^2.2.4", + "spdx-expression-validate": "^2.0.0", + "webpack-sources": "^2.0.0" + } + }, "webpack-merge": { "version": "5.7.3", "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.7.3.tgz", diff --git a/package.json b/package.json index e33aa6cd..5b36b880 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,11 @@ { "from": "dist", "to": "resources", - "filter": "*.txt" + "filter": "*.json" + }, + { + "from": "src/electron/renderer/assets/fonts/Rajdhani/OFL.txt", + "to": "resources" } ], "directories": { @@ -99,14 +103,12 @@ "@types/styled-components": "^5.1.9", "@types/tar": "^4.0.4", "@types/uuid": "^8.3.0", - "@types/webpack": "^5.28.0", "css-loader": "^5.2.4", "electron": "^13.1.2", "electron-builder": "^22.11.7", "fs-extra": "^9.1.0", "html-webpack-plugin": "^5.3.1", "jest": "^26.6.3", - "license-webpack-plugin": "^2.3.19", "patch-package": "^6.4.7", "standard-version": "^9.3.1", "style-loader": "^2.0.0", @@ -117,7 +119,8 @@ "tslib": "^2.1.0", "typescript": "^4.3.2", "webpack": "^5.35.1", - "webpack-cli": "^4.6.0" + "webpack-cli": "^4.6.0", + "webpack-license-plugin": "^4.2.0" }, "dependencies": { "@react-icons/all-files": "^4.1.0", diff --git a/src/electron/common/common.ts b/src/electron/common/common.ts index c119ca2f..9c995a9d 100644 --- a/src/electron/common/common.ts +++ b/src/electron/common/common.ts @@ -173,3 +173,13 @@ export abstract class NativeDialog { }); } } + +export interface PackageDetails { + name: string; + version: string; + author: string; + repository: string; + source: string; + license: string; + licenseText: string; +} diff --git a/src/electron/main/main.ts b/src/electron/main/main.ts index 66b9be1f..02844910 100644 --- a/src/electron/main/main.ts +++ b/src/electron/main/main.ts @@ -9,10 +9,17 @@ import { Tray, } from 'electron'; import firstRun from 'electron-first-run'; -import { copyFileSync, ensureDirSync, remove, writeJSONSync } from 'fs-extra'; +import { + copyFileSync, + ensureDirSync, + readJSON, + remove, + writeJSONSync, +} from 'fs-extra'; import { extname, join } from 'path'; +import { URL } from 'url'; import icon from '../../../resources/icon.png'; -import { Action, ActionTypes, WorkerStatus } from '../common'; +import { Action, ActionTypes, PackageDetails, WorkerStatus } from '../common'; import { Store } from './store/store'; import { BreachProtocolAutosolverUpdater } from './updater'; import { createBrowserWindows } from './windows'; @@ -31,6 +38,9 @@ export class Main { private readonly isFirstRun = firstRun({ name: 'update' }); + /** Only allow to externally open websites from this list. */ + private readonly originWhitelist = ['https://github.com']; + private helpMenuTemplate: Electron.MenuItemConstructorOptions[] = [ { label: 'About', @@ -47,15 +57,7 @@ export class Main { { type: 'separator' }, { label: 'Third-party licenses', - click() { - const licensesFileName = 'THIRD_PARTY_LICENSES.txt'; - const licensesPath = - process.env.NODE_ENV === 'production' - ? join('..', licensesFileName) - : licensesFileName; - - shell.showItemInFolder(join(app.getAppPath(), licensesPath)); - }, + click: this.showThridPartyLicesnsesDialog.bind(this), }, { type: 'separator' }, { @@ -139,12 +141,33 @@ export class Main { ipc.on('renderer:show-help-menu', this.onShowHelpMenu.bind(this)); ipc.on('renderer:key-bind-change', this.onKeyBindChange.bind(this)); ipc.on('renderer:save-snapshot', this.onSaveSnapshot.bind(this)); + ipc.on('worker:get-resources-path', this.onGetResourcesPath.bind(this)); ipc.handle('renderer:show-message-box', this.onShowMessageBox); this.renderer.once('ready-to-show', this.onRendererReadyToShow.bind(this)); this.renderer.once('closed', this.onRendererClosed.bind(this)); this.renderer.on('minimize', this.onRendererMinimize.bind(this)); this.renderer.on('restore', this.onRendererRestore.bind(this)); + this.renderer.webContents.on( + 'will-navigate', + this.onWillNavigate.bind(this) + ); + } + + private isUrlAllowed(input: string) { + const url = new URL(input); + + return this.originWhitelist.includes(url.origin); + } + + private onWillNavigate(event: Event, url: string) { + event.preventDefault(); + + if (!this.isUrlAllowed(url)) { + throw new Error(`Invalid url ${url} provided!`); + } + + return shell.openExternal(url); } private onRendererReadyToShow() { @@ -226,7 +249,7 @@ export class Main { copyFileSync(entry.fileName, tmpSourcePath); } - const { default: tar } = await import('tar'); + const { default: tar } = await import(/* webpackChunkName: "tar" */ 'tar'); await tar.create({ gzip: true, file: filePath, cwd: tmpPath }, ['./']); @@ -304,6 +327,36 @@ export class Main { } } + private getResourcesPath(fallback: string = './resources') { + return app.isPackaged ? process.resourcesPath : fallback; + } + + private onGetResourcesPath(event: Electron.IpcMainEvent) { + event.returnValue = this.getResourcesPath(); + } + + private async showThridPartyLicesnsesDialog() { + // License files are autogenerated by webpack plugin and put in output.path. + const resourcesPath = this.getResourcesPath('./dist'); + const modules = ['main', 'renderer', 'worker', 'preload']; + const contents = await Promise.all( + modules.map(this.getLicenseContent(resourcesPath)) + ); + + this.renderer.webContents.send( + 'main:third-party-licenses', + contents.flat() + ); + } + + private getLicenseContent(resourcesPath: string) { + return (name: string) => { + const path = join(resourcesPath, `${name}-licenses.json`); + + return readJSON(path) as Promise; + }; + } + private toggleKeyBind({ type, payload }: Action) { if (type === ActionTypes.SET_STATUS) { const { keyBind } = this.store.getState().settings; diff --git a/src/electron/renderer/app.tsx b/src/electron/renderer/app.tsx index 2b4485a9..dfc2ba6d 100644 --- a/src/electron/renderer/app.tsx +++ b/src/electron/renderer/app.tsx @@ -6,6 +6,7 @@ import { Navigation, ReleaseNotesDialog, StatusBar, + ThirdPartyLicensesDialog, TitleBar, } from './components'; import { Calibrate, Dashboard, History, Settings } from './pages'; @@ -41,6 +42,7 @@ export const App = () => { + ); diff --git a/src/electron/renderer/common.ts b/src/electron/renderer/common.ts index c810b7af..4d96ac4a 100644 --- a/src/electron/renderer/common.ts +++ b/src/electron/renderer/common.ts @@ -62,6 +62,19 @@ export function useIpcEvent( }, []); } +export function useIpcEventDialog(channel: string) { + const [isOpen, setIsOpen] = useState(false); + const [data, setData] = useState(null); + const close = () => setIsOpen(false); + + useIpcEvent([channel], (e, data: T) => { + setData(data); + setIsOpen(true); + }); + + return { isOpen, data, close }; +} + export function useIpcState() { const [state, setState] = useState(api.getState()); diff --git a/src/electron/renderer/components/Dialog.tsx b/src/electron/renderer/components/Dialog.tsx index 9a43fdeb..ecae2fae 100644 --- a/src/electron/renderer/components/Dialog.tsx +++ b/src/electron/renderer/components/Dialog.tsx @@ -38,17 +38,37 @@ const DialogWrapper = styled(Col)` top: 50%; left: 50%; transform: translate(-50%, -50%); - max-width: 50%; - max-height: 50%; + max-width: clamp(500px, 50%, 800px); + max-height: min(75%, 1000px); background: var(--background); border: 1px solid var(--primary); padding: 1rem; + z-index: 1; `; const OverlayBackdrop = styled.div` position: absolute; inset: 0; background: rgba(0, 0, 0, 0.5); + z-index: 1; +`; + +export const DialogBody = styled.section` + overflow-y: auto; + margin: 1rem 0; + padding-right: 1rem; + font-size: 1.1rem; + font-weight: 500; + + a { + color: var(--accent); + } +`; + +export const DialogTitle = styled.h1` + margin: 0; + color: var(--accent); + text-transform: uppercase; `; interface DialogContentProps { diff --git a/src/electron/renderer/components/ReleaseNotesDialog.tsx b/src/electron/renderer/components/ReleaseNotesDialog.tsx index debc579c..4b1a4dab 100644 --- a/src/electron/renderer/components/ReleaseNotesDialog.tsx +++ b/src/electron/renderer/components/ReleaseNotesDialog.tsx @@ -1,54 +1,15 @@ import { sanitize } from 'dompurify'; import type { UpdateInfo } from 'electron-updater'; -import React, { useState } from 'react'; -import styled from 'styled-components'; -import { useIpcEvent } from '../common'; +import { useIpcEventDialog } from '../common'; import { FlatButton } from './Buttons'; -import { Dialog } from './Dialog'; - -function useReleaseNotes() { - const [isOpen, setIsOpen] = useState(false); - const [updateInfo, setUpdateInfo] = useState(null); - const close = () => setIsOpen(false); - - useIpcEvent(['main:show-release-notes'], (e, info: UpdateInfo) => { - setUpdateInfo(info); - setIsOpen(true); - }); - - return { isOpen, updateInfo, close }; -} - -const ReleaseNotes = styled.article` - overflow-y: auto; - margin: 1rem 0; - padding-right: 1rem; - - a { - color: var(--accent); - } - - > ul { - font-size: 1.1rem; - } -`; - -const ReleaseNotesTitle = styled.h1` - margin: 0; - color: var(--accent); - text-transform: uppercase; -`; +import { Dialog, DialogBody, DialogTitle } from './Dialog'; export const ReleaseNotesDialog = () => { - const { isOpen, close, updateInfo } = useReleaseNotes(); - - function catchLink(event: React.MouseEvent | React.KeyboardEvent) { - if (event.target instanceof HTMLAnchorElement) { - event.preventDefault(); - - api.openExternal(event.target.href); - } - } + const { + isOpen, + close, + data: updateInfo, + } = useIpcEventDialog('main:show-release-notes'); if (!updateInfo) { return null; @@ -56,11 +17,9 @@ export const ReleaseNotesDialog = () => { return ( - Release notes + Release notes {updateInfo.version} - { + const { + isOpen, + close, + data: contents, + } = useIpcEventDialog('main:third-party-licenses'); + + if (!contents) { + return null; + } + + return ( + + Third party licenses + +

+ For more license information visit{' '} + + resources + {' '} + directory. +

+ {contents.map((details, i) => ( + + + {details.name}@{details.version} + + {details.repository && ( + {details.repository} + )} +

{details.licenseText ?? details.license}

+
+ ))} +
+ + Close + +
+ ); +}; diff --git a/src/electron/renderer/components/TitleBar.tsx b/src/electron/renderer/components/TitleBar.tsx index 23dcaa9e..3375a9a7 100644 --- a/src/electron/renderer/components/TitleBar.tsx +++ b/src/electron/renderer/components/TitleBar.tsx @@ -46,6 +46,7 @@ const StyledTitleBar = styled.nav` justify-content: flex-end; user-select: none; -webkit-app-region: drag; + z-index: 1; `; const IconButton = styled.button<{ close?: boolean }>` diff --git a/src/electron/renderer/components/index.ts b/src/electron/renderer/components/index.ts index 370e79a0..b89e0950 100644 --- a/src/electron/renderer/components/index.ts +++ b/src/electron/renderer/components/index.ts @@ -15,4 +15,5 @@ export * from './Select'; export * from './Spinner'; export * from './StatusBar'; export * from './Switch'; +export * from './ThirdPartyLicensesDialog'; export * from './TitleBar'; diff --git a/src/electron/renderer/preload/ipc.ts b/src/electron/renderer/preload/ipc.ts index 51e04837..fd21f982 100644 --- a/src/electron/renderer/preload/ipc.ts +++ b/src/electron/renderer/preload/ipc.ts @@ -4,6 +4,7 @@ import { ipcRenderer } from 'electron'; const onChannels = [ 'main:show-release-notes', 'main:download-progress', + 'main:third-party-licenses', 'SET_SETTINGS', 'UPDATE_SETTINGS', 'ADD_HISTORY_ENTRY', diff --git a/src/electron/renderer/preload/shell.ts b/src/electron/renderer/preload/shell.ts index 633cad7c..a6517aae 100644 --- a/src/electron/renderer/preload/shell.ts +++ b/src/electron/renderer/preload/shell.ts @@ -2,17 +2,7 @@ import { shell } from 'electron'; export const { showItemInFolder } = shell; -// TODO: add repo url to constants. -const urlWhitelist = ['https://github.com']; - -/** Wrapper around {@link shel.openExternal}. Accepts only curated list of urls. */ -export function openExternal( - url: string, - options?: Electron.OpenExternalOptions -) { - if (urlWhitelist.some((w) => w.startsWith(url))) { - return shell.openExternal(url, options); - } - - throw new Error(`Invalid url: "${url}" provided.`); +export function openResourcesFolder() { + // NOTE: this will point to incorrect folder while in development mode. + return shell.openPath(process.resourcesPath); } diff --git a/src/electron/worker/dialog.ts b/src/electron/worker/dialog.ts index db7f966a..14400d98 100644 --- a/src/electron/worker/dialog.ts +++ b/src/electron/worker/dialog.ts @@ -2,7 +2,7 @@ import { ipcRenderer as ipc } from 'electron'; import { NativeDialog } from '../common'; class WorkerNativeDialog extends NativeDialog { - showMessageBox(options: Electron.MessageBoxOptions) { + protected showMessageBox(options: Electron.MessageBoxOptions) { return ipc.invoke('renderer:show-message-box', options); } } diff --git a/src/electron/worker/worker.ts b/src/electron/worker/worker.ts index 3a115dd6..b018339f 100644 --- a/src/electron/worker/worker.ts +++ b/src/electron/worker/worker.ts @@ -27,7 +27,6 @@ import { } from '@/electron/common'; import { execSync } from 'child_process'; import { ipcRenderer as ipc, IpcRendererEvent } from 'electron'; -import { join } from 'path'; import { listDisplays, ScreenshotDisplayOutput } from 'screenshot-desktop'; import sharp from 'sharp'; import { BreachProtocolAutosolver } from './autosolver'; @@ -83,14 +82,15 @@ export class BreachProtocolWorker { } private async initTesseractScheduler() { - const langPath = - BUILD_PLATFORM === 'linux' && process.env.NODE_ENV === 'production' - ? join(__dirname, '../..') - : './resources'; + const langPath = this.getResourcesPath(); await WasmBreachProtocolRecognizer.initScheduler(langPath); } + private getResourcesPath() { + return ipc.sendSync('worker:get-resources-path'); + } + private validateExternalDependencies() { if (BUILD_PLATFORM === 'win32') { if (this.settings.engine === 'ahk' && !this.settings.ahkBinPath) {