Kills old TGUI, hardsyncs TGUI to 3.0
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
import { setupWebpack, getWebpackConfig } from './webpack.js';
|
||||
import { reloadByondCache } from './reloader.js';
|
||||
|
||||
const noHot = process.argv.includes('--no-hot');
|
||||
const reloadOnce = process.argv.includes('--reload');
|
||||
|
||||
const setupServer = async () => {
|
||||
const config = await getWebpackConfig({
|
||||
mode: 'development',
|
||||
hot: !noHot,
|
||||
});
|
||||
// Reload cache once
|
||||
if (reloadOnce) {
|
||||
const bundleDir = config.output.path;
|
||||
await reloadByondCache(bundleDir);
|
||||
return;
|
||||
}
|
||||
// Run a development server
|
||||
await setupWebpack(config);
|
||||
};
|
||||
|
||||
setupServer();
|
||||
@@ -0,0 +1,161 @@
|
||||
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
|
||||
if (value instanceof Error) {
|
||||
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 sendRawMessage = 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 10 latest messages in the queue
|
||||
if (queue.length > 10) {
|
||||
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`);
|
||||
req.timeout = 500;
|
||||
req.send(json);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const sendLogEntry = (level, ns, ...args) => {
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
try {
|
||||
sendRawMessage({
|
||||
type: 'log',
|
||||
payload: {
|
||||
level,
|
||||
ns: ns || 'client',
|
||||
args,
|
||||
},
|
||||
});
|
||||
}
|
||||
catch (err) {}
|
||||
}
|
||||
};
|
||||
|
||||
export 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);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,76 @@
|
||||
import { createLogger } from 'common/logging.js';
|
||||
import fs from 'fs';
|
||||
import { basename } from 'path';
|
||||
import SourceMap from 'source-map';
|
||||
import StackTraceParser from 'stacktrace-parser';
|
||||
import { resolveGlob } from '../util.js';
|
||||
|
||||
const logger = createLogger('retrace');
|
||||
|
||||
const { SourceMapConsumer } = SourceMap;
|
||||
const sourceMaps = [];
|
||||
|
||||
export const loadSourceMaps = async bundleDir => {
|
||||
// Destroy and garbage collect consumers
|
||||
while (sourceMaps.length !== 0) {
|
||||
const { consumer } = sourceMaps.shift();
|
||||
consumer.destroy();
|
||||
}
|
||||
// Load new sourcemaps
|
||||
const paths = await resolveGlob(bundleDir, '*.map');
|
||||
for (let path of paths) {
|
||||
try {
|
||||
const file = basename(path).replace('.map', '');
|
||||
const consumer = await new SourceMapConsumer(
|
||||
JSON.parse(fs.readFileSync(path, 'utf8')));
|
||||
sourceMaps.push({ file, consumer });
|
||||
}
|
||||
catch (err) {
|
||||
logger.error(err);
|
||||
}
|
||||
}
|
||||
logger.log(`loaded ${sourceMaps.length} source maps`);
|
||||
};
|
||||
|
||||
export const retrace = stack => {
|
||||
const header = stack.split(/\n\s.*at/)[0];
|
||||
const mappedStack = StackTraceParser.parse(stack)
|
||||
.map(frame => {
|
||||
if (!frame.file) {
|
||||
return frame;
|
||||
}
|
||||
// Find the correct source map
|
||||
const sourceMap = sourceMaps.find(sourceMap => {
|
||||
return frame.file.includes(sourceMap.file);
|
||||
});
|
||||
if (!sourceMap) {
|
||||
return frame;
|
||||
}
|
||||
// Map the frame
|
||||
const { consumer } = sourceMap;
|
||||
const mappedFrame = consumer.originalPositionFor({
|
||||
source: basename(frame.file),
|
||||
line: frame.lineNumber,
|
||||
column: frame.column,
|
||||
});
|
||||
return {
|
||||
...frame,
|
||||
file: mappedFrame.source,
|
||||
lineNumber: mappedFrame.line,
|
||||
column: mappedFrame.column,
|
||||
};
|
||||
})
|
||||
.map(frame => {
|
||||
// Stringify the frame
|
||||
const { file, methodName, lineNumber } = frame;
|
||||
if (!file) {
|
||||
return ` at ${methodName}`;
|
||||
}
|
||||
const compactPath = file
|
||||
.replace(/^webpack:\/\/\/?/, './')
|
||||
.replace(/.*node_modules\//, '');
|
||||
return ` at ${methodName} (${compactPath}:${lineNumber})`;
|
||||
})
|
||||
.join('\n');
|
||||
return header + '\n' + mappedStack;
|
||||
};
|
||||
@@ -0,0 +1,71 @@
|
||||
import { createLogger } from 'common/logging.js';
|
||||
import fs from 'fs';
|
||||
import os from 'os';
|
||||
import { basename } from 'path';
|
||||
import { promisify } from 'util';
|
||||
import { resolveGlob, resolvePath } from './util.js';
|
||||
|
||||
const logger = createLogger('reloader');
|
||||
|
||||
const HOME = os.homedir();
|
||||
const SEARCH_LOCATIONS = [
|
||||
// Custom location
|
||||
process.env.BYOND_CACHE,
|
||||
// Windows
|
||||
`${HOME}/*/BYOND/cache`,
|
||||
// Wine
|
||||
`${HOME}/.wine/drive_c/users/*/*/BYOND/cache`,
|
||||
// Lutris
|
||||
`${HOME}/Games/byond/drive_c/users/*/*/BYOND/cache`,
|
||||
// WSL
|
||||
`/mnt/c/Users/*/*/BYOND/cache`,
|
||||
];
|
||||
|
||||
let cacheRoot;
|
||||
|
||||
export const findCacheRoot = async () => {
|
||||
if (cacheRoot) {
|
||||
return cacheRoot;
|
||||
}
|
||||
logger.log('looking for byond cache');
|
||||
// Find BYOND cache folders
|
||||
for (let pattern of SEARCH_LOCATIONS) {
|
||||
if (!pattern) {
|
||||
continue;
|
||||
}
|
||||
const paths = await resolveGlob(pattern);
|
||||
if (paths.length > 0) {
|
||||
cacheRoot = paths[0];
|
||||
logger.log(`found cache at '${cacheRoot}'`);
|
||||
return cacheRoot;
|
||||
}
|
||||
}
|
||||
logger.log('found no cache directories');
|
||||
};
|
||||
|
||||
export const reloadByondCache = async bundleDir => {
|
||||
const cacheRoot = await findCacheRoot();
|
||||
if (!cacheRoot) {
|
||||
return;
|
||||
}
|
||||
// Find tmp folders in cache
|
||||
const cacheDirs = await resolveGlob(cacheRoot, './tmp*');
|
||||
if (cacheDirs.length === 0) {
|
||||
logger.log('found no tmp folder in cache');
|
||||
return;
|
||||
}
|
||||
const assets = await resolveGlob(bundleDir, './*.+(bundle|hot-update).*');
|
||||
for (let cacheDir of cacheDirs) {
|
||||
// Clear garbage
|
||||
const garbage = await resolveGlob(cacheDir, './*.+(bundle|hot-update).*');
|
||||
for (let file of garbage) {
|
||||
await promisify(fs.unlink)(file);
|
||||
}
|
||||
// Copy assets
|
||||
for (let asset of assets) {
|
||||
const destination = resolvePath(cacheDir, basename(asset));
|
||||
await promisify(fs.copyFile)(asset, destination);
|
||||
}
|
||||
logger.log(`copied ${assets.length} files to '${cacheDir}'`);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
import glob from 'glob';
|
||||
import { resolve as resolvePath } from 'path';
|
||||
import fs from 'fs';
|
||||
import { promisify } from 'util';
|
||||
|
||||
export { resolvePath };
|
||||
|
||||
/**
|
||||
* Combines path.resolve with glob patterns.
|
||||
*/
|
||||
export const resolveGlob = async (...sections) => {
|
||||
const unsafePaths = await promisify(glob)(
|
||||
resolvePath(...sections), {
|
||||
strict: false,
|
||||
silent: true,
|
||||
});
|
||||
const safePaths = [];
|
||||
for (let path of unsafePaths) {
|
||||
try {
|
||||
await promisify(fs.stat)(path);
|
||||
safePaths.push(path);
|
||||
}
|
||||
catch {}
|
||||
}
|
||||
return safePaths;
|
||||
};
|
||||
@@ -0,0 +1,54 @@
|
||||
import { createLogger } from 'common/logging.js';
|
||||
import fs from 'fs';
|
||||
import { createRequire } from 'module';
|
||||
import { promisify } from 'util';
|
||||
import webpack from 'webpack';
|
||||
import { broadcastMessage, loadSourceMaps, setupLink } from './link/server.js';
|
||||
import { reloadByondCache } from './reloader.js';
|
||||
import { resolveGlob } from './util.js';
|
||||
|
||||
const logger = createLogger('webpack');
|
||||
|
||||
export const getWebpackConfig = async options => {
|
||||
const require = createRequire(import.meta.url);
|
||||
const createConfig = await require('../tgui/webpack.config.js');
|
||||
return createConfig({}, options);
|
||||
};
|
||||
|
||||
export const setupWebpack = async config => {
|
||||
logger.log('setting up');
|
||||
const bundleDir = config.output.path;
|
||||
// Setup link
|
||||
const link = setupLink();
|
||||
// Instantiate the compiler
|
||||
const compiler = webpack(config);
|
||||
// Clear garbage before compiling
|
||||
compiler.hooks.watchRun.tapPromise('tgui-dev-server', async () => {
|
||||
const files = await resolveGlob(bundleDir, './*.hot-update.*');
|
||||
logger.log(`clearing garbage (${files.length} files)`);
|
||||
for (let file of files) {
|
||||
await promisify(fs.unlink)(file);
|
||||
}
|
||||
logger.log('compiling');
|
||||
});
|
||||
// Start reloading when it's finished
|
||||
compiler.hooks.done.tap('tgui-dev-server', async stats => {
|
||||
// Load source maps
|
||||
await loadSourceMaps(bundleDir);
|
||||
// Reload cache
|
||||
await reloadByondCache(bundleDir);
|
||||
// Notify all clients that update has happened
|
||||
broadcastMessage(link, {
|
||||
type: 'hotUpdate',
|
||||
});
|
||||
});
|
||||
// Start watching
|
||||
logger.log('watching for changes');
|
||||
compiler.watch({}, (err, stats) => {
|
||||
if (err) {
|
||||
logger.error('compilation error', err);
|
||||
return;
|
||||
}
|
||||
logger.log(stats.toString(config.devServer.stats));
|
||||
});
|
||||
};
|
||||
Reference in New Issue
Block a user