From 50c5afe011a73b4c63c6d3d821c29c58bc53783a Mon Sep 17 00:00:00 2001 From: Christopher Serr Date: Sun, 26 Jul 2026 21:50:15 +0200 Subject: [PATCH 1/2] Build Only the Needed Core API Select the parsing, timing, editing, layout, hotkey, web rendering, server protocol, and therun.gg C API groups used by the frontend. Pass the same feature set to the binding generator and Wasm build so unused exports and wrappers are omitted. Generate the subset bindings in a separate target directory instead of rewriting livesplit-core's canonical bindings. --- buildCore.js | 33 ++++++++++++++++++++++++++------- livesplit-core | 2 +- 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/buildCore.js b/buildCore.js index f1635fc2..35d94b86 100644 --- a/buildCore.js +++ b/buildCore.js @@ -10,6 +10,22 @@ let rustFlags = let wasmBindgenFlags = "--encode-into always --target web --reference-types"; let target = "wasm32-unknown-unknown"; let targetFolder = target; +const coreFeatures = [ + "parsing", + "run-saving", + "timing", + "run-editing", + "layouts", + "layout-editing", + "hotkeys", + "web-command-sink", + "server-protocol", + "web-rendering", + "therun-gg", + "localization", + "image-shrinking", +].join(","); +const bindingsDir = "target/livesplit-one-bindings"; if (process.argv.some((v) => v === "--max-opt")) { // Do a fully optimized build ready for deployment. @@ -57,13 +73,16 @@ if (process.argv.some((v) => v === "--nightly")) { } } -execSync(`cargo ${toolchain} run`, { - cwd: "livesplit-core/capi/bind_gen", - stdio: "inherit", -}); +execSync( + `cargo ${toolchain} run -- --no-default-features --features ${coreFeatures} --output-dir ${bindingsDir}`, + { + cwd: "livesplit-core/capi/bind_gen", + stdio: "inherit", + }, +); execSync( - `cargo ${toolchain} rustc -p livesplit-core-capi --crate-type cdylib --features wasm-web,web-rendering,therun-gg --target ${target} ${cargoFlags}`, + `cargo ${toolchain} rustc -p livesplit-core-capi --crate-type cdylib --no-default-features --features ${coreFeatures} --target ${target} ${cargoFlags}`, { cwd: "livesplit-core", stdio: "inherit", @@ -82,9 +101,9 @@ execSync( ); fs.createReadStream( - "livesplit-core/capi/bindings/wasm_bindgen/web/index.ts", + `livesplit-core/capi/bind_gen/${bindingsDir}/wasm_bindgen/web/index.ts`, ).pipe(fs.createWriteStream("src/livesplit-core/index.ts")); fs.createReadStream( - "livesplit-core/capi/bindings/wasm_bindgen/web/preload.ts", + `livesplit-core/capi/bind_gen/${bindingsDir}/wasm_bindgen/web/preload.ts`, ).pipe(fs.createWriteStream("src/livesplit-core/preload.ts")); diff --git a/livesplit-core b/livesplit-core index 0c4fdbb6..16fecb9f 160000 --- a/livesplit-core +++ b/livesplit-core @@ -1 +1 @@ -Subproject commit 0c4fdbb6753892b66ae53bbf881184edc9be67a1 +Subproject commit 16fecb9f2c15796d9c6ebd142d92150373fc8bad From 1f626666d4033214676cc6a189619271cdaf7c6c Mon Sep 17 00:00:00 2001 From: Christopher Serr Date: Thu, 6 Aug 2026 19:16:10 +0200 Subject: [PATCH 2/2] Migrate Build Tooling to TypeScript Rename the core build and ESLint configuration files to TypeScript and include the root tooling in type-aware linting and the Node TypeScript project. Add jiti so ESLint can load its TypeScript configuration, and tighten the Vite configuration types exposed by the broader lint scope. Advance livesplit-core to the merged master containing configurable C API features and the subsplit scrolling fix. The explicit frontend feature set continues to generate the bindings and WebAssembly without tracked output changes. --- buildCore.js => buildCore.ts | 8 +- eslint.config.mjs | 36 -------- eslint.config.ts | 47 ++++++++++ livesplit-core | 2 +- package-lock.json | 1 + package.json | 9 +- tsconfig.node.json | 7 +- vite.config.ts | 162 +++++++++++++++++++++-------------- 8 files changed, 161 insertions(+), 111 deletions(-) rename buildCore.js => buildCore.ts (96%) delete mode 100644 eslint.config.mjs create mode 100644 eslint.config.ts diff --git a/buildCore.js b/buildCore.ts similarity index 96% rename from buildCore.js rename to buildCore.ts index 35d94b86..895d0002 100644 --- a/buildCore.js +++ b/buildCore.ts @@ -1,5 +1,5 @@ -import { execSync } from "child_process"; -import fs from "fs"; +import { execSync } from "node:child_process"; +import fs from "node:fs"; let toolchain = ""; let profile = "debug"; @@ -8,8 +8,8 @@ let cargoFlags = ""; let rustFlags = "-C target-feature=+bulk-memory,+mutable-globals,+nontrapping-fptoint,+sign-ext,+simd128,+extended-const,+multivalue,+reference-types,+tail-call"; let wasmBindgenFlags = "--encode-into always --target web --reference-types"; -let target = "wasm32-unknown-unknown"; -let targetFolder = target; +const target = "wasm32-unknown-unknown"; +const targetFolder = target; const coreFeatures = [ "parsing", "run-saving", diff --git a/eslint.config.mjs b/eslint.config.mjs deleted file mode 100644 index 82f91f18..00000000 --- a/eslint.config.mjs +++ /dev/null @@ -1,36 +0,0 @@ -import tseslint from "typescript-eslint"; - -export default tseslint.config({ - ignores: ["src/livesplit-core/**", "src/type-definitions/**"], - extends: tseslint.configs.recommendedTypeChecked, - rules: { - "@typescript-eslint/consistent-type-exports": "error", - "@typescript-eslint/consistent-type-imports": [ - "error", - { fixStyle: "inline-type-imports" }, - ], - "@typescript-eslint/no-explicit-any": "off", - "@typescript-eslint/no-floating-promises": "off", - "@typescript-eslint/no-misused-promises": "off", - "@typescript-eslint/no-unsafe-argument": "off", - "@typescript-eslint/no-unsafe-assignment": "off", - "@typescript-eslint/no-unsafe-call": "off", - "@typescript-eslint/no-unsafe-member-access": "off", - "@typescript-eslint/no-unsafe-return": "off", - "@typescript-eslint/no-unused-vars": [ - "error", - { - argsIgnorePattern: "^_", - destructuredArrayIgnorePattern: "^_", - varsIgnorePattern: "^_", - caughtErrorsIgnorePattern: "^_", - }, - ], - }, - languageOptions: { - parserOptions: { - projectService: true, - tsconfigRootDir: import.meta.dirname, - }, - }, -}); diff --git a/eslint.config.ts b/eslint.config.ts new file mode 100644 index 00000000..7d33a7cf --- /dev/null +++ b/eslint.config.ts @@ -0,0 +1,47 @@ +import tseslint from "typescript-eslint"; + +export default tseslint.config({ + ignores: ["src/livesplit-core/**", "src/type-definitions/**"], + extends: tseslint.configs.recommendedTypeChecked, + rules: { + "@typescript-eslint/consistent-type-exports": "error", + "@typescript-eslint/consistent-type-imports": [ + "error", + { fixStyle: "inline-type-imports" }, + ], + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-floating-promises": "off", + "@typescript-eslint/no-misused-promises": "off", + "@typescript-eslint/no-unsafe-argument": "off", + "@typescript-eslint/no-unsafe-assignment": "off", + "@typescript-eslint/no-unsafe-call": "off", + "@typescript-eslint/no-unsafe-member-access": "off", + "@typescript-eslint/no-unsafe-return": "off", + "@typescript-eslint/no-unused-vars": [ + "error", + { + argsIgnorePattern: "^_", + destructuredArrayIgnorePattern: "^_", + varsIgnorePattern: "^_", + caughtErrorsIgnorePattern: "^_", + }, + ], + }, + languageOptions: { + parserOptions: { + // The project service only discovers files from conventionally + // named tsconfig.json files. These root-level scripts instead + // belong to tsconfig.node.json, so opt them into the service's + // default project for editor linting. + projectService: { + allowDefaultProject: [ + "buildCore.ts", + "eslint.config.ts", + "vite.config.ts", + ], + defaultProject: "tsconfig.node.json", + }, + tsconfigRootDir: import.meta.dirname, + }, + }, +}); diff --git a/livesplit-core b/livesplit-core index 16fecb9f..61070c47 160000 --- a/livesplit-core +++ b/livesplit-core @@ -1 +1 @@ -Subproject commit 16fecb9f2c15796d9c6ebd142d92150373fc8bad +Subproject commit 61070c47ea91e6e148d6801cb7a03e8e32a2ebc9 diff --git a/package-lock.json b/package-lock.json index 60bc16d6..074c476d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,6 +30,7 @@ "@vitejs/plugin-react": "^6.0.1", "eslint": "^10.0.1", "favicons": "^7.2.0", + "jiti": "^2.7.0", "typescript": "^6.0.2", "typescript-eslint": "^8.24.0", "vite": "^8.0.0", diff --git a/package.json b/package.json index 0904cbff..8c4f47b5 100644 --- a/package.json +++ b/package.json @@ -3,10 +3,10 @@ "main": "dist/bundle.js", "type": "module", "scripts": { - "build:core": "node buildCore.js", - "build:core:release": "node buildCore.js --release", - "build:core:deploy": "node buildCore.js --max-opt --nightly", - "lint": "eslint src", + "build:core": "node buildCore.ts", + "build:core:release": "node buildCore.ts --release", + "build:core:deploy": "node buildCore.ts --max-opt --nightly", + "lint": "eslint src buildCore.ts eslint.config.ts vite.config.ts", "typecheck": "tsc -p tsconfig.json && tsc -p tsconfig.node.json", "publish": "vite build", "serve": "vite", @@ -41,6 +41,7 @@ "@vitejs/plugin-react": "^6.0.1", "eslint": "^10.0.1", "favicons": "^7.2.0", + "jiti": "^2.7.0", "typescript": "^6.0.2", "typescript-eslint": "^8.24.0", "vite": "^8.0.0", diff --git a/tsconfig.node.json b/tsconfig.node.json index 9e2f326c..bd128f0b 100644 --- a/tsconfig.node.json +++ b/tsconfig.node.json @@ -26,5 +26,10 @@ "types": ["node"], "lib": ["ES2024", "DOM", "WebWorker"] }, - "include": ["vite.config.ts", "src/type-definitions/babel-core-shim.d.ts"] + "include": [ + "buildCore.ts", + "eslint.config.ts", + "vite.config.ts", + "src/type-definitions/babel-core-shim.d.ts" + ] } diff --git a/vite.config.ts b/vite.config.ts index b305b9c9..b7844843 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -5,7 +5,7 @@ import babel from "@rolldown/plugin-babel"; import { defineConfig, type Plugin, type UserConfig } from "vite"; import react from "@vitejs/plugin-react"; import { VitePWA } from "vite-plugin-pwa"; -import favicons from "favicons"; +import favicons, { type FaviconOptions } from "favicons"; type Contributor = { contributions: number; @@ -14,6 +14,20 @@ type Contributor = { type: string; }; +type FaviconIconOverride = { + readonly background?: string | boolean; + readonly offset?: number; +}; + +type FaviconOptionsWithOverrides = Omit & { + readonly icons?: { + readonly appleIcon?: FaviconIconOverride | boolean | string[]; + readonly appleStartup?: FaviconIconOverride | boolean | string[]; + readonly windows?: FaviconIconOverride | boolean | string[]; + readonly yandex?: FaviconIconOverride | boolean | string[]; + }; +}; + let faviconGenerationPromise: Promise | undefined; let faviconHtmlTags: string[] = []; @@ -23,7 +37,7 @@ async function generateFavicons() { await fs.mkdir(outputDir, { recursive: true }); - const result = await favicons(source, { + const options = { path: "/icons/", appName: "LiveSplit One", appDescription: @@ -39,16 +53,17 @@ async function generateFavicons() { appleStatusBarStyle: "black-translucent", manifestMaskable: path.resolve("src", "assets", "maskable.svg"), icons: { - appleIcon: { - offset: 10, - }, - appleStartup: { - offset: 15, - }, + appleIcon: { offset: 10 }, + appleStartup: { offset: 15 }, windows: false, yandex: false, - } as any, - }); + }, + } satisfies FaviconOptionsWithOverrides; + + // Favicons documents partial icon overrides, but its declaration exposes + // the internal fully resolved icon shape. Keep that mismatch confined to + // this API boundary instead of weakening the configuration with `any`. + const result = await favicons(source, options as FaviconOptions); faviconHtmlTags = result.html; @@ -63,9 +78,19 @@ async function generateFavicons() { const screenshotDefs = [ { file: "screenshot-wide.png", formFactor: "wide", sizes: "1280x720" }, - { file: "screenshot-narrow.png", formFactor: "narrow", sizes: "469x834" }, + { + file: "screenshot-narrow.png", + formFactor: "narrow", + sizes: "469x834", + }, ]; - const screenshots: { src: string; sizes: string; type: string; form_factor: string; label: string }[] = []; + const screenshots: { + src: string; + sizes: string; + type: string; + form_factor: string; + label: string; + }[] = []; for (const def of screenshotDefs) { const srcPath = path.resolve("src", "assets", def.file); try { @@ -103,14 +128,14 @@ function parseChangelog() { .map((commit: string) => { const dateString = commit.match(/^Date: (.*)$/m)?.[1]; if (!dateString) { - throw `Date not found in commit:\n${commit}`; + throw new Error(`Date not found in commit:\n${commit}`); } const dateValue = new Date(dateString); const date = dateValue.toISOString().split("T")[0]; const id = commit.substring(0, commit.indexOf("\n")); const changelogEntries = parseChangelogEntries(commit); if (changelogEntries.length === 0) { - throw `Changelog not found in commit:\n${commit}`; + throw new Error(`Changelog not found in commit:\n${commit}`); } const messages: Record = {}; for (const entry of changelogEntries) { @@ -186,11 +211,11 @@ function preloadPlugin() { return { name: "lso-preload-plugin", enforce: "post", - configResolved(config: any) { + configResolved(config) { base = config.base || "/"; }, transformIndexHtml: { - order: "post" as const, + order: "post", handler(html: string) { // Inject with a placeholder that will replace // with the hashed URL. @@ -200,21 +225,28 @@ function preloadPlugin() { ``, ].join("\n "); const script = ``; - return html.replace("", `\n ${preloads}\n ${script}`); + return html.replace( + "", + `\n ${preloads}\n ${script}`, + ); }, }, - generateBundle(_options: any, bundle: any) { + generateBundle(_options, bundle) { const wasmEntry = Object.keys(bundle).find((key: string) => key.endsWith(".wasm"), ); - const timerFont = Object.keys(bundle).find((key: string) => - key.endsWith(".woff") && /(?:^|[\\/])timer[^/\\]*\.woff$/.test(key), + const timerFont = Object.keys(bundle).find( + (key: string) => + key.endsWith(".woff") && + /(?:^|[\\/])timer[^/\\]*\.woff$/.test(key), ); - const firaFont = Object.keys(bundle).find((key: string) => - key.endsWith(".woff") && /(?:^|[\\/])FiraSans[^/\\]*\.woff$/.test(key), + const firaFont = Object.keys(bundle).find( + (key: string) => + key.endsWith(".woff") && + /(?:^|[\\/])FiraSans[^/\\]*\.woff$/.test(key), ); - for (const chunk of Object.values(bundle) as any[]) { + for (const chunk of Object.values(bundle)) { if ( chunk.type === "asset" && chunk.fileName.endsWith(".html") @@ -258,7 +290,10 @@ export default defineConfig(async ({ mode }) => { try { const [lsoContributorsList, coreContributorsList] = await Promise.all([ getContributorsForRepo("LiveSplitOne", process.env["GITHUB_TOKEN"]), - getContributorsForRepo("livesplit-core", process.env["GITHUB_TOKEN"]), + getContributorsForRepo( + "livesplit-core", + process.env["GITHUB_TOKEN"], + ), ]); const coreContributorsMap: Record = {}; @@ -319,47 +354,45 @@ export default defineConfig(async ({ mode }) => { ], }), ...(!isTauri - ? [ - { - name: "inject-generated-favicons", - transformIndexHtml(html: string) { - if (faviconHtmlTags.length === 0) { - return html; - } + ? ([ + { + name: "inject-generated-favicons", + transformIndexHtml(html: string) { + if (faviconHtmlTags.length === 0) { + return html; + } - return html.replace( - "", - `${faviconHtmlTags.join("\n")}\n`, - ); - }, - }, - ] as Plugin[] + return html.replace( + "", + `${faviconHtmlTags.join("\n")}\n`, + ); + }, + }, + ] as Plugin[]) : []), ...(!isTauri && isProduction ? [ - VitePWA({ - registerType: "autoUpdate", - injectRegister: "inline", - manifest: false, - filename: "service-worker.js", - workbox: { - clientsClaim: true, - skipWaiting: true, - maximumFileSizeToCacheInBytes: 100 * 1024 * 1024, - globPatterns: [ - "**/*.{js,css,html,wasm,woff}", - ], - globIgnores: ["icons/**"], - runtimeCaching: [ - { - urlPattern: ({ url }: { url: URL }) => - url.pathname.startsWith("/icons/"), - handler: "CacheFirst" as const, - }, - ], - }, - }), - ] + VitePWA({ + registerType: "autoUpdate", + injectRegister: "inline", + manifest: false, + filename: "service-worker.js", + workbox: { + clientsClaim: true, + skipWaiting: true, + maximumFileSizeToCacheInBytes: 100 * 1024 * 1024, + globPatterns: ["**/*.{js,css,html,wasm,woff}"], + globIgnores: ["icons/**"], + runtimeCaching: [ + { + urlPattern: ({ url }: { url: URL }) => + url.pathname.startsWith("/icons/"), + handler: "CacheFirst" as const, + }, + ], + }, + }), + ] : []), ], define: { @@ -377,10 +410,9 @@ export default defineConfig(async ({ mode }) => { }, build: { target: "esnext", - outDir: - isTauri - ? path.join("src-tauri", "target", "dist") - : path.join("dist"), + outDir: isTauri + ? path.join("src-tauri", "target", "dist") + : path.join("dist"), emptyOutDir: true, chunkSizeWarningLimit: 1024, },