transit-departures-widget 2.4.2 → 2.4.4

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/dist/app/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/app/index.ts
2
- import path2 from "node:path";
2
+ import path from "node:path";
3
3
  import { fileURLToPath as fileURLToPath3 } from "node:url";
4
4
  import { readFileSync } from "node:fs";
5
5
  import yargs from "yargs";
@@ -9,14 +9,13 @@ import logger from "morgan";
9
9
 
10
10
  // src/lib/utils.ts
11
11
  import { fileURLToPath as fileURLToPath2 } from "url";
12
- import { dirname, join } from "path";
12
+ import { dirname as dirname2, join as join2 } from "path";
13
13
  import { openDb, getDirections, getRoutes, getStops, getTrips } from "gtfs";
14
14
  import { groupBy, last, maxBy, size, sortBy, uniqBy } from "lodash-es";
15
15
 
16
16
  // src/lib/file-utils.ts
17
- import path from "path";
17
+ import { dirname, join, resolve } from "node:path";
18
18
  import { fileURLToPath } from "node:url";
19
- import copydir from "copy-dir";
20
19
  import beautify from "js-beautify";
21
20
  import pug from "pug";
22
21
  import untildify from "untildify";
@@ -26,12 +25,9 @@ function getTemplatePath(templateFileName, config2) {
26
25
  fullTemplateFileName += "_full";
27
26
  }
28
27
  if (config2.templatePath !== void 0) {
29
- return path.join(
30
- untildify(config2.templatePath),
31
- `${fullTemplateFileName}.pug`
32
- );
28
+ return join(untildify(config2.templatePath), `${fullTemplateFileName}.pug`);
33
29
  }
