whereto-bike 0.0.6 → 0.0.8
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/package.json +1 -1
- package/packages/create-bike-blog/templates/github/workflows/ci.yml.tpl +1 -1
- package/packages/create-bike-blog/templates/github/workflows/deploy.yml.tpl +1 -1
- package/packages/create-bike-blog/templates/scripts/setup.js +1 -1
- package/src/components/WikiHome.astro +1 -1
- package/src/components/admin/RideEditor.tsx +4 -3
- package/src/i18n/en.json +1 -1
- package/src/i18n/es.json +1 -1
- package/src/integrations/admin-routes.ts +5 -2
- package/src/lib/content/content-types.server.ts +1 -0
- package/src/lib/csp.ts +1 -1
- package/src/loaders/admin-rides.ts +21 -6
- package/src/loaders/rides.ts +59 -1
- package/src/views/admin/ride-detail.astro +2 -1
- package/src/views/routes/routes-index.json.ts +4 -2
- package/src/views/stats.astro +3 -2
- package/src/virtual-modules.d.ts +2 -2
- package/sync.js +17 -0
package/package.json
CHANGED
|
@@ -11,7 +11,7 @@ const config = getCityConfig();
|
|
|
11
11
|
const { published, filterTags, routeTagsMap } = await loadHomepageData();
|
|
12
12
|
---
|
|
13
13
|
|
|
14
|
-
<p class="site-identity-line">{t('home.identity', locale, {
|
|
14
|
+
<p class="site-identity-line">{t('home.identity', locale, { name: config.name })}</p>
|
|
15
15
|
<TagFilter tags={filterTags} />
|
|
16
16
|
<ul class="routes-grid">
|
|
17
17
|
{published.map((route, i) => {
|
|
@@ -13,6 +13,7 @@ import type { StravaImportResult } from './StravaActivityBrowser';
|
|
|
13
13
|
import { useEditorState } from './useEditorState';
|
|
14
14
|
import { useFormValidation } from './useFormValidation';
|
|
15
15
|
import { useDragDrop } from '../../lib/hooks';
|
|
16
|
+
import { paths } from '../../lib/paths';
|
|
16
17
|
import { useUnsavedGuard } from '../../lib/hooks/use-unsaved-guard';
|
|
17
18
|
import { slugify } from '../../lib/slug';
|
|
18
19
|
import SlugEditor from './SlugEditor';
|
|
@@ -118,8 +119,8 @@ export default function RideEditor({ initialData, cdnUrl, videosCdnUrl, videoPre
|
|
|
118
119
|
);
|
|
119
120
|
if (!res.ok || cancelled) return;
|
|
120
121
|
const data = await res.json();
|
|
121
|
-
const
|
|
122
|
-
if (
|
|
122
|
+
const countryName = data?.address?.country;
|
|
123
|
+
if (countryName && !cancelled) setCountry(countryName);
|
|
123
124
|
} catch { /* Nominatim failures are non-critical */ }
|
|
124
125
|
})();
|
|
125
126
|
|
|
@@ -424,7 +425,7 @@ export default function RideEditor({ initialData, cdnUrl, videosCdnUrl, videoPre
|
|
|
424
425
|
<EditorActions
|
|
425
426
|
error={error} githubUrl={githubUrl} saved={saved} saving={saving}
|
|
426
427
|
onSave={handleSave} contentType="ride"
|
|
427
|
-
viewLink={
|
|
428
|
+
viewLink={paths.ride(initialData.slug, initialData.tour_slug)}
|
|
428
429
|
showLicenseNotice={false}
|
|
429
430
|
/>
|
|
430
431
|
</div>
|
package/src/i18n/en.json
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
"footer.community": "Built by people who ride here",
|
|
8
8
|
"footer.powered_by": "made with whereto.bike",
|
|
9
9
|
"home.welcome": "New to biking? Experienced rider? Find your new favourite ride and explore nature!",
|
|
10
|
-
"home.identity": "Community-maintained cycling routes for {
|
|
10
|
+
"home.identity": "Community-maintained cycling routes for {name}. Built by people who ride here.",
|
|
11
11
|
"home.suggest_route": "Know a route that's missing? You can add it — share a ride you love with other cyclists.",
|
|
12
12
|
"filter.choose_tag": "choose a tag to filter:",
|
|
13
13
|
"route.elevation_gain": "Elevation Gain",
|
package/src/i18n/es.json
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
"footer.community": "Creado por personas que pedalean aquí",
|
|
8
8
|
"footer.powered_by": "hecho con whereto.bike",
|
|
9
9
|
"home.welcome": "¿Nuevo en el ciclismo? ¿Ciclista experimentado? ¡Encuentra tu nuevo recorrido favorito y explora la naturaleza!",
|
|
10
|
-
"home.identity": "Rutas ciclistas mantenidas por la comunidad para {
|
|
10
|
+
"home.identity": "Rutas ciclistas mantenidas por la comunidad para {name}. Creado por personas que pedalean aquí.",
|
|
11
11
|
"home.suggest_route": "¿Conoces una ruta que falta? Agrégala — comparte un recorrido que te guste con otros ciclistas.",
|
|
12
12
|
"filter.choose_tag": "elige una etiqueta para filtrar:",
|
|
13
13
|
"route.elevation_gain": "Desnivel positivo",
|
|
@@ -49,8 +49,11 @@ const routes = [
|
|
|
49
49
|
{ pattern: '/api/auth/strava/callback', entrypoint: view('api/auth/strava-callback.ts') },
|
|
50
50
|
// Content-type admin pages and API endpoints (from registry)
|
|
51
51
|
...contentTypeRoutes,
|
|
52
|
-
// Admin index — serves the primary content type's list page per instance type
|
|
53
|
-
|
|
52
|
+
// Admin index — serves the primary content type's list page per instance type.
|
|
53
|
+
// Skipped when a content type already claims /admin via adminListRoute (e.g. routes on wiki).
|
|
54
|
+
...(!contentTypeRoutes.some(r => r.pattern === '/admin')
|
|
55
|
+
? [{ pattern: '/admin', entrypoint: view(adminIndexView()) }]
|
|
56
|
+
: []),
|
|
54
57
|
// Non-content-type admin pages
|
|
55
58
|
{ pattern: '/admin/history', entrypoint: view('admin/history.astro') },
|
|
56
59
|
{ pattern: '/admin/users', entrypoint: view('admin/users.astro') },
|
|
@@ -48,6 +48,7 @@ export const contentTypes: ContentTypeConfig[] = [
|
|
|
48
48
|
label: 'Routes',
|
|
49
49
|
featureGate: 'hasRoutes',
|
|
50
50
|
ops: routeOps,
|
|
51
|
+
adminListRoute: { pattern: '/admin', entrypoint: 'admin/index.astro' },
|
|
51
52
|
adminDetailRoutes: [
|
|
52
53
|
{ pattern: '/admin/routes/new', entrypoint: 'admin/route-new.astro' },
|
|
53
54
|
{ pattern: '/admin/routes/[slug]', entrypoint: 'admin/route-detail.astro' },
|
package/src/lib/csp.ts
CHANGED
|
@@ -10,7 +10,7 @@ function originFrom(raw: string | undefined, fallback: string): string {
|
|
|
10
10
|
|
|
11
11
|
export function cspOrigins() {
|
|
12
12
|
const config = getCityConfig();
|
|
13
|
-
const cdn = originFrom(
|
|
13
|
+
const cdn = originFrom(config.cdn_url, config.cdn_url);
|
|
14
14
|
const videos = originFrom(config.videos_cdn_url, config.videos_cdn_url);
|
|
15
15
|
|
|
16
16
|
return { cdn, videos };
|
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
rideDateToIso,
|
|
24
24
|
nameFromFilename,
|
|
25
25
|
extractDateFromPath,
|
|
26
|
+
adjustTourYears,
|
|
26
27
|
buildSlug,
|
|
27
28
|
} from './rides';
|
|
28
29
|
import { readRideFile } from './ride-file-reader';
|
|
@@ -73,8 +74,8 @@ export interface RideStats {
|
|
|
73
74
|
by_year: Record<string, { rides: number; distance_km: number; elevation_m: number }>;
|
|
74
75
|
by_country: Record<string, { rides: number; distance_km: number }>;
|
|
75
76
|
records: {
|
|
76
|
-
longest_ride?: { slug: string; name: string; distance_km: number };
|
|
77
|
-
most_elevation?: { slug: string; name: string; elevation_m: number };
|
|
77
|
+
longest_ride?: { slug: string; name: string; distance_km: number; tour_slug?: string };
|
|
78
|
+
most_elevation?: { slug: string; name: string; elevation_m: number; tour_slug?: string };
|
|
78
79
|
longest_tour?: { slug: string; name: string; distance_km: number; days: number };
|
|
79
80
|
};
|
|
80
81
|
}
|
|
@@ -122,6 +123,20 @@ export async function loadAdminRideData(): Promise<AdminRideData> {
|
|
|
122
123
|
}
|
|
123
124
|
}
|
|
124
125
|
|
|
126
|
+
// Adjust years for multi-year tours (e.g. Dec 2022 → Jan 2023)
|
|
127
|
+
const adjustedDates = new Map<string, { year: number; month: number; day: number }>();
|
|
128
|
+
for (const tour of tours) {
|
|
129
|
+
const tourDates: { gpxPath: string; date: { year: number; month: number; day: number } }[] = [];
|
|
130
|
+
for (const ridePath of tour.ridePaths) {
|
|
131
|
+
const date = extractDateFromPath(ridePath);
|
|
132
|
+
if (date) tourDates.push({ gpxPath: ridePath, date });
|
|
133
|
+
}
|
|
134
|
+
adjustTourYears(tourDates.map(td => td.date));
|
|
135
|
+
for (const td of tourDates) {
|
|
136
|
+
adjustedDates.set(td.gpxPath, td.date);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
125
140
|
// Load tour-level metadata
|
|
126
141
|
const tourMeta = new Map<string, { name: string; description?: string; renderedDescription?: string; country?: string }>();
|
|
127
142
|
for (const tour of tours) {
|
|
@@ -165,7 +180,7 @@ export async function loadAdminRideData(): Promise<AdminRideData> {
|
|
|
165
180
|
const digest = computeFileDigest([gpxAbsPath, sidecarPath, mediaYmlPath]);
|
|
166
181
|
|
|
167
182
|
// Compute slug early for cache key
|
|
168
|
-
const date = extractDateFromPath(gpxRelPath);
|
|
183
|
+
const date = adjustedDates.get(gpxRelPath) || extractDateFromPath(gpxRelPath);
|
|
169
184
|
if (!date) continue;
|
|
170
185
|
const gpxFilename = path.basename(gpxRelPath);
|
|
171
186
|
const slug = buildSlug(date, gpxFilename, !!tourInfo);
|
|
@@ -188,7 +203,7 @@ export async function loadAdminRideData(): Promise<AdminRideData> {
|
|
|
188
203
|
const parsed = readRideFile(ridesDir, gpxRelPath, tourInfo?.slug);
|
|
189
204
|
if (!parsed) continue;
|
|
190
205
|
|
|
191
|
-
const isoDate = rideDateToIso(
|
|
206
|
+
const isoDate = rideDateToIso(date);
|
|
192
207
|
|
|
193
208
|
// Compute content hash from raw file contents
|
|
194
209
|
const contentHash = computeRideContentHash(
|
|
@@ -401,8 +416,8 @@ function buildStats(
|
|
|
401
416
|
by_year: byYear,
|
|
402
417
|
by_country: byCountry,
|
|
403
418
|
records: {
|
|
404
|
-
longest_ride: longestRide ? { slug: longestRide.slug, name: longestRide.name, distance_km: longestRide.distance_km } : undefined,
|
|
405
|
-
most_elevation: mostElevation ? { slug: mostElevation.slug, name: mostElevation.name, elevation_m: mostElevation.elevation_m } : undefined,
|
|
419
|
+
longest_ride: longestRide ? { slug: longestRide.slug, name: longestRide.name, distance_km: longestRide.distance_km, tour_slug: longestRide.tour_slug } : undefined,
|
|
420
|
+
most_elevation: mostElevation ? { slug: mostElevation.slug, name: mostElevation.name, elevation_m: mostElevation.elevation_m, tour_slug: mostElevation.tour_slug } : undefined,
|
|
406
421
|
longest_tour: longestTour ? { slug: longestTour.slug, name: longestTour.name, distance_km: longestTour.total_distance_km, days: longestTour.days } : undefined,
|
|
407
422
|
},
|
|
408
423
|
};
|
package/src/loaders/rides.ts
CHANGED
|
@@ -187,6 +187,49 @@ export function buildSlug(date: RideDate, gpxFilename: string, isTour?: boolean)
|
|
|
187
187
|
return `${yyyy}-${mm}-${dd}-${slug}`;
|
|
188
188
|
}
|
|
189
189
|
|
|
190
|
+
/**
|
|
191
|
+
* Adjust years for rides in a multi-year tour.
|
|
192
|
+
*
|
|
193
|
+
* When a tour directory lives under a single YYYY/ folder but spans a year
|
|
194
|
+
* boundary (e.g. December 2022 → January 2023), all rides initially get the
|
|
195
|
+
* directory year. This function detects the year rollover by finding the
|
|
196
|
+
* largest internal gap between sorted months. If months [1,2,3,10,11,12] are
|
|
197
|
+
* present, the gap between 3 and 10 (7 months) indicates the tour runs
|
|
198
|
+
* Oct→Mar across a year boundary — months 1–3 become year + 1.
|
|
199
|
+
*
|
|
200
|
+
* Only considers non-circular gaps (between consecutive sorted months).
|
|
201
|
+
* The wrap-around gap from the highest to lowest month is ignored — it's
|
|
202
|
+
* always large for any clustered set of months and would cause false positives.
|
|
203
|
+
*/
|
|
204
|
+
export function adjustTourYears(dates: RideDate[]): void {
|
|
205
|
+
if (dates.length < 2) return;
|
|
206
|
+
|
|
207
|
+
const months = [...new Set(dates.map(d => d.month))].sort((a, b) => a - b);
|
|
208
|
+
if (months.length < 2) return;
|
|
209
|
+
|
|
210
|
+
// Find the largest gap between consecutive sorted months (non-circular).
|
|
211
|
+
// A gap > 6 means the months split into two clusters across a year boundary.
|
|
212
|
+
let maxGap = 0;
|
|
213
|
+
let gapAfterMonth = 0;
|
|
214
|
+
for (let i = 1; i < months.length; i++) {
|
|
215
|
+
const gap = months[i] - months[i - 1];
|
|
216
|
+
if (gap > maxGap) {
|
|
217
|
+
maxGap = gap;
|
|
218
|
+
gapAfterMonth = months[i - 1];
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (maxGap <= 6) return;
|
|
223
|
+
|
|
224
|
+
// Months at or before gapAfterMonth are the "early" cluster (next year).
|
|
225
|
+
// Months after gapAfterMonth are the "late" cluster (directory year).
|
|
226
|
+
for (const date of dates) {
|
|
227
|
+
if (date.month <= gapAfterMonth) {
|
|
228
|
+
date.year += 1;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
190
233
|
/** Build an ISO date string from a RideDate. */
|
|
191
234
|
export function rideDateToIso(date: RideDate): string {
|
|
192
235
|
const mm = String(date.month).padStart(2, '0');
|
|
@@ -253,6 +296,21 @@ export function rideLoader(): Loader {
|
|
|
253
296
|
}
|
|
254
297
|
}
|
|
255
298
|
|
|
299
|
+
// Adjust years for multi-year tours (e.g. Dec 2022 → Jan 2023)
|
|
300
|
+
const adjustedDates = new Map<string, RideDate>();
|
|
301
|
+
for (const tour of tours) {
|
|
302
|
+
const tourDates: { gpxPath: string; date: RideDate }[] = [];
|
|
303
|
+
for (const ridePath of tour.ridePaths) {
|
|
304
|
+
const date = extractDateFromPath(ridePath);
|
|
305
|
+
if (date) tourDates.push({ gpxPath: ridePath, date });
|
|
306
|
+
}
|
|
307
|
+
const dates = tourDates.map(td => td.date);
|
|
308
|
+
adjustTourYears(dates);
|
|
309
|
+
for (const td of tourDates) {
|
|
310
|
+
adjustedDates.set(td.gpxPath, td.date);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
256
314
|
// Load privacy zone config once (if configured for this city)
|
|
257
315
|
const privacyZone = getCityConfig().privacy_zone;
|
|
258
316
|
|
|
@@ -265,7 +323,7 @@ export function rideLoader(): Loader {
|
|
|
265
323
|
const mediaYmlDigestPath = gpxAbsPath.replace(/\.gpx$/i, '-media.yml');
|
|
266
324
|
|
|
267
325
|
// Incremental caching — need date+slug before checking digest
|
|
268
|
-
const date = extractDateFromPath(gpxRelPath);
|
|
326
|
+
const date = adjustedDates.get(gpxRelPath) || extractDateFromPath(gpxRelPath);
|
|
269
327
|
if (!date) {
|
|
270
328
|
logger.warn(`Could not extract date from path: ${gpxRelPath}`);
|
|
271
329
|
continue;
|
|
@@ -10,6 +10,7 @@ import { CITY, VIDEO_PREFIX } from '../../lib/config/config';
|
|
|
10
10
|
import { getCityConfig, isBlogInstance } from '../../lib/config/city-config';
|
|
11
11
|
import { loadDetailPageData } from '../../lib/content/load-admin-content.server';
|
|
12
12
|
import { rideDetailFromCache } from '../../lib/models/ride-model';
|
|
13
|
+
import { paths } from '@/lib/paths';
|
|
13
14
|
import { t } from '@/i18n';
|
|
14
15
|
import tours from 'virtual:bike-app/tours';
|
|
15
16
|
import { db } from '@/lib/get-db';
|
|
@@ -91,7 +92,7 @@ try {
|
|
|
91
92
|
title={title}
|
|
92
93
|
backLink="/admin/rides"
|
|
93
94
|
backLabel="Back to rides"
|
|
94
|
-
viewLink={slug ? (
|
|
95
|
+
viewLink={slug ? paths.ride(slug, rideData.tour_slug) : undefined}
|
|
95
96
|
hideTitle
|
|
96
97
|
user={Astro.locals.user}
|
|
97
98
|
currentTab="routes"
|
|
@@ -7,7 +7,9 @@ import { routeShape } from '../../lib/route-insights';
|
|
|
7
7
|
import { difficultyLabel, scoreRoute } from '../../lib/difficulty';
|
|
8
8
|
import { paths, routeSlug } from '../../lib/paths';
|
|
9
9
|
import { defaultLocale } from '../../lib/i18n/locale-utils';
|
|
10
|
-
|
|
10
|
+
// NOTE: This is a single-output endpoint (not parameterized), so it must NOT
|
|
11
|
+
// use filterByBuildPlan — doing so would produce a partial index in incremental
|
|
12
|
+
// mode, overwriting the full one from the previous build.
|
|
11
13
|
|
|
12
14
|
export const prerender = true;
|
|
13
15
|
|
|
@@ -21,7 +23,7 @@ export const GET: APIRoute = async ({ currentLocale }) => {
|
|
|
21
23
|
const config = getCityConfig();
|
|
22
24
|
const locale = currentLocale || defaultLocale();
|
|
23
25
|
const allRoutes = await getCollection('routes');
|
|
24
|
-
const published =
|
|
26
|
+
const published = allRoutes.filter(isPublished);
|
|
25
27
|
|
|
26
28
|
const allScores = published
|
|
27
29
|
.map(r => scoreRoute(r))
|
package/src/views/stats.astro
CHANGED
|
@@ -3,6 +3,7 @@ export const prerender = true;
|
|
|
3
3
|
import Base from '@/layouts/Base.astro';
|
|
4
4
|
import stats from 'virtual:bike-app/ride-stats';
|
|
5
5
|
import { getCityConfig } from '@/lib/config/city-config';
|
|
6
|
+
import { paths } from '@/lib/paths';
|
|
6
7
|
import { t } from '@/i18n';
|
|
7
8
|
|
|
8
9
|
const config = getCityConfig();
|
|
@@ -55,7 +56,7 @@ const countryKeys = Object.keys(stats.by_country).sort((a, b) =>
|
|
|
55
56
|
{stats.records.longest_ride && (
|
|
56
57
|
<div class="stats-record-card">
|
|
57
58
|
<span class="stats-record-card--label">{t('stats.longest_ride', locale)}</span>
|
|
58
|
-
<a href={
|
|
59
|
+
<a href={paths.ride(stats.records.longest_ride.slug, stats.records.longest_ride.tour_slug)} class="stats-record-card--value">
|
|
59
60
|
{stats.records.longest_ride.name}
|
|
60
61
|
</a>
|
|
61
62
|
<span class="stats-record-card--detail">{stats.records.longest_ride.distance_km} km</span>
|
|
@@ -64,7 +65,7 @@ const countryKeys = Object.keys(stats.by_country).sort((a, b) =>
|
|
|
64
65
|
{stats.records.most_elevation && (
|
|
65
66
|
<div class="stats-record-card">
|
|
66
67
|
<span class="stats-record-card--label">{t('stats.most_elevation', locale)}</span>
|
|
67
|
-
<a href={
|
|
68
|
+
<a href={paths.ride(stats.records.most_elevation.slug, stats.records.most_elevation.tour_slug)} class="stats-record-card--value">
|
|
68
69
|
{stats.records.most_elevation.name}
|
|
69
70
|
</a>
|
|
70
71
|
<span class="stats-record-card--detail">{stats.records.most_elevation.elevation_m.toLocaleString()} m</span>
|
package/src/virtual-modules.d.ts
CHANGED
|
@@ -294,8 +294,8 @@ interface _RideStats {
|
|
|
294
294
|
by_year: Record<string, { rides: number; distance_km: number; elevation_m: number }>;
|
|
295
295
|
by_country: Record<string, { rides: number; distance_km: number }>;
|
|
296
296
|
records: {
|
|
297
|
-
longest_ride?: { slug: string; name: string; distance_km: number };
|
|
298
|
-
most_elevation?: { slug: string; name: string; elevation_m: number };
|
|
297
|
+
longest_ride?: { slug: string; name: string; distance_km: number; tour_slug?: string };
|
|
298
|
+
most_elevation?: { slug: string; name: string; elevation_m: number; tour_slug?: string };
|
|
299
299
|
longest_tour?: { slug: string; name: string; distance_km: number; days: number };
|
|
300
300
|
};
|
|
301
301
|
}
|
package/sync.js
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* - scripts/setup.js — interactive setup helper
|
|
12
12
|
* - astro.config.mjs — Astro config
|
|
13
13
|
* - tsconfig.json — TypeScript config
|
|
14
|
+
* - package.json — scripts only (merged, preserving name/deps)
|
|
14
15
|
*/
|
|
15
16
|
|
|
16
17
|
import fs from 'node:fs';
|
|
@@ -101,6 +102,22 @@ for (const rel of sourceFiles) {
|
|
|
101
102
|
}
|
|
102
103
|
}
|
|
103
104
|
|
|
105
|
+
// --- Sync package.json scripts from template ---
|
|
106
|
+
const templatePkgPath = path.join(templateRoot, 'package.json.tpl');
|
|
107
|
+
if (fs.existsSync(templatePkgPath)) {
|
|
108
|
+
const templatePkg = JSON.parse(renderTemplate(fs.readFileSync(templatePkgPath, 'utf-8'), vars));
|
|
109
|
+
const blogPkgPath = path.join(cwd, 'package.json');
|
|
110
|
+
const blogPkg = JSON.parse(fs.readFileSync(blogPkgPath, 'utf-8'));
|
|
111
|
+
|
|
112
|
+
const before = JSON.stringify(blogPkg.scripts);
|
|
113
|
+
blogPkg.scripts = { ...blogPkg.scripts, ...templatePkg.scripts };
|
|
114
|
+
if (JSON.stringify(blogPkg.scripts) !== before) {
|
|
115
|
+
fs.writeFileSync(blogPkgPath, JSON.stringify(blogPkg, null, 2) + '\n');
|
|
116
|
+
console.log(' updated package.json (scripts)');
|
|
117
|
+
updated++;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
104
121
|
// --- Summary ---
|
|
105
122
|
if (updated === 0) {
|
|
106
123
|
console.log(' Everything is up to date.');
|