Browse Source

Update project folder structure + setup webpack + added D3js experiment

Joeri Exelmans 3 years ago
parent
commit
e327cd348a
15 changed files with 2649 additions and 197 deletions
  1. 6 1
      .gitignore
  2. 0 119
      delta.ts
  3. 16 0
      dist/index.html
  4. 16 2
      package.json
  5. 2151 4
      pnpm-lock.yaml
  6. 108 0
      src/frontend/index.js
  7. 1 0
      src/onion/delta.test.ts
  8. 217 0
      src/onion/delta.ts
  9. 2 1
      graph_state.test.ts
  10. 64 69
      graph_state.ts
  11. 6 0
      src/onion/mock_node_util.ts
  12. 1 0
      src/onion/something.ts
  13. 26 0
      src/onion/types.ts
  14. 6 1
      tsconfig.json
  15. 29 0
      webpack.config.js

+ 6 - 1
.gitignore

@@ -1,2 +1,7 @@
 node_modules/
-.nyc_output/
+
+# coverage analysis output
+.nyc_output/
+
+# this file will be generated when running webpack:
+dist/bundle.js

+ 0 - 119
delta.ts

@@ -1,119 +0,0 @@
-
-export class NodeCreation {
-  readonly uuid: UUID;
-
-  // Inverse dependency: Incoming and outgoing edges.
-  edges: Array<EdgeCreation>; // append-only
-
-  // Inverse dependency: Deletions of this node.
-  deletions: Array<NodeDeletion>; // append-only
-
-  constructor(uuid: UUID) {
-    this.uuid = uuid;
-    this.edges = [];
-  }
-
-  // Whenever a node is deleted more than once, concurrently, there's a {DELETE,DELETE}-conflict
-  getDeleteDeleteConflicts(): Array<[NodeDeletion, NodeDeletion]> {
-    // Basically get all pairs of 'deletions'
-    const result = new Array(this.deletions**2)
-    let i = 0;
-    for (const d1 of this.deletions) {
-      for (const d2 of this.deletions) {
-        result[i] = [d1, d2];
-        i++;
-      }
-    }
-    return result;
-  }
-
-  // Whenever a node is deleted, and the source/target of a concurrent edge creation, there's a {DELETE,REQUIRE}-conflict.
-  getDeleteRequireConflicts(): Array<[NodeDeletion, EdgeCreation]> {
-
-  }
-}
-
-export class NodeDeletion  {
-  // Dependency: The node being deleted.
-  readonly deletes: NodeCreation;
-
-  // Dependency: Deletion of a node depends on deletion of its incoming and outgoing edges.
-  readonly edgeDeletions: Array<EdgeDeletion>;
-
-  constructor(deletes: NodeCreation, edgeDeletions: Array<EdgeDeletion>) {
-    this.deletes = deletes;
-    this.edgeDeletions = edgeDeletions;
-
-    // Create inverse dependency
-    this.deletes.deletions.append(this);
-  }
-}
-
-export interface EdgeOperation {
-  // Edge operations always form a tree (or sequence, if non-conflicting) where the path from root to leaf is as follows: create, update, update, ..., update, delete
-  // Therefore, any 'node' in this tree traces back to a 'root', the creation.
-  getCreation(): EdgeCreation;
-}
-
-export class EdgeCreation implements EdgeOperation {
-  // Dependency: source node
-  readonly source: NodeCreation;
-
-  // Dependency: target node
-  readonly target: NodeCreation;
-
-  readonly label: string;
-
-  // Inverse dependency: Next operation on the edge
-  nextOperations: Array<EdgeUpdate | EdgeDeletion>; // append-only
-
-  constructor(source: NodeCreation, label: string, target: NodeCreation) {
-    this.source = source;
-    this.target = target;
-    this.label = label;
-    this.nextOperations = [];
-
-    // Create inverse dependency
-    this.source.edges.push(this);
-  }
-
-  getCreation(): EdgeCreation {
-    return this;
-  }
-}
-
-export class EdgeUpdate implements EdgeOperation {
-  readonly edge: EdgeCreation | EdgeUpdate; // UPD-dependency
-  readonly newTarget: NodeCreation; // REQ-dependency
-
-  // Inverse dependency: Next operation on the edge
-  nextOperations: Array<EdgeUpdate | EdgeDeletion>; // append-only
-
-  constructor(edge: EdgeCreation | EdgeUpdate, newTarget: NodeCreation) {
-    this.edge = edge;
-    this.newTarget = newTarget;
-    this.nextOperations = [];
-
-    // Create inverse dependency
-    this.edge.nextOperations.push(this);
-  }
-
-  getCreation(): EdgeCreation {
-    return this.edge.getCreation();
-  }
-}
-
-export class EdgeDeletion implements EdgeOperation {
-  readonly edge: EdgeCreation | EdgeUpdate; // UPD-dependency
-
-  constructor(edge: EdgeCreation | EdgeUpdate) {
-    this.edge = edge;
-
-    // Create inverse dependency
-    this.edge.nextOperations.push(this);
-  }
-
-  getCreation(): EdgeCreation {
-    return this.edge.getCreation();
-  }
-}

