transit-departures-widget 2.5.5 → 2.7.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/README.md CHANGED
@@ -60,6 +60,32 @@ The following transit agencies use `transit-departures-widget` on their websites
60
60
  - [Mountain View Community Shuttle](https://mvcommunityshuttle.com)
61
61
  - [MVgo](https://mvgo.org/)
62
62
 
63
+ ## Quick Start
64
+
65
+ ### CLI
66
+
67
+ ```bash
68
+ npm install transit-departures-widget -g
69
+ transit-departures-widget --configPath ./config-sample.json
70
+ ```
71
+
72
+ Outputs are written to `html/<agency_key>/`:
73
+
74
+ - `index.html` (full page unless `noHead: true`)
75
+ - `data/routes.json`
76
+ - `data/stops.json`
77
+ - `css/`, `js/` (when `noHead` is false)
78
+
79
+ ### Programmatic
80
+
81
+ ```ts
82
+ import transitDeparturesWidget from 'transit-departures-widget'
83
+ import config from './config.json' assert { type: 'json' }
84
+
85
+ await transitDeparturesWidget(config)
86
+ // outputs to html/<agency_key>/ by default
87
+ ```
88
+
63
89
  ## Command Line Usage
64
90
 
65
91
  The `transit-departures-widget` command-line utility will download the GTFS file specified in `config.js` and then build the transit departures widget and save the HTML, CSS and JS in `html/:agency_key`.
@@ -92,20 +118,22 @@ Copy `config-sample.json` to `config.json` and then add your projects configurat
92
118
 
93
119
  cp config-sample.json config.json
94
120
 
95
- | option | type | description |
96
- | --------------------------------------------------- | ------- | --------------------------------------------------------------------------------------------------------------------------- |
97
- | [`agency`](#agency) | object | Information about the GTFS and GTFS-RT to be used. |
98
- | [`beautify`](#beautify) | boolean | Whether or not to beautify the HTML output. |
99
- | [`endDate`](#enddate) | string | A date in YYYYMMDD format to use to filter calendar.txt service. Optional, defaults to using all service in specified GTFS. |
100
- | [`includeCoordinates`](#includecoordinates) | boolean | Whether or not to include stop coordinates in JSON output. |
101
- | [`locale`](#locale) | string | The 2-letter code of the language to use for the interface. |
102
- | [`noHead`](#nohead) | boolean | Whether or not to skip the header and footer of the HTML document. |
103
- | [`refreshIntervalSeconds`](#refreshIntervalSeconds) | integer | How often the widget should refresh departure data in seconds. Optional, defaults to 20 seconds. |
104
- | [`skipImport`](#skipimport) | boolean | Whether or not to skip importing GTFS data into SQLite. |
105
- | [`sqlitePath`](#sqlitepath) | string | A path to an SQLite database. Optional, defaults to using an in-memory database. |
106
- | [`startDate`](#startdate) | string | A date in YYYYMMDD format to use to filter calendar.txt service. Optional, defaults to using all service in specified GTFS. |
107
- | [`templatePath`](#templatepath) | string | Path to custom pug template for rendering widget. |
108
- | [`timeFormat`](#timeFormat) | string | The format (12hour or 24hour) for the "as of" display. |
121
+ | option | type | default | notes |
122
+ | --------------------------------------------------- | ------- | --------- | ---------------------------------------------------------------------------------------------------------------------------- |
123
+ | [`agency`](#agency) | object | — | Required: GTFS static (url or path) and GTFS-RT trip updates URL. |
124
+ | [`assetPath`](#assetpath) | string | `''` | Prefix for assets in HTML; set when hosting assets elsewhere. |
125
+ | [`beautify`](#beautify) | boolean | `false` | Pretty-print HTML output. |
126
+ | [`endDate`](#enddate) | string | all svc | YYYYMMDD calendar filter upper bound. |
127
+ | [`includeCoordinates`](#includecoordinates) | boolean | `false` | Include stop lat/lon in `stops.json`. |
128
+ | [`locale`](#locale) | string | `en` | UI language code. |
129
+ | [`noHead`](#nohead) | boolean | `false` | If true, omit `<html>/<head>/<body>`; only widget markup is output. |
130
+ | [`outputPath`](#outputpath) | string | `./html/<agency_key>` | Where to write generated files. |
131
+ | [`refreshIntervalSeconds`](#refreshIntervalSeconds) | integer | `20` | Autorefresh interval on the widget page. |
132
+ | [`skipImport`](#skipimport) | boolean | `false` | Skip GTFS import if DB already populated (`sqlitePath` recommended). |
133
+ | [`sqlitePath`](#sqlitepath) | string | in-memory | Path to SQLite DB file; enables reusing imports across runs. |
134
+ | [`startDate`](#startdate) | string | all svc | YYYYMMDD calendar filter lower bound. |
135
+ | [`templatePath`](#templatepath) | string | built-in | Custom templates folder (expects `widget.pug` and `widget_full.pug`). |
136
+ | [`timeFormat`](#timeFormat) | string | `12hour` | `12hour` or `24hour` time display. |
109
137
 
110
138
  ### agency
111
139
 
@@ -195,6 +223,24 @@ Copy `config-sample.json` to `config.json` and then add your projects configurat
195
223
  "noHead": false
196
224
  ```
197
225
 
226
+ If `noHead` is `true`, you’ll embed the widget into an existing HTML page. See the examples below for including the generated assets.
227
+
228
+ ### assetPath
229
+
230
+ {String} Prefix to use when linking to generated assets (`css`, `js`). Useful if you host assets on a CDN or a different path from the HTML file.
231
+
232
+ ```
233
+ "assetPath": "/static/widget/"
234
+ ```
235
+
236
+ ### outputPath
237
+
238
+ {String} Path where generated files are written. Defaults to `./html/<agency_key>`.
239
+
240
+ ```
241
+ "outputPath": "/var/www/widget-output"
242
+ ```
243
+
198
244
  ### refreshIntervalSeconds
199
245
 
200
246
  {Integer} How often the widget should refresh departure data in seconds. Optional, defaults to 20 seconds.
@@ -259,6 +305,32 @@ By default, `transit-departures-widget` will look for a `config.json` file in th
259
305
 
260
306
  Once running, you can view the HTML in your browser at [localhost:3000](http://localhost:3000)
261
307
 
308
+ ## Embedding examples
309
+
310
+ ### Default (with head/footer)
311
+
312
+ If `noHead` is `false` (default), `index.html` is a complete HTML page with linked assets in `css/` and `js/` under the output directory. You can host that folder as-is (e.g., serve `html/<agency_key>/` from your web server root).
313
+
314
+ ### Headless embed (`noHead: true`)
315
+
316
+ When `noHead` is `true`, you get only the widget markup. Include the generated CSS/JS and mount it in your page:
317
+
318
+ ```html
319
+ <!doctype html>
320
+ <html>
321
+ <head>
322
+ <link rel="stylesheet" href="/path/to/css/transit-departures-widget-styles.css" />
323
+ </head>
324
+ <body>
325
+ <div id="tdw-app"></div>
326
+ <script src="/path/to/js/transit-departures-widget.js"></script>
327
+ <script>
328
+ // Transit Departures Widget initializes itself on load using data/routes.json and data/stops.json
329
+ </script>
330
+ </body>
331
+ </html>
332
+ ```
333
+
262
334
  ## Notes
263
335
 
264
336
  `transit-departures-widget` uses the [`node-gtfs`](https://github.com/blinktaginc/node-gtfs) library to handle importing and querying GTFS data.
package/dist/app/index.js CHANGED
@@ -1,39 +1,46 @@
1
1
  // src/app/index.ts
2
+ import { dirname as dirname2, join as join3 } from "path";
3
+ import { fileURLToPath as fileURLToPath2 } from "url";
2
4
  import { readFileSync } from "fs";
3
- import { join as join3 } from "path";
4
5
  import yargs from "yargs";
5
- import { getRoutes as getRoutes2, importGtfs, openDb as openDb2 } from "gtfs";
6
+ import { hideBin } from "yargs/helpers";
7
+ import { openDb as openDb2, importGtfs } from "gtfs";
8
+ import express from "express";
6
9
  import { clone, omit } from "lodash-es";
7
10
  import untildify2 from "untildify";
8
- import express from "express";
9
- import logger from "morgan";
10
-
11
- // src/lib/utils.ts
12
- import { join as join2 } from "path";
13
- import { openDb, getDirections, getRoutes, getStops, getTrips } from "gtfs";
14
- import { groupBy, last, maxBy, size, sortBy, uniqBy } from "lodash-es";
15
11
 
16
12
  // src/lib/file-utils.ts
17
13
  import { dirname, join, resolve } from "path";
18
14
  import { fileURLToPath } from "url";
19
- import { access, cp, mkdir, readdir, readFile, rm } from "fs/promises";
15
+ import {
16
+ access,
17
+ cp,
18
+ copyFile,
19
+ mkdir,
20
+ readdir,
21
+ readFile,
22
+ rm
23
+ } from "fs/promises";
20
24
  import beautify from "js-beautify";
21
25
  import pug from "pug";
22
26
  import untildify from "untildify";
23
- function getPathToViewsFolder(config2) {
24
- if (config2.templatePath) {
25
- return untildify(config2.templatePath);
26
- }
27
+ function getPathToThisModuleFolder() {
27
28
  const __dirname = dirname(fileURLToPath(import.meta.url));
28
- let viewsFolderPath;
29
+ let distFolderPath;
29
30
  if (__dirname.endsWith("/dist/bin") || __dirname.endsWith("/dist/app")) {
30
- viewsFolderPath = resolve(__dirname, "../../views/widget");
31
+ distFolderPath = resolve(__dirname, "../../");
31
32
  } else if (__dirname.endsWith("/dist")) {
32
- viewsFolderPath = resolve(__dirname, "../views/widget");
33
+ distFolderPath = resolve(__dirname, "../");
33
34
  } else {
34
- viewsFolderPath = resolve(__dirname, "views/widget");
35
+ distFolderPath = resolve(__dirname, "../../");
35
36
  }
36
- return viewsFolderPath;
37
+ return distFolderPath;
38
+ }
39
+ function getPathToViewsFolder(config2) {
40
+ if (config2.templatePath) {
41
+ return untildify(config2.templatePath);
42
+ }
43
+ return join(getPathToThisModuleFolder(), "views/default");
37
44
  }
38
45
  function getPathToTemplateFile(templateFileName, config2) {
39
46
  const fullTemplateFileName = config2.noHead !== true ? `${templateFileName}_full.pug` : `${templateFileName}.pug`;
@@ -49,15 +56,45 @@ async function renderFile(templateFileName, templateVars, config2) {
49
56
  }
50
57
 
51
58
  // src/lib/utils.ts
59
+ import { openDb, getDirections, getRoutes, getStops, getTrips } from "gtfs";
60
+ import { groupBy, last, maxBy, size, sortBy, uniqBy } from "lodash-es";
52
61
  import sqlString from "sqlstring-sqlite";
53
62
  import toposort from "toposort";
54
- import { I18n } from "i18n";
55
63
 
56
- // src/lib/log-utils.ts
64
+ // src/lib/logging/log.ts
57
65
  import { clearLine, cursorTo } from "readline";
58
66
  import { noop } from "lodash-es";
59
67
  import * as colors from "yoctocolors";
60
- function logWarning(config2) {
68
+ var formatWarning = (text) => {
69
+ const warningMessage = `${colors.underline("Warning")}: ${text}`;
70
+ return colors.yellow(warningMessage);
71
+ };
72
+ var formatError = (error) => {
73
+ const messageText = error instanceof Error ? error.message : error;
74
+ const errorMessage = `${colors.underline("Error")}: ${messageText.replace(
75
+ "Error: ",
76
+ ""
77
+ )}`;
78
+ return colors.red(errorMessage);
79
+ };
80
+ var logInfo = (config2) => {
81
+ if (config2.verbose === false) {
82
+ return noop;
83
+ }
84
+ if (config2.logFunction) {
85
+ return config2.logFunction;
86
+ }
87
+ return (text, overwrite) => {
88
+ if (overwrite === true && process.stdout.isTTY) {
89
+ clearLine(process.stdout, 0);
90
+ cursorTo(process.stdout, 0);
91
+ } else {
92
+ process.stdout.write("\n");
93
+ }
94
+ process.stdout.write(text);
95
+ };
96
+ };
97
+ var logWarn = (config2) => {
61
98
  if (config2.logFunction) {
62
99
  return config2.logFunction;
63
100
  }
@@ -66,10 +103,59 @@ function logWarning(config2) {
66
103
  ${formatWarning(text)}
67
104
  `);
68
105
  };
106
+ };
107
+ var logError = (config2) => {
108
+ if (config2.logFunction) {
109
+ return config2.logFunction;
110
+ }
111
+ return (text) => {
112
+ process.stdout.write(`
113
+ ${formatError(text)}
114
+ `);
115
+ };
116
+ };
117
+ function createLogger(config2) {
118
+ return {
119
+ info: logInfo(config2),
120
+ warn: logWarn(config2),
121
+ error: logError(config2)
122
+ };
69
123
  }
70
- function formatWarning(text) {
71
- const warningMessage = `${colors.underline("Warning")}: ${text}`;
72
- return colors.yellow(warningMessage);
124
+
125
+ // src/lib/logging/messages.ts
126
+ var messages = {
127
+ noActiveCalendarsGlobal: "No active calendars found for the configured date range - returning empty routes and stops",
128
+ noActiveCalendarsForRoute: (routeId) => `route_id ${routeId} has no active calendars in range - skipping directions`,
129
+ noActiveCalendarsForDirection: (routeId, directionId) => `route_id ${routeId} direction ${directionId} has no active calendars in range - skipping stops`,
130
+ routeHasNoDirections: (routeId) => `route_id ${routeId} has no directions - skipping`,
131
+ stopNotFound: (routeId, directionId, stopId) => `stop_id ${stopId} for route ${routeId} direction ${directionId} not found - dropping`
132
+ };
133
+
134
+ // src/lib/config/defaults.ts
135
+ import { join as join2 } from "path";
136
+ import { I18n } from "i18n";
137
+ function setDefaultConfig(initialConfig) {
138
+ const defaults = {
139
+ beautify: false,
140
+ noHead: false,
141
+ refreshIntervalSeconds: 20,
142
+ skipImport: false,
143
+ timeFormat: "12hour",
144
+ includeCoordinates: false,
145
+ overwriteExistingFiles: true,
146
+ verbose: true
147
+ };
148
+ const config2 = Object.assign(defaults, initialConfig);
149
+ const viewsFolderPath = getPathToViewsFolder(config2);
150
+ const i18n = new I18n({
151
+ directory: join2(viewsFolderPath, "locales"),
152
+ defaultLocale: config2.locale,
153
+ updateFiles: false
154
+ });
155
+ const configWithI18n = Object.assign(config2, {
156
+ __: i18n.__
157
+ });
158
+ return configWithI18n;
73
159
  }
74
160
 
75
161
  // src/lib/utils.ts
@@ -102,12 +188,20 @@ function formatRouteName(route) {
102
188
  return routeName;
103
189
  }
104
190
  function getDirectionsForRoute(route, config2) {
191
+ const logger = createLogger(config2);
105
192
  const db = openDb(config2);
106
193
  const directions = getDirections({ route_id: route.route_id }, [
107
194
  "direction_id",
108
195
  "direction"
109
- ]);
196
+ ]).filter((direction) => direction.direction_id !== void 0).map((direction) => ({
197
+ direction_id: direction.direction_id,
198
+ direction: direction.direction
199
+ }));
110
200
  const calendars = getCalendarsForDateRange(config2);
201
+ if (calendars.length === 0) {
202
+ logger.warn(messages.noActiveCalendarsForRoute(route.route_id));
203
+ return [];
204
+ }
111
205
  if (directions.length === 0) {
112
206
  const headsigns = db.prepare(
113
207
  `SELECT direction_id, trip_headsign, count(*) AS count FROM trips WHERE route_id = ? AND service_id IN (${calendars.map((calendar) => `'${calendar.service_id}'`).join(", ")}) GROUP BY direction_id, trip_headsign`
@@ -148,11 +242,24 @@ function sortStopIdsBySequence(stoptimes) {
148
242
  Object.values(stoptimesGroupedByTrip),
149
243
  (stoptimes2) => size(stoptimes2)
150
244
  );
245
+ if (!longestTripStoptimes) {
246
+ return [];
247
+ }
151
248
  return longestTripStoptimes.map((stoptime) => stoptime.stop_id);
152
249
  }
153
- function getStopsForDirection(route, direction, config2) {
250
+ function getStopsForDirection(route, direction, config2, stopCache) {
251
+ const logger = createLogger(config2);
154
252
  const db = openDb(config2);
155
253
  const calendars = getCalendarsForDateRange(config2);
254
+ if (calendars.length === 0) {
255
+ logger.warn(
256
+ messages.noActiveCalendarsForDirection(
257
+ route.route_id,
258
+ direction.direction_id
259
+ )
260
+ );
261
+ return [];
262
+ }
156
263
  const whereClause = formatWhereClauses({
157
264
  direction_id: direction.direction_id,
158
265
  route_id: route.route_id,
@@ -172,14 +279,34 @@ function getStopsForDirection(route, direction, config2) {
172
279
  []
173
280
  );
174
281
  deduplicatedStopIds.pop();
175
- const stopFields = ["stop_id", "stop_name", "stop_code", "parent_station"];
282
+ const stopFields = [
283
+ "stop_id",
284
+ "stop_name",
285
+ "stop_code",
286
+ "parent_station"
287
+ ];
176
288
  if (config2.includeCoordinates) {
177
289
  stopFields.push("stop_lat", "stop_lon");
178
290
  }
179
- const stops = getStops({ stop_id: deduplicatedStopIds }, stopFields);
180
- return deduplicatedStopIds.map(
181
- (stopId) => stops.find((stop) => stop.stop_id === stopId)
182
- );
291
+ const missingStopIds = stopCache ? deduplicatedStopIds.filter((stopId) => !stopCache.has(stopId)) : deduplicatedStopIds;
292
+ const fetchedStops = missingStopIds.length ? getStops(
293
+ { stop_id: missingStopIds },
294
+ stopFields
295
+ ) : [];
296
+ if (stopCache) {
297
+ for (const stop of fetchedStops) {
298
+ stopCache.set(stop.stop_id, stop);
299
+ }
300
+ }
301
+ return deduplicatedStopIds.map((stopId) => {
302
+ const stop = stopCache?.get(stopId) ?? fetchedStops.find((candidate) => candidate.stop_id === stopId);
303
+ if (!stop) {
304
+ logger.warn(
305
+ messages.stopNotFound(route.route_id, direction.direction_id, stopId)
306
+ );
307
+ }
308
+ return stop;
309
+ }).filter(Boolean);
183
310
  }
184
311
  function generateTransitDeparturesWidgetHtml(config2) {
185
312
  const templateVars = {
@@ -189,23 +316,37 @@ function generateTransitDeparturesWidgetHtml(config2) {
189
316
  return renderFile("widget", templateVars, config2);
190
317
  }
191
318
  function generateTransitDeparturesWidgetJson(config2) {
319
+ const logger = createLogger(config2);
320
+ const calendars = getCalendarsForDateRange(config2);
321
+ if (calendars.length === 0) {
322
+ logger.warn(messages.noActiveCalendarsGlobal);
323
+ return { routes: [], stops: [] };
324
+ }
192
325
  const routes = getRoutes();
193
326
  const stops = [];
194
327
  const filteredRoutes = [];
195
- const calendars = getCalendarsForDateRange(config2);
328
+ const stopCache = /* @__PURE__ */ new Map();
196
329
  for (const route of routes) {
197
- route.route_full_name = formatRouteName(route);
198
- const directions = getDirectionsForRoute(route, config2);
330
+ const routeWithFullName = {
331
+ ...route,
332
+ route_full_name: formatRouteName(route)
333
+ };
334
+ const directions = getDirectionsForRoute(routeWithFullName, config2);
199
335
  if (directions.length === 0) {
200
- logWarning(config2)(
201
- `route_id ${route.route_id} has no directions - skipping`
202
- );
336
+ logger.warn(messages.routeHasNoDirections(route.route_id));
203
337
  continue;
204
338
  }
205
- for (const direction of directions) {
206
- const directionStops = getStopsForDirection(route, direction, config2);
339
+ const directionsWithData = directions.map((direction) => {
340
+ const directionStops = getStopsForDirection(
341
+ routeWithFullName,
342
+ direction,
343
+ config2,
344
+ stopCache
345
+ );
346
+ if (directionStops.length === 0) {
347
+ return null;
348
+ }
207
349
  stops.push(...directionStops);
208
- direction.stopIds = directionStops.map((stop) => stop?.stop_id);
209
350
  const trips = getTrips(
210
351
  {
211
352
  route_id: route.route_id,
@@ -216,69 +357,82 @@ function generateTransitDeparturesWidgetJson(config2) {
216
357
  },
217
358
  ["trip_id"]
218
359
  );
219
- direction.tripIds = trips.map((trip) => trip.trip_id);
360
+ return {
361
+ ...direction,
362
+ stopIds: directionStops.map((stop) => stop.stop_id),
363
+ tripIds: trips.map((trip) => trip.trip_id)
364
+ };
365
+ }).filter(Boolean);
366
+ if (directionsWithData.length === 0) {
367
+ continue;
220
368
  }
221
- route.directions = directions;
222
- filteredRoutes.push(route);
369
+ filteredRoutes.push({
370
+ ...routeWithFullName,
371
+ directions: directionsWithData
372
+ });
223
373
  }
224
- const sortedRoutes = sortBy(
225
- sortBy(filteredRoutes, (route) => route.route_short_name?.toLowerCase()),
226
- (route) => Number.parseInt(route.route_short_name, 10)
227
- );
228
- const parentStationIds = new Set(stops.map((stop) => stop?.parent_station));
374
+ const sortedRoutes = [...filteredRoutes].sort((a, b) => {
375
+ const aShort = a.route_short_name ?? "";
376
+ const bShort = b.route_short_name ?? "";
377
+ const aNum = Number.parseInt(aShort, 10);
378
+ const bNum = Number.parseInt(bShort, 10);
379
+ if (!Number.isNaN(aNum) && !Number.isNaN(bNum) && aNum !== bNum) {
380
+ return aNum - bNum;
381
+ }
382
+ if (Number.isNaN(aNum) && !Number.isNaN(bNum)) {
383
+ return 1;
384
+ }
385
+ if (!Number.isNaN(aNum) && Number.isNaN(bNum)) {
386
+ return -1;
387
+ }
388
+ return aShort.localeCompare(bShort, void 0, {
389
+ numeric: true,
390
+ sensitivity: "base"
391
+ });
392
+ });
393
+ const parentStationIds = new Set(stops.map((stop) => stop.parent_station));
229
394
  const parentStationStops = getStops(
230
395
  { stop_id: Array.from(parentStationIds) },
231
396
  ["stop_id", "stop_name", "stop_code", "parent_station"]
232
397
  );
233
398
  stops.push(
234
- ...parentStationStops.map((stop) => {
235
- stop.is_parent_station = true;
236
- return stop;
237
- })
399
+ ...parentStationStops.map((stop) => ({
400
+ ...stop,
401
+ is_parent_station: true
402
+ }))
238
403
  );
239
404
  const sortedStops = sortBy(uniqBy(stops, "stop_id"), "stop_name");
240
405
  return {
241
- routes: removeNulls(sortedRoutes),
242
- stops: removeNulls(sortedStops)
406
+ routes: arrayOfArrays(removeNulls(sortedRoutes)),
407
+ stops: arrayOfArrays(removeNulls(sortedStops))
243
408
  };
244
409
  }
245
410
  function removeNulls(data) {
246
411
  if (Array.isArray(data)) {
247
412
  return data.map(removeNulls).filter((item) => item !== null && item !== void 0);
248
- } else if (typeof data === "object" && data !== null) {
249
- return Object.entries(data).reduce((acc, [key, value]) => {
250
- const cleanedValue = removeNulls(value);
251
- if (cleanedValue !== null && cleanedValue !== void 0) {
252
- acc[key] = cleanedValue;
253
- }
254
- return acc;
255
- }, {});
413
+ } else if (data !== null && typeof data === "object" && Object.getPrototypeOf(data) === Object.prototype) {
414
+ return Object.entries(data).reduce(
415
+ (acc, [key, value]) => {
416
+ const cleanedValue = removeNulls(value);
417
+ if (cleanedValue !== null && cleanedValue !== void 0) {
418
+ acc[key] = cleanedValue;
419
+ }
420
+ return acc;
421
+ },
422
+ {}
423
+ );
256
424
  } else {
257
425
  return data;
258
426
  }
259
427
  }
260
- function setDefaultConfig(initialConfig) {
261
- const defaults = {
262
- beautify: false,
263
- noHead: false,
264
- refreshIntervalSeconds: 20,
265
- skipImport: false,
266
- timeFormat: "12hour",
267
- includeCoordinates: false,
268
- overwriteExistingFiles: true,
269
- verbose: true
428
+ function arrayOfArrays(array) {
429
+ if (array.length === 0) {
430
+ return { fields: [], rows: [] };
431
+ }
432
+ return {
433
+ fields: Object.keys(array[0]),
434
+ rows: array.map((item) => Object.values(item))
270
435
  };
271
- const config2 = Object.assign(defaults, initialConfig);
272
- const viewsFolderPath = getPathToViewsFolder(config2);
273
- const i18n = new I18n({
274
- directory: join2(viewsFolderPath, "locales"),
275
- defaultLocale: config2.locale,
276
- updateFiles: false
277
- });
278
- const configWithI18n = Object.assign(config2, {
279
- __: i18n.__
280
- });
281
- return configWithI18n;
282
436
  }
283
437
  function formatWhereClause(key, value) {
284
438
  if (Array.isArray(value)) {
@@ -304,7 +458,7 @@ function formatWhereClauses(query) {
304
458
  }
305
459
 
306
460
  // src/app/index.ts
307
- var argv = yargs(process.argv).option("c", {
461
+ var argv = yargs(hideBin(process.argv)).option("c", {
308
462
  alias: "configPath",
309
463
  describe: "Path to config file",
310
464
  default: "./config.json",
@@ -319,27 +473,45 @@ config.assetPath = "/";
319
473
  config.logFunction = console.log;
320
474
  try {
321
475
  openDb2(config);
322
- getRoutes2();
323
- } catch (error) {
324
- console.log("Importing GTFS");
325
- try {
326
- const gtfsImportConfig = {
327
- ...clone(omit(config, "agency")),
328
- agencies: [
329
- {
330
- agency_key: config.agency.agency_key,
331
- path: config.agency.gtfs_static_path,
332
- url: config.agency.gtfs_static_url
333
- }
334
- ]
335
- };
336
- await importGtfs(gtfsImportConfig);
337
- } catch (error2) {
338
- console.error(
339
- `Unable to open sqlite database "${config.sqlitePath}" defined as \`sqlitePath\` config.json. Ensure the parent directory exists and import GTFS before running this app.`
476
+ const gtfsPath = config.agency.gtfs_static_path;
477
+ const gtfsUrl = config.agency.gtfs_static_url;
478
+ if (!gtfsPath && !gtfsUrl) {
479
+ throw new Error(
480
+ "Missing GTFS source. Set `agency.gtfs_static_path` or `agency.gtfs_static_url` in config.json."
340
481
  );
341
- throw error2;
342
482
  }
483
+ const agencyImportConfig = gtfsPath ? { path: gtfsPath } : { url: gtfsUrl };
484
+ const gtfsImportConfig = {
485
+ ...clone(omit(config, "agency")),
486
+ agencies: [agencyImportConfig]
487
+ };
488
+ await importGtfs(gtfsImportConfig);
489
+ } catch (error) {
490
+ console.error(
491
+ `Unable to open sqlite database "${config.sqlitePath}" defined as \`sqlitePath\` config.json. Ensure the parent directory exists and run gtfs-to-html to import GTFS before running this app.`
492
+ );
493
+ throw error;
494
+ }
495
+ app.set("views", getPathToViewsFolder(config));
496
+ app.set("view engine", "pug");
497
+ app.use((req, res, next) => {
498
+ console.log(`${req.method} ${req.url}`);
499
+ next();
500
+ });
501
+ var staticAssetPath = config.templatePath === void 0 ? getPathToViewsFolder(config) : untildify2(config.templatePath);
502
+ app.use(express.static(staticAssetPath));
503
+ var frontendLibraryPaths = [
504
+ { route: "/js", package: "pbf", subPath: "dist" },
505
+ { route: "/js", package: "gtfs-realtime-pbf-js-module", subPath: "" },
506
+ { route: "/js", package: "accessible-autocomplete", subPath: "" },
507
+ { route: "/css", package: "accessible-autocomplete", subPath: "" }
508
+ ];
509
+ var resolvePackagePath = (packageName, subPath) => {
510
+ const packagePath = dirname2(fileURLToPath2(import.meta.resolve(packageName)));
511
+ return subPath ? join3(packagePath, subPath) : packagePath;
512
+ };
513
+ for (const { route, package: pkg, subPath } of frontendLibraryPaths) {
514
+ app.use(route, express.static(resolvePackagePath(pkg, subPath)));
343
515
  }
344
516
  app.get("/", async (request, response, next) => {
345
517
  try {
@@ -365,11 +537,6 @@ app.get("/data/stops.json", async (request, response, next) => {
365
537
  next(error);
366
538
  }
367
539
  });
368
- app.set("views", getPathToViewsFolder(config));
369
- app.set("view engine", "pug");
370
- app.use(logger("dev"));
371
- var staticAssetPath = config.templatePath === void 0 ? getPathToViewsFolder(config) : untildify2(config.templatePath);
372
- app.use(express.static(staticAssetPath));
373
540
  app.use((req, res) => {
374
541
  res.status(404).send("Not Found");
375
542
  });