Merge branch 'main' into add-prettier

This commit is contained in:
Alt 2022-10-01 12:36:43 +02:00 committed by GitHub
commit 77e641d9d3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 3459 additions and 64 deletions

54
.github/workflows/ci.yml vendored Normal file
View file

@ -0,0 +1,54 @@
name: CI
on:
# Triggers the workflow on push or pull request events but only for the dev branch
push:
pull_request:
branches: [main]
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
jobs:
# lint:
# name: Lint
# runs-on: ubuntu-latest
# steps:
# - uses: actions/checkout@v2
# - name: Use Node.js 16.13.1
# uses: actions/setup-node@v2
# with:
# node-version: 16.13.1
# cache: "npm"
# - name: Install npm dependencies
# run: npm ci
# - name: Run linter
# run: npm run lint:report
# prettier:
# name: Prettier
# runs-on: ubuntu-latest
# steps:
# - uses: actions/checkout@v2
# - name: Use Node.js 16.13.1
# uses: actions/setup-node@v2
# with:
# node-version: 16.13.1
# cache: "npm"
# - name: Install npm dependencies
# run: npm ci
# - name: Run prettier check
# run: npm run format:report
test:
name: Test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Use Node.js 16.13.1
uses: actions/setup-node@v2
with:
node-version: 16.13.1
cache: "npm"
- name: Install npm dependencies
run: npm ci
- name: Run tests
run: npm run test:all

View file

@ -1,4 +0,0 @@
#!/usr/bin/env node
import {start} from "../src/index.js"
await start();

4
npx/bitburner-filesync.ts Executable file
View file

@ -0,0 +1,4 @@
#!/usr/bin/env ts-node
import { start } from "../src/index"
await start();

3103
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -3,12 +3,14 @@
"version": "1.1.5",
"description": "Official implementation of the Bitburner Filesync server",
"type": "module",
"bin": "./npx/bitburner-filesync.js",
"main": "./npx/bitburner-filesync.js",
"bin": "./npx/bitburner-filesync.ts",
"main": "./npx/bitburner-filesync.ts",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"format": "prettier -w .",
"start": "node src/index.js"
"test:all": "mocha --recursive test",
"test:single": "mocha",
"start": "npx/bitburner-filesync.ts"
},
"repository": {
"type": "git",
@ -29,9 +31,45 @@
"cheap-watch": "^1.0.4",
"convict": "^6.2.3",
"signal-js": "^3.0.1",
"ts-node": "^10.9.1",
"typescript": "^4.8.4",
"ws": "^8.8.1"
},
"devDependencies": {
"prettier": "^2.7.1"
"@types/chai": "^4.3.3",
"@types/convict": "^6.1.1",
"@types/expect": "^24.3.0",
"@types/mocha": "^10.0.0",
"@types/node": "^18.7.23",
"@types/sinon": "^10.0.13",
"@types/sinon-chai": "^3.2.8",
"@types/ws": "^8.5.3",
"chai": "^4.3.6",
"mocha": "^10.0.0",
"sinon": "^14.0.0",
"sinon-chai": "^3.7.0"
},
"mocha": {
"bail": false,
"delay": false,
"diff": true,
"extension": [
"ts"
],
"node-option": "loader=ts-node/esm",
"parallel": true,
"reporter": "spec",
"require": [
"./test/setup-tests.ts"
],
"slow": "75",
"timeout": "2000",
"ui": "bdd",
"watch-files": [
"test/**/*.ts"
],
"watch-ignore": []
}
}

View file

