Add basic multiplayer

This commit is contained in:
schmelczerandras 2020-10-05 19:07:17 +02:00
parent 0f0a1eaf67
commit 46a48e7c15
113 changed files with 1362 additions and 754 deletions

View file

@ -15,7 +15,6 @@
"@types/express": "^4.17.8",
"@types/gl-matrix": "^2.4.5",
"@types/node": "^14.11.2",
"@types/uuid": "^8.0.0",
"@typescript-eslint/eslint-plugin": "^3.9.1",
"@typescript-eslint/parser": "^3.9.1",
"clean-webpack-plugin": "^3.0.0",
@ -32,20 +31,22 @@
"terser-webpack-plugin": "^2.3.5",
"ts-loader": "^8.0.1",
"typescript": "^3.8.3",
"@types/uuid": "^8.0.0",
"uuid": "^8.2.0",
"webpack": "^4.43.0",
"webpack-cli": "^3.3.11",
"webpack-dev-server": "^3.10.3",
"file-loader": "^6.1.0"
},
"dependencies": {
"file-loader": "^6.1.0",
"@types/cors": "^2.8.7",
"@types/socket.io": "^2.1.11",
"webpack-node-externals": "^2.5.2",
"shared": "file:../shared"
},
"dependencies": {
"cors": "^2.8.5",
"express": "^4.17.1",
"http": "0.0.1-security",
"socket.io": "^2.3.0",
"uws": "^10.148.1",
"webpack-node-externals": "^2.5.2"
"uws": "^10.148.1"
}
}

View file

