From 4f24b9ce86ae8cc61587ec3d982367466ccb4e87 Mon Sep 17 00:00:00 2001 From: Jan Laupetin Date: Mon, 6 Oct 2025 21:19:02 +0200 Subject: [PATCH] chore: update modman vite setup to support dev server --- src/ModMan.lua | 2 +- src/ModMan/Web/Edge/AssetHandlerEdge.cpp | 9 + src/ModMan/Web/UiAssets.h | 2 +- src/ModMan/main.cpp | 12 +- .../build/HeaderTransformationPlugin.ts | 211 +++++++++++------- src/ModManUi/src/native.ts | 7 +- src/ModManUi/vite.config.ts | 17 +- 7 files changed, 164 insertions(+), 96 deletions(-) diff --git a/src/ModMan.lua b/src/ModMan.lua index 7aae5f18..1de3aa1a 100644 --- a/src/ModMan.lua +++ b/src/ModMan.lua @@ -37,7 +37,7 @@ function ModMan:project() } includedirs { - "%{prj.location}" + "%{wks.location}/src/ModMan" } filter { "system:linux", "action:gmake" } diff --git a/src/ModMan/Web/Edge/AssetHandlerEdge.cpp b/src/ModMan/Web/Edge/AssetHandlerEdge.cpp index 95edb6bf..9f57d808 100644 --- a/src/ModMan/Web/Edge/AssetHandlerEdge.cpp +++ b/src/ModMan/Web/Edge/AssetHandlerEdge.cpp @@ -18,6 +18,8 @@ namespace { + constexpr auto LOCALHOST_PREFIX = "http://localhost:"; + std::unordered_map assetLookup; std::string WideStringToString(const std::wstring& wideString) @@ -95,6 +97,13 @@ namespace const auto uri = WideStringToString(wUri); bool fileFound = false; + +#ifdef _DEBUG + // Allow dev server access + if (uri.starts_with(LOCALHOST_PREFIX)) + return S_OK; +#endif + if (uri.starts_with(edge::URL_PREFIX)) { const auto asset = uri.substr(std::char_traits::length(edge::URL_PREFIX) - 1); diff --git a/src/ModMan/Web/UiAssets.h b/src/ModMan/Web/UiAssets.h index 2bc9542b..13e838a2 100644 --- a/src/ModMan/Web/UiAssets.h +++ b/src/ModMan/Web/UiAssets.h @@ -1,6 +1,6 @@ #pragma once -#include "ui/modmanui.h" +#include "Web/ViteAssets.h" #include #include diff --git a/src/ModMan/main.cpp b/src/ModMan/main.cpp index 39b11a55..562638b3 100644 --- a/src/ModMan/main.cpp +++ b/src/ModMan/main.cpp @@ -2,6 +2,7 @@ #include "webview/webview.h" #pragma warning(pop) +#include "Web/ViteAssets.h" #include "Web/Edge/AssetHandlerEdge.h" #include "Web/Gtk/AssetHandlerGtk.h" @@ -58,12 +59,19 @@ int main() #if defined(WEBVIEW_PLATFORM_WINDOWS) && defined(WEBVIEW_EDGE) edge::InstallCustomProtocolHandler(w); - w.navigate(edge::URL_PREFIX + "index.html"s); + constexpr auto urlPrefix = edge::URL_PREFIX; #elif defined(WEBVIEW_PLATFORM_LINUX) && defined(WEBVIEW_GTK) gtk::InstallCustomProtocolHandler(w); - w.navigate(gtk::URL_PREFIX + "index.html"s); + constexpr auto urlPrefix = gtk::URL_PREFIX; +#else +#error Unsupported platform #endif +#ifdef _DEBUG + w.navigate(VITE_DEV_SERVER ? std::format("http://localhost:{}", VITE_DEV_SERVER_PORT) : std::format("{}index.html", urlPrefix)); +#else + w.navigate(std::format("{}index.html", urlPrefix)); +#endif w.run(); } catch (const webview::exception& e) diff --git a/src/ModManUi/build/HeaderTransformationPlugin.ts b/src/ModManUi/build/HeaderTransformationPlugin.ts index bf1f4706..4d6fba3c 100644 --- a/src/ModManUi/build/HeaderTransformationPlugin.ts +++ b/src/ModManUi/build/HeaderTransformationPlugin.ts @@ -1,84 +1,88 @@ -import type { Plugin, ViteDevServer } from "vite"; -import type { OutputOptions, OutputBundle, OutputAsset, OutputChunk } from "rollup"; +import type { Plugin } from "vite"; +import type { OutputAsset, OutputChunk } from "rollup"; import path from "node:path"; import fs from "node:fs"; -function createTransformedTextSource(varName: string, previousSource: string) { - const str = [...previousSource] - .map((v) => `0x${v.charCodeAt(0).toString(16).padStart(2, "0")}`) - .join(", "); - return `#pragma once - -static inline const unsigned char ${varName}[] { -${str} -}; -`; -} +type MinimalOutputAsset = Pick; +type MinimalOutputChunk = Pick; +type MinimalOutputBundle = Record; function createVarName(fileName: string) { return fileName.replaceAll(".", "_").toUpperCase(); } -function transformAsset(asset: OutputAsset) { - const varName = createVarName(asset.names[0]); +function transformAsset(asset: MinimalOutputAsset) { + const varName = createVarName(asset.fileName); + let bytes: string; if (typeof asset.source === "string") { - asset.source = createTransformedTextSource(varName, asset.source); + bytes = [...asset.source].map((v) => String(v.charCodeAt(0))).join(","); } else { - const str = [...asset.source].map((v) => `0x${v.toString(16).padStart(2, "0")}`).join(", "); - asset.source = `#pragma once - -static inline const unsigned char ${varName}[] { -${str} -}; -`; + bytes = [...asset.source].map((v) => String(v)).join(","); } - return varName; + return `constexpr const unsigned char ${varName}[] {${bytes}}; +`; } -function transformChunk(chunk: OutputChunk) { +function transformChunk(chunk: MinimalOutputChunk) { const varName = createVarName(chunk.fileName); - chunk.code = createTransformedTextSource(varName, chunk.code); - return varName; + const bytes = [...chunk.code].map((v) => String(v.charCodeAt(0))).join(","); + + return `constexpr const unsigned char ${varName}[] {${bytes}}; +`; } -export function headerTransformationPlugin(): Plugin { - return { - name: "header-transformation", - apply: "build", - generateBundle(options: OutputOptions, bundle: OutputBundle, isWrite: boolean) { - const includesStr: string[] = [`#include "index.html.h"`]; - const uiFilesStr: string[] = [ - `{ "index.html", INDEX_HTML, std::extent_v }`, - ]; +function writeHeader( + bundle: MinimalOutputBundle, + outputDir?: string, + options?: HeaderTransformationPluginOptions, + devServerPort?: number, +) { + const outputPath = options?.outputPath ?? path.join(outputDir ?? "dist", "ViteAssets.h"); + const outputPathParentDir = path.dirname(outputPath); - for (const curBundle of Object.values(bundle)) { - let varName: string; - if (curBundle.type === "asset") { - varName = transformAsset(curBundle); - } else { - varName = transformChunk(curBundle); - } + fs.mkdirSync(outputPathParentDir, { recursive: true }); - includesStr.push(`#include "${curBundle.fileName}.h"`); - uiFilesStr.push( - `{ "${curBundle.fileName}", ${varName}, std::extent_v }`, - ); + const fd = fs.openSync(outputPath, "w"); + const includeFileEnumeration = options?.includeFileEnumeration ?? true; - curBundle.fileName = `${curBundle.fileName}.h`; - } + fs.writeSync( + fd, + `#pragma once - this.emitFile({ - type: "asset", - fileName: "modmanui.h", - source: `#pragma once +`, + ); -${includesStr.join("\n")} - -#include + if (includeFileEnumeration) { + fs.writeSync( + fd, + `#include #include +`, + ); + } + + fs.writeSync( + fd, + `constexpr auto VITE_DEV_SERVER = ${devServerPort ? "true" : "false"}; +constexpr auto VITE_DEV_SERVER_PORT = ${devServerPort ? String(devServerPort) : "-1"}; +`, + ); + + for (const curBundle of Object.values(bundle)) { + if (curBundle.type === "asset") { + fs.writeSync(fd, transformAsset(curBundle)); + } else { + fs.writeSync(fd, transformChunk(curBundle)); + } + } + + if (includeFileEnumeration) { + fs.writeSync( + fd, + ` struct UiFile { const char* filename; @@ -87,36 +91,81 @@ struct UiFile }; static inline const UiFile MOD_MAN_UI_FILES[] { -${uiFilesStr.join(",\n")} +`, + ); + + let index = 0; + for (const curBundle of Object.values(bundle)) { + const fileName = curBundle.fileName; + const varName = createVarName(fileName); + + let prefix = " "; + if (index > 0) { + prefix = `, + `; + } + + fs.writeSync( + fd, + `${prefix}{ "${fileName}", ${varName}, std::extent_v }`, + ); + index++; + } + + fs.writeSync( + fd, + ` }; `, + ); + + fs.closeSync(fd); + } +} + +export interface HeaderTransformationPluginOptions { + outputPath?: string; + includeFileEnumeration?: boolean; +} + +export default function headerTransformationPlugin( + options?: HeaderTransformationPluginOptions, +): Plugin { + let writeServerActive = false; + let writeBundleActive = false; + + return { + name: "vite-plugin-header-transformation", + enforce: "post", + config(_userOptions, env) { + if (env.command === "serve") { + writeServerActive = true; + } else { + writeBundleActive = true; + } + }, + configureServer(server) { + if (!writeServerActive) { + return; + } + server.httpServer?.once("listening", () => { + writeHeader( + { + // We need at least one array entry for MSVC + dummyfile: { type: "chunk", fileName: "dummyfile", code: "dummy" }, + }, + server.config.build.outDir, + options, + server.config.server.port, + ); }); }, - transformIndexHtml( - html: string, - ctx: { - path: string; - filename: string; - server?: ViteDevServer; - bundle?: OutputBundle; - chunk?: OutputChunk; - }, - ) { - html = html.replaceAll("index.js.h", "index.js"); - - html = createTransformedTextSource(createVarName("index.html"), html); - ctx.filename = `${ctx.filename}.h`; - - return html; - }, - writeBundle(options, bundle) { - for (const curBundle of Object.values(bundle)) { - if (curBundle.fileName === "index.html" && curBundle.type === "asset") { - const outputFilePath = path.join(options.dir!, curBundle.fileName); - fs.renameSync(outputFilePath, outputFilePath + ".h"); - curBundle.fileName += ".h"; - } + writeBundle(outputOptions, bundle) { + if (!writeBundleActive) { + return; } + + writeHeader(bundle, outputOptions.dir, options); }, }; } diff --git a/src/ModManUi/src/native.ts b/src/ModManUi/src/native.ts index 0b3879b9..f2531ffc 100644 --- a/src/ModManUi/src/native.ts +++ b/src/ModManUi/src/native.ts @@ -1,7 +1,6 @@ -export interface NativeMethods{ - - greet: (name: string) => Promise; +export interface NativeMethods { + greet: (name: string) => Promise; } -// @ts-expect-error +// @ts-expect-error Typescript expects this to be an error, it is not here though export const nativeMethods: NativeMethods = window as NativeMethods; diff --git a/src/ModManUi/vite.config.ts b/src/ModManUi/vite.config.ts index 41388585..f5edcd61 100644 --- a/src/ModManUi/vite.config.ts +++ b/src/ModManUi/vite.config.ts @@ -3,12 +3,11 @@ import { fileURLToPath, URL } from "node:url"; import { defineConfig } from "vite"; import vue from "@vitejs/plugin-vue"; import vueDevTools from "vite-plugin-vue-devtools"; -import { headerTransformationPlugin } from "./build/HeaderTransformationPlugin"; +import headerTransformationPlugin from "./build/HeaderTransformationPlugin"; // https://vite.dev/config/ export default defineConfig({ build: { - outDir: "../../build/src/ModMan/ui", copyPublicDir: false, emptyOutDir: true, rollupOptions: { @@ -19,14 +18,18 @@ export default defineConfig({ }, }, }, - plugins: [vue(), vueDevTools(), headerTransformationPlugin()], + plugins: [ + vue(), + vueDevTools(), + headerTransformationPlugin({ + outputPath: fileURLToPath( + new URL("../../build/src/ModMan/Web/ViteAssets.h", import.meta.url), + ), + }), + ], resolve: { alias: { "@": fileURLToPath(new URL("./src", import.meta.url)), }, }, - server: { - port: 1420, - strictPort: true, - }, });