meta: integrate vite, remove webpack

This commit is contained in:
Xymorot 2021-07-25 20:38:51 +02:00
parent dc68771232
commit 9512210624
139 changed files with 501 additions and 1777 deletions

View File

@ -82,7 +82,6 @@
]
}
],
"import/no-default-export": "warn",
"import/first": "warn",
"import/order": [
"warn",
@ -98,9 +97,11 @@
},
"overrides": [
{
"files": ["src/**/*.*"],
"files": ["src/**/*"],
"rules": {
"no-console": "warn"
"no-console": "warn",
"import/no-default-export": "warn"
}
},
{
@ -222,7 +223,7 @@
"processor": "svelte3/svelte3"
},
{
"files": ["src/shared/types/**/*"],
"files": ["src/shared/**/*"],
"rules": {
"@typescript-eslint/no-magic-numbers": "off"
}

1
.gitignore vendored
View File

@ -8,6 +8,7 @@ node_modules
/src/**/*.js
/src/**/*.js.map
/frontend
/index.html
# created by testing
/test-paths

View File

@ -9,6 +9,7 @@
!*.md
!*.ts
!*.js
!*.mjs
!*.drawio
# but ignore stuff from .gitignore

View File

@ -186,17 +186,20 @@ This also means potentially updating:
- the `node` version key under `engines` in [package.json](package.json)
- the `@types/node` package
- the electron version in the `target` field of the [webpack config](scripts/webpack.config.js)
- the chrome version in the `build.target` field of the [vite config](scripts/vite.config.mjs)
- the `compilerOptions.target` fields of the [main](tsconfig.json) and [renderer](tsconfig.renderer.json) typescript configuration files
- the `parserOptions.ecmaVersion` field in the [ESLint configuration](.eslintrc.json)
Run `npm run electron-version` to find out which specific version is currently running.
Also might be worth checking out https://github.com/electron/electron/issues/21457 if the project can be converted to transpile to ES Modules.
### Frontend
#### Translation
With the help of [intl-messageformat](https://www.npmjs.com/package/intl-messageformat) frontend message can be defined in the [ICU Message Format](https://unicode-org.github.io/icu/userguide/format_parse/messages/). The corresponding code can be found [here](src/shared/services/translation/t.ts). I don't think a language other than English will be necessary, but the formatting features are nice anyway.
With the help of [intl-messageformat](https://www.npmjs.com/package/intl-messageformat) frontend messages can be defined in the [ICU Message Format](https://unicode-org.github.io/icu/userguide/format_parse/messages/). The corresponding code can be found [here](src/renderer/services/translation/t.ts). I don't think a language other than English will be necessary, but the formatting features are nice anyway.
This code is duplicated in [src/main/modules/translation/t.ts](src/main/modules/translation/t.ts), because one is run in the browser, the other in node and it led to complications with different tsconfig options.
## Design

1943
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -10,6 +10,7 @@
"email": "xymorot@mailbox.org"
},
"main": "src/main.js",
"module": "src/renderer.js",
"engines": {
"node": "14",
"npm": "7"
@ -24,14 +25,18 @@
"typeorm:migrate": "npm run typeorm:migrate:library && npm run typeorm:migrate:store",
"typeorm:migrate:library": "typeorm migration:run -c library",
"typeorm:migrate:store": "typeorm migration:run -c store",
"build:webpack": "webpack --config scripts/webpack.config.js",
"dev:webpack": "webpack --config scripts/webpack.config.js --mode development -w",
"build:frontend:typescript": "tsc --build tsconfig.renderer.json",
"dev:frontend:typescript": "tsc --build tsconfig.renderer.json -w --pretty --preserveWatchOutput",
"build:frontend:bundle": "vite build -c scripts/vite.config.mjs",
"dev:frontend:bundle": "vite -c scripts/vite.config.mjs",
"build:frontend": "npm run build:frontend:typescript && npm run build:frontend:bundle",
"dev:frontend": "concurrently -c blue,red -n tsc,bundle \"npm run dev:frontend:typescript\" \"npm run dev:frontend:bundle\"",
"build:index": "node scripts/buildfile.js",
"dev:index": "node scripts/buildfile.js --watch --dev",
"build:ts": "tsc",
"dev:ts": "tsc -w --pretty --preserveWatchOutput",
"build": "concurrently -c green,yellow,cyan -n webpack,index,typescript \"npm run build:webpack\" \"npm run build:index\" \"npm run build:ts\"",
"dev": "concurrently -c green,yellow,cyan -n webpack,index,typescript \"npm run dev:webpack\" \"npm run dev:index\" \"npm run dev:ts\"",
"build:backend": "tsc --build tsconfig.json",
"dev:backend": "tsc --build tsconfig.json -w --pretty --preserveWatchOutput",
"build": "concurrently -c green,yellow,cyan -n frontend,index,backend \"npm run build:frontend\" \"npm run build:index\" \"npm run build:backend\"",
"dev": "concurrently -c green,yellow,cyan -n frontend,index,backend \"npm run dev:frontend\" \"npm run dev:index\" \"npm run dev:backend\"",
"test:fast": "electron-mocha --config .mocharc.json --grep \"@slow\" --invert",
"test": "electron-mocha --config .mocharc.json",
"coverage": "nyc npm run test",
@ -61,6 +66,7 @@
"@electron-forge/cli": "^6.0.0-beta.55",
"@electron-forge/maker-squirrel": "^6.0.0-beta.55",
"@prettier/plugin-xml": "^0.13.1",
"@sveltejs/vite-plugin-svelte": "^1.0.0-next.12",
"@types/better-sqlite3": "^5.4.1",
"@types/chai": "^4.2.18",
"@types/chai-fs": "^2.0.2",
@ -72,7 +78,6 @@
"@types/node": "^14.17.0",
"@types/sinon": "^10.0.0",
"@types/uuid": "^8.3.0",
"@types/webpack": "^5.28.0",
"@typescript-eslint/eslint-plugin": "^4.24.0",
"@typescript-eslint/parser": "^4.24.0",
"chai": "^4.3.4",
@ -100,11 +105,8 @@
"prettier-plugin-svelte": "^2.3.0",
"sinon": "^11.1.1",
"svelte": "^3.38.2",
"svelte-loader": "^3.1.1",
"ts-loader": "^9.2.0",
"typescript": "^4.2.4",
"webpack": "^5.37.1",
"webpack-cli": "^4.7.0"
"vite": "^2.4.1"
},
"repository": "https://git.fuwafuwa.moe/Xymorot/RenaiApp",
"bugs": "https://git.fuwafuwa.moe/Xymorot/RenaiApp/issues",

View File

@ -4,15 +4,14 @@ const { watch } = require('chokidar');
const { debounce } = require('lodash');
const minimist = require('minimist');
const templating = require('../templates');
const webpackConfig = require('./webpack.config');
/** @var {Object} argv */
const argv = minimist(process.argv);
function buildIndexHtml() {
const html = templating.compile(!!argv.dev);
const html = templating.compile();
fs.writeFileSync(path.resolve(webpackConfig.output.path, 'index.html'), html);
fs.writeFileSync(path.resolve('index.html'), html);
console.log('compiled index.html');
}
@ -21,7 +20,7 @@ const debouncedBuildIndexHtml = debounce(buildIndexHtml, 100, { leading: true, t
function build() {
if (argv.watch) {
const templatesWatcher = watch('./templates/**/*');
templatesWatcher.on('all', debouncedBuildIndexHtml);
templatesWatcher.on('all', () => debouncedBuildIndexHtml());
} else {
buildIndexHtml();
}

View File

@ -4,6 +4,7 @@ const ignoreList = [
/^\/\.idea($|\/)/,
/^\/\.vscode($|\/)/,
/^\/\.husky($|\/)/,
/^\/\.nyc_output($|\/)/,
/^\/declarations($|\/)/,
/^\/templates($|\/)/,
@ -21,18 +22,20 @@ const ignoreList = [
/^\/\.nycrc\.yml/,
/^\/\.prettierrc\.yml/,
/^\/CONTRIBUTING\.md/,
/^\/index\.html/,
/^\/ormconfig\.yml/,
/^\/package-lock\.json/,
/^\/tsconfig\.json/,
/^\/tsconfig\.renderer\.json/,
/^\/node_modules\/\.cache($|\/)/,
/^\/node_modules\/\.vite($|\/)/,
// test and mock files:
/^.*\.(spec|mock)\.(ts|js(\.map)?)/,
/^\/src\/.*\/test\/.*$/,
// original typescript source and generated source map files:
// original source and generated source map files:
/^\/src\/.*\.(ts|js\.map)/,
/^\/src\/.*\.eslintrc\.json/,
/^\/src\/(renderer($|\/)|renderer\.js)/,
];
const name = packageJson.productName;

View File

@ -0,0 +1,6 @@
/**
* @type {import('rollup').RollupOptions}
*/
const options = {};
export default options;

17
scripts/vite.config.mjs Normal file
View File

@ -0,0 +1,17 @@
import { svelte } from '@sveltejs/vite-plugin-svelte';
import rollupOptions from './rollup.config.mjs';
/**
* @type {import('vite').UserConfig}
*/
const config = {
plugins: [svelte()],
base: '',
build: {
target: 'chrome91',
outDir: 'frontend',
rollupOptions,
},
};
export default config;

View File

@ -1,29 +0,0 @@
const path = require('path');
module.exports = {
mode: 'production',
entry: path.resolve(__dirname, '../src/renderer.ts'),
target: 'electron13-renderer',
output: {
path: path.resolve(__dirname, '../frontend'),
filename: 'bundle.js',
},
module: {
rules: [
{
test: /\.svelte$/,
loader: 'svelte-loader',
},
{
test: /\.ts$/,
loader: 'ts-loader',
options: {
configFile: path.resolve('tsconfig.renderer.json'),
},
},
],
},
resolve: {
extensions: ['.ts', '.js'],
},
};

View File

@ -2,6 +2,7 @@ import 'reflect-metadata';
import { Container, interfaces } from 'inversify';
import type { AppWindowInterface } from '../modules/app-window/app-window-interface';
import { MainAppWindow } from '../modules/app-window/main-app-window';
import { MainAppWindow as MainAppWindowDev } from '../modules/app-window/main-app-window.dev';
import { Dialog } from '../modules/dialog/dialog';
import type { DialogInterface } from '../modules/dialog/dialog-interface';
import { Logger } from '../modules/logger/logger';
@ -40,6 +41,7 @@ import { WorldNameSerializer } from '../modules/serialization/serializers/world-
import { WorldSerializer } from '../modules/serialization/serializers/world-serializer';
import type { SourceGetterInterface } from '../modules/source/source-getter-interface';
import { Store } from '../modules/store/store';
import { isDev } from './env';
import BindingToSyntax = interfaces.BindingToSyntax;
export const enum Service {
@ -182,4 +184,8 @@ container.bind(Service.NHENTAI_APP_WINDOW).to(NhentaiAppWindow);
container.bind(Service.NHENTAI_API).to(NhentaiApi);
container.bind(Service.NHENTAI_SOURCE_GETTER).to(NhentaiSourceGetter);
container.bind(Service.APP_WINDOW_MAIN).to(MainAppWindow);
if (isDev()) {
container.bind(Service.APP_WINDOW_MAIN).to(MainAppWindowDev);
} else {
container.bind(Service.APP_WINDOW_MAIN).to(MainAppWindow);
}

View File

@ -2,7 +2,7 @@ import { app, BrowserWindow, Event, LoadFileOptions, LoadURLOptions, NewWindowWe
import os from 'os';
import path from 'path';
import { isDev } from '../../core/env';
import { completeContentSecurityPolicy, setWindowCsp } from '../session/session-util';
import { setWindowCsp } from '../session/session-util';
import type { AppWindowInterface } from './app-window-interface';
import { WindowClosedError } from './window-closed-error';
import BrowserWindowConstructorOptions = Electron.BrowserWindowConstructorOptions;
@ -64,7 +64,7 @@ export abstract class AppWindow implements AppWindowInterface {
public open(): Promise<void> {
this._window = new BrowserWindow(this.options);
setWindowCsp(this._window, completeContentSecurityPolicy(this.getCsp()));
setWindowCsp(this._window, this.getCsp());
this._window.on('closed', () => {
this.onClosed();

View File

@ -17,4 +17,10 @@ export abstract class FileAppWindow extends AppWindow {
protected load(window: BrowserWindow): Promise<void> {
return window.loadFile(this.uri, this.loadOptions);
}
protected getCsp(): Session.ContentSecurityPolicy {
return {
'default-src': ['file:'],
};
}
}

View File

@ -0,0 +1,24 @@
import path from 'path';
import { injectable } from 'inversify';
import { Service } from '../../core/container';
import { inject } from '../../core/inject';
import { mergeContentSecurityPolicy } from '../session/session-util';
import { UrlAppWindow } from './url-app-window';
@injectable()
export class MainAppWindow extends UrlAppWindow {
public constructor(@inject(Service.LOGGER) logger: LoggerInterface) {
super(logger, 'http://localhost:3000', {
webPreferences: {
preload: path.resolve('src/preload.js'),
},
});
}
protected getCsp(): Session.ContentSecurityPolicy {
return mergeContentSecurityPolicy(super.getCsp(), {
'script-src-elem': ["'self'"],
'connect-src': ['ws:'],
});
}
}

View File

@ -1,3 +1,4 @@
import path from 'path';
import { injectable } from 'inversify';
import { Service } from '../../core/container';
import { inject } from '../../core/inject';
@ -8,8 +9,7 @@ export class MainAppWindow extends FileAppWindow {
public constructor(@inject(Service.LOGGER) logger: LoggerInterface) {
super(logger, 'frontend/index.html', {
webPreferences: {
nodeIntegration: true,
contextIsolation: false,
preload: path.resolve('src/preload.js'),
},
});
}

View File

@ -1,6 +1,6 @@
import { dialog, OpenDialogOptions } from 'electron';
import { injectable } from 'inversify';
import { t } from '../../../shared/services/translation/t';
import { t } from '../translation/t';
import type { DialogInterface } from './dialog-interface';
@injectable()

View File

@ -1,8 +1,8 @@
import path from 'path';
import { createWriteStream } from 'fs-extra';
import { t } from '../../../shared/services/translation/t';
import { container, Service } from '../../core/container';
import { ipcServer } from '../ipc/ipc-server';
import { t } from '../translation/t';
ipcServer.answer(IpcChannel.NHENTAI_SAVE_FAVORITES, async (): Promise<void> => {
const nhentaiApi = container.get(Service.NHENTAI_API);

View File

@ -1,10 +1,9 @@
import { isDev } from '../../core/env';
import ContentSecurityPolicy = Session.ContentSecurityPolicy;
import CompleteContentSecurityPolicy = Session.CompleteContentSecurityPolicy;
const defaultCsp: Session.ContentSecurityPolicy = {
'default-src': ["'self'"],
'style-src': ["'unsafe-inline'"],
'style-src-attr': ["'unsafe-inline'"],
};
function stringifyCspHeader(csp: ContentSecurityPolicy): string {
@ -30,7 +29,7 @@ export function mergeContentSecurityPolicy(...contentSecurityPolicies: ContentSe
}, {});
}
export function setWindowCsp(window: Electron.BrowserWindow, csp: CompleteContentSecurityPolicy): void {
export function setWindowCsp(window: Electron.BrowserWindow, csp: ContentSecurityPolicy): void {
const mergedCsp: ContentSecurityPolicy = { ...defaultCsp, ...csp };
if (isDev()) {
@ -52,27 +51,3 @@ export function setWindowCsp(window: Electron.BrowserWindow, csp: CompleteConten
});
});
}
/**
* fills script-src-elem and script-src-attr with script-src when not present,
* fills rest of missing policies with default-src or nothing when it is not present
*/
export function completeContentSecurityPolicy(csp: ContentSecurityPolicy): CompleteContentSecurityPolicy {
const defaultSrc = csp['default-src'] ?? [];
const scriptSrc = csp['script-src'] ?? [];
return {
'child-src': csp['child-src'] ?? defaultSrc,
'connect-src': csp['connect-src'] ?? defaultSrc,
'default-src': csp['default-src'] ?? defaultSrc,
'font-src': csp['font-src'] ?? defaultSrc,
'frame-src': csp['frame-src'] ?? defaultSrc,
'img-src': csp['img-src'] ?? defaultSrc,
'media-src': csp['media-src'] ?? defaultSrc,
'object-src': ["'none'"],
'script-src': csp['script-src'] ?? defaultSrc,
'script-src-elem': csp['script-src-elem'] ?? scriptSrc ?? defaultSrc,
'script-src-attr': csp['script-src-attr'] ?? scriptSrc ?? defaultSrc,
'style-src': csp['style-src'] ?? defaultSrc,
'worker-src': csp['worker-src'] ?? defaultSrc,
};
}

View File

@ -19,25 +19,8 @@ declare namespace Session {
'script-src-elem'?: CspValue[];
'script-src-attr'?: CspValue[];
'style-src'?: CspValue[];
'style-src-elem'?: CspValue[];
'style-src-attr'?: CspValue[];
'worker-src'?: CspValue[];
};
/**
* meant for usages where the browser shouldn't unexpectedly fall back to default-src
*/
type CompleteContentSecurityPolicy = {
'child-src': CspValue[];
'connect-src': CspValue[];
'default-src': CspValue[];
'font-src': CspValue[];
'frame-src': CspValue[];
'img-src': CspValue[];
'media-src': CspValue[];
'object-src': ["'none'"];
'script-src': CspValue[];
'script-src-elem': CspValue[];
'script-src-attr': CspValue[];
'style-src': CspValue[];
'worker-src': CspValue[];
};
}

View File

@ -0,0 +1,50 @@
// same as in renderer
import { IntlMessageFormat } from 'intl-messageformat';
import * as en from '../../../shared/translation/en.json';
const localeData = {
[Locale.EN]: en as TranslationData,
};
function getDeep(deepKey: string, data: TranslationData): string {
const keyChain = deepKey.split('.');
if (!keyChain.length) {
return deepKey;
}
let index = 0;
let curr: TranslationData | string = data;
do {
const key = keyChain[index];
curr = curr[key];
index++;
} while (typeof curr !== 'string' && index < keyChain.length);
if (typeof curr === 'string') {
return curr;
}
return deepKey;
}
const intlMessageFormatCache: Record<Locale, Record<string, IntlMessageFormat>> = {
[Locale.EN]: {},
};
/**
* translates a message
*
* @param key - a deep key of the format "labels.actions.save"
* @param values - arguments for the message
* @param locale - see {@link Locale} for implemented locales
*/
export function t(key: string, values?: Record<string, string | number | boolean>, locale: Locale = Locale.EN): string {
if (!intlMessageFormatCache[locale][key]) {
const message = getDeep(key, localeData[locale]);
intlMessageFormatCache[locale][key] = new IntlMessageFormat(message, locale);
}
const intlMessageFormat = intlMessageFormatCache[locale][key];
const formatted = intlMessageFormat.format(values);
if (typeof formatted === 'string') {
return formatted;
}
return key;
}

3
src/preload.ts Normal file
View File

@ -0,0 +1,3 @@
import { contextBridge, ipcRenderer } from 'electron';
contextBridge.exposeInMainWorld('ipcRenderer', ipcRenderer);

View File

@ -1,6 +1,6 @@
<script>
import { t } from '../../../shared/services/translation/t';
import { nhentaiGetWork } from '../../services/api';
import { t } from '../../services/translation/t';
import Work from '../content/Work.svelte';
import SvelteButton from '../elements/SvelteButton.svelte';

View File

@ -1,6 +1,6 @@
<script>
import { t } from '../../../shared/services/translation/t';
import { nhentaiSaveFavorites } from '../../services/api';
import { t } from '../../services/translation/t';
import SvelteButton from '../elements/SvelteButton.svelte';
function handleClick() {

View File

@ -1,4 +1,7 @@
import { ipcRenderer } from 'electron';
import type { IpcRenderer } from 'electron';
// @ts-ignore -- https://www.electronjs.org/docs/latest/tutorial/context-isolation
const ipcRenderer: IpcRenderer = window.ipcRenderer as IpcRenderer;
export const ipcClient: IpcClient = {
ask<T extends IpcChannel>(channel: T, data?: IpcParameter<T>): Promise<IpcAnswer<T>> {

View File

@ -1,12 +1,10 @@
import { IntlMessageFormat } from 'intl-messageformat';
import { en } from './en';
// same as in main
const enum Locale {
EN = 'en',
}
import { IntlMessageFormat } from 'intl-messageformat';
import * as en from '../../../shared/translation/en.json';
const localeData = {
[Locale.EN]: en,
[Locale.EN]: en as TranslationData,
};
function getDeep(deepKey: string, data: TranslationData): string {

Some files were not shown because too many files have changed in this diff Show More