signalk-tides 1.3.0 → 1.5.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.
@@ -0,0 +1,12 @@
1
+ version: 2
2
+ updates:
3
+ - package-ecosystem: "npm"
4
+ directory: "/"
5
+ schedule:
6
+ interval: "weekly"
7
+ open-pull-requests-limit: 100
8
+ - package-ecosystem: "github-actions"
9
+ directory: "/"
10
+ schedule:
11
+ interval: "weekly"
12
+ open-pull-requests-limit: 100
@@ -0,0 +1,39 @@
1
+ name: CI
2
+
3
+ on:
4
+ push: {}
5
+
6
+ jobs:
7
+ lint:
8
+ name: Lint
9
+ runs-on: ubuntu-latest
10
+ steps:
11
+ - name: Checkout repository
12
+ uses: actions/checkout@v6
13
+
14
+ - name: Setup Node.js
15
+ uses: actions/setup-node@v6
16
+
17
+ - name: Install dependencies
18
+ run: npm install
19
+
20
+ - name: Run linter
21
+ run: npm run lint
22
+
23
+ build:
24
+ name: Build
25
+ runs-on: ubuntu-latest
26
+ env:
27
+ CI: true
28
+ steps:
29
+ - name: Checkout repository
30
+ uses: actions/checkout@v6
31
+
32
+ - name: Setup Node.js
33
+ uses: actions/setup-node@v6
34
+
35
+ - name: Install dependencies
36
+ run: npm install
37
+
38
+ - name: Run build
39
+ run: npm run build
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # signalk-tides
2
2
 
3
- A SignalK plugin that provides tidal predictions for the vessel's position from various online sources.
3
+ A SignalK plugin that provides tidal predictions for the vessel's position from various [sources](#sources).
4
4
 
5
5
  ## Installation
6
6
 