+ 16 - 0
dist/index.html

@@ -0,0 +1,16 @@
+<html>
+  <head>
+    <meta charset="UTF-8"/>
+    <style>
+      body {
+        background-color: #ddd;
+      }
+      svg {
+        background-color: white;
+      }
+    </style>
+  </head>
+  <body>
+    <script type="module" src="./bundle.js"></script>
+  </body>
+</html>

+ 16 - 2
package.json

@@ -1,16 +1,30 @@
 {
 	"devDependencies": {
+		"@types/d3-drag": "^3.0.1",
 		"@types/mocha": "^9.1.1",
+		"d3-drag": "^3.0.0",
 		"mocha": "^10.0.0",
-		"nyc": "^15.1.0"
+		"nyc": "^15.1.0",
+		"ts-loader": "^9.3.1",
+		"webpack": "^5.74.0",
+		"webpack-cli": "^4.10.0",
+		"webpack-dev-server": "^4.9.3"
 	},
 	"dependencies": {
+		"@types/d3": "^7.4.0",
+		"@types/d3-force": "^3.0.3",
+		"@types/d3-selection": "^3.0.3",
 		"@types/node": "^18.6.1",
+		"d3": "^7.6.1",
+		"d3-force": "^3.0.0",
+		"d3-selection": "^3.0.0",
 		"ts-node": "^10.9.1",
 		"typescript": "^4.7.4"
 	},
 	"scripts": {
 		"test": "mocha --require ts-node/register './*.test.ts'",
-		"test-with-coverage": "nyc --reporter=text mocha --require ts-node/register './*.test.ts'"
+		"test-with-coverage": "nyc --reporter=text mocha --require ts-node/register './*.test.ts'",
+		"webpack": "webpack --mode=development",
+		"dev-server": "webpack serve --mode=development"
 	}
 }

File diff suppressed because it is too large
+ 2151 - 4
pnpm-lock.yaml


File diff suppressed because it is too large
+ 108 - 0
src/frontend/index.js


+ 1 - 0
src/onion/delta.test.ts

@@ -0,0 +1 @@
+import {} from "./delta"

+ 217 - 0
src/onion/delta.ts

