refactor: use react router

This commit is contained in:
ReaJason
2025-04-06 13:55:17 +08:00
parent cd231c5af7
commit fa206e8acc
12 changed files with 151 additions and 212 deletions
@@ -1,11 +1,9 @@
package com.reajason.javaweb.boot.controller; package com.reajason.javaweb.boot.controller;
import com.reajason.javaweb.boot.entity.Config;
import com.reajason.javaweb.memshell.Packers; import com.reajason.javaweb.memshell.Packers;
import com.reajason.javaweb.memshell.Server; import com.reajason.javaweb.memshell.Server;
import com.reajason.javaweb.memshell.ShellTool; import com.reajason.javaweb.memshell.ShellTool;
import com.reajason.javaweb.memshell.server.AbstractShell; import com.reajason.javaweb.memshell.server.AbstractShell;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.CrossOrigin; import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
@@ -20,8 +18,28 @@ import java.util.*;
@RequestMapping("/config") @RequestMapping("/config")
@CrossOrigin("*") @CrossOrigin("*")
public class ConfigController { public class ConfigController {
@RequestMapping("/servers")
public Map<String, List<String>> getServers() {
Map<String, List<String>> servers = new LinkedHashMap<>();
for (Server server : Server.values()) {
if (server.getShell() != null) {
Set<String> supportedShellTypes = server.getShell().getShellInjectorMapping().getSupportedShellTypes();
servers.put(server.name(), supportedShellTypes.stream().toList());
}
}
return servers;
}
@RequestMapping("/packers")
public List<String> getPackers() {
return Arrays.stream(Packers.values())
.filter(packers -> packers.getParentPacker() == null)
.map(Packers::name).toList();
}
@RequestMapping @RequestMapping
public ResponseEntity<?> config() { public Map<String, Map<?, ?>> config() {
Map<String, Map<?, ?>> coreMap = new HashMap<>(16); Map<String, Map<?, ?>> coreMap = new HashMap<>(16);
for (Server value : Server.values()) { for (Server value : Server.values()) {
AbstractShell shell = value.getShell(); AbstractShell shell = value.getShell();
@@ -38,21 +56,6 @@ public class ConfigController {
} }
coreMap.put(value.name(), map); coreMap.put(value.name(), map);
} }
Config config = new Config(); return coreMap;
Map<String, List<String>> servers = new LinkedHashMap<>();
for (Server server : Server.values()) {
if (server.getShell() != null) {
Set<String> supportedShellTypes = server.getShell().getShellInjectorMapping().getSupportedShellTypes();
servers.put(server.name(), supportedShellTypes.stream().toList());
}
}
config.setServers(servers);
config.setCore(coreMap);
config.setPackers(
Arrays.stream(Packers.values())
.filter(packers -> packers.getParentPacker() == null)
.map(Packers::name).toList()
);
return ResponseEntity.ok(config);
} }
} }
@@ -1,8 +1,8 @@
package com.reajason.javaweb.boot.controller; package com.reajason.javaweb.boot.controller;
import com.reajason.javaweb.memshell.MemShellGenerator;
import com.reajason.javaweb.boot.dto.GenerateRequest; import com.reajason.javaweb.boot.dto.GenerateRequest;
import com.reajason.javaweb.boot.dto.GenerateResponse; import com.reajason.javaweb.boot.dto.GenerateResponse;
import com.reajason.javaweb.memshell.MemShellGenerator;
import com.reajason.javaweb.memshell.config.GenerateResult; import com.reajason.javaweb.memshell.config.GenerateResult;
import com.reajason.javaweb.memshell.config.InjectorConfig; import com.reajason.javaweb.memshell.config.InjectorConfig;
import com.reajason.javaweb.memshell.config.ShellConfig; import com.reajason.javaweb.memshell.config.ShellConfig;
@@ -10,7 +10,6 @@ import com.reajason.javaweb.memshell.config.ShellToolConfig;
import com.reajason.javaweb.memshell.packer.AggregatePacker; import com.reajason.javaweb.memshell.packer.AggregatePacker;
import com.reajason.javaweb.memshell.packer.Packer; import com.reajason.javaweb.memshell.packer.Packer;
import com.reajason.javaweb.memshell.packer.jar.JarPacker; import com.reajason.javaweb.memshell.packer.jar.JarPacker;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import java.util.Base64; import java.util.Base64;
@@ -24,18 +23,18 @@ import java.util.Base64;
@CrossOrigin("*") @CrossOrigin("*")
public class GeneratorController { public class GeneratorController {
@PostMapping @PostMapping
public ResponseEntity<?> generate(@RequestBody GenerateRequest request) { public GenerateResponse generate(@RequestBody GenerateRequest request) {
ShellConfig shellConfig = request.getShellConfig(); ShellConfig shellConfig = request.getShellConfig();
ShellToolConfig shellToolConfig = request.parseShellToolConfig(); ShellToolConfig shellToolConfig = request.parseShellToolConfig();
InjectorConfig injectorConfig = request.getInjectorConfig(); InjectorConfig injectorConfig = request.getInjectorConfig();
GenerateResult generateResult = MemShellGenerator.generate(shellConfig, injectorConfig, shellToolConfig); GenerateResult generateResult = MemShellGenerator.generate(shellConfig, injectorConfig, shellToolConfig);
Packer packer = request.getPacker().getInstance(); Packer packer = request.getPacker().getInstance();
if (packer instanceof JarPacker) { if (packer instanceof JarPacker) {
return ResponseEntity.ok(new GenerateResponse(generateResult, Base64.getEncoder().encodeToString(((JarPacker) packer).packBytes(generateResult)))); return new GenerateResponse(generateResult, Base64.getEncoder().encodeToString(((JarPacker) packer).packBytes(generateResult)));
} else if (packer instanceof AggregatePacker) { } else if (packer instanceof AggregatePacker) {
return ResponseEntity.ok(new GenerateResponse(generateResult, ((AggregatePacker) packer).packAll(generateResult))); return new GenerateResponse(generateResult, ((AggregatePacker) packer).packAll(generateResult));
} else { } else {
return ResponseEntity.ok(new GenerateResponse(generateResult, packer.pack(generateResult))); return new GenerateResponse(generateResult, packer.pack(generateResult));
} }
} }
} }
BIN
View File
Binary file not shown.
+9 -12
View File
@@ -15,17 +15,16 @@
}, },
"devDependencies": { "devDependencies": {
"@biomejs/biome": "1.9.4", "@biomejs/biome": "1.9.4",
"@tanstack/router-plugin": "^1.114.32", "@types/node": "^22.14.0",
"@types/node": "^22.13.17",
"@types/react": "^19.1.0", "@types/react": "^19.1.0",
"@types/react-copy-to-clipboard": "^5.0.7", "@types/react-copy-to-clipboard": "^5.0.7",
"@types/react-dom": "^19.1.1", "@types/react-dom": "^19.1.1",
"@types/react-syntax-highlighter": "^15.5.13", "@types/react-syntax-highlighter": "^15.5.13",
"@vitejs/plugin-react": "^4.3.4", "@vitejs/plugin-react": "^4.3.4",
"rimraf": "^6.0.1", "rimraf": "^6.0.1",
"tailwindcss": "^4.1.1", "tailwindcss": "^4.1.3",
"typescript": "^5.8.2", "typescript": "^5.8.3",
"vite": "^6.2.4", "vite": "^6.2.5",
"vite-bundle-visualizer": "^1.2.1" "vite-bundle-visualizer": "^1.2.1"
}, },
"dependencies": { "dependencies": {
@@ -56,19 +55,15 @@
"@radix-ui/react-toggle": "^1.1.2", "@radix-ui/react-toggle": "^1.1.2",
"@radix-ui/react-toggle-group": "^1.1.2", "@radix-ui/react-toggle-group": "^1.1.2",
"@radix-ui/react-tooltip": "^1.1.8", "@radix-ui/react-tooltip": "^1.1.8",
"@tailwindcss/vite": "^4.1.1", "@tailwindcss/vite": "^4.1.3",
"@tanstack/react-query": "^5.71.3",
"@tanstack/react-router": "^1.114.32",
"@tanstack/router-devtools": "^1.114.32",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"cmdk": "^1.1.1", "cmdk": "^1.1.1",
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
"embla-carousel-react": "^8.5.2", "embla-carousel-react": "^8.6.0",
"i18next": "^24.2.3", "i18next": "^24.2.3",
"input-otp": "^1.4.2", "input-otp": "^1.4.2",
"lucide-react": "^0.487.0", "lucide-react": "^0.487.0",
"next-themes": "^0.4.6",
"react": "^19.1.0", "react": "^19.1.0",
"react-copy-to-clipboard": "^5.1.0", "react-copy-to-clipboard": "^5.1.0",
"react-day-picker": "9.6.4", "react-day-picker": "9.6.4",
@@ -76,8 +71,10 @@
"react-hook-form": "^7.55.0", "react-hook-form": "^7.55.0",
"react-i18next": "^15.4.1", "react-i18next": "^15.4.1",
"react-resizable-panels": "^2.1.7", "react-resizable-panels": "^2.1.7",
"react-router": "^7.5.0",
"react-router-dom": "^7.5.0",
"react-syntax-highlighter": "^15.6.1", "react-syntax-highlighter": "^15.6.1",
"recharts": "^2.15.1", "recharts": "^2.15.2",
"sonner": "^2.0.3", "sonner": "^2.0.3",
"tailwind-merge": "^3.1.0", "tailwind-merge": "^3.1.0",
"tailwind-scrollbar": "^4.0.2", "tailwind-scrollbar": "^4.0.2",
@@ -1,17 +1,14 @@
import { LanguageSwitcher } from "@/components/language-switcher"; import { LanguageSwitcher } from "@/components/language-switcher";
import { ModeToggle } from "@/components/mode-toggle.tsx"; import { ModeToggle } from "@/components/mode-toggle.tsx";
import { ThemeProvider } from "@/components/theme-provider.tsx"; import { ThemeProvider } from "@/components/theme-provider.tsx";
import { Button } from "@/components/ui/button.tsx";
import { Toaster } from "@/components/ui/sonner"; import { Toaster } from "@/components/ui/sonner";
import VersionBadge from "@/components/version-badge"; import VersionBadge from "@/components/version-badge";
import { GitHubIcon } from "@/icon"; import { GitHubIcon } from "@/icon";
import { Outlet, createRootRoute } from "@tanstack/react-router"; import { Button } from "@/components/ui/button";
import { Outlet } from "react-router-dom";
export const Route = createRootRoute({
component: RootComponent,
});
function RootComponent() { export default function RootLayout() {
return ( return (
<ThemeProvider defaultTheme="system" storageKey="vite-ui-theme"> <ThemeProvider defaultTheme="system" storageKey="vite-ui-theme">
<Toaster /> <Toaster />
@@ -42,4 +39,4 @@ function RootComponent() {
</div> </div>
</ThemeProvider> </ThemeProvider>
); );
} }
+5 -19
View File
@@ -1,37 +1,23 @@
import { RouterProvider, createRouter } from "@tanstack/react-router";
import ReactDOM from "react-dom/client"; import ReactDOM from "react-dom/client";
import { routeTree } from "./routeTree.gen";
import "./index.css"; import "./index.css";
import { TailwindIndicator } from "@/components/tailwind-indicator.tsx"; import { TailwindIndicator } from "@/components/tailwind-indicator.tsx";
import { env } from "@/config.ts"; import { env } from "@/config.ts";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { I18nextProvider } from "react-i18next"; import { I18nextProvider } from "react-i18next";
import i18n from "./i18n/i18n"; import i18n from "./i18n/i18n";
import { RouterProvider } from "react-router-dom";
const queryClient = new QueryClient(); import { router } from "./router";
import { QueryProvider } from "./providers/query-client-provider";
const router = createRouter({
routeTree,
basepath: env.BASE_PATH,
defaultPreload: "intent",
});
declare module "@tanstack/react-router" {
interface Register {
router: typeof router;
}
}
const rootElement = document.getElementById("app") as HTMLElement; const rootElement = document.getElementById("app") as HTMLElement;
if (!rootElement.innerHTML) { if (!rootElement.innerHTML) {
const root = ReactDOM.createRoot(rootElement); const root = ReactDOM.createRoot(rootElement);
root.render( root.render(
<QueryClientProvider client={queryClient}> <QueryProvider>
<I18nextProvider i18n={i18n}> <I18nextProvider i18n={i18n}>
<RouterProvider router={router} /> <RouterProvider router={router} />
{env.MODE !== "production" && <TailwindIndicator />} {env.MODE !== "production" && <TailwindIndicator />}
</I18nextProvider> </I18nextProvider>
</QueryClientProvider>, </QueryProvider>
); );
} }
@@ -10,41 +10,56 @@ import {
ConfigResponseType, ConfigResponseType,
GenerateResponse, GenerateResponse,
GenerateResult, GenerateResult,
MainConfig,
PackerConfig,
ServerConfig,
ShellToolType, ShellToolType,
} from "@/types/shell.ts"; } from "@/types/shell.ts";
import { transformToPostData } from "@/utils/transformer.ts"; import { customValidation, transformToPostData } from "@/utils/transformer.ts";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { useQuery } from "@tanstack/react-query";
import { createFileRoute } from "@tanstack/react-router";
import { LoaderCircle, WandSparklesIcon } from "lucide-react"; import { LoaderCircle, WandSparklesIcon } from "lucide-react";
import { useState, useTransition } from "react"; import { useState, useTransition } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { toast } from "sonner"; import { toast } from "sonner";
import { useQuery } from "@tanstack/react-query";
export const Route = createFileRoute("/")({ export default function IndexPage() {
component: IndexComponent, const { data: serverConfig } = useQuery<ServerConfig>({
}); queryKey: ["serverConfig"],
queryFn: async () => {
const response = await fetch(`${env.API_URL}/config/servers`);
return await response.json();
},
});
function IndexComponent() { const { data: mainConfig } = useQuery<MainConfig>({
const { data } = useQuery<ConfigResponseType>({ queryKey: ["mainConfig"],
queryKey: ["config"],
queryFn: async () => { queryFn: async () => {
const response = await fetch(`${env.API_URL}/config`); const response = await fetch(`${env.API_URL}/config`);
return await response.json(); return await response.json();
}, },
}); });
const { data: packerConfig } = useQuery<PackerConfig>({
queryKey: ["packerConfig"],
queryFn: async () => {
const response = await fetch(`${env.API_URL}/config/packers`);
return await response.json();
},
});
const { t } = useTranslation(); const { t } = useTranslation();
const form = useForm<FormSchema>({ const form = useForm<FormSchema>({
resolver: zodResolver(formSchema), resolver: zodResolver(formSchema),
defaultValues: { defaultValues: {
server: "", server: "Tomcat",
targetJdkVersion: "50", targetJdkVersion: "50",
debug: false, debug: false,
bypassJavaModule: false, bypassJavaModule: false,
shellClassName: "", shellClassName: "",
shellTool: "Godzilla", shellTool: "Godzilla",
shellType: "", shellType: "Listener",
urlPattern: "/*", urlPattern: "/*",
godzillaPass: "pass", godzillaPass: "pass",
godzillaKey: "key", godzillaKey: "key",
@@ -64,40 +79,11 @@ function IndexComponent() {
const [packMethod, setPackMethod] = useState<string>(""); const [packMethod, setPackMethod] = useState<string>("");
const [isActionPending, startTransition] = useTransition(); const [isActionPending, startTransition] = useTransition();
function customValidation(values: FormSchema) { const onSubmit = async (data: FormSchema) => {
if (values.shellType.endsWith("Servlet") && (values.urlPattern === "/*" || !values.urlPattern)) {
toast.warning(t("tips.servletUrlPattern"));
return false;
}
if (values.shellType.endsWith("ControllerHandler") && (values.urlPattern === "/*" || !values.urlPattern)) {
toast.warning(t("tips.controllerUrlPattern"));
return false;
}
if (
(values.shellType === "HandlerMethod" || values.shellType === "HandlerFunction") &&
(values.urlPattern === "/*" || !values.urlPattern)
) {
toast.warning(t("tips.handlerUrlPattern"));
return false;
}
if (values.shellTool === ShellToolType.Custom && !values.shellClassBase64) {
toast.warning(t("tips.customShellClass"));
return false;
}
return true;
}
async function onSubmit(values: FormSchema) {
startTransition(async () => { startTransition(async () => {
if (!customValidation(values)) {
return;
}
const postData = transformToPostData(values);
try { try {
customValidation(t, data);
const postData = transformToPostData(data);
const response = await fetch(`${env.API_URL}/generate`, { const response = await fetch(`${env.API_URL}/generate`, {
method: "POST", method: "POST",
headers: { headers: {
@@ -105,31 +91,30 @@ function IndexComponent() {
}, },
body: JSON.stringify(postData), body: JSON.stringify(postData),
}); });
await new Promise((resolve) => setTimeout(resolve, 200));
if (response.ok) { if (!response.ok) {
const json: GenerateResponse = await response.json();
setPackResult(json.packResult);
setAllPackResults(json.allPackResults);
setGenerateResult(json.generateResult);
setPackMethod(values.packingMethod);
toast.success(t("success.generated"));
} else {
const json: APIErrorResponse = await response.json(); const json: APIErrorResponse = await response.json();
toast.error(t("errors.generationFailed", { error: json.error })); toast.error(t("errors.generationFailed", { error: json.error }));
} }
} catch (err) {
const error = err as Error; const result = (await response.json()) as GenerateResponse;
toast.error(t("errors.generationFailed", { error: error.message })); setGenerateResult(result.generateResult);
setPackResult(result.packResult);
setAllPackResults(result.allPackResults);
setPackMethod(data.packingMethod);
toast.success(t("success.generated"));
} catch (error) {
toast.error(t("errors.generationFailed", { error: (error as Error).message }));
} }
}); });
} };
return ( return (
<Form {...form}> <Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="flex flex-col xl:flex-row gap-4 p-4"> <form onSubmit={form.handleSubmit(onSubmit)} className="flex flex-col xl:flex-row gap-4 p-4">
<div className="w-full xl:w-1/2 space-y-4"> <div className="w-full xl:w-1/2 space-y-4">
<MainConfigCard servers={data?.servers} mainConfig={data?.core} form={form} /> <MainConfigCard servers={serverConfig} mainConfig={mainConfig} form={form} />
<PackageConfigCard packerConfig={data?.packers} form={form} /> <PackageConfigCard packerConfig={packerConfig} form={form} />
<Button className="w-full" type="submit" disabled={isActionPending}> <Button className="w-full" type="submit" disabled={isActionPending}>
{isActionPending ? <LoaderCircle className="animate-spin" /> : <WandSparklesIcon />} {isActionPending ? <LoaderCircle className="animate-spin" /> : <WandSparklesIcon />}
{t("buttons.generate")} {t("buttons.generate")}
@@ -146,4 +131,4 @@ function IndexComponent() {
</form> </form>
</Form> </Form>
); );
} }
@@ -0,0 +1,15 @@
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ReactNode } from "react";
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60 * 5, // 5 minutes
retry: 1,
},
},
});
export function QueryProvider({ children }: { children: ReactNode }) {
return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>;
}
-86
View File
@@ -1,86 +0,0 @@
/* eslint-disable */
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// This file was automatically generated by TanStack Router.
// You should NOT make any changes in this file as it will be overwritten.
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
// Import Routes
import { Route as rootRoute } from "./routes/__root";
import { Route as IndexImport } from "./routes/index";
// Create/Update Routes
const IndexRoute = IndexImport.update({
id: "/",
path: "/",
getParentRoute: () => rootRoute,
} as any);
// Populate the FileRoutesByPath interface
declare module "@tanstack/react-router" {
interface FileRoutesByPath {
"/": {
id: "/";
path: "/";
fullPath: "/";
preLoaderRoute: typeof IndexImport;
parentRoute: typeof rootRoute;
};
}
}
// Create and export the route tree
export interface FileRoutesByFullPath {
"/": typeof IndexRoute;
}
export interface FileRoutesByTo {
"/": typeof IndexRoute;
}
export interface FileRoutesById {
__root__: typeof rootRoute;
"/": typeof IndexRoute;
}
export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath;
fullPaths: "/";
fileRoutesByTo: FileRoutesByTo;
to: "/";
id: "__root__" | "/";
fileRoutesById: FileRoutesById;
}
export interface RootRouteChildren {
IndexRoute: typeof IndexRoute;
}
const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute,
};
export const routeTree = rootRoute._addFileChildren(rootRouteChildren)._addFileTypes<FileRouteTypes>();
/* ROUTE_MANIFEST_START
{
"routes": {
"__root__": {
"filePath": "__root.tsx",
"children": [
"/"
]
},
"/": {
"filePath": "index.tsx"
}
}
}
ROUTE_MANIFEST_END */
+22
View File
@@ -0,0 +1,22 @@
import { createBrowserRouter } from "react-router-dom";
import { env } from "@/config";
import RootLayout from "@/components/layouts/root-layout";
import IndexPage from "@/pages";
export const router = createBrowserRouter(
[
{
path: "/",
element: <RootLayout />,
children: [
{
index: true,
element: <IndexPage />,
},
],
},
],
{
basename: env.BASE_PATH,
}
);
+23 -1
View File
@@ -1,5 +1,27 @@
import { FormSchema } from "@/types/schema.ts"; import { FormSchema } from "@/types/schema.ts";
import { InjectorConfig, ShellConfig, ShellToolConfig } from "@/types/shell.ts"; import { InjectorConfig, ShellConfig, ShellToolConfig, ShellToolType } from "@/types/shell.ts";
import { TFunction } from "i18next";
export function customValidation(t: TFunction<"translation", undefined>, values: FormSchema) {
if (values.shellType.endsWith("Servlet") && (values.urlPattern === "/*" || !values.urlPattern)) {
throw new Error(t("tips.servletUrlPattern"));
}
if (values.shellType.endsWith("ControllerHandler") && (values.urlPattern === "/*" || !values.urlPattern)) {
throw new Error(t("tips.controllerUrlPattern"));
}
if (
(values.shellType === "HandlerMethod" || values.shellType === "HandlerFunction") &&
(values.urlPattern === "/*" || !values.urlPattern)
) {
throw new Error(t("tips.handlerUrlPattern"));
}
if (values.shellTool === ShellToolType.Custom && !values.shellClassBase64) {
throw new Error(t("tips.customShellClass"));
}
}
export function transformToPostData(formValue: FormSchema) { export function transformToPostData(formValue: FormSchema) {
const shellConfig: ShellConfig = { const shellConfig: ShellConfig = {
+1 -2
View File
@@ -1,12 +1,11 @@
import path from "node:path"; import path from "node:path";
import tailwindcss from "@tailwindcss/vite"; import tailwindcss from "@tailwindcss/vite";
import { TanStackRouterVite } from "@tanstack/router-plugin/vite";
import react from "@vitejs/plugin-react"; import react from "@vitejs/plugin-react";
import { defineConfig } from "vite"; import { defineConfig } from "vite";
// https://vitejs.dev/config/ // https://vitejs.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [TanStackRouterVite({ autoCodeSplitting: true }), react(), tailwindcss()], plugins: [react(), tailwindcss()],
resolve: { resolve: {
alias: { alias: {
"@": path.resolve(__dirname, "./src"), "@": path.resolve(__dirname, "./src"),