34
- return path.join(
30
+ return join(
35
31
  fileURLToPath(import.meta.url),
36
32
  "../../../views/widget",
37
33
  `${fullTemplateFileName}.pug`
@@ -149,12 +145,7 @@ function getStopsForDirection(route, direction, config2) {
149
145
  []
150
146
  );
151
147
  deduplicatedStopIds.pop();
152
- const stopFields = [
153
- "stop_id",
154
- "stop_name",
155
- "stop_code",
156
- "parent_station"
157
- ];
148
+ const stopFields = ["stop_id", "stop_name", "stop_code", "parent_station"];
158
149
  if (config2.includeCoordinates) {
159
150
  stopFields.push("stop_lat", "stop_lon");
160
151
  }
@@ -165,7 +156,7 @@ function getStopsForDirection(route, direction, config2) {
165
156
  }
166
157
  function generateTransitDeparturesWidgetHtml(config2) {
167
158
  i18n.configure({
168
- directory: join(dirname(fileURLToPath2(import.meta.url)), "../../locales"),
159
+ directory: join2(dirname2(fileURLToPath2(import.meta.url)), "../../locales"),
169
160
  defaultLocale: config2.locale,
170
161
  updateFiles: false
171
162
  });
@@ -184,7 +175,7 @@ function generateTransitDeparturesWidgetJson(config2) {
184
175
  route.route_full_name = formatRouteName(route);
185
176
  const directions = getDirectionsForRoute(route, config2);
186
177
  if (directions.length === 0) {
187
- config2.logWarning(
178
+ logWarning(config2)(
188
179
  `route_id ${route.route_id} has no directions - skipping`
189
180
  );
190
181
  continue;
@@ -251,7 +242,9 @@ function setDefaultConfig(initialConfig) {
251
242
  refreshIntervalSeconds: 20,
252
243
  skipImport: false,
253
244
  timeFormat: "12hour",
254
- includeCoordinates: false
245
+ includeCoordinates: false,
246
+ overwriteExistingFiles: true,
247
+ verbose: true
255
248
  };
256
249
  return Object.assign(defaults, initialConfig);
257
250
  }
@@ -292,17 +285,13 @@ var selectedConfig = JSON.parse(readFileSync(configPath).toString());
292
285
  var config = setDefaultConfig(selectedConfig);
293
286
  config.noHead = false;
294
287
  config.assetPath = "/";
295
- config.log = console.log;
296
- config.logWarning = console.warn;
297
- config.logError = console.error;
288
+ config.logFunction = console.log;
298
289
  try {
299
290
  openDb2(config);
300
291
  } catch (error) {
301
- if (error?.code === "SQLITE_CANTOPEN") {
302
- config.logError(
303
- `Unable to open sqlite database "${config.sqlitePath}" defined as \`sqlitePath\` config.json. Ensure the parent directory exists or remove \`sqlitePath\` from config.json.`
304
- );
305
- }
292
+ console.error(
293
+ `Unable to open sqlite database "${config.sqlitePath}" defined as \`sqlitePath\` config.json. Ensure the parent directory exists and import GTFS before running this app.`
294
+ );
306
295
  throw error;
307
296
  }
308
297
  router.get("/", async (request, response, next) => {
@@ -329,11 +318,11 @@ router.get("/data/stops.json", async (request, response, next) => {
329
318
  next(error);
330
319
  }
331
320
  });
332
- app.set("views", path2.join(fileURLToPath3(import.meta.url), "../../../views"));
321
+ app.set("views", path.join(fileURLToPath3(import.meta.url), "../../../views"));
333
322
  app.set("view engine", "pug");
334
323
  app.use(logger("dev"));
335
324
  app.use(
336
- express.static(path2.join(fileURLToPath3(import.meta.url), "../../../public"))
325
+ express.static(path.join(fileURLToPath3(import.meta.url), "../../../public"))
337
326
  );
338
327
  app.use("/", router);
339
328
  app.set("port", process.env.PORT || 3e3);
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/app/index.ts","../../src/lib/utils.ts","../../src/lib/file-utils.ts"],"sourcesContent":["import path from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport { readFileSync } from 'node:fs'\nimport yargs from 'yargs'\nimport { openDb } from 'gtfs'\n\nimport express, { Router } from 'express'\nimport logger from 'morgan'\n\nimport {\n setDefaultConfig,\n generateTransitDeparturesWidgetHtml,\n generateTransitDeparturesWidgetJson,\n} from '../lib/utils.ts'\n\nconst argv = yargs(process.argv)\n .option('c', {\n alias: 'configPath',\n describe: 'Path to config file',\n default: './config.json',\n type: 'string',\n })\n .parseSync()\n\nconst app = express()\nconst router = Router()\n\nconst configPath = (argv.configPath ||\n new URL('../../config.json', import.meta.url)) as string\n\nconst selectedConfig = JSON.parse(readFileSync(configPath).toString())\n\nconst config = setDefaultConfig(selectedConfig)\n// Override noHead config option so full HTML pages are generated\nconfig.noHead = false\nconfig.assetPath = '/'\nconfig.log = console.log\nconfig.logWarning = console.warn\nconfig.logError = console.error\n\ntry {\n openDb(config)\n} catch (error: any) {\n if (error?.code === 'SQLITE_CANTOPEN') {\n config.logError(\n `Unable to open sqlite database \"${config.sqlitePath}\" defined as \\`sqlitePath\\` config.json. Ensure the parent directory exists or remove \\`sqlitePath\\` from config.json.`,\n )\n }\n\n throw error\n}\n\n/*\n * Show the transit departures widget\n */\nrouter.get('/', async (request, response, next) => {\n try {\n const html = await generateTransitDeparturesWidgetHtml(config)\n response.send(html)\n } catch (error) {\n next(error)\n }\n})\n\n/*\n * Provide data\n */\nrouter.get('/data/routes.json', async (request, response, next) => {\n try {\n const { routes } = await generateTransitDeparturesWidgetJson(config)\n response.json(routes)\n } catch (error) {\n next(error)\n }\n})\n\nrouter.get('/data/stops.json', async (request, response, next) => {\n try {\n const { stops } = await generateTransitDeparturesWidgetJson(config)\n response.json(stops)\n } catch (error) {\n next(error)\n }\n})\n\napp.set('views', path.join(fileURLToPath(import.meta.url), '../../../views'))\napp.set('view engine', 'pug')\n\napp.use(logger('dev'))\napp.use(\n express.static(path.join(fileURLToPath(import.meta.url), '../../../public')),\n)\n\napp.use('/', router)\napp.set('port', process.env.PORT || 3000)\n\nconst server = app.listen(app.get('port'), () => {\n console.log(`Express server listening on port ${app.get('port')}`)\n})\n","import { fileURLToPath } from 'url'\nimport { dirname, join } from 'path'\nimport { openDb, getDirections, getRoutes, getStops, getTrips } from 'gtfs'\nimport { groupBy, last, maxBy, size, sortBy, uniqBy } from 'lodash-es'\nimport { renderFile } from './file-utils.ts'\nimport sqlString from 'sqlstring-sqlite'\nimport toposort from 'toposort'\nimport i18n from 'i18n'\n\nimport { IConfig, SqlWhere, SqlValue } from '../types/global_interfaces.ts'\n\n/*\n * Get calendars for a specified date range\n */\nconst getCalendarsForDateRange = (config: IConfig) => {\n const db = openDb(config)\n let whereClause = ''\n const whereClauses = []\n\n if (config.endDate) {\n whereClauses.push(`start_date <= ${sqlString.escape(config.endDate)}`)\n }\n\n if (config.startDate) {\n whereClauses.push(`end_date >= ${sqlString.escape(config.startDate)}`)\n }\n\n if (whereClauses.length > 0) {\n whereClause = `WHERE ${whereClauses.join(' AND ')}`\n }\n\n return db.prepare(`SELECT * FROM calendar ${whereClause}`).all()\n}\n\n/*\n * Format a route name.\n */\nfunction formatRouteName(route) {\n let routeName = ''\n\n if (route.route_short_name !== null) {\n routeName += route.route_short_name\n }\n\n if (route.route_short_name !== null && route.route_long_name !== null) {\n routeName += ' - '\n }\n\n if (route.route_long_name !== null) {\n routeName += route.route_long_name\n }\n\n return routeName\n}\n\n/*\n * Get directions for a route\n */\nfunction getDirectionsForRoute(route: Record<string, string>, config: IConfig) {\n const db = openDb(config)\n\n // Lookup direction names from non-standard directions.txt file\n const directions = getDirections({ route_id: route.route_id }, [\n 'direction_id',\n 'direction',\n ])\n\n const calendars = getCalendarsForDateRange(config)\n\n // Else use the most common headsigns as directions from trips.txt file\n if (directions.length === 0) {\n const headsigns = db\n .prepare(\n `SELECT direction_id, trip_headsign, count(*) AS count FROM trips WHERE route_id = ? AND service_id IN (${calendars\n .map((calendar: Record<string, string>) => `'${calendar.service_id}'`)\n .join(', ')}) GROUP BY direction_id, trip_headsign`,\n )\n .all(route.route_id)\n\n for (const group of Object.values(groupBy(headsigns, 'direction_id'))) {\n const mostCommonHeadsign = maxBy(group, 'count')\n directions.push({\n direction_id: mostCommonHeadsign.direction_id,\n direction: i18n.__('To {{{headsign}}}', {\n headsign: mostCommonHeadsign.trip_headsign,\n }),\n })\n }\n }\n\n return directions\n}\n\n/*\n * Sort an array of stoptimes by stop_sequence using a directed graph\n */\nfunction sortStopIdsBySequence(stoptimes: Record<string, string>[]) {\n const stoptimesGroupedByTrip = groupBy(stoptimes, 'trip_id')\n\n // First, try using a directed graph to determine stop order.\n try {\n const stopGraph = []\n\n for (const tripStoptimes of Object.values(stoptimesGroupedByTrip)) {\n const sortedStopIds = sortBy(tripStoptimes, 'stop_sequence').map(\n (stoptime) => stoptime.stop_id,\n )\n\n for (const [index, stopId] of sortedStopIds.entries()) {\n if (index === sortedStopIds.length - 1) {\n continue\n }\n\n stopGraph.push([stopId, sortedStopIds[index + 1]])\n }\n }\n\n return toposort(\n stopGraph as unknown as readonly [string, string | undefined][],\n )\n } catch {\n // Ignore errors and move to next strategy.\n }\n\n // Finally, fall back to using the stop order from the trip with the most stoptimes.\n const longestTripStoptimes = maxBy(\n Object.values(stoptimesGroupedByTrip),\n (stoptimes) => size(stoptimes),\n )\n\n return longestTripStoptimes.map((stoptime) => stoptime.stop_id)\n}\n\n/*\n * Get stops in order for a route and direction\n */\nfunction getStopsForDirection(route, direction, config: IConfig) {\n const db = openDb(config)\n const calendars = getCalendarsForDateRange(config)\n const whereClause = formatWhereClauses({\n direction_id: direction.direction_id,\n route_id: route.route_id,\n service_id: calendars.map((calendar) => calendar.service_id),\n })\n const stoptimes = db\n .prepare(\n `SELECT stop_id, stop_sequence, trip_id FROM stop_times WHERE trip_id IN (SELECT trip_id FROM trips ${whereClause}) ORDER BY stop_sequence ASC`,\n )\n .all()\n\n const sortedStopIds = sortStopIdsBySequence(stoptimes)\n\n const deduplicatedStopIds = sortedStopIds.reduce(\n (memo: string[], stopId: string) => {\n // Remove duplicated stop_ids in a row\n if (last(memo) !== stopId) {\n memo.push(stopId)\n }\n\n return memo\n },\n [],\n )\n\n // Remove last stop of route since boarding is not allowed\n deduplicatedStopIds.pop()\n\n // Fetch stop details\n\n const stopFields = [\n 'stop_id',\n 'stop_name',\n 'stop_code',\n 'parent_station',\n ]\n\n if (config.includeCoordinates) {\n stopFields.push('stop_lat', 'stop_lon')\n }\n\n const stops = getStops({ stop_id: deduplicatedStopIds }, stopFields)\n\n return deduplicatedStopIds.map((stopId: string) =>\n stops.find((stop) => stop.stop_id === stopId),\n )\n}\n\n/*\n * Generate HTML for transit departures widget.\n */\nexport function generateTransitDeparturesWidgetHtml(config: IConfig) {\n i18n.configure({\n directory: join(dirname(fileURLToPath(import.meta.url)), '../../locales'),\n defaultLocale: config.locale,\n updateFiles: false,\n })\n\n const templateVars = {\n __: i18n.__,\n config,\n }\n return renderFile('widget', templateVars, config)\n}\n\n/*\n * Generate JSON of routes and stops for transit departures widget.\n */\nexport function generateTransitDeparturesWidgetJson(config: IConfig) {\n const routes = getRoutes()\n const stops = []\n const filteredRoutes = []\n const calendars = getCalendarsForDateRange(config)\n\n for (const route of routes) {\n route.route_full_name = formatRouteName(route)\n\n const directions = getDirectionsForRoute(route, config)\n\n // Filter out routes with no directions\n if (directions.length === 0) {\n config.logWarning(\n `route_id ${route.route_id} has no directions - skipping`,\n )\n continue\n }\n\n for (const direction of directions) {\n const directionStops = getStopsForDirection(route, direction, config)\n stops.push(...directionStops)\n direction.stopIds = directionStops.map((stop) => stop?.stop_id)\n\n const trips = getTrips(\n {\n route_id: route.route_id,\n direction_id: direction.direction_id,\n service_id: calendars.map(\n (calendar: Record<string, string>) => calendar.service_id,\n ),\n },\n ['trip_id'],\n )\n direction.tripIds = trips.map((trip) => trip.trip_id)\n }\n\n route.directions = directions\n filteredRoutes.push(route)\n }\n\n // Sort routes twice to handle integers with alphabetical characters, such as ['14', '14L', '14X']\n const sortedRoutes = sortBy(\n sortBy(filteredRoutes, (route) => route.route_short_name?.toLowerCase()),\n (route) => Number.parseInt(route.route_short_name, 10),\n )\n\n // Get Parent Station Stops\n const parentStationIds = new Set(stops.map((stop) => stop?.parent_station))\n\n const parentStationStops = getStops(\n { stop_id: Array.from(parentStationIds) },\n ['stop_id', 'stop_name', 'stop_code', 'parent_station'],\n )\n\n stops.push(\n ...parentStationStops.map((stop) => {\n stop.is_parent_station = true\n return stop\n }),\n )\n\n // Sort unique list of stops\n const sortedStops = sortBy(uniqBy(stops, 'stop_id'), 'stop_name')\n\n return {\n routes: removeNulls(sortedRoutes),\n stops: removeNulls(sortedStops),\n }\n}\n\n/*\n * Remove null values from array or object\n */\nfunction removeNulls(data: any): any {\n if (Array.isArray(data)) {\n return data\n .map(removeNulls)\n .filter((item) => item !== null && item !== undefined)\n } else if (typeof data === 'object' && data !== null) {\n return Object.entries(data).reduce((acc, [key, value]) => {\n const cleanedValue = removeNulls(value)\n if (cleanedValue !== null && cleanedValue !== undefined) {\n acc[key] = cleanedValue\n }\n return acc\n }, {})\n } else {\n return data\n }\n}\n\n/*\n * Initialize configuration with defaults.\n */\nexport function setDefaultConfig(initialConfig: IConfig) {\n const defaults = {\n beautify: false,\n noHead: false,\n refreshIntervalSeconds: 20,\n skipImport: false,\n timeFormat: '12hour',\n includeCoordinates: false,\n }\n\n return Object.assign(defaults, initialConfig)\n}\n\nexport function formatWhereClause(\n key: string,\n value: null | SqlValue | SqlValue[],\n) {\n if (Array.isArray(value)) {\n let whereClause = `${sqlString.escapeId(key)} IN (${value\n .filter((v) => v !== null)\n .map((v) => sqlString.escape(v))\n .join(', ')})`\n\n if (value.includes(null)) {\n whereClause = `(${whereClause} OR ${sqlString.escapeId(key)} IS NULL)`\n }\n\n return whereClause\n }\n\n if (value === null) {\n return `${sqlString.escapeId(key)} IS NULL`\n }\n\n return `${sqlString.escapeId(key)} = ${sqlString.escape(value)}`\n}\n\nexport function formatWhereClauses(query: SqlWhere) {\n if (Object.keys(query).length === 0) {\n return ''\n }\n\n const whereClauses = Object.entries(query).map(([key, value]) =>\n formatWhereClause(key, value),\n )\n return `WHERE ${whereClauses.join(' AND ')}`\n}\n","import path from 'path'\nimport { fileURLToPath } from 'node:url'\nimport { readFile, rm, mkdir } from 'node:fs/promises'\nimport copydir from 'copy-dir'\nimport beautify from 'js-beautify'\nimport pug from 'pug'\nimport untildify from 'untildify'\n\nimport { IConfig } from '../types/global_interfaces.ts'\n\n/*\n * Attempt to parse the specified config JSON file.\n */\nexport async function getConfig(argv) {\n try {\n const data = await readFile(\n path.resolve(untildify(argv.configPath)),\n 'utf8',\n ).catch((error) => {\n console.error(\n new Error(\n `Cannot find configuration file at \\`${argv.configPath}\\`. Use config-sample.json as a starting point, pass --configPath option`,\n ),\n )\n throw error\n })\n const config = JSON.parse(data)\n\n if (argv.skipImport === true) {\n config.skipImport = argv.skipImport\n }\n\n return config\n } catch (error) {\n console.error(\n new Error(\n `Cannot parse configuration file at \\`${argv.configPath}\\`. Check to ensure that it is valid JSON.`,\n ),\n )\n throw error\n }\n}\n\n/*\n * Get the full path of the template file for generating transit departures widget based on\n * config.\n */\nfunction getTemplatePath(templateFileName, config) {\n let fullTemplateFileName = templateFileName\n if (config.noHead !== true) {\n fullTemplateFileName += '_full'\n }\n\n if (config.templatePath !== undefined) {\n return path.join(\n untildify(config.templatePath),\n `${fullTemplateFileName}.pug`,\n )\n }\n\n return path.join(\n fileURLToPath(import.meta.url),\n '../../../views/widget',\n `${fullTemplateFileName}.pug`,\n )\n}\n\n/*\n * Prepare the specified directory for saving HTML widget by deleting everything.\n */\nexport async function prepDirectory(exportPath: string) {\n await rm(exportPath, { recursive: true, force: true })\n try {\n await mkdir(exportPath, { recursive: true })\n } catch (error: any) {\n if (error?.code === 'ENOENT') {\n throw new Error(\n `Unable to write to ${exportPath}. Try running this command from a writable directory.`,\n )\n }\n\n throw error\n }\n}\n\n/*\n * Copy needed CSS and JS to export path.\n */\nexport function copyStaticAssets(exportPath: string) {\n const staticAssetPath = path.join(\n fileURLToPath(import.meta.url),\n '../../../public',\n )\n copydir.sync(path.join(staticAssetPath, 'img'), path.join(exportPath, 'img'))\n copydir.sync(path.join(staticAssetPath, 'css'), path.join(exportPath, 'css'))\n copydir.sync(path.join(staticAssetPath, 'js'), path.join(exportPath, 'js'))\n}\n\n/*\n * Render the HTML based on the config.\n */\nexport async function renderFile(\n templateFileName: string,\n templateVars: any,\n config: IConfig,\n) {\n const templatePath = getTemplatePath(templateFileName, config)\n const html = await pug.renderFile(templatePath, templateVars)\n\n // Beautify HTML if setting is set\n if (config.beautify === true) {\n return beautify.html_beautify(html, { indent_size: 2 })\n }\n\n return html\n}\n"],"mappings":"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","names":["path","fileURLToPath","openDb","fileURLToPath","config","config","config","stoptimes","fileURLToPath","openDb","path","fileURLToPath"]}
1
+ {"version":3,"sources":["../../src/app/index.ts","../../src/lib/utils.ts","../../src/lib/file-utils.ts"],"sourcesContent":["import path from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport { readFileSync } from 'node:fs'\nimport yargs from 'yargs'\nimport { openDb } from 'gtfs'\n\nimport express, { Router } from 'express'\nimport logger from 'morgan'\n\nimport {\n setDefaultConfig,\n generateTransitDeparturesWidgetHtml,\n generateTransitDeparturesWidgetJson,\n} from '../lib/utils.ts'\n\nconst argv = yargs(process.argv)\n .option('c', {\n alias: 'configPath',\n describe: 'Path to config file',\n default: './config.json',\n type: 'string',\n })\n .parseSync()\n\nconst app = express()\nconst router = Router()\n\nconst configPath = (argv.configPath ||\n new URL('../../config.json', import.meta.url)) as string\n\nconst selectedConfig = JSON.parse(readFileSync(configPath).toString())\n\nconst config = setDefaultConfig(selectedConfig)\n// Override noHead config option so full HTML pages are generated\nconfig.noHead = false\nconfig.assetPath = '/'\nconfig.logFunction = console.log\n\ntry {\n openDb(config)\n} catch (error: any) {\n console.error(\n `Unable to open sqlite database \"${config.sqlitePath}\" defined as \\`sqlitePath\\` config.json. Ensure the parent directory exists and import GTFS before running this app.`,\n )\n throw error\n}\n\n/*\n * Show the transit departures widget\n */\nrouter.get('/', async (request, response, next) => {\n try {\n const html = await generateTransitDeparturesWidgetHtml(config)\n response.send(html)\n } catch (error) {\n next(error)\n }\n})\n\n/*\n * Provide data\n */\nrouter.get('/data/routes.json', async (request, response, next) => {\n try {\n const { routes } = await generateTransitDeparturesWidgetJson(config)\n response.json(routes)\n } catch (error) {\n next(error)\n }\n})\n\nrouter.get('/data/stops.json', async (request, response, next) => {\n try {\n const { stops } = await generateTransitDeparturesWidgetJson(config)\n response.json(stops)\n } catch (error) {\n next(error)\n }\n})\n\napp.set('views', path.join(fileURLToPath(import.meta.url), '../../../views'))\napp.set('view engine', 'pug')\n\napp.use(logger('dev'))\napp.use(\n express.static(path.join(fileURLToPath(import.meta.url), '../../../public')),\n)\n\napp.use('/', router)\napp.set('port', process.env.PORT || 3000)\n\nconst server = app.listen(app.get('port'), () => {\n console.log(`Express server listening on port ${app.get('port')}`)\n})\n","import { fileURLToPath } from 'url'\nimport { dirname, join } from 'path'\nimport { openDb, getDirections, getRoutes, getStops, getTrips } from 'gtfs'\nimport { groupBy, last, maxBy, size, sortBy, uniqBy } from 'lodash-es'\nimport { renderFile } from './file-utils.ts'\nimport sqlString from 'sqlstring-sqlite'\nimport toposort from 'toposort'\nimport i18n from 'i18n'\n\nimport { Config, SqlWhere, SqlValue } from '../types/global_interfaces.ts'\n\n/*\n * Get calendars for a specified date range\n */\nconst getCalendarsForDateRange = (config: Config) => {\n const db = openDb(config)\n let whereClause = ''\n const whereClauses = []\n\n if (config.endDate) {\n whereClauses.push(`start_date <= ${sqlString.escape(config.endDate)}`)\n }\n\n if (config.startDate) {\n whereClauses.push(`end_date >= ${sqlString.escape(config.startDate)}`)\n }\n\n if (whereClauses.length > 0) {\n whereClause = `WHERE ${whereClauses.join(' AND ')}`\n }\n\n return db.prepare(`SELECT * FROM calendar ${whereClause}`).all()\n}\n\n/*\n * Format a route name.\n */\nfunction formatRouteName(route) {\n let routeName = ''\n\n if (route.route_short_name !== null) {\n routeName += route.route_short_name\n }\n\n if (route.route_short_name !== null && route.route_long_name !== null) {\n routeName += ' - '\n }\n\n if (route.route_long_name !== null) {\n routeName += route.route_long_name\n }\n\n return routeName\n}\n\n/*\n * Get directions for a route\n */\nfunction getDirectionsForRoute(route: Record<string, string>, config: Config) {\n const db = openDb(config)\n\n // Lookup direction names from non-standard directions.txt file\n const directions = getDirections({ route_id: route.route_id }, [\n 'direction_id',\n 'direction',\n ])\n\n const calendars = getCalendarsForDateRange(config)\n\n // Else use the most common headsigns as directions from trips.txt file\n if (directions.length === 0) {\n const headsigns = db\n .prepare(\n `SELECT direction_id, trip_headsign, count(*) AS count FROM trips WHERE route_id = ? AND service_id IN (${calendars\n .map((calendar: Record<string, string>) => `'${calendar.service_id}'`)\n .join(', ')}) GROUP BY direction_id, trip_headsign`,\n )\n .all(route.route_id)\n\n for (const group of Object.values(groupBy(headsigns, 'direction_id'))) {\n const mostCommonHeadsign = maxBy(group, 'count')\n directions.push({\n direction_id: mostCommonHeadsign.direction_id,\n direction: i18n.__('To {{{headsign}}}', {\n headsign: mostCommonHeadsign.trip_headsign,\n }),\n })\n }\n }\n\n return directions\n}\n\n/*\n * Sort an array of stoptimes by stop_sequence using a directed graph\n */\nfunction sortStopIdsBySequence(stoptimes: Record<string, string>[]) {\n const stoptimesGroupedByTrip = groupBy(stoptimes, 'trip_id')\n\n // First, try using a directed graph to determine stop order.\n try {\n const stopGraph = []\n\n for (const tripStoptimes of Object.values(stoptimesGroupedByTrip)) {\n const sortedStopIds = sortBy(tripStoptimes, 'stop_sequence').map(\n (stoptime) => stoptime.stop_id,\n )\n\n for (const [index, stopId] of sortedStopIds.entries()) {\n if (index === sortedStopIds.length - 1) {\n continue\n }\n\n stopGraph.push([stopId, sortedStopIds[index + 1]])\n }\n }\n\n return toposort(\n stopGraph as unknown as readonly [string, string | undefined][],\n )\n } catch {\n // Ignore errors and move to next strategy.\n }\n\n // Finally, fall back to using the stop order from the trip with the most stoptimes.\n const longestTripStoptimes = maxBy(\n Object.values(stoptimesGroupedByTrip),\n (stoptimes) => size(stoptimes),\n )\n\n return longestTripStoptimes.map((stoptime) => stoptime.stop_id)\n}\n\n/*\n * Get stops in order for a route and direction\n */\nfunction getStopsForDirection(route, direction, config: Config) {\n const db = openDb(config)\n const calendars = getCalendarsForDateRange(config)\n const whereClause = formatWhereClauses({\n direction_id: direction.direction_id,\n route_id: route.route_id,\n service_id: calendars.map((calendar) => calendar.service_id),\n })\n const stoptimes = db\n .prepare(\n `SELECT stop_id, stop_sequence, trip_id FROM stop_times WHERE trip_id IN (SELECT trip_id FROM trips ${whereClause}) ORDER BY stop_sequence ASC`,\n )\n .all()\n\n const sortedStopIds = sortStopIdsBySequence(stoptimes)\n\n const deduplicatedStopIds = sortedStopIds.reduce(\n (memo: string[], stopId: string) => {\n // Remove duplicated stop_ids in a row\n if (last(memo) !== stopId) {\n memo.push(stopId)\n }\n\n return memo\n },\n [],\n )\n\n // Remove last stop of route since boarding is not allowed\n deduplicatedStopIds.pop()\n\n // Fetch stop details\n\n const stopFields = ['stop_id', 'stop_name', 'stop_code', 'parent_station']\n\n if (config.includeCoordinates) {\n stopFields.push('stop_lat', 'stop_lon')\n }\n\n const stops = getStops({ stop_id: deduplicatedStopIds }, stopFields)\n\n return deduplicatedStopIds.map((stopId: string) =>\n stops.find((stop) => stop.stop_id === stopId),\n )\n}\n\n/*\n * Generate HTML for transit departures widget.\n */\nexport function generateTransitDeparturesWidgetHtml(config: Config) {\n i18n.configure({\n directory: join(dirname(fileURLToPath(import.meta.url)), '../../locales'),\n defaultLocale: config.locale,\n updateFiles: false,\n })\n\n const templateVars = {\n __: i18n.__,\n config,\n }\n return renderFile('widget', templateVars, config)\n}\n\n/*\n * Generate JSON of routes and stops for transit departures widget.\n */\nexport function generateTransitDeparturesWidgetJson(config: Config) {\n const routes = getRoutes()\n const stops = []\n const filteredRoutes = []\n const calendars = getCalendarsForDateRange(config)\n\n for (const route of routes) {\n route.route_full_name = formatRouteName(route)\n\n const directions = getDirectionsForRoute(route, config)\n\n // Filter out routes with no directions\n if (directions.length === 0) {\n logWarning(config)(\n `route_id ${route.route_id} has no directions - skipping`,\n )\n continue\n }\n\n for (const direction of directions) {\n const directionStops = getStopsForDirection(route, direction, config)\n stops.push(...directionStops)\n direction.stopIds = directionStops.map((stop) => stop?.stop_id)\n\n const trips = getTrips(\n {\n route_id: route.route_id,\n direction_id: direction.direction_id,\n service_id: calendars.map(\n (calendar: Record<string, string>) => calendar.service_id,\n ),\n },\n ['trip_id'],\n )\n direction.tripIds = trips.map((trip) => trip.trip_id)\n }\n\n route.directions = directions\n filteredRoutes.push(route)\n }\n\n // Sort routes twice to handle integers with alphabetical characters, such as ['14', '14L', '14X']\n const sortedRoutes = sortBy(\n sortBy(filteredRoutes, (route) => route.route_short_name?.toLowerCase()),\n (route) => Number.parseInt(route.route_short_name, 10),\n )\n\n // Get Parent Station Stops\n const parentStationIds = new Set(stops.map((stop) => stop?.parent_station))\n\n const parentStationStops = getStops(\n { stop_id: Array.from(parentStationIds) },\n ['stop_id', 'stop_name', 'stop_code', 'parent_station'],\n )\n\n stops.push(\n ...parentStationStops.map((stop) => {\n stop.is_parent_station = true\n return stop\n }),\n )\n\n // Sort unique list of stops\n const sortedStops = sortBy(uniqBy(stops, 'stop_id'), 'stop_name')\n\n return {\n routes: removeNulls(sortedRoutes),\n stops: removeNulls(sortedStops),\n }\n}\n\n/*\n * Remove null values from array or object\n */\nfunction removeNulls(data: any): any {\n if (Array.isArray(data)) {\n return data\n .map(removeNulls)\n .filter((item) => item !== null && item !== undefined)\n } else if (typeof data === 'object' && data !== null) {\n return Object.entries(data).reduce((acc, [key, value]) => {\n const cleanedValue = removeNulls(value)\n if (cleanedValue !== null && cleanedValue !== undefined) {\n acc[key] = cleanedValue\n }\n return acc\n }, {})\n } else {\n return data\n }\n}\n\n/*\n * Initialize configuration with defaults.\n */\nexport function setDefaultConfig(initialConfig: Config) {\n const defaults = {\n beautify: false,\n noHead: false,\n refreshIntervalSeconds: 20,\n skipImport: false,\n timeFormat: '12hour',\n includeCoordinates: false,\n overwriteExistingFiles: true,\n verbose: true,\n }\n\n return Object.assign(defaults, initialConfig)\n}\n\nexport function formatWhereClause(\n key: string,\n value: null | SqlValue | SqlValue[],\n) {\n if (Array.isArray(value)) {\n let whereClause = `${sqlString.escapeId(key)} IN (${value\n .filter((v) => v !== null)\n .map((v) => sqlString.escape(v))\n .join(', ')})`\n\n if (value.includes(null)) {\n whereClause = `(${whereClause} OR ${sqlString.escapeId(key)} IS NULL)`\n }\n\n return whereClause\n }\n\n if (value === null) {\n return `${sqlString.escapeId(key)} IS NULL`\n }\n\n return `${sqlString.escapeId(key)} = ${sqlString.escape(value)}`\n}\n\nexport function formatWhereClauses(query: SqlWhere) {\n if (Object.keys(query).length === 0) {\n return ''\n }\n\n const whereClauses = Object.entries(query).map(([key, value]) =>\n formatWhereClause(key, value),\n )\n return `WHERE ${whereClauses.join(' AND ')}`\n}\n","import { dirname, join, resolve } from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport { access, cp, mkdir, readdir, readFile, rm } from 'node:fs/promises'\nimport beautify from 'js-beautify'\nimport pug from 'pug'\nimport untildify from 'untildify'\n\nimport { Config } from '../types/global_interfaces.ts'\n\n/*\n * Attempt to parse the specified config JSON file.\n */\nexport async function getConfig(argv) {\n try {\n const data = await readFile(\n resolve(untildify(argv.configPath)),\n 'utf8',\n ).catch((error) => {\n console.error(\n new Error(\n `Cannot find configuration file at \\`${argv.configPath}\\`. Use config-sample.json as a starting point, pass --configPath option`,\n ),\n )\n throw error\n })\n const config = JSON.parse(data)\n\n if (argv.skipImport === true) {\n config.skipImport = argv.skipImport\n }\n\n return config\n } catch (error) {\n console.error(\n new Error(\n `Cannot parse configuration file at \\`${argv.configPath}\\`. Check to ensure that it is valid JSON.`,\n ),\n )\n throw error\n }\n}\n\n/*\n * Get the full path to the views folder.\n */\nexport function getPathToViewsFolder(config: Config) {\n if (config.templatePath) {\n return untildify(config.templatePath)\n }\n\n const __dirname = dirname(fileURLToPath(import.meta.url))\n\n // Dynamically calculate the path to the views directory\n let viewsFolderPath\n if (__dirname.endsWith('/dist/bin') || __dirname.endsWith('/dist/app')) {\n // When the file is in 'dist/bin' or 'dist/app'\n viewsFolderPath = resolve(__dirname, '../../views/widget')\n } else if (__dirname.endsWith('/dist')) {\n // When the file is in 'dist'\n viewsFolderPath = resolve(__dirname, '../views/widget')\n } else {\n // In case it's neither, fallback to project root\n viewsFolderPath = resolve(__dirname, 'views/widget')\n }\n\n return viewsFolderPath\n}\n\n/*\n * Get the full path of the template file for generating transit departures widget based on\n * config.\n */\nfunction getTemplatePath(templateFileName: string, config: Config) {\n let fullTemplateFileName = templateFileName\n if (config.noHead !== true) {\n fullTemplateFileName += '_full'\n }\n\n if (config.templatePath !== undefined) {\n return join(untildify(config.templatePath), `${fullTemplateFileName}.pug`)\n }\n\n return join(\n fileURLToPath(import.meta.url),\n '../../../views/widget',\n `${fullTemplateFileName}.pug`,\n )\n}\n\n/*\n * Prepare the outputPath directory for writing timetable files.\n */\nexport async function prepDirectory(outputPath: string, config: Config) {\n // Check if outputPath exists\n try {\n await access(outputPath)\n } catch (error: any) {\n try {\n await mkdir(outputPath, { recursive: true })\n await mkdir(join(outputPath, 'data'))\n } catch (error: any) {\n if (error?.code === 'ENOENT') {\n throw new Error(\n `Unable to write to ${outputPath}. Try running this command from a writable directory.`,\n )\n }\n\n throw error\n }\n }\n\n // Check if outputPath is empty\n const files = await readdir(outputPath)\n if (config.overwriteExistingFiles === false && files.length > 0) {\n throw new Error(\n `Output directory ${outputPath} is not empty. Please specify an empty directory.`,\n )\n }\n\n // Delete all files in outputPath if `overwriteExistingFiles` is true\n if (config.overwriteExistingFiles === true) {\n await rm(join(outputPath, '*'), { recursive: true, force: true })\n }\n}\n\n/*\n * Copy needed CSS and JS to export path.\n */\nexport async function copyStaticAssets(config: Config, outputPath: string) {\n const viewsFolderPath = getPathToViewsFolder(config)\n\n const foldersToCopy = ['css', 'js', 'img']\n\n for (const folder of foldersToCopy) {\n if (\n await access(join(viewsFolderPath, folder))\n .then(() => true)\n .catch(() => false)\n ) {\n await cp(join(viewsFolderPath, folder), join(outputPath, folder), {\n recursive: true,\n })\n }\n }\n}\n\n/*\n * Render the HTML based on the config.\n */\nexport async function renderFile(\n templateFileName: string,\n templateVars: any,\n config: Config,\n) {\n const templatePath = getTemplatePath(templateFileName, config)\n const html = await pug.renderFile(templatePath, templateVars)\n\n // Beautify HTML if setting is set\n if (config.beautify === true) {\n return beautify.html_beautify(html, { indent_size: 2 })\n }\n\n return html\n}\n"],"mappings":"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","names":["fileURLToPath","openDb","fileURLToPath","dirname","join","config","config","config","stoptimes","join","dirname","fileURLToPath","openDb","fileURLToPath"]}
@@ -6,17 +6,16 @@ import { hideBin } from "yargs/helpers";
6
6
  import PrettyError from "pretty-error";
7
7
 
8
8
  // src/lib/file-utils.ts
9
- import path from "path";
9
+ import { dirname, join, resolve } from "node:path";
10
10
  import { fileURLToPath } from "node:url";
11
- import { readFile, rm, mkdir } from "node:fs/promises";
12
- import copydir from "copy-dir";
11
+ import { access, cp, mkdir, readdir, readFile, rm } from "node:fs/promises";
13
12
  import beautify from "js-beautify";
14
13
  import pug from "pug";
15
14
  import untildify from "untildify";
16
15
  async function getConfig(argv2) {
17
16
  try {
18
17
  const data = await readFile(
19
- path.resolve(untildify(argv2.configPath)),
18
+ resolve(untildify(argv2.configPath)),
20
19
  "utf8"
21
20
  ).catch((error) => {
22
21
  console.error(
@@ -40,44 +39,71 @@ async function getConfig(argv2) {
40
39
  throw error;
41
40
  }
42
41
  }
42
+ function getPathToViewsFolder(config) {
43
+ if (config.templatePath) {
44
+ return untildify(config.templatePath);
45
+ }
46
+ const __dirname = dirname(fileURLToPath(import.meta.url));
47
+ let viewsFolderPath;
48
+ if (__dirname.endsWith("/dist/bin") || __dirname.endsWith("/dist/app")) {
49
+ viewsFolderPath = resolve(__dirname, "../../views/widget");
50
+ } else if (__dirname.endsWith("/dist")) {
51
+ viewsFolderPath = resolve(__dirname, "../views/widget");
52
+ } else {
53
+ viewsFolderPath = resolve(__dirname, "views/widget");
54
+ }
55
+ return viewsFolderPath;
56
+ }
43
57
  function getTemplatePath(templateFileName, config) {
44
58
  let fullTemplateFileName = templateFileName;
45
59
  if (config.noHead !== true) {
46
60
  fullTemplateFileName += "_full";
47
61
  }
48
62
  if (config.templatePath !== void 0) {
49
- return path.join(
50
- untildify(config.templatePath),
51
- `${fullTemplateFileName}.pug`
52
- );
63
+ return join(untildify(config.templatePath), `${fullTemplateFileName}.pug`);
53
64
  }
54
- return path.join(
65
+ return join(
55
66
  fileURLToPath(import.meta.url),
56
67
  "../../../views/widget",
57
68
  `${fullTemplateFileName}.pug`
58
69
  );
59
70
  }
60
- async function prepDirectory(exportPath) {
61
- await rm(exportPath, { recursive: true, force: true });
71
+ async function prepDirectory(outputPath, config) {
62
72
  try {
63
- await mkdir(exportPath, { recursive: true });
73
+ await access(outputPath);
64
74
  } catch (error) {
65
- if (error?.code === "ENOENT") {
66
- throw new Error(
67
- `Unable to write to ${exportPath}. Try running this command from a writable directory.`
68
- );
75
+ try {
76
+ await mkdir(outputPath, { recursive: true });
77
+ await mkdir(join(outputPath, "data"));
78
+ } catch (error2) {
79
+ if (error2?.code === "ENOENT") {
80
+ throw new Error(
81
+ `Unable to write to ${outputPath}. Try running this command from a writable directory.`
82
+ );
83
+ }
84
+ throw error2;
69
85
  }
70
- throw error;
86
+ }
87
+ const files = await readdir(outputPath);
88
+ if (config.overwriteExistingFiles === false && files.length > 0) {
89
+ throw new Error(
90
+ `Output directory ${outputPath} is not empty. Please specify an empty directory.`
91
+ );
92
+ }
93
+ if (config.overwriteExistingFiles === true) {
94
+ await rm(join(outputPath, "*"), { recursive: true, force: true });
71
95
  }
72
96
  }
73
- function copyStaticAssets(exportPath) {
74
- const staticAssetPath = path.join(
75
- fileURLToPath(import.meta.url),
76
- "../../../public"
77
- );
78
- copydir.sync(path.join(staticAssetPath, "img"), path.join(exportPath, "img"));
79
- copydir.sync(path.join(staticAssetPath, "css"), path.join(exportPath, "css"));
80
- copydir.sync(path.join(staticAssetPath, "js"), path.join(exportPath, "js"));
97
+ async function copyStaticAssets(config, outputPath) {
98
+ const viewsFolderPath = getPathToViewsFolder(config);
99
+ const foldersToCopy = ["css", "js", "img"];
100
+ for (const folder of foldersToCopy) {
101
+ if (await access(join(viewsFolderPath, folder)).then(() => true).catch(() => false)) {
102
+ await cp(join(viewsFolderPath, folder), join(outputPath, folder), {
103
+ recursive: true
104
+ });
105
+ }
106
+ }
81
107
  }
82
108
  async function renderFile(templateFileName, templateVars, config) {
83
109
  const templatePath = getTemplatePath(templateFileName, config);
@@ -89,7 +115,7 @@ async function renderFile(templateFileName, templateVars, config) {
89
115
  }
90
116
 
91
117
  // src/lib/log-utils.ts
92
- import readline from "readline";
118
+ import { clearLine, cursorTo } from "node:readline";
93
119
  import { noop } from "lodash-es";
94
120
  import * as colors from "yoctocolors";
95
121
  function log(config) {
@@ -100,25 +126,15 @@ function log(config) {
100
126
  return config.logFunction;
101
127
  }
102
128
  return (text, overwrite) => {
103
- if (overwrite === true) {
104
- readline.clearLine(process.stdout, 0);
105
- readline.cursorTo(process.stdout, 0);
129
+ if (overwrite === true && process.stdout.isTTY) {
130
+ clearLine(process.stdout, 0);
131
+ cursorTo(process.stdout, 0);
106
132
  } else {
107
133
  process.stdout.write("\n");
108
134
  }
109
135
  process.stdout.write(text);
110
136
  };
111
137
  }
112
- function logWarning(config) {
113
- if (config.logFunction) {
114
- return config.logFunction;
115
- }
116
- return (text) => {
117
- process.stdout.write(`
118
- ${formatWarning(text)}
119
- `);
120
- };
121
- }
122
138
  function logError(config) {
123
139
  if (config.logFunction) {
124
140
  return config.logFunction;
@@ -129,10 +145,6 @@ ${formatError(text)}
129
145
  `);
130
146
  };
131
147
  }
132
- function formatWarning(text) {
133
- const warningMessage = `${colors.underline("Warning")}: ${text}`;
134
- return colors.yellow(warningMessage);
135
- }
136
148
  function formatError(error) {
137
149
  const messageText = error instanceof Error ? error.message : error;
138
150
  const errorMessage = `${colors.underline("Error")}: ${messageText.replace(
@@ -143,16 +155,17 @@ function formatError(error) {
143
155
  }
144
156
 
145
157
  // src/lib/transit-departures-widget.ts
146
- import path2 from "path";
158
+ import path from "path";
147
159
  import { clone, omit } from "lodash-es";
148
160
  import { writeFile } from "node:fs/promises";
149
161
  import { importGtfs, openDb as openDb2 } from "gtfs";
150
162
  import sanitize from "sanitize-filename";
151
163
  import Timer from "timer-machine";
164
+ import untildify2 from "untildify";
152
165
 
153
166
  // src/lib/utils.ts
154
167
  import { fileURLToPath as fileURLToPath2 } from "url";
155
- import { dirname, join } from "path";
168
+ import { dirname as dirname2, join as join2 } from "path";
156
169
  import { openDb, getDirections, getRoutes, getStops, getTrips } from "gtfs";
157
170
  import { groupBy, last, maxBy, size, sortBy, uniqBy } from "lodash-es";
158
171
  import sqlString from "sqlstring-sqlite";
@@ -257,12 +270,7 @@ function getStopsForDirection(route, direction, config) {
257
270
  []
258
271
  );
259
272
  deduplicatedStopIds.pop();
260
- const stopFields = [
261
- "stop_id",
262
- "stop_name",
263
- "stop_code",
264
- "parent_station"
265
- ];
273
+ const stopFields = ["stop_id", "stop_name", "stop_code", "parent_station"];
266
274
  if (config.includeCoordinates) {
267
275
  stopFields.push("stop_lat", "stop_lon");
268
276
  }
@@ -273,7 +281,7 @@ function getStopsForDirection(route, direction, config) {
273
281
  }
274
282
  function generateTransitDeparturesWidgetHtml(config) {
275
283
  i18n.configure({
276
- directory: join(dirname(fileURLToPath2(import.meta.url)), "../../locales"),
284
+ directory: join2(dirname2(fileURLToPath2(import.meta.url)), "../../locales"),
277
285
  defaultLocale: config.locale,
278
286
  updateFiles: false
279
287
  });
@@ -292,7 +300,7 @@ function generateTransitDeparturesWidgetJson(config) {
292
300
  route.route_full_name = formatRouteName(route);
293
301
  const directions = getDirectionsForRoute(route, config);
294
302
  if (directions.length === 0) {
295
- config.logWarning(
303
+ logWarning(config)(
296
304
  `route_id ${route.route_id} has no directions - skipping`
297
305
  );
298
306
  continue;
@@ -359,7 +367,9 @@ function setDefaultConfig(initialConfig) {
359
367
  refreshIntervalSeconds: 20,
360
368
  skipImport: false,
361
369
  timeFormat: "12hour",
362
- includeCoordinates: false
370
+ includeCoordinates: false,
371
+ overwriteExistingFiles: true,
372
+ verbose: true
363
373
  };
364
374
  return Object.assign(defaults, initialConfig);
365
375
  }
@@ -389,14 +399,11 @@ function formatWhereClauses(query) {
389
399
  // src/lib/transit-departures-widget.ts
390
400
  async function transitDeparturesWidget(initialConfig) {
391
401
  const config = setDefaultConfig(initialConfig);
392
- config.log = log(config);
393
- config.logWarning = logWarning(config);
394
- config.logError = logError(config);
395
402
  try {
396
403
  openDb2(config);
397
404
  } catch (error) {
398
405
  if (error?.code === "SQLITE_CANTOPEN") {
399
- config.logError(
406
+ logError(config)(
400
407
  `Unable to open sqlite database "${config.sqlitePath}" defined as \`sqlitePath\` config.json. Ensure the parent directory exists or remove \`sqlitePath\` from config.json.`
401
408
  );
402
409
  }
@@ -406,8 +413,8 @@ async function transitDeparturesWidget(initialConfig) {
406
413
  throw new Error("No agency defined in `config.json`");
407
414
  }
408
415
  const timer = new Timer();
409
- const agencyKey = config.agency.agency_key;
410
- const exportPath = path2.join(process.cwd(), "html", sanitize(agencyKey));
416
+ const agencyKey = config.agency.agency_key ?? "unknown";
417
+ const outputPath = config.outputPath ? untildify2(config.outputPath) : path.join(process.cwd(), "html", sanitize(agencyKey));
411
418
  timer.start();
412
419
  if (!config.skipImport) {
413
420
  const gtfsImportConfig = {
@@ -422,30 +429,29 @@ async function transitDeparturesWidget(initialConfig) {
422
429
  };
423
430
  await importGtfs(gtfsImportConfig);
424
431
  }
425
- await prepDirectory(exportPath);
426
- await prepDirectory(path2.join(exportPath, "data"));
432
+ await prepDirectory(outputPath, config);
427
433
  if (config.noHead !== true) {
428
- copyStaticAssets(exportPath);
434
+ await copyStaticAssets(config, outputPath);
429
435
  }
430
- config.log(`${agencyKey}: Generating Transit Departures Widget HTML`);
436
+ log(config)(`${agencyKey}: Generating Transit Departures Widget HTML`);
431
437
  config.assetPath = "";
432
438
  const { routes, stops } = generateTransitDeparturesWidgetJson(config);
433
439
  await writeFile(
434
- path2.join(exportPath, "data", "routes.json"),
440
+ path.join(outputPath, "data", "routes.json"),
435
441
  JSON.stringify(routes, null, 2)
436
442
  );
437
443
  await writeFile(
438
- path2.join(exportPath, "data", "stops.json"),
444
+ path.join(outputPath, "data", "stops.json"),
439
445
  JSON.stringify(stops, null, 2)
440
446
  );
441
447
  const html = await generateTransitDeparturesWidgetHtml(config);
442
- await writeFile(path2.join(exportPath, "index.html"), html);
448
+ await writeFile(path.join(outputPath, "index.html"), html);
443
449
  timer.stop();
444
- config.log(
445
- `${agencyKey}: Transit Departures Widget HTML created at ${exportPath}`
450
+ log(config)(
451
+ `${agencyKey}: Transit Departures Widget HTML created at ${outputPath}`
446
452
  );
447
453
  const seconds = Math.round(timer.time() / 1e3);
448
- config.log(`${agencyKey}: HTML generation required ${seconds} seconds`);
454
+ log(config)(`${agencyKey}: HTML generation required ${seconds} seconds`);
449
455
  }
450
456
  var transit_departures_widget_default = transitDeparturesWidget;
451
457
 
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/bin/transit-departures-widget.ts","../../src/lib/file-utils.ts","../../src/lib/log-utils.ts","../../src/lib/transit-departures-widget.ts","../../src/lib/utils.ts"],"sourcesContent":["#!/usr/bin/env node\n\nimport yargs from 'yargs'\nimport { hideBin } from 'yargs/helpers'\nimport PrettyError from 'pretty-error'\n\nimport { getConfig } from '../lib/file-utils.ts'\nimport { formatError } from '../lib/log-utils.ts'\nimport transitDeparturesWidget from '../index.ts'\n\nconst pe = new PrettyError()\n\nconst argv = yargs(hideBin(process.argv))\n .usage('Usage: $0 --config ./config.json')\n .help()\n .option('c', {\n alias: 'configPath',\n describe: 'Path to config file',\n default: './config.json',\n type: 'string',\n })\n .option('s', {\n alias: 'skipImport',\n describe: 'Don’t import GTFS file.',\n type: 'boolean',\n })\n .default('skipImport', undefined)\n .parseSync()\n\nconst handleError = (error: any) => {\n const text = error || 'Unknown Error'\n process.stdout.write(`\\n${formatError(text)}\\n`)\n console.error(pe.render(error))\n process.exit(1)\n}\n\nconst setupImport = async () => {\n const config = await getConfig(argv)\n await transitDeparturesWidget(config)\n process.exit()\n}\n\nsetupImport().catch(handleError)\n","import path from 'path'\nimport { fileURLToPath } from 'node:url'\nimport { readFile, rm, mkdir } from 'node:fs/promises'\nimport copydir from 'copy-dir'\nimport beautify from 'js-beautify'\nimport pug from 'pug'\nimport untildify from 'untildify'\n\nimport { IConfig } from '../types/global_interfaces.ts'\n\n/*\n * Attempt to parse the specified config JSON file.\n */\nexport async function getConfig(argv) {\n try {\n const data = await readFile(\n path.resolve(untildify(argv.configPath)),\n 'utf8',\n ).catch((error) => {\n console.error(\n new Error(\n `Cannot find configuration file at \\`${argv.configPath}\\`. Use config-sample.json as a starting point, pass --configPath option`,\n ),\n )\n throw error\n })\n const config = JSON.parse(data)\n\n if (argv.skipImport === true) {\n config.skipImport = argv.skipImport\n }\n\n return config\n } catch (error) {\n console.error(\n new Error(\n `Cannot parse configuration file at \\`${argv.configPath}\\`. Check to ensure that it is valid JSON.`,\n ),\n )\n throw error\n }\n}\n\n/*\n * Get the full path of the template file for generating transit departures widget based on\n * config.\n */\nfunction getTemplatePath(templateFileName, config) {\n let fullTemplateFileName = templateFileName\n if (config.noHead !== true) {\n fullTemplateFileName += '_full'\n }\n\n if (config.templatePath !== undefined) {\n return path.join(\n untildify(config.templatePath),\n `${fullTemplateFileName}.pug`,\n )\n }\n\n return path.join(\n fileURLToPath(import.meta.url),\n '../../../views/widget',\n `${fullTemplateFileName}.pug`,\n )\n}\n\n/*\n * Prepare the specified directory for saving HTML widget by deleting everything.\n */\nexport async function prepDirectory(exportPath: string) {\n await rm(exportPath, { recursive: true, force: true })\n try {\n await mkdir(exportPath, { recursive: true })\n } catch (error: any) {\n if (error?.code === 'ENOENT') {\n throw new Error(\n `Unable to write to ${exportPath}. Try running this command from a writable directory.`,\n )\n }\n\n throw error\n }\n}\n\n/*\n * Copy needed CSS and JS to export path.\n */\nexport function copyStaticAssets(exportPath: string) {\n const staticAssetPath = path.join(\n fileURLToPath(import.meta.url),\n '../../../public',\n )\n copydir.sync(path.join(staticAssetPath, 'img'), path.join(exportPath, 'img'))\n copydir.sync(path.join(staticAssetPath, 'css'), path.join(exportPath, 'css'))\n copydir.sync(path.join(staticAssetPath, 'js'), path.join(exportPath, 'js'))\n}\n\n/*\n * Render the HTML based on the config.\n */\nexport async function renderFile(\n templateFileName: string,\n templateVars: any,\n config: IConfig,\n) {\n const templatePath = getTemplatePath(templateFileName, config)\n const html = await pug.renderFile(templatePath, templateVars)\n\n // Beautify HTML if setting is set\n if (config.beautify === true) {\n return beautify.html_beautify(html, { indent_size: 2 })\n }\n\n return html\n}\n","import readline from 'readline'\nimport { noop } from 'lodash-es'\nimport * as colors from 'yoctocolors'\n\nimport { IConfig } from '../types/global_interfaces.ts'\n\n/*\n * Returns a log function based on config settings\n */\nexport function log(config: IConfig) {\n if (config.verbose === false) {\n return noop\n }\n\n if (config.logFunction) {\n return config.logFunction\n }\n\n return (text: string, overwrite: boolean) => {\n if (overwrite === true) {\n readline.clearLine(process.stdout, 0)\n readline.cursorTo(process.stdout, 0)\n } else {\n process.stdout.write('\\n')\n }\n\n process.stdout.write(text)\n }\n}\n\n/*\n * Returns an warning log function based on config settings\n */\nexport function logWarning(config: IConfig) {\n if (config.logFunction) {\n return config.logFunction\n }\n\n return (text: string) => {\n process.stdout.write(`\\n${formatWarning(text)}\\n`)\n }\n}\n\n/*\n * Returns an error log function based on config settings\n */\nexport function logError(config: IConfig) {\n if (config.logFunction) {\n return config.logFunction\n }\n\n return (text: string) => {\n process.stdout.write(`\\n${formatError(text)}\\n`)\n }\n}\n\n/*\n * Format console warning text\n */\nexport function formatWarning(text: string) {\n const warningMessage = `${colors.underline('Warning')}: ${text}`\n return colors.yellow(warningMessage)\n}\n\n/*\n * Format console error text\n */\nexport function formatError(error: any) {\n const messageText = error instanceof Error ? error.message : error\n const errorMessage = `${colors.underline('Error')}: ${messageText.replace(\n 'Error: ',\n '',\n )}`\n return colors.red(errorMessage)\n}\n","import path from 'path'\nimport { clone, omit } from 'lodash-es'\nimport { writeFile } from 'node:fs/promises'\nimport { importGtfs, openDb } from 'gtfs'\nimport sanitize from 'sanitize-filename'\nimport Timer from 'timer-machine'\n\nimport { copyStaticAssets, prepDirectory } from './file-utils.ts'\nimport { log, logWarning, logError } from './log-utils.ts'\nimport {\n generateTransitDeparturesWidgetHtml,\n generateTransitDeparturesWidgetJson,\n setDefaultConfig,\n} from './utils.ts'\nimport { IConfig } from '../types/global_interfaces.ts'\n\n/*\n * Generate transit departures widget HTML from GTFS.\n */\nasync function transitDeparturesWidget(initialConfig: IConfig) {\n const config = setDefaultConfig(initialConfig)\n config.log = log(config)\n config.logWarning = logWarning(config)\n config.logError = logError(config)\n\n try {\n openDb(config)\n } catch (error: any) {\n if (error?.code === 'SQLITE_CANTOPEN') {\n config.logError(\n `Unable to open sqlite database \"${config.sqlitePath}\" defined as \\`sqlitePath\\` config.json. Ensure the parent directory exists or remove \\`sqlitePath\\` from config.json.`,\n )\n }\n\n throw error\n }\n\n if (!config.agency) {\n throw new Error('No agency defined in `config.json`')\n }\n\n const timer = new Timer()\n const agencyKey = config.agency.agency_key\n const exportPath = path.join(process.cwd(), 'html', sanitize(agencyKey))\n\n timer.start()\n\n if (!config.skipImport) {\n // Import GTFS\n const gtfsImportConfig = {\n ...clone(omit(config, 'agency')),\n agencies: [\n {\n agency_key: config.agency.agency_key,\n path: config.agency.gtfs_static_path,\n url: config.agency.gtfs_static_url,\n },\n ],\n }\n\n await importGtfs(gtfsImportConfig)\n }\n\n await prepDirectory(exportPath)\n await prepDirectory(path.join(exportPath, 'data'))\n\n if (config.noHead !== true) {\n copyStaticAssets(exportPath)\n }\n\n config.log(`${agencyKey}: Generating Transit Departures Widget HTML`)\n\n config.assetPath = ''\n\n // Generate JSON of routes and stops\n const { routes, stops } = generateTransitDeparturesWidgetJson(config)\n await writeFile(\n path.join(exportPath, 'data', 'routes.json'),\n JSON.stringify(routes, null, 2),\n )\n await writeFile(\n path.join(exportPath, 'data', 'stops.json'),\n JSON.stringify(stops, null, 2),\n )\n\n const html = await generateTransitDeparturesWidgetHtml(config)\n await writeFile(path.join(exportPath, 'index.html'), html)\n\n timer.stop()\n\n // Print stats\n config.log(\n `${agencyKey}: Transit Departures Widget HTML created at ${exportPath}`,\n )\n\n const seconds = Math.round(timer.time() / 1000)\n config.log(`${agencyKey}: HTML generation required ${seconds} seconds`)\n}\n\nexport default transitDeparturesWidget\n","import { fileURLToPath } from 'url'\nimport { dirname, join } from 'path'\nimport { openDb, getDirections, getRoutes, getStops, getTrips } from 'gtfs'\nimport { groupBy, last, maxBy, size, sortBy, uniqBy } from 'lodash-es'\nimport { renderFile } from './file-utils.ts'\nimport sqlString from 'sqlstring-sqlite'\nimport toposort from 'toposort'\nimport i18n from 'i18n'\n\nimport { IConfig, SqlWhere, SqlValue } from '../types/global_interfaces.ts'\n\n/*\n * Get calendars for a specified date range\n */\nconst getCalendarsForDateRange = (config: IConfig) => {\n const db = openDb(config)\n let whereClause = ''\n const whereClauses = []\n\n if (config.endDate) {\n whereClauses.push(`start_date <= ${sqlString.escape(config.endDate)}`)\n }\n\n if (config.startDate) {\n whereClauses.push(`end_date >= ${sqlString.escape(config.startDate)}`)\n }\n\n if (whereClauses.length > 0) {\n whereClause = `WHERE ${whereClauses.join(' AND ')}`\n }\n\n return db.prepare(`SELECT * FROM calendar ${whereClause}`).all()\n}\n\n/*\n * Format a route name.\n */\nfunction formatRouteName(route) {\n let routeName = ''\n\n if (route.route_short_name !== null) {\n routeName += route.route_short_name\n }\n\n if (route.route_short_name !== null && route.route_long_name !== null) {\n routeName += ' - '\n }\n\n if (route.route_long_name !== null) {\n routeName += route.route_long_name\n }\n\n return routeName\n}\n\n/*\n * Get directions for a route\n */\nfunction getDirectionsForRoute(route: Record<string, string>, config: IConfig) {\n const db = openDb(config)\n\n // Lookup direction names from non-standard directions.txt file\n const directions = getDirections({ route_id: route.route_id }, [\n 'direction_id',\n 'direction',\n ])\n\n const calendars = getCalendarsForDateRange(config)\n\n // Else use the most common headsigns as directions from trips.txt file\n if (directions.length === 0) {\n const headsigns = db\n .prepare(\n `SELECT direction_id, trip_headsign, count(*) AS count FROM trips WHERE route_id = ? AND service_id IN (${calendars\n .map((calendar: Record<string, string>) => `'${calendar.service_id}'`)\n .join(', ')}) GROUP BY direction_id, trip_headsign`,\n )\n .all(route.route_id)\n\n for (const group of Object.values(groupBy(headsigns, 'direction_id'))) {\n const mostCommonHeadsign = maxBy(group, 'count')\n directions.push({\n direction_id: mostCommonHeadsign.direction_id,\n direction: i18n.__('To {{{headsign}}}', {\n headsign: mostCommonHeadsign.trip_headsign,\n }),\n })\n }\n }\n\n return directions\n}\n\n/*\n * Sort an array of stoptimes by stop_sequence using a directed graph\n */\nfunction sortStopIdsBySequence(stoptimes: Record<string, string>[]) {\n const stoptimesGroupedByTrip = groupBy(stoptimes, 'trip_id')\n\n // First, try using a directed graph to determine stop order.\n try {\n const stopGraph = []\n\n for (const tripStoptimes of Object.values(stoptimesGroupedByTrip)) {\n const sortedStopIds = sortBy(tripStoptimes, 'stop_sequence').map(\n (stoptime) => stoptime.stop_id,\n )\n\n for (const [index, stopId] of sortedStopIds.entries()) {\n if (index === sortedStopIds.length - 1) {\n continue\n }\n\n stopGraph.push([stopId, sortedStopIds[index + 1]])\n }\n }\n\n return toposort(\n stopGraph as unknown as readonly [string, string | undefined][],\n )\n } catch {\n // Ignore errors and move to next strategy.\n }\n\n // Finally, fall back to using the stop order from the trip with the most stoptimes.\n const longestTripStoptimes = maxBy(\n Object.values(stoptimesGroupedByTrip),\n (stoptimes) => size(stoptimes),\n )\n\n return longestTripStoptimes.map((stoptime) => stoptime.stop_id)\n}\n\n/*\n * Get stops in order for a route and direction\n */\nfunction getStopsForDirection(route, direction, config: IConfig) {\n const db = openDb(config)\n const calendars = getCalendarsForDateRange(config)\n const whereClause = formatWhereClauses({\n direction_id: direction.direction_id,\n route_id: route.route_id,\n service_id: calendars.map((calendar) => calendar.service_id),\n })\n const stoptimes = db\n .prepare(\n `SELECT stop_id, stop_sequence, trip_id FROM stop_times WHERE trip_id IN (SELECT trip_id FROM trips ${whereClause}) ORDER BY stop_sequence ASC`,\n )\n .all()\n\n const sortedStopIds = sortStopIdsBySequence(stoptimes)\n\n const deduplicatedStopIds = sortedStopIds.reduce(\n (memo: string[], stopId: string) => {\n // Remove duplicated stop_ids in a row\n if (last(memo) !== stopId) {\n memo.push(stopId)\n }\n\n return memo\n },\n [],\n )\n\n // Remove last stop of route since boarding is not allowed\n deduplicatedStopIds.pop()\n\n // Fetch stop details\n\n const stopFields = [\n 'stop_id',\n 'stop_name',\n 'stop_code',\n 'parent_station',\n ]\n\n if (config.includeCoordinates) {\n stopFields.push('stop_lat', 'stop_lon')\n }\n\n const stops = getStops({ stop_id: deduplicatedStopIds }, stopFields)\n\n return deduplicatedStopIds.map((stopId: string) =>\n stops.find((stop) => stop.stop_id === stopId),\n )\n}\n\n/*\n * Generate HTML for transit departures widget.\n */\nexport function generateTransitDeparturesWidgetHtml(config: IConfig) {\n i18n.configure({\n directory: join(dirname(fileURLToPath(import.meta.url)), '../../locales'),\n defaultLocale: config.locale,\n updateFiles: false,\n })\n\n const templateVars = {\n __: i18n.__,\n config,\n }\n return renderFile('widget', templateVars, config)\n}\n\n/*\n * Generate JSON of routes and stops for transit departures widget.\n */\nexport function generateTransitDeparturesWidgetJson(config: IConfig) {\n const routes = getRoutes()\n const stops = []\n const filteredRoutes = []\n const calendars = getCalendarsForDateRange(config)\n\n for (const route of routes) {\n route.route_full_name = formatRouteName(route)\n\n const directions = getDirectionsForRoute(route, config)\n\n // Filter out routes with no directions\n if (directions.length === 0) {\n config.logWarning(\n `route_id ${route.route_id} has no directions - skipping`,\n )\n continue\n }\n\n for (const direction of directions) {\n const directionStops = getStopsForDirection(route, direction, config)\n stops.push(...directionStops)\n direction.stopIds = directionStops.map((stop) => stop?.stop_id)\n\n const trips = getTrips(\n {\n route_id: route.route_id,\n direction_id: direction.direction_id,\n service_id: calendars.map(\n (calendar: Record<string, string>) => calendar.service_id,\n ),\n },\n ['trip_id'],\n )\n direction.tripIds = trips.map((trip) => trip.trip_id)\n }\n\n route.directions = directions\n filteredRoutes.push(route)\n }\n\n // Sort routes twice to handle integers with alphabetical characters, such as ['14', '14L', '14X']\n const sortedRoutes = sortBy(\n sortBy(filteredRoutes, (route) => route.route_short_name?.toLowerCase()),\n (route) => Number.parseInt(route.route_short_name, 10),\n )\n\n // Get Parent Station Stops\n const parentStationIds = new Set(stops.map((stop) => stop?.parent_station))\n\n const parentStationStops = getStops(\n { stop_id: Array.from(parentStationIds) },\n ['stop_id', 'stop_name', 'stop_code', 'parent_station'],\n )\n\n stops.push(\n ...parentStationStops.map((stop) => {\n stop.is_parent_station = true\n return stop\n }),\n )\n\n // Sort unique list of stops\n const sortedStops = sortBy(uniqBy(stops, 'stop_id'), 'stop_name')\n\n return {\n routes: removeNulls(sortedRoutes),\n stops: removeNulls(sortedStops),\n }\n}\n\n/*\n * Remove null values from array or object\n */\nfunction removeNulls(data: any): any {\n if (Array.isArray(data)) {\n return data\n .map(removeNulls)\n .filter((item) => item !== null && item !== undefined)\n } else if (typeof data === 'object' && data !== null) {\n return Object.entries(data).reduce((acc, [key, value]) => {\n const cleanedValue = removeNulls(value)\n if (cleanedValue !== null && cleanedValue !== undefined) {\n acc[key] = cleanedValue\n }\n return acc\n }, {})\n } else {\n return data\n }\n}\n\n/*\n * Initialize configuration with defaults.\n */\nexport function setDefaultConfig(initialConfig: IConfig) {\n const defaults = {\n beautify: false,\n noHead: false,\n refreshIntervalSeconds: 20,\n skipImport: false,\n timeFormat: '12hour',\n includeCoordinates: false,\n }\n\n return Object.assign(defaults, initialConfig)\n}\n\nexport function formatWhereClause(\n key: string,\n value: null | SqlValue | SqlValue[],\n) {\n if (Array.isArray(value)) {\n let whereClause = `${sqlString.escapeId(key)} IN (${value\n .filter((v) => v !== null)\n .map((v) => sqlString.escape(v))\n .join(', ')})`\n\n if (value.includes(null)) {\n whereClause = `(${whereClause} OR ${sqlString.escapeId(key)} IS NULL)`\n }\n\n return whereClause\n }\n\n if (value === null) {\n return `${sqlString.escapeId(key)} IS NULL`\n }\n\n return `${sqlString.escapeId(key)} = ${sqlString.escape(value)}`\n}\n\nexport function formatWhereClauses(query: SqlWhere) {\n if (Object.keys(query).length === 0) {\n return ''\n }\n\n const whereClauses = Object.entries(query).map(([key, value]) =>\n formatWhereClause(key, value),\n )\n return `WHERE ${whereClauses.join(' AND ')}`\n}\n"],"mappings":"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","names":["argv","path","openDb","fileURLToPath","stoptimes","fileURLToPath","openDb","path"]}
1
+ {"version":3,"sources":["../../src/bin/transit-departures-widget.ts","../../src/lib/file-utils.ts","../../src/lib/log-utils.ts","../../src/lib/transit-departures-widget.ts","../../src/lib/utils.ts"],"sourcesContent":["#!/usr/bin/env node\n\nimport yargs from 'yargs'\nimport { hideBin } from 'yargs/helpers'\nimport PrettyError from 'pretty-error'\n\nimport { getConfig } from '../lib/file-utils.ts'\nimport { formatError } from '../lib/log-utils.ts'\nimport transitDeparturesWidget from '../index.ts'\n\nconst pe = new PrettyError()\n\nconst argv = yargs(hideBin(process.argv))\n .usage('Usage: $0 --config ./config.json')\n .help()\n .option('c', {\n alias: 'configPath',\n describe: 'Path to config file',\n default: './config.json',\n type: 'string',\n })\n .option('s', {\n alias: 'skipImport',\n describe: 'Don’t import GTFS file.',\n type: 'boolean',\n })\n .default('skipImport', undefined)\n .parseSync()\n\nconst handleError = (error: any) => {\n const text = error || 'Unknown Error'\n process.stdout.write(`\\n${formatError(text)}\\n`)\n console.error(pe.render(error))\n process.exit(1)\n}\n\nconst setupImport = async () => {\n const config = await getConfig(argv)\n await transitDeparturesWidget(config)\n process.exit()\n}\n\nsetupImport().catch(handleError)\n","import { dirname, join, resolve } from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport { access, cp, mkdir, readdir, readFile, rm } from 'node:fs/promises'\nimport beautify from 'js-beautify'\nimport pug from 'pug'\nimport untildify from 'untildify'\n\nimport { Config } from '../types/global_interfaces.ts'\n\n/*\n * Attempt to parse the specified config JSON file.\n */\nexport async function getConfig(argv) {\n try {\n const data = await readFile(\n resolve(untildify(argv.configPath)),\n 'utf8',\n ).catch((error) => {\n console.error(\n new Error(\n `Cannot find configuration file at \\`${argv.configPath}\\`. Use config-sample.json as a starting point, pass --configPath option`,\n ),\n )\n throw error\n })\n const config = JSON.parse(data)\n\n if (argv.skipImport === true) {\n config.skipImport = argv.skipImport\n }\n\n return config\n } catch (error) {\n console.error(\n new Error(\n `Cannot parse configuration file at \\`${argv.configPath}\\`. Check to ensure that it is valid JSON.`,\n ),\n )\n throw error\n }\n}\n\n/*\n * Get the full path to the views folder.\n */\nexport function getPathToViewsFolder(config: Config) {\n if (config.templatePath) {\n return untildify(config.templatePath)\n }\n\n const __dirname = dirname(fileURLToPath(import.meta.url))\n\n // Dynamically calculate the path to the views directory\n let viewsFolderPath\n if (__dirname.endsWith('/dist/bin') || __dirname.endsWith('/dist/app')) {\n // When the file is in 'dist/bin' or 'dist/app'\n viewsFolderPath = resolve(__dirname, '../../views/widget')\n } else if (__dirname.endsWith('/dist')) {\n // When the file is in 'dist'\n viewsFolderPath = resolve(__dirname, '../views/widget')\n } else {\n // In case it's neither, fallback to project root\n viewsFolderPath = resolve(__dirname, 'views/widget')\n }\n\n return viewsFolderPath\n}\n\n/*\n * Get the full path of the template file for generating transit departures widget based on\n * config.\n */\nfunction getTemplatePath(templateFileName: string, config: Config) {\n let fullTemplateFileName = templateFileName\n if (config.noHead !== true) {\n fullTemplateFileName += '_full'\n }\n\n if (config.templatePath !== undefined) {\n return join(untildify(config.templatePath), `${fullTemplateFileName}.pug`)\n }\n\n return join(\n fileURLToPath(import.meta.url),\n '../../../views/widget',\n `${fullTemplateFileName}.pug`,\n )\n}\n\n/*\n * Prepare the outputPath directory for writing timetable files.\n */\nexport async function prepDirectory(outputPath: string, config: Config) {\n // Check if outputPath exists\n try {\n await access(outputPath)\n } catch (error: any) {\n try {\n await mkdir(outputPath, { recursive: true })\n await mkdir(join(outputPath, 'data'))\n } catch (error: any) {\n if (error?.code === 'ENOENT') {\n throw new Error(\n `Unable to write to ${outputPath}. Try running this command from a writable directory.`,\n )\n }\n\n throw error\n }\n }\n\n // Check if outputPath is empty\n const files = await readdir(outputPath)\n if (config.overwriteExistingFiles === false && files.length > 0) {\n throw new Error(\n `Output directory ${outputPath} is not empty. Please specify an empty directory.`,\n )\n }\n\n // Delete all files in outputPath if `overwriteExistingFiles` is true\n if (config.overwriteExistingFiles === true) {\n await rm(join(outputPath, '*'), { recursive: true, force: true })\n }\n}\n\n/*\n * Copy needed CSS and JS to export path.\n */\nexport async function copyStaticAssets(config: Config, outputPath: string) {\n const viewsFolderPath = getPathToViewsFolder(config)\n\n const foldersToCopy = ['css', 'js', 'img']\n\n for (const folder of foldersToCopy) {\n if (\n await access(join(viewsFolderPath, folder))\n .then(() => true)\n .catch(() => false)\n ) {\n await cp(join(viewsFolderPath, folder), join(outputPath, folder), {\n recursive: true,\n })\n }\n }\n}\n\n/*\n * Render the HTML based on the config.\n */\nexport async function renderFile(\n templateFileName: string,\n templateVars: any,\n config: Config,\n) {\n const templatePath = getTemplatePath(templateFileName, config)\n const html = await pug.renderFile(templatePath, templateVars)\n\n // Beautify HTML if setting is set\n if (config.beautify === true) {\n return beautify.html_beautify(html, { indent_size: 2 })\n }\n\n return html\n}\n","import { clearLine, cursorTo } from 'node:readline'\nimport { noop } from 'lodash-es'\nimport * as colors from 'yoctocolors'\n\nimport { Config } from '../types/global_interfaces.ts'\n\n/*\n * Returns a log function based on config settings\n */\nexport function log(config: Config) {\n if (config.verbose === false) {\n return noop\n }\n\n if (config.logFunction) {\n return config.logFunction\n }\n\n return (text: string, overwrite: boolean) => {\n if (overwrite === true && process.stdout.isTTY) {\n clearLine(process.stdout, 0)\n cursorTo(process.stdout, 0)\n } else {\n process.stdout.write('\\n')\n }\n\n process.stdout.write(text)\n }\n}\n\n/*\n * Returns an warning log function based on config settings\n */\nexport function logWarning(config: Config) {\n if (config.logFunction) {\n return config.logFunction\n }\n\n return (text: string) => {\n process.stdout.write(`\\n${formatWarning(text)}\\n`)\n }\n}\n\n/*\n * Returns an error log function based on config settings\n */\nexport function logError(config: Config) {\n if (config.logFunction) {\n return config.logFunction\n }\n\n return (text: string) => {\n process.stdout.write(`\\n${formatError(text)}\\n`)\n }\n}\n\n/*\n * Format console warning text\n */\nexport function formatWarning(text: string) {\n const warningMessage = `${colors.underline('Warning')}: ${text}`\n return colors.yellow(warningMessage)\n}\n\n/*\n * Format console error text\n */\nexport function formatError(error: any) {\n const messageText = error instanceof Error ? error.message : error\n const errorMessage = `${colors.underline('Error')}: ${messageText.replace(\n 'Error: ',\n '',\n )}`\n return colors.red(errorMessage)\n}\n","import path from 'path'\nimport { clone, omit } from 'lodash-es'\nimport { writeFile } from 'node:fs/promises'\nimport { importGtfs, openDb } from 'gtfs'\nimport sanitize from 'sanitize-filename'\nimport Timer from 'timer-machine'\nimport untildify from 'untildify'\n\nimport { copyStaticAssets, prepDirectory } from './file-utils.ts'\nimport { log, logError } from './log-utils.ts'\nimport {\n generateTransitDeparturesWidgetHtml,\n generateTransitDeparturesWidgetJson,\n setDefaultConfig,\n} from './utils.ts'\nimport { Config } from '../types/global_interfaces.ts'\n\n/*\n * Generate transit departures widget HTML from GTFS.\n */\nasync function transitDeparturesWidget(initialConfig: Config) {\n const config = setDefaultConfig(initialConfig)\n\n try {\n openDb(config)\n } catch (error: any) {\n if (error?.code === 'SQLITE_CANTOPEN') {\n logError(config)(\n `Unable to open sqlite database \"${config.sqlitePath}\" defined as \\`sqlitePath\\` config.json. Ensure the parent directory exists or remove \\`sqlitePath\\` from config.json.`,\n )\n }\n\n throw error\n }\n\n if (!config.agency) {\n throw new Error('No agency defined in `config.json`')\n }\n\n const timer = new Timer()\n const agencyKey = config.agency.agency_key ?? 'unknown'\n\n const outputPath = config.outputPath\n ? untildify(config.outputPath)\n : path.join(process.cwd(), 'html', sanitize(agencyKey))\n\n timer.start()\n\n if (!config.skipImport) {\n // Import GTFS\n const gtfsImportConfig = {\n ...clone(omit(config, 'agency')),\n agencies: [\n {\n agency_key: config.agency.agency_key,\n path: config.agency.gtfs_static_path,\n url: config.agency.gtfs_static_url,\n },\n ],\n }\n\n await importGtfs(gtfsImportConfig)\n }\n\n await prepDirectory(outputPath, config)\n\n if (config.noHead !== true) {\n await copyStaticAssets(config, outputPath)\n }\n\n log(config)(`${agencyKey}: Generating Transit Departures Widget HTML`)\n\n config.assetPath = ''\n\n // Generate JSON of routes and stops\n const { routes, stops } = generateTransitDeparturesWidgetJson(config)\n await writeFile(\n path.join(outputPath, 'data', 'routes.json'),\n JSON.stringify(routes, null, 2),\n )\n await writeFile(\n path.join(outputPath, 'data', 'stops.json'),\n JSON.stringify(stops, null, 2),\n )\n\n const html = await generateTransitDeparturesWidgetHtml(config)\n await writeFile(path.join(outputPath, 'index.html'), html)\n\n timer.stop()\n\n // Print stats\n log(config)(\n `${agencyKey}: Transit Departures Widget HTML created at ${outputPath}`,\n )\n\n const seconds = Math.round(timer.time() / 1000)\n log(config)(`${agencyKey}: HTML generation required ${seconds} seconds`)\n}\n\nexport default transitDeparturesWidget\n","import { fileURLToPath } from 'url'\nimport { dirname, join } from 'path'\nimport { openDb, getDirections, getRoutes, getStops, getTrips } from 'gtfs'\nimport { groupBy, last, maxBy, size, sortBy, uniqBy } from 'lodash-es'\nimport { renderFile } from './file-utils.ts'\nimport sqlString from 'sqlstring-sqlite'\nimport toposort from 'toposort'\nimport i18n from 'i18n'\n\nimport { Config, SqlWhere, SqlValue } from '../types/global_interfaces.ts'\n\n/*\n * Get calendars for a specified date range\n */\nconst getCalendarsForDateRange = (config: Config) => {\n const db = openDb(config)\n let whereClause = ''\n const whereClauses = []\n\n if (config.endDate) {\n whereClauses.push(`start_date <= ${sqlString.escape(config.endDate)}`)\n }\n\n if (config.startDate) {\n whereClauses.push(`end_date >= ${sqlString.escape(config.startDate)}`)\n }\n\n if (whereClauses.length > 0) {\n whereClause = `WHERE ${whereClauses.join(' AND ')}`\n }\n\n return db.prepare(`SELECT * FROM calendar ${whereClause}`).all()\n}\n\n/*\n * Format a route name.\n */\nfunction formatRouteName(route) {\n let routeName = ''\n\n if (route.route_short_name !== null) {\n routeName += route.route_short_name\n }\n\n if (route.route_short_name !== null && route.route_long_name !== null) {\n routeName += ' - '\n }\n\n if (route.route_long_name !== null) {\n routeName += route.route_long_name\n }\n\n return routeName\n}\n\n/*\n * Get directions for a route\n */\nfunction getDirectionsForRoute(route: Record<string, string>, config: Config) {\n const db = openDb(config)\n\n // Lookup direction names from non-standard directions.txt file\n const directions = getDirections({ route_id: route.route_id }, [\n 'direction_id',\n 'direction',\n ])\n\n const calendars = getCalendarsForDateRange(config)\n\n // Else use the most common headsigns as directions from trips.txt file\n if (directions.length === 0) {\n const headsigns = db\n .prepare(\n `SELECT direction_id, trip_headsign, count(*) AS count FROM trips WHERE route_id = ? AND service_id IN (${calendars\n .map((calendar: Record<string, string>) => `'${calendar.service_id}'`)\n .join(', ')}) GROUP BY direction_id, trip_headsign`,\n )\n .all(route.route_id)\n\n for (const group of Object.values(groupBy(headsigns, 'direction_id'))) {\n const mostCommonHeadsign = maxBy(group, 'count')\n directions.push({\n direction_id: mostCommonHeadsign.direction_id,\n direction: i18n.__('To {{{headsign}}}', {\n headsign: mostCommonHeadsign.trip_headsign,\n }),\n })\n }\n }\n\n return directions\n}\n\n/*\n * Sort an array of stoptimes by stop_sequence using a directed graph\n */\nfunction sortStopIdsBySequence(stoptimes: Record<string, string>[]) {\n const stoptimesGroupedByTrip = groupBy(stoptimes, 'trip_id')\n\n // First, try using a directed graph to determine stop order.\n try {\n const stopGraph = []\n\n for (const tripStoptimes of Object.values(stoptimesGroupedByTrip)) {\n const sortedStopIds = sortBy(tripStoptimes, 'stop_sequence').map(\n (stoptime) => stoptime.stop_id,\n )\n\n for (const [index, stopId] of sortedStopIds.entries()) {\n if (index === sortedStopIds.length - 1) {\n continue\n }\n\n stopGraph.push([stopId, sortedStopIds[index + 1]])\n }\n }\n\n return toposort(\n stopGraph as unknown as readonly [string, string | undefined][],\n )\n } catch {\n // Ignore errors and move to next strategy.\n }\n\n // Finally, fall back to using the stop order from the trip with the most stoptimes.\n const longestTripStoptimes = maxBy(\n Object.values(stoptimesGroupedByTrip),\n (stoptimes) => size(stoptimes),\n )\n\n return longestTripStoptimes.map((stoptime) => stoptime.stop_id)\n}\n\n/*\n * Get stops in order for a route and direction\n */\nfunction getStopsForDirection(route, direction, config: Config) {\n const db = openDb(config)\n const calendars = getCalendarsForDateRange(config)\n const whereClause = formatWhereClauses({\n direction_id: direction.direction_id,\n route_id: route.route_id,\n service_id: calendars.map((calendar) => calendar.service_id),\n })\n const stoptimes = db\n .prepare(\n `SELECT stop_id, stop_sequence, trip_id FROM stop_times WHERE trip_id IN (SELECT trip_id FROM trips ${whereClause}) ORDER BY stop_sequence ASC`,\n )\n .all()\n\n const sortedStopIds = sortStopIdsBySequence(stoptimes)\n\n const deduplicatedStopIds = sortedStopIds.reduce(\n (memo: string[], stopId: string) => {\n // Remove duplicated stop_ids in a row\n if (last(memo) !== stopId) {\n memo.push(stopId)\n }\n\n return memo\n },\n [],\n )\n\n // Remove last stop of route since boarding is not allowed\n deduplicatedStopIds.pop()\n\n // Fetch stop details\n\n const stopFields = ['stop_id', 'stop_name', 'stop_code', 'parent_station']\n\n if (config.includeCoordinates) {\n stopFields.push('stop_lat', 'stop_lon')\n }\n\n const stops = getStops({ stop_id: deduplicatedStopIds }, stopFields)\n\n return deduplicatedStopIds.map((stopId: string) =>\n stops.find((stop) => stop.stop_id === stopId),\n )\n}\n\n/*\n * Generate HTML for transit departures widget.\n */\nexport function generateTransitDeparturesWidgetHtml(config: Config) {\n i18n.configure({\n directory: join(dirname(fileURLToPath(import.meta.url)), '../../locales'),\n defaultLocale: config.locale,\n updateFiles: false,\n })\n\n const templateVars = {\n __: i18n.__,\n config,\n }\n return renderFile('widget', templateVars, config)\n}\n\n/*\n * Generate JSON of routes and stops for transit departures widget.\n */\nexport function generateTransitDeparturesWidgetJson(config: Config) {\n const routes = getRoutes()\n const stops = []\n const filteredRoutes = []\n const calendars = getCalendarsForDateRange(config)\n\n for (const route of routes) {\n route.route_full_name = formatRouteName(route)\n\n const directions = getDirectionsForRoute(route, config)\n\n // Filter out routes with no directions\n if (directions.length === 0) {\n logWarning(config)(\n `route_id ${route.route_id} has no directions - skipping`,\n )\n continue\n }\n\n for (const direction of directions) {\n const directionStops = getStopsForDirection(route, direction, config)\n stops.push(...directionStops)\n direction.stopIds = directionStops.map((stop) => stop?.stop_id)\n\n const trips = getTrips(\n {\n route_id: route.route_id,\n direction_id: direction.direction_id,\n service_id: calendars.map(\n (calendar: Record<string, string>) => calendar.service_id,\n ),\n },\n ['trip_id'],\n )\n direction.tripIds = trips.map((trip) => trip.trip_id)\n }\n\n route.directions = directions\n filteredRoutes.push(route)\n }\n\n // Sort routes twice to handle integers with alphabetical characters, such as ['14', '14L', '14X']\n const sortedRoutes = sortBy(\n sortBy(filteredRoutes, (route) => route.route_short_name?.toLowerCase()),\n (route) => Number.parseInt(route.route_short_name, 10),\n )\n\n // Get Parent Station Stops\n const parentStationIds = new Set(stops.map((stop) => stop?.parent_station))\n\n const parentStationStops = getStops(\n { stop_id: Array.from(parentStationIds) },\n ['stop_id', 'stop_name', 'stop_code', 'parent_station'],\n )\n\n stops.push(\n ...parentStationStops.map((stop) => {\n stop.is_parent_station = true\n return stop\n }),\n )\n\n // Sort unique list of stops\n const sortedStops = sortBy(uniqBy(stops, 'stop_id'), 'stop_name')\n\n return {\n routes: removeNulls(sortedRoutes),\n stops: removeNulls(sortedStops),\n }\n}\n\n/*\n * Remove null values from array or object\n */\nfunction removeNulls(data: any): any {\n if (Array.isArray(data)) {\n return data\n .map(removeNulls)\n .filter((item) => item !== null && item !== undefined)\n } else if (typeof data === 'object' && data !== null) {\n return Object.entries(data).reduce((acc, [key, value]) => {\n const cleanedValue = removeNulls(value)\n if (cleanedValue !== null && cleanedValue !== undefined) {\n acc[key] = cleanedValue\n }\n return acc\n }, {})\n } else {\n return data\n }\n}\n\n/*\n * Initialize configuration with defaults.\n */\nexport function setDefaultConfig(initialConfig: Config) {\n const defaults = {\n beautify: false,\n noHead: false,\n refreshIntervalSeconds: 20,\n skipImport: false,\n timeFormat: '12hour',\n includeCoordinates: false,\n overwriteExistingFiles: true,\n verbose: true,\n }\n\n return Object.assign(defaults, initialConfig)\n}\n\nexport function formatWhereClause(\n key: string,\n value: null | SqlValue | SqlValue[],\n) {\n if (Array.isArray(value)) {\n let whereClause = `${sqlString.escapeId(key)} IN (${value\n .filter((v) => v !== null)\n .map((v) => sqlString.escape(v))\n .join(', ')})`\n\n if (value.includes(null)) {\n whereClause = `(${whereClause} OR ${sqlString.escapeId(key)} IS NULL)`\n }\n\n return whereClause\n }\n\n if (value === null) {\n return `${sqlString.escapeId(key)} IS NULL`\n }\n\n return `${sqlString.escapeId(key)} = ${sqlString.escape(value)}`\n}\n\nexport function formatWhereClauses(query: SqlWhere) {\n if (Object.keys(query).length === 0) {\n return ''\n }\n\n const whereClauses = Object.entries(query).map(([key, value]) =>\n formatWhereClause(key, value),\n )\n return `WHERE ${whereClauses.join(' AND ')}`\n}\n"],"mappings":"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","names":["argv","error","openDb","untildify","fileURLToPath","dirname","join","stoptimes","join","dirname","fileURLToPath","openDb","untildify"]}
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- interface IConfig {
1
+ interface Config {
2
2
  agency: {
3
3
  agency_key: string;
4
4
  gtfs_static_path?: string;
@@ -14,16 +14,17 @@ interface IConfig {
14
14
  locale?: string;
15
15
  includeCoordinates?: boolean;
16
16
  noHead?: boolean;
17
+ outputPath?: string;
18
+ overwriteExistingFiles?: boolean;
17
19
  refreshIntervalSeconds?: number;
18
20
  skipImport?: boolean;
19
21
  sqlitePath?: string;
20
22
  templatePath?: string;
21
23
  timeFormat?: string;
22
- log: (text: string) => void;
23
- logWarning: (text: string) => void;
24
- logError: (text: string) => void;
24
+ verbose?: boolean;
25
+ logFunction?: (text: string) => void;
25
26
  }
26
27
 
27
- declare function transitDeparturesWidget(initialConfig: IConfig): Promise<void>;
28
+ declare function transitDeparturesWidget(initialConfig: Config): Promise<void>;
28
29
 
29
30
  export { transitDeparturesWidget as default };
package/dist/index.js CHANGED
@@ -1,57 +1,84 @@
1
1
  // src/lib/transit-departures-widget.ts
2
- import path2 from "path";
2
+ import path from "path";
3
3
  import { clone, omit } from "lodash-es";
4
4
  import { writeFile } from "node:fs/promises";
5
5
  import { importGtfs, openDb as openDb2 } from "gtfs";
6
6
  import sanitize from "sanitize-filename";
7
7
  import Timer from "timer-machine";
8
+ import untildify2 from "untildify";
8
9
 
9
10
  // src/lib/file-utils.ts
10
- import path from "path";
11
+ import { dirname, join, resolve } from "node:path";
11
12
  import { fileURLToPath } from "node:url";
12
- import { readFile, rm, mkdir } from "node:fs/promises";
13
- import copydir from "copy-dir";
13
+ import { access, cp, mkdir, readdir, readFile, rm } from "node:fs/promises";
14
14
  import beautify from "js-beautify";
15
15
  import pug from "pug";
16
16
  import untildify from "untildify";
17
+ function getPathToViewsFolder(config) {
18
+ if (config.templatePath) {
19
+ return untildify(config.templatePath);
20
+ }
21
+ const __dirname = dirname(fileURLToPath(import.meta.url));
22
+ let viewsFolderPath;
23
+ if (__dirname.endsWith("/dist/bin") || __dirname.endsWith("/dist/app")) {
24
+ viewsFolderPath = resolve(__dirname, "../../views/widget");
25
+ } else if (__dirname.endsWith("/dist")) {
26
+ viewsFolderPath = resolve(__dirname, "../views/widget");
27
+ } else {
28
+ viewsFolderPath = resolve(__dirname, "views/widget");
29
+ }
30
+ return viewsFolderPath;
31
+ }
17
32
  function getTemplatePath(templateFileName, config) {
18
33
  let fullTemplateFileName = templateFileName;
19
34
  if (config.noHead !== true) {
20
35
  fullTemplateFileName += "_full";
21
36
  }
22
37
  if (config.templatePath !== void 0) {
23
- return path.join(
24
- untildify(config.templatePath),
25
- `${fullTemplateFileName}.pug`
26
- );
38
+ return join(untildify(config.templatePath), `${fullTemplateFileName}.pug`);
27
39
  }
28
- return path.join(
40
+ return join(
29
41
  fileURLToPath(import.meta.url),
30
42
  "../../../views/widget",
31
43
  `${fullTemplateFileName}.pug`
32
44
  );
33
45
  }
34
- async function prepDirectory(exportPath) {
35
- await rm(exportPath, { recursive: true, force: true });
46
+ async function prepDirectory(outputPath, config) {
36
47
  try {
37
- await mkdir(exportPath, { recursive: true });
48
+ await access(outputPath);
38
49
  } catch (error) {
39
- if (error?.code === "ENOENT") {
40
- throw new Error(
41
- `Unable to write to ${exportPath}. Try running this command from a writable directory.`
42
- );
50
+ try {
51
+ await mkdir(outputPath, { recursive: true });
52
+ await mkdir(join(outputPath, "data"));
53
+ } catch (error2) {
54
+ if (error2?.code === "ENOENT") {
55
+ throw new Error(
56
+ `Unable to write to ${outputPath}. Try running this command from a writable directory.`
57
+ );
58
+ }
59
+ throw error2;
43
60
  }
44
- throw error;
61
+ }
62
+ const files = await readdir(outputPath);
63
+ if (config.overwriteExistingFiles === false && files.length > 0) {
64
+ throw new Error(
65
+ `Output directory ${outputPath} is not empty. Please specify an empty directory.`
66
+ );
67
+ }
68
+ if (config.overwriteExistingFiles === true) {
69
+ await rm(join(outputPath, "*"), { recursive: true, force: true });
45
70
  }
46
71
  }
47
- function copyStaticAssets(exportPath) {
48
- const staticAssetPath = path.join(
49
- fileURLToPath(import.meta.url),
50
- "../../../public"
51
- );
52
- copydir.sync(path.join(staticAssetPath, "img"), path.join(exportPath, "img"));
53
- copydir.sync(path.join(staticAssetPath, "css"), path.join(exportPath, "css"));
54
- copydir.sync(path.join(staticAssetPath, "js"), path.join(exportPath, "js"));
72
+ async function copyStaticAssets(config, outputPath) {
73
+ const viewsFolderPath = getPathToViewsFolder(config);
74
+ const foldersToCopy = ["css", "js", "img"];
75
+ for (const folder of foldersToCopy) {
76
+ if (await access(join(viewsFolderPath, folder)).then(() => true).catch(() => false)) {
77
+ await cp(join(viewsFolderPath, folder), join(outputPath, folder), {
78
+ recursive: true
79
+ });
80
+ }
81
+ }
55
82
  }
56
83
  async function renderFile(templateFileName, templateVars, config) {
57
84
  const templatePath = getTemplatePath(templateFileName, config);
@@ -63,7 +90,7 @@ async function renderFile(templateFileName, templateVars, config) {
63
90
  }
64
91
 
65
92
  // src/lib/log-utils.ts
66
- import readline from "readline";
93
+ import { clearLine, cursorTo } from "node:readline";
67
94
  import { noop } from "lodash-es";
68
95
  import * as colors from "yoctocolors";
69
96
  function log(config) {
@@ -74,25 +101,15 @@ function log(config) {
74
101
  return config.logFunction;
75
102
  }
76
103
  return (text, overwrite) => {
77
- if (overwrite === true) {
78
- readline.clearLine(process.stdout, 0);
79
- readline.cursorTo(process.stdout, 0);
104
+ if (overwrite === true && process.stdout.isTTY) {
105
+ clearLine(process.stdout, 0);
106
+ cursorTo(process.stdout, 0);
80
107
  } else {
81
108
  process.stdout.write("\n");
82
109
  }
83
110
  process.stdout.write(text);
84
111
  };
85
112
  }
86
- function logWarning(config) {
87
- if (config.logFunction) {
88
- return config.logFunction;
89
- }
90
- return (text) => {
91
- process.stdout.write(`
92
- ${formatWarning(text)}
93
- `);
94
- };
95
- }
96
113
  function logError(config) {
97
114
  if (config.logFunction) {
98
115
  return config.logFunction;
@@ -103,10 +120,6 @@ ${formatError(text)}
103
120
  `);
104
121
  };
105
122
  }
106
- function formatWarning(text) {
107
- const warningMessage = `${colors.underline("Warning")}: ${text}`;
108
- return colors.yellow(warningMessage);
109
- }
110
123
  function formatError(error) {
111
124
  const messageText = error instanceof Error ? error.message : error;
112
125
  const errorMessage = `${colors.underline("Error")}: ${messageText.replace(
@@ -118,7 +131,7 @@ function formatError(error) {
118
131
 
119
132
  // src/lib/utils.ts
120
133
  import { fileURLToPath as fileURLToPath2 } from "url";
121
- import { dirname, join } from "path";
134
+ import { dirname as dirname2, join as join2 } from "path";
122
135
  import { openDb, getDirections, getRoutes, getStops, getTrips } from "gtfs";
123
136
  import { groupBy, last, maxBy, size, sortBy, uniqBy } from "lodash-es";
124
137
  import sqlString from "sqlstring-sqlite";
@@ -223,12 +236,7 @@ function getStopsForDirection(route, direction, config) {
223
236
  []
224
237
  );
225
238
  deduplicatedStopIds.pop();
226
- const stopFields = [
227
- "stop_id",
228
- "stop_name",
229
- "stop_code",
230
- "parent_station"
231
- ];
239
+ const stopFields = ["stop_id", "stop_name", "stop_code", "parent_station"];
232
240
  if (config.includeCoordinates) {
233
241
  stopFields.push("stop_lat", "stop_lon");
234
242
  }
@@ -239,7 +247,7 @@ function getStopsForDirection(route, direction, config) {
239
247
  }
240
248
  function generateTransitDeparturesWidgetHtml(config) {
241
249
  i18n.configure({
242
- directory: join(dirname(fileURLToPath2(import.meta.url)), "../../locales"),
250
+ directory: join2(dirname2(fileURLToPath2(import.meta.url)), "../../locales"),
243
251
  defaultLocale: config.locale,
244
252
  updateFiles: false
245
253
  });
@@ -258,7 +266,7 @@ function generateTransitDeparturesWidgetJson(config) {
258
266
  route.route_full_name = formatRouteName(route);
259
267
  const directions = getDirectionsForRoute(route, config);
260
268
  if (directions.length === 0) {
261
- config.logWarning(
269
+ logWarning(config)(
262
270
  `route_id ${route.route_id} has no directions - skipping`
263
271
  );
264
272
  continue;
@@ -325,7 +333,9 @@ function setDefaultConfig(initialConfig) {
325
333
  refreshIntervalSeconds: 20,
326
334
  skipImport: false,
327
335
  timeFormat: "12hour",
328
- includeCoordinates: false
336
+ includeCoordinates: false,
337
+ overwriteExistingFiles: true,
338
+ verbose: true
329
339
  };
330
340
  return Object.assign(defaults, initialConfig);
331
341
  }
@@ -355,14 +365,11 @@ function formatWhereClauses(query) {
355
365
  // src/lib/transit-departures-widget.ts
356
366
  async function transitDeparturesWidget(initialConfig) {
357
367
  const config = setDefaultConfig(initialConfig);
358
- config.log = log(config);
359
- config.logWarning = logWarning(config);
360
- config.logError = logError(config);
361
368
  try {
362
369
  openDb2(config);
363
370
  } catch (error) {
364
371
  if (error?.code === "SQLITE_CANTOPEN") {
365
- config.logError(
372
+ logError(config)(
366
373
  `Unable to open sqlite database "${config.sqlitePath}" defined as \`sqlitePath\` config.json. Ensure the parent directory exists or remove \`sqlitePath\` from config.json.`
367
374
  );
368
375
  }
@@ -372,8 +379,8 @@ async function transitDeparturesWidget(initialConfig) {
372
379
  throw new Error("No agency defined in `config.json`");
373
380
  }
374
381
  const timer = new Timer();
375
- const agencyKey = config.agency.agency_key;
376
- const exportPath = path2.join(process.cwd(), "html", sanitize(agencyKey));
382
+ const agencyKey = config.agency.agency_key ?? "unknown";
383
+ const outputPath = config.outputPath ? untildify2(config.outputPath) : path.join(process.cwd(), "html", sanitize(agencyKey));
377
384
  timer.start();
378
385
  if (!config.skipImport) {
379
386
  const gtfsImportConfig = {
@@ -388,30 +395,29 @@ async function transitDeparturesWidget(initialConfig) {
388
395
  };
389
396
  await importGtfs(gtfsImportConfig);
390
397
  }
391
- await prepDirectory(exportPath);
392
- await prepDirectory(path2.join(exportPath, "data"));
398
+ await prepDirectory(outputPath, config);
393
399
  if (config.noHead !== true) {
394
- copyStaticAssets(exportPath);
400
+ await copyStaticAssets(config, outputPath);
395
401
  }
396
- config.log(`${agencyKey}: Generating Transit Departures Widget HTML`);
402
+ log(config)(`${agencyKey}: Generating Transit Departures Widget HTML`);
397
403
  config.assetPath = "";
398
404
  const { routes, stops } = generateTransitDeparturesWidgetJson(config);
399
405
  await writeFile(
400
- path2.join(exportPath, "data", "routes.json"),
406
+ path.join(outputPath, "data", "routes.json"),
401
407
  JSON.stringify(routes, null, 2)
402
408
  );
403
409
  await writeFile(
404
- path2.join(exportPath, "data", "stops.json"),
410
+ path.join(outputPath, "data", "stops.json"),
405
411
  JSON.stringify(stops, null, 2)
406
412
  );
407
413
  const html = await generateTransitDeparturesWidgetHtml(config);
408
- await writeFile(path2.join(exportPath, "index.html"), html);
414
+ await writeFile(path.join(outputPath, "index.html"), html);
409
415
  timer.stop();
410
- config.log(
411
- `${agencyKey}: Transit Departures Widget HTML created at ${exportPath}`
416
+ log(config)(
417
+ `${agencyKey}: Transit Departures Widget HTML created at ${outputPath}`
412
418
  );
413
419
  const seconds = Math.round(timer.time() / 1e3);
414
- config.log(`${agencyKey}: HTML generation required ${seconds} seconds`);
420
+ log(config)(`${agencyKey}: HTML generation required ${seconds} seconds`);
415
421
  }
416
422
  var transit_departures_widget_default = transitDeparturesWidget;
417
423
  export {
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/lib/transit-departures-widget.ts","../src/lib/file-utils.ts","../src/lib/log-utils.ts","../src/lib/utils.ts"],"sourcesContent":["import path from 'path'\nimport { clone, omit } from 'lodash-es'\nimport { writeFile } from 'node:fs/promises'\nimport { importGtfs, openDb } from 'gtfs'\nimport sanitize from 'sanitize-filename'\nimport Timer from 'timer-machine'\n\nimport { copyStaticAssets, prepDirectory } from './file-utils.ts'\nimport { log, logWarning, logError } from './log-utils.ts'\nimport {\n generateTransitDeparturesWidgetHtml,\n generateTransitDeparturesWidgetJson,\n setDefaultConfig,\n} from './utils.ts'\nimport { IConfig } from '../types/global_interfaces.ts'\n\n/*\n * Generate transit departures widget HTML from GTFS.\n */\nasync function transitDeparturesWidget(initialConfig: IConfig) {\n const config = setDefaultConfig(initialConfig)\n config.log = log(config)\n config.logWarning = logWarning(config)\n config.logError = logError(config)\n\n try {\n openDb(config)\n } catch (error: any) {\n if (error?.code === 'SQLITE_CANTOPEN') {\n config.logError(\n `Unable to open sqlite database \"${config.sqlitePath}\" defined as \\`sqlitePath\\` config.json. Ensure the parent directory exists or remove \\`sqlitePath\\` from config.json.`,\n )\n }\n\n throw error\n }\n\n if (!config.agency) {\n throw new Error('No agency defined in `config.json`')\n }\n\n const timer = new Timer()\n const agencyKey = config.agency.agency_key\n const exportPath = path.join(process.cwd(), 'html', sanitize(agencyKey))\n\n timer.start()\n\n if (!config.skipImport) {\n // Import GTFS\n const gtfsImportConfig = {\n ...clone(omit(config, 'agency')),\n agencies: [\n {\n agency_key: config.agency.agency_key,\n path: config.agency.gtfs_static_path,\n url: config.agency.gtfs_static_url,\n },\n ],\n }\n\n await importGtfs(gtfsImportConfig)\n }\n\n await prepDirectory(exportPath)\n await prepDirectory(path.join(exportPath, 'data'))\n\n if (config.noHead !== true) {\n copyStaticAssets(exportPath)\n }\n\n config.log(`${agencyKey}: Generating Transit Departures Widget HTML`)\n\n config.assetPath = ''\n\n // Generate JSON of routes and stops\n const { routes, stops } = generateTransitDeparturesWidgetJson(config)\n await writeFile(\n path.join(exportPath, 'data', 'routes.json'),\n JSON.stringify(routes, null, 2),\n )\n await writeFile(\n path.join(exportPath, 'data', 'stops.json'),\n JSON.stringify(stops, null, 2),\n )\n\n const html = await generateTransitDeparturesWidgetHtml(config)\n await writeFile(path.join(exportPath, 'index.html'), html)\n\n timer.stop()\n\n // Print stats\n config.log(\n `${agencyKey}: Transit Departures Widget HTML created at ${exportPath}`,\n )\n\n const seconds = Math.round(timer.time() / 1000)\n config.log(`${agencyKey}: HTML generation required ${seconds} seconds`)\n}\n\nexport default transitDeparturesWidget\n","import path from 'path'\nimport { fileURLToPath } from 'node:url'\nimport { readFile, rm, mkdir } from 'node:fs/promises'\nimport copydir from 'copy-dir'\nimport beautify from 'js-beautify'\nimport pug from 'pug'\nimport untildify from 'untildify'\n\nimport { IConfig } from '../types/global_interfaces.ts'\n\n/*\n * Attempt to parse the specified config JSON file.\n */\nexport async function getConfig(argv) {\n try {\n const data = await readFile(\n path.resolve(untildify(argv.configPath)),\n 'utf8',\n ).catch((error) => {\n console.error(\n new Error(\n `Cannot find configuration file at \\`${argv.configPath}\\`. Use config-sample.json as a starting point, pass --configPath option`,\n ),\n )\n throw error\n })\n const config = JSON.parse(data)\n\n if (argv.skipImport === true) {\n config.skipImport = argv.skipImport\n }\n\n return config\n } catch (error) {\n console.error(\n new Error(\n `Cannot parse configuration file at \\`${argv.configPath}\\`. Check to ensure that it is valid JSON.`,\n ),\n )\n throw error\n }\n}\n\n/*\n * Get the full path of the template file for generating transit departures widget based on\n * config.\n */\nfunction getTemplatePath(templateFileName, config) {\n let fullTemplateFileName = templateFileName\n if (config.noHead !== true) {\n fullTemplateFileName += '_full'\n }\n\n if (config.templatePath !== undefined) {\n return path.join(\n untildify(config.templatePath),\n `${fullTemplateFileName}.pug`,\n )\n }\n\n return path.join(\n fileURLToPath(import.meta.url),\n '../../../views/widget',\n `${fullTemplateFileName}.pug`,\n )\n}\n\n/*\n * Prepare the specified directory for saving HTML widget by deleting everything.\n */\nexport async function prepDirectory(exportPath: string) {\n await rm(exportPath, { recursive: true, force: true })\n try {\n await mkdir(exportPath, { recursive: true })\n } catch (error: any) {\n if (error?.code === 'ENOENT') {\n throw new Error(\n `Unable to write to ${exportPath}. Try running this command from a writable directory.`,\n )\n }\n\n throw error\n }\n}\n\n/*\n * Copy needed CSS and JS to export path.\n */\nexport function copyStaticAssets(exportPath: string) {\n const staticAssetPath = path.join(\n fileURLToPath(import.meta.url),\n '../../../public',\n )\n copydir.sync(path.join(staticAssetPath, 'img'), path.join(exportPath, 'img'))\n copydir.sync(path.join(staticAssetPath, 'css'), path.join(exportPath, 'css'))\n copydir.sync(path.join(staticAssetPath, 'js'), path.join(exportPath, 'js'))\n}\n\n/*\n * Render the HTML based on the config.\n */\nexport async function renderFile(\n templateFileName: string,\n templateVars: any,\n config: IConfig,\n) {\n const templatePath = getTemplatePath(templateFileName, config)\n const html = await pug.renderFile(templatePath, templateVars)\n\n // Beautify HTML if setting is set\n if (config.beautify === true) {\n return beautify.html_beautify(html, { indent_size: 2 })\n }\n\n return html\n}\n","import readline from 'readline'\nimport { noop } from 'lodash-es'\nimport * as colors from 'yoctocolors'\n\nimport { IConfig } from '../types/global_interfaces.ts'\n\n/*\n * Returns a log function based on config settings\n */\nexport function log(config: IConfig) {\n if (config.verbose === false) {\n return noop\n }\n\n if (config.logFunction) {\n return config.logFunction\n }\n\n return (text: string, overwrite: boolean) => {\n if (overwrite === true) {\n readline.clearLine(process.stdout, 0)\n readline.cursorTo(process.stdout, 0)\n } else {\n process.stdout.write('\\n')\n }\n\n process.stdout.write(text)\n }\n}\n\n/*\n * Returns an warning log function based on config settings\n */\nexport function logWarning(config: IConfig) {\n if (config.logFunction) {\n return config.logFunction\n }\n\n return (text: string) => {\n process.stdout.write(`\\n${formatWarning(text)}\\n`)\n }\n}\n\n/*\n * Returns an error log function based on config settings\n */\nexport function logError(config: IConfig) {\n if (config.logFunction) {\n return config.logFunction\n }\n\n return (text: string) => {\n process.stdout.write(`\\n${formatError(text)}\\n`)\n }\n}\n\n/*\n * Format console warning text\n */\nexport function formatWarning(text: string) {\n const warningMessage = `${colors.underline('Warning')}: ${text}`\n return colors.yellow(warningMessage)\n}\n\n/*\n * Format console error text\n */\nexport function formatError(error: any) {\n const messageText = error instanceof Error ? error.message : error\n const errorMessage = `${colors.underline('Error')}: ${messageText.replace(\n 'Error: ',\n '',\n )}`\n return colors.red(errorMessage)\n}\n","import { fileURLToPath } from 'url'\nimport { dirname, join } from 'path'\nimport { openDb, getDirections, getRoutes, getStops, getTrips } from 'gtfs'\nimport { groupBy, last, maxBy, size, sortBy, uniqBy } from 'lodash-es'\nimport { renderFile } from './file-utils.ts'\nimport sqlString from 'sqlstring-sqlite'\nimport toposort from 'toposort'\nimport i18n from 'i18n'\n\nimport { IConfig, SqlWhere, SqlValue } from '../types/global_interfaces.ts'\n\n/*\n * Get calendars for a specified date range\n */\nconst getCalendarsForDateRange = (config: IConfig) => {\n const db = openDb(config)\n let whereClause = ''\n const whereClauses = []\n\n if (config.endDate) {\n whereClauses.push(`start_date <= ${sqlString.escape(config.endDate)}`)\n }\n\n if (config.startDate) {\n whereClauses.push(`end_date >= ${sqlString.escape(config.startDate)}`)\n }\n\n if (whereClauses.length > 0) {\n whereClause = `WHERE ${whereClauses.join(' AND ')}`\n }\n\n return db.prepare(`SELECT * FROM calendar ${whereClause}`).all()\n}\n\n/*\n * Format a route name.\n */\nfunction formatRouteName(route) {\n let routeName = ''\n\n if (route.route_short_name !== null) {\n routeName += route.route_short_name\n }\n\n if (route.route_short_name !== null && route.route_long_name !== null) {\n routeName += ' - '\n }\n\n if (route.route_long_name !== null) {\n routeName += route.route_long_name\n }\n\n return routeName\n}\n\n/*\n * Get directions for a route\n */\nfunction getDirectionsForRoute(route: Record<string, string>, config: IConfig) {\n const db = openDb(config)\n\n // Lookup direction names from non-standard directions.txt file\n const directions = getDirections({ route_id: route.route_id }, [\n 'direction_id',\n 'direction',\n ])\n\n const calendars = getCalendarsForDateRange(config)\n\n // Else use the most common headsigns as directions from trips.txt file\n if (directions.length === 0) {\n const headsigns = db\n .prepare(\n `SELECT direction_id, trip_headsign, count(*) AS count FROM trips WHERE route_id = ? AND service_id IN (${calendars\n .map((calendar: Record<string, string>) => `'${calendar.service_id}'`)\n .join(', ')}) GROUP BY direction_id, trip_headsign`,\n )\n .all(route.route_id)\n\n for (const group of Object.values(groupBy(headsigns, 'direction_id'))) {\n const mostCommonHeadsign = maxBy(group, 'count')\n directions.push({\n direction_id: mostCommonHeadsign.direction_id,\n direction: i18n.__('To {{{headsign}}}', {\n headsign: mostCommonHeadsign.trip_headsign,\n }),\n })\n }\n }\n\n return directions\n}\n\n/*\n * Sort an array of stoptimes by stop_sequence using a directed graph\n */\nfunction sortStopIdsBySequence(stoptimes: Record<string, string>[]) {\n const stoptimesGroupedByTrip = groupBy(stoptimes, 'trip_id')\n\n // First, try using a directed graph to determine stop order.\n try {\n const stopGraph = []\n\n for (const tripStoptimes of Object.values(stoptimesGroupedByTrip)) {\n const sortedStopIds = sortBy(tripStoptimes, 'stop_sequence').map(\n (stoptime) => stoptime.stop_id,\n )\n\n for (const [index, stopId] of sortedStopIds.entries()) {\n if (index === sortedStopIds.length - 1) {\n continue\n }\n\n stopGraph.push([stopId, sortedStopIds[index + 1]])\n }\n }\n\n return toposort(\n stopGraph as unknown as readonly [string, string | undefined][],\n )\n } catch {\n // Ignore errors and move to next strategy.\n }\n\n // Finally, fall back to using the stop order from the trip with the most stoptimes.\n const longestTripStoptimes = maxBy(\n Object.values(stoptimesGroupedByTrip),\n (stoptimes) => size(stoptimes),\n )\n\n return longestTripStoptimes.map((stoptime) => stoptime.stop_id)\n}\n\n/*\n * Get stops in order for a route and direction\n */\nfunction getStopsForDirection(route, direction, config: IConfig) {\n const db = openDb(config)\n const calendars = getCalendarsForDateRange(config)\n const whereClause = formatWhereClauses({\n direction_id: direction.direction_id,\n route_id: route.route_id,\n service_id: calendars.map((calendar) => calendar.service_id),\n })\n const stoptimes = db\n .prepare(\n `SELECT stop_id, stop_sequence, trip_id FROM stop_times WHERE trip_id IN (SELECT trip_id FROM trips ${whereClause}) ORDER BY stop_sequence ASC`,\n )\n .all()\n\n const sortedStopIds = sortStopIdsBySequence(stoptimes)\n\n const deduplicatedStopIds = sortedStopIds.reduce(\n (memo: string[], stopId: string) => {\n // Remove duplicated stop_ids in a row\n if (last(memo) !== stopId) {\n memo.push(stopId)\n }\n\n return memo\n },\n [],\n )\n\n // Remove last stop of route since boarding is not allowed\n deduplicatedStopIds.pop()\n\n // Fetch stop details\n\n const stopFields = [\n 'stop_id',\n 'stop_name',\n 'stop_code',\n 'parent_station',\n ]\n\n if (config.includeCoordinates) {\n stopFields.push('stop_lat', 'stop_lon')\n }\n\n const stops = getStops({ stop_id: deduplicatedStopIds }, stopFields)\n\n return deduplicatedStopIds.map((stopId: string) =>\n stops.find((stop) => stop.stop_id === stopId),\n )\n}\n\n/*\n * Generate HTML for transit departures widget.\n */\nexport function generateTransitDeparturesWidgetHtml(config: IConfig) {\n i18n.configure({\n directory: join(dirname(fileURLToPath(import.meta.url)), '../../locales'),\n defaultLocale: config.locale,\n updateFiles: false,\n })\n\n const templateVars = {\n __: i18n.__,\n config,\n }\n return renderFile('widget', templateVars, config)\n}\n\n/*\n * Generate JSON of routes and stops for transit departures widget.\n */\nexport function generateTransitDeparturesWidgetJson(config: IConfig) {\n const routes = getRoutes()\n const stops = []\n const filteredRoutes = []\n const calendars = getCalendarsForDateRange(config)\n\n for (const route of routes) {\n route.route_full_name = formatRouteName(route)\n\n const directions = getDirectionsForRoute(route, config)\n\n // Filter out routes with no directions\n if (directions.length === 0) {\n config.logWarning(\n `route_id ${route.route_id} has no directions - skipping`,\n )\n continue\n }\n\n for (const direction of directions) {\n const directionStops = getStopsForDirection(route, direction, config)\n stops.push(...directionStops)\n direction.stopIds = directionStops.map((stop) => stop?.stop_id)\n\n const trips = getTrips(\n {\n route_id: route.route_id,\n direction_id: direction.direction_id,\n service_id: calendars.map(\n (calendar: Record<string, string>) => calendar.service_id,\n ),\n },\n ['trip_id'],\n )\n direction.tripIds = trips.map((trip) => trip.trip_id)\n }\n\n route.directions = directions\n filteredRoutes.push(route)\n }\n\n // Sort routes twice to handle integers with alphabetical characters, such as ['14', '14L', '14X']\n const sortedRoutes = sortBy(\n sortBy(filteredRoutes, (route) => route.route_short_name?.toLowerCase()),\n (route) => Number.parseInt(route.route_short_name, 10),\n )\n\n // Get Parent Station Stops\n const parentStationIds = new Set(stops.map((stop) => stop?.parent_station))\n\n const parentStationStops = getStops(\n { stop_id: Array.from(parentStationIds) },\n ['stop_id', 'stop_name', 'stop_code', 'parent_station'],\n )\n\n stops.push(\n ...parentStationStops.map((stop) => {\n stop.is_parent_station = true\n return stop\n }),\n )\n\n // Sort unique list of stops\n const sortedStops = sortBy(uniqBy(stops, 'stop_id'), 'stop_name')\n\n return {\n routes: removeNulls(sortedRoutes),\n stops: removeNulls(sortedStops),\n }\n}\n\n/*\n * Remove null values from array or object\n */\nfunction removeNulls(data: any): any {\n if (Array.isArray(data)) {\n return data\n .map(removeNulls)\n .filter((item) => item !== null && item !== undefined)\n } else if (typeof data === 'object' && data !== null) {\n return Object.entries(data).reduce((acc, [key, value]) => {\n const cleanedValue = removeNulls(value)\n if (cleanedValue !== null && cleanedValue !== undefined) {\n acc[key] = cleanedValue\n }\n return acc\n }, {})\n } else {\n return data\n }\n}\n\n/*\n * Initialize configuration with defaults.\n */\nexport function setDefaultConfig(initialConfig: IConfig) {\n const defaults = {\n beautify: false,\n noHead: false,\n refreshIntervalSeconds: 20,\n skipImport: false,\n timeFormat: '12hour',\n includeCoordinates: false,\n }\n\n return Object.assign(defaults, initialConfig)\n}\n\nexport function formatWhereClause(\n key: string,\n value: null | SqlValue | SqlValue[],\n) {\n if (Array.isArray(value)) {\n let whereClause = `${sqlString.escapeId(key)} IN (${value\n .filter((v) => v !== null)\n .map((v) => sqlString.escape(v))\n .join(', ')})`\n\n if (value.includes(null)) {\n whereClause = `(${whereClause} OR ${sqlString.escapeId(key)} IS NULL)`\n }\n\n return whereClause\n }\n\n if (value === null) {\n return `${sqlString.escapeId(key)} IS NULL`\n }\n\n return `${sqlString.escapeId(key)} = ${sqlString.escape(value)}`\n}\n\nexport function formatWhereClauses(query: SqlWhere) {\n if (Object.keys(query).length === 0) {\n return ''\n }\n\n const whereClauses = Object.entries(query).map(([key, value]) =>\n formatWhereClause(key, value),\n )\n return `WHERE ${whereClauses.join(' AND ')}`\n}\n"],"mappings":"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","names":["path","openDb","fileURLToPath","stoptimes","fileURLToPath","openDb","path"]}
1
+ {"version":3,"sources":["../src/lib/transit-departures-widget.ts","../src/lib/file-utils.ts","../src/lib/log-utils.ts","../src/lib/utils.ts"],"sourcesContent":["import path from 'path'\nimport { clone, omit } from 'lodash-es'\nimport { writeFile } from 'node:fs/promises'\nimport { importGtfs, openDb } from 'gtfs'\nimport sanitize from 'sanitize-filename'\nimport Timer from 'timer-machine'\nimport untildify from 'untildify'\n\nimport { copyStaticAssets, prepDirectory } from './file-utils.ts'\nimport { log, logError } from './log-utils.ts'\nimport {\n generateTransitDeparturesWidgetHtml,\n generateTransitDeparturesWidgetJson,\n setDefaultConfig,\n} from './utils.ts'\nimport { Config } from '../types/global_interfaces.ts'\n\n/*\n * Generate transit departures widget HTML from GTFS.\n */\nasync function transitDeparturesWidget(initialConfig: Config) {\n const config = setDefaultConfig(initialConfig)\n\n try {\n openDb(config)\n } catch (error: any) {\n if (error?.code === 'SQLITE_CANTOPEN') {\n logError(config)(\n `Unable to open sqlite database \"${config.sqlitePath}\" defined as \\`sqlitePath\\` config.json. Ensure the parent directory exists or remove \\`sqlitePath\\` from config.json.`,\n )\n }\n\n throw error\n }\n\n if (!config.agency) {\n throw new Error('No agency defined in `config.json`')\n }\n\n const timer = new Timer()\n const agencyKey = config.agency.agency_key ?? 'unknown'\n\n const outputPath = config.outputPath\n ? untildify(config.outputPath)\n : path.join(process.cwd(), 'html', sanitize(agencyKey))\n\n timer.start()\n\n if (!config.skipImport) {\n // Import GTFS\n const gtfsImportConfig = {\n ...clone(omit(config, 'agency')),\n agencies: [\n {\n agency_key: config.agency.agency_key,\n path: config.agency.gtfs_static_path,\n url: config.agency.gtfs_static_url,\n },\n ],\n }\n\n await importGtfs(gtfsImportConfig)\n }\n\n await prepDirectory(outputPath, config)\n\n if (config.noHead !== true) {\n await copyStaticAssets(config, outputPath)\n }\n\n log(config)(`${agencyKey}: Generating Transit Departures Widget HTML`)\n\n config.assetPath = ''\n\n // Generate JSON of routes and stops\n const { routes, stops } = generateTransitDeparturesWidgetJson(config)\n await writeFile(\n path.join(outputPath, 'data', 'routes.json'),\n JSON.stringify(routes, null, 2),\n )\n await writeFile(\n path.join(outputPath, 'data', 'stops.json'),\n JSON.stringify(stops, null, 2),\n )\n\n const html = await generateTransitDeparturesWidgetHtml(config)\n await writeFile(path.join(outputPath, 'index.html'), html)\n\n timer.stop()\n\n // Print stats\n log(config)(\n `${agencyKey}: Transit Departures Widget HTML created at ${outputPath}`,\n )\n\n const seconds = Math.round(timer.time() / 1000)\n log(config)(`${agencyKey}: HTML generation required ${seconds} seconds`)\n}\n\nexport default transitDeparturesWidget\n","import { dirname, join, resolve } from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport { access, cp, mkdir, readdir, readFile, rm } from 'node:fs/promises'\nimport beautify from 'js-beautify'\nimport pug from 'pug'\nimport untildify from 'untildify'\n\nimport { Config } from '../types/global_interfaces.ts'\n\n/*\n * Attempt to parse the specified config JSON file.\n */\nexport async function getConfig(argv) {\n try {\n const data = await readFile(\n resolve(untildify(argv.configPath)),\n 'utf8',\n ).catch((error) => {\n console.error(\n new Error(\n `Cannot find configuration file at \\`${argv.configPath}\\`. Use config-sample.json as a starting point, pass --configPath option`,\n ),\n )\n throw error\n })\n const config = JSON.parse(data)\n\n if (argv.skipImport === true) {\n config.skipImport = argv.skipImport\n }\n\n return config\n } catch (error) {\n console.error(\n new Error(\n `Cannot parse configuration file at \\`${argv.configPath}\\`. Check to ensure that it is valid JSON.`,\n ),\n )\n throw error\n }\n}\n\n/*\n * Get the full path to the views folder.\n */\nexport function getPathToViewsFolder(config: Config) {\n if (config.templatePath) {\n return untildify(config.templatePath)\n }\n\n const __dirname = dirname(fileURLToPath(import.meta.url))\n\n // Dynamically calculate the path to the views directory\n let viewsFolderPath\n if (__dirname.endsWith('/dist/bin') || __dirname.endsWith('/dist/app')) {\n // When the file is in 'dist/bin' or 'dist/app'\n viewsFolderPath = resolve(__dirname, '../../views/widget')\n } else if (__dirname.endsWith('/dist')) {\n // When the file is in 'dist'\n viewsFolderPath = resolve(__dirname, '../views/widget')\n } else {\n // In case it's neither, fallback to project root\n viewsFolderPath = resolve(__dirname, 'views/widget')\n }\n\n return viewsFolderPath\n}\n\n/*\n * Get the full path of the template file for generating transit departures widget based on\n * config.\n */\nfunction getTemplatePath(templateFileName: string, config: Config) {\n let fullTemplateFileName = templateFileName\n if (config.noHead !== true) {\n fullTemplateFileName += '_full'\n }\n\n if (config.templatePath !== undefined) {\n return join(untildify(config.templatePath), `${fullTemplateFileName}.pug`)\n }\n\n return join(\n fileURLToPath(import.meta.url),\n '../../../views/widget',\n `${fullTemplateFileName}.pug`,\n )\n}\n\n/*\n * Prepare the outputPath directory for writing timetable files.\n */\nexport async function prepDirectory(outputPath: string, config: Config) {\n // Check if outputPath exists\n try {\n await access(outputPath)\n } catch (error: any) {\n try {\n await mkdir(outputPath, { recursive: true })\n await mkdir(join(outputPath, 'data'))\n } catch (error: any) {\n if (error?.code === 'ENOENT') {\n throw new Error(\n `Unable to write to ${outputPath}. Try running this command from a writable directory.`,\n )\n }\n\n throw error\n }\n }\n\n // Check if outputPath is empty\n const files = await readdir(outputPath)\n if (config.overwriteExistingFiles === false && files.length > 0) {\n throw new Error(\n `Output directory ${outputPath} is not empty. Please specify an empty directory.`,\n )\n }\n\n // Delete all files in outputPath if `overwriteExistingFiles` is true\n if (config.overwriteExistingFiles === true) {\n await rm(join(outputPath, '*'), { recursive: true, force: true })\n }\n}\n\n/*\n * Copy needed CSS and JS to export path.\n */\nexport async function copyStaticAssets(config: Config, outputPath: string) {\n const viewsFolderPath = getPathToViewsFolder(config)\n\n const foldersToCopy = ['css', 'js', 'img']\n\n for (const folder of foldersToCopy) {\n if (\n await access(join(viewsFolderPath, folder))\n .then(() => true)\n .catch(() => false)\n ) {\n await cp(join(viewsFolderPath, folder), join(outputPath, folder), {\n recursive: true,\n })\n }\n }\n}\n\n/*\n * Render the HTML based on the config.\n */\nexport async function renderFile(\n templateFileName: string,\n templateVars: any,\n config: Config,\n) {\n const templatePath = getTemplatePath(templateFileName, config)\n const html = await pug.renderFile(templatePath, templateVars)\n\n // Beautify HTML if setting is set\n if (config.beautify === true) {\n return beautify.html_beautify(html, { indent_size: 2 })\n }\n\n return html\n}\n","import { clearLine, cursorTo } from 'node:readline'\nimport { noop } from 'lodash-es'\nimport * as colors from 'yoctocolors'\n\nimport { Config } from '../types/global_interfaces.ts'\n\n/*\n * Returns a log function based on config settings\n */\nexport function log(config: Config) {\n if (config.verbose === false) {\n return noop\n }\n\n if (config.logFunction) {\n return config.logFunction\n }\n\n return (text: string, overwrite: boolean) => {\n if (overwrite === true && process.stdout.isTTY) {\n clearLine(process.stdout, 0)\n cursorTo(process.stdout, 0)\n } else {\n process.stdout.write('\\n')\n }\n\n process.stdout.write(text)\n }\n}\n\n/*\n * Returns an warning log function based on config settings\n */\nexport function logWarning(config: Config) {\n if (config.logFunction) {\n return config.logFunction\n }\n\n return (text: string) => {\n process.stdout.write(`\\n${formatWarning(text)}\\n`)\n }\n}\n\n/*\n * Returns an error log function based on config settings\n */\nexport function logError(config: Config) {\n if (config.logFunction) {\n return config.logFunction\n }\n\n return (text: string) => {\n process.stdout.write(`\\n${formatError(text)}\\n`)\n }\n}\n\n/*\n * Format console warning text\n */\nexport function formatWarning(text: string) {\n const warningMessage = `${colors.underline('Warning')}: ${text}`\n return colors.yellow(warningMessage)\n}\n\n/*\n * Format console error text\n */\nexport function formatError(error: any) {\n const messageText = error instanceof Error ? error.message : error\n const errorMessage = `${colors.underline('Error')}: ${messageText.replace(\n 'Error: ',\n '',\n )}`\n return colors.red(errorMessage)\n}\n","import { fileURLToPath } from 'url'\nimport { dirname, join } from 'path'\nimport { openDb, getDirections, getRoutes, getStops, getTrips } from 'gtfs'\nimport { groupBy, last, maxBy, size, sortBy, uniqBy } from 'lodash-es'\nimport { renderFile } from './file-utils.ts'\nimport sqlString from 'sqlstring-sqlite'\nimport toposort from 'toposort'\nimport i18n from 'i18n'\n\nimport { Config, SqlWhere, SqlValue } from '../types/global_interfaces.ts'\n\n/*\n * Get calendars for a specified date range\n */\nconst getCalendarsForDateRange = (config: Config) => {\n const db = openDb(config)\n let whereClause = ''\n const whereClauses = []\n\n if (config.endDate) {\n whereClauses.push(`start_date <= ${sqlString.escape(config.endDate)}`)\n }\n\n if (config.startDate) {\n whereClauses.push(`end_date >= ${sqlString.escape(config.startDate)}`)\n }\n\n if (whereClauses.length > 0) {\n whereClause = `WHERE ${whereClauses.join(' AND ')}`\n }\n\n return db.prepare(`SELECT * FROM calendar ${whereClause}`).all()\n}\n\n/*\n * Format a route name.\n */\nfunction formatRouteName(route) {\n let routeName = ''\n\n if (route.route_short_name !== null) {\n routeName += route.route_short_name\n }\n\n if (route.route_short_name !== null && route.route_long_name !== null) {\n routeName += ' - '\n }\n\n if (route.route_long_name !== null) {\n routeName += route.route_long_name\n }\n\n return routeName\n}\n\n/*\n * Get directions for a route\n */\nfunction getDirectionsForRoute(route: Record<string, string>, config: Config) {\n const db = openDb(config)\n\n // Lookup direction names from non-standard directions.txt file\n const directions = getDirections({ route_id: route.route_id }, [\n 'direction_id',\n 'direction',\n ])\n\n const calendars = getCalendarsForDateRange(config)\n\n // Else use the most common headsigns as directions from trips.txt file\n if (directions.length === 0) {\n const headsigns = db\n .prepare(\n `SELECT direction_id, trip_headsign, count(*) AS count FROM trips WHERE route_id = ? AND service_id IN (${calendars\n .map((calendar: Record<string, string>) => `'${calendar.service_id}'`)\n .join(', ')}) GROUP BY direction_id, trip_headsign`,\n )\n .all(route.route_id)\n\n for (const group of Object.values(groupBy(headsigns, 'direction_id'))) {\n const mostCommonHeadsign = maxBy(group, 'count')\n directions.push({\n direction_id: mostCommonHeadsign.direction_id,\n direction: i18n.__('To {{{headsign}}}', {\n headsign: mostCommonHeadsign.trip_headsign,\n }),\n })\n }\n }\n\n return directions\n}\n\n/*\n * Sort an array of stoptimes by stop_sequence using a directed graph\n */\nfunction sortStopIdsBySequence(stoptimes: Record<string, string>[]) {\n const stoptimesGroupedByTrip = groupBy(stoptimes, 'trip_id')\n\n // First, try using a directed graph to determine stop order.\n try {\n const stopGraph = []\n\n for (const tripStoptimes of Object.values(stoptimesGroupedByTrip)) {\n const sortedStopIds = sortBy(tripStoptimes, 'stop_sequence').map(\n (stoptime) => stoptime.stop_id,\n )\n\n for (const [index, stopId] of sortedStopIds.entries()) {\n if (index === sortedStopIds.length - 1) {\n continue\n }\n\n stopGraph.push([stopId, sortedStopIds[index + 1]])\n }\n }\n\n return toposort(\n stopGraph as unknown as readonly [string, string | undefined][],\n )\n } catch {\n // Ignore errors and move to next strategy.\n }\n\n // Finally, fall back to using the stop order from the trip with the most stoptimes.\n const longestTripStoptimes = maxBy(\n Object.values(stoptimesGroupedByTrip),\n (stoptimes) => size(stoptimes),\n )\n\n return longestTripStoptimes.map((stoptime) => stoptime.stop_id)\n}\n\n/*\n * Get stops in order for a route and direction\n */\nfunction getStopsForDirection(route, direction, config: Config) {\n const db = openDb(config)\n const calendars = getCalendarsForDateRange(config)\n const whereClause = formatWhereClauses({\n direction_id: direction.direction_id,\n route_id: route.route_id,\n service_id: calendars.map((calendar) => calendar.service_id),\n })\n const stoptimes = db\n .prepare(\n `SELECT stop_id, stop_sequence, trip_id FROM stop_times WHERE trip_id IN (SELECT trip_id FROM trips ${whereClause}) ORDER BY stop_sequence ASC`,\n )\n .all()\n\n const sortedStopIds = sortStopIdsBySequence(stoptimes)\n\n const deduplicatedStopIds = sortedStopIds.reduce(\n (memo: string[], stopId: string) => {\n // Remove duplicated stop_ids in a row\n if (last(memo) !== stopId) {\n memo.push(stopId)\n }\n\n return memo\n },\n [],\n )\n\n // Remove last stop of route since boarding is not allowed\n deduplicatedStopIds.pop()\n\n // Fetch stop details\n\n const stopFields = ['stop_id', 'stop_name', 'stop_code', 'parent_station']\n\n if (config.includeCoordinates) {\n stopFields.push('stop_lat', 'stop_lon')\n }\n\n const stops = getStops({ stop_id: deduplicatedStopIds }, stopFields)\n\n return deduplicatedStopIds.map((stopId: string) =>\n stops.find((stop) => stop.stop_id === stopId),\n )\n}\n\n/*\n * Generate HTML for transit departures widget.\n */\nexport function generateTransitDeparturesWidgetHtml(config: Config) {\n i18n.configure({\n directory: join(dirname(fileURLToPath(import.meta.url)), '../../locales'),\n defaultLocale: config.locale,\n updateFiles: false,\n })\n\n const templateVars = {\n __: i18n.__,\n config,\n }\n return renderFile('widget', templateVars, config)\n}\n\n/*\n * Generate JSON of routes and stops for transit departures widget.\n */\nexport function generateTransitDeparturesWidgetJson(config: Config) {\n const routes = getRoutes()\n const stops = []\n const filteredRoutes = []\n const calendars = getCalendarsForDateRange(config)\n\n for (const route of routes) {\n route.route_full_name = formatRouteName(route)\n\n const directions = getDirectionsForRoute(route, config)\n\n // Filter out routes with no directions\n if (directions.length === 0) {\n logWarning(config)(\n `route_id ${route.route_id} has no directions - skipping`,\n )\n continue\n }\n\n for (const direction of directions) {\n const directionStops = getStopsForDirection(route, direction, config)\n stops.push(...directionStops)\n direction.stopIds = directionStops.map((stop) => stop?.stop_id)\n\n const trips = getTrips(\n {\n route_id: route.route_id,\n direction_id: direction.direction_id,\n service_id: calendars.map(\n (calendar: Record<string, string>) => calendar.service_id,\n ),\n },\n ['trip_id'],\n )\n direction.tripIds = trips.map((trip) => trip.trip_id)\n }\n\n route.directions = directions\n filteredRoutes.push(route)\n }\n\n // Sort routes twice to handle integers with alphabetical characters, such as ['14', '14L', '14X']\n const sortedRoutes = sortBy(\n sortBy(filteredRoutes, (route) => route.route_short_name?.toLowerCase()),\n (route) => Number.parseInt(route.route_short_name, 10),\n )\n\n // Get Parent Station Stops\n const parentStationIds = new Set(stops.map((stop) => stop?.parent_station))\n\n const parentStationStops = getStops(\n { stop_id: Array.from(parentStationIds) },\n ['stop_id', 'stop_name', 'stop_code', 'parent_station'],\n )\n\n stops.push(\n ...parentStationStops.map((stop) => {\n stop.is_parent_station = true\n return stop\n }),\n )\n\n // Sort unique list of stops\n const sortedStops = sortBy(uniqBy(stops, 'stop_id'), 'stop_name')\n\n return {\n routes: removeNulls(sortedRoutes),\n stops: removeNulls(sortedStops),\n }\n}\n\n/*\n * Remove null values from array or object\n */\nfunction removeNulls(data: any): any {\n if (Array.isArray(data)) {\n return data\n .map(removeNulls)\n .filter((item) => item !== null && item !== undefined)\n } else if (typeof data === 'object' && data !== null) {\n return Object.entries(data).reduce((acc, [key, value]) => {\n const cleanedValue = removeNulls(value)\n if (cleanedValue !== null && cleanedValue !== undefined) {\n acc[key] = cleanedValue\n }\n return acc\n }, {})\n } else {\n return data\n }\n}\n\n/*\n * Initialize configuration with defaults.\n */\nexport function setDefaultConfig(initialConfig: Config) {\n const defaults = {\n beautify: false,\n noHead: false,\n refreshIntervalSeconds: 20,\n skipImport: false,\n timeFormat: '12hour',\n includeCoordinates: false,\n overwriteExistingFiles: true,\n verbose: true,\n }\n\n return Object.assign(defaults, initialConfig)\n}\n\nexport function formatWhereClause(\n key: string,\n value: null | SqlValue | SqlValue[],\n) {\n if (Array.isArray(value)) {\n let whereClause = `${sqlString.escapeId(key)} IN (${value\n .filter((v) => v !== null)\n .map((v) => sqlString.escape(v))\n .join(', ')})`\n\n if (value.includes(null)) {\n whereClause = `(${whereClause} OR ${sqlString.escapeId(key)} IS NULL)`\n }\n\n return whereClause\n }\n\n if (value === null) {\n return `${sqlString.escapeId(key)} IS NULL`\n }\n\n return `${sqlString.escapeId(key)} = ${sqlString.escape(value)}`\n}\n\nexport function formatWhereClauses(query: SqlWhere) {\n if (Object.keys(query).length === 0) {\n return ''\n }\n\n const whereClauses = Object.entries(query).map(([key, value]) =>\n formatWhereClause(key, value),\n )\n return `WHERE ${whereClauses.join(' AND ')}`\n}\n"],"mappings":"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","names":["openDb","untildify","error","fileURLToPath","dirname","join","stoptimes","join","dirname","fileURLToPath","openDb","untildify"]}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "transit-departures-widget",
3
3
  "description": "Build a realtime transit departures tool from GTFS and GTFS-Realtime.",
4
- "version": "2.4.2",
4
+ "version": "2.4.4",
5
5
  "keywords": [
6
6
  "transit",
7
7
  "gtfs",
@@ -36,9 +36,8 @@
36
36
  "dist"
37
37
  ],
38
38
  "dependencies": {
39
- "copy-dir": "^1.3.0",
40
- "express": "^4.19.2",
41
- "gtfs": "^4.12.0",
39
+ "express": "^4.21.1",
40
+ "gtfs": "^4.15.9",
42
41
  "i18n": "^0.15.1",
43
42
  "js-beautify": "^1.15.1",
44
43
  "lodash-es": "^4.17.21",
@@ -59,16 +58,16 @@
59
58
  "@types/js-beautify": "^1.14.3",
60
59
  "@types/lodash-es": "^4.17.12",
61
60
  "@types/morgan": "^1.9.9",
62
- "@types/node": "^20.14.9",
61
+ "@types/node": "^20.17.6",
63
62
  "@types/pug": "^2.0.10",
64
63
  "@types/timer-machine": "^1.1.3",
65
64
  "@types/toposort": "^2.0.7",
66
- "@types/yargs": "^17.0.32",
67
- "husky": "^9.0.11",
68
- "lint-staged": "^15.2.7",
69
- "prettier": "^3.3.2",
70
- "tsup": "^8.1.0",
71
- "typescript": "^5.5.3"
65
+ "@types/yargs": "^17.0.33",
66
+ "husky": "^9.1.6",
67
+ "lint-staged": "^15.2.10",
68
+ "prettier": "^3.3.3",
69
+ "tsup": "^8.3.5",
70
+ "typescript": "^5.6.3"
72
71
  },
73
72
  "engines": {
74
73
  "node": ">= 14.15.4"
@@ -81,6 +80,9 @@
81
80
  "@release-it/keep-a-changelog": {
82
81
  "filename": "CHANGELOG.md"
83
82
  }
83
+ },
84
+ "hooks": {
85
+ "after:bump": "npm run build"
84
86
  }
85
87
  },
86
88
  "prettier": {