@ -2,11 +2,27 @@ import ioserver, { Socket } from 'socket.io';
import express from 'express';
import { Server } from 'http';
import cors from 'cors';
import { PlayerContainer } from './players/player-container';
import { applyArrayPlugins, Random, TransportEvents, deserializeCommand } from 'shared';
import './index.html';
import { TransportEvents } from '../../shared/src/transport/transport-events';
import { Player } from './players/player';
import { PhysicalGameObjectContainer } from './physics/physical-game-object-container';
import { createDungeon } from './map/create-dungeon';
import { glMatrix } from 'gl-matrix';
glMatrix.setMatrixArrayType(Array);
applyArrayPlugins();
Random.seed = 42;
const objects = new PhysicalGameObjectContainer();
createDungeon(objects);
createDungeon(objects);
createDungeon(objects);
createDungeon(objects);
objects.initialize();
const app = express();
@ -23,8 +39,6 @@ const port = 3000;
const server = new Server(app);
const io = ioserver(server);
const players = new PlayerContainer();
const log = (text: string) => {
io.to('insights').emit('insights', text + '\n');
};
@ -35,17 +49,15 @@ app.get('/', function (req, res) {
io.on('connection', (socket: SocketIO.Socket) => {
socket.on(TransportEvents.PlayerJoining, () => {
log('player joined');
const player = new Player(objects, socket);
const player = new Player(socket);
players.addPlayer(player);
socket.on(TransportEvents.PlayerSendingInfo, () => {});
socket.on(TransportEvents.PlayerToServer, (text: string) => {
const command = deserializeCommand(JSON.parse(text));
player.sendCommand(command);
});
socket.on('disconnect', () => {
log('player disconnected');
players.removePlayerBySocketId(player.socketId);
player.destroy();
});
});

View file

@ -0,0 +1,46 @@
import { vec2, vec3 } from 'gl-matrix';
import { Random } from 'shared';
import { LampPhysics } from '../objects/lamp-physics';
import { TunnelPhysics } from '../objects/tunnel-physics';
import { PhysicalGameObjectContainer } from '../physics/physical-game-object-container';
export const createDungeon = (objects: PhysicalGameObjectContainer) => {
let previousRadius = 350;
let previousEnd = vec2.create();
let tunnelsCountSinceLastLight = 0;
for (let i = 0; i < 500000; i += 500) {
const deltaHeight = (Random.getRandom() - 0.5) * 2000;
const height = previousEnd.y + deltaHeight;
const currentEnd = vec2.fromValues(i, height);
const currentToRadius = Random.getRandom() * 300 + 150;
const tunnel = new TunnelPhysics(
previousEnd,
currentEnd,
previousRadius,
currentToRadius
);
objects.addObject(tunnel, false);
if (++tunnelsCountSinceLastLight > 3 && Random.getRandom() > 0.7) {
objects.addObject(
new LampPhysics(
currentEnd,
vec3.normalize(
vec3.create(),
vec3.fromValues(Random.getRandom(), 0, Random.getRandom())
),
0.5
),
false
);
tunnelsCountSinceLastLight = 0;
}
previousEnd = currentEnd;
previousRadius = currentToRadius;
}
};

View file

@ -0,0 +1,31 @@
import { id, CharacterBase } from 'shared';
import { ImmutableBoundingBox } from '../physics/bounds/immutable-bounding-box';
import { PhysicalGameObject } from '../physics/physical-game-object';
import { CirclePhysics } from './circle-physics';
export class CharacterPhysics extends CharacterBase implements PhysicalGameObject {
public readonly canCollide = true;
public readonly isInverted = false;
public readonly canMove = true;
constructor(head: CirclePhysics, leftFoot: CirclePhysics, rightFoot: CirclePhysics) {
super(id(), head, leftFoot, rightFoot);
}
private boundingBox?: ImmutableBoundingBox;
public getBoundingBox(): ImmutableBoundingBox {
if (!this.boundingBox) {
this.boundingBox = (this.head as CirclePhysics).boundingBox;
(this.head as CirclePhysics).boundingBox.owner = this;
}
return this.boundingBox;
}
public toJSON(): any {
const { type, id, head, leftFoot, rightFoot } = this;
return [type, id, head, leftFoot, rightFoot];
}
}

View file

@ -0,0 +1,76 @@
import { vec2 } from 'gl-matrix';
import { Circle } from 'shared';
import { BoundingBox } from '../physics/bounds/bounding-box';
import { BoundingBoxBase } from '../physics/bounds/bounding-box-base';
export class CirclePhysics implements Circle {
private _boundingBox: BoundingBox;
constructor(private _center: vec2, private _radius: number) {
this._boundingBox = new BoundingBox(null);
this.recalculateBoundingBox();
}
public get boundingBox(): BoundingBoxBase {
return this._boundingBox;
}
public get center(): vec2 {
return this._center;
}
public set center(value: vec2) {
this._center = value;
this.recalculateBoundingBox();
}
public get radius(): number {
return this._radius;
}
public set radius(value: number) {
this._radius = value;
this.recalculateBoundingBox();
}
public distance(target: vec2): number {
return vec2.distance(target, this.center) - this.radius;
}
public distanceBetween(target: CirclePhysics): number {
return vec2.distance(target.center, this.center) - this.radius - target.radius;
}
public areIntersecting(other: CirclePhysics): boolean {
return other.distance(this.center) < this.radius;
}
public isInside(other: CirclePhysics): boolean {
return other.distance(this.center) < -this.radius;
}
public getPerimeterPoints(count: number): Array<vec2> {
const result: Array<vec2> = [];
for (let i = 0; i < count; i++) {
result.push(
vec2.fromValues(
Math.cos((2 * Math.PI * i) / count) * this.radius + this.center.x,
Math.sin((2 * Math.PI * i) / count) * this.radius + this.center.y
)
);
}
return result;
}
private recalculateBoundingBox() {
this._boundingBox.xMin = this.center.x - this._radius;
this._boundingBox.xMax = this.center.x + this._radius;
this._boundingBox.yMin = this.center.y - this._radius;
this._boundingBox.yMax = this.center.y + this._radius;
}
public toJSON(): any {
const { center, radius } = this;
return { center, radius };
}
}

View file

@ -0,0 +1,36 @@
import { vec2, vec3 } from 'gl-matrix';
import { LampBase, settings, id } from 'shared';
import { ImmutableBoundingBox } from '../physics/bounds/immutable-bounding-box';
import { PhysicalGameObject } from '../physics/physical-game-object';
export class LampPhysics extends LampBase implements PhysicalGameObject {
public readonly canCollide = false;
public readonly isInverted = false;
public readonly canMove = false;
constructor(center: vec2, color: vec3, lightness: number) {
super(id(), center, color, lightness);
}
private boundingBox?: ImmutableBoundingBox;
public getBoundingBox(): ImmutableBoundingBox {
if (!this.boundingBox) {
this.boundingBox = new ImmutableBoundingBox(
this,
this.center.x - settings.lightCutoffDistance,
this.center.x + settings.lightCutoffDistance,
this.center.y - settings.lightCutoffDistance,
this.center.y + settings.lightCutoffDistance
);
}
return this.boundingBox;
}
public toJSON(): any {
const { type, id, center, color, lightness } = this;
return [type, id, center, color, lightness];
}
}

View file

@ -0,0 +1,48 @@
import { vec2 } from 'gl-matrix';
import { clamp01, mix, TunnelBase, id } from 'shared';
import { PhysicalGameObject } from '../physics/physical-game-object';
import { ImmutableBoundingBox } from '../physics/bounds/immutable-bounding-box';
export class TunnelPhysics extends TunnelBase implements PhysicalGameObject {
public readonly canCollide = true;
public readonly isInverted = true;
public readonly canMove = false;
private boundingBox?: ImmutableBoundingBox;
constructor(from: vec2, to: vec2, fromRadius: number, toRadius: number) {
super(id(), from, to, fromRadius, toRadius);
}
public distance(target: vec2): number {
const toFromDelta = vec2.subtract(vec2.create(), this.to, this.from);
const targetFromDelta = vec2.subtract(vec2.create(), target, this.from);
const h = clamp01(
vec2.dot(targetFromDelta, toFromDelta) / vec2.dot(toFromDelta, toFromDelta)
);
return (
vec2.distance(targetFromDelta, vec2.scale(vec2.create(), toFromDelta, h)) -
mix(this.fromRadius, this.toRadius, h)
);
}
public getBoundingBox(): ImmutableBoundingBox {
if (!this.boundingBox) {
const xMin = Math.min(this.from.x - this.fromRadius, this.to.x - this.toRadius);
const yMin = Math.min(this.from.y - this.fromRadius, this.to.y - this.toRadius);
const xMax = Math.max(this.from.x + this.fromRadius, this.to.x + this.toRadius);
const yMax = Math.max(this.from.y + this.fromRadius, this.to.y + this.toRadius);
this.boundingBox = new ImmutableBoundingBox(this, xMin, xMax, yMin, yMax, true);
}
return this.boundingBox;
}
public toJSON(): any {
const { type, id, from, to, fromRadius, toRadius } = this;
return [type, id, from, to, fromRadius, toRadius];
}
}

View file

@ -0,0 +1,62 @@
import { vec2 } from 'gl-matrix';
import { PhysicalGameObject } from '../physical-game-object';
export abstract class BoundingBoxBase {
constructor(
public owner: PhysicalGameObject,
protected _xMin: number = 0,
protected _xMax: number = 0,
protected _yMin: number = 0,
protected _yMax: number = 0,
public readonly isInverted = false
) {}
public get 0(): number {
return this._xMin;
}
public get 1(): number {
return this._xMax;
}
public get 2(): number {
return this._yMin;
}
public get 3(): number {
return this._yMax;
}
public get xMin(): number {
return this._xMin;
}
public get xMax(): number {
return this._xMax;
}
public get yMin(): number {
return this._yMin;
}
public get yMax(): number {
return this._yMax;
}
public get topLeft(): vec2 {
return vec2.fromValues(this._xMin, this._yMax);
}
public get size(): vec2 {
return vec2.fromValues(this._xMax - this._xMin, this._yMax - this._yMin);
}
public intersects(other: BoundingBoxBase): boolean {
return (
this._xMin < other._xMax &&
this._xMax > other._xMin &&
this._yMin < other._yMax &&
this._yMax > other._yMin
);
}
}

View file

@ -0,0 +1,65 @@
import { vec2 } from 'gl-matrix';
import { BoundingBoxBase } from './bounding-box-base';
import { ImmutableBoundingBox } from './immutable-bounding-box';
export class BoundingBox extends BoundingBoxBase {
public get xMin(): number {
return this._xMin;
}
public set xMin(value: number) {
this._xMin = value;
}
public set xMax(value: number) {
this._xMax = value;
}
public get xMax(): number {
return this._xMax;
}
public set yMin(value: number) {
this._yMin = value;
}
public get yMin(): number {
return this._yMin;
}
public set yMax(value: number) {
this._yMax = value;
}
public get yMax(): number {
return this._yMax;
}
public get topLeft(): vec2 {
return vec2.fromValues(this._xMin, this._yMax);
}
public set topLeft(value: vec2) {
this._xMin = value.x;
this._yMax = value.y;
}
public set size(value: vec2) {
this._xMax = this.xMin + value.x;
this._yMin = this.yMax - value.y;
}
public get size(): vec2 {
return vec2.fromValues(this._xMax - this._xMin, this._yMax - this._yMin);
}
public cloneAsImmutable(): ImmutableBoundingBox {
return new ImmutableBoundingBox(
this.owner,
this.xMin,
this.xMax,
this.yMin,
this.yMax
);
}
}

View file

@ -0,0 +1,3 @@
import { BoundingBoxBase } from './bounding-box-base';
export class ImmutableBoundingBox extends BoundingBoxBase {}

View file

@ -0,0 +1,20 @@
import { BoundingBoxBase } from '../bounds/bounding-box-base';
export class BoundingBoxList {
constructor(private boundingBoxes: Array<BoundingBoxBase> = []) {}
public insert(box: BoundingBoxBase) {
this.boundingBoxes.push(box);
}
public remove(box: BoundingBoxBase) {
this.boundingBoxes.splice(
this.boundingBoxes.findIndex((i) => i === box),
1
);
}
public findIntersecting(box: BoundingBoxBase): Array<BoundingBoxBase> {
return this.boundingBoxes.filter((b) => b.intersects(box));
}
}

View file

@ -0,0 +1,121 @@
// source: https://github.com/ubilabs/kd-tree-javascript/blob/master/kdTree.js
import { ImmutableBoundingBox } from '../bounds/immutable-bounding-box';
class Node {
public left?: Node = null;
public right?: Node = null;
constructor(public rectangle: ImmutableBoundingBox, public parent: Node) {}
}
export class BoundingBoxTree {
root?: Node;
constructor(boxes: Array<ImmutableBoundingBox> = []) {
this.build(boxes);
}
public build(boxes: Array<ImmutableBoundingBox>) {
this.root = this.buildRecursive(boxes, 0, null);
}
private buildRecursive(
boxes: Array<ImmutableBoundingBox>,
depth: number,
parent: Node
): Node {
if (boxes.length === 0) {
return null;
}
if (boxes.length === 1) {
return new Node(boxes[0], parent);
}
const dimension = depth % 4;
boxes.sort((a, b) => a[dimension] - b[dimension]);
const median = Math.floor(boxes.length / 2);
const node = new Node(boxes[median], parent);
node.left = this.buildRecursive(boxes.slice(0, median), depth + 1, node);
node.right = this.buildRecursive(boxes.slice(median + 1), depth + 1, node);
return node;
}
public insert(box: ImmutableBoundingBox) {
const [insertPosition, depth] = this.findParent(box, this.root, 0, null);
if (insertPosition === null) {
this.root = new Node(box, null);
} else {
const node = new Node(box, insertPosition);
const dimension = depth % 4;
if (box[dimension] < insertPosition.rectangle[dimension]) {
insertPosition.left = node;
} else {
insertPosition.right = node;
}
}
}
public findIntersecting(box: ImmutableBoundingBox): Array<ImmutableBoundingBox> {
const maybeResults = this.findMaybeIntersecting(box, this.root, 0);
const results = maybeResults.filter((b) => b.intersects(box));
return results;
}
private findMaybeIntersecting(
box: ImmutableBoundingBox,
node: Node,
depth: number
): Array<ImmutableBoundingBox> {
if (node === null) {
return [];
}
if (depth % 4 == 0 && box.xMax < node.rectangle.xMin) {
return this.findMaybeIntersecting(box, node.left, depth + 1);
}
if (depth % 4 == 1 && box.xMin > node.rectangle.xMax) {
return this.findMaybeIntersecting(box, node.right, depth + 1);
}
if (depth % 4 == 2 && box.yMax < node.rectangle.yMin) {
return this.findMaybeIntersecting(box, node.left, depth + 1);
}
if (depth % 4 == 3 && box.yMin > node.rectangle.yMax) {
return this.findMaybeIntersecting(box, node.right, depth + 1);
}
return [
node.rectangle,
...this.findMaybeIntersecting(box, node.left, depth + 1),
...this.findMaybeIntersecting(box, node.right, depth + 1),
];
}
private findParent(
box: ImmutableBoundingBox,
node: Node,
depth: number,
parent: Node
): [Node, number] {
if (node === null) {
return [parent, depth - 1];
}
const dimension = depth % 4;
if (box[dimension] < node.rectangle[dimension]) {
return this.findParent(box, node.left, depth + 1, node);
}
return this.findParent(box, node.right, depth + 1, node);
}
}

View file

@ -0,0 +1,86 @@
import { GameObject, Id } from 'shared';
import { BoundingBoxBase } from './bounds/bounding-box-base';
import { BoundingBoxList } from './containers/bounding-box-list';
import { BoundingBoxTree } from './containers/bounding-box-tree';
import { Command } from 'shared';
import { PhysicalGameObject } from './physical-game-object';
import { ImmutableBoundingBox } from './bounds/immutable-bounding-box';
export class PhysicalGameObjectContainer {
private isTreeInitialized = false;
private staticBoundingBoxesWaitList: Array<ImmutableBoundingBox> = [];
private staticBoundingBoxes = new BoundingBoxTree();
private dynamicBoundingBoxes = new BoundingBoxList();
protected objects: Map<Id, GameObject> = new Map();
private objectsGroupedByAbilities: Map<string, Array<GameObject>> = new Map();
public initialize() {
this.staticBoundingBoxes.build(this.staticBoundingBoxesWaitList);
this.isTreeInitialized = true;
}
public addObject(object: PhysicalGameObject, isDynamic) {
this.objects.set(object.id, object);
for (const command of this.objectsGroupedByAbilities.keys()) {
if (object.reactsToCommand(command)) {
this.objectsGroupedByAbilities.get(command).push(object);
}
}
if (isDynamic) {
this.dynamicBoundingBoxes.insert(object.getBoundingBox());
} else {
if (!this.isTreeInitialized) {
this.staticBoundingBoxesWaitList.push(object.getBoundingBox());
} else {
this.staticBoundingBoxes.insert(object.getBoundingBox());
}
}
}
public removeObject(object: PhysicalGameObject) {
this.objects.delete(object.id);
for (const command of this.objectsGroupedByAbilities.keys()) {
if (object.reactsToCommand(command)) {
const array = this.objectsGroupedByAbilities.get(command);
array.splice(
array.findIndex((i) => i.id == object.id),
1
);
}
}
this.dynamicBoundingBoxes.remove(object.getBoundingBox());
}
public sendCommand(e: Command) {
if (!this.objectsGroupedByAbilities.has(e.type)) {
this.createGroupForCommand(e.type);
}
this.objectsGroupedByAbilities.get(e.type).forEach((o, _) => o.sendCommand(e));
}
private createGroupForCommand(commandType: string) {
const objectsReactingToCommand = [];
this.objects.forEach((o, _) => {
if (o.reactsToCommand(commandType)) {
objectsReactingToCommand.push(o);
}
});
this.objectsGroupedByAbilities.set(commandType, objectsReactingToCommand);
}
public findIntersecting(box: BoundingBoxBase): Array<PhysicalGameObject> {
return [
...this.staticBoundingBoxes.findIntersecting(box),
...this.dynamicBoundingBoxes.findIntersecting(box),
].map((b) => b.owner);
}
}

View file

@ -0,0 +1,10 @@
import { GameObject } from 'shared';
import { BoundingBoxBase } from './bounds/bounding-box-base';
export interface PhysicalGameObject extends GameObject {
getBoundingBox(): BoundingBoxBase;
//distance(target: vec2): number;
readonly isInverted: boolean;
readonly canCollide: boolean;
readonly canMove: boolean;
}

View file

@ -1,13 +0,0 @@
import { Player } from './player';
export class PlayerContainer {
private socketIdToPlayer = new Map<string, Player>();
public addPlayer(player: Player) {
this.socketIdToPlayer.set(player.socketId, player);
}
public removePlayerBySocketId(socketId: string) {
this.socketIdToPlayer.delete(socketId);
}
}

View file

@ -1,9 +1,119 @@
import { Socket } from 'dgram';
import { vec2 } from 'gl-matrix';
import {
Command,
CommandExecutors,
CommandReceiver,
CreateObjectsCommand,
CreatePlayerCommand,
DeleteObjectsCommand,
MoveActionCommand,
SetViewAreaActionCommand,
TransportEvents,
UpdateObjectsCommand,
} from 'shared';
import { CharacterPhysics } from '../objects/character-physics';
import { CirclePhysics } from '../objects/circle-physics';
export class Player {
constructor(private readonly socket: SocketIO.Socket) {}
import { BoundingBox } from '../physics/bounds/bounding-box';
import { PhysicalGameObject } from '../physics/physical-game-object';
import { PhysicalGameObjectContainer } from '../physics/physical-game-object-container';
import { jsonSerialize } from '../serialize';
public get socketId(): string {
return this.socket.id;
export class Player extends CommandReceiver {
public isActive = true;
private character: CharacterPhysics = new CharacterPhysics(
new CirclePhysics(vec2.fromValues(50, 50), 50),
new CirclePhysics(vec2.fromValues(50, 50), 50),
new CirclePhysics(vec2.fromValues(50, 50), 50)
);
private objectsPreviouslyInViewArea: Array<PhysicalGameObject> = [];
private objectsInViewArea: Array<PhysicalGameObject> = [];
protected commandExecutors: CommandExecutors = {
[SetViewAreaActionCommand.type]: this.setViewArea.bind(this),
[MoveActionCommand.type]: (c: MoveActionCommand) => {
vec2.normalize(c.delta, c.delta);
vec2.scale(c.delta, c.delta, 40);
this.character.head.center = vec2.add(
this.character.head.center,
this.character.head.center,
c.delta
);
},
};
protected defaultCommandExecutor(command: Command) {}
constructor(
private readonly objects: PhysicalGameObjectContainer,
private readonly socket: SocketIO.Socket
) {
super();
this.objectsPreviouslyInViewArea.push(this.character);
this.objectsInViewArea.push(this.character);
this.objects.addObject(this.character, true);
socket.emit(
TransportEvents.ServerToPlayer,
jsonSerialize(new CreatePlayerCommand(jsonSerialize(this.character)))
);
this.sendObjects();
}
public setViewArea(c: SetViewAreaActionCommand) {
const viewArea = new BoundingBox(null);
viewArea.topLeft = c.viewArea.topLeft;
viewArea.size = c.viewArea.size;
this.objectsInViewArea = this.objects.findIntersecting(viewArea);
}
public sendObjects() {
const newlyIntersecting = this.objectsInViewArea.filter(
(o) => !this.objectsPreviouslyInViewArea.includes(o)
);
const noLongerIntersecting = this.objectsPreviouslyInViewArea.filter(
(o) => !this.objectsInViewArea.includes(o)
);
this.objectsPreviouslyInViewArea = this.objectsInViewArea;
if (noLongerIntersecting.length > 0) {
this.socket.emit(
TransportEvents.ServerToPlayer,
jsonSerialize(new DeleteObjectsCommand(noLongerIntersecting.map((o) => o.id)))
);
}
if (newlyIntersecting.length > 0) {
this.socket.emit(
TransportEvents.ServerToPlayer,
jsonSerialize(new CreateObjectsCommand(jsonSerialize(newlyIntersecting)))
);
}
this.socket.emit(
TransportEvents.ServerToPlayer,
jsonSerialize(
new UpdateObjectsCommand(
jsonSerialize(this.objectsInViewArea.filter((o) => o.canMove))
)
)
);
if (this.isActive) {
//setImmediate(this.sendObjects.bind(this));
setTimeout(this.sendObjects.bind(this), 5);
}
}
public destroy() {
this.isActive = false;
this.objects.removeObject(this.character);
}
}

2
backend/src/serialize.ts Normal file
View file

@ -0,0 +1,2 @@
export const jsonSerialize = (o: any): string =>
JSON.stringify(o, (key, value) => (value?.toFixed ? Number(value.toFixed(3)) : value));

View file

@ -3,17 +3,12 @@
"outDir": "./dist/",
"sourceMap": true,
"noImplicitAny": false,
"target": "es6",
"target": "es5",
"downlevelIteration": true,
"allowJs": true,
"experimentalDecorators": true,
"moduleResolution": "node",
"module": "commonjs",
"esModuleInterop": true,
"baseUrl": ".",
"paths": {
"*": ["node_modules/*"]
}
},
"include": ["src/**/*"]
"esModuleInterop": true
}
}

View file

@ -11,7 +11,11 @@ module.exports = {
entry: {
main: [PATHS.entryPoint],
},
externals: [nodeExternals()],
externals: [
nodeExternals({
allowlist: [/(^shared)/],
}),
],
target: 'node',
output: {
filename: '[name].js',