From ddb4c292521b7789b0f047c53638b4527b2e83c1 Mon Sep 17 00:00:00 2001 From: Konstantin Chernyshev Date: Tue, 2 Sep 2025 19:13:31 +0200 Subject: [PATCH] feat(dev): add hot-realoding proxy dev server on :30001 --- README.md | 10 +-- daggerheart.mjs | 20 +++++ package-lock.json | 213 +++++++++++++++++++++++++++++++++++++------- package.json | 6 +- tools/dev-proxy.mjs | 86 ++++++++++++++++++ tools/dev-setup.mjs | 7 +- tools/run-start.mjs | 13 ++- 7 files changed, 315 insertions(+), 40 deletions(-) create mode 100644 tools/dev-proxy.mjs diff --git a/README.md b/README.md index 0c2dabc3..25e977da 100644 --- a/README.md +++ b/README.md @@ -49,14 +49,14 @@ You can find the documentation here: https://github.com/Foundryborne/daggerheart ### Available Scripts -- `npm start` - Start development with file watching and Foundry launching -- `npm run build` - One-time build - `npm run setup:dev -- --foundry-path="" --data-path=""` - Configure development environment +- `npm run build` - One-time build +- `npm start` - Start development with file watching, Foundry launching, and a dev proxy overlay (no symlink, no copy) -### Notes +### Dev Notes -- The repo should be placed in your Foundry `Data/systems/` directory or symlinked there -- Linux symlink can be made using `ln -snf daggerheart` inside the systems folder +- No symlink or copying required: a dev proxy overlays `systems/daggerheart` assets from your repo at `http://localhost:30001` while the official install remains intact on the base Foundry port. +- Usage: start Foundry (served at `http://localhost:30000`) and the dev proxy (served at `http://localhost:30001`). Open `http://localhost:30001` to see your dev version; close it to return to the installed version at `http://localhost:30000`. - Your `.env` file is ignored by git, so each developer can have their own configuration [Foundry VTT Website][1] diff --git a/daggerheart.mjs b/daggerheart.mjs index 96a6783b..d0ad32ff 100644 --- a/daggerheart.mjs +++ b/daggerheart.mjs @@ -171,6 +171,26 @@ Hooks.on('ready', async () => { } runMigrations(); + + // Development live-reload: connect to local WS server if available + try { + const defaultPort = 30123; + const storedPort = Number(localStorage.getItem('DH_DEV_RELOAD_PORT')); + const port = Number.isFinite(storedPort) ? storedPort : defaultPort; + const ws = new WebSocket(`ws://localhost:${port}`); + let reloadTimer; + ws.onmessage = () => { + // Debounce rapid changes + clearTimeout(reloadTimer); + reloadTimer = setTimeout(() => { + window.location.reload(); + }, 100); + }; + ws.onerror = () => {}; + ws.onclose = () => {}; + } catch (err) { + // Ignore if WebSocket is not available or blocked + } }); Hooks.once('dicesoniceready', () => {}); diff --git a/package-lock.json b/package-lock.json index 864d027c..51423a32 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,5 +1,5 @@ { - "name": "daggerheart", + "name": "fork-foundry-daggerheart", "lockfileVersion": 3, "requires": true, "packages": { @@ -15,9 +15,13 @@ "@foundryvtt/foundryvtt-cli": "^1.0.2", "@rollup/plugin-commonjs": "^25.0.7", "@rollup/plugin-node-resolve": "^15.2.3", + "chokidar": "^4.0.3", "concurrently": "^8.2.2", + "fs-extra": "^11.2.0", + "http-proxy": "^1.18.1", "husky": "^9.1.5", "lint-staged": "^15.2.10", + "mime-types": "^2.1.35", "postcss": "^8.4.32", "prettier": "^3.5.3", "rollup-plugin-postcss": "^4.0.2" @@ -682,6 +686,7 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "license": "MIT", "engines": { "node": ">=8" }, @@ -881,26 +886,19 @@ } }, "node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" + "readdirp": "^4.0.1" }, "engines": { - "node": ">= 8.10.0" + "node": ">= 14.16.0" }, "funding": { "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" } }, "node_modules/classic-level": { @@ -1771,6 +1769,27 @@ "node": ">= 10.13.0" } }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/for-each": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", @@ -1805,6 +1824,21 @@ "node": ">=0.10.0" } }, + "node_modules/fs-extra": { + "version": "11.3.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.1.tgz", + "integrity": "sha512-eXvGGwZ5CL17ZSwHWd3bbgk7UUpF6IFHtP57NYYakPvHOs8GDgDe5KJI36jIJzDkJ6eJjuzRA8eBQb6SkKue0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, "node_modules/fs-mkdirp-stream": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/fs-mkdirp-stream/-/fs-mkdirp-stream-2.0.1.tgz", @@ -1946,6 +1980,7 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "license": "ISC", "dependencies": { "is-glob": "^4.0.1" }, @@ -1994,6 +2029,54 @@ "node": ">= 10.13.0" } }, + "node_modules/glob-watcher/node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/glob-watcher/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/glob-watcher/node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, "node_modules/global-modules": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-1.0.0.tgz", @@ -2309,6 +2392,28 @@ "node": ">=0.10.0" } }, + "node_modules/http-proxy": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eventemitter3": "^4.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/http-proxy/node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "dev": true, + "license": "MIT" + }, "node_modules/human-signals": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", @@ -2484,6 +2589,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "license": "MIT", "dependencies": { "binary-extensions": "^2.0.0" }, @@ -2769,6 +2875,19 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, "node_modules/last-run": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/last-run/-/last-run-2.0.0.tgz", @@ -3104,6 +3223,29 @@ "node": ">=4" } }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/mimic-fn": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", @@ -4242,25 +4384,17 @@ } }, "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dependencies": { - "picomatch": "^2.2.1" - }, + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/readdirp/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "engines": { - "node": ">=8.6" + "node": ">= 14.18.0" }, "funding": { - "url": "https://github.com/sponsors/jonschlinkert" + "type": "individual", + "url": "https://paulmillr.com/funding/" } }, "node_modules/rechoir": { @@ -4303,6 +4437,13 @@ "node": ">=0.10.0" } }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true, + "license": "MIT" + }, "node_modules/resolve": { "version": "1.22.10", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", @@ -5040,6 +5181,16 @@ "node": ">= 10.13.0" } }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/update-browserslist-db": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", diff --git a/package.json b/package.json index dbece0c7..479c5df6 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,11 @@ "lint-staged": "^15.2.10", "postcss": "^8.4.32", "prettier": "^3.5.3", - "rollup-plugin-postcss": "^4.0.2" + "rollup-plugin-postcss": "^4.0.2", + "chokidar": "^4.0.3", + "fs-extra": "^11.2.0", + "http-proxy": "^1.18.1", + "mime-types": "^2.1.35" }, "lint-staged": { "**/*": "prettier --write --ignore-unknown" diff --git a/tools/dev-proxy.mjs b/tools/dev-proxy.mjs new file mode 100644 index 00000000..9cf1ffc1 --- /dev/null +++ b/tools/dev-proxy.mjs @@ -0,0 +1,86 @@ +#!/usr/bin/env node +import http from 'http'; +import httpProxy from 'http-proxy'; +import path from 'path'; +import fs from 'fs'; +import url from 'url'; +import { lookup as mimeLookup } from 'mime-types'; + +// Load .env +const envPath = path.resolve(process.cwd(), '.env'); +if (fs.existsSync(envPath)) { + const envFile = fs.readFileSync(envPath, 'utf8'); + envFile.split('\n').forEach(line => { + const [key, value] = line.split('='); + if (key && value) process.env[key] = value; + }); +} + +const systemId = 'daggerheart'; +const pkgBase = `/systems/${systemId}`; +const devRoot = process.cwd(); + +// Foundry server target (reverse proxy) +const targetPort = Number(process.env.DH_DEV_TARGET_PORT || process.env.FOUNDRY_PORT || 30000); +const target = `http://localhost:${targetPort}`; + +// Dev proxy port +const proxyPort = Number(process.env.DH_DEV_PROXY_PORT || 30001); + +// Asset overlay: serve files from repo for /systems/daggerheart/* +function tryServeOverlay(req, res) { + const parsed = url.parse(req.url); + const pathname = parsed.pathname || '/'; + if (!pathname.startsWith(pkgBase + '/')) return false; + + // Map /systems/daggerheart/... to local repo path + const rel = pathname.replace(pkgBase + '/', ''); + + const serveCandidates = [ + // direct match in repo + path.join(devRoot, rel), + // common tree roots + path.join(devRoot, 'assets', rel.replace(/^assets\//, '')), + path.join(devRoot, 'build', rel.replace(/^build\//, '')), + path.join(devRoot, 'lang', rel.replace(/^lang\//, '')), + path.join(devRoot, 'module', rel.replace(/^module\//, '')), + path.join(devRoot, 'styles', rel.replace(/^styles\//, '')), + path.join(devRoot, 'templates', rel.replace(/^templates\//, '')), + path.join(devRoot, 'system.json'), + path.join(devRoot, 'daggerheart.mjs') + ]; + + for (const candidate of serveCandidates) { + try { + const stat = fs.statSync(candidate); + if (stat.isFile()) { + const stream = fs.createReadStream(candidate); + stream.on('error', () => res.end()); + const contentType = mimeLookup(candidate) || 'application/octet-stream'; + res.writeHead(200, { 'Content-Type': contentType }); + stream.pipe(res); + return true; + } + } catch (_) {} + } + return false; +} + +const proxy = httpProxy.createProxyServer({ target, ws: true, changeOrigin: true, selfHandleResponse: false }); + +const server = http.createServer((req, res) => { + // If the request targets our system path, try to serve local overlay first + if (tryServeOverlay(req, res)) return; + proxy.web(req, res, { target }); +}); + +server.on('upgrade', (req, socket, head) => { + proxy.ws(req, socket, head, { target }); +}); + +server.listen(proxyPort, () => { + console.log(`Daggerheart dev proxy listening on http://localhost:${proxyPort} (target ${target})`); + console.log(`Overlaying ${pkgBase} from ${devRoot}`); +}); + + diff --git a/tools/dev-setup.mjs b/tools/dev-setup.mjs index f232f5a8..cc024f75 100644 --- a/tools/dev-setup.mjs +++ b/tools/dev-setup.mjs @@ -4,15 +4,18 @@ import fs from 'fs'; const args = process.argv.slice(2); const foundryPath = args.find(arg => arg.startsWith('--foundry-path='))?.split('=')[1]; const dataPath = args.find(arg => arg.startsWith('--data-path='))?.split('=')[1]; +const portArg = args.find(arg => arg.startsWith('--port='))?.split('=')[1]; if (!foundryPath || !dataPath) { - console.log('Usage: npm run setup:dev -- --foundry-path="/path/to/foundry/main.js" --data-path="/path/to/data"'); + console.log('Usage: npm run setup:dev -- --foundry-path="/path/to/foundry/main.js" --data-path="/path/to/data" [--port=30000]'); process.exit(1); } +const port = portArg || '30000'; const envContent = `FOUNDRY_MAIN_PATH=${foundryPath} FOUNDRY_DATA_PATH=${dataPath} +FOUNDRY_PORT=${port} `; fs.writeFileSync('.env', envContent); -console.log(`✅ Development environment configured: ${foundryPath}, ${dataPath}`); +console.log(`✅ Development environment configured:\n Foundry main: ${foundryPath}\n Data path: ${dataPath}\n Port: ${port}`); diff --git a/tools/run-start.mjs b/tools/run-start.mjs index e620d13f..6560c66e 100644 --- a/tools/run-start.mjs +++ b/tools/run-start.mjs @@ -16,12 +16,23 @@ if (fs.existsSync('.env')) { // Set defaults if not in environment const foundryPath = process.env.FOUNDRY_MAIN_PATH || '../../../../FoundryDev/main.js'; const dataPath = process.env.FOUNDRY_DATA_PATH || '../../../'; +const foundryPort = process.env.FOUNDRY_PORT || '30000'; // Run the original command with proper environment -const args = ['rollup -c --watch', `node "${foundryPath}" --dataPath="${dataPath}" --noupnp`, 'gulp']; +const args = [ + 'rollup -c --watch', + `node "${foundryPath}" --dataPath="${dataPath}" --noupnp --port=${foundryPort}`, + 'gulp', + 'node ./tools/dev-proxy.mjs' +]; spawn('npx', ['concurrently', ...args.map(arg => `"${arg}"`)], { stdio: 'inherit', cwd: process.cwd(), shell: true }); + +// Friendly hint +console.log('\n\x1b[32mDev proxy: http://localhost:30001\x1b[0m (overlay)'); +console.log(`\x1b[36mFoundry: http://localhost:${foundryPort}\x1b[0m (installed)`); +console.log('\nOpen http://localhost:30001 to see your dev version, or close it to return to the installed version.');