transit-departures-widget 2.5.3 → 2.5.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -3
- package/dist/app/index.js +28 -9
- package/dist/app/index.js.map +1 -1
- package/dist/bin/transit-departures-widget.js +5 -5
- package/dist/index.js +5 -5
- package/package.json +10 -10
package/README.md
CHANGED
|
@@ -54,7 +54,10 @@ An demo of the widget is available at https://transit-departures-widget.blinktag
|
|
|
54
54
|
|
|
55
55
|
The following transit agencies use `transit-departures-widget` on their websites:
|
|
56
56
|
|
|
57
|
+
- [County Connection](https://countyconnection.com)
|
|
58
|
+
- [Kings Area Regional Transit](https://kartbus.org)
|
|
57
59
|
- [Marin Transit](https://marintransit.org/)
|
|
60
|
+
- [Mountain View Community Shuttle](https://mvcommunityshuttle.com)
|
|
58
61
|
- [MVgo](https://mvgo.org/)
|
|
59
62
|
|
|
60
63
|
## Command Line Usage
|
|
@@ -114,7 +117,7 @@ Copy `config-sample.json` to `config.json` and then add your projects configurat
|
|
|
114
117
|
|
|
115
118
|
`gtfs_static_path` is the local path to an agency's static GTFS on your local machine. Either `gtfs_static_url` or `gtfs_static_path` is required.
|
|
116
119
|
|
|
117
|
-
`gtfs_rt_tripupdates_url` is the URL of an agency's GTFS-RT trip updates. Note that the GTFS-RT URL must support [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) in order for the widget to work.
|
|
120
|
+
`gtfs_rt_tripupdates_url` is the URL of an agency's GTFS-RT trip updates. Note that the GTFS-RT URL must support [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) in order for the widget to work. You may need to set up a proxy that adds CORS headers to your GTFS-RT URLS. [GTFS Realtime Proxy](https://github.com/BlinkTagInc/gtfs-realtime-proxy) is an open-source tool that you could use for adding CORS headers.
|
|
118
121
|
|
|
119
122
|
- Specify a download URL for static GTFS:
|
|
120
123
|
|
|
@@ -248,11 +251,11 @@ After an initial run of `transit-departures-widget`, the GTFS data will be downl
|
|
|
248
251
|
|
|
249
252
|
You can view an individual route HTML on demand by running the included Express app:
|
|
250
253
|
|
|
251
|
-
|
|
254
|
+
npm start
|
|
252
255
|
|
|
253
256
|
By default, `transit-departures-widget` will look for a `config.json` file in the project root. To specify a different path for the configuration file:
|
|
254
257
|
|
|
255
|
-
|
|
258
|
+
npm start -- --configPath /path/to/your/custom-config.json
|
|
256
259
|
|
|
257
260
|
Once running, you can view the HTML in your browser at [localhost:3000](http://localhost:3000)
|
|
258
261
|
|
package/dist/app/index.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
// src/app/index.ts
|
|
2
|
-
import { readFileSync } from "
|
|
3
|
-
import { join as join3 } from "
|
|
2
|
+
import { readFileSync } from "fs";
|
|
3
|
+
import { join as join3 } from "path";
|
|
4
4
|
import yargs from "yargs";
|
|
5
|
-
import { openDb as openDb2 } from "gtfs";
|
|
5
|
+
import { getRoutes as getRoutes2, importGtfs, openDb as openDb2 } from "gtfs";
|
|
6
|
+
import { clone, omit } from "lodash-es";
|
|
6
7
|
import untildify2 from "untildify";
|
|
7
8
|
import express from "express";
|
|
8
9
|
import logger from "morgan";
|
|
@@ -13,8 +14,9 @@ import { openDb, getDirections, getRoutes, getStops, getTrips } from "gtfs";
|
|
|
13
14
|
import { groupBy, last, maxBy, size, sortBy, uniqBy } from "lodash-es";
|
|
14
15
|
|
|
15
16
|
// src/lib/file-utils.ts
|
|
16
|
-
import { dirname, join, resolve } from "
|
|
17
|
-
import { fileURLToPath } from "
|
|
17
|
+
import { dirname, join, resolve } from "path";
|
|
18
|
+
import { fileURLToPath } from "url";
|
|
19
|
+
import { access, cp, mkdir, readdir, readFile, rm } from "fs/promises";
|
|
18
20
|
import beautify from "js-beautify";
|
|
19
21
|
import pug from "pug";
|
|
20
22
|
import untildify from "untildify";
|
|
@@ -52,6 +54,7 @@ import toposort from "toposort";
|
|
|
52
54
|
import { I18n } from "i18n";
|
|
53
55
|
|
|
54
56
|
// src/lib/log-utils.ts
|
|
57
|
+
import { clearLine, cursorTo } from "readline";
|
|
55
58
|
import { noop } from "lodash-es";
|
|
56
59
|
import * as colors from "yoctocolors";
|
|
57
60
|
function logWarning(config2) {
|
|
@@ -316,11 +319,27 @@ config.assetPath = "/";
|
|
|
316
319
|
config.logFunction = console.log;
|
|
317
320
|
try {
|
|
318
321
|
openDb2(config);
|
|
322
|
+
getRoutes2();
|
|
319
323
|
} catch (error) {
|
|
320
|
-
console.
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
+
console.log("Importing GTFS");
|
|
325
|
+
try {
|
|
326
|
+
const gtfsImportConfig = {
|
|
327
|
+
...clone(omit(config, "agency")),
|
|
328
|
+
agencies: [
|
|
329
|
+
{
|
|
330
|
+
agency_key: config.agency.agency_key,
|
|
331
|
+
path: config.agency.gtfs_static_path,
|
|
332
|
+
url: config.agency.gtfs_static_url
|
|
333
|
+
}
|
|
334
|
+
]
|
|
335
|
+
};
|
|
336
|
+
await importGtfs(gtfsImportConfig);
|
|
337
|
+
} catch (error2) {
|
|
338
|
+
console.error(
|
|
339
|
+
`Unable to open sqlite database "${config.sqlitePath}" defined as \`sqlitePath\` config.json. Ensure the parent directory exists and import GTFS before running this app.`
|
|
340
|
+
);
|
|
341
|
+
throw error2;
|
|
342
|
+
}
|
|
324
343
|
}
|
|
325
344
|
app.get("/", async (request, response, next) => {
|
|
326
345
|
try {
|
package/dist/app/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/app/index.ts","../../src/lib/utils.ts","../../src/lib/file-utils.ts","../../src/lib/log-utils.ts"],"sourcesContent":["import { readFileSync } from 'node:fs'\nimport { join } from 'node:path'\nimport yargs from 'yargs'\nimport { openDb } from 'gtfs'\nimport untildify from 'untildify'\nimport express from 'express'\nimport logger from 'morgan'\n\nimport {\n setDefaultConfig,\n generateTransitDeparturesWidgetHtml,\n generateTransitDeparturesWidgetJson,\n} from '../lib/utils.ts'\nimport { getPathToViewsFolder } from '../lib/file-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()\n\nconst configPath =\n (argv.configPath as string) || join(process.cwd(), 'config.json')\nconst selectedConfig = JSON.parse(readFileSync(configPath, 'utf8'))\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 */\napp.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 */\napp.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\napp.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', getPathToViewsFolder(config))\napp.set('view engine', 'pug')\n\napp.use(logger('dev'))\n\n// Serve static assets\nconst staticAssetPath =\n config.templatePath === undefined\n ? getPathToViewsFolder(config)\n : untildify(config.templatePath)\n\napp.use(express.static(staticAssetPath))\n\n// Fallback 404 route\napp.use((req, res) => {\n res.status(404).send('Not Found')\n})\n\n// Error handling middleware\napp.use(\n (\n err: Error,\n req: express.Request,\n res: express.Response,\n next: express.NextFunction,\n ) => {\n console.error(err.stack)\n res.status(500).send('Something broke!')\n },\n)\n\nconst startServer = async (port: number): Promise<void> => {\n try {\n await new Promise<void>((resolve, reject) => {\n const server = app\n .listen(port)\n .once('listening', () => {\n console.log(`Express server listening on port ${port}`)\n resolve()\n })\n .once('error', (err: NodeJS.ErrnoException) => {\n if (err.code === 'EADDRINUSE') {\n console.log(`Port ${port} is in use, trying ${port + 1}`)\n server.close()\n resolve(startServer(port + 1))\n } else {\n reject(err)\n }\n })\n })\n } catch (err) {\n console.error('Failed to start server:', err)\n process.exit(1)\n }\n}\n\nconst port = process.env.PORT ? parseInt(process.env.PORT, 10) : 3000\nstartServer(port)\n","import { join } from 'path'\nimport { openDb, getDirections, getRoutes, getStops, getTrips } from 'gtfs'\nimport { groupBy, last, maxBy, size, sortBy, uniqBy } from 'lodash-es'\nimport { getPathToViewsFolder, 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'\nimport { logWarning } from './log-utils.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(\n route: Record<string, string>,\n config: Config & { __: I18n['__'] },\n) {\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: config.__('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 const templateVars = {\n config,\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 const config = Object.assign(defaults, initialConfig)\n const viewsFolderPath = getPathToViewsFolder(config)\n const i18n = new I18n({\n directory: join(viewsFolderPath, 'locales'),\n defaultLocale: config.locale,\n updateFiles: false,\n })\n const configWithI18n = Object.assign(config, {\n __: i18n.__,\n })\n return configWithI18n\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 a template file.\n */\nfunction getPathToTemplateFile(templateFileName: string, config: Config) {\n const fullTemplateFileName =\n config.noHead !== true\n ? `${templateFileName}_full.pug`\n : `${templateFileName}.pug`\n\n return join(getPathToViewsFolder(config), fullTemplateFileName)\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 = getPathToTemplateFile(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"],"mappings":"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","names":["join","openDb","untildify","join","config","config","config","config","stoptimes","join","join","openDb","untildify","port","resolve"]}
|
|
1
|
+
{"version":3,"sources":["../../src/app/index.ts","../../src/lib/utils.ts","../../src/lib/file-utils.ts","../../src/lib/log-utils.ts"],"sourcesContent":["import { readFileSync } from 'node:fs'\nimport { join } from 'node:path'\nimport yargs from 'yargs'\nimport { getRoutes, importGtfs, openDb } from 'gtfs'\nimport { clone, omit } from 'lodash-es'\nimport untildify from 'untildify'\nimport express from 'express'\nimport logger from 'morgan'\n\nimport {\n setDefaultConfig,\n generateTransitDeparturesWidgetHtml,\n generateTransitDeparturesWidgetJson,\n} from '../lib/utils.ts'\nimport { getPathToViewsFolder } from '../lib/file-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()\n\nconst configPath =\n (argv.configPath as string) || join(process.cwd(), 'config.json')\nconst selectedConfig = JSON.parse(readFileSync(configPath, 'utf8'))\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 getRoutes()\n} catch (error: any) {\n console.log('Importing GTFS')\n\n try {\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 } 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/*\n * Show the transit departures widget\n */\napp.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 */\napp.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\napp.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', getPathToViewsFolder(config))\napp.set('view engine', 'pug')\n\napp.use(logger('dev'))\n\n// Serve static assets\nconst staticAssetPath =\n config.templatePath === undefined\n ? getPathToViewsFolder(config)\n : untildify(config.templatePath)\n\napp.use(express.static(staticAssetPath))\n\n// Fallback 404 route\napp.use((req, res) => {\n res.status(404).send('Not Found')\n})\n\n// Error handling middleware\napp.use(\n (\n err: Error,\n req: express.Request,\n res: express.Response,\n next: express.NextFunction,\n ) => {\n console.error(err.stack)\n res.status(500).send('Something broke!')\n },\n)\n\nconst startServer = async (port: number): Promise<void> => {\n try {\n await new Promise<void>((resolve, reject) => {\n const server = app\n .listen(port)\n .once('listening', () => {\n console.log(`Express server listening on port ${port}`)\n resolve()\n })\n .once('error', (err: NodeJS.ErrnoException) => {\n if (err.code === 'EADDRINUSE') {\n console.log(`Port ${port} is in use, trying ${port + 1}`)\n server.close()\n resolve(startServer(port + 1))\n } else {\n reject(err)\n }\n })\n })\n } catch (err) {\n console.error('Failed to start server:', err)\n process.exit(1)\n }\n}\n\nconst port = process.env.PORT ? parseInt(process.env.PORT, 10) : 3000\nstartServer(port)\n","import { join } from 'path'\nimport { openDb, getDirections, getRoutes, getStops, getTrips } from 'gtfs'\nimport { groupBy, last, maxBy, size, sortBy, uniqBy } from 'lodash-es'\nimport { getPathToViewsFolder, 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'\nimport { logWarning } from './log-utils.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(\n route: Record<string, string>,\n config: Config & { __: I18n['__'] },\n) {\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: config.__('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 const templateVars = {\n config,\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 const config = Object.assign(defaults, initialConfig)\n const viewsFolderPath = getPathToViewsFolder(config)\n const i18n = new I18n({\n directory: join(viewsFolderPath, 'locales'),\n defaultLocale: config.locale,\n updateFiles: false,\n })\n const configWithI18n = Object.assign(config, {\n __: i18n.__,\n })\n return configWithI18n\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 a template file.\n */\nfunction getPathToTemplateFile(templateFileName: string, config: Config) {\n const fullTemplateFileName =\n config.noHead !== true\n ? `${templateFileName}_full.pug`\n : `${templateFileName}.pug`\n\n return join(getPathToViewsFolder(config), fullTemplateFileName)\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 = getPathToTemplateFile(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"],"mappings":"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","names":["join","getRoutes","openDb","untildify","join","config","config","config","config","stoptimes","join","join","openDb","getRoutes","error","untildify","port","resolve"]}
|
|
@@ -6,9 +6,9 @@ import { hideBin } from "yargs/helpers";
|
|
|
6
6
|
import PrettyError from "pretty-error";
|
|
7
7
|
|
|
8
8
|
// src/lib/file-utils.ts
|
|
9
|
-
import { dirname, join, resolve } from "
|
|
10
|
-
import { fileURLToPath } from "
|
|
11
|
-
import { access, cp, mkdir, readdir, readFile, rm } from "
|
|
9
|
+
import { dirname, join, resolve } from "path";
|
|
10
|
+
import { fileURLToPath } from "url";
|
|
11
|
+
import { access, cp, mkdir, readdir, readFile, rm } from "fs/promises";
|
|
12
12
|
import beautify from "js-beautify";
|
|
13
13
|
import pug from "pug";
|
|
14
14
|
import untildify from "untildify";
|
|
@@ -105,7 +105,7 @@ async function renderFile(templateFileName, templateVars, config) {
|
|
|
105
105
|
}
|
|
106
106
|
|
|
107
107
|
// src/lib/log-utils.ts
|
|
108
|
-
import { clearLine, cursorTo } from "
|
|
108
|
+
import { clearLine, cursorTo } from "readline";
|
|
109
109
|
import { noop } from "lodash-es";
|
|
110
110
|
import * as colors from "yoctocolors";
|
|
111
111
|
function log(config) {
|
|
@@ -161,7 +161,7 @@ function formatError(error) {
|
|
|
161
161
|
// src/lib/transit-departures-widget.ts
|
|
162
162
|
import path from "path";
|
|
163
163
|
import { clone, omit } from "lodash-es";
|
|
164
|
-
import { writeFile } from "
|
|
164
|
+
import { writeFile } from "fs/promises";
|
|
165
165
|
import { importGtfs, openDb as openDb2 } from "gtfs";
|
|
166
166
|
import sanitize from "sanitize-filename";
|
|
167
167
|
import Timer from "timer-machine";
|
package/dist/index.js
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
// src/lib/transit-departures-widget.ts
|
|
2
2
|
import path from "path";
|
|
3
3
|
import { clone, omit } from "lodash-es";
|
|
4
|
-
import { writeFile } from "
|
|
4
|
+
import { writeFile } from "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
8
|
import untildify2 from "untildify";
|
|
9
9
|
|
|
10
10
|
// src/lib/file-utils.ts
|
|
11
|
-
import { dirname, join, resolve } from "
|
|
12
|
-
import { fileURLToPath } from "
|
|
13
|
-
import { access, cp, mkdir, readdir, readFile, rm } from "
|
|
11
|
+
import { dirname, join, resolve } from "path";
|
|
12
|
+
import { fileURLToPath } from "url";
|
|
13
|
+
import { access, cp, mkdir, readdir, readFile, rm } from "fs/promises";
|
|
14
14
|
import beautify from "js-beautify";
|
|
15
15
|
import pug from "pug";
|
|
16
16
|
import untildify from "untildify";
|
|
@@ -80,7 +80,7 @@ async function renderFile(templateFileName, templateVars, config) {
|
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
// src/lib/log-utils.ts
|
|
83
|
-
import { clearLine, cursorTo } from "
|
|
83
|
+
import { clearLine, cursorTo } from "readline";
|
|
84
84
|
import { noop } from "lodash-es";
|
|
85
85
|
import * as colors from "yoctocolors";
|
|
86
86
|
function log(config) {
|
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.5.
|
|
4
|
+
"version": "2.5.5",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"transit",
|
|
7
7
|
"gtfs",
|
|
@@ -36,10 +36,10 @@
|
|
|
36
36
|
"dist"
|
|
37
37
|
],
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"express": "^
|
|
40
|
-
"gtfs": "^4.
|
|
39
|
+
"express": "^5.1.0",
|
|
40
|
+
"gtfs": "^4.17.3",
|
|
41
41
|
"i18n": "^0.15.1",
|
|
42
|
-
"js-beautify": "^1.15.
|
|
42
|
+
"js-beautify": "^1.15.4",
|
|
43
43
|
"lodash-es": "^4.17.21",
|
|
44
44
|
"morgan": "^1.10.0",
|
|
45
45
|
"pretty-error": "^4.0.0",
|
|
@@ -53,21 +53,21 @@
|
|
|
53
53
|
"yoctocolors": "^2.1.1"
|
|
54
54
|
},
|
|
55
55
|
"devDependencies": {
|
|
56
|
-
"@types/express": "^5.0.
|
|
56
|
+
"@types/express": "^5.0.2",
|
|
57
57
|
"@types/i18n": "^0.13.12",
|
|
58
58
|
"@types/js-beautify": "^1.14.3",
|
|
59
59
|
"@types/lodash-es": "^4.17.12",
|
|
60
60
|
"@types/morgan": "^1.9.9",
|
|
61
|
-
"@types/node": "^22.
|
|
61
|
+
"@types/node": "^22.15.21",
|
|
62
62
|
"@types/pug": "^2.0.10",
|
|
63
63
|
"@types/timer-machine": "^1.1.3",
|
|
64
64
|
"@types/toposort": "^2.0.7",
|
|
65
65
|
"@types/yargs": "^17.0.33",
|
|
66
66
|
"husky": "^9.1.7",
|
|
67
|
-
"lint-staged": "^
|
|
68
|
-
"prettier": "^3.
|
|
69
|
-
"tsup": "^8.
|
|
70
|
-
"typescript": "^5.
|
|
67
|
+
"lint-staged": "^16.0.0",
|
|
68
|
+
"prettier": "^3.5.3",
|
|
69
|
+
"tsup": "^8.5.0",
|
|
70
|
+
"typescript": "^5.8.3"
|
|
71
71
|
},
|
|
72
72
|
"engines": {
|
|
73
73
|
"node": ">= 14.15.4"
|