Kills old TGUI, hardsyncs TGUI to 3.0

This commit is contained in:
Artur
2020-04-23 13:31:48 +03:00
parent 321902c35a
commit fe26034410
431 changed files with 2381 additions and 34240 deletions
+22
View File
@@ -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;
};
+71
View File
@@ -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}'`);
}
};
+26
View File
@@ -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;
};
+54
View File
@@ -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));
});
};