@@ -0,0 +1,217 @@
+import {NodeId, PrimitiveType, UUID, nodeIdsEqual} from "./types";
+
+type Conflict = [MicroOperation, MicroOperation];
+
+interface MicroOperation {
+  // get conflicts between MicroOperations that depend on this MicroOperation.
+  getConflicts(): Array<Conflict>;
+}
+
+export class NodeCreation implements MicroOperation {
+  readonly id: NodeId;
+
+  // Inverse dependency: Deletions of this node.
+  deletions: Array<NodeDeletion>; // append-only
+
+  // Inverse dependency: Creation of incoming and outgoing edges.
+  edges: Array<EdgeCreation>; // append-only
+
+  constructor(id: NodeId) {
+    this.id = id;
+
+    this.deletions = [];
+    this.edges = [];
+  }
+
+  // There are 2 types of conflicts that can occur whose most nearby common ancestor is a NodeCreation: {DELETE,DELETE} and {DELETE,REQUIRE}.
+
+  // Whenever a node is deleted more than once, concurrently, there's a {DELETE,DELETE}-conflict
+  getDeleteDeleteConflicts(): Array<[NodeDeletion, NodeDeletion]> {
+    // Basically get all pairs of 'deletions'
+    const result = new Array(this.deletions.length*(this.deletions.length-1));
+    let i = 0;
+    for (const del1 of this.deletions) {
+      for (const del2 of this.deletions) {
+        if (del1 !== del2) {
+          result[i] = [del1, del2]; // conflict: this node is concurrently deleted more than once
+          i++;
+        }
+      }
+    }
+    return result;
+  }
+
+  // Whenever a node is deleted, and the source/target of a concurrent edge creation, there's a {DELETE,REQUIRE}-conflict.
+  getDeleteRequireConflicts(): Array<[NodeDeletion, EdgeCreation]> {
+    const result: Array<[NodeDeletion, EdgeCreation]> = [];
+    for (const del of this.deletions) {
+      for (const edgeCreation of this.edges) {
+        let edgeDeleted = false;
+        for (const edgeDel of del.edgeDeletions) {
+          if (edgeDel.getCreation() === edgeCreation) {
+            edgeDeleted = true;
+            break;
+          }
+        }
+        if (!edgeDeleted) {
+          result.push([del, edgeCreation]); // conflict: this node is deleted, and at the same time it is the source or target of an edge
+        }
+      }
+    }
+    return result;
+  }
+
+  // Whenever two edges with the same source and the same label are concurrently created
+  getEdgeCreationConflicts(): Array<[EdgeCreation, EdgeCreation]> {
+    const result: Array<[EdgeCreation, EdgeCreation]> = [];
+    for (const c1 of this.edges) {
+      for (const c2 of this.edges) {
+        if (c1 !== c2) {
+          if (c1.label === c2.label) {
+            result.push([c1, c2]); // conflict: same edge created concurrently
+          }
+        }
+      }
+    }
+    return result;
+  }
+
+  getConflicts(): Array<Conflict> {
+    return (this.getDeleteDeleteConflicts() as Array<Conflict>) // downcast :)
+      .concat(this.getDeleteRequireConflicts() as Array<Conflict>) // downcast
+      .concat(this.getEdgeCreationConflicts() as Array<Conflict>); // downcast
+  }
+}
+
+export class NodeDeletion implements MicroOperation {
+  // Dependency: The node being deleted.
+  readonly deletes: NodeCreation;
+
+  // Dependency: Deletion of a node depends on deletion of its incoming and outgoing edges.
+  readonly edgeDeletions: Array<EdgeDeletion>;
+
+  constructor(deletes: NodeCreation, edgeDeletions: Array<EdgeDeletion>) {
+    this.deletes = deletes;
+    this.edgeDeletions = edgeDeletions;
+
+    // Create inverse dependency
+    this.deletes.deletions.push(this);
+  }
+
+  getConflicts(): Array<Conflict> {
+    return [];
+  }
+}
+
+export interface EdgeOperation extends MicroOperation {
+  // Edge operations always form a tree (or sequence, if non-conflicting) where the path from root to leaf is as follows: create, update, update, ..., update, delete
+  // Therefore, any 'node' in this tree traces back to a 'root', the creation.
+  getCreation(): EdgeCreation;
+}
+
+type EdgeUpdateOrDeletion = EdgeUpdate | EdgeDeletion;
+
+abstract class EdgeCreationOrUpdate {
+  // Dependency: (new) target node
+  readonly target: NodeCreation;
+
+  // Inverse dependency: Next operation on the edge
+  nextOperations: Array<EdgeUpdate | EdgeDeletion>; // append-only
+
+  constructor(target: NodeCreation) {
+    this.target = target;
+
+    this.nextOperations = [];
+  }
+
+  // {UPDATE,UPDATE}, {UDPATE,DELETE}, {DELETE,DELETE} on two edges with the same source and label.
+  getUpdateOrDeleteConflicts(): Array<[EdgeUpdateOrDeletion, EdgeUpdateOrDeletion]> {
+    const result = new Array(this.nextOperations.length*(this.nextOperations.length-1));
+    let i = 0;
+    for (const n1 of this.nextOperations) {
+      for (const n2 of this.nextOperations) {
+        if (n1 !== n2) {
+          result[i] = [n1, n2];
+          i++;
+        }
+      }
+    }
+    return result;
+  }
+
+  getConflicts(): Array<Conflict> {
+    return this.getUpdateOrDeleteConflicts();
+  }
+
+  abstract getCreation(): EdgeCreation;
+}
+
+export class EdgeCreation extends EdgeCreationOrUpdate implements EdgeOperation {
+  // Dependency: source node
+  readonly source: NodeCreation;
+
+  readonly label: string;
+
+  constructor(source: NodeCreation, label: string, target: NodeCreation) {
+    super(target);
+
+    this.source = source;
+    this.label = label;
+
+    // Create inverse dependency
+    this.source.edges.push(this);
+  }
+
+  getCreation(): EdgeCreation {
+    return this;
+  }
+}
+
+export class EdgeUpdate extends EdgeCreationOrUpdate implements EdgeOperation {
+  readonly edge: EdgeCreationOrUpdate; // UPD-dependency
+
+  constructor(edge: EdgeCreationOrUpdate, newTarget: NodeCreation) {
+    super(newTarget);
+
+    this.edge = edge;
+
+    // Create inverse dependency
+    this.edge.nextOperations.push(this);
+  }
+
+  getCreation(): EdgeCreation {
+    return this.edge.getCreation();
+  }
+}
+
+export class EdgeDeletion implements EdgeOperation {
+  readonly edge: EdgeCreationOrUpdate; // UPD-dependency
+
+  constructor(edge: EdgeCreationOrUpdate) {
+    this.edge = edge;
+
+    // Create inverse dependency
+    this.edge.nextOperations.push(this);
+  }
+
+  getCreation(): EdgeCreation {
+    return this.edge.getCreation();
+  }
+
+  getConflicts(): Array<Conflict> {
+    return [];
+  }
+}
+
+
+// class GraphState {
+//   nodes: Map<NodeId, NodeCreation>;
+
+//   createNode(id: NodeId): NodeCreation {
+//     nodes.set()
+//   }
+
+//   createEdge(source: NodeCreation, label: string, target: NodeCreation) {
+
+//   }
+// }

