signalk-edge-link 2.5.1 → 2.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/instance.js CHANGED
@@ -111,6 +111,7 @@ function createInstance(app, options, instanceId, pluginId, onStatusChange) {
111
111
  metaDiffFlushTimer: null,
112
112
  metaSnapshotTimers: [],
113
113
  lastMetaRequestAt: 0,
114
+ lastFullStatusRequestAt: 0,
114
115
  sourceRegistry: (0, source_replication_1.createSourceRegistry)(app)
115
116
  };
116
117
  const metricsApi = (0, metrics_1.default)();
@@ -384,6 +385,22 @@ function createInstance(app, options, instanceId, pluginId, onStatusChange) {
384
385
  app.debug(`[${instanceId}] META_REQUEST snapshot failed: ${msg}`);
385
386
  });
386
387
  }
388
+ /** Minimum gap between server-initiated full-status replays. Prevents a
389
+ * restarting or misconfigured server from flooding the link. */
390
+ const FULL_STATUS_REQUEST_RATE_LIMIT_MS = 10000;
391
+ /** Server asked for a full values snapshot (FULL_STATUS_REQUEST control
392
+ * packet). Replays the entire current Signal K tree to the server.
393
+ * Rate-limited to prevent replay floods across rapid server restarts. */
394
+ function handleFullStatusRequest() {
395
+ const now = Date.now();
396
+ if (now - state.lastFullStatusRequestAt < FULL_STATUS_REQUEST_RATE_LIMIT_MS) {
397
+ app.debug(`[${instanceId}] FULL_STATUS_REQUEST rate-limited, skipping`);
398
+ return;
399
+ }
400
+ state.lastFullStatusRequestAt = now;
401
+ app.debug(`[${instanceId}] FULL_STATUS_REQUEST received — replaying values snapshot`);
402
+ replayValuesSnapshot("full-status-request");
403
+ }
387
404
  async function sendSourceSnapshot() {
388
405
  if (state.stopped ||
389
406
  !state.readyToSend ||
@@ -1128,6 +1145,9 @@ function createInstance(app, options, instanceId, pluginId, onStatusChange) {
1128
1145
  if (typeof v2Pipeline.setMetaRequestHandler === "function") {
1129
1146
  v2Pipeline.setMetaRequestHandler(handleMetaRequest);
1130
1147
  }
1148
+ if (typeof v2Pipeline.setFullStatusRequestHandler === "function") {
1149
+ v2Pipeline.setFullStatusRequestHandler(handleFullStatusRequest);
1150
+ }
1131
1151
  v2Pipeline.startMetricsPublishing();
1132
1152
  if (options.congestionControl && options.congestionControl.enabled) {
1133
1153
  v2Pipeline.startCongestionControl();
@@ -1210,6 +1230,7 @@ function createInstance(app, options, instanceId, pluginId, onStatusChange) {
1210
1230
  Object.keys(state.configContentHashes).forEach((k) => delete state.configContentHashes[k]);
1211
1231
  state.excludedSentences = ["GSV"];
1212
1232
  state.lastPacketTime = 0;
1233
+ state.lastFullStatusRequestAt = 0;
1213
1234
  // Reset metrics
1214
1235
  resetMetrics();
1215
1236
  // Clear timers
package/lib/packet.js CHANGED
@@ -55,7 +55,9 @@ const PacketType = Object.freeze({
55
55
  HEARTBEAT: 0x04,
56
56
  HELLO: 0x05,
57
57
  METADATA: 0x06,
58
- META_REQUEST: 0x07
58
+ META_REQUEST: 0x07,
59
+ /** Server → client: request a full values snapshot replay. */
60
+ FULL_STATUS_REQUEST: 0x08
59
61
  });
60
62
  exports.PacketType = PacketType;
61
63
  /**
@@ -176,6 +178,14 @@ class PacketBuilder {
176
178
  buildMetaRequestPacket(options = {}) {
177
179
  return this._buildPacket(PacketType.META_REQUEST, Buffer.alloc(0), {}, options);
178
180
  }
181
+ /**
182
+ * Build a FULL_STATUS_REQUEST control packet (server → client).
183
+ * Payload is empty. Instructs the client to replay its full values snapshot
184
+ * so the server can rebuild state after a restart.
185
+ */
186
+ buildFullStatusRequestPacket(options = {}) {
187
+ return this._buildPacket(PacketType.FULL_STATUS_REQUEST, Buffer.alloc(0), {}, options);
188
+ }
179
189
  /**
180
190
  * Build an ACK packet
181
191
  * @param {number} ackedSequence - Sequence number being acknowledged
@@ -404,11 +414,13 @@ class PacketParser {
404
414
  payload = payloadData;
405
415
  }
406
416
  else {
407
- // HEARTBEAT and META_REQUEST packets carry a 0-byte payload with no CRC
408
- // — accept as-is. ACK / NAK / HELLO must include a 2-byte CRC16 trailer;
409
- // reject undersized payloads so forged control frames cannot slip
410
- // through unverified.
411
- if (type !== PacketType.HEARTBEAT && type !== PacketType.META_REQUEST) {
417
+ // HEARTBEAT, META_REQUEST, and FULL_STATUS_REQUEST carry a 0-byte
418
+ // payload with no CRC — accept as-is. ACK / NAK / HELLO must include
419
+ // a 2-byte CRC16 trailer; reject undersized payloads so forged control
420
+ // frames cannot slip through unverified.
421
+ if (type !== PacketType.HEARTBEAT &&
422
+ type !== PacketType.META_REQUEST &&
423
+ type !== PacketType.FULL_STATUS_REQUEST) {
412
424
  if (payload.length < 2) {
413
425
  throw new Error(`Control packet payload too short for CRC: ${payload.length} byte(s)`);
414
426
  }
@@ -504,7 +516,8 @@ function getTypeName(type) {
504
516
  [PacketType.HEARTBEAT]: "HEARTBEAT",
505
517
  [PacketType.HELLO]: "HELLO",
506
518
  [PacketType.METADATA]: "METADATA",
507
- [PacketType.META_REQUEST]: "META_REQUEST"
519
+ [PacketType.META_REQUEST]: "META_REQUEST",
520
+ [PacketType.FULL_STATUS_REQUEST]: "FULL_STATUS_REQUEST"
508
521
  };
509
522
  return names[type] || "UNKNOWN";
510
523
  }
@@ -106,6 +106,9 @@ function createPipelineV2Client(app, state, metricsApi) {
106
106
  // (META_REQUEST control packet). Wired up by instance.ts, which is the only
107
107
  // layer that knows how to build a snapshot from `app.signalk.retrieve()`.
108
108
  let metaRequestHandler = null;
109
+ // Callback fired when the server sends FULL_STATUS_REQUEST, asking the client
110
+ // to replay its complete current values snapshot.
111
+ let fullStatusRequestHandler = null;
109
112
  let metaEnvelopeSeq = 0;
110
113
  let sourceEnvelopeSeq = 0;
111
114
  // Seed all four meta bandwidth counters so downstream consumers (metrics
@@ -632,6 +635,9 @@ function createPipelineV2Client(app, state, metricsApi) {
632
635
  function setMetaRequestHandler(handler) {
633
636
  metaRequestHandler = handler;
634
637
  }
638
+ function setFullStatusRequestHandler(handler) {
639
+ fullStatusRequestHandler = handler;
640
+ }
635
641
  /**
636
642
  * Handle incoming ACK packet from server.
637
643
  * Removes acknowledged packets from the retransmit queue.
@@ -772,6 +778,21 @@ function createPipelineV2Client(app, state, metricsApi) {
772
778
  }
773
779
  }
774
780
  }
781
+ else if (parsed.type === packet_1.PacketType.FULL_STATUS_REQUEST) {
782
+ // Server asks us to replay our full values snapshot (e.g. after a
783
+ // server restart). Rate-limited in instance.ts to prevent abuse.
784
+ if (fullStatusRequestHandler) {
785
+ try {
786
+ Promise.resolve(fullStatusRequestHandler()).catch((err) => {
787
+ app.debug(`FULL_STATUS_REQUEST handler rejected: ${err instanceof Error ? err.message : String(err)}`);
788
+ });
789
+ }
790
+ catch (err) {
791
+ const errMsg = err instanceof Error ? err.message : String(err);
792
+ app.debug(`FULL_STATUS_REQUEST handler error: ${errMsg}`);
793
+ }
794
+ }
795
+ }
775
796
  // Ignore other packet types on client side
776
797
  }
777
798
  catch (err) {
@@ -1110,6 +1131,7 @@ function createPipelineV2Client(app, state, metricsApi) {
1110
1131
  sendMetadata,
1111
1132
  sendSourceSnapshot,
1112
1133
  setMetaRequestHandler,
1134
+ setFullStatusRequestHandler,
1113
1135
  getPacketBuilder,
1114
1136
  getRetransmitQueue,
1115
1137
  getMetricsPublisher,
@@ -178,6 +178,7 @@ function createPipelineV2Server(app, state, metricsApi) {
178
178
  rateLimitCount: 0,
179
179
  rateLimitWindowStart: Date.now(),
180
180
  metaRequested: false,
181
+ statusRequested: false,
181
182
  lastMetaEnvSeq: null,
182
183
  seenMetaChunkIdx: new Set(),
183
184
  lastSourceEnvSeq: null,
@@ -212,6 +213,8 @@ function createPipelineV2Server(app, state, metricsApi) {
212
213
  rateLimitWindowStart: Date.now(),
213
214
  // META_REQUEST bookkeeping
214
215
  metaRequested: false,
216
+ // FULL_STATUS_REQUEST bookkeeping
217
+ statusRequested: false,
215
218
  // Stale-envelope rejection for METADATA packets
216
219
  lastMetaEnvSeq: null,
217
220
  seenMetaChunkIdx: new Set(),
@@ -491,6 +494,7 @@ function createPipelineV2Server(app, state, metricsApi) {
491
494
  seenChunkIdx.clear();
492
495
  if (!isSource) {
493
496
  session.metaRequested = false;
497
+ session.statusRequested = false;
494
498
  }
495
499
  }
496
500
  if (lastEnvSeq !== null) {
@@ -610,6 +614,7 @@ function createPipelineV2Server(app, state, metricsApi) {
610
614
  session.lastMetaEnvSeq = null;
611
615
  session.seenMetaChunkIdx.clear();
612
616
  session.metaRequested = false;
617
+ session.statusRequested = false;
613
618
  }
614
619
  if (session.lastMetaEnvSeq !== null) {
615
620
  const distance = (envSeq - session.lastMetaEnvSeq) >>> 0;
@@ -697,6 +702,21 @@ function createPipelineV2Server(app, state, metricsApi) {
697
702
  recordError("general", `v2 META decode error: ${msg}`);
698
703
  }
699
704
  }
705
+ /**
706
+ * Build and send a FULL_STATUS_REQUEST (0x08) control packet to a client.
707
+ * Instructs the client to replay its complete current values snapshot so the
708
+ * server can rebuild state immediately after a restart.
709
+ */
710
+ async function _sendFullStatusRequest(session, secretKey) {
711
+ try {
712
+ const packet = packetBuilder.buildFullStatusRequestPacket({ secretKey });
713
+ await _sendUDP(packet, { address: session.address, port: session.port });
714
+ app.debug(`[v2-server] FULL_STATUS_REQUEST sent to ${session.key}`);
715
+ }
716
+ catch (err) {
717
+ throw err;
718
+ }
719
+ }
700
720
  /**
701
721
  * Build and send a META_REQUEST (0x07) control packet to a client.
702
722
  * Instructs the client to emit a fresh metadata snapshot — used on first
@@ -819,6 +839,15 @@ function createPipelineV2Server(app, state, metricsApi) {
819
839
  app.debug(`[v2-server] META_REQUEST send failed: ${err instanceof Error ? err.message : String(err)}`);
820
840
  });
821
841
  }
842
+ // If the operator enabled full-status-on-restart, also request a values
843
+ // snapshot. Capped at one per session so rapid HELLOs (e.g. NAT churn)
844
+ // don't create repeated replay bursts.
845
+ if (session && !session.statusRequested && state.options?.requestFullStatusOnRestart) {
846
+ session.statusRequested = true;
847
+ _sendFullStatusRequest(session, secretKey).catch((err) => {
848
+ app.debug(`[v2-server] FULL_STATUS_REQUEST send failed: ${err instanceof Error ? err.message : String(err)}`);
849
+ });
850
+ }
822
851
  return;
823
852
  }
824
853
  if (parsed.type === packet_1.PacketType.METADATA) {
@@ -892,6 +921,14 @@ function createPipelineV2Server(app, state, metricsApi) {
892
921
  if (session) {
893
922
  session.hasReceivedData = true;
894
923
  }
924
+ // On first DATA from a new session, request full-status replay if enabled.
925
+ // This covers the case where the client sends data before its next HELLO.
926
+ if (session && !session.statusRequested && state.options?.requestFullStatusOnRestart) {
927
+ session.statusRequested = true;
928
+ _sendFullStatusRequest(session, secretKey).catch((err) => {
929
+ app.debug(`[v2-server] FULL_STATUS_REQUEST (data trigger) send failed: ${err instanceof Error ? err.message : String(err)}`);
930
+ });
931
+ }
895
932
  const dataSeq = parsed.sequence >>> 0;
896
933
  if (session) {
897
934
  if (seqResult.resynced || session.lossBaseSeq === null) {
@@ -15,7 +15,7 @@
15
15
  * results to `RJSFSchema` at call sites.
16
16
  */
17
17
  Object.defineProperty(exports, "__esModule", { value: true });
18
- exports.alertThresholdsProperty = exports.skipOwnDataProperty = exports.enableNotificationsProperty = exports.bondingProperty = exports.congestionControlProperty = exports.serverReliabilityProperty = exports.clientReliabilityProperty = exports.clientTransportProperties = exports.v1ClientPingProperties = exports.commonConnectionProperties = void 0;
18
+ exports.alertThresholdsProperty = exports.skipOwnDataProperty = exports.enableNotificationsProperty = exports.bondingProperty = exports.congestionControlProperty = exports.serverReliabilityProperty = exports.requestFullStatusOnRestartProperty = exports.clientReliabilityProperty = exports.clientTransportProperties = exports.v1ClientPingProperties = exports.commonConnectionProperties = void 0;
19
19
  exports.buildConnectionItemSchema = buildConnectionItemSchema;
20
20
  exports.buildWebappConnectionSchema = buildWebappConnectionSchema;
21
21
  const crypto_constants_1 = require("./crypto-constants");
@@ -246,6 +246,12 @@ exports.clientReliabilityProperty = {
246
246
  }
247
247
  };
248
248
  // ── v2/v3 reliability (server pipeline — ACK/NAK timing) ──────────────────────
249
+ exports.requestFullStatusOnRestartProperty = {
250
+ type: "boolean",
251
+ title: "Request Full Status on Server Start (v2/v3 only)",
252
+ description: "When enabled, the server sends a request to each client on first contact asking it to replay its complete current values snapshot. This rebuilds the server's state immediately after a restart instead of waiting for incremental deltas to arrive.",
253
+ default: false
254
+ };
249
255
  exports.serverReliabilityProperty = {
250
256
  type: "object",
251
257
  title: "Reliability Settings (v2/v3 only)",
@@ -511,6 +517,7 @@ function buildConnectionItemSchema() {
511
517
  {
512
518
  properties: {
513
519
  serverType: { enum: ["server"] },
520
+ requestFullStatusOnRestart: exports.requestFullStatusOnRestartProperty,
514
521
  reliability: exports.serverReliabilityProperty
515
522
  }
516
523
  },
@@ -567,6 +574,7 @@ function buildWebappConnectionSchema(isClient, protocolVersion) {
567
574
  }
568
575
  }
569
576
  else if (isReliableProtocol) {
577
+ props.requestFullStatusOnRestart = exports.requestFullStatusOnRestartProperty;
570
578
  props.reliability = exports.serverReliabilityProperty;
571
579
  }
572
580
  return { type: "object", required, properties: props };
package/package.json CHANGED
@@ -1,165 +1,165 @@
1
- {
2
- "name": "signalk-edge-link",
3
- "version": "2.5.1",
4
- "description": "SignalK Edge Link. Secure UDP link for data exchange.",
5
- "main": "lib/index.js",
6
- "files": [
7
- "lib/",
8
- "public/"
9
- ],
10
- "keywords": [
11
- "signalk-node-server-plugin",
12
- "signalk-category-network",
13
- "signalk-webapp",
14
- "signalk-category-utility",
15
- "signalk-plugin-configurator"
16
- ],
17
- "signalk": {
18
- "appIcon": "./icons/icon-72x72.png",
19
- "displayName": "Edge Link Configuration"
20
- },
21
- "signalk-plugin-enabled-by-default": false,
22
- "scripts": {
23
- "clean:lib": "node -e \"const fs=require('fs');if(fs.existsSync('lib'))fs.rmSync('lib',{recursive:true,force:true});\"",
24
- "clean:public": "node -e \"const fs=require('fs');if(fs.existsSync('public'))fs.rmSync('public',{recursive:true,force:true});\"",
25
- "build": "npm run build:ts && npm run build:web",
26
- "build:web": "npm run clean:public && webpack --mode production",
27
- "build:ts": "npm run clean:lib && tsc",
28
- "check:ts": "tsc --noEmit",
29
- "check:release-docs": "node scripts/check-release-truth.js",
30
- "dev": "webpack --mode development --watch",
31
- "test": "jest --runInBand",
32
- "test:v2": "jest __tests__/v2/",
33
- "test:integration": "jest test/integration/",
34
- "test:watch": "jest --watch",
35
- "test:coverage": "jest --coverage",
36
- "lint": "eslint .",
37
- "lint:fix": "eslint . --fix",
38
- "format": "prettier --write \"**/*.{js,ts,json,md}\"",
39
- "migrate:config": "node lib/scripts/migrate-config.js",
40
- "cli": "node lib/bin/edge-link-cli.js",
41
- "prepare": "husky"
42
- },
43
- "dependencies": {
44
- "@msgpack/msgpack": "^3.0.0",
45
- "@rjsf/core": "^5.18.4",
46
- "@rjsf/utils": "^5.18.4",
47
- "@rjsf/validator-ajv8": "^5.18.4",
48
- "ping-monitor": "^0.8.2"
49
- },
50
- "devDependencies": {
51
- "@babel/core": "^7.22.0",
52
- "@babel/preset-env": "^7.22.0",
53
- "@babel/preset-react": "^7.22.0",
54
- "@testing-library/jest-dom": "^5.17.0",
55
- "@testing-library/react": "^12.1.5",
56
- "@types/node": "^25.3.5",
57
- "@types/react": "^16.14.0",
58
- "@types/react-dom": "^16.9.0",
59
- "babel-loader": "^9.1.2",
60
- "copy-webpack-plugin": "^14.0.0",
61
- "css-loader": "^6.8.1",
62
- "eslint": "^8.57.1",
63
- "eslint-plugin-react": "^7.37.5",
64
- "html-webpack-plugin": "^5.5.3",
65
- "husky": "^9.1.7",
66
- "jest": "^29.7.0",
67
- "jest-environment-jsdom": "^30.3.0",
68
- "lint-staged": "^15.4.3",
69
- "mini-css-extract-plugin": "^2.7.6",
70
- "prettier": "^3.6.2",
71
- "react": "^16.13.1",
72
- "react-dom": "^16.13.1",
73
- "react-test-renderer": "^16.14.0",
74
- "style-loader": "^3.3.3",
75
- "ts-jest": "^29.4.6",
76
- "ts-loader": "^9.5.4",
77
- "typescript": "^5.9.3",
78
- "webpack": "^5.102.1",
79
- "webpack-cli": "^5.1.4"
80
- },
81
- "engines": {
82
- "node": ">=16"
83
- },
84
- "author": "Karl-Erik Gustafsson",
85
- "repository": "https://github.com/KEGustafsson/signalk-edge-link",
86
- "homepage": "https://github.com/KEGustafsson/signalk-edge-link#readme",
87
- "bugs": {
88
- "url": "https://github.com/KEGustafsson/signalk-edge-link/issues"
89
- },
90
- "license": "MIT",
91
- "jest": {
92
- "testEnvironment": "node",
93
- "coverageDirectory": "coverage",
94
- "collectCoverageFrom": [
95
- "lib/**/*.js",
96
- "!lib/webapp/**",
97
- "!lib/components/**",
98
- "!lib/utils/**"
99
- ],
100
- "testMatch": [
101
- "**/__tests__/**/*.js",
102
- "**/*.test.js",
103
- "**/*.spec.js"
104
- ],
105
- "transform": {
106
- "^.+\\.js$": [
107
- "babel-jest",
108
- {
109
- "presets": [
110
- [
111
- "@babel/preset-env",
112
- {
113
- "targets": {
114
- "node": "current"
115
- }
116
- }
117
- ]
118
- ]
119
- }
120
- ],
121
- ".+\\.tsx$": [
122
- "ts-jest",
123
- {
124
- "tsconfig": "tsconfig.webapp.json",
125
- "diagnostics": false
126
- }
127
- ],
128
- "^.+\\.ts$": "ts-jest"
129
- },
130
- "moduleFileExtensions": [
131
- "ts",
132
- "tsx",
133
- "js",
134
- "json",
135
- "node"
136
- ],
137
- "testPathIgnorePatterns": [
138
- "/node_modules/",
139
- "/public/"
140
- ],
141
- "coverageThreshold": {
142
- "global": {
143
- "branches": 60,
144
- "functions": 65,
145
- "lines": 65,
146
- "statements": 65
147
- }
148
- }
149
- },
150
- "bin": {
151
- "edge-link-cli": "lib/bin/edge-link-cli.js"
152
- },
153
- "lint-staged": {
154
- "*.js": [
155
- "prettier --write",
156
- "eslint --fix"
157
- ],
158
- "*.ts": [
159
- "prettier --write"
160
- ],
161
- "*.{json,md}": [
162
- "prettier --write"
163
- ]
164
- }
165
- }
1
+ {
2
+ "name": "signalk-edge-link",
3
+ "version": "2.6.0",
4
+ "description": "SignalK Edge Link. Secure UDP link for data exchange.",
5
+ "main": "lib/index.js",
6
+ "files": [
7
+ "lib/",
8
+ "public/"
9
+ ],
10
+ "keywords": [
11
+ "signalk-node-server-plugin",
12
+ "signalk-category-network",
13
+ "signalk-webapp",
14
+ "signalk-category-utility",
15
+ "signalk-plugin-configurator"
16
+ ],
17
+ "signalk": {
18
+ "appIcon": "./icons/icon-72x72.png",
19
+ "displayName": "Edge Link Configuration"
20
+ },
21
+ "signalk-plugin-enabled-by-default": false,
22
+ "scripts": {
23
+ "clean:lib": "node -e \"const fs=require('fs');if(fs.existsSync('lib'))fs.rmSync('lib',{recursive:true,force:true});\"",
24
+ "clean:public": "node -e \"const fs=require('fs');if(fs.existsSync('public'))fs.rmSync('public',{recursive:true,force:true});\"",
25
+ "build": "npm run build:ts && npm run build:web",
26
+ "build:web": "npm run clean:public && webpack --mode production",
27
+ "build:ts": "npm run clean:lib && tsc",
28
+ "check:ts": "tsc --noEmit",
29
+ "check:release-docs": "node scripts/check-release-truth.js",
30
+ "dev": "webpack --mode development --watch",
31
+ "test": "jest --runInBand",
32
+ "test:v2": "jest __tests__/v2/",
33
+ "test:integration": "jest test/integration/",
34
+ "test:watch": "jest --watch",
35
+ "test:coverage": "jest --coverage",
36
+ "lint": "eslint .",
37
+ "lint:fix": "eslint . --fix",
38
+ "format": "prettier --write \"**/*.{js,ts,json,md}\"",
39
+ "migrate:config": "node lib/scripts/migrate-config.js",
40
+ "cli": "node lib/bin/edge-link-cli.js",
41
+ "prepare": "husky"
42
+ },
43
+ "dependencies": {
44
+ "@msgpack/msgpack": "^3.0.0",
45
+ "@rjsf/core": "^5.18.4",
46
+ "@rjsf/utils": "^5.18.4",
47
+ "@rjsf/validator-ajv8": "^5.18.4",
48
+ "ping-monitor": "^0.8.2"
49
+ },
50
+ "devDependencies": {
51
+ "@babel/core": "^7.22.0",
52
+ "@babel/preset-env": "^7.22.0",
53
+ "@babel/preset-react": "^7.22.0",
54
+ "@testing-library/jest-dom": "^5.17.0",
55
+ "@testing-library/react": "^12.1.5",
56
+ "@types/node": "^25.3.5",
57
+ "@types/react": "^16.14.0",
58
+ "@types/react-dom": "^16.9.0",
59
+ "babel-loader": "^9.1.2",
60
+ "copy-webpack-plugin": "^14.0.0",
61
+ "css-loader": "^6.8.1",
62
+ "eslint": "^8.57.1",
63
+ "eslint-plugin-react": "^7.37.5",
64
+ "html-webpack-plugin": "^5.5.3",
65
+ "husky": "^9.1.7",
66
+ "jest": "^29.7.0",
67
+ "jest-environment-jsdom": "^30.3.0",
68
+ "lint-staged": "^15.4.3",
69
+ "mini-css-extract-plugin": "^2.7.6",
70
+ "prettier": "^3.6.2",
71
+ "react": "^16.13.1",
72
+ "react-dom": "^16.13.1",
73
+ "react-test-renderer": "^16.14.0",
74
+ "style-loader": "^3.3.3",
75
+ "ts-jest": "^29.4.6",
76
+ "ts-loader": "^9.5.4",
77
+ "typescript": "^5.9.3",
78
+ "webpack": "^5.102.1",
79
+ "webpack-cli": "^5.1.4"
80
+ },
81
+ "engines": {
82
+ "node": ">=16"
83
+ },
84
+ "author": "Karl-Erik Gustafsson",
85
+ "repository": "https://github.com/KEGustafsson/signalk-edge-link",
86
+ "homepage": "https://github.com/KEGustafsson/signalk-edge-link#readme",
87
+ "bugs": {
88
+ "url": "https://github.com/KEGustafsson/signalk-edge-link/issues"
89
+ },
90
+ "license": "MIT",
91
+ "jest": {
92
+ "testEnvironment": "node",
93
+ "coverageDirectory": "coverage",
94
+ "collectCoverageFrom": [
95
+ "lib/**/*.js",
96
+ "!lib/webapp/**",
97
+ "!lib/components/**",
98
+ "!lib/utils/**"
99
+ ],
100
+ "testMatch": [
101
+ "**/__tests__/**/*.js",
102
+ "**/*.test.js",
103
+ "**/*.spec.js"
104
+ ],
105
+ "transform": {
106
+ "^.+\\.js$": [
107
+ "babel-jest",
108
+ {
109
+ "presets": [
110
+ [
111
+ "@babel/preset-env",
112
+ {
113
+ "targets": {
114
+ "node": "current"
115
+ }
116
+ }
117
+ ]
118
+ ]
119
+ }
120
+ ],
121
+ ".+\\.tsx$": [
122
+ "ts-jest",
123
+ {
124
+ "tsconfig": "tsconfig.webapp.json",
125
+ "diagnostics": false
126
+ }
127
+ ],
128
+ "^.+\\.ts$": "ts-jest"
129
+ },
130
+ "moduleFileExtensions": [
131
+ "ts",
132
+ "tsx",
133
+ "js",
134
+ "json",
135
+ "node"
136
+ ],
137
+ "testPathIgnorePatterns": [
138
+ "/node_modules/",
139
+ "/public/"
140
+ ],
141
+ "coverageThreshold": {
142
+ "global": {
143
+ "branches": 60,
144
+ "functions": 65,
145
+ "lines": 65,
146
+ "statements": 65
147
+ }
148
+ }
149
+ },
150
+ "bin": {
151
+ "edge-link-cli": "lib/bin/edge-link-cli.js"
152
+ },
153
+ "lint-staged": {
154
+ "*.js": [
155
+ "prettier --write",
156
+ "eslint --fix"
157
+ ],
158
+ "*.ts": [
159
+ "prettier --write"
160
+ ],
161
+ "*.{json,md}": [
162
+ "prettier --write"
163
+ ]
164
+ }
165
+ }