gtfs-to-chart 2.0.11 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/app/index.js +16 -15
- package/config-sample.json +1 -0
- package/lib/utils.js +169 -39
- package/package.json +8 -8
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,18 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [2.1.0] - 2025-06-06
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- Support for frequencies.txt
|
|
13
|
+
|
|
14
|
+
## [2.0.12] - 2025-02-25
|
|
15
|
+
|
|
16
|
+
### Updated
|
|
17
|
+
|
|
18
|
+
- Dependency updates
|
|
19
|
+
|
|
8
20
|
## [2.0.11] - 2024-12-05
|
|
9
21
|
|
|
10
22
|
### Updated
|
package/app/index.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { fileURLToPath } from 'node:url';
|
|
3
|
-
|
|
4
3
|
import express from 'express';
|
|
5
4
|
import logger from 'morgan';
|
|
6
5
|
import slashes from 'connect-slashes';
|
|
@@ -8,61 +7,63 @@ import slashes from 'connect-slashes';
|
|
|
8
7
|
import routes from './routes.js';
|
|
9
8
|
|
|
10
9
|
const app = express();
|
|
10
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
11
11
|
|
|
12
12
|
// View engine setup
|
|
13
|
-
app.set('views', path.join(
|
|
13
|
+
app.set('views', path.join(__dirname, './views'));
|
|
14
14
|
app.set('view engine', 'pug');
|
|
15
15
|
|
|
16
|
+
// Middleware
|
|
16
17
|
app.use(logger('dev'));
|
|
17
|
-
app.use(express.static(path.join(
|
|
18
|
+
app.use(express.static(path.join(__dirname, '../public')));
|
|
18
19
|
app.use(slashes());
|
|
19
20
|
|
|
21
|
+
// Routes
|
|
20
22
|
app.use('/', routes);
|
|
21
23
|
|
|
22
24
|
// Error handlers
|
|
23
|
-
|
|
24
|
-
// 404 error handler
|
|
25
25
|
app.use((request, response) => {
|
|
26
26
|
const error = {
|
|
27
27
|
message: 'Not Found',
|
|
28
|
-
status: 404
|
|
28
|
+
status: 404,
|
|
29
29
|
};
|
|
30
30
|
response.status(404);
|
|
31
31
|
if (request.xhr) {
|
|
32
|
-
response.
|
|
32
|
+
response.json({
|
|
33
33
|
message: error.message,
|
|
34
|
-
error
|
|
34
|
+
error,
|
|
35
35
|
});
|
|
36
36
|
} else {
|
|
37
37
|
response.render('error', {
|
|
38
38
|
message: error.message,
|
|
39
|
-
error
|
|
39
|
+
error,
|
|
40
40
|
});
|
|
41
41
|
}
|
|
42
42
|
});
|
|
43
43
|
|
|
44
44
|
// Development error handler: will print stacktrace
|
|
45
45
|
if (process.env.NODE_ENV === 'development') {
|
|
46
|
-
app.use((error, request, response) => {
|
|
46
|
+
app.use((error, request, response, next) => {
|
|
47
47
|
response.status(error.status || 500);
|
|
48
48
|
response.render('error', {
|
|
49
49
|
message: error.message,
|
|
50
|
-
error
|
|
50
|
+
error,
|
|
51
51
|
});
|
|
52
52
|
});
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
// Production error handler: no stacktraces leaked to user
|
|
56
|
-
app.use((error, request, response) => {
|
|
56
|
+
app.use((error, request, response, next) => {
|
|
57
57
|
response.status(error.status || 500);
|
|
58
58
|
response.render('error', {
|
|
59
59
|
message: error.message,
|
|
60
|
-
error: {}
|
|
60
|
+
error: {},
|
|
61
61
|
});
|
|
62
62
|
});
|
|
63
63
|
|
|
64
|
-
|
|
64
|
+
const port = process.env.PORT || 3000;
|
|
65
|
+
app.set('port', port);
|
|
65
66
|
|
|
66
|
-
const server = app.listen(
|
|
67
|
+
const server = app.listen(port, () => {
|
|
67
68
|
console.log(`Express server listening on port ${server.address().port}`);
|
|
68
69
|
});
|
package/config-sample.json
CHANGED
package/lib/utils.js
CHANGED
|
@@ -1,7 +1,17 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { readFileSync } from 'node:fs';
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
max,
|
|
6
|
+
map,
|
|
7
|
+
maxBy,
|
|
8
|
+
size,
|
|
9
|
+
every,
|
|
10
|
+
uniq,
|
|
11
|
+
groupBy,
|
|
12
|
+
first,
|
|
13
|
+
sortBy,
|
|
14
|
+
} from 'lodash-es';
|
|
5
15
|
import { getStops, openDb, getStoptimes, getAgencies, getRoutes } from 'gtfs';
|
|
6
16
|
import sanitize from 'sanitize-filename';
|
|
7
17
|
import moment from 'moment';
|
|
@@ -10,7 +20,29 @@ import sqlString from 'sqlstring';
|
|
|
10
20
|
import { renderTemplate } from './file-utils.js';
|
|
11
21
|
import { formatRouteName } from './formatters.js';
|
|
12
22
|
|
|
13
|
-
const { version } = JSON.parse(
|
|
23
|
+
const { version } = JSON.parse(
|
|
24
|
+
readFileSync(new URL('../package.json', import.meta.url)),
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
/*
|
|
28
|
+
* Convert a GTFS formatted time string into a moment less than 24 hours.
|
|
29
|
+
*/
|
|
30
|
+
export function fromGTFSTime(timeString) {
|
|
31
|
+
const duration = moment.duration(timeString);
|
|
32
|
+
|
|
33
|
+
return moment({
|
|
34
|
+
hour: duration.hours(),
|
|
35
|
+
minute: duration.minutes(),
|
|
36
|
+
second: duration.seconds(),
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/*
|
|
41
|
+
* Convert a moment into a GTFS formatted time string.
|
|
42
|
+
*/
|
|
43
|
+
export function toGTFSTime(time) {
|
|
44
|
+
return time.format('HH:mm:ss');
|
|
45
|
+
}
|
|
14
46
|
|
|
15
47
|
/*
|
|
16
48
|
* Calculate the distance between two coordinates.
|
|
@@ -20,17 +52,19 @@ function calculateDistanceMi(lat1, lon1, lat2, lon2) {
|
|
|
20
52
|
return 0;
|
|
21
53
|
}
|
|
22
54
|
|
|
23
|
-
const radlat1 = Math.PI * lat1 / 180;
|
|
24
|
-
const radlat2 = Math.PI * lat2 / 180;
|
|
55
|
+
const radlat1 = (Math.PI * lat1) / 180;
|
|
56
|
+
const radlat2 = (Math.PI * lat2) / 180;
|
|
25
57
|
const theta = lon1 - lon2;
|
|
26
|
-
const radtheta = Math.PI * theta / 180;
|
|
27
|
-
let dist =
|
|
58
|
+
const radtheta = (Math.PI * theta) / 180;
|
|
59
|
+
let dist =
|
|
60
|
+
Math.sin(radlat1) * Math.sin(radlat2) +
|
|
61
|
+
Math.cos(radlat1) * Math.cos(radlat2) * Math.cos(radtheta);
|
|
28
62
|
if (dist > 1) {
|
|
29
63
|
dist = 1;
|
|
30
64
|
}
|
|
31
65
|
|
|
32
66
|
dist = Math.acos(dist);
|
|
33
|
-
dist = dist * 180 / Math.PI;
|
|
67
|
+
dist = (dist * 180) / Math.PI;
|
|
34
68
|
dist = dist * 60 * 1.1515;
|
|
35
69
|
return dist;
|
|
36
70
|
}
|
|
@@ -42,7 +76,9 @@ const reverseStationDistances = (stations, oppositeDirectionDistance) => {
|
|
|
42
76
|
const tripDistance = max(map(stations, 'distance'));
|
|
43
77
|
for (const station of stations) {
|
|
44
78
|
// Scale distances to match opposite direction total distance
|
|
45
|
-
station.distance =
|
|
79
|
+
station.distance =
|
|
80
|
+
((tripDistance - station.distance) * oppositeDirectionDistance) /
|
|
81
|
+
tripDistance;
|
|
46
82
|
}
|
|
47
83
|
};
|
|
48
84
|
|
|
@@ -65,7 +101,7 @@ const isTimepoint = (stoptime) => {
|
|
|
65
101
|
const getStationsFromTrip = (trip) => {
|
|
66
102
|
const stops = trip.stoptimes.map((stoptime) => {
|
|
67
103
|
const stops = getStops({
|
|
68
|
-
stop_id: stoptime.stop_id
|
|
104
|
+
stop_id: stoptime.stop_id,
|
|
69
105
|
});
|
|
70
106
|
|
|
71
107
|
if (stops.length === 0) {
|
|
@@ -78,20 +114,29 @@ const getStationsFromTrip = (trip) => {
|
|
|
78
114
|
let previousStationCoordinates;
|
|
79
115
|
return trip.stoptimes.map((stoptime, index) => {
|
|
80
116
|
const stop = stops[index];
|
|
81
|
-
const hasShapeDistance = every(
|
|
117
|
+
const hasShapeDistance = every(
|
|
118
|
+
trip.stoptimes,
|
|
119
|
+
(stoptime) => stoptime.shape_dist_traveled !== null,
|
|
120
|
+
);
|
|
82
121
|
|
|
83
122
|
if (!hasShapeDistance) {
|
|
84
123
|
if (index === 0) {
|
|
85
124
|
stoptime.shape_dist_traveled = 0;
|
|
86
125
|
} else {
|
|
87
126
|
const previousStopTime = trip.stoptimes[index - 1];
|
|
88
|
-
const distanceFromPreviousStation = calculateDistanceMi(
|
|
89
|
-
|
|
127
|
+
const distanceFromPreviousStation = calculateDistanceMi(
|
|
128
|
+
stop.stop_lat,
|
|
129
|
+
stop.stop_lon,
|
|
130
|
+
previousStationCoordinates.stop_lat,
|
|
131
|
+
previousStationCoordinates.stop_lon,
|
|
132
|
+
);
|
|
133
|
+
stoptime.shape_dist_traveled =
|
|
134
|
+
previousStopTime.shape_dist_traveled + distanceFromPreviousStation;
|
|
90
135
|
}
|
|
91
136
|
|
|
92
137
|
previousStationCoordinates = {
|
|
93
138
|
stop_lat: stop.stop_lat,
|
|
94
|
-
stop_lon: stop.stop_lon
|
|
139
|
+
stop_lon: stop.stop_lon,
|
|
95
140
|
};
|
|
96
141
|
}
|
|
97
142
|
|
|
@@ -99,7 +144,7 @@ const getStationsFromTrip = (trip) => {
|
|
|
99
144
|
stop_id: stop.stop_id,
|
|
100
145
|
name: stop.stop_name,
|
|
101
146
|
distance: stoptime.shape_dist_traveled,
|
|
102
|
-
direction_id: trip.direction_id
|
|
147
|
+
direction_id: trip.direction_id,
|
|
103
148
|
};
|
|
104
149
|
});
|
|
105
150
|
};
|
|
@@ -110,20 +155,32 @@ const getStationsFromTrip = (trip) => {
|
|
|
110
155
|
const getDataforChart = (config, routeId) => {
|
|
111
156
|
const db = openDb(config);
|
|
112
157
|
const notes = [];
|
|
113
|
-
const dayOfWeek = moment(config.chartDate, 'YYYYMMDD')
|
|
114
|
-
|
|
158
|
+
const dayOfWeek = moment(config.chartDate, 'YYYYMMDD')
|
|
159
|
+
.format('dddd')
|
|
160
|
+
.toLowerCase();
|
|
161
|
+
const calendars = db
|
|
162
|
+
.prepare(
|
|
163
|
+
`SELECT DISTINCT service_id FROM calendar WHERE start_date <= $date AND end_date >= $date AND ${sqlString.escapeId(dayOfWeek)} = 1`,
|
|
164
|
+
)
|
|
165
|
+
.all({ date: config.chartDate });
|
|
115
166
|
|
|
116
167
|
if (calendars.length === 0) {
|
|
117
|
-
throw new Error(
|
|
168
|
+
throw new Error(
|
|
169
|
+
`No calendars found for route ${routeId} on ${moment(config.chartDate, 'YYYYMMDD').format('MMM D, YYYY')}. Try changing the chartDate in your config.json file to a date that has service.`,
|
|
170
|
+
);
|
|
118
171
|
}
|
|
119
172
|
|
|
120
173
|
const serviceIds = calendars.map((calendar) => calendar.service_id);
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
174
|
+
let trips = db
|
|
175
|
+
.prepare(
|
|
176
|
+
`SELECT service_id, trip_id, trip_headsign, direction_id, shape_id FROM trips where route_id = ? AND service_id IN (${serviceIds.map(() => '?').join(', ')})`,
|
|
177
|
+
)
|
|
178
|
+
.all(routeId, ...serviceIds);
|
|
124
179
|
|
|
125
180
|
if (trips.length === 0) {
|
|
126
|
-
throw new Error(
|
|
181
|
+
throw new Error(
|
|
182
|
+
`No trips found for route ${routeId} on ${moment(config.chartDate, 'YYYYMMDD').format('MMM D, YYYY')}`,
|
|
183
|
+
);
|
|
127
184
|
}
|
|
128
185
|
|
|
129
186
|
const shapeIds = uniq(map(trips, 'shape_id'));
|
|
@@ -135,23 +192,83 @@ const getDataforChart = (config, routeId) => {
|
|
|
135
192
|
for (const trip of trips) {
|
|
136
193
|
const stoptimes = getStoptimes(
|
|
137
194
|
{
|
|
138
|
-
trip_id: trip.trip_id
|
|
195
|
+
trip_id: trip.trip_id,
|
|
139
196
|
},
|
|
140
197
|
[
|
|
141
198
|
'arrival_time',
|
|
142
199
|
'departure_time',
|
|
143
200
|
'stop_id',
|
|
144
201
|
'shape_dist_traveled',
|
|
145
|
-
'timepoint'
|
|
202
|
+
'timepoint',
|
|
146
203
|
],
|
|
147
|
-
[
|
|
148
|
-
['stop_sequence', 'ASC']
|
|
149
|
-
]
|
|
204
|
+
[['stop_sequence', 'ASC']],
|
|
150
205
|
);
|
|
151
206
|
|
|
152
207
|
trip.stoptimes = stoptimes.filter((stoptime) => isTimepoint(stoptime));
|
|
153
208
|
}
|
|
154
209
|
|
|
210
|
+
const frequencies = db
|
|
211
|
+
.prepare(
|
|
212
|
+
`SELECT * FROM frequencies WHERE trip_id IN (${trips.map((trip) => '?').join(', ')})`,
|
|
213
|
+
)
|
|
214
|
+
.all(...trips.map((trip) => trip.trip_id));
|
|
215
|
+
|
|
216
|
+
// Create trips from frequencies.txt
|
|
217
|
+
if (frequencies.length > 0) {
|
|
218
|
+
for (const frequency of frequencies) {
|
|
219
|
+
const exampleTrip = trips.find(
|
|
220
|
+
(trip) => trip.trip_id === frequency.trip_id,
|
|
221
|
+
);
|
|
222
|
+
if (!exampleTrip) {
|
|
223
|
+
console.log(`No example trip found for frequency ${frequency.trip_id}`);
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const stoptimesOffsets = exampleTrip.stoptimes.map((stoptime, index) => ({
|
|
228
|
+
departure_offset: fromGTFSTime(stoptime.departure_time).diff(
|
|
229
|
+
fromGTFSTime(exampleTrip.stoptimes[0].departure_time),
|
|
230
|
+
'seconds',
|
|
231
|
+
),
|
|
232
|
+
arrival_offset: fromGTFSTime(stoptime.arrival_time).diff(
|
|
233
|
+
fromGTFSTime(exampleTrip.stoptimes[0].arrival_time),
|
|
234
|
+
'seconds',
|
|
235
|
+
),
|
|
236
|
+
}));
|
|
237
|
+
|
|
238
|
+
for (
|
|
239
|
+
let offset = 0;
|
|
240
|
+
fromGTFSTime(frequency.start_time)
|
|
241
|
+
.add(offset, 'seconds')
|
|
242
|
+
.isBefore(fromGTFSTime(frequency.end_time));
|
|
243
|
+
offset += frequency.headway_secs
|
|
244
|
+
) {
|
|
245
|
+
trips.push({
|
|
246
|
+
...exampleTrip,
|
|
247
|
+
trip_id: `${exampleTrip.trip_id}_${toGTFSTime(fromGTFSTime(frequency.start_time).add(offset, 'seconds'))}`,
|
|
248
|
+
stoptimes: exampleTrip.stoptimes.map((stoptime, index) => ({
|
|
249
|
+
...stoptime,
|
|
250
|
+
arrival_time: toGTFSTime(
|
|
251
|
+
fromGTFSTime(frequency.start_time)
|
|
252
|
+
.add(offset, 'seconds')
|
|
253
|
+
.add(stoptimesOffsets[index].arrival_offset, 'seconds'),
|
|
254
|
+
),
|
|
255
|
+
departure_time: toGTFSTime(
|
|
256
|
+
fromGTFSTime(frequency.start_time)
|
|
257
|
+
.add(offset, 'seconds')
|
|
258
|
+
.add(stoptimesOffsets[index].departure_offset, 'seconds'),
|
|
259
|
+
),
|
|
260
|
+
})),
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Remove the example trips from the trips array
|
|
266
|
+
trips = trips.filter(
|
|
267
|
+
(trip) =>
|
|
268
|
+
!frequencies.some((frequency) => frequency.trip_id === trip.trip_id),
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
|
|
155
272
|
const longestTrip = findLongestTrip(trips);
|
|
156
273
|
let stations = getStationsFromTrip(longestTrip);
|
|
157
274
|
const tripDistance = max(map(stations, 'distance'));
|
|
@@ -160,15 +277,22 @@ const getDataforChart = (config, routeId) => {
|
|
|
160
277
|
// If there are two directions, get stops in other direction
|
|
161
278
|
if (size(directionGroups) > 1) {
|
|
162
279
|
const oppositeDirection = longestTrip.direction_id === 1 ? '0' : '1';
|
|
163
|
-
const longestTripOppositeDirection = findLongestTrip(
|
|
164
|
-
|
|
280
|
+
const longestTripOppositeDirection = findLongestTrip(
|
|
281
|
+
directionGroups[oppositeDirection],
|
|
282
|
+
);
|
|
283
|
+
const stationsOppositeDirection = getStationsFromTrip(
|
|
284
|
+
longestTripOppositeDirection,
|
|
285
|
+
);
|
|
165
286
|
|
|
166
287
|
reverseStationDistances(stationsOppositeDirection, tripDistance);
|
|
167
288
|
|
|
168
289
|
stations = [...stations, ...stationsOppositeDirection];
|
|
169
290
|
}
|
|
170
291
|
|
|
171
|
-
const hasShapeDistance = every(
|
|
292
|
+
const hasShapeDistance = every(
|
|
293
|
+
longestTrip.stoptimes,
|
|
294
|
+
(stoptime) => stoptime.shape_dist_traveled !== null,
|
|
295
|
+
);
|
|
172
296
|
if (!hasShapeDistance) {
|
|
173
297
|
notes.push('Distance between stops calculated assuming a straight line.');
|
|
174
298
|
}
|
|
@@ -176,7 +300,7 @@ const getDataforChart = (config, routeId) => {
|
|
|
176
300
|
return {
|
|
177
301
|
trips,
|
|
178
302
|
stations,
|
|
179
|
-
notes
|
|
303
|
+
notes,
|
|
180
304
|
};
|
|
181
305
|
};
|
|
182
306
|
|
|
@@ -188,7 +312,7 @@ export function setDefaultConfig(initialConfig) {
|
|
|
188
312
|
beautify: false,
|
|
189
313
|
gtfsToChartVersion: version,
|
|
190
314
|
chartDate: moment().format('YYYYMMDD'),
|
|
191
|
-
skipImport: false
|
|
315
|
+
skipImport: false,
|
|
192
316
|
};
|
|
193
317
|
|
|
194
318
|
return { ...defaults, ...initialConfig };
|
|
@@ -206,13 +330,15 @@ export async function generateOverviewHTML(config, routes) {
|
|
|
206
330
|
const agency = first(agencies);
|
|
207
331
|
|
|
208
332
|
for (const route of routes) {
|
|
209
|
-
route.relativePath = config.isLocal
|
|
333
|
+
route.relativePath = config.isLocal
|
|
334
|
+
? path.join('charts', sanitize(route.route_id))
|
|
335
|
+
: path.join('charts', sanitize(`${formatRouteName(route)}.html`));
|
|
210
336
|
}
|
|
211
337
|
|
|
212
338
|
const templateVars = {
|
|
213
339
|
agency,
|
|
214
340
|
config,
|
|
215
|
-
routes: sortBy(routes, (r) => Number.parseInt(r.route_short_name, 10))
|
|
341
|
+
routes: sortBy(routes, (r) => Number.parseInt(r.route_short_name, 10)),
|
|
216
342
|
};
|
|
217
343
|
return renderTemplate('overview_page', templateVars, config);
|
|
218
344
|
}
|
|
@@ -222,7 +348,7 @@ export async function generateOverviewHTML(config, routes) {
|
|
|
222
348
|
*/
|
|
223
349
|
export async function generateChartHTML(config, routeId) {
|
|
224
350
|
const routes = getRoutes({
|
|
225
|
-
route_id: routeId
|
|
351
|
+
route_id: routeId,
|
|
226
352
|
});
|
|
227
353
|
|
|
228
354
|
if (routes.length === 0) {
|
|
@@ -231,10 +357,14 @@ export async function generateChartHTML(config, routeId) {
|
|
|
231
357
|
|
|
232
358
|
const chartData = getDataforChart(config, routeId);
|
|
233
359
|
|
|
234
|
-
return renderTemplate(
|
|
235
|
-
|
|
236
|
-
|
|
360
|
+
return renderTemplate(
|
|
361
|
+
'chart_page',
|
|
362
|
+
{
|
|
363
|
+
route: routes[0],
|
|
364
|
+
chartData,
|
|
365
|
+
config,
|
|
366
|
+
moment,
|
|
367
|
+
},
|
|
237
368
|
config,
|
|
238
|
-
|
|
239
|
-
}, config);
|
|
369
|
+
);
|
|
240
370
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gtfs-to-chart",
|
|
3
|
-
"version": "2.0
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Generate stringline charts of a transit routes from GTFS",
|
|
6
6
|
"keywords": [
|
|
@@ -30,11 +30,11 @@
|
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
32
|
"better-copy": "^1.0.4",
|
|
33
|
-
"chalk": "^5.
|
|
33
|
+
"chalk": "^5.4.1",
|
|
34
34
|
"connect-slashes": "^1.4.0",
|
|
35
|
-
"express": "^
|
|
36
|
-
"gtfs": "^4.
|
|
37
|
-
"js-beautify": "^1.15.
|
|
35
|
+
"express": "^5.1.0",
|
|
36
|
+
"gtfs": "^4.17.4",
|
|
37
|
+
"js-beautify": "^1.15.4",
|
|
38
38
|
"lodash-es": "^4.17.21",
|
|
39
39
|
"moment": "^2.30.1",
|
|
40
40
|
"morgan": "^1.10.0",
|
|
@@ -45,12 +45,12 @@
|
|
|
45
45
|
"sqlstring": "^2.3.3",
|
|
46
46
|
"timer-machine": "^1.1.0",
|
|
47
47
|
"untildify": "^5.0.0",
|
|
48
|
-
"yargs": "^
|
|
48
|
+
"yargs": "^18.0.0"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
51
|
"husky": "^9.1.7",
|
|
52
|
-
"lint-staged": "^
|
|
53
|
-
"prettier": "^3.
|
|
52
|
+
"lint-staged": "^16.1.0",
|
|
53
|
+
"prettier": "^3.5.3"
|
|
54
54
|
},
|
|
55
55
|
"engines": {
|
|
56
56
|
"node": ">= 20.11.0"
|