+ 2 - 1
graph_state.test.ts

@@ -1,4 +1,5 @@
-import { UUID, GraphState } from "./graph_state"
+import {GraphState} from "./graph_state"
+import {NodeId, PrimitiveType, UUID, nodeIdsEqual} from "./types";
 
 function getUuidCallback() {
   let nextId = 0;

+ 64 - 69
graph_state.ts

@@ -1,77 +1,68 @@
-// NodeJS libraries
-import {inspect} from "util";
+import {NodeId, PrimitiveType, UUID, nodeIdsEqual} from "./types";
 
-type PrimitiveType = string | number | boolean;
+// import {
+//   Genesis,
+//   NodeCreation,
+//   NodeDeletion,
+//   EdgeCreation,
+//   EdgeUpdate,
+//   EdgeDeletion,
+// } from "./delta";
 
-// This class is here to distinguish UUIDs from ordinary strings
-export class UUID {
-  uuid: PrimitiveType;
-  
-  constructor(uuid: PrimitiveType) {
-    this.uuid = uuid;
-  }
-
-  [inspect.custom](depth: number, options: object) {
-    return "UUID{" + inspect(this.uuid, options) + "}"
-  }
-}
-
-type NodeId = PrimitiveType | UUID;
-
-function nodeIdsEqual(a: NodeId, b: NodeId) {
-  if (a === b) return true;
-  if (a instanceof UUID && b instanceof UUID) {
-    return a.uuid === b.uuid;
-  }
-}
 
 // In- and outgoing edges of a node.
 // This is the only place where edges are recorded.
 // Every edge corresponds to one entry in the source's 'outgoing', and one entry in the target's 'incoming'.
 class Node {
-  outgoing: Map<string, NodeId>;
+  // creation: NodeCreation;
+  outgoing: Map<string, NodeId>; // key: edge label, value: target id.
   incoming: Array<{label: string, srcId: NodeId}>;
 
-  constructor() {
+  constructor(/*creation: NodeCreation*/) {
+    //this.creation = creation;
     this.outgoing = new Map();
     this.incoming = [];
   }
 }
 
-// Helper class. Stores all nodes.
+// Helper class.
+// Abstracts away the fact that we use 2 maps for our nodes: one for our 'ordinary' nodes, and one for our value nodes.
 class NodeMap {
-  // ordinary nodes: they are created and deleted
-  nodes: Map<PrimitiveType, Node>;
+  // ordinary nodes: they are created and deleted, and identified by a UUID
+  ordinary: Map<PrimitiveType, Node>;
 
-  // value nodes: we pretend that they always already exist
+  // value nodes: we pretend that they always already exist, and are identified by a PrimitiveType
   values: Map<PrimitiveType, Node>;
 
   constructor() {
-    this.nodes = new Map();
+    this.ordinary = new Map();
     this.values = new Map();
   }
 
+  // get a node by its ID. if node doesn't exist, returns undefined.
   getOptional(id: NodeId): Node | undefined {
     if (id instanceof UUID) {
       // ordinary node: can only get it if it actually exists,
       // i.e., it has already been created and has not yet been deleted.
-      return this.nodes.get(id.uuid); // may return undefined
+      return this.ordinary.get(id.value); // may return undefined
     }
-
-    // value node: implicitly create it if it doesn't exist yet,
-    // pretending that it's "always already there"
-    const v = this.values.get(id);
-    if (v !== undefined) {
-      return v;
+    else {
+      // value node: implicitly create it if it doesn't exist yet,
+      // pretending that it's "always already there"
+      const valueNode = this.values.get(id);
+      if (valueNode !== undefined) {
+        return valueNode;
+      } else {
+        // auto-construct non-existing value node
+        const valueNode = new Node();
+        this.values.set(id, valueNode);
+        return valueNode;
+      }
     }
-    // auto-construct non-existing value node
-    const node = new Node();
-    this.values.set(id, node);
-    return node;
   }
 
   // same as get, but raises error when not found
-  get(id: NodeId): Node {
+  getOrThrow(id: NodeId): Node {
     const node = this.getOptional(id);
     if (node === undefined) {
       throw Error("node not found");
@@ -82,14 +73,14 @@ class NodeMap {
   // create a new ordinary node
   create(id: UUID): Node {
     const node = new Node();
-    this.nodes.set(id.uuid, node);
+    this.ordinary.set(id.value, node);
     return node;
   }
 
   // delete an ordinary node
   // Idempotent.
   delete(id: UUID) {
-    this.nodes.delete(id.uuid);
+    this.ordinary.delete(id.value);
   }
 }
 
@@ -99,6 +90,8 @@ export class GraphState {
   nodes: NodeMap;
   uuidCallback: UUIDCallbackType;
 
+  // creations: Map<PrimitiveType, NodeCreation>;
+
   constructor(uuidCallback: UUIDCallbackType) {
     this.nodes = new NodeMap();
     this.uuidCallback = uuidCallback;
@@ -108,25 +101,27 @@ export class GraphState {
   createNode(): UUID {
     const uuid = this.uuidCallback();
     this.nodes.create(uuid);
+    // const creation = new NodeCreation(uuid);
+    // this.creations.set(uuid.value, creation);
     return uuid;
   }
 
   // Delete node and delete all of its outgoing + incoming edges.
-  // Does nothing when given uuid does not exist.
+  // Does nothing when given uuid does not exist./
   // Idempotent.
   deleteNode(uuid: UUID) {
     const node = this.nodes.getOptional(uuid);
     if (node !== undefined) {
       // delete outgoing edges
-      for (const [label, targetId] of node.outgoing.entries()) {
-        const targetNode = this.nodes.get(targetId);
-        // remove edge from targetNode.incoming
-        const i = this.lookupIncoming(targetNode, label, uuid);
-        targetNode.incoming.splice(i, 1);
+      for (const [label, tgtId] of node.outgoing.entries()) {
+        const tgtNode = this.nodes.getOrThrow(tgtId);
+        // remove edge from tgtNode.incoming
+        const i = this.lookupIncoming(tgtNode, label, uuid);
+        tgtNode.incoming.splice(i, 1);
       }
       // delete incoming edges
       for (const {label, srcId} of node.incoming) {
-        const srcNode = this.nodes.get(srcId);
+        const srcNode = this.nodes.getOrThrow(srcId);
         // remove edge from srcNode.outgoing
         srcNode.outgoing.delete(label);
       }
@@ -137,26 +132,26 @@ export class GraphState {
 
   // Create or update a node's outgoing edge to point to a node
   // Idempotent.
-  setEdge(srcId: NodeId, label: string, targetId: NodeId) {
+  setEdge(srcId: NodeId, label: string, tgtId: NodeId) {
     // gotta remove the existing edge first, if it exists
     this.deleteEdge(srcId, label);
 
-    const srcNode = this.nodes.get(srcId);
-    srcNode.outgoing.set(label, targetId);
-    const targetNode = this.nodes.get(targetId);
-    targetNode.incoming.push({label, srcId});
+    const srcNode = this.nodes.getOrThrow(srcId);
+    srcNode.outgoing.set(label, tgtId);
+    const tgtNode = this.nodes.getOrThrow(tgtId);
+    tgtNode.incoming.push({label, srcId});
   }
 
   // Delete an edge.
   // Idempotent.
   deleteEdge(srcId: NodeId, label: string) {
-    const srcNode = this.nodes.get(srcId);
-    const existingTargetId = srcNode.outgoing.get(label);
-    if (existingTargetId !== undefined) {
-      // remove the respective entry in the existingTargetNode's 'incoming' array:
-      const existingTargetNode = this.nodes.get(existingTargetId);
-      const i = this.lookupIncoming(existingTargetNode, label, srcId);
-      existingTargetNode.incoming.splice(i, 1); // remove from array
+    const srcNode = this.nodes.getOrThrow(srcId);
+    const existingTgtId = srcNode.outgoing.get(label);
+    if (existingTgtId !== undefined) {
+      // remove the respective entry in the existingTgtNode's 'incoming' array:
+      const existingTgtNode = this.nodes.getOrThrow(existingTgtId);
+      const i = this.lookupIncoming(existingTgtNode, label, srcId);
+      existingTgtNode.incoming.splice(i, 1); // remove from array
     }
     srcNode.outgoing.delete(label);
   }
@@ -184,15 +179,15 @@ export class GraphState {
   getEdges(): Array<[NodeId, NodeId, string]> {
     const result: Array<[NodeId, NodeId, string]> = [];
     // get all outgoing edges of ordinary nodes
-    for (const [srcId, srcNode] of this.nodes.nodes.entries()) {
-      for (const [label, targetId] of srcNode.outgoing) {
-        result.push([new UUID(srcId), targetId, label]);
+    for (const [srcId, srcNode] of this.nodes.ordinary.entries()) {
+      for (const [label, tgtId] of srcNode.outgoing) {
+        result.push([new UUID(srcId), tgtId, label]);
       }
     }
     // get all outgoing edges of value nodes
     for (const [srcId, srcNode] of this.nodes.values.entries()) {
-      for (const [label, targetId] of srcNode.outgoing) {
-        result.push([srcId, targetId, label]);
+      for (const [label, tgtId] of srcNode.outgoing) {
+        result.push([srcId, tgtId, label]);
       }
     }
     return result;

+ 6 - 0
src/onion/mock_node_util.ts

@@ -0,0 +1,6 @@
+// When building the 'onion' library for browser, the 'util' NodeJS library will not be found.
+// A real polyfill would be overkill, so we just provide this 'mock':
+
+export const inspect = {
+  custom: Symbol(),
+};

+ 1 - 0
src/onion/something.ts

@@ -0,0 +1 @@
+export const x = 10;

+ 26 - 0
src/onion/types.ts

@@ -0,0 +1,26 @@
+import {inspect} from "util"; // NodeJS library 
+
+export type PrimitiveType = string | number | boolean;
+
+// This class is here to distinguish UUIDs from ordinary strings
+export class UUID {
+  value: PrimitiveType;
+  
+  constructor(value: PrimitiveType) {
+    this.value = value;
+  }
+
+  // pretty print to console
+  [inspect.custom](depth: number, options: object) {
+    return "UUID{" + inspect(this.value, options) + "}"
+  }
+}
+
+export type NodeId = PrimitiveType | UUID;
+
+export function nodeIdsEqual(a: NodeId, b: NodeId) {
+  if (a === b) return true;
+  if (a instanceof UUID && b instanceof UUID) {
+    return a.value === b.value;
+  }
+}

+ 6 - 1
tsconfig.json

@@ -2,6 +2,11 @@
   "compilerOptions": {
     "types": ["mocha", "node"],
     "target": "es6",
-    "strict": true
+    "noImplicitThis": true,
+    "strictBindCallApply": true,
+    "strictNullChecks": true,
+    "strictFunctionTypes": true,
+    "strictPropertyInitialization": true,
+    "alwaysStrict": true
   }
 }

+ 29 - 0
webpack.config.js

@@ -0,0 +1,29 @@
+const path = require('path');
+
+module.exports = {
+  entry: path.resolve(__dirname, 'src', 'frontend', 'index.js'),
+  devtool: 'inline-source-map',
+  module: {
+    rules: [
+      {
+        test: /\.tsx?$/,
+        use: 'ts-loader',
+        exclude: /node_modules/,
+      },
+    ],
+  },
+  resolve: {
+    extensions: ['.tsx', '.ts', '.js'],
+    fallback: {
+      util: path.resolve(__dirname, 'src', 'onion', 'mock_nodejs_util.ts'),
+    },
+  },
+  output: {
+    filename: 'bundle.js',
+    path: path.resolve(__dirname, 'dist'),
+  },
+  devServer: {
+    static: path.resolve(__dirname, 'dist'),
+    port: 9000,
+  },
+};