feat: implement nhentai login and refactor large pieces of code in the process
This commit is contained in:
parent
6d82b16d5c
commit
389df67605
|
@ -1,2 +1,3 @@
|
|||
node_modules
|
||||
dist
|
||||
frontend
|
||||
|
|
|
@ -13,5 +13,13 @@
|
|||
"no-shadow": "warn",
|
||||
"no-magic-numbers": ["warn", { "ignore": [0, 1, -1] }],
|
||||
"no-param-reassign": "warn"
|
||||
}
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["src/renderer/**/*.*"],
|
||||
"parserOptions": {
|
||||
"sourceType": "module"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
29
src/main.ts
29
src/main.ts
|
@ -1,22 +1,41 @@
|
|||
import { app, BrowserWindow } from 'electron';
|
||||
import os from 'os';
|
||||
|
||||
import './main/controllers/api';
|
||||
import './main/services/database';
|
||||
import session from './main/services/session';
|
||||
import * as session from './main/services/session';
|
||||
import BrowserWindowConstructorOptions = Electron.BrowserWindowConstructorOptions;
|
||||
|
||||
let mainWindow: Electron.BrowserWindow;
|
||||
|
||||
async function createWindow(): Promise<void> {
|
||||
session.init();
|
||||
session.setHeaders();
|
||||
|
||||
// Create the browser window.
|
||||
mainWindow = new BrowserWindow({
|
||||
// universal options
|
||||
let options: BrowserWindowConstructorOptions = {
|
||||
width: 1600,
|
||||
height: 900,
|
||||
webPreferences: {
|
||||
nodeIntegration: true,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// platform specifics
|
||||
switch (os.platform()) {
|
||||
case 'win32':
|
||||
options = {
|
||||
...options,
|
||||
...{
|
||||
icon: 'resources/icon.ico',
|
||||
},
|
||||
};
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
// Create the browser window.
|
||||
mainWindow = new BrowserWindow(options);
|
||||
|
||||
// and load the index.html of the app.
|
||||
await mainWindow.loadFile('index.html');
|
||||
|
|
|
@ -1,13 +1,32 @@
|
|||
import { ipcMain } from 'electron';
|
||||
import nhentai from './../services/nhentai-crawler';
|
||||
import { isLoggedIn, login } from '../services/nhentai-crawler';
|
||||
|
||||
ipcMain.on(IpcChannels.Credentials, (event: IpcEvent, args: ICredentials) => {
|
||||
nhentai
|
||||
.login(args.name, args.password)
|
||||
.then(() => {
|
||||
console.log('success');
|
||||
})
|
||||
.catch(() => {
|
||||
console.log('fail');
|
||||
const ipcServer: IIpcServer = {
|
||||
answer: (channel: IpcChannels, handler: (data?: any) => Promise<any>): void => {
|
||||
ipcMain.on(channel, (event: IpcEvent, payload: IIpcPayload) => {
|
||||
handler(payload.data)
|
||||
.then((result: any) => {
|
||||
const response: IIpcResponse = {
|
||||
success: true,
|
||||
data: result,
|
||||
};
|
||||
event.reply(channel, response);
|
||||
})
|
||||
.catch((reason: any) => {
|
||||
const response: IIpcResponse = {
|
||||
success: false,
|
||||
error: reason.toString(),
|
||||
};
|
||||
event.reply(channel, response);
|
||||
});
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
ipcServer.answer(IpcChannels.LOGIN, (credentials: ICredentials) => {
|
||||
return login(credentials.name, credentials.password);
|
||||
});
|
||||
|
||||
ipcServer.answer(IpcChannels.LOGGED_IN, () => {
|
||||
return isLoggedIn();
|
||||
});
|
||||
|
|
|
@ -1,22 +1,21 @@
|
|||
import 'reflect-metadata';
|
||||
import { Connection, createConnection } from 'typeorm';
|
||||
|
||||
export let library: Connection;
|
||||
|
||||
function init(): void {
|
||||
initConnection();
|
||||
export const enum Databases {
|
||||
LIBRARY = 'library',
|
||||
}
|
||||
|
||||
function initConnection(): void {
|
||||
// createConnection method will automatically read connection options
|
||||
// from your ormconfig file or environment variables
|
||||
createConnection('library')
|
||||
.then((c: Connection) => {
|
||||
library = c;
|
||||
})
|
||||
.catch((reason: any) => {
|
||||
throw reason;
|
||||
const connections: {
|
||||
[key in Databases]?: Connection;
|
||||
} = {};
|
||||
|
||||
export function getConnection(database: Databases): Promise<Connection> {
|
||||
if (connections[database] === undefined) {
|
||||
return createConnection(database).then((connection: Connection) => {
|
||||
connections[database] = connection;
|
||||
return connection;
|
||||
});
|
||||
} else {
|
||||
return Promise.resolve(connections[database]);
|
||||
}
|
||||
}
|
||||
|
||||
init();
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
import { JSDOM } from 'jsdom';
|
||||
import { RequestInit, Response } from 'node-fetch';
|
||||
import RenaiError, { Errors } from '../../types/error';
|
||||
import fetch from './web-crawler';
|
||||
import { Errors, RenaiError } from '../../types/error';
|
||||
import { fetch } from './web-crawler';
|
||||
|
||||
const url = 'https://nhentai.net/';
|
||||
const domain = 'nhentai.net';
|
||||
const url = `https://${domain}/`;
|
||||
|
||||
const paths = {
|
||||
books: 'g/',
|
||||
|
@ -25,33 +26,6 @@ interface ILoginAuth {
|
|||
|
||||
interface ILoginParams extends ILoginMeta, ILoginAuth {}
|
||||
|
||||
function login(name: string, password: string): Promise<void> {
|
||||
return getLoginMeta()
|
||||
.then((meta: ILoginMeta) => {
|
||||
const loginParams: ILoginParams = {
|
||||
...meta,
|
||||
...{
|
||||
// tslint:disable-next-line: object-literal-sort-keys
|
||||
username_or_email: name,
|
||||
password,
|
||||
},
|
||||
};
|
||||
|
||||
return postNHentai(paths.login, {
|
||||
body: encodeURI(
|
||||
Object.keys(loginParams)
|
||||
.map((key: keyof ILoginParams) => `${key}=${loginParams[key]}`)
|
||||
.join('&')
|
||||
),
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
});
|
||||
})
|
||||
.then(() => {})
|
||||
.catch(() => Promise.reject(new RenaiError(Errors.ELOGINFAIL)));
|
||||
}
|
||||
|
||||
function getNHentai(path: string): Promise<Document> {
|
||||
return fetch(`${url}${path}`)
|
||||
.then((res: Response) => {
|
||||
|
@ -63,8 +37,21 @@ function getNHentai(path: string): Promise<Document> {
|
|||
});
|
||||
}
|
||||
|
||||
function postNHentai(path: string, init: RequestInit = {}): Promise<Response> {
|
||||
return fetch(`${url}${path}`, { ...init, ...{ method: 'post' } });
|
||||
function postNHentai(path: string, requestInit: RequestInit = {}): Promise<Response> {
|
||||
const postUrl = `${url}${path}`;
|
||||
return fetch(postUrl, {
|
||||
...requestInit,
|
||||
...{
|
||||
headers: {
|
||||
...requestInit.headers,
|
||||
...{
|
||||
Host: domain,
|
||||
Referer: postUrl,
|
||||
},
|
||||
},
|
||||
},
|
||||
method: 'post',
|
||||
});
|
||||
}
|
||||
|
||||
function getLoginMeta(): Promise<ILoginMeta> {
|
||||
|
@ -93,6 +80,36 @@ function getLoginMeta(): Promise<ILoginMeta> {
|
|||
});
|
||||
}
|
||||
|
||||
export default {
|
||||
login,
|
||||
};
|
||||
export function isLoggedIn(): Promise<boolean> {
|
||||
return fetch(`${url}${paths.favorites}`, { redirect: 'manual' }).then((res: Response) => {
|
||||
return res.status === HttpCode.OK;
|
||||
});
|
||||
}
|
||||
|
||||
export function login(name: string, password: string): Promise<void> {
|
||||
return getLoginMeta()
|
||||
.then((meta: ILoginMeta) => {
|
||||
const loginParams: ILoginParams = {
|
||||
...meta,
|
||||
...{
|
||||
// tslint:disable-next-line: object-literal-sort-keys
|
||||
username_or_email: name,
|
||||
password,
|
||||
},
|
||||
};
|
||||
|
||||
return postNHentai(paths.login, {
|
||||
body: encodeURI(
|
||||
Object.keys(loginParams)
|
||||
.map((key: keyof ILoginParams) => `${key}=${loginParams[key]}`)
|
||||
.join('&')
|
||||
),
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
redirect: 'manual',
|
||||
});
|
||||
})
|
||||
.then(() => {})
|
||||
.catch(() => Promise.reject(new RenaiError(Errors.ELOGINFAIL)));
|
||||
}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { session } from 'electron';
|
||||
import OnHeadersReceivedDetails = Electron.OnHeadersReceivedDetails;
|
||||
|
||||
function init(): void {
|
||||
// these headers only work on webrequests, file:// protocol is handled via meta tags in index.html
|
||||
export function setHeaders(): void {
|
||||
// these headers only work on web requests, file:// protocol is handled via meta tags in index.html
|
||||
session.defaultSession.webRequest.onHeadersReceived(
|
||||
(details: OnHeadersReceivedDetails, callback: (response: {}) => void) => {
|
||||
callback({
|
||||
|
@ -14,5 +14,3 @@ function init(): void {
|
|||
}
|
||||
);
|
||||
}
|
||||
|
||||
export default { init };
|
||||
|
|
|
@ -1,27 +1,55 @@
|
|||
import { CookieJar } from 'jsdom';
|
||||
import nodeFetch, { RequestInit, Response } from 'node-fetch';
|
||||
import { Cookie, CookieJar } from 'tough-cookie';
|
||||
import { Errors, RenaiError } from '../../types/error';
|
||||
import { load, save, StoreKeys } from './store';
|
||||
|
||||
const cookieJar: CookieJar = new CookieJar();
|
||||
export let cookieJar: CookieJar = new CookieJar();
|
||||
|
||||
function fetch(url: string, init: RequestInit = {}): Promise<Response> {
|
||||
const headers: HeadersInit = {};
|
||||
cookieJar.getCookiesSync(url).forEach((cookie: Cookie) => {
|
||||
headers[cookie.key] = cookie.value;
|
||||
});
|
||||
let error: Error;
|
||||
|
||||
function init(): void {
|
||||
load(StoreKeys.COOKIES)
|
||||
.then((cookies: any) => {
|
||||
if (cookies !== undefined) {
|
||||
cookieJar = CookieJar.deserializeSync(cookies);
|
||||
}
|
||||
})
|
||||
.catch((reason: any) => {
|
||||
error = new RenaiError(Errors.EINITFAIL, reason);
|
||||
});
|
||||
}
|
||||
|
||||
export function fetch(url: string, requestInit: RequestInit = {}): Promise<Response> {
|
||||
if (error !== undefined) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
const cookiedInit = {
|
||||
...init,
|
||||
...{ headers: { ...init.headers, ...headers } },
|
||||
...requestInit,
|
||||
...{
|
||||
headers: {
|
||||
...requestInit.headers,
|
||||
...{
|
||||
Cookie: cookieJar.getCookieStringSync(url),
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
return nodeFetch(url, cookiedInit).then((res: Response) => {
|
||||
setCookies(res.headers.raw()['set-cookie'], url);
|
||||
setCookies(res.headers.raw()['set-cookie'], url).catch((reason: any) => {
|
||||
error = new Error(reason);
|
||||
});
|
||||
return res;
|
||||
});
|
||||
}
|
||||
|
||||
function setCookies(header: string[], url: string): void {
|
||||
header.forEach((cookie: string) => {
|
||||
cookieJar.setCookieSync(cookie, url);
|
||||
});
|
||||
function setCookies(header: string[], url: string): Promise<void> {
|
||||
if (header) {
|
||||
header.forEach((cookie: string) => {
|
||||
cookieJar.setCookieSync(cookie, url);
|
||||
});
|
||||
return save(StoreKeys.COOKIES, cookieJar.serializeSync());
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
export default fetch;
|
||||
init();
|
||||
|
|
|
@ -1,16 +1,7 @@
|
|||
{Divide,NhentaiLogin;}
|
||||
<script>
|
||||
import Bttn from 'atoms/Bttn.svelte';
|
||||
import Divide from 'molecules/Divide.svelte';
|
||||
import api from 'services/api';
|
||||
|
||||
let form = {
|
||||
name: '',
|
||||
password: '',
|
||||
};
|
||||
|
||||
function handleClick() {
|
||||
api.sendCredentials(form);
|
||||
}
|
||||
import NhentaiLogin from 'polymers/NhentaiLogin.svelte';
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
@ -57,18 +48,7 @@
|
|||
<main>
|
||||
<Divide>
|
||||
<div slot="1">
|
||||
<h1>Login</h1>
|
||||
<form>
|
||||
<label>
|
||||
<span>Username/Email</span>
|
||||
<input bind:value="{form.name}" />
|
||||
</label>
|
||||
<label>
|
||||
<span>Password</span>
|
||||
<input bind:value="{form.password}" type="password" />
|
||||
</label>
|
||||
<Bttn on:click="{handleClick}">submit</Bttn>
|
||||
</form>
|
||||
<NhentaiLogin></NhentaiLogin>
|
||||
</div>
|
||||
<div slot="2">
|
||||
<Divide mode="v">
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
<script>
|
||||
import { onMount } from 'svelte/internal';
|
||||
import { t } from 'services/utils';
|
||||
import Bttn from 'atoms/Bttn.svelte';
|
||||
import { loggedIn } from 'services/store';
|
||||
|
||||
let form = {
|
||||
name: '',
|
||||
password: '',
|
||||
};
|
||||
|
||||
function handleClick() {
|
||||
loggedIn.fetchLogin(form).catch((reason) => {
|
||||
console.log(reason);
|
||||
});
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
loggedIn.fetchIsLoggedIn();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
|
||||
<div class="nhentai-login">
|
||||
{#if $loggedIn}
|
||||
<div>logged in!</div>
|
||||
{:else}
|
||||
<form class="nhentai-login">
|
||||
<label>
|
||||
<span>{ t('Username/Email') }</span>
|
||||
<input bind:value="{form.name}" />
|
||||
</label>
|
||||
<label>
|
||||
<span>{ t('Password') }</span>
|
||||
<input bind:value="{form.password}" type="password" />
|
||||
</label>
|
||||
<Bttn on:click="{handleClick}">submit</Bttn>
|
||||
</form>
|
||||
{/if}
|
||||
</div>
|
|
@ -1,9 +1,31 @@
|
|||
import { ipcRenderer } from 'electron';
|
||||
import IpcMessageEvent = Electron.IpcMessageEvent;
|
||||
|
||||
function sendCredentials(credentials: ICredentials): void {
|
||||
ipcRenderer.send(IpcChannels.Credentials, credentials);
|
||||
const ipcClient: IIpcClient = {
|
||||
ask: (channel: IpcChannels, data?: any): Promise<any> => {
|
||||
const payload: IIpcPayload = {
|
||||
data,
|
||||
};
|
||||
|
||||
return new Promise((resolve: (value?: any) => void, reject: (reason?: any) => void): void => {
|
||||
const listener = (event: IpcMessageEvent, response: IIpcResponse): void => {
|
||||
if (response.success) {
|
||||
resolve(response.data);
|
||||
} else {
|
||||
reject(response.error);
|
||||
}
|
||||
ipcRenderer.removeListener(channel, listener);
|
||||
};
|
||||
ipcRenderer.send(channel, payload);
|
||||
ipcRenderer.on(channel, listener);
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export function login(credentials: ICredentials): Promise<void> {
|
||||
return ipcClient.ask(IpcChannels.LOGIN, credentials);
|
||||
}
|
||||
|
||||
export default {
|
||||
sendCredentials,
|
||||
};
|
||||
export function isLoggedIn(): Promise<boolean> {
|
||||
return ipcClient.ask(IpcChannels.LOGGED_IN);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
import { writable } from 'svelte/store';
|
||||
import * as api from './api';
|
||||
|
||||
const { subscribe, set } = writable<boolean>(false);
|
||||
|
||||
export const loggedIn = {
|
||||
subscribe,
|
||||
fetchIsLoggedIn(): Promise<void> {
|
||||
return api.isLoggedIn().then((isLoggedIn: boolean) => {
|
||||
set(isLoggedIn);
|
||||
});
|
||||
},
|
||||
fetchLogin(credentials: ICredentials): Promise<void> {
|
||||
return api.login(credentials).then(() => {
|
||||
return this.fetchIsLoggedIn();
|
||||
});
|
||||
},
|
||||
};
|
|
@ -11,3 +11,8 @@ export function s(styles: object): string {
|
|||
.map((key: keyof object) => `${key}:${styles[key]}`)
|
||||
.join(';');
|
||||
}
|
||||
|
||||
export function t(text: string): string {
|
||||
// If you want to implement frontend translation, begin here.
|
||||
return text;
|
||||
}
|
||||
|
|
|
@ -2,16 +2,18 @@ export const enum Errors {
|
|||
ERROR = 'ERROR',
|
||||
ENOLOGIN = 'ENOLOGIN',
|
||||
ELOGINFAIL = 'ELOGINFAIL',
|
||||
EINITFAIL = 'EINITFAIL',
|
||||
}
|
||||
|
||||
const messages = {
|
||||
[Errors.ERROR]: 'error',
|
||||
[Errors.ERROR]: 'generic error',
|
||||
[Errors.ENOLOGIN]: 'no login form found',
|
||||
[Errors.ELOGINFAIL]: 'login failed',
|
||||
[Errors.EINITFAIL]: 'initialization failed',
|
||||
};
|
||||
|
||||
export default class RenaiError extends Error {
|
||||
export class RenaiError extends Error {
|
||||
constructor(eno: Errors = Errors.ERROR, msg: string = '') {
|
||||
super(`Error ${eno}: ${messages[eno]}.${msg ? ` ${msg}` : ''}`);
|
||||
super(`${messages[eno]}.${msg ? ` ${msg}` : ''}`);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,65 @@
|
|||
const enum HttpCode {
|
||||
// 100
|
||||
'CONTINUE' = 100,
|
||||
'SWITCHING_PROTOCOLS' = 101,
|
||||
'PROCESSING' = 102,
|
||||
|
||||
// 200
|
||||
'OK' = 200,
|
||||
'CREATED' = 201,
|
||||
'ACCEPTED' = 202,
|
||||
'NON_AUTHORITATIVE_INFORMATION' = 203,
|
||||
'NO_CONTENT' = 204,
|
||||
'RESET_CONTENT' = 205,
|
||||
'PARTIAL_CONTENT' = 206,
|
||||
'MULTI_STATUS' = 207,
|
||||
|
||||
// 300
|
||||
'MULTIPLE_CHOICES' = 300,
|
||||
'MOVED_PERMANENTLY' = 301,
|
||||
'MOVED_TEMPORARILY' = 302,
|
||||
'SEE_OTHER' = 303,
|
||||
'NOT_MODIFIED' = 304,
|
||||
'USE_PROXY' = 305,
|
||||
'TEMPORARY_REDIRECT' = 307,
|
||||
'PERMANENT_REDIRECT' = 308,
|
||||
|
||||
// 400
|
||||
'BAD_REQUEST' = 400,
|
||||
'UNAUTHORIZED' = 401,
|
||||
'PAYMENT_REQUIRED' = 402,
|
||||
'FORBIDDEN' = 403,
|
||||
'NOT_FOUND' = 404,
|
||||
'METHOD_NOT_ALLOWED' = 405,
|
||||
'NOT_ACCEPTABLE' = 406,
|
||||
'PROXY_AUTHENTICATION_REQUIRED' = 407,
|
||||
'REQUEST_TIMEOUT' = 408,
|
||||
'CONFLICT' = 409,
|
||||
'GONE' = 410,
|
||||
'LENGTH_REQUIRED' = 411,
|
||||
'PRECONDITION_FAILED' = 412,
|
||||
'REQUEST_TOO_LONG' = 413,
|
||||
'REQUEST_URI_TOO_LONG' = 414,
|
||||
'UNSUPPORTED_MEDIA_TYPE' = 415,
|
||||
'REQUESTED_RANGE_NOT_SATISFIABLE' = 416,
|
||||
'EXPECTATION_FAILED' = 417,
|
||||
'IM_A_TEAPOT' = 418,
|
||||
'INSUFFICIENT_SPACE_ON_RESOURCE' = 419,
|
||||
'METHOD_FAILURE' = 420, // nice
|
||||
'UNPROCESSABLE_ENTITY' = 422,
|
||||
'LOCKED' = 423,
|
||||
'FAILED_DEPENDENCY' = 424,
|
||||
'PRECONDITION_REQUIRED' = 428,
|
||||
'TOO_MANY_REQUESTS' = 429,
|
||||
'REQUEST_HEADER_FIELDS_TOO_LARGE' = 431,
|
||||
|
||||
// 500
|
||||
'INTERNAL_SERVER_ERROR' = 500,
|
||||
'NOT_IMPLEMENTED' = 501,
|
||||
'BAD_GATEWAY' = 502,
|
||||
'SERVICE_UNAVAILABLE' = 503,
|
||||
'GATEWAY_TIMEOUT' = 504,
|
||||
'HTTP_VERSION_NOT_SUPPORTED' = 505,
|
||||
'INSUFFICIENT_STORAGE' = 507,
|
||||
'NETWORK_AUTHENTICATION_REQUIRED' = 511,
|
||||
}
|
|
@ -1,8 +1,27 @@
|
|||
const enum IpcChannels {
|
||||
Credentials = 'CREDENTIALS',
|
||||
LOGIN = 'LOGIN',
|
||||
LOGGED_IN = 'LOGGED_IN',
|
||||
}
|
||||
|
||||
interface IIpcPayload {
|
||||
data: any;
|
||||
}
|
||||
|
||||
interface IIpcResponse {
|
||||
success: boolean;
|
||||
data?: any;
|
||||
error?: any;
|
||||
}
|
||||
|
||||
interface ICredentials {
|
||||
name: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
interface IIpcClient {
|
||||
ask: (channel: IpcChannels, data?: any) => Promise<any>;
|
||||
}
|
||||
|
||||
interface IIpcServer {
|
||||
answer: (channel: IpcChannels, handler: (data?: any) => Promise<any>) => void;
|
||||
}
|
||||
|
|
|
@ -25,7 +25,8 @@
|
|||
"no-empty": [true, "allow-empty-functions"],
|
||||
"no-magic-numbers": true,
|
||||
"no-parameter-reassignment": true,
|
||||
"arrow-return-shorthand": true
|
||||
"arrow-return-shorthand": true,
|
||||
"no-default-export": true
|
||||
},
|
||||
"jsRules": true
|
||||
}
|
||||
|
|
|
@ -27,10 +27,15 @@ module.exports = {
|
|||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
test: /\.mjs$/,
|
||||
include: /node_modules/,
|
||||
type: 'javascript/auto',
|
||||
},
|
||||
],
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['.js', '.ts'],
|
||||
extensions: ['.js', '.ts', '.mjs'],
|
||||
alias: {
|
||||
atoms: path.resolve(__dirname, 'src/renderer/components/1-atoms'),
|
||||
molecules: path.resolve(__dirname, 'src/renderer/components/2-molecules'),
|
||||
|
|
Loading…
Reference in New Issue