@ -1,9 +1,11 @@
import CheapWatch from "cheap-watch";
import { config } from "./config.js";
import { EventType } from "./eventTypes.js";
import { config } from "./config";
import { EventType } from "./eventTypes";
import { resolve } from "path";
import type { Signal } from 'signal-js';
import type { File } from './interfaces';
function fileFilter(file) {
function fileFilter(file: File) {
if (config.get("allowedFiletypes").some(extension => file.path.endsWith(extension)))
return true;
if (file.stats.isDirectory())
@ -11,7 +13,7 @@ function fileFilter(file) {
return false;
}
export async function setupWatch(signaller) {
export async function setupWatch(signaller: Signal) {
const watch = new CheapWatch({
dir: config.get("scriptsFolder"),
filter: fileFilter,
@ -32,4 +34,4 @@ export async function setupWatch(signaller) {
}
return watch;
}
}

View file

@ -1,11 +1,11 @@
"use strict"
import { setupWatch } from "./fileWatch.js";
import { config, loadConfig } from "./config.js";
import { setupSocket } from "./networking/webSocket.js";
import { setupWatch } from "./fileWatch";
import { config, loadConfig } from "./config";
import { setupSocket } from "./networking/webSocket";
import signal from "signal-js";
import { fileChangeEventToMsg, fileRemovalEventToMsg, requestFilenames, requestDefinitionFile } from "./networking/messageGenerators.js";
import { EventType } from "./eventTypes.js";
import { messageHandler } from "./networking/messageHandler.js";
import { fileChangeEventToMsg, fileRemovalEventToMsg, requestFilenames, requestDefinitionFile } from "./networking/messageGenerators";
import { EventType } from "./eventTypes";
import { messageHandler } from "./networking/messageHandler";
import { FileEvent, Message } from './interfaces';
export async function start() {
loadConfig();
@ -13,7 +13,7 @@ export async function start() {
const socket = setupSocket(signal);
// Add a handler for received messages.
signal.on(EventType.MessageReceived, msg => messageHandler(signal, msg, watch.paths));
signal.on(EventType.MessageReceived, (msg: Message) => messageHandler(signal, msg, watch.paths));
// Add a handler for when a connection to a game is made.
signal.on(EventType.ConnectionMade, () => {
@ -27,7 +27,7 @@ export async function start() {
const extensions = config.get("allowedFiletypes");
for (const path of watch.paths.keys()) {
if (extensions.some(extension => path.endsWith(extension)))
signal.emit(EventType.MessageSend, fileChangeEventToMsg({ path: path }))
signal.emit(EventType.MessageSend, fileChangeEventToMsg({ path }))
}
} else {
// Upload missing files to the game.
@ -36,14 +36,14 @@ export async function start() {
})
// Add a handler for changed files.
signal.on(EventType.FileChanged, fileEvent => {
signal.on(EventType.FileChanged, (fileEvent: FileEvent) => {
if (!config.get("quiet")) console.log(fileEvent.path + " changed");
signal.emit(EventType.MessageSend, fileChangeEventToMsg(fileEvent))
});
// Add a handler for removed files, if allowed.
if (config.get("allowDeletingFiles"))
signal.on(EventType.FileDeleted, fileEvent =>
signal.on(EventType.FileDeleted, (fileEvent: FileEvent) =>
signal.emit(EventType.MessageSend, fileRemovalEventToMsg(fileEvent)));
console.log(`Server is ready, running on ${config.get("port")}!`)

16
src/interfaces.ts Normal file
View file

@ -0,0 +1,16 @@
import type { Stats } from 'fs';
export interface Message {
id: string;
method?: string;
jsonrpc: string;
params?: object;
}
export interface FileEvent {
path: string;
}
export interface File extends FileEvent {
stats: Stats
}

View file

@ -1,10 +1,11 @@
import { readFileSync } from "fs";
import { config } from "../config.js";
import { config } from "../config";
import { join } from "path";
import type { FileEvent, Message } from '../interfaces';
let messageCounter = 0;
export function fileChangeEventToMsg({ path }) {
export function fileChangeEventToMsg({ path }: FileEvent): Message {
return {
"jsonrpc": "2.0",
"method": "pushFile",
@ -13,11 +14,11 @@ export function fileChangeEventToMsg({ path }) {
"filename": addLeadingSlash(path),
"content": readFileSync(join(config.get("scriptsFolder"), path)).toString()
},
"id": messageCounter++
"id": (messageCounter++).toString()
}
}
export function fileRemovalEventToMsg({ path }) {
export function fileRemovalEventToMsg({ path }: FileEvent): Message {
return {
"jsonrpc": "2.0",
"method": "deleteFile",
@ -25,30 +26,30 @@ export function fileRemovalEventToMsg({ path }) {
"server": "home",
"filename": addLeadingSlash(path),
},
"id": messageCounter++
"id": (messageCounter++).toString()
}
}
export function requestDefinitionFile() {
export function requestDefinitionFile(): Message {
return {
"jsonrpc": "2.0",
"method": "getDefinitionFile",
"id": messageCounter++
"id": (messageCounter++).toString()
}
}
export function requestFilenames() {
export function requestFilenames(): Message {
return {
"jsonrpc": "2.0",
"method": "getFileNames",
"params": {
"server": "home",
},
"id": messageCounter++
"id": (messageCounter++).toString()
}
}
function addLeadingSlash(path){
function addLeadingSlash(path: string): string {
const slashes = path.match('/');
if (slashes)
return `/${path}`

View file

@ -1,10 +1,12 @@
import { messageTracker } from "./messageTracker.js";
import { writeFile } from "fs";
import { config } from "../config.js";
import { EventType } from "../eventTypes.js";
import { fileChangeEventToMsg } from "./messageGenerators.js";
import { messageTracker } from "./messageTracker";
import { Stats, writeFile } from "fs";
import { config } from "../config";
import { EventType } from "../eventTypes";
import { fileChangeEventToMsg } from "./messageGenerators";
import type { Signal } from 'signal-js';
import { Message } from '../interfaces';
export function messageHandler(signaller, msg, paths) {
export function messageHandler(signaller: Signal, msg: Message, paths: Map<string, Stats>) {
let incoming;
try { incoming = JSON.parse(msg.toString()); }
@ -25,7 +27,7 @@ export function messageHandler(signaller, msg, paths) {
if (request?.method &&
request.method == "getFileNames"
&& incoming.result) {
const gameFiles = incoming.result.map(file => removeLeadingSlash(file));
const gameFiles = incoming.result.map((file: string) => removeLeadingSlash(file));
paths.forEach((stats, fileName) => {
if (!stats.isDirectory() && !gameFiles.includes(fileName))
@ -35,7 +37,7 @@ export function messageHandler(signaller, msg, paths) {
}
}
function removeLeadingSlash(path) {
function removeLeadingSlash(path: string) {
const reg = /^\//;
return path.replace(reg, "")
}
}

View file

@ -1,8 +1,10 @@
class MessageTracker {
data = new Map()
#maxLength = 200
import type { Message } from '../interfaces';
push(msg) {
class MessageTracker {
data = new Map<string, Message>();
#maxLength = 200;
push(msg: Message) {
this.data.set(msg.id, msg);
if (this.data.size > this.#maxLength) {
@ -11,9 +13,9 @@ class MessageTracker {
}
}
get(index) {
get(index: string) {
return this.data.get(index);
}
}
export const messageTracker = new MessageTracker();
export const messageTracker = new MessageTracker();

View file

@ -1,16 +1,18 @@
import type { Signal } from 'signal-js';
import { WebSocketServer } from 'ws';
import { config } from "../config.js";
import { EventType } from "../eventTypes.js"
import { requestDefinitionFile } from './messageGenerators.js';
import { messageTracker } from "./messageTracker.js"
import { config } from "../config";
import { EventType } from "../eventTypes"
import { Message } from '../interfaces';
import { requestDefinitionFile } from './messageGenerators';
import { messageTracker } from "./messageTracker"
export function setupSocket(signaller) {
export function setupSocket(signaller: Signal) {
const wss = new WebSocketServer({ port: config.get("port") });
wss.on('connection', function connection(ws) {
function sendMessage(msg) {
function sendMessage(msg: Message) {
messageTracker.push(msg);
ws.send(JSON.stringify(msg));
}
@ -19,7 +21,7 @@ export function setupSocket(signaller) {
signaller.emit(EventType.MessageReceived, msg);
});
signaller.on(EventType.MessageSend, msg => {
signaller.on(EventType.MessageSend, (msg: Message) => {
sendMessage(msg);
});

10
src/signals.d.ts vendored Normal file
View file

@ -0,0 +1,10 @@
declare module 'signal-js' {
export type Signal = typeof signal;
export default class signal {
static on<T>(event: string, callback: (data: T) => void): void
static emit<T>(event: string, data: T): void
static trigger(event: string): void
}
}

29
test/fileWatch.spec.ts Normal file
View file

@ -0,0 +1,29 @@
import { setupWatch } from '../src/fileWatch';
import { expect } from 'chai';
import { stub, createStubInstance } from 'sinon';
import CheapWatch from 'cheap-watch';
import signal from 'signal-js';
describe('fileWatch', () => {
describe('setupWatch', () => {
it('should exist', () => {
expect(setupWatch).to.exist;
});
it('should instantiate and initialize CheapWatch', async () => {
const consoleStub = stub(console, 'log');
const watchInstance = createStubInstance(CheapWatch);
const watchConstructorStub = stub().returns(watchInstance);
Object.setPrototypeOf(CheapWatch, watchConstructorStub);
const result = await setupWatch(signal);
expect(result).to.eq(watchInstance);
expect(watchConstructorStub).to.have.been.called;
expect(watchInstance.init).to.have.been.called;
expect(watchInstance.on).to.have.been.calledTwice;
consoleStub.restore();
});
});
});

View file

@ -0,0 +1,28 @@
import { expect } from 'chai';
import { fileChangeEventToMsg, fileRemovalEventToMsg, requestDefinitionFile, requestFilenames } from '../../src/networking/messageGenerators';
describe('messageGenerators', () => {
describe('fileChangeEventToMsg', () => {
it('should exist', () => {
expect(fileChangeEventToMsg).to.exist;
});
});
describe('fileRemovalEventToMsg', () => {
it('should exist', () => {
expect(fileRemovalEventToMsg).to.exist;
});
});
describe('requestDefinitionFile', () => {
it('should exist', () => {
expect(requestDefinitionFile).to.exist;
});
});
describe('requestFilenames', () => {
it('should exist', () => {
expect(requestFilenames).to.exist;
});
});
});

View file

@ -0,0 +1,8 @@
import { expect } from 'chai';
import { messageHandler } from '../../src/networking/messageHandler';
describe('messageHandler', () => {
it('should exist', () => {
expect(messageHandler).to.exist;
});
});

View file

@ -0,0 +1,8 @@
import { expect } from 'chai';
import { messageTracker } from '../../src/networking/messageTracker';
describe('messageTracker', () => {
it('should exist', () => {
expect(messageTracker).to.exist;
});
});

View file

@ -0,0 +1,7 @@
import { expect } from 'chai';
describe('webSocket', () => {
it('should run test', () => {
expect(true).to.eq(true)
})
});

31
test/setup-tests.ts Normal file
View file

@ -0,0 +1,31 @@
import chai from 'chai';
import { createSandbox, stub } from 'sinon';
import sinonChai from 'sinon-chai';
export async function mochaGlobalSetup() {
// initial global setup
// runs once for all threads
}
export const mochaHooks: Mocha.RootHookObject = {
// runs once at the beginning of each thread
beforeAll(done: () => void) {
done();
},
// runs once at the end of each thread
afterAll(done: () => void) {
done();
},
// runs once before each test
beforeEach(done: () => void) {
chai.should();
chai.use(sinonChai);
this.sandbox = createSandbox();
done();
},
// runs once after each test
afterEach(done: () => void) {
this.sandbox.restore();
done();
}
}

70
tsconfig.json Normal file
View file

@ -0,0 +1,70 @@
{
"ts-node": {
"files": true,
"esm": true,
"experimentalSpecifierResolution": "node"
},
"include": [
"src/signals.d.ts",
"src/**/*",
"test/**/*"
],
"compilerOptions": {
/* Basic Options */
// "incremental": true, /* Enable incremental compilation */
"target": "esnext", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */
"module": "esnext", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
// "lib": [], /* Specify library files to be included in the compilation. */
// "allowJs": true, /* Allow javascript files to be compiled. */
// "checkJs": true, /* Report errors in .js files. */
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
// "declaration": true, /* Generates corresponding '.d.ts' file. */
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
// "sourceMap": true, /* Generates corresponding '.map' file. */
// "outFile": "./", /* Concatenate and emit output to single file. */
"outDir": "./dist", /* Redirect output structure to the directory. */
// "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
// "composite": true, /* Enable project compilation */
// "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
// "removeComments": true, /* Do not emit comments to output. */
// "noEmit": true, /* Do not emit outputs. */
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
/* Strict Type-Checking Options */
"strict": true, /* Enable all strict type-checking options. */
// "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* Enable strict null checks. */
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
// "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
// "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
/* Additional Checks */
// "noUnusedLocals": true, /* Report errors on unused locals. */
// "noUnusedParameters": true, /* Report errors on unused parameters. */
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
/* Module Resolution Options */
"moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
// "typeRoots": [], /* List of folders to include type definitions from. */
// "types": [], /* Type declaration files to be included in compilation. */
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
"esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
/* Source Map Options */
// "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
/* Experimental Options */
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
/* Advanced Options */
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
}
}