@@ -18,6 +18,8 @@ It publishes the following [tide data](https://signalk.org/specification/1.7.0/d
18
18
  * `environment.tide.timeHigh`
19
19
  * `environment.tide.heightLow`
20
20
  * `environment.tide.timeLow`
21
+ * `environment.tide.heightNow`
22
+ * `environment.tide.stationName`
21
23
 
22
24
  ### Tides resource
23
25
 
@@ -204,9 +206,18 @@ $ curl http://localhost:3000/signalk/v2/api/resources/tides
204
206
 
205
207
  ## Sources
206
208
 
207
- - [NOAA](https://tidesandcurrents.noaa.gov/web_services_info.html) (US only)
208
- - [WorldTides API](https://www.worldtides.info/) (requires an API key)
209
- - [StormGlass.io](https://stormglass.io/) (requires an API key)
209
+ - [Neaps](https://github.com/neaps/neaps)
210
+ - Offline, open source, runs locally
211
+ - Coverage: Regional — United States and territories (currently)
212
+ - [NOAA](https://tidesandcurrents.noaa.gov/web_services_info.html)
213
+ - Online (NOAA web services)
214
+ - Regional — United States and territories
215
+ - [WorldTides API](https://www.worldtides.info/)
216
+ - Online (requires API key, subject to API quotas)
217
+ - Coverage: Global - station and model coverage varies by location
218
+ - [StormGlass.io](https://stormglass.io/)
219
+ - Online (requires API key)
220
+ - Coverage: Global - aggregated marine data; tide availability varies
210
221
 
211
222
  ## License
212
223
 
package/dist/cache.js ADDED
@@ -0,0 +1,25 @@
1
+ import { join } from "path";
2
+ import { writeFile, readFile, mkdir } from "fs/promises";
3
+ export default class FileCache {
4
+ path;
5
+ constructor(path) {
6
+ this.path = path;
7
+ }
8
+ async get(key) {
9
+ try {
10
+ const content = await readFile(this.getKey(key), "utf-8");
11
+ return JSON.parse(content);
12
+ }
13
+ catch {
14
+ return undefined;
15
+ }
16
+ }
17
+ async set(key, value) {
18
+ // Ensure directory exists
19
+ await mkdir(this.path, { recursive: true });
20
+ return writeFile(this.getKey(key), JSON.stringify(value));
21
+ }
22
+ getKey(key) {
23
+ return join(this.path, `${key}.json`);
24
+ }
25
+ }
@@ -1,8 +1,5 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.approximateTideHeightAt = approximateTideHeightAt;
4
1
  /** Given a list of tide extremes, estimate the height at a specific time. */
5
- function approximateTideHeightAt(extremes, time) {
2
+ export function approximateTideHeightAt(extremes, time) {
6
3
  const sorted = extremes.slice().sort((a, b) => new Date(a.time).getTime() - new Date(b.time).getTime());
7
4
  const prev = sorted.filter(h => new Date(h.time) <= time).at(-1);
8
5
  const next = sorted.filter(h => new Date(h.time) >= time).at(0);
package/dist/index.js CHANGED
@@ -1,4 +1,3 @@
1
- "use strict";
2
1
  /*
3
2
  * Copyright 2017 Scott Bender <scott@scottbender.net> and Joachim Bakke
4
3
  * Copyright 2025 Brandon Keepers <brandon@opensoul.org>
@@ -14,27 +13,17 @@
14
13
  * See the License for the specific language governing permissions and
15
14
  * limitations under the License.
16
15
  */
17
- var __importDefault = (this && this.__importDefault) || function (mod) {
18
- return (mod && mod.__esModule) ? mod : { "default": mod };
19
- };
20
- const noaa_js_1 = __importDefault(require("./sources/noaa.js"));
21
- const stormglass_js_1 = __importDefault(require("./sources/stormglass.js"));
22
- const worldtides_js_1 = __importDefault(require("./sources/worldtides.js"));
23
- const calculations_js_1 = require("./calculations.js");
24
- module.exports = function (app) {
16
+ import { approximateTideHeightAt } from "./calculations.js";
17
+ import FileCache from "./cache.js";
18
+ import createSources from "./sources/index.js";
19
+ export default function (app) {
25
20
  // Interval to update tide data
26
21
  const defaultPeriod = 60; // 1 hour
27
22
  let unsubscribes = [];
28
- let lastForecast = null;
29
- const sources = [
30
- (0, noaa_js_1.default)(app),
31
- (0, stormglass_js_1.default)(app),
32
- (0, worldtides_js_1.default)(app),
33
- ];
23
+ const sources = createSources(app);
34
24
  const plugin = {
35
25
  id: "tides",
36
26
  name: "Tides",
37
- // @ts-expect-error: TODO[TS]: fix Plugin type upstream
38
27
  description: "Tidal predictions for the vessel's position from various online sources.",
39
28
  schema: () => ({
40
29
  title: "Tides API",
@@ -43,9 +32,9 @@ module.exports = function (app) {
43
32
  source: {
44
33
  title: "Data source",
45
34
  type: "string",
46
- "anyOf": sources.map(({ id, title }) => ({
35
+ anyOf: sources.map(({ id, title }) => ({
47
36
  const: id,
48
- title
37
+ title,
49
38
  })),
50
39
  default: sources[0].id,
51
40
  },
@@ -58,17 +47,21 @@ module.exports = function (app) {
58
47
  default: 60,
59
48
  minimum: 1,
60
49
  },
61
- }
50
+ },
62
51
  }),
52
+ start,
63
53
  stop() {
64
54
  unsubscribes.forEach((f) => f());
65
55
  unsubscribes = [];
66
- }
56
+ },
67
57
  };
68
- plugin.start = async function (props) {
58
+ async function start(props) {
69
59
  app.debug("Starting tides-api: " + JSON.stringify(props));
60
+ let lastForecast = null;
61
+ let lastPosition = null;
62
+ const cache = new FileCache(app.getDataDirPath());
70
63
  // Use the selected source, or the first one if not specified
71
- const source = sources.find(source => source.id === props.source) || sources[0];
64
+ const source = sources.find((source) => source.id === props.source) || sources[0];
72
65
  // Load the selected source
73
66
  const provider = await source.start(props);
74
67
  // Register the source as a resource provider
@@ -76,7 +69,9 @@ module.exports = function (app) {
76
69
  type: "tides",
77
70
  methods: {
78
71
  async listResources(query) {
79
- return provider(query);
72
+ if (!lastPosition)
73
+ throw new Error("No position available");
74
+ return provider({ position: lastPosition, ...query });
80
75
  },
81
76
  getResource() {
82
77
  throw new Error("Not implemented");
@@ -86,11 +81,11 @@ module.exports = function (app) {
86
81
  },
87
82
  deleteResource() {
88
83
  throw new Error("Not implemented");
89
- }
90
- }
84
+ },
85
+ },
91
86
  });
92
87
  app.subscriptionmanager.subscribe({
93
- context: "vessels." + app.selfId,
88
+ context: ("vessels." + app.selfId),
94
89
  subscribe: [
95
90
  {
96
91
  path: "navigation.position",
@@ -100,45 +95,24 @@ module.exports = function (app) {
100
95
  ],
101
96
  }, unsubscribes, (subscriptionError) => {
102
97
  app.error("Error:" + subscriptionError);
103
- }, (delta) => {
104
- // @ts-expect-error: TODO[TS]: fix Delta type upstream
105
- delta.updates.forEach(({ values }) => {
106
- // @ts-expect-error: TODO[TS]: fix Delta type upstream
107
- values.forEach(({ path }) => {
108
- if (path === "navigation.position") {
109
- updateForecast();
110
- }
111
- });
112
- });
113
- });
114
- async function updateForecast(now = new Date()) {
98
+ }, updatePosition);
99
+ async function updatePosition() {
100
+ lastPosition =
101
+ app.getSelfPath("navigation.position.value") || (await cache.get("position")) || null;
102
+ if (lastPosition) {
103
+ await cache.set("position", lastPosition);
104
+ await updateForecast();
105
+ }
106
+ }
107
+ async function updateForecast() {
108
+ if (!lastPosition) {
109
+ app.debug("No position available, cannot fetch tide data");
110
+ return;
111
+ }
115
112
  try {
116
- lastForecast = await provider();
117
- const nextTide = {};
118
- lastForecast.extremes.forEach(({ type, value, time }) => {
119
- // Get the first tide of this type after now
120
- if (!nextTide[type] && new Date(time) > now) {
121
- nextTide[type] = { time, value };
122
- }
123
- });
124
- const delta = {
125
- context: "vessels." + app.selfId,
126
- updates: [
127
- {
128
- timestamp: now.toISOString(),
129
- values: Object.entries(nextTide).flatMap(([type, { time, value }]) => {
130
- return [
131
- { path: `environment.tide.height${type}`, value: value },
132
- { path: `environment.tide.time${type}`, value: time },
133
- ];
134
- }),
135
- },
136
- ],
137
- };
138
- app.debug("Sending delta: " + JSON.stringify(delta));
139
- app.handleMessage(plugin.id, delta);
140
- updateHeightNow();
141
- app.setPluginStatus("Updated tide data");
113
+ lastForecast = await provider({ position: lastPosition });
114
+ app.setPluginStatus("Updated tide forecast from " + source.title);
115
+ updateTides();
142
116
  }
143
117
  catch (e) {
144
118
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -147,28 +121,47 @@ module.exports = function (app) {
147
121
  app.error(e);
148
122
  }
149
123
  }
150
- async function updateHeightNow(now = new Date()) {
124
+ async function updateTides(now = new Date()) {
151
125
  if (!lastForecast)
152
126
  return;
153
- // Combine heights and extremes to account for min/max being between height samples
154
- const height = (0, calculations_js_1.approximateTideHeightAt)(lastForecast.extremes, now);
155
- app.handleMessage(plugin.id, {
156
- context: "vessels." + app.selfId,
127
+ // Get the next two upcoming extremes
128
+ const nextTides = lastForecast.extremes
129
+ .filter(({ time }) => new Date(time) >= now)
130
+ .slice(0, 2);
131
+ const delta = {
132
+ context: ("vessels." + app.selfId),
157
133
  updates: [
158
134
  {
159
135
  timestamp: now.toISOString(),
160
- values: [{ path: "environment.tide.heightNow", value: height }]
161
- }
162
- ]
163
- });
136
+ values: [
137
+ {
138
+ path: "environment.tide.stationName",
139
+ value: lastForecast.station.name,
140
+ },
141
+ {
142
+ path: "environment.tide.heightNow",
143
+ value: approximateTideHeightAt(lastForecast.extremes, now),
144
+ },
145
+ ...nextTides.flatMap(({ type, time, value }) => {
146
+ return [
147
+ { path: `environment.tide.height${type}`, value },
148
+ { path: `environment.tide.time${type}`, value: time },
149
+ ];
150
+ }),
151
+ ],
152
+ },
153
+ ],
154
+ };
155
+ app.debug("Sending delta: " + JSON.stringify(delta));
156
+ app.handleMessage(plugin.id, delta);
164
157
  }
165
158
  // Perform initial update on startup after short delay to allow gnss position to be populated
166
- delay(4000).then(() => { updateForecast(); });
167
- // Update heightNow every minute
168
- setInterval(updateHeightNow, 60 * 1000);
169
- };
159
+ delay(4000).then(updatePosition);
160
+ // Update every minute
161
+ setInterval(updateTides, 60 * 1000);
162
+ }
170
163
  function delay(time) {
171
164
  return new Promise((resolve) => setTimeout(resolve, time));
172
165
  }
173
166
  return plugin;
174
- };
167
+ }
@@ -0,0 +1,7 @@
1
+ import neaps from "./neaps.js";
2
+ import noaa from "./noaa.js";
3
+ import stormglass from "./stormglass.js";
4
+ import worldtides from "./worldtides.js";
5
+ export default function createSources(app) {
6
+ return [neaps, noaa, stormglass, worldtides].map(s => s(app));
7
+ }
@@ -0,0 +1,39 @@
1
+ import { getExtremesPrediction } from "neaps";
2
+ import moment from 'moment';
3
+ export default function (app) {
4
+ return {
5
+ id: 'neaps',
6
+ title: 'Neaps',
7
+ properties: {},
8
+ start() {
9
+ app.debug("Using Neaps");
10
+ return async ({ position, date = moment().subtract(1, "days").toISOString() }) => {
11
+ const { station, extremes } = getExtremesPrediction({
12
+ ...position,
13
+ start: new Date(date),
14
+ end: moment(date).add(7, "days").toDate(),
15
+ labels: {
16
+ high: "High",
17
+ low: "Low"
18
+ }
19
+ });
20
+ return {
21
+ station: {
22
+ name: station.name,
23
+ position: {
24
+ latitude: station.latitude,
25
+ longitude: station.longitude,
26
+ },
27
+ },
28
+ extremes: extremes.map(({ time, level, label }) => {
29
+ return {
30
+ type: label,
31
+ value: level,
32
+ time: time.toISOString(),
33
+ };
34
+ }),
35
+ };
36
+ };
37
+ }
38
+ };
39
+ }
@@ -1,33 +1,32 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.default = default_1;
7
- const path_1 = __importDefault(require("path"));
8
- const promises_1 = __importDefault(require("fs/promises"));
9
- const geolib_1 = require("geolib");
10
- const moment_1 = __importDefault(require("moment"));
1
+ import path from "path";
2
+ import { unlink } from "fs/promises";
3
+ import { getDistance } from "geolib";
4
+ import moment from "moment";
5
+ import FileCache from "../cache.js";
11
6
  const stationsUrl = `https://api.tidesandcurrents.noaa.gov/mdapi/prod/webapi/stations.json?type=tidepredictions`;
12
- const dataGetterUrl = 'https://api.tidesandcurrents.noaa.gov/api/prod/datagetter';
13
- const datum = 'MLLW';
14
- function default_1(app) {
7
+ const dataGetterUrl = "https://api.tidesandcurrents.noaa.gov/api/prod/datagetter";
8
+ const datum = "MLLW";
9
+ export default function (app) {
15
10
  return {
16
- id: 'noaa',
17
- title: 'NOAA (US only)',
11
+ id: "noaa",
12
+ title: "NOAA (US only)",
18
13
  async start() {
19
- const stations = await StationList.load(app);
20
- return async ({ date } = {}) => {
21
- const position = app.getSelfPath("navigation.position.value");
22
- if (!position)
23
- throw new Error("no position");
14
+ const cache = new FileCache(path.join(app.getDataDirPath(), "noaa"));
15
+ const stations = await StationList.load(cache, app);
16
+ // Remove old cache file
17
+ // @ts-expect-error: configPath exists, just not part of the types
18
+ unlink(path.join(app.config.configPath, "noaastations.json")).catch(() => {
19
+ /* ignore */
20
+ });
21
+ return async (params) => {
22
+ const { position, date = moment().subtract(1, "days") } = params;
24
23
  const station = stations.closestTo(position);
25
24
  const endpoint = new URL(dataGetterUrl);
26
25
  endpoint.search = new URLSearchParams({
27
26
  product: "predictions",
28
27
  application: "signalk.org/node-server",
29
- begin_date: (0, moment_1.default)(date).format("YYYYMMDD"),
30
- end_date: (0, moment_1.default)(date).add(7, "days").format("YYYYMMDD"),
28
+ begin_date: moment(date).format("YYYYMMDD"),
29
+ end_date: moment(date).add(7, "days").format("YYYYMMDD"),
31
30
  datum,
32
31
  station: station.id,
33
32
  time_zone: "gmt",
@@ -40,7 +39,7 @@ function default_1(app) {
40
39
  const res = await fetch(endpoint.toString());
41
40
  if (!res.ok)
42
41
  throw new Error("Failed to fetch NOAA tides: " + res.statusText);
43
- const body = await res.json();
42
+ const body = (await res.json());
44
43
  app.debug("NOAA response: \n" + JSON.stringify(body, null, 2));
45
44
  if (body.error)
46
45
  throw new Error(body.error.message);
@@ -66,26 +65,22 @@ function default_1(app) {
66
65
  throw err;
67
66
  }
68
67
  };
69
- }
68
+ },
70
69
  };
71
70
  }
72
- ;
73
71
  class StationList extends Map {
74
- static async load(app) {
75
- const filename = path_1.default.join(app.config.configPath, "noaastations.json");
76
- let data;
77
- try {
78
- data = JSON.parse(await promises_1.default.readFile(filename, 'utf-8'));
79
- app.debug("NOAA: Loaded cached tide stations from " + filename);
72
+ static async load(cache, app) {
73
+ let data = (await cache.get("stations"));
74
+ if (data) {
75
+ app.debug("NOAA: Loaded cached tide stations");
80
76
  }
81
- catch (e) {
82
- app.debug(`NOAA: failed to load cached tide stations: ${e}`);
83
- app.debug('NOAA: Downloading tide stations');
77
+ else {
78
+ app.debug("NOAA: Downloading tide stations");
84
79
  const res = await fetch(stationsUrl);
85
80
  if (!res.ok)
86
81
  throw new Error(`Failed to download stations: ${res.statusText}`);
87
- data = await res.json();
88
- await promises_1.default.writeFile(filename, JSON.stringify(data));
82
+ data = (await res.json());
83
+ await cache.set("stations", data);
89
84
  }
90
85
  return new this(data.stations);
91
86
  }
@@ -98,8 +93,13 @@ class StationList extends Map {
98
93
  near(position, limit = 10) {
99
94
  const stationsWithDistances = Array.from(this.values()).map((station) => ({
100
95
  ...station,
101
- distance: (0, geolib_1.getDistance)(position, { latitude: station.lat, longitude: station.lng })
96
+ distance: getDistance(position, {
97
+ latitude: station.lat,
98
+ longitude: station.lng,
99
+ }),
102
100
  }));
103
- return stationsWithDistances.sort((a, b) => a.distance - b.distance).slice(0, limit);
101
+ return stationsWithDistances
102
+ .sort((a, b) => a.distance - b.distance)
103
+ .slice(0, limit);
104
104
  }
105
105
  }
@@ -1,11 +1,5 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.default = default_1;
7
- const moment_1 = __importDefault(require("moment"));
8
- function default_1(app) {
1
+ import moment from 'moment';
2
+ export default function (app) {
9
3
  return {
10
4
  id: 'stormglass',
11
5
  title: 'StormGlass.io',
@@ -17,18 +11,14 @@ function default_1(app) {
17
11
  },
18
12
  start({ stormglassApiKey } = {}) {
19
13
  app.debug("Using StormGlass.io API");
20
- return async (params = {}) => {
21
- const { date = (0, moment_1.default)().subtract(1, "days").toISOString() } = params;
14
+ return async ({ position, date = moment().subtract(1, "days").toISOString() }) => {
22
15
  const endPoint = new URL("https://api.stormglass.io/v2/tide/extremes/point");
23
- const position = app.getSelfPath("navigation.position.value");
24
- if (!position)
25
- throw new Error("no position");
26
16
  endPoint.search = new URLSearchParams({
27
- start: (0, moment_1.default)(date).format("YYYY-MM-DD"),
28
- end: (0, moment_1.default)(date).add(7, "days").format("YYYY-MM-DD"),
17
+ start: moment(date).format("YYYY-MM-DD"),
18
+ end: moment(date).add(7, "days").format("YYYY-MM-DD"),
29
19
  // datum: "CD",
30
- lat: position.latitude,
31
- lng: position.longitude,
20
+ lat: position.latitude.toString(),
21
+ lng: position.longitude.toString()
32
22
  }).toString();
33
23
  app.debug("Fetching StormGlass.io: " + endPoint.toString());
34
24
  const res = await fetch(endPoint, {
@@ -40,7 +30,7 @@ function default_1(app) {
40
30
  app.debug(JSON.stringify(data, null, 2));
41
31
  return {
42
32
  station: {
43
- name: data.meta.station.name,
33
+ name: `${data.meta.station.name} (${data.meta.station.source})`,
44
34
  position: {
45
35
  latitude: data.meta.station.lat,
46
36
  longitude: data.meta.station.lng,
@@ -1,11 +1,5 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.default = default_1;
7
- const moment_1 = __importDefault(require("moment"));
8
- function default_1(app) {
1
+ import moment from 'moment';
2
+ export default function (app) {
9
3
  return {
10
4
  id: 'WorldTides.info',
11
5
  title: 'worldtides.info',
@@ -17,20 +11,17 @@ function default_1(app) {
17
11
  },
18
12
  start({ worldtidesApiKey = '' } = {}) {
19
13
  app.debug("Using WorldTides API");
20
- return async (params = {}) => {
21
- const { date = (0, moment_1.default)().subtract(1, "days") } = params;
14
+ return async (params) => {
15
+ const { position, date = moment().subtract(1, "days") } = params;
22
16
  const endPoint = new URL("https://www.worldtides.info/api/v3");
23
- const position = app.getSelfPath("navigation.position.value");
24
- if (!position)
25
- throw new Error("no position");
26
17
  endPoint.search = new URLSearchParams({
27
- date: (0, moment_1.default)(date).format("YYYY-MM-DD"),
18
+ date: moment(date).format("YYYY-MM-DD"),
28
19
  datum: "CD",
29
20
  days: "7",
30
21
  extremes: "true",
31
22
  key: worldtidesApiKey,
32
- lat: position.latitude,
33
- lon: position.longitude,
23
+ lat: position.latitude.toString(),
24
+ lon: position.longitude.toString(),
34
25
  }).toString();
35
26
  app.debug("Fetching worldtides: " + endPoint.toString());
36
27
  const res = await fetch(endPoint);
@@ -42,7 +33,7 @@ function default_1(app) {
42
33
  throw new Error("worldtides data: " + data.error ? data.error : "none");
43
34
  return {
44
35
  station: {
45
- name: "WorldTides",
36
+ name: `${data.station} (${data.atlas})`,
46
37
  position: {
47
38
  latitude: data.responseLat,
48
39
  longitude: data.responseLon,
package/dist/types.js CHANGED
@@ -1,2 +1 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
1
+ export {};