sync dev server

This commit is contained in:
vm-silicons
2025-12-20 05:35:26 -08:00
parent 327b85ae60
commit 37dda671cf
13 changed files with 364 additions and 361 deletions
+3 -4
View File
@@ -6,9 +6,8 @@
import { exec } from 'node:child_process';
import { promisify } from 'node:util';
import { require } from './require.js';
const axios = require('axios');
import axios from 'axios';
import { createLogger } from './logging.js';
@@ -119,6 +118,6 @@ export class DreamSeeker {
}
}
const plural = (word, n) => {
function plural(word, n) {
return n !== 1 ? word + 's' : word;
};
}
+5 -5
View File
@@ -4,27 +4,27 @@
* @license MIT
*/
import { createCompiler } from './webpack.js';
import { reloadByondCache } from './reloader.js';
import { createCompiler } from './webpack.js';
const noHot = process.argv.includes('--no-hot');
const noTmp = process.argv.includes('--no-tmp');
const reloadOnce = process.argv.includes('--reload');
const setupServer = async () => {
async function setupServer() {
const compiler = await createCompiler({
mode: 'development',
hot: !noHot,
devServer: true,
useTmpFolder: !noTmp,
});
// Reload cache once
if (reloadOnce) {
await reloadByondCache(compiler.bundleDir);
return;
}
// Run a development server
await compiler.watch();
};
}
setupServer();
@@ -1,177 +0,0 @@
/**
* @file
* @copyright 2020 Aleksej Komarov
* @license MIT
*/
let socket;
const queue = [];
const subscribers = [];
const ensureConnection = () => {
if (process.env.NODE_ENV !== 'production') {
if (!window.WebSocket) {
return;
}
if (!socket || socket.readyState === WebSocket.CLOSED) {
const DEV_SERVER_IP = process.env.DEV_SERVER_IP || '127.0.0.1';
socket = new WebSocket(`ws://${DEV_SERVER_IP}:3000`);
socket.onopen = () => {
// Empty the message queue
while (queue.length !== 0) {
const msg = queue.shift();
socket.send(msg);
}
};
socket.onmessage = event => {
const msg = JSON.parse(event.data);
for (let subscriber of subscribers) {
subscriber(msg);
}
};
}
}
};
if (process.env.NODE_ENV !== 'production') {
window.onunload = () => socket && socket.close();
}
const subscribe = fn => subscribers.push(fn);
/**
* A json serializer which handles circular references and other junk.
*/
const serializeObject = obj => {
let refs = [];
const primitiveReviver = value => {
if (typeof value === 'number' && !Number.isFinite(value)) {
return {
__number__: String(value),
};
}
if (typeof value === 'undefined') {
return {
__undefined__: true,
};
}
return value;
};
const objectReviver = (key, value) => {
if (typeof value === 'object') {
if (value === null) {
return value;
}
// Circular reference
if (refs.includes(value)) {
return '[circular ref]';
}
refs.push(value);
// Error object
const isError = value instanceof Error || (
value.code && value.message && value.message.includes('Error')
);
if (isError) {
return {
__error__: true,
string: String(value),
stack: value.stack,
};
}
// Array
if (Array.isArray(value)) {
return value.map(primitiveReviver);
}
return value;
}
return primitiveReviver(value);
};
const json = JSON.stringify(obj, objectReviver);
refs = null;
return json;
};
const sendMessage = msg => {
if (process.env.NODE_ENV !== 'production') {
const json = serializeObject(msg);
// Send message using WebSocket
if (window.WebSocket) {
ensureConnection();
if (socket.readyState === WebSocket.OPEN) {
socket.send(json);
}
else {
// Keep only 100 latest messages in the queue
if (queue.length > 100) {
queue.shift();
}
queue.push(json);
}
}
// Send message using plain HTTP request.
else {
const DEV_SERVER_IP = process.env.DEV_SERVER_IP || '127.0.0.1';
const req = new XMLHttpRequest();
req.open('POST', `http://${DEV_SERVER_IP}:3001`, true);
req.timeout = 250;
req.send(json);
}
}
};
const sendLogEntry = (level, ns, ...args) => {
if (process.env.NODE_ENV !== 'production') {
try {
sendMessage({
type: 'log',
payload: {
level,
ns: ns || 'client',
args,
},
});
}
catch (err) {}
}
};
const setupHotReloading = () => {
if (process.env.NODE_ENV !== 'production'
&& process.env.WEBPACK_HMR_ENABLED
&& window.WebSocket) {
if (module.hot) {
ensureConnection();
sendLogEntry(0, null, 'setting up hot reloading');
subscribe(msg => {
const { type } = msg;
sendLogEntry(0, null, 'received', type);
if (type === 'hotUpdate') {
const status = module.hot.status();
if (status !== 'idle') {
sendLogEntry(0, null, 'hot reload status:', status);
return;
}
module.hot
.check({
ignoreUnaccepted: true,
ignoreDeclined: true,
ignoreErrored: true,
})
.then(modules => {
sendLogEntry(0, null, 'outdated modules', modules);
})
.catch(err => {
sendLogEntry(0, null, 'reload error', err);
});
}
});
}
}
};
module.exports = {
subscribe,
sendMessage,
sendLogEntry,
setupHotReloading,
};
@@ -0,0 +1,169 @@
/**
* @file
* @copyright 2020 Aleksej Komarov
* @license MIT
*/
let socket;
const queue = [];
const subscribers = [];
function ensureConnection() {
if (process.env.NODE_ENV === 'production') return;
if (socket && socket.readyState !== WebSocket.CLOSED) return;
if (!window.WebSocket) return;
const DEV_SERVER_IP = process.env.DEV_SERVER_IP || '127.0.0.1';
socket = new WebSocket(`ws://${DEV_SERVER_IP}:3000`);
socket.onopen = () => {
// Empty the message queue
while (queue.length !== 0) {
const msg = queue.shift();
socket.send(msg);
}
};
socket.onmessage = (event) => {
const msg = JSON.parse(event.data);
for (let subscriber of subscribers) {
subscriber(msg);
}
};
window.onunload = () => socket?.close();
}
export function subscribe(fn) {
subscribers.push(fn);
}
function primitiveReviver(value) {
if (typeof value === 'number' && !Number.isFinite(value)) {
return {
__number__: String(value),
};
}
if (typeof value === 'undefined') {
return {
__undefined__: true,
};
}
return value;
}
/**
* A json serializer which handles circular references and other junk.
*/
function serializeObject(obj) {
let refs = [];
function objectReviver(key, value) {
if (typeof value !== 'object') {
return primitiveReviver(value);
}
if (value === null) {
return value;
}
// Circular reference
if (refs.includes(value)) {
return '[circular ref]';
}
refs.push(value);
// Error object
const isError =
value instanceof Error ||
(value.code && value.message && value.message.includes('Error'));
if (isError) {
return {
__error__: true,
string: String(value),
stack: value.stack,
};
}
// Array
if (Array.isArray(value)) {
return value.map(primitiveReviver);
}
return value;
}
const json = JSON.stringify(obj, objectReviver);
refs = null;
return json;
}
export function sendMessage(msg) {
if (process.env.NODE_ENV === 'production') return;
const json = serializeObject(msg);
// Send message using WebSocket
if (!window.WebSocket) return;
ensureConnection();
if (socket.readyState === WebSocket.OPEN) {
socket.send(json);
} else {
// Keep only 100 latest messages in the queue
if (queue.length > 100) {
queue.shift();
}
queue.push(json);
}
}
export function sendLogEntry(level, ns, ...args) {
if (process.env.NODE_ENV !== 'production') {
try {
sendMessage({
type: 'log',
payload: {
level,
ns: ns || 'client',
args,
},
});
} catch (err) {}
}
}
export function setupHotReloading() {
if (
process.env.NODE_ENV === 'production' ||
!process.env.WEBPACK_HMR_ENABLED ||
!window.WebSocket
) {
return;
}
if (!import.meta.webpackHot) return;
ensureConnection();
sendLogEntry(0, null, 'setting up hot reloading');
subscribe(({ type }) => {
sendLogEntry(0, null, 'received', type);
if (type !== 'hotUpdate') return;
const status = import.meta.webpackHot.status();
if (status !== 'idle') {
sendLogEntry(0, null, 'hot reload status:', status);
return;
}
import.meta.webpackHot
.check({
ignoreUnaccepted: true,
ignoreDeclined: true,
ignoreErrored: true,
})
.then((modules) => {
sendLogEntry(0, null, 'outdated modules', modules);
})
.catch((err) => {
sendLogEntry(0, null, 'reload error', err);
});
});
}
+18 -19
View File
@@ -4,21 +4,20 @@
* @license MIT
*/
import fs from 'fs';
import { basename } from 'path';
import { createLogger } from '../logging.js';
import { require } from '../require.js';
import { resolveGlob } from '../util.js';
import fs from 'node:fs';
import { basename } from 'node:path';
const SourceMap = require('source-map');
const { parse: parseStackTrace } = require('stacktrace-parser');
import { SourceMapConsumer } from 'source-map';
import { parse as parseStackTrace } from 'stacktrace-parser';
import { createLogger } from '../logging.js';
import { resolveGlob } from '../util.js';
const logger = createLogger('retrace');
const { SourceMapConsumer } = SourceMap;
const sourceMaps = [];
export const loadSourceMaps = async bundleDir => {
export async function loadSourceMaps(bundleDir) {
// Destroy and garbage collect consumers
while (sourceMaps.length !== 0) {
const { consumer } = sourceMaps.shift();
@@ -30,29 +29,29 @@ export const loadSourceMaps = async bundleDir => {
try {
const file = basename(path).replace('.map', '');
const consumer = await new SourceMapConsumer(
JSON.parse(fs.readFileSync(path, 'utf8')));
JSON.parse(fs.readFileSync(path, 'utf8')),
);
sourceMaps.push({ file, consumer });
}
catch (err) {
} catch (err) {
logger.error(err);
}
}
logger.log(`loaded ${sourceMaps.length} source maps`);
};
}
export const retrace = stack => {
export function retrace(stack) {
if (typeof stack !== 'string') {
logger.log('ERROR: Stack is not a string!', stack);
return stack;
}
const header = stack.split(/\n\s.*at/)[0];
const mappedStack = parseStackTrace(stack)
.map(frame => {
.map((frame) => {
if (!frame.file) {
return frame;
}
// Find the correct source map
const sourceMap = sourceMaps.find(sourceMap => {
const sourceMap = sourceMaps.find((sourceMap) => {
return frame.file.includes(sourceMap.file);
});
if (!sourceMap) {
@@ -72,17 +71,17 @@ export const retrace = stack => {
column: mappedFrame.column,
};
})
.map(frame => {
.map((frame) => {
// Stringify the frame
const { file, methodName, lineNumber } = frame;
if (!file) {
return ` at ${methodName}`;
}
const compactPath = file
.replace(/^webpack:\/\/\/?/, './')
.replace(/^rspack:\/\/\/?/, './')
.replace(/.*node_modules\//, '');
return ` at ${methodName} (${compactPath}:${lineNumber})`;
})
.join('\n');
return header + '\n' + mappedStack;
};
}
+39 -46
View File
@@ -4,13 +4,12 @@
* @license MIT
*/
import http from 'http';
import { inspect } from 'util';
import { createLogger, directLog } from '../logging.js';
import { require } from '../require.js';
import { loadSourceMaps, retrace } from './retrace.js';
import { inspect } from 'node:util';
const WebSocket = require('ws');
import * as WebSocket from 'ws';
import { createLogger, directLog } from '../logging.js';
import { loadSourceMaps, retrace } from './retrace.js';
const logger = createLogger('link');
@@ -18,23 +17,26 @@ const DEBUG = process.argv.includes('--debug');
export { loadSourceMaps };
export const setupLink = () => new LinkServer();
export function setupLink() {
return new LinkServer();
}
class LinkServer {
constructor() {
logger.log('setting up');
/** @type {WebSocket.Server | null} */
this.wss = null;
this.setupWebSocketLink();
this.setupHttpLink();
}
// WebSocket-based client link
setupWebSocketLink() {
const port = 3000;
this.wss = new WebSocket.Server({ port });
this.wss.on('connection', ws => {
this.wss = new WebSocket.WebSocketServer({ port });
this.wss.on('connection', (ws) => {
logger.log('client connected');
ws.on('message', json => {
ws.on('message', (json) => {
const msg = deserializeObject(json);
this.handleLinkMessage(ws, msg);
});
@@ -45,29 +47,10 @@ class LinkServer {
logger.log(`listening on port ${port} (WebSocket)`);
}
// One way HTTP-based client link for IE8
setupHttpLink() {
const port = 3001;
this.httpServer = http.createServer((req, res) => {
if (req.method === 'POST') {
let body = '';
req.on('data', chunk => {
body += chunk.toString();
});
req.on('end', () => {
const msg = deserializeObject(body);
this.handleLinkMessage(null, msg);
res.end();
});
return;
}
res.write('Hello');
res.end();
});
this.httpServer.listen(port);
logger.log(`listening on port ${port} (HTTP)`);
}
/**
* @param {WebSocket.Client} ws
* @param {WebSocket.MessageEvent} msg
*/
handleLinkMessage(ws, msg) {
const { type, payload } = msg;
if (type === 'log') {
@@ -76,19 +59,26 @@ class LinkServer {
if (level <= 0 && !DEBUG) {
return;
}
directLog(ns, ...args.map(arg => {
if (typeof arg === 'object') {
return inspect(arg, {
depth: Infinity,
colors: true,
compact: 8,
});
}
return arg;
}));
directLog(
ns,
...args.map((arg) => {
if (typeof arg === 'object') {
return inspect(arg, {
depth: Infinity,
colors: true,
compact: 8,
});
}
return arg;
}),
);
return;
}
if (type === 'relay') {
if (!this.wss) {
return;
}
for (let client of this.wss.clients) {
if (client === ws) {
continue;
@@ -105,6 +95,9 @@ class LinkServer {
}
broadcastMessage(msg) {
if (!this.wss) {
return;
}
const clients = [...this.wss.clients];
if (clients.length === 0) {
return;
@@ -117,7 +110,7 @@ class LinkServer {
}
}
const deserializeObject = str => {
function deserializeObject(str) {
return JSON.parse(str, (key, value) => {
if (typeof value === 'object' && value !== null) {
if (value.__undefined__) {
@@ -140,4 +133,4 @@ const deserializeObject = str => {
}
return value;
});
};
}
+21 -19
View File
@@ -7,22 +7,22 @@
const inception = Date.now();
// Runtime detection
const isNode = process && process.release && process.release.name === 'node';
const isNode = process?.release?.name === 'node';
let isChrome = false;
try {
isChrome = window.navigator.userAgent.toLowerCase().includes('chrome');
}
catch {}
} catch {}
// Timestamping function
const getTimestamp = () => {
function getTimestamp() {
const timestamp = String(Date.now() - inception)
.padStart(4, '0')
.padStart(7, ' ');
const seconds = timestamp.substr(0, timestamp.length - 3);
const millis = timestamp.substr(-3);
return `${seconds}.${millis}`;
};
}
const getPrefix = (() => {
if (isNode) {
@@ -32,7 +32,7 @@ const getPrefix = (() => {
bright: '\x1b[37;1m',
reset: '\x1b[0m',
};
return ns => [
return (ns) => [
`${ESC.dimmed}${getTimestamp()} ${ESC.bright}${ns}${ESC.reset}`,
];
}
@@ -42,31 +42,33 @@ const getPrefix = (() => {
dimmed: 'color: #888',
bright: 'font-weight: bold',
};
return ns => [
return (ns) => [
`%c${getTimestamp()}%c ${ns}`,
styles.dimmed,
styles.bright,
];
}
return ns => [
`${getTimestamp()} ${ns}`,
];
return (ns) => [`${getTimestamp()} ${ns}`];
})();
/**
* Creates a logger object.
*/
export const createLogger = ns => ({
log: (...args) => console.log(...getPrefix(ns), ...args),
trace: (...args) => console.trace(...getPrefix(ns), ...args),
debug: (...args) => console.debug(...getPrefix(ns), ...args),
info: (...args) => console.info(...getPrefix(ns), ...args),
warn: (...args) => console.warn(...getPrefix(ns), ...args),
error: (...args) => console.error(...getPrefix(ns), ...args),
});
export function createLogger(ns) {
return {
log: (...args) => console.log(...getPrefix(ns), ...args),
trace: (...args) => console.trace(...getPrefix(ns), ...args),
debug: (...args) => console.debug(...getPrefix(ns), ...args),
info: (...args) => console.info(...getPrefix(ns), ...args),
warn: (...args) => console.warn(...getPrefix(ns), ...args),
error: (...args) => console.error(...getPrefix(ns), ...args),
};
}
/**
* Explicitly log with chosen namespace.
*/
export const directLog = (ns, ...args) =>
export function directLog(ns, ...args) {
console.log(...getPrefix(ns), ...args);
}
+14 -11
View File
@@ -1,13 +1,16 @@
{
"private": true,
"name": "tgui-dev-server",
"version": "4.3.0",
"type": "module",
"dependencies": {
"axios": "^1.8.4",
"glob": "^13.0.0",
"source-map": "^0.7.6",
"stacktrace-parser": "^0.1.11",
"ws": "^8.18.3"
}
"private": true,
"name": "tgui-dev-server",
"version": "6.0.0",
"type": "module",
"dependencies": {
"@rspack/core": "^1.3.7",
"axios": "^1.8.4",
"source-map": "^0.7.4",
"stacktrace-parser": "^0.1.11",
"ws": "^8.18.1"
},
"devDependencies": {
"@types/ws": "^8.18.1"
}
}
+31 -27
View File
@@ -4,9 +4,10 @@
* @license MIT
*/
import fs from 'fs';
import os from 'os';
import { basename } from 'path';
import fs from 'node:fs';
import os from 'node:os';
import { basename } from 'node:path';
import { DreamSeeker } from './dreamseeker.js';
import { createLogger } from './logging.js';
import { resolveGlob, resolvePath } from './util.js';
@@ -30,7 +31,7 @@ const SEARCH_LOCATIONS = [
let cacheRoot;
export const findCacheRoot = async () => {
export async function findCacheRoot() {
if (cacheRoot) {
return cacheRoot;
}
@@ -50,49 +51,53 @@ export const findCacheRoot = async () => {
// Query the Windows Registry
if (process.platform === 'win32') {
logger.log('querying windows registry');
let userpath = await regQuery(
'HKCU\\Software\\Dantom\\BYOND',
'userpath');
let userpath = await regQuery('HKCU\\Software\\Dantom\\BYOND', 'userpath');
if (userpath) {
cacheRoot = userpath
.replace(/\\$/, '')
.replace(/\\/g, '/')
+ '/cache';
cacheRoot = userpath.replace(/\\$/, '').replace(/\\/g, '/') + '/cache';
onCacheRootFound(cacheRoot);
return cacheRoot;
}
}
logger.log('found no cache directories');
};
}
const onCacheRootFound = cacheRoot => {
function onCacheRootFound(cacheRoot) {
logger.log(`found cache at '${cacheRoot}'`);
};
// Plant a dummy browser window file, we'll be using this to avoid world topic. For byond 514.
fs.closeSync(fs.openSync(cacheRoot + '/dummy.htm', 'w'));
}
export const reloadByondCache = async bundleDir => {
export async function reloadByondCache(bundleDir) {
const cacheRoot = await findCacheRoot();
if (!cacheRoot) {
return;
}
// Find tmp folders in cache
const cacheDirs = await resolveGlob(cacheRoot, './tmp*');
const cacheDirs = resolveGlob(cacheRoot, './tmp*');
if (cacheDirs.length === 0) {
logger.log('found no tmp folder in cache');
return;
}
// Get dreamseeker instances
const pids = cacheDirs.map(cacheDir => (
parseInt(cacheDir.split('/cache/tmp').pop(), 10)
));
const pids = cacheDirs.map((cacheDir) => {
return parseInt(cacheDir.split('\\cache\\tmp')[1], 10);
});
const dssPromise = DreamSeeker.getInstancesByPids(pids);
// Copy assets
const assets = await resolveGlob(bundleDir, './*.+(bundle|chunk|hot-update).*');
const assets = await resolveGlob(
bundleDir,
'./*.+(bundle|chunk|hot-update).*',
);
for (let cacheDir of cacheDirs) {
// Clear garbage
const garbage = await resolveGlob(cacheDir, './*.+(bundle|chunk|hot-update).*');
const garbage = await resolveGlob(
cacheDir,
'./*.+(bundle|chunk|hot-update).*',
);
try {
// Plant a dummy browser window file, we'll be using this to avoid world topic
fs.closeSync(fs.openSync(cacheDir + '/dummy', 'w'));
// Plant a dummy browser window file, we'll be using this to avoid world topic. For byond 515-516.
fs.closeSync(fs.openSync(cacheDir + '/dummy.htm', 'w'));
for (let file of garbage) {
fs.unlinkSync(file);
@@ -103,8 +108,7 @@ export const reloadByondCache = async bundleDir => {
fs.writeFileSync(destination, fs.readFileSync(asset));
}
logger.log(`copied ${assets.length} files to '${cacheDir}'`);
}
catch (err) {
} catch (err) {
logger.error(`failed copying to '${cacheDir}'`);
logger.error(err);
}
@@ -120,4 +124,4 @@ export const reloadByondCache = async bundleDir => {
});
}
}
};
}
+1 -1
View File
@@ -4,6 +4,6 @@
* @license MIT
*/
import { createRequire } from 'module';
import { createRequire } from 'node:module';
export const require = createRequire(import.meta.url);
+11 -16
View File
@@ -4,30 +4,25 @@
* @license MIT
*/
import fs from 'fs';
import path from 'path';
import { require } from './require.js';
const globPkg = require('glob');
import fs, { globSync } from 'node:fs';
import path from 'node:path';
export const resolvePath = path.resolve;
/**
* Combines path.resolve with glob patterns.
*/
export const resolveGlob = (...sections) => {
const unsafePaths = globPkg.sync(path.resolve(...sections), {
strict: false,
silent: true,
windowsPathsNoEscape: true,
});
/** Combines path.resolve with glob patterns. */
export function resolveGlob(...sections) {
/** @type {string[]} */
const unsafePaths = globSync(path.resolve(...sections));
/** @type {string[]} */
const safePaths = [];
for (let path of unsafePaths) {
try {
fs.statSync(path);
safePaths.push(path);
} catch {}
}
return safePaths;
};
}
+29 -20
View File
@@ -4,40 +4,49 @@
* @license MIT
*/
import fs from 'fs';
import { createRequire } from 'module';
import { dirname } from 'path';
import fs from 'node:fs';
import { createRequire } from 'node:module';
import { loadSourceMaps, setupLink } from './link/server.js';
import { createLogger } from './logging.js';
import { reloadByondCache } from './reloader.js';
import { resolveGlob } from './util.js';
const logger = createLogger('webpack');
const logger = createLogger('rspack');
/**
* @param {any} config
* @return {WebpackCompiler}
* @return {RspackCompiler}
*/
export const createCompiler = async options => {
const compiler = new WebpackCompiler();
export async function createCompiler(options) {
const compiler = new RspackCompiler();
await compiler.setup(options);
return compiler;
};
class WebpackCompiler {
return compiler;
}
class RspackCompiler {
async setup(options) {
// Create a require context that is relative to project root
// and retrieve all necessary dependencies.
const requireFromRoot = createRequire(dirname(import.meta.url) + '/../..');
const webpack = await requireFromRoot('webpack');
const createConfig = await requireFromRoot('./webpack.config.js');
const requireFromRoot = createRequire(import.meta.dirname + '/../../..');
/** @type {typeof import('@rspack/core')} */
const rspack = await requireFromRoot('@rspack/core');
const createConfig = await requireFromRoot('./rspack.config.cjs');
const createDevConfig = await requireFromRoot('./rspack.config-dev.cjs');
const config = createConfig({}, options);
const devConfig = createDevConfig({}, options);
const mergedConfig = { ...config, ...devConfig };
// Inject the HMR plugin into the config if we're using it
if (options.hot) {
config.plugins.push(new webpack.HotModuleReplacementPlugin());
mergedConfig.plugins.push(new rspack.HotModuleReplacementPlugin());
}
this.webpack = webpack;
this.config = config;
this.rspack = rspack;
this.config = mergedConfig;
this.bundleDir = config.output.path;
}
@@ -46,7 +55,7 @@ class WebpackCompiler {
// Setup link
const link = setupLink();
// Instantiate the compiler
const compiler = this.webpack.webpack(this.config);
const compiler = this.rspack.rspack(this.config);
// Clear garbage before compiling
compiler.hooks.watchRun.tapPromise('tgui-dev-server', async () => {
const files = await resolveGlob(this.bundleDir, './*.hot-update.*');
@@ -57,7 +66,7 @@ class WebpackCompiler {
logger.log('compiling');
});
// Start reloading when it's finished
compiler.hooks.done.tap('tgui-dev-server', async stats => {
compiler.hooks.done.tap('tgui-dev-server', async (stats) => {
// Load source maps
await loadSourceMaps(this.bundleDir);
// Reload cache
@@ -75,9 +84,9 @@ class WebpackCompiler {
return;
}
stats
.toString(this.config.devServer.stats)
?.toString(this.config.devServer.stats)
.split('\n')
.forEach(line => logger.log(line));
.forEach((line) => logger.log(line));
});
}
}
+23 -16
View File
@@ -6,42 +6,49 @@
* @license MIT
*/
import { exec } from 'child_process';
import { promisify } from 'util';
import { exec } from 'node:child_process';
import { promisify } from 'node:util';
import { createLogger } from './logging.js';
const logger = createLogger('winreg');
export const regQuery = async (path, key) => {
/**
* Query a registry key.
* @param {string} path
* @param {string} key
* @return {Promise<string>}
*/
export async function regQuery(path, key) {
if (process.platform !== 'win32') {
return null;
return;
}
try {
const command = `reg query "${path}" /v ${key}`;
const { stdout } = await promisify(exec)(command);
const keyPattern = ` ${key} `;
const indexOfKey = stdout.indexOf(keyPattern);
if (indexOfKey === -1) {
logger.error('could not find the registry key');
return null;
return;
}
const indexOfEol = stdout.indexOf('\r\n', indexOfKey);
if (indexOfEol === -1) {
logger.error('could not find the end of the line');
return null;
return;
}
const indexOfValue = stdout.indexOf(
' ',
indexOfKey + keyPattern.length);
const indexOfValue = stdout.indexOf(' ', indexOfKey + keyPattern.length);
if (indexOfValue === -1) {
logger.error('could not find the start of the key value');
return null;
return;
}
const value = stdout.substring(indexOfValue + 4, indexOfEol);
return value;
}
catch (err) {
return stdout.substring(indexOfValue + 4, indexOfEol);
} catch (err) {
logger.error(err);
return null;
return;
}
};
}