diff --git a/.gitattributes b/.gitattributes index b39d7370623649d53d1a6e65701fe35a51c7f2ec..deea3b31efc725f501994000bbb402ffee67834a 100644 --- a/.gitattributes +++ b/.gitattributes @@ -32,3 +32,13 @@ client/public/fonts/Poppins-Regular.ttf filter=lfs diff=lfs merge=lfs -text client/public/fonts/Poppins-SemiBold.ttf filter=lfs diff=lfs merge=lfs -text client/public/icons/trek-loading-dark.gif filter=lfs diff=lfs merge=lfs -text client/public/icons/trek-loading-light.gif filter=lfs diff=lfs merge=lfs -text +docs/TREK-Generated-by-MCP.pdf filter=lfs diff=lfs merge=lfs -text +docs/logo-trek-dark.gif filter=lfs diff=lfs merge=lfs -text +docs/logo-trek-light.gif filter=lfs diff=lfs merge=lfs -text +docs/screenshots/atlas.png filter=lfs diff=lfs merge=lfs -text +docs/screenshots/budget.png filter=lfs diff=lfs merge=lfs -text +docs/screenshots/dashboard.png filter=lfs diff=lfs merge=lfs -text +docs/screenshots/journey.png filter=lfs diff=lfs merge=lfs -text +docs/screenshots/trip-iceland.png filter=lfs diff=lfs merge=lfs -text +docs/screenshots/trip-planner.png filter=lfs diff=lfs merge=lfs -text +docs/screenshots/vacay.png filter=lfs diff=lfs merge=lfs -text diff --git a/client/tests/environment/jsdom-native-abort.ts b/client/tests/environment/jsdom-native-abort.ts new file mode 100644 index 0000000000000000000000000000000000000000..1413dd8ae1b40fc1e027e468cee5aaa82fe2d737 --- /dev/null +++ b/client/tests/environment/jsdom-native-abort.ts @@ -0,0 +1,38 @@ +/** + * Custom Vitest environment that extends jsdom but preserves the native + * Node.js AbortController and AbortSignal. + * + * Problem: jsdom replaces globalThis.AbortController and AbortSignal with its + * own implementations. Node.js's undici-based fetch validates signals via + * `signal instanceof AbortSignal` against its own native class reference. + * jsdom's AbortSignal instances fail this check, causing fetch to throw: + * TypeError: RequestInit: Expected signal ("AbortSignal {}") to be an + * instance of AbortSignal. + * + * Fix: after jsdom installs its globals, restore the native AbortController + * and AbortSignal so fetch works correctly in tests. + */ + +import { builtinEnvironments } from 'vitest/environments'; + +const jsdomEnv = builtinEnvironments.jsdom; + +export default { + name: 'jsdom-native-abort', + transformMode: 'web' as const, + + async setup(global: typeof globalThis, options: Record) { + // Capture native AbortController/AbortSignal BEFORE jsdom patches them + const NativeAbortController = global.AbortController; + const NativeAbortSignal = global.AbortSignal; + + // Run standard jsdom setup (installs jsdom globals, including its own AbortController) + const env = await jsdomEnv.setup(global, options as Parameters[1]); + + // Restore native AbortController so Node.js fetch (undici) accepts the signals + global.AbortController = NativeAbortController; + global.AbortSignal = NativeAbortSignal; + + return env; + }, +}; diff --git a/client/tests/helpers/factories.ts b/client/tests/helpers/factories.ts new file mode 100644 index 0000000000000000000000000000000000000000..fec8f1a42813773cf9a6a12da81dcdfdd794e954 --- /dev/null +++ b/client/tests/helpers/factories.ts @@ -0,0 +1,304 @@ +/** + * Pure data builder functions for frontend tests. + * These return typed objects matching interfaces in src/types.ts. + * They do NOT touch a database. + */ + +import type { + User, + Trip, + Day, + Place, + Assignment, + DayNote, + PackingItem, + TodoItem, + BudgetItem, + Reservation, + TripFile, + Tag, + Category, + Settings, + AppConfig, +} from '../../src/types'; + +// ── Counters ────────────────────────────────────────────────────────────────── + +let _seq = 0; +function next(): number { + return ++_seq; +} + +// ── InAppNotification (local interface, not in types.ts) ────────────────────── + +export interface InAppNotification { + id: number; + type: string; + message: string; + read: boolean; + created_at: string; + trip_id?: number | null; +} + +// ── Builders ────────────────────────────────────────────────────────────────── + +export function buildUser(overrides: Partial = {}): User { + const id = next(); + return { + id, + username: `user${id}`, + email: `user${id}@example.com`, + role: 'user', + avatar_url: null, + maps_api_key: null, + created_at: '2025-01-01T00:00:00.000Z', + mfa_enabled: false, + must_change_password: false, + ...overrides, + }; +} + +export function buildAdmin(overrides: Partial = {}): User { + return buildUser({ role: 'admin', ...overrides }); +} + +export function buildTrip(overrides: Partial = {}): Trip { + const id = next(); + return { + id, + user_id: 1, + title: `Trip ${id}`, + description: null, + start_date: '2025-06-01', + end_date: '2025-06-05', + currency: 'EUR', + cover_image: null, + is_archived: 0, + reminder_days: 7, + created_at: '2025-01-01T00:00:00.000Z', + updated_at: '2025-01-01T00:00:00.000Z', + ...overrides, + }; +} + +export function buildDay(overrides: Partial = {}): Day { + const id = next(); + return { + id, + trip_id: 1, + date: '2025-06-01', + title: null, + notes: null, + assignments: [], + notes_items: [], + ...overrides, + }; +} + +export function buildPlace(overrides: Partial = {}): Place { + const id = next(); + return { + id, + trip_id: 1, + name: `Place ${id}`, + description: null, + lat: 48.8566, + lng: 2.3522, + address: null, + category_id: null, + price: null, + currency: null, + image_url: null, + google_place_id: null, + osm_id: null, + route_geometry: null, + place_time: null, + end_time: null, + duration_minutes: 60, + notes: null, + transport_mode: 'walking', + website: null, + phone: null, + created_at: '2025-01-01T00:00:00.000Z', + ...overrides, + }; +} + +export function buildAssignment(overrides: Partial = {}): Assignment { + const id = next(); + const place = overrides.place ?? buildPlace(); + return { + id, + day_id: 1, + place_id: place.id, + order_index: 0, + notes: null, + place, + ...overrides, + }; +} + +export function buildDayNote(overrides: Partial = {}): DayNote { + const id = next(); + return { + id, + day_id: 1, + text: 'Test note', + time: null, + icon: null, + sort_order: 0, + created_at: '2025-01-01T00:00:00.000Z', + ...overrides, + }; +} + +export function buildPackingItem(overrides: Partial = {}): PackingItem { + const id = next(); + return { + id, + trip_id: 1, + name: `Packing item ${id}`, + category: null, + checked: 0, + sort_order: 0, + quantity: 1, + ...overrides, + }; +} + +export function buildTodoItem(overrides: Partial = {}): TodoItem { + const id = next(); + return { + id, + trip_id: 1, + name: `Todo ${id}`, + category: null, + checked: 0, + sort_order: 0, + due_date: null, + description: null, + assigned_user_id: null, + priority: 0, + ...overrides, + }; +} + +export function buildBudgetItem(overrides: Partial = {}): BudgetItem { + const id = next(); + return { + id, + trip_id: 1, + category: 'Other', + name: `Budget item ${id}`, + total_price: 100, + persons: 1, + days: null, + note: null, + sort_order: 0, + members: [], + expense_date: null, + created_at: '2025-01-01T00:00:00.000Z', + ...overrides, + }; +} + +export function buildReservation(overrides: Partial = {}): Reservation { + const id = next(); + return { + id, + trip_id: 1, + title: `Reservation ${id}`, + type: 'restaurant', + status: 'confirmed', + reservation_time: null, + reservation_end_time: null, + location: null, + confirmation_number: null, + notes: null, + created_at: '2025-01-01T00:00:00.000Z', + ...overrides, + }; +} + +export function buildTripFile(overrides: Partial = {}): TripFile { + const id = next(); + return { + id, + trip_id: 1, + filename: 'test.pdf', + original_name: 'test.pdf', + mime_type: 'application/pdf', + url: `/api/trips/1/files/${id}/download`, + created_at: '2025-01-01T00:00:00.000Z', + ...overrides, + }; +} + +export function buildTag(overrides: Partial = {}): Tag { + const id = next(); + return { + id, + name: `Tag ${id}`, + color: '#ff0000', + user_id: 1, + ...overrides, + }; +} + +export function buildCategory(overrides: Partial = {}): Category { + const id = next(); + return { + id, + name: `Category ${id}`, + color: '#6366f1', + icon: 'restaurant', + user_id: 1, + ...overrides, + }; +} + +export function buildSettings(overrides: Partial = {}): Settings { + return { + map_tile_url: '', + default_lat: 48.8566, + default_lng: 2.3522, + default_zoom: 10, + dark_mode: false, + default_currency: 'USD', + language: 'en', + temperature_unit: 'fahrenheit', + time_format: '12h', + show_place_description: false, + blur_booking_codes: false, + ...overrides, + }; +} + +export function buildInAppNotification(overrides: Partial = {}): InAppNotification { + const id = next(); + return { + id, + type: 'trip_invite', + message: `Notification ${id}`, + read: false, + created_at: '2025-01-01T00:00:00.000Z', + trip_id: null, + ...overrides, + }; +} + +export function buildAppConfig(overrides: Partial = {}): AppConfig { + return { + has_users: true, + allow_registration: true, + demo_mode: false, + oidc_configured: false, + oidc_only_mode: false, + password_login: true, + password_registration: true, + oidc_login: true, + oidc_registration: true, + env_override_oidc_only: false, + ...overrides, + }; +} diff --git a/client/tests/helpers/msw/handlers/addons.ts b/client/tests/helpers/msw/handlers/addons.ts new file mode 100644 index 0000000000000000000000000000000000000000..8e2f84687aa5f0da9d641e5570206144b61b5b43 --- /dev/null +++ b/client/tests/helpers/msw/handlers/addons.ts @@ -0,0 +1,13 @@ +import { http, HttpResponse } from 'msw'; + +export const addonHandlers = [ + http.get('/api/addons', () => { + return HttpResponse.json({ + bagTracking: false, + addons: [ + { id: 'vacay', name: 'Vacay', type: 'feature', icon: 'calendar', enabled: true }, + { id: 'atlas', name: 'Atlas', type: 'feature', icon: 'map', enabled: true }, + ], + }); + }), +]; diff --git a/client/tests/helpers/msw/handlers/admin.ts b/client/tests/helpers/msw/handlers/admin.ts new file mode 100644 index 0000000000000000000000000000000000000000..35d245c67b9fe33fb2a09f2a1ebba7964d62fcfb --- /dev/null +++ b/client/tests/helpers/msw/handlers/admin.ts @@ -0,0 +1,137 @@ +import { http, HttpResponse } from 'msw'; +import { buildUser, buildAdmin } from '../../factories'; + +export const adminHandlers = [ + http.get('/api/admin/users', () => { + const user1 = buildUser({ username: 'alice', email: 'alice@example.com' }); + const admin1 = buildAdmin({ username: 'admin', email: 'admin@example.com' }); + return HttpResponse.json({ users: [admin1, user1] }); + }), + + http.post('/api/admin/users', async ({ request }) => { + const body = await request.json() as Record; + const user = buildUser({ ...body }); + return HttpResponse.json({ user }); + }), + + http.put('/api/admin/users/:id', async ({ params, request }) => { + const body = await request.json() as Record; + const user = buildUser({ id: Number(params.id), ...body }); + return HttpResponse.json({ user }); + }), + + http.delete('/api/admin/users/:id', () => { + return HttpResponse.json({ success: true }); + }), + + http.get('/api/admin/stats', () => { + return HttpResponse.json({ + totalUsers: 2, + totalTrips: 5, + totalPlaces: 42, + totalFiles: 8, + }); + }), + + http.get('/api/admin/invites', () => { + return HttpResponse.json({ invites: [] }); + }), + + http.post('/api/admin/invites', async ({ request }) => { + const body = await request.json() as Record; + return HttpResponse.json({ invite: { id: 1, token: 'test-invite-token', ...body } }); + }), + + http.delete('/api/admin/invites/:id', () => { + return HttpResponse.json({ success: true }); + }), + + http.get('/api/admin/oidc', () => { + return HttpResponse.json({ + issuer: '', + client_id: '', + client_secret: '', + client_secret_set: false, + display_name: '', + oidc_only: false, + discovery_url: '', + }); + }), + + http.put('/api/admin/oidc', async ({ request }) => { + const body = await request.json() as Record; + return HttpResponse.json({ ...body }); + }), + + http.get('/api/admin/version-check', () => { + return HttpResponse.json({ update_available: false, latest: '1.0.0', current: '1.0.0' }); + }), + + http.get('/api/admin/bag-tracking', () => { + return HttpResponse.json({ enabled: false }); + }), + + http.put('/api/admin/bag-tracking', async ({ request }) => { + const body = await request.json() as Record; + return HttpResponse.json({ enabled: body.enabled }); + }), + + http.get('/api/admin/addons', () => { + return HttpResponse.json({ addons: [] }); + }), + + http.get('/api/admin/packing-templates', () => { + return HttpResponse.json({ templates: [] }); + }), + + http.get('/api/admin/audit-log', () => { + return HttpResponse.json({ logs: [], total: 0 }); + }), + + http.get('/api/admin/mcp-tokens', () => { + return HttpResponse.json({ tokens: [] }); + }), + + http.get('/api/admin/oauth-sessions', () => { + return HttpResponse.json({ sessions: [] }); + }), + + http.delete('/api/admin/oauth-sessions/:id', () => { + return HttpResponse.json({ success: true }); + }), + + http.delete('/api/admin/mcp-tokens/:id', () => { + return HttpResponse.json({ success: true }); + }), + + http.get('/api/admin/permissions', () => { + return HttpResponse.json({ permissions: {} }); + }), + + http.get('/api/admin/notification-preferences', () => { + return HttpResponse.json({ + event_types: [], + available_channels: {}, + implemented_combos: {}, + preferences: {}, + }); + }), + + // Auth settings endpoints used by AdminPage + http.get('/api/auth/app-settings', () => { + return HttpResponse.json({}); + }), + + http.put('/api/auth/app-settings', async ({ request }) => { + const body = await request.json() as Record; + return HttpResponse.json({ ...body }); + }), + + http.get('/api/auth/me/settings', () => { + return HttpResponse.json({ settings: { maps_api_key: '', openweather_api_key: '' } }); + }), + + http.get('/api/auth/validate-keys', () => { + return HttpResponse.json({ maps: true, weather: true }); + }), +]; diff --git a/client/tests/helpers/msw/handlers/assignments.ts b/client/tests/helpers/msw/handlers/assignments.ts new file mode 100644 index 0000000000000000000000000000000000000000..62065badc0a3016d0a3fa2e99e46d908acee60ea --- /dev/null +++ b/client/tests/helpers/msw/handlers/assignments.ts @@ -0,0 +1,28 @@ +import { http, HttpResponse } from 'msw'; +import { buildAssignment, buildPlace } from '../../factories'; + +export const assignmentsHandlers = [ + http.post('/api/trips/:id/days/:dayId/assignments', async ({ params, request }) => { + const body = await request.json() as { place_id: number }; + const place = buildPlace({ id: body.place_id, trip_id: Number(params.id) }); + const assignment = buildAssignment({ + day_id: Number(params.dayId), + place_id: body.place_id, + place, + order_index: 0, + }); + return HttpResponse.json({ assignment }); + }), + + http.delete('/api/trips/:id/days/:dayId/assignments/:assignmentId', () => { + return HttpResponse.json({ success: true }); + }), + + http.put('/api/trips/:id/days/:dayId/assignments/reorder', () => { + return HttpResponse.json({ success: true }); + }), + + http.put('/api/trips/:id/assignments/:assignmentId/move', () => { + return HttpResponse.json({ success: true }); + }), +]; diff --git a/client/tests/helpers/msw/handlers/auth.ts b/client/tests/helpers/msw/handlers/auth.ts new file mode 100644 index 0000000000000000000000000000000000000000..cb23efaef90fe0c69840a4c0e38ec2ca902d29e7 --- /dev/null +++ b/client/tests/helpers/msw/handlers/auth.ts @@ -0,0 +1,31 @@ +import { http, HttpResponse } from 'msw'; +import { buildUser, buildAppConfig } from '../../factories'; + +export const authHandlers = [ + http.post('/api/auth/login', () => { + const user = buildUser(); + return HttpResponse.json({ user, token: 'mock-token' }); + }), + + http.get('/api/auth/me', () => { + const user = buildUser(); + return HttpResponse.json({ user }); + }), + + http.post('/api/auth/register', () => { + const user = buildUser(); + return HttpResponse.json({ user, token: 'mock-token' }); + }), + + http.get('/api/auth/app-config', () => { + return HttpResponse.json(buildAppConfig()); + }), + + http.post('/api/auth/ws-token', () => { + return HttpResponse.json({ token: 'mock-ws-token' }); + }), + + http.post('/api/auth/logout', () => { + return HttpResponse.json({ success: true }); + }), +]; diff --git a/client/tests/helpers/msw/handlers/budget.ts b/client/tests/helpers/msw/handlers/budget.ts new file mode 100644 index 0000000000000000000000000000000000000000..7b01d37ab7b85b759fd28b95424fe0905b6bad33 --- /dev/null +++ b/client/tests/helpers/msw/handlers/budget.ts @@ -0,0 +1,38 @@ +import { http, HttpResponse } from 'msw'; +import { buildBudgetItem } from '../../factories'; + +export const budgetHandlers = [ + http.get('/api/trips/:id/budget', ({ params }) => { + return HttpResponse.json({ + items: [buildBudgetItem({ trip_id: Number(params.id) })], + }); + }), + + http.post('/api/trips/:id/budget', async ({ params, request }) => { + const body = await request.json() as Record; + const item = buildBudgetItem({ trip_id: Number(params.id), ...body }); + return HttpResponse.json({ item }); + }), + + http.put('/api/trips/:id/budget/:itemId', async ({ params, request }) => { + const body = await request.json() as Record; + const item = buildBudgetItem({ id: Number(params.itemId), trip_id: Number(params.id), ...body }); + return HttpResponse.json({ item }); + }), + + http.delete('/api/trips/:id/budget/:itemId', () => { + return HttpResponse.json({ success: true }); + }), + + http.put('/api/trips/:id/budget/:itemId/members', async ({ params, request }) => { + const body = await request.json() as { user_ids: number[] }; + const members = body.user_ids.map(uid => ({ user_id: uid, paid: 0, username: `user${uid}` })); + const item = buildBudgetItem({ id: Number(params.itemId), trip_id: Number(params.id), persons: body.user_ids.length, members }); + return HttpResponse.json({ members, item }); + }), + + http.put('/api/trips/:id/budget/:itemId/members/:userId/paid', async ({ params, request }) => { + const body = await request.json() as { paid: boolean }; + return HttpResponse.json({ success: true, paid: body.paid }); + }), +]; diff --git a/client/tests/helpers/msw/handlers/dayNotes.ts b/client/tests/helpers/msw/handlers/dayNotes.ts new file mode 100644 index 0000000000000000000000000000000000000000..13a142769a33a8b374e2cc5ed02a14585c00b25e --- /dev/null +++ b/client/tests/helpers/msw/handlers/dayNotes.ts @@ -0,0 +1,31 @@ +import { http, HttpResponse } from 'msw'; +import { buildDayNote } from '../../factories'; + +export const dayNotesHandlers = [ + http.get('/api/trips/:id/days/:dayId/notes', ({ params }) => { + return HttpResponse.json({ + notes: [buildDayNote({ day_id: Number(params.dayId) })], + }); + }), + + http.post('/api/trips/:id/days/:dayId/notes', async ({ params, request }) => { + const body = await request.json() as Record; + const note = buildDayNote({ day_id: Number(params.dayId), ...body }); + return HttpResponse.json({ note }); + }), + + http.put('/api/trips/:id/days/:dayId/notes/:noteId', async ({ params, request }) => { + const body = await request.json() as Record; + const note = buildDayNote({ id: Number(params.noteId), day_id: Number(params.dayId), ...body }); + return HttpResponse.json({ note }); + }), + + http.delete('/api/trips/:id/days/:dayId/notes/:noteId', () => { + return HttpResponse.json({ success: true }); + }), + + http.put('/api/trips/:id/days/:dayId', async ({ params, request }) => { + const body = await request.json() as Record; + return HttpResponse.json({ day: { id: Number(params.dayId), trip_id: Number(params.id), ...body } }); + }), +]; diff --git a/client/tests/helpers/msw/handlers/files.ts b/client/tests/helpers/msw/handlers/files.ts new file mode 100644 index 0000000000000000000000000000000000000000..eb03d5fe17d7f9dfe9cf58e3a7456d6e7c5c85c5 --- /dev/null +++ b/client/tests/helpers/msw/handlers/files.ts @@ -0,0 +1,19 @@ +import { http, HttpResponse } from 'msw'; +import { buildTripFile } from '../../factories'; + +export const filesHandlers = [ + http.get('/api/trips/:id/files', ({ params }) => { + return HttpResponse.json({ + files: [buildTripFile({ trip_id: Number(params.id) })], + }); + }), + + http.post('/api/trips/:id/files', ({ params }) => { + const file = buildTripFile({ trip_id: Number(params.id) }); + return HttpResponse.json({ file }); + }), + + http.delete('/api/trips/:id/files/:fileId', () => { + return HttpResponse.json({ success: true }); + }), +]; diff --git a/client/tests/helpers/msw/handlers/index.ts b/client/tests/helpers/msw/handlers/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..3459b3b26c2ad93dcc5fb314b93ffbb619474639 --- /dev/null +++ b/client/tests/helpers/msw/handlers/index.ts @@ -0,0 +1,37 @@ +import { authHandlers } from './auth'; +import { settingsHandlers } from './settings'; +import { addonHandlers } from './addons'; +import { notificationHandlers } from './notifications'; +import { vacayHandlers } from './vacay'; +import { tripsHandlers } from './trips'; +import { placesHandlers } from './places'; +import { assignmentsHandlers } from './assignments'; +import { packingHandlers } from './packing'; +import { todoHandlers } from './todo'; +import { budgetHandlers } from './budget'; +import { reservationsHandlers } from './reservations'; +import { filesHandlers } from './files'; +import { tagsHandlers } from './tags'; +import { dayNotesHandlers } from './dayNotes'; +import { adminHandlers } from './admin'; +import { sharedHandlers } from './shared'; + +export const defaultHandlers = [ + ...authHandlers, + ...settingsHandlers, + ...addonHandlers, + ...notificationHandlers, + ...vacayHandlers, + ...tripsHandlers, + ...placesHandlers, + ...assignmentsHandlers, + ...packingHandlers, + ...todoHandlers, + ...budgetHandlers, + ...reservationsHandlers, + ...filesHandlers, + ...tagsHandlers, + ...dayNotesHandlers, + ...adminHandlers, + ...sharedHandlers, +]; diff --git a/client/tests/helpers/msw/handlers/notifications.ts b/client/tests/helpers/msw/handlers/notifications.ts new file mode 100644 index 0000000000000000000000000000000000000000..f009cee0f7603805e931d32cb9d39c6d391b8554 --- /dev/null +++ b/client/tests/helpers/msw/handlers/notifications.ts @@ -0,0 +1,94 @@ +import { http, HttpResponse } from 'msw'; + +export const notificationHandlers = [ + http.get('/api/notifications/in-app', ({ request }) => { + const url = new URL(request.url); + const offset = parseInt(url.searchParams.get('offset') || '0', 10); + const limit = parseInt(url.searchParams.get('limit') || '20', 10); + + const allNotifications = Array.from({ length: 25 }, (_, i) => ({ + id: i + 1, + type: 'simple', + scope: 'trip', + target: 1, + sender_id: 2, + sender_username: 'alice', + sender_avatar: null, + recipient_id: 1, + title_key: 'notif.title', + title_params: '{}', + text_key: 'notif.text', + text_params: '{}', + positive_text_key: null, + negative_text_key: null, + response: null, + navigate_text_key: null, + navigate_target: null, + is_read: i < 5 ? 0 : 1, + created_at: '2025-01-01T00:00:00.000Z', + })); + + const page = allNotifications.slice(offset, offset + limit); + + return HttpResponse.json({ + notifications: page, + total: allNotifications.length, + unread_count: 5, + }); + }), + + http.get('/api/notifications/in-app/unread-count', () => { + return HttpResponse.json({ count: 5 }); + }), + + http.put('/api/notifications/in-app/:id/read', () => { + return HttpResponse.json({ success: true }); + }), + + http.put('/api/notifications/in-app/:id/unread', () => { + return HttpResponse.json({ success: true }); + }), + + http.put('/api/notifications/in-app/read-all', () => { + return HttpResponse.json({ success: true }); + }), + + http.delete('/api/notifications/in-app/:id', () => { + return HttpResponse.json({ success: true }); + }), + + http.delete('/api/notifications/in-app/all', () => { + return HttpResponse.json({ success: true }); + }), + + http.post('/api/notifications/test-ntfy', async () => { + return HttpResponse.json({ success: true }); + }), + + http.post('/api/notifications/in-app/:id/respond', async ({ request, params }) => { + const body = await request.json() as { response: string }; + return HttpResponse.json({ + notification: { + id: Number(params.id), + type: 'boolean', + scope: 'trip', + target: 1, + sender_id: 2, + sender_username: 'alice', + sender_avatar: null, + recipient_id: 1, + title_key: 'notif.title', + title_params: '{}', + text_key: 'notif.text', + text_params: '{}', + positive_text_key: 'accept', + negative_text_key: 'decline', + response: body.response, + navigate_text_key: null, + navigate_target: null, + is_read: 1, + created_at: '2025-01-01T00:00:00.000Z', + }, + }); + }), +]; diff --git a/client/tests/helpers/msw/handlers/packing.ts b/client/tests/helpers/msw/handlers/packing.ts new file mode 100644 index 0000000000000000000000000000000000000000..c3b0ed621d492c6c93ca0f3554d6e493bbfe1d53 --- /dev/null +++ b/client/tests/helpers/msw/handlers/packing.ts @@ -0,0 +1,26 @@ +import { http, HttpResponse } from 'msw'; +import { buildPackingItem } from '../../factories'; + +export const packingHandlers = [ + http.get('/api/trips/:id/packing', ({ params }) => { + return HttpResponse.json({ + items: [buildPackingItem({ trip_id: Number(params.id) })], + }); + }), + + http.post('/api/trips/:id/packing', async ({ params, request }) => { + const body = await request.json() as Record; + const item = buildPackingItem({ trip_id: Number(params.id), ...body }); + return HttpResponse.json({ item }); + }), + + http.put('/api/trips/:id/packing/:itemId', async ({ params, request }) => { + const body = await request.json() as Record; + const item = buildPackingItem({ id: Number(params.itemId), trip_id: Number(params.id), ...body }); + return HttpResponse.json({ item }); + }), + + http.delete('/api/trips/:id/packing/:itemId', () => { + return HttpResponse.json({ success: true }); + }), +]; diff --git a/client/tests/helpers/msw/handlers/places.ts b/client/tests/helpers/msw/handlers/places.ts new file mode 100644 index 0000000000000000000000000000000000000000..45f65a124783ecf9092ef46cb5a28b45884fad65 --- /dev/null +++ b/client/tests/helpers/msw/handlers/places.ts @@ -0,0 +1,25 @@ +import { http, HttpResponse } from 'msw'; +import { buildPlace } from '../../factories'; + +export const placesHandlers = [ + http.get('/api/trips/:id/places', ({ params }) => { + const tripId = Number(params.id); + return HttpResponse.json({ places: [buildPlace({ trip_id: tripId }), buildPlace({ trip_id: tripId })] }); + }), + + http.post('/api/trips/:id/places', async ({ params, request }) => { + const body = await request.json() as Record; + const place = buildPlace({ trip_id: Number(params.id), ...body }); + return HttpResponse.json({ place }); + }), + + http.put('/api/trips/:id/places/:placeId', async ({ params, request }) => { + const body = await request.json() as Record; + const place = buildPlace({ id: Number(params.placeId), trip_id: Number(params.id), ...body }); + return HttpResponse.json({ place }); + }), + + http.delete('/api/trips/:id/places/:placeId', () => { + return HttpResponse.json({ success: true }); + }), +]; diff --git a/client/tests/helpers/msw/handlers/reservations.ts b/client/tests/helpers/msw/handlers/reservations.ts new file mode 100644 index 0000000000000000000000000000000000000000..d99a883449dd6447739e5b5c372fea12c8106b20 --- /dev/null +++ b/client/tests/helpers/msw/handlers/reservations.ts @@ -0,0 +1,30 @@ +import { http, HttpResponse } from 'msw'; +import { buildReservation } from '../../factories'; + +export const reservationsHandlers = [ + http.get('/api/trips/:id/reservations', ({ params }) => { + return HttpResponse.json({ + reservations: [buildReservation({ trip_id: Number(params.id) })], + }); + }), + + http.post('/api/trips/:id/reservations', async ({ params, request }) => { + const body = await request.json() as Record; + const reservation = buildReservation({ trip_id: Number(params.id), ...body }); + return HttpResponse.json({ reservation }); + }), + + http.put('/api/trips/:id/reservations/:reservationId', async ({ params, request }) => { + const body = await request.json() as Record; + const reservation = buildReservation({ + id: Number(params.reservationId), + trip_id: Number(params.id), + ...body, + }); + return HttpResponse.json({ reservation }); + }), + + http.delete('/api/trips/:id/reservations/:reservationId', () => { + return HttpResponse.json({ success: true }); + }), +]; diff --git a/client/tests/helpers/msw/handlers/settings.ts b/client/tests/helpers/msw/handlers/settings.ts new file mode 100644 index 0000000000000000000000000000000000000000..99c027168105fa6436478e2920a84bcc31152fb7 --- /dev/null +++ b/client/tests/helpers/msw/handlers/settings.ts @@ -0,0 +1,16 @@ +import { http, HttpResponse } from 'msw'; +import { buildSettings } from '../../factories'; + +export const settingsHandlers = [ + http.get('/api/settings', () => { + return HttpResponse.json({ settings: buildSettings() }); + }), + + http.put('/api/settings', () => { + return HttpResponse.json({ success: true }); + }), + + http.post('/api/settings/bulk', () => { + return HttpResponse.json({ success: true }); + }), +]; diff --git a/client/tests/helpers/msw/handlers/shared.ts b/client/tests/helpers/msw/handlers/shared.ts new file mode 100644 index 0000000000000000000000000000000000000000..891f6ebbf8be8b82b32e7ea3c5269a06f9415d31 --- /dev/null +++ b/client/tests/helpers/msw/handlers/shared.ts @@ -0,0 +1,36 @@ +import { http, HttpResponse } from 'msw'; +import { buildTrip, buildDay, buildPlace } from '../../factories'; + +export const sharedHandlers = [ + http.get('/api/shared/:token', ({ params }) => { + const { token } = params; + + if (token === 'invalid-token' || token === 'expired-token') { + return new HttpResponse(null, { status: 404 }); + } + + const trip = { ...buildTrip({ start_date: '2026-07-01', end_date: '2026-07-05' }), title: 'Shared Paris Trip' }; + const day1 = buildDay({ trip_id: trip.id, date: '2026-07-01' }); + const place1 = buildPlace({ trip_id: trip.id, name: 'Eiffel Tower', lat: 48.8584, lng: 2.2945 }); + + return HttpResponse.json({ + trip, + days: [day1], + assignments: {}, + dayNotes: {}, + places: [place1], + reservations: [], + accommodations: [], + packing: [], + budget: [], + categories: [], + permissions: { + share_bookings: true, + share_packing: false, + share_budget: false, + share_collab: false, + }, + collab: [], + }); + }), +]; diff --git a/client/tests/helpers/msw/handlers/tags.ts b/client/tests/helpers/msw/handlers/tags.ts new file mode 100644 index 0000000000000000000000000000000000000000..ab8aa9419fa51f486456d35237dfa40536e1f65e --- /dev/null +++ b/client/tests/helpers/msw/handlers/tags.ts @@ -0,0 +1,24 @@ +import { http, HttpResponse } from 'msw'; +import { buildTag, buildCategory } from '../../factories'; + +export const tagsHandlers = [ + http.get('/api/tags', () => { + return HttpResponse.json({ tags: [buildTag(), buildTag()] }); + }), + + http.post('/api/tags', async ({ request }) => { + const body = await request.json() as Record; + const tag = buildTag(body); + return HttpResponse.json({ tag }); + }), + + http.get('/api/categories', () => { + return HttpResponse.json({ categories: [buildCategory(), buildCategory()] }); + }), + + http.post('/api/categories', async ({ request }) => { + const body = await request.json() as Record; + const category = buildCategory(body); + return HttpResponse.json({ category }); + }), +]; diff --git a/client/tests/helpers/msw/handlers/todo.ts b/client/tests/helpers/msw/handlers/todo.ts new file mode 100644 index 0000000000000000000000000000000000000000..e9ad6f0307998d2b3d1390ec2fe0dbf37601be36 --- /dev/null +++ b/client/tests/helpers/msw/handlers/todo.ts @@ -0,0 +1,26 @@ +import { http, HttpResponse } from 'msw'; +import { buildTodoItem } from '../../factories'; + +export const todoHandlers = [ + http.get('/api/trips/:id/todo', ({ params }) => { + return HttpResponse.json({ + items: [buildTodoItem({ trip_id: Number(params.id) })], + }); + }), + + http.post('/api/trips/:id/todo', async ({ params, request }) => { + const body = await request.json() as Record; + const item = buildTodoItem({ trip_id: Number(params.id), ...body }); + return HttpResponse.json({ item }); + }), + + http.put('/api/trips/:id/todo/:itemId', async ({ params, request }) => { + const body = await request.json() as Record; + const item = buildTodoItem({ id: Number(params.itemId), trip_id: Number(params.id), ...body }); + return HttpResponse.json({ item }); + }), + + http.delete('/api/trips/:id/todo/:itemId', () => { + return HttpResponse.json({ success: true }); + }), +]; diff --git a/client/tests/helpers/msw/handlers/trips.ts b/client/tests/helpers/msw/handlers/trips.ts new file mode 100644 index 0000000000000000000000000000000000000000..9ffde4797c9841fee3cb5c8709d7953506804007 --- /dev/null +++ b/client/tests/helpers/msw/handlers/trips.ts @@ -0,0 +1,75 @@ +import { http, HttpResponse } from 'msw'; +import { buildTrip, buildDay, buildUser, buildPlace, buildPackingItem, buildTodoItem, buildBudgetItem, buildReservation, buildTripFile } from '../../factories'; + +export const tripsHandlers = [ + // List all trips (active or archived) + http.get('/api/trips', ({ request }) => { + const url = new URL(request.url); + const archived = url.searchParams.get('archived'); + if (archived) { + return HttpResponse.json({ trips: [] }); + } + const trip1 = buildTrip({ title: 'Paris Adventure', start_date: '2026-07-01', end_date: '2026-07-10' }); + const trip2 = buildTrip({ title: 'Tokyo Trip', start_date: '2026-09-01', end_date: '2026-09-15' }); + return HttpResponse.json({ trips: [trip1, trip2] }); + }), + + http.get('/api/trips/:id', ({ params }) => { + const trip = buildTrip({ id: Number(params.id) }); + return HttpResponse.json({ trip }); + }), + + http.get('/api/trips/:id/days', ({ params }) => { + const tripId = Number(params.id); + const day1 = buildDay({ trip_id: tripId, assignments: [], notes_items: [] }); + const day2 = buildDay({ trip_id: tripId, assignments: [], notes_items: [] }); + return HttpResponse.json({ days: [day1, day2] }); + }), + + http.put('/api/trips/:id', async ({ params, request }) => { + const body = await request.json() as Record; + const trip = buildTrip({ id: Number(params.id), ...body }); + return HttpResponse.json({ trip }); + }), + + http.post('/api/trips', async ({ request }) => { + const body = await request.json() as Record; + const trip = buildTrip({ ...body }); + return HttpResponse.json({ trip }); + }), + + http.get('/api/trips/:id/members', ({ params }) => { + const owner = buildUser(); + return HttpResponse.json({ owner, members: [] }); + }), + + http.get('/api/trips/:id/accommodations', () => { + return HttpResponse.json({ accommodations: [] }); + }), + + http.get('/api/trips/:id/bundle', ({ params }) => { + const tripId = Number(params.id); + const trip = buildTrip({ id: tripId }); + const day = buildDay({ trip_id: tripId, assignments: [], notes_items: [] }); + return HttpResponse.json({ + trip, + days: [day], + places: [buildPlace({ trip_id: tripId })], + packingItems: [buildPackingItem({ trip_id: tripId })], + todoItems: [buildTodoItem({ trip_id: tripId })], + budgetItems: [buildBudgetItem({ trip_id: tripId })], + reservations: [buildReservation({ trip_id: tripId })], + files: [buildTripFile({ trip_id: tripId })], + }); + }), + + http.delete('/api/trips/:id', () => { + return HttpResponse.json({ success: true }); + }), + + http.post('/api/trips/:id/copy', async ({ params, request }) => { + const body = await request.json() as Record; + const trip = buildTrip({ id: Number(params.id) + 1000, ...body }); + return HttpResponse.json({ trip }); + }), +]; diff --git a/client/tests/helpers/msw/handlers/vacay.ts b/client/tests/helpers/msw/handlers/vacay.ts new file mode 100644 index 0000000000000000000000000000000000000000..70506526e79981c86c5f6b7c27fd5523b1ddd5cb --- /dev/null +++ b/client/tests/helpers/msw/handlers/vacay.ts @@ -0,0 +1,127 @@ +import { http, HttpResponse } from 'msw'; + +export const vacayHandlers = [ + http.get('/api/addons/vacay/plan', () => { + return HttpResponse.json({ + plan: { + id: 1, + holidays_enabled: false, + holidays_region: null, + holiday_calendars: [], + block_weekends: true, + carry_over_enabled: false, + company_holidays_enabled: false, + }, + users: [{ id: 1, username: 'user1', color: '#3b82f6' }], + pendingInvites: [], + incomingInvites: [], + isOwner: true, + isFused: false, + }); + }), + + http.put('/api/addons/vacay/plan', () => { + return HttpResponse.json({ + plan: { + id: 1, + holidays_enabled: true, + holidays_region: null, + holiday_calendars: [], + block_weekends: true, + carry_over_enabled: false, + company_holidays_enabled: false, + }, + }); + }), + + http.get('/api/addons/vacay/years', () => { + return HttpResponse.json({ years: [2025, 2026] }); + }), + + http.post('/api/addons/vacay/years', () => { + return HttpResponse.json({ years: [2025, 2026, 2027] }); + }), + + http.delete('/api/addons/vacay/years/:year', () => { + return HttpResponse.json({ years: [2025] }); + }), + + http.get('/api/addons/vacay/entries/:year', () => { + return HttpResponse.json({ + entries: [ + { date: '2025-06-15', user_id: 1 }, + { date: '2025-06-16', user_id: 1 }, + ], + companyHolidays: [], + }); + }), + + http.post('/api/addons/vacay/entries/toggle', () => { + return HttpResponse.json({ success: true }); + }), + + http.post('/api/addons/vacay/entries/company-holiday', () => { + return HttpResponse.json({ success: true }); + }), + + http.get('/api/addons/vacay/stats/:year', () => { + return HttpResponse.json({ + stats: [{ user_id: 1, vacation_days: 30, used: 2 }], + }); + }), + + http.put('/api/addons/vacay/stats/:year', () => { + return HttpResponse.json({ success: true }); + }), + + http.get('/api/addons/vacay/holidays/countries', () => { + return HttpResponse.json({ countries: ['DE', 'US', 'FR'] }); + }), + + http.get('/api/addons/vacay/holidays/:year/:country', () => { + return HttpResponse.json([ + { date: '2025-12-25', name: 'Christmas', localName: 'Weihnachten', global: true, counties: null }, + { date: '2025-01-01', name: 'New Year', localName: 'Neujahr', global: true, counties: null }, + ]); + }), + + http.put('/api/addons/vacay/color', () => { + return HttpResponse.json({ success: true }); + }), + + http.post('/api/addons/vacay/invite', () => { + return HttpResponse.json({ success: true }); + }), + + http.post('/api/addons/vacay/invite/accept', () => { + return HttpResponse.json({ success: true }); + }), + + http.post('/api/addons/vacay/invite/decline', () => { + return HttpResponse.json({ success: true }); + }), + + http.post('/api/addons/vacay/invite/cancel', () => { + return HttpResponse.json({ success: true }); + }), + + http.post('/api/addons/vacay/dissolve', () => { + return HttpResponse.json({ success: true }); + }), + + http.post('/api/addons/vacay/plan/holiday-calendars', () => { + return HttpResponse.json({ + calendar: { id: 1, plan_id: 1, region: 'DE', label: null, color: '#ef4444', sort_order: 0 }, + }); + }), + + http.put('/api/addons/vacay/plan/holiday-calendars/:id', () => { + return HttpResponse.json({ + calendar: { id: 1, plan_id: 1, region: 'US', label: 'US Holidays', color: '#3b82f6', sort_order: 0 }, + }); + }), + + http.delete('/api/addons/vacay/plan/holiday-calendars/:id', () => { + return HttpResponse.json({ success: true }); + }), +]; diff --git a/client/tests/helpers/msw/server.ts b/client/tests/helpers/msw/server.ts new file mode 100644 index 0000000000000000000000000000000000000000..6d0f50bd100adaddbc2922b91d64e4c1443b17af --- /dev/null +++ b/client/tests/helpers/msw/server.ts @@ -0,0 +1,4 @@ +import { setupServer } from 'msw/node'; +import { defaultHandlers } from './handlers'; + +export const server = setupServer(...defaultHandlers); diff --git a/client/tests/helpers/render.tsx b/client/tests/helpers/render.tsx new file mode 100644 index 0000000000000000000000000000000000000000..b62cdefb53b6d3896093b4e974e0aaa559425251 --- /dev/null +++ b/client/tests/helpers/render.tsx @@ -0,0 +1,26 @@ +import React from 'react'; +import { render, type RenderOptions } from '@testing-library/react'; +import { MemoryRouter, type MemoryRouterProps } from 'react-router-dom'; +import { TranslationProvider } from '../../src/i18n/TranslationContext'; + +interface RenderWithProvidersOptions extends Omit { + initialEntries?: MemoryRouterProps['initialEntries']; +} + +function renderWithProviders( + ui: React.ReactElement, + { initialEntries = ['/'], ...options }: RenderWithProvidersOptions = {}, +) { + function Wrapper({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ); + } + + return render(ui, { wrapper: Wrapper, ...options }); +} + +export * from '@testing-library/react'; +export { renderWithProviders as render }; diff --git a/client/tests/helpers/store.ts b/client/tests/helpers/store.ts new file mode 100644 index 0000000000000000000000000000000000000000..9df8a5dd2c0843d36e77a68bfe14b759aa965be4 --- /dev/null +++ b/client/tests/helpers/store.ts @@ -0,0 +1,41 @@ +import { useAuthStore } from '../../src/store/authStore'; +import { useTripStore } from '../../src/store/tripStore'; +import { useSettingsStore } from '../../src/store/settingsStore'; +import { useVacayStore } from '../../src/store/vacayStore'; +import { useAddonStore } from '../../src/store/addonStore'; +import { useInAppNotificationStore } from '../../src/store/inAppNotificationStore'; +import { usePermissionsStore } from '../../src/store/permissionsStore'; +// Journey store is reset individually in journey tests to avoid circular import issues + +// Capture initial states at import time (before any test modifies them) +const initialAuthState = useAuthStore.getState(); +const initialTripState = useTripStore.getState(); +const initialSettingsState = useSettingsStore.getState(); +const initialVacayState = useVacayStore.getState(); +const initialAddonState = useAddonStore.getState(); +const initialNotifState = useInAppNotificationStore.getState(); +const initialPermsState = usePermissionsStore.getState(); +export function resetAllStores(): void { + useAuthStore.setState(initialAuthState, true); + useTripStore.setState(initialTripState, true); + useSettingsStore.setState(initialSettingsState, true); + useVacayStore.setState(initialVacayState, true); + useAddonStore.setState(initialAddonState, true); + useInAppNotificationStore.setState(initialNotifState, true); + usePermissionsStore.setState(initialPermsState, true); +} + +/** + * Tests routinely seed a store with a partially-populated slice of state, + * including partial nested objects (e.g. only `settings.time_format`). The + * store's own setState wants the exact field types, so seeding accepts a + * deep-partial view and casts at the boundary. + */ +type DeepPartial = T extends object ? { [P in keyof T]?: DeepPartial } : T; + +export function seedStore( + store: { setState: (partial: Partial, replace?: boolean) => void }, + state: DeepPartial, +): void { + store.setState(state as Partial); +} diff --git a/client/tests/integration/api/client.test.ts b/client/tests/integration/api/client.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..0a6ecd05c418bbbc779f1b6736171a3c52e98b68 --- /dev/null +++ b/client/tests/integration/api/client.test.ts @@ -0,0 +1,974 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { http, HttpResponse } from 'msw'; +import { server } from '../../helpers/msw/server'; +import { buildUser } from '../../helpers/factories'; + +// The global setup.ts mocks websocket with getSocketId returning null. +// We need to be able to control what getSocketId returns per-test. +// Re-mock here to get full control. +vi.mock('../../../src/api/websocket', () => ({ + connect: vi.fn(), + disconnect: vi.fn(), + getSocketId: vi.fn(() => 'mock-socket-id'), + setRefetchCallback: vi.fn(), + setPreReconnectHook: vi.fn(), + joinTrip: vi.fn(), + leaveTrip: vi.fn(), + addListener: vi.fn(), + removeListener: vi.fn(), +})); + +const wsMock = await import('../../../src/api/websocket'); + +// Import the API client AFTER the mock is set up so it picks up our getSocketId mock +const { + apiClient, + authApi, + tripsApi, + placesApi, + packingApi, + inAppNotificationsApi, + shareApi, + backupApi, + daysApi, + assignmentsApi, + tagsApi, + categoriesApi, + adminApi, + addonsApi, + mapsApi, + budgetApi, + filesApi, + reservationsApi, + weatherApi, + settingsApi, + accommodationsApi, + dayNotesApi, + collabApi, + notificationsApi, +} = await import('../../../src/api/client'); + +describe('API client interceptors', () => { + beforeEach(() => { + vi.clearAllMocks(); + // Default: socket ID available + (wsMock.getSocketId as ReturnType).mockReturnValue('mock-socket-id'); + }); + + afterEach(() => { + // Reset window.location to a neutral path + Object.defineProperty(window, 'location', { + writable: true, + value: { href: 'http://localhost/', pathname: '/', search: '', hash: '' }, + }); + }); + + it('FE-API-001: requests include X-Socket-Id header when getSocketId returns a value', async () => { + let receivedSocketId: string | null = null; + + server.use( + http.get('/api/auth/me', ({ request }) => { + receivedSocketId = request.headers.get('X-Socket-Id'); + return HttpResponse.json({ user: buildUser() }); + }) + ); + + await authApi.me(); + + expect(receivedSocketId).toBe('mock-socket-id'); + }); + + it('FE-API-002: X-Socket-Id header is absent when getSocketId returns null', async () => { + (wsMock.getSocketId as ReturnType).mockReturnValue(null); + let receivedSocketId: string | null = 'sentinel'; + + server.use( + http.get('/api/auth/me', ({ request }) => { + receivedSocketId = request.headers.get('X-Socket-Id'); + return HttpResponse.json({ user: buildUser() }); + }) + ); + + await authApi.me(); + + expect(receivedSocketId).toBeNull(); + }); + + it('FE-API-003: 401 with AUTH_REQUIRED → redirects to /login with redirect param', async () => { + Object.defineProperty(window, 'location', { + writable: true, + value: { href: 'http://localhost/', pathname: '/dashboard', search: '', hash: '' }, + }); + + server.use( + http.get('/api/auth/me', () => { + return HttpResponse.json({ code: 'AUTH_REQUIRED' }, { status: 401 }); + }) + ); + + try { + await authApi.me(); + } catch { + // Expected to reject + } + + expect(window.location.href).toBe('/login?redirect=%2Fdashboard'); + }); + + it('FE-API-003b: 401 without AUTH_REQUIRED code does not redirect', async () => { + Object.defineProperty(window, 'location', { + writable: true, + value: { href: 'http://localhost/dashboard', pathname: '/dashboard', search: '' }, + }); + + const originalHref = window.location.href; + + server.use( + http.get('/api/auth/me', () => { + return HttpResponse.json({ error: 'Unauthorized' }, { status: 401 }); + }) + ); + + try { + await authApi.me(); + } catch { + // Expected to reject + } + + expect(window.location.href).toBe(originalHref); + }); + + it('FE-API-003c: 401 on /login page does not redirect', async () => { + Object.defineProperty(window, 'location', { + writable: true, + value: { href: 'http://localhost/login', pathname: '/login', search: '' }, + }); + + server.use( + http.get('/api/auth/me', () => { + return HttpResponse.json({ code: 'AUTH_REQUIRED' }, { status: 401 }); + }) + ); + + try { + await authApi.me(); + } catch { + // Expected to reject + } + + // href should NOT have been changed to /login?redirect=... + expect(window.location.href).toBe('http://localhost/login'); + }); + + it('FE-API-004: 403 with MFA_REQUIRED → redirects to /settings?mfa=required', async () => { + Object.defineProperty(window, 'location', { + writable: true, + value: { href: 'http://localhost/', pathname: '/dashboard', search: '' }, + }); + + server.use( + http.get('/api/auth/me', () => { + return HttpResponse.json({ code: 'MFA_REQUIRED' }, { status: 403 }); + }) + ); + + try { + await authApi.me(); + } catch { + // Expected to reject + } + + expect(window.location.href).toBe('/settings?mfa=required'); + }); + + it('FE-API-004b: 403 with MFA_REQUIRED on /settings page does not redirect', async () => { + Object.defineProperty(window, 'location', { + writable: true, + value: { href: 'http://localhost/settings', pathname: '/settings', search: '' }, + }); + + server.use( + http.get('/api/auth/me', () => { + return HttpResponse.json({ code: 'MFA_REQUIRED' }, { status: 403 }); + }) + ); + + try { + await authApi.me(); + } catch { + // Expected to reject + } + + // Should NOT redirect when already on /settings + expect(window.location.href).toBe('http://localhost/settings'); + }); + + it('FE-API-005: successful API call returns response data', async () => { + const user = buildUser(); + + server.use( + http.get('/api/auth/me', () => { + return HttpResponse.json({ user }); + }) + ); + + const data = await authApi.me(); + + expect(data).toMatchObject({ user: { id: user.id, email: user.email } }); + }); + + it('FE-API-006: socket ID header reflects current value from getSocketId at request time', async () => { + const headers: Array = []; + + (wsMock.getSocketId as ReturnType) + .mockReturnValueOnce('socket-A') + .mockReturnValueOnce('socket-B'); + + server.use( + http.get('/api/auth/me', ({ request }) => { + headers.push(request.headers.get('X-Socket-Id')); + return HttpResponse.json({ user: buildUser() }); + }) + ); + + await authApi.me(); + await authApi.me(); + + expect(headers[0]).toBe('socket-A'); + expect(headers[1]).toBe('socket-B'); + }); + + it('FE-API-007: non-401/403 errors are passed through as rejections', async () => { + server.use( + http.get('/api/auth/me', () => { + return HttpResponse.json({ error: 'Internal error' }, { status: 500 }); + }) + ); + + await expect(authApi.me()).rejects.toThrow(); + }); + + // ── 401 edge cases ─────────────────────────────────────────────────────────── + + it('FE-API-008: 401 AUTH_REQUIRED on /register path does not redirect', async () => { + Object.defineProperty(window, 'location', { + writable: true, + value: { href: 'http://localhost/register', pathname: '/register', search: '' }, + }); + + server.use( + http.get('/api/auth/me', () => { + return HttpResponse.json({ code: 'AUTH_REQUIRED' }, { status: 401 }); + }) + ); + + try { await authApi.me(); } catch { /* expected */ } + + expect(window.location.href).toBe('http://localhost/register'); + }); + + it('FE-API-009: 401 AUTH_REQUIRED on /shared/:token path does not redirect', async () => { + Object.defineProperty(window, 'location', { + writable: true, + value: { href: 'http://localhost/shared/abc123', pathname: '/shared/abc123', search: '' }, + }); + + server.use( + http.get('/api/auth/me', () => { + return HttpResponse.json({ code: 'AUTH_REQUIRED' }, { status: 401 }); + }) + ); + + try { await authApi.me(); } catch { /* expected */ } + + expect(window.location.href).toBe('http://localhost/shared/abc123'); + }); + + it('FE-API-010: 401 AUTH_REQUIRED still rejects the promise even when redirect fires', async () => { + Object.defineProperty(window, 'location', { + writable: true, + value: { href: 'http://localhost/dashboard', pathname: '/dashboard', search: '' }, + }); + + server.use( + http.get('/api/auth/me', () => { + return HttpResponse.json({ code: 'AUTH_REQUIRED' }, { status: 401 }); + }) + ); + + await expect(authApi.me()).rejects.toThrow(); + }); + + // ── 403 edge cases ─────────────────────────────────────────────────────────── + + it('FE-API-011: 403 without MFA_REQUIRED code does not redirect', async () => { + Object.defineProperty(window, 'location', { + writable: true, + value: { href: 'http://localhost/dashboard', pathname: '/dashboard', search: '' }, + }); + + server.use( + http.get('/api/auth/me', () => { + return HttpResponse.json({ error: 'Forbidden' }, { status: 403 }); + }) + ); + + try { await authApi.me(); } catch { /* expected */ } + + expect(window.location.href).toBe('http://localhost/dashboard'); + }); + + it('FE-API-012: 403 MFA_REQUIRED still rejects the promise after redirect fires', async () => { + Object.defineProperty(window, 'location', { + writable: true, + value: { href: 'http://localhost/dashboard', pathname: '/dashboard', search: '' }, + }); + + server.use( + http.get('/api/auth/me', () => { + return HttpResponse.json({ code: 'MFA_REQUIRED' }, { status: 403 }); + }) + ); + + await expect(authApi.me()).rejects.toThrow(); + }); + + // ── backupApi.download ─────────────────────────────────────────────────────── + + it('FE-API-013: backupApi.download creates a temp anchor and clicks it', async () => { + // backupApi.download uses native fetch (not axios). Mock fetch directly and + // use a plain-object Response duck-type to avoid MSW patching the Response + // constructor (which calls blob.stream() — not implemented in jsdom's Blob). + const blob = new Blob(['zip-bytes'], { type: 'application/zip' }); + vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce({ + ok: true, + blob: () => Promise.resolve(blob), + } as unknown as Response); + const createObjectURL = vi.spyOn(URL, 'createObjectURL').mockReturnValue('blob:mock-url'); + const revokeObjectURL = vi.spyOn(URL, 'revokeObjectURL').mockImplementation(() => {}); + + // Spy on createElement to intercept the anchor click + const originalCreate = document.createElement.bind(document); + const clickSpy = vi.fn(); + vi.spyOn(document, 'createElement').mockImplementation((tag: string) => { + const el = originalCreate(tag); + if (tag === 'a') { + Object.defineProperty(el, 'click', { writable: true, value: clickSpy }); + } + return el; + }); + + await expect(backupApi.download('backup.zip')).resolves.toBeUndefined(); + expect(createObjectURL).toHaveBeenCalled(); + expect(revokeObjectURL).toHaveBeenCalled(); + + vi.restoreAllMocks(); + }); + + it('FE-API-014: backupApi.download throws when response is not ok', async () => { + server.use( + http.get('/api/backup/download/missing.zip', () => { + return new HttpResponse(null, { status: 404 }); + }) + ); + + await expect(backupApi.download('missing.zip')).rejects.toThrow('Download failed'); + }); + + // ── API namespace URL spot-checks ──────────────────────────────────────────── + + it('FE-API-015: tripsApi.list() makes GET to /api/trips', async () => { + server.use( + http.get('/api/trips', () => HttpResponse.json([])) + ); + + const result = await tripsApi.list(); + expect(result).toEqual([]); + }); + + it('FE-API-016: tripsApi.get(42) makes GET to /api/trips/42', async () => { + let hitUrl = ''; + server.use( + http.get('/api/trips/42', ({ request }) => { + hitUrl = new URL(request.url).pathname; + return HttpResponse.json({ id: 42 }); + }) + ); + + await tripsApi.get(42); + expect(hitUrl).toBe('/api/trips/42'); + }); + + it('FE-API-017: placesApi.create posts to /api/trips/1/places and returns data directly', async () => { + const place = { id: 1, name: 'Paris', trip_id: 1 }; + server.use( + http.post('/api/trips/1/places', () => HttpResponse.json(place)) + ); + + const result = await placesApi.create(1, { name: 'Paris' }); + expect(result).toMatchObject({ name: 'Paris' }); + }); + + it('FE-API-018: packingApi.bulkImport posts correct payload', async () => { + let receivedBody: unknown; + server.use( + http.post('/api/trips/1/packing/import', async ({ request }) => { + receivedBody = await request.json(); + return HttpResponse.json({ imported: 1 }); + }) + ); + + await packingApi.bulkImport(1, [{ name: 'Sunscreen' }]); + expect(receivedBody).toMatchObject({ items: [{ name: 'Sunscreen' }] }); + }); + + it('FE-API-019: inAppNotificationsApi.list passes unread_only query param', async () => { + let searchParams: URLSearchParams | null = null; + server.use( + http.get('/api/notifications/in-app', ({ request }) => { + searchParams = new URL(request.url).searchParams; + return HttpResponse.json([]); + }) + ); + + await inAppNotificationsApi.list({ unread_only: true }); + expect(searchParams?.get('unread_only')).toBe('true'); + }); + + it('FE-API-020: shareApi.getSharedTrip hits /api/shared/tok123', async () => { + let hitPath = ''; + server.use( + http.get('/api/shared/tok123', ({ request }) => { + hitPath = new URL(request.url).pathname; + return HttpResponse.json({ token: 'tok123' }); + }) + ); + + const result = await shareApi.getSharedTrip('tok123'); + expect(hitPath).toBe('/api/shared/tok123'); + expect(result).toMatchObject({ token: 'tok123' }); + }); + + // ── authApi method spot-checks ─────────────────────────────────────────────── + + it('FE-API-021: authApi.login posts email and password to /api/auth/login', async () => { + const user = buildUser(); + let receivedBody: unknown; + server.use( + http.post('/api/auth/login', async ({ request }) => { + receivedBody = await request.json(); + return HttpResponse.json({ user }); + }) + ); + + const result = await authApi.login({ email: 'a@b.com', password: 'pass' }); + expect(receivedBody).toMatchObject({ email: 'a@b.com', password: 'pass' }); + expect(result).toMatchObject({ user: { id: user.id } }); + }); + + it('FE-API-022: authApi.uploadAvatar sends multipart/form-data', async () => { + // jsdom's FormData ≠ undici's FormData — MSW body serialisation of FormData + // hangs under CI resource constraints. Spy + mock at the axios level to verify + // the correct args are passed without going through the network stack. + const postSpy = vi.spyOn(apiClient, 'post').mockResolvedValueOnce({ data: { avatar_url: '/uploads/avatar.jpg' } } as any); + + const formData = new FormData(); + formData.append('avatar', new Blob(['img'], { type: 'image/jpeg' }), 'avatar.jpg'); + + await authApi.uploadAvatar(formData); + expect(postSpy).toHaveBeenCalledWith('/auth/avatar', expect.any(FormData), expect.anything()); + postSpy.mockRestore(); + }); + + it('FE-API-023: authApi.mcpTokens.create posts name to /api/auth/mcp-tokens', async () => { + let receivedBody: unknown; + server.use( + http.post('/api/auth/mcp-tokens', async ({ request }) => { + receivedBody = await request.json(); + return HttpResponse.json({ id: 1, name: 'My Token', token: 'tok' }); + }) + ); + + await authApi.mcpTokens.create('My Token'); + expect(receivedBody).toMatchObject({ name: 'My Token' }); + }); +}); + +describe('API namespace smoke tests', () => { + it('daysApi.list fetches trip days', async () => { + server.use(http.get('/api/trips/1/days', () => HttpResponse.json([]))); + await expect(daysApi.list(1)).resolves.toEqual([]); + }); + + it('assignmentsApi.list fetches day assignments', async () => { + server.use(http.get('/api/trips/1/days/1/assignments', () => HttpResponse.json([]))); + await expect(assignmentsApi.list(1, 1)).resolves.toEqual([]); + }); + + it('tagsApi.list fetches tags', async () => { + server.use(http.get('/api/tags', () => HttpResponse.json([]))); + await expect(tagsApi.list()).resolves.toEqual([]); + }); + + it('categoriesApi.list fetches categories', async () => { + server.use(http.get('/api/categories', () => HttpResponse.json([]))); + await expect(categoriesApi.list()).resolves.toEqual([]); + }); + + it('adminApi.users fetches admin users', async () => { + server.use(http.get('/api/admin/users', () => HttpResponse.json([]))); + await expect(adminApi.users()).resolves.toEqual([]); + }); + + it('addonsApi.enabled fetches enabled addons', async () => { + server.use(http.get('/api/addons', () => HttpResponse.json([]))); + await expect(addonsApi.enabled()).resolves.toEqual([]); + }); + + it('mapsApi.search posts query', async () => { + server.use(http.post('/api/maps/search', () => HttpResponse.json({ results: [] }))); + await expect(mapsApi.search('Paris')).resolves.toMatchObject({ results: [] }); + }); + + it('budgetApi.list fetches budget items', async () => { + server.use(http.get('/api/trips/1/budget', () => HttpResponse.json([]))); + await expect(budgetApi.list(1)).resolves.toEqual([]); + }); + + it('filesApi.list fetches trip files', async () => { + server.use(http.get('/api/trips/1/files', () => HttpResponse.json([]))); + await expect(filesApi.list(1)).resolves.toEqual([]); + }); + + it('reservationsApi.list fetches reservations', async () => { + server.use(http.get('/api/trips/1/reservations', () => HttpResponse.json([]))); + await expect(reservationsApi.list(1)).resolves.toEqual([]); + }); + + it('weatherApi.get fetches weather data', async () => { + server.use(http.get('/api/weather', () => HttpResponse.json({ temp: 20 }))); + await expect(weatherApi.get(48.8, 2.3, '2025-06-01')).resolves.toMatchObject({ temp: 20 }); + }); + + it('settingsApi.get fetches settings', async () => { + server.use(http.get('/api/settings', () => HttpResponse.json({ dark_mode: false }))); + await expect(settingsApi.get()).resolves.toMatchObject({ dark_mode: false }); + }); + + it('accommodationsApi.list fetches accommodations', async () => { + server.use(http.get('/api/trips/1/accommodations', () => HttpResponse.json([]))); + await expect(accommodationsApi.list(1)).resolves.toEqual([]); + }); + + it('dayNotesApi.list fetches day notes', async () => { + server.use(http.get('/api/trips/1/days/1/notes', () => HttpResponse.json([]))); + await expect(dayNotesApi.list(1, 1)).resolves.toEqual([]); + }); + + it('collabApi.getNotes fetches collab notes', async () => { + server.use(http.get('/api/trips/1/collab/notes', () => HttpResponse.json([]))); + await expect(collabApi.getNotes(1)).resolves.toEqual([]); + }); + + it('notificationsApi.getPreferences fetches preferences', async () => { + server.use(http.get('/api/notifications/preferences', () => HttpResponse.json({ email: true }))); + await expect(notificationsApi.getPreferences()).resolves.toMatchObject({ email: true }); + }); + + it('inAppNotificationsApi.unreadCount fetches unread count', async () => { + server.use(http.get('/api/notifications/in-app/unread-count', () => HttpResponse.json({ count: 3 }))); + await expect(inAppNotificationsApi.unreadCount()).resolves.toMatchObject({ count: 3 }); + }); + + it('inAppNotificationsApi.markRead marks a notification read', async () => { + server.use(http.put('/api/notifications/in-app/5/read', () => HttpResponse.json({ ok: true }))); + await expect(inAppNotificationsApi.markRead(5)).resolves.toMatchObject({ ok: true }); + }); + + it('inAppNotificationsApi.markAllRead marks all notifications read', async () => { + server.use(http.put('/api/notifications/in-app/read-all', () => HttpResponse.json({ ok: true }))); + await expect(inAppNotificationsApi.markAllRead()).resolves.toMatchObject({ ok: true }); + }); + + it('inAppNotificationsApi.delete deletes a notification', async () => { + server.use(http.delete('/api/notifications/in-app/5', () => HttpResponse.json({ ok: true }))); + await expect(inAppNotificationsApi.delete(5)).resolves.toMatchObject({ ok: true }); + }); + + it('inAppNotificationsApi.markUnread marks a notification unread', async () => { + server.use(http.put('/api/notifications/in-app/5/unread', () => HttpResponse.json({ ok: true }))); + await expect(inAppNotificationsApi.markUnread(5)).resolves.toMatchObject({ ok: true }); + }); + + it('inAppNotificationsApi.deleteAll deletes all notifications', async () => { + server.use(http.delete('/api/notifications/in-app/all', () => HttpResponse.json({ ok: true }))); + await expect(inAppNotificationsApi.deleteAll()).resolves.toMatchObject({ ok: true }); + }); + + it('inAppNotificationsApi.respond posts a response', async () => { + server.use(http.post('/api/notifications/in-app/5/respond', () => HttpResponse.json({ ok: true }))); + await expect(inAppNotificationsApi.respond(5, 'positive')).resolves.toMatchObject({ ok: true }); + }); + + it('notificationsApi.updatePreferences updates preferences', async () => { + server.use(http.put('/api/notifications/preferences', () => HttpResponse.json({ ok: true }))); + await expect(notificationsApi.updatePreferences({ email: { trip_invite: true } })).resolves.toMatchObject({ ok: true }); + }); + + it('backupApi.list fetches backup list', async () => { + server.use(http.get('/api/backup/list', () => HttpResponse.json([]))); + await expect(backupApi.list()).resolves.toEqual([]); + }); + + // ── tripsApi additional methods ────────────────────────────────────────────── + + it('tripsApi.create posts new trip', async () => { + server.use(http.post('/api/trips', () => HttpResponse.json({ id: 1, title: 'Test' }))); + await expect(tripsApi.create({ title: 'Test' })).resolves.toMatchObject({ id: 1 }); + }); + + it('tripsApi.update puts trip data', async () => { + server.use(http.put('/api/trips/1', () => HttpResponse.json({ id: 1 }))); + await expect(tripsApi.update(1, { title: 'Updated' })).resolves.toMatchObject({ id: 1 }); + }); + + it('tripsApi.delete deletes a trip', async () => { + server.use(http.delete('/api/trips/1', () => HttpResponse.json({ ok: true }))); + await expect(tripsApi.delete(1)).resolves.toMatchObject({ ok: true }); + }); + + it('tripsApi.getMembers fetches trip members', async () => { + server.use(http.get('/api/trips/1/members', () => HttpResponse.json([]))); + await expect(tripsApi.getMembers(1)).resolves.toEqual([]); + }); + + it('tripsApi.copy copies a trip', async () => { + server.use(http.post('/api/trips/1/copy', () => HttpResponse.json({ id: 99 }))); + await expect(tripsApi.copy(1)).resolves.toMatchObject({ id: 99 }); + }); + + // ── placesApi additional methods ───────────────────────────────────────────── + + it('placesApi.list fetches places', async () => { + server.use(http.get('/api/trips/1/places', () => HttpResponse.json([]))); + await expect(placesApi.list(1)).resolves.toEqual([]); + }); + + it('placesApi.get fetches a place', async () => { + server.use(http.get('/api/trips/1/places/5', () => HttpResponse.json({ id: 5 }))); + await expect(placesApi.get(1, 5)).resolves.toMatchObject({ id: 5 }); + }); + + it('placesApi.update updates a place', async () => { + server.use(http.put('/api/trips/1/places/5', () => HttpResponse.json({ id: 5 }))); + await expect(placesApi.update(1, 5, { name: 'Rome' })).resolves.toMatchObject({ id: 5 }); + }); + + it('placesApi.delete deletes a place', async () => { + server.use(http.delete('/api/trips/1/places/5', () => HttpResponse.json({ ok: true }))); + await expect(placesApi.delete(1, 5)).resolves.toMatchObject({ ok: true }); + }); + + // ── packingApi additional methods ──────────────────────────────────────────── + + it('packingApi.list fetches packing items', async () => { + server.use(http.get('/api/trips/1/packing', () => HttpResponse.json([]))); + await expect(packingApi.list(1)).resolves.toEqual([]); + }); + + it('packingApi.create creates a packing item', async () => { + server.use(http.post('/api/trips/1/packing', () => HttpResponse.json({ id: 1, name: 'Towel' }))); + await expect(packingApi.create(1, { name: 'Towel' })).resolves.toMatchObject({ id: 1 }); + }); + + it('packingApi.delete deletes a packing item', async () => { + server.use(http.delete('/api/trips/1/packing/1', () => HttpResponse.json({ ok: true }))); + await expect(packingApi.delete(1, 1)).resolves.toMatchObject({ ok: true }); + }); + + // ── assignmentsApi additional methods ──────────────────────────────────────── + + it('assignmentsApi.create creates an assignment', async () => { + server.use(http.post('/api/trips/1/days/1/assignments', () => HttpResponse.json({ id: 1 }))); + await expect(assignmentsApi.create(1, 1, { place_id: 5 })).resolves.toMatchObject({ id: 1 }); + }); + + it('assignmentsApi.delete deletes an assignment', async () => { + server.use(http.delete('/api/trips/1/days/1/assignments/1', () => HttpResponse.json({ ok: true }))); + await expect(assignmentsApi.delete(1, 1, 1)).resolves.toMatchObject({ ok: true }); + }); + + it('assignmentsApi.reorder reorders assignments', async () => { + server.use(http.put('/api/trips/1/days/1/assignments/reorder', () => HttpResponse.json({ ok: true }))); + await expect(assignmentsApi.reorder(1, 1, [3, 1, 2])).resolves.toMatchObject({ ok: true }); + }); + + // ── daysApi additional methods ─────────────────────────────────────────────── + + it('daysApi.create creates a day', async () => { + server.use(http.post('/api/trips/1/days', () => HttpResponse.json({ id: 1 }))); + await expect(daysApi.create(1, { date: '2025-06-01' })).resolves.toMatchObject({ id: 1 }); + }); + + it('daysApi.delete deletes a day', async () => { + server.use(http.delete('/api/trips/1/days/1', () => HttpResponse.json({ ok: true }))); + await expect(daysApi.delete(1, 1)).resolves.toMatchObject({ ok: true }); + }); + + // ── tagsApi / categoriesApi additional methods ──────────────────────────────── + + it('tagsApi.create creates a tag', async () => { + server.use(http.post('/api/tags', () => HttpResponse.json({ id: 1, name: 'Fun' }))); + await expect(tagsApi.create({ name: 'Fun' })).resolves.toMatchObject({ id: 1 }); + }); + + it('tagsApi.delete deletes a tag', async () => { + server.use(http.delete('/api/tags/1', () => HttpResponse.json({ ok: true }))); + await expect(tagsApi.delete(1)).resolves.toMatchObject({ ok: true }); + }); + + it('categoriesApi.create creates a category', async () => { + server.use(http.post('/api/categories', () => HttpResponse.json({ id: 1, name: 'Food' }))); + await expect(categoriesApi.create({ name: 'Food' })).resolves.toMatchObject({ id: 1 }); + }); + + it('categoriesApi.delete deletes a category', async () => { + server.use(http.delete('/api/categories/1', () => HttpResponse.json({ ok: true }))); + await expect(categoriesApi.delete(1)).resolves.toMatchObject({ ok: true }); + }); + + // ── adminApi additional methods ─────────────────────────────────────────────── + + it('adminApi.stats fetches admin stats', async () => { + server.use(http.get('/api/admin/stats', () => HttpResponse.json({ trips: 5 }))); + await expect(adminApi.stats()).resolves.toMatchObject({ trips: 5 }); + }); + + it('adminApi.createUser creates a user', async () => { + server.use(http.post('/api/admin/users', () => HttpResponse.json({ id: 10 }))); + await expect(adminApi.createUser({ email: 'x@x.com' })).resolves.toMatchObject({ id: 10 }); + }); + + // ── budgetApi additional methods ───────────────────────────────────────────── + + it('budgetApi.create creates a budget item', async () => { + server.use(http.post('/api/trips/1/budget', () => HttpResponse.json({ id: 1 }))); + await expect(budgetApi.create(1, { name: 'Hotel' })).resolves.toMatchObject({ id: 1 }); + }); + + it('budgetApi.delete deletes a budget item', async () => { + server.use(http.delete('/api/trips/1/budget/1', () => HttpResponse.json({ ok: true }))); + await expect(budgetApi.delete(1, 1)).resolves.toMatchObject({ ok: true }); + }); + + // ── reservationsApi additional methods ─────────────────────────────────────── + + it('reservationsApi.create creates a reservation', async () => { + server.use(http.post('/api/trips/1/reservations', () => HttpResponse.json({ id: 1 }))); + await expect(reservationsApi.create(1, { title: 'Hotel' })).resolves.toMatchObject({ id: 1 }); + }); + + it('reservationsApi.delete deletes a reservation', async () => { + server.use(http.delete('/api/trips/1/reservations/1', () => HttpResponse.json({ ok: true }))); + await expect(reservationsApi.delete(1, 1)).resolves.toMatchObject({ ok: true }); + }); + + // ── settingsApi additional methods ─────────────────────────────────────────── + + it('settingsApi.set updates a setting', async () => { + server.use(http.put('/api/settings', () => HttpResponse.json({ ok: true }))); + await expect(settingsApi.set('dark_mode', true)).resolves.toMatchObject({ ok: true }); + }); + + // ── accommodationsApi additional methods ───────────────────────────────────── + + it('accommodationsApi.create creates accommodation', async () => { + server.use(http.post('/api/trips/1/accommodations', () => HttpResponse.json({ id: 1 }))); + await expect(accommodationsApi.create(1, { place_id: 1, start_day_id: 1, end_day_id: 1 })).resolves.toMatchObject({ id: 1 }); + }); + + it('accommodationsApi.delete deletes accommodation', async () => { + server.use(http.delete('/api/trips/1/accommodations/1', () => HttpResponse.json({ ok: true }))); + await expect(accommodationsApi.delete(1, 1)).resolves.toMatchObject({ ok: true }); + }); + + // ── dayNotesApi additional methods ─────────────────────────────────────────── + + it('dayNotesApi.create creates a day note', async () => { + server.use(http.post('/api/trips/1/days/1/notes', () => HttpResponse.json({ id: 1 }))); + await expect(dayNotesApi.create(1, 1, { text: 'Hello' })).resolves.toMatchObject({ id: 1 }); + }); + + it('dayNotesApi.delete deletes a day note', async () => { + server.use(http.delete('/api/trips/1/days/1/notes/1', () => HttpResponse.json({ ok: true }))); + await expect(dayNotesApi.delete(1, 1, 1)).resolves.toMatchObject({ ok: true }); + }); + + // ── collabApi additional methods ───────────────────────────────────────────── + + it('collabApi.createNote creates a note', async () => { + server.use(http.post('/api/trips/1/collab/notes', () => HttpResponse.json({ id: 1 }))); + await expect(collabApi.createNote(1, { title: 'Note' })).resolves.toMatchObject({ id: 1 }); + }); + + it('collabApi.deleteNote deletes a note', async () => { + server.use(http.delete('/api/trips/1/collab/notes/1', () => HttpResponse.json({ ok: true }))); + await expect(collabApi.deleteNote(1, 1)).resolves.toMatchObject({ ok: true }); + }); + + // ── backupApi additional methods ───────────────────────────────────────────── + + it('backupApi.getAutoSettings fetches auto backup settings', async () => { + server.use(http.get('/api/backup/auto-settings', () => HttpResponse.json({ enabled: true }))); + await expect(backupApi.getAutoSettings()).resolves.toMatchObject({ enabled: true }); + }); + + it('backupApi.delete deletes a backup', async () => { + server.use(http.delete('/api/backup/backup.zip', () => HttpResponse.json({ ok: true }))); + await expect(backupApi.delete('backup.zip')).resolves.toMatchObject({ ok: true }); + }); + + // ── shareApi additional methods ─────────────────────────────────────────────── + + it('shareApi.createLink creates a share link', async () => { + server.use(http.post('/api/trips/1/share-link', () => HttpResponse.json({ token: 'abc' }))); + await expect(shareApi.createLink(1)).resolves.toMatchObject({ token: 'abc' }); + }); + + it('shareApi.deleteLink deletes a share link', async () => { + server.use(http.delete('/api/trips/1/share-link', () => HttpResponse.json({ ok: true }))); + await expect(shareApi.deleteLink(1)).resolves.toMatchObject({ ok: true }); + }); + + // ── notificationsApi additional methods ─────────────────────────────────────── + + it('notificationsApi.testWebhook tests webhook endpoint', async () => { + server.use(http.post('/api/notifications/test-webhook', () => HttpResponse.json({ ok: true }))); + await expect(notificationsApi.testWebhook('http://example.com')).resolves.toMatchObject({ ok: true }); + }); + + it('notificationsApi.testSmtp tests smtp endpoint', async () => { + server.use(http.post('/api/notifications/test-smtp', () => HttpResponse.json({ ok: true }))); + await expect(notificationsApi.testSmtp('user@example.com')).resolves.toMatchObject({ ok: true }); + }); + + // ── mapsApi additional methods ──────────────────────────────────────────────── + + it('mapsApi.reverse fetches reverse geocode', async () => { + server.use(http.get('/api/maps/reverse', () => HttpResponse.json({ address: 'Paris' }))); + await expect(mapsApi.reverse(48.8, 2.3)).resolves.toMatchObject({ address: 'Paris' }); + }); + + // ── collabApi messaging methods ─────────────────────────────────────────────── + + it('collabApi.getMessages fetches messages', async () => { + server.use(http.get('/api/trips/1/collab/messages', () => HttpResponse.json([]))); + await expect(collabApi.getMessages(1)).resolves.toEqual([]); + }); + + it('collabApi.sendMessage sends a message', async () => { + server.use(http.post('/api/trips/1/collab/messages', () => HttpResponse.json({ id: 1 }))); + await expect(collabApi.sendMessage(1, { text: 'Hello' })).resolves.toMatchObject({ id: 1 }); + }); + + it('collabApi.deleteMessage deletes a message', async () => { + server.use(http.delete('/api/trips/1/collab/messages/1', () => HttpResponse.json({ ok: true }))); + await expect(collabApi.deleteMessage(1, 1)).resolves.toMatchObject({ ok: true }); + }); + + it('collabApi.reactMessage reacts to a message', async () => { + server.use(http.post('/api/trips/1/collab/messages/1/react', () => HttpResponse.json({ ok: true }))); + await expect(collabApi.reactMessage(1, 1, '👍')).resolves.toMatchObject({ ok: true }); + }); + + it('collabApi.getPolls fetches polls', async () => { + server.use(http.get('/api/trips/1/collab/polls', () => HttpResponse.json([]))); + await expect(collabApi.getPolls(1)).resolves.toEqual([]); + }); + + it('backupApi.uploadRestore uploads and restores a backup', async () => { + // FormData POST hangs on CI — mock at the axios level (see FE-API-022 comment). + const postSpy = vi.spyOn(apiClient, 'post').mockResolvedValueOnce({ data: { ok: true } } as any); + const file = new File(['data'], 'backup.zip', { type: 'application/zip' }); + await expect(backupApi.uploadRestore(file)).resolves.toMatchObject({ ok: true }); + postSpy.mockRestore(); + }); + + it('backupApi.restore restores a named backup', async () => { + server.use(http.post('/api/backup/restore/backup.zip', () => HttpResponse.json({ ok: true }))); + await expect(backupApi.restore('backup.zip')).resolves.toMatchObject({ ok: true }); + }); + + it('backupApi.create creates a backup', async () => { + server.use(http.post('/api/backup/create', () => HttpResponse.json({ filename: 'backup.zip' }))); + await expect(backupApi.create()).resolves.toMatchObject({ filename: 'backup.zip' }); + }); +}); + +describe('mapsApi', () => { + it('FE-MAPS-001: mapsApi.autocomplete sends input, lang, and locationBias', async () => { + let capturedBody: any = null; + + server.use( + http.post('/api/maps/autocomplete', async ({ request }) => { + capturedBody = await request.json(); + return HttpResponse.json({ + suggestions: [{ placeId: 'ChIJ1234', mainText: 'Paris', secondaryText: 'France' }], + source: 'google', + }); + }) + ); + + const result = await mapsApi.autocomplete('Par', 'fr', { low: { lat: 48.5, lng: 2.0 }, high: { lat: 49.0, lng: 2.8 } }); + + expect(capturedBody).toEqual({ + input: 'Par', + lang: 'fr', + locationBias: { low: { lat: 48.5, lng: 2.0 }, high: { lat: 49.0, lng: 2.8 } }, + }); + expect(result.suggestions).toHaveLength(1); + expect(result.suggestions[0].mainText).toBe('Paris'); + expect(result.source).toBe('google'); + }); + + it('FE-MAPS-002: mapsApi.autocomplete works without optional params', async () => { + server.use( + http.post('/api/maps/autocomplete', async ({ request }) => { + const body: any = await request.json(); + expect(body.lang).toBeUndefined(); + expect(body.locationBias).toBeUndefined(); + return HttpResponse.json({ suggestions: [], source: 'nominatim' }); + }) + ); + + const result = await mapsApi.autocomplete('test'); + expect(result.suggestions).toEqual([]); + }); + + it('FE-MAPS-003: mapsApi.autocomplete rejects on server error', async () => { + server.use( + http.post('/api/maps/autocomplete', () => { + return HttpResponse.json({ error: 'Rate limited' }, { status: 429 }); + }) + ); + + await expect(mapsApi.autocomplete('test')).rejects.toThrow(); + }); + + it('FE-MAPS-004: mapsApi.autocomplete rejects when AbortSignal is aborted', async () => { + const controller = new AbortController(); + + server.use( + http.post('/api/maps/autocomplete', async () => { + // Never resolves — request will be aborted + await new Promise(() => {}); + return HttpResponse.json({ suggestions: [] }); + }) + ); + + const promise = mapsApi.autocomplete('Paris', undefined, undefined, controller.signal); + controller.abort(); + + await expect(promise).rejects.toThrow(); + }); +}); diff --git a/client/tests/integration/api/websocket.test.ts b/client/tests/integration/api/websocket.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..566a6e95c7769b60ba3ba6128338dda04ccb94f4 --- /dev/null +++ b/client/tests/integration/api/websocket.test.ts @@ -0,0 +1,438 @@ +// IMPORTANT: unmock must be the very first statement before any imports +vi.unmock('../../../src/api/websocket'); + +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { http, HttpResponse } from 'msw'; +import { server } from '../../helpers/msw/server'; +import { + connect, + disconnect, + joinTrip, + leaveTrip, + addListener, + removeListener, + getSocketId, + setRefetchCallback, +} from '../../../src/api/websocket'; + +// ── Fake WebSocket ──────────────────────────────────────────────────────────── + +class MockWebSocket { + static CONNECTING = 0; + static OPEN = 1; + static CLOSING = 2; + static CLOSED = 3; + + readyState: number = MockWebSocket.OPEN; + send = vi.fn(); + close = vi.fn(); + onopen: (() => void) | null = null; + onmessage: ((event: { data: string }) => void) | null = null; + onclose: (() => void) | null = null; + onerror: (() => void) | null = null; + + constructor(public url: string) { + MockWebSocket.instances.push(this); + } + + static instances: MockWebSocket[] = []; + static reset() { + MockWebSocket.instances = []; + } +} + +beforeEach(() => { + vi.useFakeTimers(); + MockWebSocket.reset(); + + // Replace globalThis.WebSocket with MockWebSocket directly. + // jsdom marks WebSocket as non-writable, so we must use defineProperty. + Object.defineProperty(globalThis, 'WebSocket', { + writable: true, + configurable: true, + value: MockWebSocket, + }); + + // Default handler: ws-token returns a valid token + server.use( + http.post('/api/auth/ws-token', () => + HttpResponse.json({ token: 'test-ws-token' }) + ) + ); +}); + +afterEach(() => { + disconnect(); + setRefetchCallback(null); + vi.useRealTimers(); + server.resetHandlers(); +}); + +// Helper to get the most recently created MockWebSocket instance +function lastSocket(): MockWebSocket { + return MockWebSocket.instances[MockWebSocket.instances.length - 1]; +} + +// ── connect / disconnect ────────────────────────────────────────────────────── + +describe('connect / disconnect', () => { + it('FE-COMP-WS-001: connect() fetches ws-token and creates a WebSocket with it', async () => { + connect(); + await vi.advanceTimersByTimeAsync(0); + + expect(MockWebSocket.instances).toHaveLength(1); + expect(MockWebSocket.instances[0].url).toContain('token=test-ws-token'); + }); + + it('FE-COMP-WS-002: connect() sets shouldReconnect so onclose triggers reconnect', async () => { + connect(); + await vi.advanceTimersByTimeAsync(0); + + expect(MockWebSocket.instances).toHaveLength(1); + + // Simulate socket close (triggers scheduleReconnect) + lastSocket().onclose!(); + + // Advance past initial reconnect delay (1000ms) — reconnect fires + await vi.advanceTimersByTimeAsync(1001); + await vi.advanceTimersByTimeAsync(0); + + expect(MockWebSocket.instances).toHaveLength(2); + }); + + it('FE-COMP-WS-003: disconnect() prevents reconnect after socket close', async () => { + connect(); + await vi.advanceTimersByTimeAsync(0); + + const sock = lastSocket(); + disconnect(); + + // After disconnect, onclose is nulled — simulating close should be safe + // but we also fire it manually to be sure + if (sock.onclose) sock.onclose(); + + await vi.advanceTimersByTimeAsync(5000); + await vi.advanceTimersByTimeAsync(0); + + // Still only the original socket + expect(MockWebSocket.instances).toHaveLength(1); + }); + + it('FE-COMP-WS-004: connect() is idempotent — calling twice creates only one socket', async () => { + connect(); + connect(); + await vi.advanceTimersByTimeAsync(0); + + expect(MockWebSocket.instances).toHaveLength(1); + }); +}); + +// ── ws-token fetch failures ─────────────────────────────────────────────────── + +describe('ws-token fetch failures', () => { + it('FE-COMP-WS-005: 401 on ws-token fetch stops reconnect entirely', async () => { + server.use( + http.post('/api/auth/ws-token', () => + new HttpResponse(null, { status: 401 }) + ) + ); + + connect(); + await vi.advanceTimersByTimeAsync(0); + + // No socket should be created + expect(MockWebSocket.instances).toHaveLength(0); + + // Advance timers — no retry should fire + await vi.advanceTimersByTimeAsync(10000); + await vi.advanceTimersByTimeAsync(0); + + expect(MockWebSocket.instances).toHaveLength(0); + }); + + it('FE-COMP-WS-006: non-401 error on ws-token schedules a reconnect', async () => { + server.use( + http.post('/api/auth/ws-token', () => + new HttpResponse(null, { status: 503 }) + ) + ); + + connect(); + await vi.advanceTimersByTimeAsync(0); + + // No socket yet + expect(MockWebSocket.instances).toHaveLength(0); + + // Now allow the next fetch to succeed + server.use( + http.post('/api/auth/ws-token', () => + HttpResponse.json({ token: 'retry-token' }) + ) + ); + + // Advance past initial reconnect delay + await vi.advanceTimersByTimeAsync(1001); + await vi.advanceTimersByTimeAsync(0); + + // A socket should now be created + expect(MockWebSocket.instances).toHaveLength(1); + }); +}); + +// ── onopen / join on reconnect ──────────────────────────────────────────────── + +describe('onopen / join on reconnect', () => { + it('FE-COMP-WS-007: onopen sends join messages for all active trips', async () => { + joinTrip(42); + connect(); + await vi.advanceTimersByTimeAsync(0); + + const sock = lastSocket(); + sock.onopen!(); + + expect(sock.send).toHaveBeenCalledWith( + JSON.stringify({ type: 'join', tripId: '42' }) + ); + }); + + it('FE-COMP-WS-008: onopen invokes refetchCallback for each active trip', async () => { + const refetch = vi.fn(); + setRefetchCallback(refetch); + joinTrip(1); + connect(); + await vi.advanceTimersByTimeAsync(0); + + lastSocket().onopen!(); + + expect(refetch).toHaveBeenCalledWith('1'); + }); +}); + +// ── joinTrip / leaveTrip ────────────────────────────────────────────────────── + +describe('joinTrip / leaveTrip', () => { + it('FE-COMP-WS-009: joinTrip sends join message immediately when socket is open', async () => { + connect(); + await vi.advanceTimersByTimeAsync(0); + const sock = lastSocket(); + sock.onopen!(); + + joinTrip(99); + + expect(sock.send).toHaveBeenCalledWith( + JSON.stringify({ type: 'join', tripId: '99' }) + ); + }); + + it('FE-COMP-WS-010: joinTrip queues trip when socket is not open yet', async () => { + joinTrip(5); + connect(); + await vi.advanceTimersByTimeAsync(0); + + const sock = lastSocket(); + sock.onopen!(); + + expect(sock.send).toHaveBeenCalledWith( + JSON.stringify({ type: 'join', tripId: '5' }) + ); + }); + + it('FE-COMP-WS-011: leaveTrip sends leave message and removes from activeTrips', async () => { + connect(); + await vi.advanceTimersByTimeAsync(0); + const sock = lastSocket(); + sock.onopen!(); + + joinTrip(7); + leaveTrip(7); + + expect(sock.send).toHaveBeenCalledWith( + JSON.stringify({ type: 'leave', tripId: '7' }) + ); + + // Simulate close + reconnect — trip 7 should NOT be re-joined + sock.onclose!(); + await vi.advanceTimersByTimeAsync(1001); + await vi.advanceTimersByTimeAsync(0); + + const sock2 = lastSocket(); + sock2.onopen!(); + + // send called for initial join (trip 7) but not after leaveTrip + const joinCalls = sock2.send.mock.calls.filter( + c => JSON.parse(c[0]).tripId === '7' + ); + expect(joinCalls).toHaveLength(0); + }); +}); + +// ── handleMessage / listeners ───────────────────────────────────────────────── + +describe('handleMessage / listeners', () => { + async function setupConnectedSocket() { + connect(); + await vi.advanceTimersByTimeAsync(0); + const sock = lastSocket(); + sock.onopen!(); + return sock; + } + + it('FE-COMP-WS-012: welcome message sets socketId and is NOT dispatched to listeners', async () => { + const sock = await setupConnectedSocket(); + const listener = vi.fn(); + addListener(listener); + + sock.onmessage!({ data: JSON.stringify({ type: 'welcome', socketId: 'server-sid-1' }) }); + + expect(getSocketId()).toBe('server-sid-1'); + expect(listener).not.toHaveBeenCalled(); + + removeListener(listener); + }); + + it('FE-COMP-WS-013: non-welcome messages are dispatched to all registered listeners', async () => { + const sock = await setupConnectedSocket(); + const l1 = vi.fn(); + const l2 = vi.fn(); + addListener(l1); + addListener(l2); + + const msg = { type: 'place_added', tripId: '1' }; + sock.onmessage!({ data: JSON.stringify(msg) }); + + expect(l1).toHaveBeenCalledWith(msg); + expect(l2).toHaveBeenCalledWith(msg); + + removeListener(l1); + removeListener(l2); + }); + + it('FE-COMP-WS-014: listener error is caught and does not prevent other listeners from firing', async () => { + const sock = await setupConnectedSocket(); + const throwing = vi.fn().mockImplementation(() => { throw new Error('boom'); }); + const working = vi.fn(); + addListener(throwing); + addListener(working); + + expect(() => { + sock.onmessage!({ data: JSON.stringify({ type: 'some_event' }) }); + }).not.toThrow(); + + expect(working).toHaveBeenCalled(); + + removeListener(throwing); + removeListener(working); + }); + + it('FE-COMP-WS-015: malformed JSON in message is caught silently', async () => { + const sock = await setupConnectedSocket(); + const listener = vi.fn(); + addListener(listener); + + expect(() => { + sock.onmessage!({ data: 'not-json' }); + }).not.toThrow(); + + expect(listener).not.toHaveBeenCalled(); + + removeListener(listener); + }); + + it('FE-COMP-WS-016: removeListener stops a listener from receiving messages', async () => { + const sock = await setupConnectedSocket(); + const listener = vi.fn(); + addListener(listener); + removeListener(listener); + + sock.onmessage!({ data: JSON.stringify({ type: 'update' }) }); + + expect(listener).not.toHaveBeenCalled(); + }); +}); + +// ── addListener / removeListener ───────────────────────────────────────────── + +describe('addListener / removeListener symmetry', () => { + it('FE-COMP-WS-017: listener set grows and shrinks correctly', async () => { + connect(); + await vi.advanceTimersByTimeAsync(0); + const sock = lastSocket(); + sock.onopen!(); + + const l1 = vi.fn(); + const l2 = vi.fn(); + addListener(l1); + addListener(l2); + + sock.onmessage!({ data: JSON.stringify({ type: 'ping' }) }); + expect(l1).toHaveBeenCalledTimes(1); + expect(l2).toHaveBeenCalledTimes(1); + + removeListener(l1); + + sock.onmessage!({ data: JSON.stringify({ type: 'ping' }) }); + expect(l1).toHaveBeenCalledTimes(1); // no new calls + expect(l2).toHaveBeenCalledTimes(2); + + removeListener(l2); + }); +}); + +// ── getSocketId / setRefetchCallback ───────────────────────────────────────── + +describe('getSocketId / setRefetchCallback', () => { + it('FE-COMP-WS-018: getSocketId() returns null before welcome message', async () => { + // mySocketId is a module-level singleton that persists between tests. + // Use vi.resetModules() + dynamic import to get a fresh module state. + vi.resetModules(); + const freshWs = await import('../../../src/api/websocket'); + expect(freshWs.getSocketId()).toBeNull(); + // Clean up: restore the real module for subsequent tests by resetting again + vi.resetModules(); + }); + + it('FE-COMP-WS-019: setRefetchCallback(null) clears the callback', async () => { + const cb = vi.fn(); + setRefetchCallback(cb); + setRefetchCallback(null); + + joinTrip(10); + connect(); + await vi.advanceTimersByTimeAsync(0); + lastSocket().onopen!(); + + expect(cb).not.toHaveBeenCalled(); + }); +}); + +// ── Reconnect backoff ───────────────────────────────────────────────────────── + +describe('reconnect backoff', () => { + it('FE-COMP-WS-020: reconnect delay doubles on each failure up to 30s max', async () => { + // Make every fetch fail with 503 so reconnect keeps firing + server.use( + http.post('/api/auth/ws-token', () => + new HttpResponse(null, { status: 503 }) + ) + ); + + connect(); + + const delays = [1000, 2000, 4000, 8000, 16000, 30000]; + let totalAdvanced = 0; + + for (const delay of delays) { + // Wait for the fetch to complete + await vi.advanceTimersByTimeAsync(0); + // No socket should ever be created + expect(MockWebSocket.instances).toHaveLength(0); + // Advance to trigger next reconnect + await vi.advanceTimersByTimeAsync(delay + 1); + totalAdvanced += delay + 1; + } + + // After advancing through all delays, still no socket + await vi.advanceTimersByTimeAsync(0); + expect(MockWebSocket.instances).toHaveLength(0); + }); +}); diff --git a/client/tests/integration/hooks/useDayNotes.test.ts b/client/tests/integration/hooks/useDayNotes.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..88c0cdab09a93e7995f14ffc6c1e885c9da06d96 --- /dev/null +++ b/client/tests/integration/hooks/useDayNotes.test.ts @@ -0,0 +1,448 @@ +import React from 'react'; +import { renderHook, act } from '@testing-library/react'; +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { http, HttpResponse } from 'msw'; +import { useDayNotes } from '../../../src/hooks/useDayNotes'; +import { useTripStore } from '../../../src/store/tripStore'; +import { TranslationProvider } from '../../../src/i18n/TranslationContext'; +import { server } from '../../helpers/msw/server'; +import { buildDayNote } from '../../helpers/factories'; +import { resetAllStores } from '../../helpers/store'; + +const wrapper = ({ children }: { children: React.ReactNode }) => + React.createElement(TranslationProvider, null, children); + +const TRIP_ID = 1; +const DAY_ID = 10; + +describe('useDayNotes', () => { + beforeEach(() => { + resetAllStores(); + vi.clearAllMocks(); + }); + + it('FE-HOOK-DAYNOTES-001: initial noteUi state is empty', () => { + const { result } = renderHook(() => useDayNotes(TRIP_ID), { wrapper }); + expect(result.current.noteUi).toEqual({}); + }); + + it('FE-HOOK-DAYNOTES-002: initial dayNotes comes from tripStore', () => { + const note = buildDayNote({ day_id: DAY_ID }); + useTripStore.setState({ dayNotes: { [String(DAY_ID)]: [note] } }); + + const { result } = renderHook(() => useDayNotes(TRIP_ID), { wrapper }); + expect(result.current.dayNotes[String(DAY_ID)]).toEqual([note]); + }); + + it('FE-HOOK-DAYNOTES-003: openAddNote sets mode=add and default sort order', () => { + const { result } = renderHook(() => useDayNotes(TRIP_ID), { wrapper }); + + act(() => { + result.current.openAddNote(DAY_ID, () => []); + }); + + expect(result.current.noteUi[DAY_ID]).toMatchObject({ + mode: 'add', + text: '', + sortOrder: 0, // maxKey(-1) + 1 = 0 + }); + }); + + it('FE-HOOK-DAYNOTES-004: openAddNote calculates sortOrder as max(sortKey) + 1 from merged items', () => { + const { result } = renderHook(() => useDayNotes(TRIP_ID), { wrapper }); + + const getMergedItems = () => [ + { type: 'note' as const, sortKey: 5, data: buildDayNote() }, + { type: 'note' as const, sortKey: 10, data: buildDayNote() }, + ]; + + act(() => { + result.current.openAddNote(DAY_ID, getMergedItems); + }); + + expect(result.current.noteUi[DAY_ID]).toMatchObject({ + mode: 'add', + sortOrder: 11, // max(5,10) + 1 + }); + }); + + it('FE-HOOK-DAYNOTES-005: openEditNote sets mode=edit with note data', () => { + const note = buildDayNote({ id: 99, text: 'Hello', time: '10:00', icon: 'Star' }); + const { result } = renderHook(() => useDayNotes(TRIP_ID), { wrapper }); + + act(() => { + result.current.openEditNote(DAY_ID, note); + }); + + expect(result.current.noteUi[DAY_ID]).toMatchObject({ + mode: 'edit', + noteId: 99, + text: 'Hello', + time: '10:00', + icon: 'Star', + }); + }); + + it('FE-HOOK-DAYNOTES-006: cancelNote removes the UI entry for that day', () => { + const { result } = renderHook(() => useDayNotes(TRIP_ID), { wrapper }); + + act(() => { + result.current.openAddNote(DAY_ID, () => []); + }); + expect(result.current.noteUi[DAY_ID]).toBeDefined(); + + act(() => { + result.current.cancelNote(DAY_ID); + }); + expect(result.current.noteUi[DAY_ID]).toBeUndefined(); + }); + + it('FE-HOOK-DAYNOTES-007: saveNote with empty text is a no-op', async () => { + const spy = vi.fn(); + server.use( + http.post('/api/trips/:id/days/:dayId/notes', () => { + spy(); + return HttpResponse.json({ note: buildDayNote() }); + }) + ); + + const { result } = renderHook(() => useDayNotes(TRIP_ID), { wrapper }); + + act(() => { + result.current.setNoteUi({ [DAY_ID]: { mode: 'add', text: '', time: '', icon: 'FileText', sortOrder: 0 } }); + }); + + await act(async () => { + await result.current.saveNote(DAY_ID); + }); + + expect(spy).not.toHaveBeenCalled(); + // noteUi remains set (no cancelNote was called) + expect(result.current.noteUi[DAY_ID]).toBeDefined(); + }); + + it('FE-HOOK-DAYNOTES-008: saveNote in add mode calls addDayNote and clears UI', async () => { + const createdNote = buildDayNote({ day_id: DAY_ID, text: 'New note' }); + server.use( + http.post('/api/trips/:id/days/:dayId/notes', async () => { + return HttpResponse.json({ note: createdNote }); + }) + ); + + const { result } = renderHook(() => useDayNotes(TRIP_ID), { wrapper }); + + act(() => { + result.current.setNoteUi({ + [DAY_ID]: { mode: 'add', text: 'New note', time: '', icon: 'FileText', sortOrder: 0 }, + }); + }); + + await act(async () => { + await result.current.saveNote(DAY_ID); + }); + + // UI should be cleared after successful save + expect(result.current.noteUi[DAY_ID]).toBeUndefined(); + }); + + it('FE-HOOK-DAYNOTES-009: saveNote in edit mode calls updateDayNote and clears UI', async () => { + const noteId = 55; + const updatedNote = buildDayNote({ id: noteId, day_id: DAY_ID, text: 'Updated' }); + server.use( + http.put('/api/trips/:id/days/:dayId/notes/:noteId', async () => { + return HttpResponse.json({ note: updatedNote }); + }) + ); + + const { result } = renderHook(() => useDayNotes(TRIP_ID), { wrapper }); + + act(() => { + result.current.setNoteUi({ + [DAY_ID]: { mode: 'edit', noteId, text: 'Updated', time: '', icon: 'FileText' }, + }); + }); + + await act(async () => { + await result.current.saveNote(DAY_ID); + }); + + expect(result.current.noteUi[DAY_ID]).toBeUndefined(); + }); + + it('FE-HOOK-DAYNOTES-010: deleteNote calls deleteDayNote on the store', async () => { + const note = buildDayNote({ id: 77, day_id: DAY_ID }); + useTripStore.setState({ dayNotes: { [String(DAY_ID)]: [note] } }); + + server.use( + http.delete('/api/trips/:id/days/:dayId/notes/:noteId', () => { + return HttpResponse.json({ success: true }); + }) + ); + + const { result } = renderHook(() => useDayNotes(TRIP_ID), { wrapper }); + + await act(async () => { + await result.current.deleteNote(DAY_ID, 77); + }); + + // Note should be removed from the store + const dayNotes = useTripStore.getState().dayNotes[String(DAY_ID)] || []; + expect(dayNotes.find((n) => n.id === 77)).toBeUndefined(); + }); + + it('FE-HOOK-DAYNOTES-011: saveNote on API error shows toast', async () => { + const toastSpy = vi.fn(); + window.__addToast = toastSpy; + + server.use( + http.post('/api/trips/:id/days/:dayId/notes', () => { + return HttpResponse.json({ error: 'Server error' }, { status: 500 }); + }) + ); + + const { result } = renderHook(() => useDayNotes(TRIP_ID), { wrapper }); + + act(() => { + result.current.setNoteUi({ + [DAY_ID]: { mode: 'add', text: 'Test note', time: '', icon: 'FileText', sortOrder: 0 }, + }); + }); + + await act(async () => { + await result.current.saveNote(DAY_ID); + }); + + expect(toastSpy).toHaveBeenCalledWith(expect.any(String), 'error', undefined); + delete window.__addToast; + }); + + it('FE-HOOK-DAYNOTES-012: deleteNote on API error shows toast', async () => { + const toastSpy = vi.fn(); + window.__addToast = toastSpy; + + const note = buildDayNote({ id: 88, day_id: DAY_ID }); + useTripStore.setState({ dayNotes: { [String(DAY_ID)]: [note] } }); + + server.use( + http.delete('/api/trips/:id/days/:dayId/notes/:noteId', () => { + return HttpResponse.json({ error: 'Server error' }, { status: 500 }); + }) + ); + + const { result } = renderHook(() => useDayNotes(TRIP_ID), { wrapper }); + + await act(async () => { + await result.current.deleteNote(DAY_ID, 88); + }); + + expect(toastSpy).toHaveBeenCalledWith(expect.any(String), 'error', undefined); + delete window.__addToast; + }); + + it('FE-HOOK-DAYNOTES-013: moveNote up calculates midpoint sort order', async () => { + let capturedBody: Record = {}; + server.use( + http.put('/api/trips/:id/days/:dayId/notes/:noteId', async ({ request }) => { + capturedBody = await request.json() as Record; + return HttpResponse.json({ note: buildDayNote() }); + }) + ); + + const { result } = renderHook(() => useDayNotes(TRIP_ID), { wrapper }); + + const noteA = buildDayNote({ id: 1 }); + const noteB = buildDayNote({ id: 2 }); + const noteC = buildDayNote({ id: 3 }); + + // merged items with sortKeys 0, 2, 4 + const getMergedItems = () => [ + { type: 'note' as const, sortKey: 0, data: noteA }, + { type: 'note' as const, sortKey: 2, data: noteB }, + { type: 'note' as const, sortKey: 4, data: noteC }, + ]; + + // Move noteC (idx=2) up → new order should be between idx=0 and idx=1 → (0+2)/2 = 1 + await act(async () => { + await result.current.moveNote(DAY_ID, noteC.id, 'up', getMergedItems); + }); + + expect(capturedBody.sort_order).toBe(1); // (sortKey[0] + sortKey[1]) / 2 = (0+2)/2 + }); + + it('FE-HOOK-DAYNOTES-014: moveNote down calculates midpoint sort order', async () => { + let capturedBody: Record = {}; + server.use( + http.put('/api/trips/:id/days/:dayId/notes/:noteId', async ({ request }) => { + capturedBody = await request.json() as Record; + return HttpResponse.json({ note: buildDayNote() }); + }) + ); + + const { result } = renderHook(() => useDayNotes(TRIP_ID), { wrapper }); + + const noteA = buildDayNote({ id: 1 }); + const noteB = buildDayNote({ id: 2 }); + const noteC = buildDayNote({ id: 3 }); + + const getMergedItems = () => [ + { type: 'note' as const, sortKey: 0, data: noteA }, + { type: 'note' as const, sortKey: 2, data: noteB }, + { type: 'note' as const, sortKey: 4, data: noteC }, + ]; + + // Move noteA (idx=0) down → new order between idx=1 and idx=2 → (2+4)/2 = 3 + await act(async () => { + await result.current.moveNote(DAY_ID, noteA.id, 'down', getMergedItems); + }); + + expect(capturedBody.sort_order).toBe(3); // (sortKey[1] + sortKey[2]) / 2 = (2+4)/2 + }); + + it('FE-HOOK-DAYNOTES-015: moveNote up at index 0 is a no-op', async () => { + const spy = vi.fn(); + server.use( + http.put('/api/trips/:id/days/:dayId/notes/:noteId', () => { + spy(); + return HttpResponse.json({ note: buildDayNote() }); + }) + ); + + const { result } = renderHook(() => useDayNotes(TRIP_ID), { wrapper }); + + const noteA = buildDayNote({ id: 1 }); + const getMergedItems = () => [ + { type: 'note' as const, sortKey: 0, data: noteA }, + ]; + + await act(async () => { + await result.current.moveNote(DAY_ID, noteA.id, 'up', getMergedItems); + }); + + expect(spy).not.toHaveBeenCalled(); + }); + + it('FE-HOOK-DAYNOTES-016: moveNote down at last index is a no-op', async () => { + const spy = vi.fn(); + server.use( + http.put('/api/trips/:id/days/:dayId/notes/:noteId', () => { + spy(); + return HttpResponse.json({ note: buildDayNote() }); + }) + ); + + const { result } = renderHook(() => useDayNotes(TRIP_ID), { wrapper }); + + const noteA = buildDayNote({ id: 1 }); + const getMergedItems = () => [ + { type: 'note' as const, sortKey: 0, data: noteA }, + ]; + + await act(async () => { + await result.current.moveNote(DAY_ID, noteA.id, 'down', getMergedItems); + }); + + expect(spy).not.toHaveBeenCalled(); + }); + + it('FE-HOOK-DAYNOTES-017: moveNote down at last item uses sortKey + 1', async () => { + let capturedBody: Record = {}; + server.use( + http.put('/api/trips/:id/days/:dayId/notes/:noteId', async ({ request }) => { + capturedBody = await request.json() as Record; + return HttpResponse.json({ note: buildDayNote() }); + }) + ); + + const { result } = renderHook(() => useDayNotes(TRIP_ID), { wrapper }); + + const noteA = buildDayNote({ id: 1 }); + const noteB = buildDayNote({ id: 2 }); + + const getMergedItems = () => [ + { type: 'note' as const, sortKey: 5, data: noteA }, + { type: 'note' as const, sortKey: 10, data: noteB }, + ]; + + // Move noteA (idx=0) down — only 2 items, so idx < length-1 is false after going down + // direction=down, idx=0, length=2, idx < length-2 is false (0 < 0), so newSortOrder = sortKey[1]+1 = 11 + await act(async () => { + await result.current.moveNote(DAY_ID, noteA.id, 'down', getMergedItems); + }); + + expect(capturedBody.sort_order).toBe(11); // sortKey[idx+1] + 1 = 10 + 1 + }); + + it('FE-HOOK-DAYNOTES-018: moveNote on error shows toast', async () => { + const toastSpy = vi.fn(); + window.__addToast = toastSpy; + + server.use( + http.put('/api/trips/:id/days/:dayId/notes/:noteId', () => { + return HttpResponse.json({ error: 'Server error' }, { status: 500 }); + }) + ); + + const { result } = renderHook(() => useDayNotes(TRIP_ID), { wrapper }); + + const noteA = buildDayNote({ id: 1 }); + const noteB = buildDayNote({ id: 2 }); + + const getMergedItems = () => [ + { type: 'note' as const, sortKey: 0, data: noteA }, + { type: 'note' as const, sortKey: 1, data: noteB }, + ]; + + await act(async () => { + await result.current.moveNote(DAY_ID, noteA.id, 'down', getMergedItems); + }); + + expect(toastSpy).toHaveBeenCalledWith(expect.any(String), 'error', undefined); + delete window.__addToast; + }); + + it('FE-HOOK-DAYNOTES-019: moveNote up with only 1 item before uses sortKey - 1', async () => { + let capturedBody: Record = {}; + server.use( + http.put('/api/trips/:id/days/:dayId/notes/:noteId', async ({ request }) => { + capturedBody = await request.json() as Record; + return HttpResponse.json({ note: buildDayNote() }); + }) + ); + + const { result } = renderHook(() => useDayNotes(TRIP_ID), { wrapper }); + + const noteA = buildDayNote({ id: 1 }); + const noteB = buildDayNote({ id: 2 }); + + const getMergedItems = () => [ + { type: 'note' as const, sortKey: 5, data: noteA }, + { type: 'note' as const, sortKey: 10, data: noteB }, + ]; + + // Move noteB (idx=1) up — idx >= 2 is false, so newSortOrder = sortKey[idx-1] - 1 = 5-1 = 4 + await act(async () => { + await result.current.moveNote(DAY_ID, noteB.id, 'up', getMergedItems); + }); + + expect(capturedBody.sort_order).toBe(4); // sortKey[0] - 1 = 5 - 1 + }); + + it('FE-HOOK-DAYNOTES-020: openAddNote calls expandDay if provided', () => { + const expandDay = vi.fn(); + const { result } = renderHook(() => useDayNotes(TRIP_ID), { wrapper }); + + act(() => { + result.current.openAddNote(DAY_ID, () => [], expandDay); + }); + + expect(expandDay).toHaveBeenCalledWith(DAY_ID); + }); +}); + +// Type augment for window.__addToast — must mirror the canonical declaration +// in components/shared/Toast.tsx (a divergent signature is a merge conflict). +declare global { + interface Window { + __addToast?: (message: string, type?: 'success' | 'error' | 'warning' | 'info', duration?: number) => number; + } +} diff --git a/client/tests/integration/hooks/useInAppNotificationListener.test.ts b/client/tests/integration/hooks/useInAppNotificationListener.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..13cfeb0ae3039c424e8e9ed5468d426209e319b7 --- /dev/null +++ b/client/tests/integration/hooks/useInAppNotificationListener.test.ts @@ -0,0 +1,226 @@ +import { renderHook, act } from '@testing-library/react'; +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { useInAppNotificationStore } from '../../../src/store/inAppNotificationStore'; +import { resetAllStores } from '../../helpers/store'; + +// Capture the listener registered via addListener so we can simulate WS events +let capturedListener: ((event: Record) => void) | null = null; + +vi.mock('../../../src/api/websocket', () => ({ + connect: vi.fn(), + disconnect: vi.fn(), + getSocketId: vi.fn(() => null), + setRefetchCallback: vi.fn(), + setPreReconnectHook: vi.fn(), + joinTrip: vi.fn(), + leaveTrip: vi.fn(), + addListener: vi.fn((fn) => { + capturedListener = fn; + }), + removeListener: vi.fn(), +})); + +const wsMock = await import('../../../src/api/websocket'); + +// Import the hook after the mock is in place +const { useInAppNotificationListener } = await import('../../../src/hooks/useInAppNotificationListener'); + +describe('useInAppNotificationListener', () => { + beforeEach(() => { + capturedListener = null; + resetAllStores(); + vi.clearAllMocks(); + // Re-capture after clear + (wsMock.addListener as ReturnType).mockImplementation((fn) => { + capturedListener = fn; + }); + }); + + it('FE-HOOK-NOTIFLISTENER-001: on mount, addListener is called once', () => { + const { unmount } = renderHook(() => useInAppNotificationListener()); + expect(wsMock.addListener).toHaveBeenCalledTimes(1); + unmount(); + }); + + it('FE-HOOK-NOTIFLISTENER-002: on unmount, removeListener is called with the same function', () => { + const { unmount } = renderHook(() => useInAppNotificationListener()); + + const registeredFn = (wsMock.addListener as ReturnType).mock.calls[0][0]; + unmount(); + + expect(wsMock.removeListener).toHaveBeenCalledWith(registeredFn); + }); + + it('FE-HOOK-NOTIFLISTENER-003: notification:new event calls handleNewNotification on the store', () => { + const handleNew = vi.fn(); + useInAppNotificationStore.setState({ handleNewNotification: handleNew } as any); + + const { unmount } = renderHook(() => useInAppNotificationListener()); + + expect(capturedListener).toBeTypeOf('function'); + + const notification = { + id: 1, type: 'simple', scope: 'trip', target: 1, sender_id: null, sender_username: null, + sender_avatar: null, recipient_id: 2, title_key: 'test', title_params: '{}', + text_key: 'test_body', text_params: '{}', positive_text_key: null, negative_text_key: null, + response: null, navigate_text_key: null, navigate_target: null, is_read: 0, + created_at: '2025-01-01T00:00:00Z', + }; + + act(() => { + capturedListener!({ type: 'notification:new', notification }); + }); + + expect(handleNew).toHaveBeenCalledWith(notification); + unmount(); + }); + + it('FE-HOOK-NOTIFLISTENER-004: notification:updated event calls handleUpdatedNotification on the store', () => { + const handleUpdated = vi.fn(); + useInAppNotificationStore.setState({ handleUpdatedNotification: handleUpdated } as any); + + const { unmount } = renderHook(() => useInAppNotificationListener()); + + const notification = { + id: 5, type: 'simple', scope: 'user', target: 1, sender_id: null, sender_username: null, + sender_avatar: null, recipient_id: 2, title_key: 'updated', title_params: '{}', + text_key: 'updated_body', text_params: '{}', positive_text_key: null, negative_text_key: null, + response: 'positive', navigate_text_key: null, navigate_target: null, is_read: 1, + created_at: '2025-01-01T00:00:00Z', + }; + + act(() => { + capturedListener!({ type: 'notification:updated', notification }); + }); + + expect(handleUpdated).toHaveBeenCalledWith(notification); + unmount(); + }); + + it('FE-HOOK-NOTIFLISTENER-005: unrelated event types are ignored', () => { + const handleNew = vi.fn(); + const handleUpdated = vi.fn(); + useInAppNotificationStore.setState({ + handleNewNotification: handleNew, + handleUpdatedNotification: handleUpdated, + } as any); + + const { unmount } = renderHook(() => useInAppNotificationListener()); + + act(() => { + capturedListener!({ type: 'place:created', data: {} }); + }); + + expect(handleNew).not.toHaveBeenCalled(); + expect(handleUpdated).not.toHaveBeenCalled(); + unmount(); + }); + + it('FE-HOOK-NOTIFLISTENER-006: notification:new actually updates the store unreadCount', () => { + renderHook(() => useInAppNotificationListener()); + + const initialCount = useInAppNotificationStore.getState().unreadCount; + + act(() => { + capturedListener!({ + type: 'notification:new', + notification: { + id: 99, type: 'simple', scope: 'trip', target: 1, sender_id: null, sender_username: null, + sender_avatar: null, recipient_id: 2, title_key: 'test', title_params: {}, + text_key: 'body', text_params: {}, positive_text_key: null, negative_text_key: null, + response: null, navigate_text_key: null, navigate_target: null, is_read: false, + created_at: '2025-01-01T00:00:00Z', + }, + }); + }); + + expect(useInAppNotificationStore.getState().unreadCount).toBe(initialCount + 1); + }); + + it('FE-HOOK-NOTIFLISTENER-007: notification:updated updates the notification in the store', () => { + // Seed a notification + useInAppNotificationStore.setState({ + notifications: [{ + id: 10, type: 'simple', scope: 'trip', target: 1, sender_id: null, sender_username: null, + sender_avatar: null, recipient_id: 2, title_key: 'test', title_params: {}, + text_key: 'body', text_params: {}, positive_text_key: null, negative_text_key: null, + response: null, navigate_text_key: null, navigate_target: null, is_read: false, + created_at: '2025-01-01T00:00:00Z', + }], + }); + + renderHook(() => useInAppNotificationListener()); + + act(() => { + capturedListener!({ + type: 'notification:updated', + notification: { + id: 10, type: 'simple', scope: 'trip', target: 1, sender_id: null, sender_username: null, + sender_avatar: null, recipient_id: 2, title_key: 'test', title_params: {}, + text_key: 'body', text_params: {}, positive_text_key: null, negative_text_key: null, + response: 'positive', navigate_text_key: null, navigate_target: null, is_read: true, + created_at: '2025-01-01T00:00:00Z', + }, + }); + }); + + const updated = useInAppNotificationStore.getState().notifications.find((n) => n.id === 10); + expect(updated?.response).toBe('positive'); + expect(updated?.is_read).toBe(true); + }); + + it('FE-HOOK-NOTIFLISTENER-008: multiple events processed correctly in sequence', () => { + const { unmount } = renderHook(() => useInAppNotificationListener()); + + const initial = useInAppNotificationStore.getState().unreadCount; + + act(() => { + capturedListener!({ + type: 'notification:new', + notification: { + id: 101, type: 'simple', scope: 'trip', target: 1, sender_id: null, sender_username: null, + sender_avatar: null, recipient_id: 2, title_key: 'k1', title_params: {}, + text_key: 'b1', text_params: {}, positive_text_key: null, negative_text_key: null, + response: null, navigate_text_key: null, navigate_target: null, is_read: false, + created_at: '2025-01-01T00:00:00Z', + }, + }); + capturedListener!({ + type: 'notification:new', + notification: { + id: 102, type: 'simple', scope: 'trip', target: 1, sender_id: null, sender_username: null, + sender_avatar: null, recipient_id: 2, title_key: 'k2', title_params: {}, + text_key: 'b2', text_params: {}, positive_text_key: null, negative_text_key: null, + response: null, navigate_text_key: null, navigate_target: null, is_read: false, + created_at: '2025-01-01T00:00:00Z', + }, + }); + }); + + expect(useInAppNotificationStore.getState().unreadCount).toBe(initial + 2); + unmount(); + }); + + it('FE-HOOK-NOTIFLISTENER-009: listener added on mount is the same one removed on unmount', () => { + const { unmount } = renderHook(() => useInAppNotificationListener()); + + const addedFn = (wsMock.addListener as ReturnType).mock.calls[0][0]; + unmount(); + const removedFn = (wsMock.removeListener as ReturnType).mock.calls[0][0]; + + expect(addedFn).toBe(removedFn); + }); + + it('FE-HOOK-NOTIFLISTENER-010: after unmount, listener no longer processes events', () => { + const handleNew = vi.fn(); + useInAppNotificationStore.setState({ handleNewNotification: handleNew } as any); + + const { unmount } = renderHook(() => useInAppNotificationListener()); + unmount(); + + // capturedListener is captured but the component is unmounted + // The removeListener was called — the actual implementation would have unregistered it + // We verify removeListener was called (the cleanup ran) + expect(wsMock.removeListener).toHaveBeenCalled(); + }); +}); diff --git a/client/tests/integration/hooks/useResizablePanels.test.ts b/client/tests/integration/hooks/useResizablePanels.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..b3b085336d6460b4ae3a51d0c0444e74e0c77cdc --- /dev/null +++ b/client/tests/integration/hooks/useResizablePanels.test.ts @@ -0,0 +1,168 @@ +import { renderHook, act } from '@testing-library/react'; +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { fireEvent } from '@testing-library/react'; +import { useResizablePanels } from '../../../src/hooks/useResizablePanels'; + +describe('useResizablePanels', () => { + beforeEach(() => { + localStorage.clear(); + vi.clearAllMocks(); + }); + + it('FE-HOOK-PANELS-001: default leftWidth is 340 when localStorage is empty', () => { + const { result } = renderHook(() => useResizablePanels()); + expect(result.current.leftWidth).toBe(340); + }); + + it('FE-HOOK-PANELS-002: default rightWidth is 300 when localStorage is empty', () => { + const { result } = renderHook(() => useResizablePanels()); + expect(result.current.rightWidth).toBe(300); + }); + + it('FE-HOOK-PANELS-003: leftWidth loaded from localStorage when set', () => { + localStorage.setItem('sidebarLeftWidth', '400'); + const { result } = renderHook(() => useResizablePanels()); + expect(result.current.leftWidth).toBe(400); + }); + + it('FE-HOOK-PANELS-004: rightWidth loaded from localStorage when set', () => { + localStorage.setItem('sidebarRightWidth', '350'); + const { result } = renderHook(() => useResizablePanels()); + expect(result.current.rightWidth).toBe(350); + }); + + it('FE-HOOK-PANELS-005: startResizeLeft sets body cursor to col-resize', () => { + const { result } = renderHook(() => useResizablePanels()); + act(() => { + result.current.startResizeLeft(); + }); + expect(document.body.style.cursor).toBe('col-resize'); + }); + + it('FE-HOOK-PANELS-006: startResizeRight sets body cursor to col-resize', () => { + const { result } = renderHook(() => useResizablePanels()); + act(() => { + result.current.startResizeRight(); + }); + expect(document.body.style.cursor).toBe('col-resize'); + }); + + it('FE-HOOK-PANELS-007: mousedown → mousemove → mouseup updates leftWidth and persists to localStorage', async () => { + const { result } = renderHook(() => useResizablePanels()); + + act(() => { + result.current.startResizeLeft(); + }); + + // mousemove with clientX=350 → w = max(200, min(520, 350-10)) = 340 + act(() => { + fireEvent.mouseMove(document, { clientX: 350 }); + }); + + expect(result.current.leftWidth).toBe(340); + expect(localStorage.getItem('sidebarLeftWidth')).toBe('340'); + + act(() => { + fireEvent.mouseUp(document); + }); + + expect(document.body.style.cursor).toBe(''); + }); + + it('FE-HOOK-PANELS-008: mousedown → mousemove → mouseup updates rightWidth and persists to localStorage', () => { + // Set window.innerWidth for the right panel calculation + Object.defineProperty(window, 'innerWidth', { writable: true, configurable: true, value: 1200 }); + + const { result } = renderHook(() => useResizablePanels()); + + act(() => { + result.current.startResizeRight(); + }); + + // mousemove with clientX=800 → w = max(200, min(520, 1200-800-10)) = max(200, min(520, 390)) = 390 + act(() => { + fireEvent.mouseMove(document, { clientX: 800 }); + }); + + expect(result.current.rightWidth).toBe(390); + expect(localStorage.getItem('sidebarRightWidth')).toBe('390'); + + act(() => { + fireEvent.mouseUp(document); + }); + + expect(document.body.style.cursor).toBe(''); + }); + + it('FE-HOOK-PANELS-009: min width constraint (200) is enforced for left panel', () => { + const { result } = renderHook(() => useResizablePanels()); + + act(() => { + result.current.startResizeLeft(); + }); + + // clientX=50 → w = max(200, min(520, 50-10)) = max(200, 40) = 200 + act(() => { + fireEvent.mouseMove(document, { clientX: 50 }); + }); + + expect(result.current.leftWidth).toBe(200); + }); + + it('FE-HOOK-PANELS-010: max width constraint (520) is enforced for left panel', () => { + const { result } = renderHook(() => useResizablePanels()); + + act(() => { + result.current.startResizeLeft(); + }); + + // clientX=600 → w = max(200, min(520, 600-10)) = min(520, 590) = 520 + act(() => { + fireEvent.mouseMove(document, { clientX: 600 }); + }); + + expect(result.current.leftWidth).toBe(520); + }); + + it('FE-HOOK-PANELS-011: mousemove without prior startResize does nothing', () => { + const { result } = renderHook(() => useResizablePanels()); + + const initialLeft = result.current.leftWidth; + const initialRight = result.current.rightWidth; + + act(() => { + fireEvent.mouseMove(document, { clientX: 400 }); + }); + + expect(result.current.leftWidth).toBe(initialLeft); + expect(result.current.rightWidth).toBe(initialRight); + }); + + it('FE-HOOK-PANELS-012: body userSelect set to none during resize, cleared on mouseup', () => { + const { result } = renderHook(() => useResizablePanels()); + + act(() => { + result.current.startResizeLeft(); + }); + + expect(document.body.style.userSelect).toBe('none'); + + act(() => { + fireEvent.mouseUp(document); + }); + + expect(document.body.style.userSelect).toBe(''); + }); + + it('FE-HOOK-PANELS-013: leftCollapsed and rightCollapsed default to false', () => { + const { result } = renderHook(() => useResizablePanels()); + expect(result.current.leftCollapsed).toBe(false); + expect(result.current.rightCollapsed).toBe(false); + }); + + it('FE-HOOK-PANELS-014: setLeftCollapsed and setRightCollapsed are exposed', () => { + const { result } = renderHook(() => useResizablePanels()); + expect(result.current.setLeftCollapsed).toBeTypeOf('function'); + expect(result.current.setRightCollapsed).toBeTypeOf('function'); + }); +}); diff --git a/client/tests/integration/hooks/useRouteCalculation.test.ts b/client/tests/integration/hooks/useRouteCalculation.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..aa97af5be44f6cb0b7e8cec5f44443b38615be1b --- /dev/null +++ b/client/tests/integration/hooks/useRouteCalculation.test.ts @@ -0,0 +1,294 @@ +import { renderHook, act } from '@testing-library/react'; +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { useRouteCalculation } from '../../../src/hooks/useRouteCalculation'; +import { useTripStore } from '../../../src/store/tripStore'; +import { buildAssignment, buildPlace } from '../../helpers/factories'; +import type { TripStoreState } from '../../../src/store/tripStore'; +import type { RouteSegment } from '../../../src/types'; + +// Mock the RouteCalculator module to avoid real OSRM fetch calls +vi.mock('../../../src/components/Map/RouteCalculator', () => ({ + calculateRouteWithLegs: vi.fn(), + calculateRoute: vi.fn(), + optimizeRoute: vi.fn((waypoints: unknown[]) => waypoints), + generateGoogleMapsUrl: vi.fn(), +})); + +const { calculateRouteWithLegs } = await import('../../../src/components/Map/RouteCalculator'); + +function buildMockStore(assignments: Record[]> = {}): Partial { + // Also populate the real Zustand store so updateRouteForDay (which reads from + // useTripStore.getState()) sees the same assignments as the hook's tripStore param. + // Reset reservations and days to empty so transport-split logic doesn't interfere. + useTripStore.setState({ assignments, reservations: [], days: [] } as any); + return { assignments } as Partial; +} + +const MOCK_SEGMENTS: RouteSegment[] = [ + { + mid: [48.5, 2.5], + from: [48.86, 2.35], + to: [48.21, 16.37], + distance: 343000, + duration: 12600, + distanceText: '343 km', + durationText: '3 h 30 min', + walkingText: '70 h', + drivingText: '3 h 30 min', + }, +]; + +// Empty coordinates make the hook fall back to the straight-line geometry, +// so the `route` assertions keep checking the raw waypoints while the legs +// still flow through to `routeSegments`. +const MOCK_ROUTE_WITH_LEGS = { + coordinates: [] as [number, number][], + distance: 343000, + duration: 12600, + legs: MOCK_SEGMENTS, +}; + +describe('useRouteCalculation', () => { + beforeEach(() => { + vi.clearAllMocks(); + // Reset trip store assignments so each test starts clean + useTripStore.setState({ assignments: {} } as any); + (calculateRouteWithLegs as ReturnType).mockResolvedValue(MOCK_ROUTE_WITH_LEGS); + }); + + it('FE-HOOK-ROUTE-001: with no selectedDayId, route is null', () => { + const store = buildMockStore({}); + const { result } = renderHook(() => + useRouteCalculation(store as TripStoreState, null) + ); + expect(result.current.route).toBeNull(); + }); + + it('FE-HOOK-ROUTE-002: with < 2 waypoints, route remains null', async () => { + const place = buildPlace({ lat: 48.8566, lng: 2.3522 }); + const assignment = buildAssignment({ day_id: 5, order_index: 0, place }); + const store = buildMockStore({ '5': [assignment] }); + + const { result } = renderHook(() => + useRouteCalculation(store as TripStoreState, 5) + ); + + await act(async () => {}); + expect(result.current.route).toBeNull(); + }); + + it('FE-HOOK-ROUTE-003: with ≥ 2 geo-coded assignments, sets route coordinates', async () => { + const p1 = buildPlace({ lat: 48.8566, lng: 2.3522 }); + const p2 = buildPlace({ lat: 51.5074, lng: -0.1278 }); + const a1 = buildAssignment({ day_id: 5, order_index: 0, place: p1 }); + const a2 = buildAssignment({ day_id: 5, order_index: 1, place: p2 }); + const store = buildMockStore({ '5': [a1, a2] }); + + const { result } = renderHook(() => + useRouteCalculation(store as TripStoreState, 5) + ); + + await act(async () => {}); + // route is an array of segments; no transport → single segment with all places + expect(result.current.route).toEqual([ + [[p1.lat, p1.lng], [p2.lat, p2.lng]], + ]); + }); + + it('FE-HOOK-ROUTE-004: calls calculateRouteWithLegs and exposes the returned segments', async () => { + const p1 = buildPlace({ lat: 48.8566, lng: 2.3522 }); + const p2 = buildPlace({ lat: 51.5074, lng: -0.1278 }); + const a1 = buildAssignment({ day_id: 5, order_index: 0, place: p1 }); + const a2 = buildAssignment({ day_id: 5, order_index: 1, place: p2 }); + const store = buildMockStore({ '5': [a1, a2] }); + + const { result } = renderHook(() => + useRouteCalculation(store as TripStoreState, 5) + ); + + await act(async () => {}); + + expect(calculateRouteWithLegs).toHaveBeenCalled(); + expect(result.current.routeSegments).toEqual(MOCK_SEGMENTS); + }); + + it('FE-HOOK-ROUTE-006: assignments are sorted by order_index before extracting waypoints', async () => { + const p1 = buildPlace({ lat: 10, lng: 10 }); + const p2 = buildPlace({ lat: 20, lng: 20 }); + // order_index 1 comes before 0 in the array, but should be sorted + const a1 = buildAssignment({ day_id: 5, order_index: 1, place: p1 }); + const a2 = buildAssignment({ day_id: 5, order_index: 0, place: p2 }); + const store = buildMockStore({ '5': [a1, a2] }); + + const { result } = renderHook(() => + useRouteCalculation(store as TripStoreState, 5) + ); + + await act(async () => {}); + + // After sort: a2 (order_index=0) first, then a1 (order_index=1) + expect(result.current.route).toEqual([ + [[p2.lat, p2.lng], [p1.lat, p1.lng]], + ]); + }); + + it('FE-HOOK-ROUTE-007: assignments with no lat/lng are filtered out', async () => { + const pValid = buildPlace({ lat: 48.8566, lng: 2.3522 }); + const pNoGeo = buildPlace({ lat: null as any, lng: null as any }); + const a1 = buildAssignment({ day_id: 5, order_index: 0, place: pNoGeo }); + const a2 = buildAssignment({ day_id: 5, order_index: 1, place: pValid }); + const store = buildMockStore({ '5': [a1, a2] }); + + const { result } = renderHook(() => + useRouteCalculation(store as TripStoreState, 5) + ); + + await act(async () => {}); + // Only 1 valid waypoint → route is null + expect(result.current.route).toBeNull(); + }); + + it('FE-HOOK-ROUTE-008: AbortController.abort() is called when selectedDayId changes', async () => { + + // Make calculateRouteWithLegs resolve slowly + let resolveSegments!: (val: typeof MOCK_ROUTE_WITH_LEGS) => void; + (calculateRouteWithLegs as ReturnType).mockImplementationOnce( + (_waypoints: unknown[], options: { signal?: AbortSignal }) => { + return new Promise((resolve) => { + resolveSegments = resolve; + options?.signal?.addEventListener('abort', () => resolve(MOCK_ROUTE_WITH_LEGS)); + }); + } + ); + + const p1 = buildPlace({ lat: 10, lng: 10 }); + const p2 = buildPlace({ lat: 20, lng: 20 }); + const a1 = buildAssignment({ day_id: 5, order_index: 0, place: p1 }); + const a2 = buildAssignment({ day_id: 5, order_index: 1, place: p2 }); + + const store1 = buildMockStore({ '5': [a1, a2], '6': [a1, a2] }); + + const { rerender } = renderHook( + ({ dayId }: { dayId: number }) => useRouteCalculation(store1 as TripStoreState, dayId), + { initialProps: { dayId: 5 } } + ); + + // Change to day 6 — should abort in-flight request for day 5 + await act(async () => { + rerender({ dayId: 6 }); + }); + + // calculateRouteWithLegs should have been called at least once for day 5 + // and once more for day 6 + expect((calculateRouteWithLegs as ReturnType).mock.calls.length).toBeGreaterThanOrEqual(1); + + // Cleanup + resolveSegments?.(MOCK_ROUTE_WITH_LEGS); + }); + + it('FE-HOOK-ROUTE-009: AbortError from calculateSegments does not set routeSegments to []', async () => { + + const abortError = new Error('Aborted'); + abortError.name = 'AbortError'; + (calculateRouteWithLegs as ReturnType).mockRejectedValueOnce(abortError); + + const p1 = buildPlace({ lat: 10, lng: 10 }); + const p2 = buildPlace({ lat: 20, lng: 20 }); + const a1 = buildAssignment({ day_id: 5, order_index: 0, place: p1 }); + const a2 = buildAssignment({ day_id: 5, order_index: 1, place: p2 }); + const store = buildMockStore({ '5': [a1, a2] }); + + const { result } = renderHook(() => + useRouteCalculation(store as TripStoreState, 5) + ); + + await act(async () => {}); + // AbortError should be swallowed silently — segments remain empty + expect(result.current.routeSegments).toEqual([]); + }); + + it('FE-HOOK-ROUTE-010: non-AbortError from calculateSegments sets routeSegments to []', async () => { + + (calculateRouteWithLegs as ReturnType).mockRejectedValueOnce(new Error('Network error')); + + const p1 = buildPlace({ lat: 10, lng: 10 }); + const p2 = buildPlace({ lat: 20, lng: 20 }); + const a1 = buildAssignment({ day_id: 5, order_index: 0, place: p1 }); + const a2 = buildAssignment({ day_id: 5, order_index: 1, place: p2 }); + const store = buildMockStore({ '5': [a1, a2] }); + + const { result } = renderHook(() => + useRouteCalculation(store as TripStoreState, 5) + ); + + await act(async () => {}); + expect(result.current.routeSegments).toEqual([]); + }); + + it('FE-HOOK-ROUTE-011: when selectedDayId is null, route and segments are cleared', async () => { + const p1 = buildPlace({ lat: 10, lng: 10 }); + const p2 = buildPlace({ lat: 20, lng: 20 }); + const a1 = buildAssignment({ day_id: 5, order_index: 0, place: p1 }); + const a2 = buildAssignment({ day_id: 5, order_index: 1, place: p2 }); + const store = buildMockStore({ '5': [a1, a2] }); + + const { result, rerender } = renderHook( + ({ dayId }: { dayId: number | null }) => useRouteCalculation(store as TripStoreState, dayId), + { initialProps: { dayId: 5 as number | null } } + ); + + await act(async () => {}); + // Some route may have been set for day 5 + + await act(async () => { + rerender({ dayId: null }); + }); + + expect(result.current.route).toBeNull(); + expect(result.current.routeSegments).toEqual([]); + }); + + it('FE-HOOK-ROUTE-012: setRoute and setRouteInfo are exposed', () => { + const store = buildMockStore({}); + const { result } = renderHook(() => + useRouteCalculation(store as TripStoreState, null) + ); + expect(result.current.setRoute).toBeTypeOf('function'); + expect(result.current.setRouteInfo).toBeTypeOf('function'); + }); + + it('FE-HOOK-ROUTE-013: route recalculates when assignments change via store update', async () => { + + const p1 = buildPlace({ lat: 10, lng: 10 }); + const p2 = buildPlace({ lat: 20, lng: 20 }); + const a1 = buildAssignment({ day_id: 5, order_index: 0, place: p1 }); + const a2 = buildAssignment({ day_id: 5, order_index: 1, place: p2 }); + + let storeData = buildMockStore({ '5': [a1, a2] }); + + const { result, rerender } = renderHook(() => + useRouteCalculation(storeData as TripStoreState, 5) + ); + + await act(async () => {}); + + expect(result.current.route).toEqual([ + [[p1.lat, p1.lng], [p2.lat, p2.lng]], + ]); + + // Now add a third place — update both the local store object and the Zustand store + const p3 = buildPlace({ lat: 30, lng: 30 }); + const a3 = buildAssignment({ day_id: 5, order_index: 2, place: p3 }); + storeData = buildMockStore({ '5': [a1, a2, a3] }); // also calls useTripStore.setState + + await act(async () => { + rerender(); + }); + + await act(async () => {}); + + expect(result.current.route).toEqual([ + [[p1.lat, p1.lng], [p2.lat, p2.lng], [p3.lat, p3.lng]], + ]); + }); +}); diff --git a/client/tests/integration/hooks/useTripWebSocket.test.ts b/client/tests/integration/hooks/useTripWebSocket.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..6ea0ccbc63d2abf130a0f58f91d6520bec7feb71 --- /dev/null +++ b/client/tests/integration/hooks/useTripWebSocket.test.ts @@ -0,0 +1,135 @@ +import { renderHook, act } from '@testing-library/react'; +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { useTripWebSocket } from '../../../src/hooks/useTripWebSocket'; +import { useTripStore } from '../../../src/store/tripStore'; + +vi.mock('../../../src/api/websocket', () => ({ + connect: vi.fn(), + disconnect: vi.fn(), + getSocketId: vi.fn(() => 'mock-socket-id'), + joinTrip: vi.fn(), + leaveTrip: vi.fn(), + addListener: vi.fn(), + removeListener: vi.fn(), + setRefetchCallback: vi.fn(), + setPreReconnectHook: vi.fn(), +})); + +// Import the mocked module AFTER vi.mock +const wsMock = await import('../../../src/api/websocket'); + +describe('useTripWebSocket', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('FE-HOOK-WS-001: on mount, joinTrip(tripId) is called', () => { + const { unmount } = renderHook(() => useTripWebSocket(42)); + expect(wsMock.joinTrip).toHaveBeenCalledWith(42); + unmount(); + }); + + it('FE-HOOK-WS-002: on mount, addListener is called (registers event handlers)', () => { + const { unmount } = renderHook(() => useTripWebSocket(42)); + // addListener is called twice: once for handleRemoteEvent, once for collabFileSync + expect(wsMock.addListener).toHaveBeenCalled(); + expect((wsMock.addListener as ReturnType).mock.calls.length).toBeGreaterThanOrEqual(1); + unmount(); + }); + + it('FE-HOOK-WS-003: on unmount, leaveTrip(tripId) is called', () => { + const { unmount } = renderHook(() => useTripWebSocket(42)); + unmount(); + expect(wsMock.leaveTrip).toHaveBeenCalledWith(42); + }); + + it('FE-HOOK-WS-004: on unmount, removeListener is called', () => { + const { unmount } = renderHook(() => useTripWebSocket(42)); + unmount(); + expect(wsMock.removeListener).toHaveBeenCalled(); + }); + + it('FE-HOOK-WS-005: when tripId changes, leaves old trip and joins new one', () => { + const { rerender, unmount } = renderHook(({ id }) => useTripWebSocket(id), { + initialProps: { id: 1 as number | undefined }, + }); + expect(wsMock.joinTrip).toHaveBeenCalledWith(1); + + rerender({ id: 2 }); + + expect(wsMock.leaveTrip).toHaveBeenCalledWith(1); + expect(wsMock.joinTrip).toHaveBeenCalledWith(2); + unmount(); + }); + + it('FE-HOOK-WS-006: one of the registered listeners is handleRemoteEvent from tripStore', () => { + const handler = useTripStore.getState().handleRemoteEvent; + renderHook(() => useTripWebSocket(42)); + + const addListenerCalls = (wsMock.addListener as ReturnType).mock.calls; + const registeredFunctions = addListenerCalls.map((call) => call[0]); + expect(registeredFunctions).toContain(handler); + }); + + it('FE-HOOK-WS-006b: collab file sync listener is also registered (second addListener call)', () => { + const { unmount } = renderHook(() => useTripWebSocket(42)); + // Two listeners registered: handleRemoteEvent + collabFileSync + expect((wsMock.addListener as ReturnType).mock.calls.length).toBe(2); + unmount(); + }); + + it('FE-HOOK-WS-006c: collab file sync listener reacts to collab:note:deleted events', () => { + const mockLoadFiles = vi.fn(); + useTripStore.setState({ loadFiles: mockLoadFiles } as any); + + renderHook(() => useTripWebSocket(42)); + + // The second addListener call is the collabFileSync function + const addListenerCalls = (wsMock.addListener as ReturnType).mock.calls; + const collabFileSync = addListenerCalls[1]?.[0]; + expect(collabFileSync).toBeTypeOf('function'); + + act(() => { + collabFileSync({ type: 'collab:note:deleted' }); + }); + + expect(mockLoadFiles).toHaveBeenCalledWith(42); + }); + + it('FE-HOOK-WS-006d: collab file sync listener reacts to collab:note:updated events', () => { + const mockLoadFiles = vi.fn(); + useTripStore.setState({ loadFiles: mockLoadFiles } as any); + + renderHook(() => useTripWebSocket(42)); + + const addListenerCalls = (wsMock.addListener as ReturnType).mock.calls; + const collabFileSync = addListenerCalls[1]?.[0]; + + act(() => { + collabFileSync({ type: 'collab:note:updated' }); + }); + + expect(mockLoadFiles).toHaveBeenCalledWith(42); + }); + + it('FE-HOOK-WS-006e: collab file sync listener ignores unrelated event types', () => { + const mockLoadFiles = vi.fn(); + useTripStore.setState({ loadFiles: mockLoadFiles } as any); + + renderHook(() => useTripWebSocket(42)); + + const addListenerCalls = (wsMock.addListener as ReturnType).mock.calls; + const collabFileSync = addListenerCalls[1]?.[0]; + + act(() => { + collabFileSync({ type: 'place:created' }); + }); + + expect(mockLoadFiles).not.toHaveBeenCalled(); + }); + + it('FE-HOOK-WS-007: no joinTrip call when tripId is undefined', () => { + renderHook(() => useTripWebSocket(undefined)); + expect(wsMock.joinTrip).not.toHaveBeenCalled(); + }); +}); diff --git a/client/tests/setup.ts b/client/tests/setup.ts new file mode 100644 index 0000000000000000000000000000000000000000..0c0fc01231f8821f757d6679acca8d95eb75b852 --- /dev/null +++ b/client/tests/setup.ts @@ -0,0 +1,86 @@ +import '@testing-library/jest-dom/vitest'; +import 'fake-indexeddb/auto'; +import { cleanup } from '@testing-library/react'; +import { afterAll, afterEach, beforeAll, vi } from 'vitest'; +import { server } from './helpers/msw/server'; + +// Mock the websocket module so stores don't try to open real connections +vi.mock('../src/api/websocket', () => ({ + connect: vi.fn(), + disconnect: vi.fn(), + getSocketId: vi.fn(() => null), + setRefetchCallback: vi.fn(), + setPreReconnectHook: vi.fn(), +})); + +// MSW lifecycle +beforeAll(() => server.listen({ onUnhandledRequest: 'warn' })); +afterEach(() => { + server.resetHandlers(); + cleanup(); + localStorage.clear(); + sessionStorage.clear(); +}); +afterAll(() => server.close()); + +// ── jsdom stubs ──────────────────────────────────────────────────────────────── + +// Force en-US locale for toLocaleDateString so tests are deterministic on +// non-US dev machines (Windows-de-DE returns "Sonntag" instead of "Sunday"). +// Only affects calls without an explicit locale — callers that pass a locale +// keep their behavior. +const _origToLocaleDateString = Date.prototype.toLocaleDateString +Date.prototype.toLocaleDateString = function (locales?: Intl.LocalesArgument, options?: Intl.DateTimeFormatOptions) { + return _origToLocaleDateString.call(this, locales ?? 'en-US', options) +} + +// window.matchMedia — used by dark mode / responsive components +Object.defineProperty(window, 'matchMedia', { + writable: true, + value: vi.fn().mockImplementation((query: string) => ({ + matches: false, + media: query, + onchange: null, + addListener: vi.fn(), + removeListener: vi.fn(), + addEventListener: vi.fn(), + removeEventListener: vi.fn(), + dispatchEvent: vi.fn(), + })), +}); + +// IntersectionObserver — used by lazy loading +// Must use a class or regular function (not arrow function) so 'new IntersectionObserver()' works +class _MockIntersectionObserver { + observe = vi.fn() + unobserve = vi.fn() + disconnect = vi.fn() + root = null + rootMargin = '' + thresholds: ReadonlyArray = [] + takeRecords = vi.fn(() => []) + constructor(_callback: IntersectionObserverCallback, _options?: IntersectionObserverInit) {} +} +globalThis.IntersectionObserver = _MockIntersectionObserver as unknown as typeof IntersectionObserver; + +// ResizeObserver — used by resizable panels +class _MockResizeObserver { + observe = vi.fn() + unobserve = vi.fn() + disconnect = vi.fn() + constructor(_callback: ResizeObserverCallback) {} +} +globalThis.ResizeObserver = _MockResizeObserver as unknown as typeof ResizeObserver; + +// URL.createObjectURL / revokeObjectURL — Node 22 URL.createObjectURL requires +// a native node:buffer Blob; passing a jsdom Blob throws ERR_INVALID_ARG_TYPE. +// Tests that need blob URLs should mock fetch to return node:buffer Blobs so +// the real URL.createObjectURL works. For tests that only need the method to +// exist without returning a real URL, stub it here as a vi.fn fallback. +if (typeof URL.createObjectURL === 'undefined') { + Object.defineProperty(URL, 'createObjectURL', { writable: true, configurable: true, value: vi.fn(() => 'blob:mock') }); + Object.defineProperty(URL, 'revokeObjectURL', { writable: true, configurable: true, value: vi.fn() }); +} + +// Element.prototype.scrollIntoView — jsdom doesn't implement it +Element.prototype.scrollIntoView = vi.fn(); diff --git a/client/tests/unit/api/authUrl.test.ts b/client/tests/unit/api/authUrl.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..e40cb0b3aed1839b578065d103654ab5f0ad2cbc --- /dev/null +++ b/client/tests/unit/api/authUrl.test.ts @@ -0,0 +1,237 @@ +/// +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { http, HttpResponse } from 'msw'; +import { server } from '../../helpers/msw/server'; +import { getAuthUrl, fetchImageAsBlob, clearImageQueue } from '../../../src/api/authUrl'; + +// Flush microtasks + a macro-task so async handlers finish +const flushPromises = () => new Promise(r => setTimeout(r, 10)); + +beforeEach(() => { + clearImageQueue(); + vi.restoreAllMocks(); + vi.unstubAllGlobals(); +}); + +// ── getAuthUrl ───────────────────────────────────────────────────────────────── + +describe('getAuthUrl', () => { + describe('FE-COMP-AUTHURL-001: empty URL returns early', () => { + it('returns empty string without hitting the network', async () => { + const result = await getAuthUrl('', 'download'); + expect(result).toBe(''); + }); + }); + + describe('FE-COMP-AUTHURL-002: token appended with ?', () => { + it('appends token as first query param when URL has no query string', async () => { + server.use( + http.post('/api/auth/resource-token', () => + HttpResponse.json({ token: 'abc123' }) + ) + ); + const result = await getAuthUrl('/uploads/file.pdf', 'download'); + expect(result).toBe('/uploads/file.pdf?token=abc123'); + }); + }); + + describe('FE-COMP-AUTHURL-003: token appended with &', () => { + it('appends token as additional query param when URL already has a query string', async () => { + server.use( + http.post('/api/auth/resource-token', () => + HttpResponse.json({ token: 'xyz' }) + ) + ); + const result = await getAuthUrl('/uploads/file.pdf?size=lg', 'download'); + expect(result).toBe('/uploads/file.pdf?size=lg&token=xyz'); + }); + }); + + describe('FE-COMP-AUTHURL-004: non-ok API response returns original URL', () => { + it('returns original URL unchanged when resource-token returns 500', async () => { + server.use( + http.post('/api/auth/resource-token', () => + HttpResponse.json({}, { status: 500 }) + ) + ); + const result = await getAuthUrl('/uploads/file.pdf', 'download'); + expect(result).toBe('/uploads/file.pdf'); + }); + }); + + describe('FE-COMP-AUTHURL-005: fetch throws returns original URL', () => { + it('returns original URL when fetch throws a network error', async () => { + vi.spyOn(globalThis, 'fetch').mockRejectedValueOnce( + new TypeError('Network error') + ); + const result = await getAuthUrl('/uploads/file.pdf', 'download'); + expect(result).toBe('/uploads/file.pdf'); + }); + }); +}); + +// ── fetchImageAsBlob ─────────────────────────────────────────────────────────── + +describe('fetchImageAsBlob', () => { + describe('FE-COMP-AUTHURL-006: empty URL returns empty string', () => { + it('resolves to empty string without network call', async () => { + const result = await fetchImageAsBlob(''); + expect(result).toBe(''); + }); + }); + + describe('FE-COMP-AUTHURL-007: successful fetch returns blob object URL', () => { + it('resolves to a blob URL for a valid image response', async () => { + // Node 22 URL.createObjectURL requires a native node:buffer Blob, not a + // jsdom Blob — passing the wrong type throws ERR_INVALID_ARG_TYPE (caught, + // returns ''). Mock fetch directly with a Node Blob so the real + // URL.createObjectURL works without any mocking needed. + const { Blob: NodeBlob } = await import('node:buffer'); + vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce({ + ok: true, + blob: () => Promise.resolve(new NodeBlob(['fake-image'], { type: 'image/jpeg' }) as unknown as Blob), + } as unknown as Response); + const result = await fetchImageAsBlob('/uploads/photo.jpg'); + expect(result).toMatch(/^blob:/); + }); + }); + + describe('FE-COMP-AUTHURL-008: non-ok response resolves to empty string', () => { + it('resolves to empty string when image URL returns 404', async () => { + server.use( + http.get('/uploads/missing.jpg', () => + HttpResponse.json({}, { status: 404 }) + ) + ); + const result = await fetchImageAsBlob('/uploads/missing.jpg'); + expect(result).toBe(''); + }); + }); + + describe('FE-COMP-AUTHURL-009: fetch throws resolves to empty string', () => { + it('resolves to empty string when fetch rejects', async () => { + vi.spyOn(globalThis, 'fetch').mockRejectedValueOnce( + new TypeError('Network error') + ); + const result = await fetchImageAsBlob('/uploads/error.jpg'); + expect(result).toBe(''); + }); + }); + + // ── Concurrency tests use vi.spyOn(fetch) for synchronous barrier control ── + // When the spy mock runs, it executes synchronously up to its first `await`, + // so `resolvers.push(r)` happens synchronously inside fetchImageAsBlob(), giving + // us deterministic access to in-flight requests without needing flushPromises(). + + describe('FE-COMP-AUTHURL-010: concurrency cap at MAX_CONCURRENT=6', () => { + it('fires at most 6 requests simultaneously', async () => { + let concurrent = 0; + let maxConcurrent = 0; + const resolvers: Array<() => void> = []; + + vi.spyOn(globalThis, 'fetch').mockImplementation(async () => { + concurrent++; + maxConcurrent = Math.max(maxConcurrent, concurrent); + await new Promise(r => resolvers.push(r)); + concurrent--; + return new Response(new Blob(['img'], { type: 'image/jpeg' }), { status: 200 }); + }); + + const urls = Array.from({ length: 8 }, (_, i) => `/uploads/img${i}.jpg`); + const promises = urls.map(url => fetchImageAsBlob(url)); + + // After synchronous calls: 6 run()s called fetch() and pushed to resolvers, + // 2 are in the module queue + expect(resolvers.length).toBe(6); + expect(maxConcurrent).toBeLessThanOrEqual(6); + + // Drain iteratively: each pass resolves current in-flight requests, + // then the next batch from the queue starts and pushes new resolvers + while (resolvers.length > 0) { + resolvers.splice(0).forEach(r => r()); + await flushPromises(); + } + + await Promise.all(promises); + expect(maxConcurrent).toBeLessThanOrEqual(6); + }); + }); + + describe('FE-COMP-AUTHURL-011: queued request runs after active slot frees', () => { + it('7th request eventually resolves once one of the 6 active slots is freed', async () => { + const resolvers: Array<() => void> = []; + const { Blob: NodeBlob } = await import('node:buffer'); + + vi.spyOn(globalThis, 'fetch').mockImplementation(async () => { + await new Promise(r => resolvers.push(r)); + return { + ok: true, + blob: () => Promise.resolve(new NodeBlob(['img'], { type: 'image/jpeg' }) as unknown as Blob), + } as unknown as Response; + }); + + const urls = Array.from({ length: 7 }, (_, i) => `/uploads/queue${i}.jpg`); + const promises = urls.map(url => fetchImageAsBlob(url)); + + // 6 in-flight, 1 queued + expect(resolvers.length).toBe(6); + + // Resolve the 6 active requests + resolvers.splice(0).forEach(r => r()); + await flushPromises(); + + // 7th should now have started + expect(resolvers.length).toBe(1); + + // Resolve the 7th + resolvers.splice(0).forEach(r => r()); + + const results = await Promise.all(promises); + expect(results).toHaveLength(7); + results.forEach(r => expect(r).toMatch(/^blob:/)); + }); + }); +}); + +// ── clearImageQueue ──────────────────────────────────────────────────────────── + +describe('clearImageQueue', () => { + describe('FE-COMP-AUTHURL-012: clearImageQueue discards pending entries', () => { + it('removes queued items so they never execute after active slots drain', async () => { + const resolvers: Array<() => void> = []; + // Track completions via fetch mock instead of URL.createObjectURL spy — + // URL.createObjectURL is a Node built-in whose identity varies across + // Node versions, making it unreliable to spy on in jsdom tests on CI. + let completedFetches = 0; + const { Blob: NodeBlob } = await import('node:buffer'); + + vi.spyOn(globalThis, 'fetch').mockImplementation(async () => { + await new Promise(r => resolvers.push(r)); + completedFetches++; + return { + ok: true, + blob: () => Promise.resolve(new NodeBlob(['img'], { type: 'image/jpeg' }) as unknown as Blob), + } as unknown as Response; + }); + + const urls = Array.from({ length: 7 }, (_, i) => `/uploads/clear${i}.jpg`); + const promises = urls.map(url => fetchImageAsBlob(url)); + + // 6 in-flight, 1 queued + expect(resolvers.length).toBe(6); + + // Discard the queued 7th request + clearImageQueue(); + + // Resolve the 6 active requests and let them drain + resolvers.splice(0).forEach(r => r()); + await flushPromises(); + + // 6 active slots completed; queue was cleared so the 7th never ran + expect(completedFetches).toBe(6); + + // First 6 promises resolved; 7th is orphaned (never resolves) + await Promise.all(promises.slice(0, 6)); + }); + }); +}); diff --git a/client/tests/unit/api/client.interceptor.test.ts b/client/tests/unit/api/client.interceptor.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..a28e3d21c13155479e21b5871b95f6ec9c306de0 --- /dev/null +++ b/client/tests/unit/api/client.interceptor.test.ts @@ -0,0 +1,71 @@ +// FE-CLIENT-INTERCEPTOR-001 to FE-CLIENT-INTERCEPTOR-012 +import { describe, it, expect } from 'vitest' +import { isAuthPublicPath } from '../../../src/api/client' + +describe('FE-CLIENT-INTERCEPTOR: 401 AUTH_REQUIRED redirect allowlist', () => { + describe('exact-match public paths — no redirect', () => { + it('FE-CLIENT-INTERCEPTOR-001: /login', () => { + expect(isAuthPublicPath('/login')).toBe(true) + }) + + it('FE-CLIENT-INTERCEPTOR-002: /register', () => { + expect(isAuthPublicPath('/register')).toBe(true) + }) + + it('FE-CLIENT-INTERCEPTOR-003: /forgot-password', () => { + expect(isAuthPublicPath('/forgot-password')).toBe(true) + }) + + it('FE-CLIENT-INTERCEPTOR-004: /reset-password', () => { + expect(isAuthPublicPath('/reset-password')).toBe(true) + }) + }) + + describe('prefix-match public paths — no redirect', () => { + it('FE-CLIENT-INTERCEPTOR-005: /shared/:token', () => { + expect(isAuthPublicPath('/shared/abc123token')).toBe(true) + }) + + it('FE-CLIENT-INTERCEPTOR-006: /public/journey/:token', () => { + expect(isAuthPublicPath('/public/journey/xyz789')).toBe(true) + }) + }) + + describe('paths that matched via includes() before fix — must redirect', () => { + it('FE-CLIENT-INTERCEPTOR-007: /admin/login', () => { + expect(isAuthPublicPath('/admin/login')).toBe(false) + }) + + it('FE-CLIENT-INTERCEPTOR-008: /admin/register', () => { + expect(isAuthPublicPath('/admin/register')).toBe(false) + }) + + it('FE-CLIENT-INTERCEPTOR-009: /some-login-page', () => { + expect(isAuthPublicPath('/some-login-page')).toBe(false) + }) + }) + + describe('paths that matched via loose startsWith before fix — must redirect', () => { + it('FE-CLIENT-INTERCEPTOR-010: /reset-password-extra', () => { + expect(isAuthPublicPath('/reset-password-extra')).toBe(false) + }) + + it('FE-CLIENT-INTERCEPTOR-011: /forgot-password-extra', () => { + expect(isAuthPublicPath('/forgot-password-extra')).toBe(false) + }) + }) + + describe('private app paths — must redirect', () => { + it('FE-CLIENT-INTERCEPTOR-012: /dashboard', () => { + expect(isAuthPublicPath('/dashboard')).toBe(false) + }) + + it('FE-CLIENT-INTERCEPTOR-013: /trips/123', () => { + expect(isAuthPublicPath('/trips/123')).toBe(false) + }) + + it('FE-CLIENT-INTERCEPTOR-014: / (root)', () => { + expect(isAuthPublicPath('/')).toBe(false) + }) + }) +}) diff --git a/client/tests/unit/db/offlineDb.test.ts b/client/tests/unit/db/offlineDb.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..1d130a72429682c34861f29a6cff50f7039ae4c1 --- /dev/null +++ b/client/tests/unit/db/offlineDb.test.ts @@ -0,0 +1,370 @@ +/** + * offlineDb unit tests. + * + * Uses fake-indexeddb so no real browser IDB is needed. + * Each test gets a fresh database by using `use-fake-indexeddb` with Dexie. + */ +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import 'fake-indexeddb/auto'; +import Dexie from 'dexie'; + +// Re-import after fake-indexeddb is set up so Dexie picks up the shim. +// We re-open a clean db in each test to isolate state. +import { + offlineDb, + clearTripData, + clearAll, + upsertTrip, + upsertDays, + upsertPlaces, + upsertPackingItems, + upsertTodoItems, + upsertBudgetItems, + upsertReservations, + upsertTripFiles, + upsertSyncMeta, + reopenForUser, + reopenAnonymous, + deleteCurrentUserDb, + enforceBlobBudget, + type QueuedMutation, + type SyncMeta, + type BlobCacheEntry, +} from '../../../src/db/offlineDb'; +import type { Trip, Day, Place, PackingItem, TodoItem, BudgetItem, Reservation, TripFile } from '../../../src/types'; + +// ── Fixtures ────────────────────────────────────────────────────────────────── + +const makeTrip = (id = 1): Trip => ({ + id, + user_id: 42, + title: `Trip ${id}`, + description: null, + start_date: '2026-07-01', + end_date: '2026-07-05', + currency: 'EUR', + cover_image: null, + is_archived: 0, + reminder_days: 3, + created_at: '2026-01-01T00:00:00Z', + updated_at: '2026-01-01T00:00:00Z', +}); + +const makeDay = (id: number, tripId = 1): Day => ({ + id, + trip_id: tripId, + date: '2026-07-01', + title: null, + notes: null, + assignments: [], + notes_items: [], +}); + +const makePlace = (id: number, tripId = 1): Place => ({ + id, + trip_id: tripId, + name: `Place ${id}`, + description: null, + notes: null, + lat: 48.8566, + lng: 2.3522, + address: null, + category_id: null, + price: null, + currency: null, + image_url: null, + google_place_id: null, + osm_id: null, + route_geometry: null, + place_time: null, + end_time: null, + duration_minutes: null, + transport_mode: null, + website: null, + phone: null, + created_at: '2026-01-01T00:00:00Z', +}); + +const makeBlob = (url: string, tripId = 1, bytes = 10, cachedAt = 1): BlobCacheEntry => ({ + url, + tripId, + blob: new Blob(['x'.repeat(bytes)], { type: 'application/pdf' }), + bytes, + mime: 'application/pdf', + cachedAt, +}); + +// ── Lifecycle ───────────────────────────────────────────────────────────────── + +beforeEach(async () => { + // Ensure DB is open (fake-indexeddb resets between test files but not between tests). + if (!offlineDb.isOpen()) await offlineDb.open(); + // Clear all tables before each test. + await clearAll(); +}); + +afterEach(async () => { + if (!offlineDb.isOpen()) await offlineDb.open(); +}); + +// ── Tests ───────────────────────────────────────────────────────────────────── + +describe('offlineDb — trips', () => { + it('stores and retrieves a trip via upsertTrip', async () => { + const trip = makeTrip(10); + await upsertTrip(trip); + const stored = await offlineDb.trips.get(10); + expect(stored).toBeDefined(); + expect(stored!.title).toBe('Trip 10'); + }); + + it('upsertTrip overwrites an existing trip (put semantics)', async () => { + await upsertTrip(makeTrip(1)); + await upsertTrip({ ...makeTrip(1), title: 'Updated' }); + const stored = await offlineDb.trips.get(1); + expect(stored!.title).toBe('Updated'); + }); +}); + +describe('offlineDb — days', () => { + it('stores days and retrieves by trip_id index', async () => { + await upsertDays([makeDay(1, 5), makeDay(2, 5), makeDay(3, 9)]); + const trip5Days = await offlineDb.days.where('trip_id').equals(5).toArray(); + expect(trip5Days).toHaveLength(2); + expect(trip5Days.map(d => d.id)).toContain(1); + expect(trip5Days.map(d => d.id)).toContain(2); + }); +}); + +describe('offlineDb — places', () => { + it('stores places and retrieves by trip_id', async () => { + await upsertPlaces([makePlace(10, 1), makePlace(11, 1), makePlace(12, 2)]); + const places = await offlineDb.places.where('trip_id').equals(1).toArray(); + expect(places).toHaveLength(2); + }); +}); + +describe('offlineDb — packing / todo / budget / reservations / files', () => { + it('upserts packing items', async () => { + const item: PackingItem = { id: 1, trip_id: 1, name: 'Passport', category: null, checked: 0, sort_order: 0, quantity: 1 }; + await upsertPackingItems([item]); + expect(await offlineDb.packingItems.count()).toBe(1); + }); + + it('upserts todo items', async () => { + const item: TodoItem = { + id: 1, trip_id: 1, name: 'Book hotel', category: null, checked: 0, + sort_order: 0, due_date: null, description: null, assigned_user_id: null, priority: 0, + }; + await upsertTodoItems([item]); + expect(await offlineDb.todoItems.count()).toBe(1); + }); + + it('upserts budget items', async () => { + const item: BudgetItem = { + id: 1, trip_id: 1, name: 'Flight', total_price: 500, + category: 'Transport', persons: 1, members: [], expense_date: null, sort_order: 0, + }; + await upsertBudgetItems([item]); + expect(await offlineDb.budgetItems.count()).toBe(1); + }); + + it('upserts reservations', async () => { + const item: Reservation = { + id: 1, trip_id: 1, title: 'Hotel', type: 'hotel', status: 'confirmed', + reservation_time: null, confirmation_number: null, notes: null, created_at: '2026-01-01T00:00:00Z', + }; + await upsertReservations([item]); + expect(await offlineDb.reservations.count()).toBe(1); + }); + + it('upserts trip files', async () => { + const file: TripFile = { + id: 1, trip_id: 1, filename: 'ticket.pdf', original_name: 'Ticket.pdf', + mime_type: 'application/pdf', url: '/api/trips/1/files/1/download', created_at: '2026-01-01T00:00:00Z', + }; + await upsertTripFiles([file]); + expect(await offlineDb.tripFiles.count()).toBe(1); + }); +}); + +describe('offlineDb — syncMeta', () => { + it('stores and retrieves syncMeta by tripId', async () => { + const meta: SyncMeta = { + tripId: 7, + lastSyncedAt: Date.now(), + status: 'idle', + tilesBbox: null, + filesCachedCount: 0, + }; + await upsertSyncMeta(meta); + const stored = await offlineDb.syncMeta.get(7); + expect(stored).toBeDefined(); + expect(stored!.status).toBe('idle'); + }); +}); + +describe('offlineDb — mutationQueue', () => { + it('stores queued mutations queryable by status', async () => { + const pending: QueuedMutation = { + id: 'uuid-1', tripId: 1, method: 'POST', url: '/api/trips/1/places', + body: { name: 'Eiffel Tower' }, createdAt: Date.now(), + status: 'pending', attempts: 0, lastError: null, + }; + const failed: QueuedMutation = { + id: 'uuid-2', tripId: 1, method: 'PUT', url: '/api/trips/1/places/5', + body: { name: 'Updated' }, createdAt: Date.now(), + status: 'failed', attempts: 3, lastError: 'Network error', + }; + await offlineDb.mutationQueue.bulkPut([pending, failed]); + + const pendingRows = await offlineDb.mutationQueue.where('status').equals('pending').toArray(); + expect(pendingRows).toHaveLength(1); + expect(pendingRows[0].id).toBe('uuid-1'); + + const failedRows = await offlineDb.mutationQueue.where('status').equals('failed').toArray(); + expect(failedRows).toHaveLength(1); + expect(failedRows[0].lastError).toBe('Network error'); + }); +}); + +describe('offlineDb — blobCache', () => { + it('stores and retrieves a Blob entry', async () => { + const blob = new Blob(['%PDF-1.4 test'], { type: 'application/pdf' }); + const entry: BlobCacheEntry = { + url: '/api/files/99/download', + tripId: 1, + blob, + bytes: blob.size, + mime: 'application/pdf', + cachedAt: Date.now(), + }; + await offlineDb.blobCache.put(entry); + + const stored = await offlineDb.blobCache.get('/api/files/99/download'); + expect(stored).toBeDefined(); + expect(stored!.mime).toBe('application/pdf'); + expect(stored!.blob).toBeDefined(); + }); + + it('queries blobs by tripId index', async () => { + await offlineDb.blobCache.bulkPut([ + makeBlob('/api/files/1/download', 1), + makeBlob('/api/files/2/download', 1), + makeBlob('/api/files/3/download', 2), + ]); + const trip1 = await offlineDb.blobCache.where('tripId').equals(1).toArray(); + expect(trip1).toHaveLength(2); + }); +}); + +describe('offlineDb — enforceBlobBudget', () => { + it('evicts oldest-by-cachedAt entries past the count budget', async () => { + // 5 entries with strictly increasing cachedAt; cap to 3. + for (let i = 0; i < 5; i++) { + await offlineDb.blobCache.put(makeBlob(`/api/files/${i}/download`, 1, 10, i + 1)); + } + await enforceBlobBudget(3, Infinity); + + expect(await offlineDb.blobCache.count()).toBe(3); + // Oldest two (cachedAt 1 and 2) are gone; newest survive. + expect(await offlineDb.blobCache.get('/api/files/0/download')).toBeUndefined(); + expect(await offlineDb.blobCache.get('/api/files/1/download')).toBeUndefined(); + expect(await offlineDb.blobCache.get('/api/files/4/download')).toBeDefined(); + }); + + it('evicts oldest entries past the byte budget', async () => { + // 3 entries of 100 bytes each; cap to 250 bytes → newest two (200) survive. + for (let i = 0; i < 3; i++) { + await offlineDb.blobCache.put(makeBlob(`/api/files/${i}/download`, 1, 100, i + 1)); + } + await enforceBlobBudget(Infinity, 250); + + expect(await offlineDb.blobCache.count()).toBe(2); + expect(await offlineDb.blobCache.get('/api/files/0/download')).toBeUndefined(); + }); + + it('is a no-op when already within budget', async () => { + await offlineDb.blobCache.put(makeBlob('/api/files/1/download', 1)); + await enforceBlobBudget(10, Infinity); + expect(await offlineDb.blobCache.count()).toBe(1); + }); +}); + +describe('offlineDb — clearTripData', () => { + it('removes all data for the given trip across all tables', async () => { + await upsertTrip(makeTrip(1)); + await upsertDays([makeDay(1, 1), makeDay(2, 1)]); + await upsertPlaces([makePlace(10, 1)]); + const item: PackingItem = { id: 5, trip_id: 1, name: 'Towel', category: null, checked: 0, sort_order: 0, quantity: 1 }; + await upsertPackingItems([item]); + + await offlineDb.blobCache.put(makeBlob('/api/files/1/download', 1)); + + // Also add data for a different trip — should NOT be removed + await upsertTrip(makeTrip(2)); + await upsertDays([makeDay(99, 2)]); + await offlineDb.blobCache.put(makeBlob('/api/files/2/download', 2)); + + await clearTripData(1); + + expect(await offlineDb.trips.get(1)).toBeUndefined(); + expect(await offlineDb.days.where('trip_id').equals(1).count()).toBe(0); + expect(await offlineDb.places.where('trip_id').equals(1).count()).toBe(0); + expect(await offlineDb.packingItems.where('trip_id').equals(1).count()).toBe(0); + expect(await offlineDb.blobCache.where('tripId').equals(1).count()).toBe(0); + + // Trip 2 intact + expect(await offlineDb.trips.get(2)).toBeDefined(); + expect(await offlineDb.days.where('trip_id').equals(2).count()).toBe(1); + expect(await offlineDb.blobCache.get('/api/files/2/download')).toBeDefined(); + }); +}); + +describe('offlineDb — clearAll', () => { + it('empties all tables', async () => { + await upsertTrip(makeTrip(1)); + await upsertDays([makeDay(1, 1), makeDay(2, 1)]); + await upsertPlaces([makePlace(10, 1)]); + + await clearAll(); + + expect(await offlineDb.trips.count()).toBe(0); + expect(await offlineDb.days.count()).toBe(0); + expect(await offlineDb.places.count()).toBe(0); + }); +}); + +describe('offlineDb — per-user scoping (B4)', () => { + afterEach(async () => { + // Leave the suite on the anonymous DB so other tests are unaffected. + await reopenAnonymous(); + }); + + it('isolates one user\'s cached data from another', async () => { + await reopenForUser(1); + await upsertPlaces([makePlace(10, 1)]); + expect(await offlineDb.places.count()).toBe(1); + + // Switching users must not expose user 1's rows. + await reopenForUser(2); + expect(await offlineDb.places.count()).toBe(0); + + // Switching back restores user 1's data (different physical DB). + await reopenForUser(1); + expect(await offlineDb.places.get(10)).toBeDefined(); + }); + + it('deleteCurrentUserDb wipes the user DB and returns to anonymous', async () => { + await reopenForUser(5); + await upsertPlaces([makePlace(20, 1)]); + + await deleteCurrentUserDb(); + // Now on the anonymous DB — no user data. + expect(await offlineDb.places.count()).toBe(0); + + // Re-opening user 5 starts empty (DB was deleted, not just detached). + await reopenForUser(5); + expect(await offlineDb.places.count()).toBe(0); + }); +}); diff --git a/client/tests/unit/hooks/usePlaceSelection.test.ts b/client/tests/unit/hooks/usePlaceSelection.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..a21a9404f002fcaadb4623a1f361b833d42eb425 --- /dev/null +++ b/client/tests/unit/hooks/usePlaceSelection.test.ts @@ -0,0 +1,63 @@ +import { describe, it, expect } from 'vitest'; +import { renderHook, act } from '@testing-library/react'; +import { usePlaceSelection } from '../../../src/hooks/usePlaceSelection'; + +// FE-HOOK-SEL-001 onwards + +describe('usePlaceSelection', () => { + it('FE-HOOK-SEL-001: initially both IDs are null', () => { + const { result } = renderHook(() => usePlaceSelection()); + expect(result.current.selectedPlaceId).toBeNull(); + expect(result.current.selectedAssignmentId).toBeNull(); + }); + + it('FE-HOOK-SEL-002: setSelectedPlaceId sets selectedPlaceId', () => { + const { result } = renderHook(() => usePlaceSelection()); + act(() => { result.current.setSelectedPlaceId(42); }); + expect(result.current.selectedPlaceId).toBe(42); + }); + + it('FE-HOOK-SEL-003: setSelectedPlaceId clears selectedAssignmentId', () => { + const { result } = renderHook(() => usePlaceSelection()); + // First set an assignment via selectAssignment + act(() => { result.current.selectAssignment(99, 10); }); + expect(result.current.selectedAssignmentId).toBe(99); + + // Now change the place — assignment must be cleared + act(() => { result.current.setSelectedPlaceId(20); }); + expect(result.current.selectedPlaceId).toBe(20); + expect(result.current.selectedAssignmentId).toBeNull(); + }); + + it('FE-HOOK-SEL-004: selectAssignment sets both selectedAssignmentId and selectedPlaceId', () => { + const { result } = renderHook(() => usePlaceSelection()); + act(() => { result.current.selectAssignment(7, 3); }); + expect(result.current.selectedAssignmentId).toBe(7); + expect(result.current.selectedPlaceId).toBe(3); + }); + + it('FE-HOOK-SEL-005: setSelectedPlaceId(null) resets selectedPlaceId to null and clears assignment', () => { + const { result } = renderHook(() => usePlaceSelection()); + act(() => { result.current.selectAssignment(5, 1); }); + act(() => { result.current.setSelectedPlaceId(null); }); + expect(result.current.selectedPlaceId).toBeNull(); + expect(result.current.selectedAssignmentId).toBeNull(); + }); + + it('FE-HOOK-SEL-006: selectAssignment(null, null) clears both IDs', () => { + const { result } = renderHook(() => usePlaceSelection()); + act(() => { result.current.selectAssignment(5, 1); }); + act(() => { result.current.selectAssignment(null, null); }); + expect(result.current.selectedAssignmentId).toBeNull(); + expect(result.current.selectedPlaceId).toBeNull(); + }); + + it('FE-HOOK-SEL-007: selecting a different place after an assignment clears the assignment', () => { + const { result } = renderHook(() => usePlaceSelection()); + act(() => { result.current.selectAssignment(11, 5); }); + // Switch to a different place without going through selectAssignment + act(() => { result.current.setSelectedPlaceId(99); }); + expect(result.current.selectedPlaceId).toBe(99); + expect(result.current.selectedAssignmentId).toBeNull(); + }); +}); diff --git a/client/tests/unit/hooks/usePlannerHistory.test.ts b/client/tests/unit/hooks/usePlannerHistory.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..9fb0d31d163cc8e64c6518283718628978e36fc1 --- /dev/null +++ b/client/tests/unit/hooks/usePlannerHistory.test.ts @@ -0,0 +1,92 @@ +import { describe, it, expect, vi } from 'vitest'; +import { renderHook, act } from '@testing-library/react'; +import { usePlannerHistory } from '../../../src/hooks/usePlannerHistory'; + +// FE-HOOK-HIST-001 onwards + +describe('usePlannerHistory', () => { + it('FE-HOOK-HIST-001: starts with canUndo=false and lastActionLabel=null', () => { + const { result } = renderHook(() => usePlannerHistory()); + expect(result.current.canUndo).toBe(false); + expect(result.current.lastActionLabel).toBeNull(); + }); + + it('FE-HOOK-HIST-002: pushing an entry sets canUndo=true and lastActionLabel', () => { + const { result } = renderHook(() => usePlannerHistory()); + act(() => { + result.current.pushUndo('Delete place', vi.fn()); + }); + expect(result.current.canUndo).toBe(true); + expect(result.current.lastActionLabel).toBe('Delete place'); + }); + + it('FE-HOOK-HIST-003: calling undo fires the undo function and sets canUndo=false', async () => { + const { result } = renderHook(() => usePlannerHistory()); + const undoFn = vi.fn(); + act(() => { + result.current.pushUndo('Add place', undoFn); + }); + await act(async () => { + await result.current.undo(); + }); + expect(undoFn).toHaveBeenCalledOnce(); + expect(result.current.canUndo).toBe(false); + }); + + it('FE-HOOK-HIST-004: multiple entries stack in LIFO order', () => { + const { result } = renderHook(() => usePlannerHistory()); + act(() => { + result.current.pushUndo('First', vi.fn()); + result.current.pushUndo('Second', vi.fn()); + result.current.pushUndo('Third', vi.fn()); + }); + expect(result.current.lastActionLabel).toBe('Third'); + }); + + it('FE-HOOK-HIST-005: undo consumes entries in LIFO order', async () => { + const { result } = renderHook(() => usePlannerHistory()); + const fn1 = vi.fn(); + const fn2 = vi.fn(); + act(() => { + result.current.pushUndo('First', fn1); + result.current.pushUndo('Second', fn2); + }); + await act(async () => { await result.current.undo(); }); + expect(fn2).toHaveBeenCalledOnce(); + expect(fn1).not.toHaveBeenCalled(); + expect(result.current.lastActionLabel).toBe('First'); + + await act(async () => { await result.current.undo(); }); + expect(fn1).toHaveBeenCalledOnce(); + expect(result.current.canUndo).toBe(false); + }); + + it('FE-HOOK-HIST-006: caps history at 30 entries', () => { + const { result } = renderHook(() => usePlannerHistory()); + act(() => { + for (let i = 0; i < 31; i++) { + result.current.pushUndo(`Action ${i}`, vi.fn()); + } + }); + // After 31 pushes with cap=30, the oldest entry (Action 0) should be dropped. + // canUndo must be true and the stack should not exceed 30. + expect(result.current.canUndo).toBe(true); + expect(result.current.lastActionLabel).toBe('Action 30'); + }); + + it('FE-HOOK-HIST-007: undo on an empty stack does not throw', async () => { + const { result } = renderHook(() => usePlannerHistory()); + await expect( + act(async () => { await result.current.undo(); }) + ).resolves.not.toThrow(); + expect(result.current.canUndo).toBe(false); + }); + + it('FE-HOOK-HIST-008: undo still sets canUndo=false after consuming the last entry', async () => { + const { result } = renderHook(() => usePlannerHistory()); + act(() => { result.current.pushUndo('Only', vi.fn()); }); + await act(async () => { await result.current.undo(); }); + expect(result.current.canUndo).toBe(false); + expect(result.current.lastActionLabel).toBeNull(); + }); +}); diff --git a/client/tests/unit/i18n/index.test.ts b/client/tests/unit/i18n/index.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..c5d966e7b4ae1b78867098313b9a40dfc670a091 --- /dev/null +++ b/client/tests/unit/i18n/index.test.ts @@ -0,0 +1,265 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest' +import { render } from '@testing-library/react' +import React from 'react' +import { + TranslationProvider, + useTranslation, + getLocaleForLanguage, + getIntlLanguage, + isRtlLanguage, + SUPPORTED_LANGUAGES, + detectBrowserLanguage, +} from '../../../src/i18n' +import { resetAllStores, seedStore } from '../../helpers/store' +import { useSettingsStore } from '../../../src/store/settingsStore' +import { buildSettings } from '../../helpers/factories' + +beforeEach(() => { + resetAllStores() + vi.clearAllMocks() +}) + +// ── FE-COMP-I18N-001: Barrel re-exports ─────────────────────────────────────── + +describe('barrel re-exports', () => { + it('FE-COMP-I18N-001: all named exports are defined with expected types', () => { + expect(TranslationProvider).toBeDefined() + expect(typeof TranslationProvider).toBe('function') + expect(useTranslation).toBeDefined() + expect(typeof useTranslation).toBe('function') + expect(getLocaleForLanguage).toBeDefined() + expect(typeof getLocaleForLanguage).toBe('function') + expect(getIntlLanguage).toBeDefined() + expect(typeof getIntlLanguage).toBe('function') + expect(isRtlLanguage).toBeDefined() + expect(typeof isRtlLanguage).toBe('function') + expect(SUPPORTED_LANGUAGES).toBeDefined() + expect(Array.isArray(SUPPORTED_LANGUAGES)).toBe(true) + }) +}) + +// ── FE-COMP-I18N-002/003: getLocaleForLanguage ──────────────────────────────── + +describe('getLocaleForLanguage', () => { + it('FE-COMP-I18N-002: returns correct locale for known languages', () => { + expect(getLocaleForLanguage('en')).toBe('en-US') + expect(getLocaleForLanguage('de')).toBe('de-DE') + expect(getLocaleForLanguage('zh-TW')).toBe('zh-TW') + expect(getLocaleForLanguage('ar')).toBe('ar-SA') + expect(getLocaleForLanguage('br')).toBe('pt-BR') + }) + + it('FE-COMP-I18N-003: falls back to en-US for unknown language codes', () => { + expect(getLocaleForLanguage('xx')).toBe('en-US') + }) +}) + +// ── FE-COMP-I18N-004/005/006: getIntlLanguage ───────────────────────────────── + +describe('getIntlLanguage', () => { + it('FE-COMP-I18N-004: returns language code for known supported languages', () => { + expect(getIntlLanguage('de')).toBe('de') + expect(getIntlLanguage('fr')).toBe('fr') + expect(getIntlLanguage('zh-TW')).toBe('zh-TW') + }) + + it('FE-COMP-I18N-005: maps br to pt-BR', () => { + expect(getIntlLanguage('br')).toBe('pt-BR') + }) + + it('FE-COMP-I18N-006: falls back to en for unknown codes', () => { + expect(getIntlLanguage('xx')).toBe('en') + }) +}) + +// ── FE-COMP-I18N-007/008: isRtlLanguage ────────────────────────────────────── + +describe('isRtlLanguage', () => { + it('FE-COMP-I18N-007: returns true only for Arabic', () => { + expect(isRtlLanguage('ar')).toBe(true) + }) + + it('FE-COMP-I18N-008: returns false for all other supported languages', () => { + expect(isRtlLanguage('en')).toBe(false) + expect(isRtlLanguage('de')).toBe(false) + expect(isRtlLanguage('zh-TW')).toBe(false) + }) +}) + +// ── FE-COMP-I18N-009: SUPPORTED_LANGUAGES ──────────────────────────────────── + +describe('SUPPORTED_LANGUAGES', () => { + it('FE-COMP-I18N-009: contains expected entries with value/label shape', () => { + expect(Array.isArray(SUPPORTED_LANGUAGES)).toBe(true) + expect(SUPPORTED_LANGUAGES).toHaveLength(20) + expect(SUPPORTED_LANGUAGES).toContainEqual(expect.objectContaining({ value: 'en', label: 'English' })) + expect(SUPPORTED_LANGUAGES).toContainEqual(expect.objectContaining({ value: 'tr', label: 'Türkçe' })) + expect(SUPPORTED_LANGUAGES).toContainEqual(expect.objectContaining({ value: 'ja', label: '日本語' })) + expect(SUPPORTED_LANGUAGES).toContainEqual(expect.objectContaining({ value: 'ko', label: '한국어' })) + expect(SUPPORTED_LANGUAGES).toContainEqual(expect.objectContaining({ value: 'uk', label: 'Українська' })) + expect(SUPPORTED_LANGUAGES).toContainEqual(expect.objectContaining({ value: 'ar', label: 'العربية' })) + }) +}) + +// ── FE-COMP-I18N-016 to 023: detectBrowserLanguage ─────────────────────────── + +describe('detectBrowserLanguage', () => { + afterEach(() => { + Object.defineProperty(navigator, 'languages', { value: [], configurable: true }) + Object.defineProperty(navigator, 'language', { value: '', configurable: true }) + }) + + it('FE-COMP-I18N-016: exact match returns the matched code', () => { + Object.defineProperty(navigator, 'languages', { value: ['de'], configurable: true }) + expect(detectBrowserLanguage()).toBe('de') + }) + + it('FE-COMP-I18N-017: region-tagged exact match (zh-TW) returns zh-TW', () => { + Object.defineProperty(navigator, 'languages', { value: ['zh-TW'], configurable: true }) + expect(detectBrowserLanguage()).toBe('zh-TW') + }) + + it('FE-COMP-I18N-018: prefix match (de-AT → de)', () => { + Object.defineProperty(navigator, 'languages', { value: ['de-AT'], configurable: true }) + expect(detectBrowserLanguage()).toBe('de') + }) + + it('FE-COMP-I18N-019: pt-PT returns null (European Portuguese is a distinct language)', () => { + Object.defineProperty(navigator, 'languages', { value: ['pt-PT'], configurable: true }) + expect(detectBrowserLanguage()).toBeNull() + }) + + it('FE-COMP-I18N-020: pt-BR maps to br', () => { + Object.defineProperty(navigator, 'languages', { value: ['pt-BR'], configurable: true }) + expect(detectBrowserLanguage()).toBe('br') + }) + + it('FE-COMP-I18N-021: first-match-wins across multiple entries', () => { + Object.defineProperty(navigator, 'languages', { value: ['xx-XX', 'fr'], configurable: true }) + expect(detectBrowserLanguage()).toBe('fr') + }) + + it('FE-COMP-I18N-022: unknown language returns null', () => { + Object.defineProperty(navigator, 'languages', { value: ['xx'], configurable: true }) + expect(detectBrowserLanguage()).toBeNull() + }) + + it('FE-COMP-I18N-023: falls back to navigator.language when navigator.languages is empty', () => { + Object.defineProperty(navigator, 'languages', { value: [], configurable: true }) + Object.defineProperty(navigator, 'language', { value: 'es', configurable: true }) + expect(detectBrowserLanguage()).toBe('es') + }) +}) + +// ── FE-COMP-I18N-010 to 015: TranslationProvider + useTranslation ───────────── + +describe('TranslationProvider + useTranslation integration', () => { + it('FE-COMP-I18N-010: useTranslation returns t, language, and locale', () => { + seedStore(useSettingsStore, { settings: buildSettings({ language: 'en' }) }) + + let result: { language: string; locale: string; tResult: string } | null = null + + function TestComponent() { + const { t, language, locale } = useTranslation() + result = { language, locale, tResult: t('common.loading') } + return null + } + + render( + React.createElement(TranslationProvider, null, React.createElement(TestComponent)) + ) + + expect(result).not.toBeNull() + expect(result!.language).toBe('en') + expect(result!.locale).toBe('en-US') + expect(result!.tResult).toBeTruthy() + expect(typeof result!.tResult).toBe('string') + }) + + it('FE-COMP-I18N-011: t() with params substitutes {count} placeholders', () => { + seedStore(useSettingsStore, { settings: buildSettings({ language: 'en' }) }) + + let translated = '' + + function TestComponent() { + const { t } = useTranslation() + translated = t('dashboard.subtitle.trips', { count: 5, archived: 2 }) + return null + } + + render( + React.createElement(TranslationProvider, null, React.createElement(TestComponent)) + ) + + expect(translated).toContain('5') + expect(translated).toContain('2') + expect(translated).not.toContain('{count}') + expect(translated).not.toContain('{archived}') + }) + + it('FE-COMP-I18N-012: TranslationProvider sets document.documentElement.lang', () => { + seedStore(useSettingsStore, { settings: buildSettings({ language: 'de' }) }) + + function TestComponent() { + useTranslation() + return null + } + + render( + React.createElement(TranslationProvider, null, React.createElement(TestComponent)) + ) + + expect(document.documentElement.lang).toBe('de') + }) + + it('FE-COMP-I18N-013: TranslationProvider sets dir=rtl for Arabic', () => { + seedStore(useSettingsStore, { settings: buildSettings({ language: 'ar' }) }) + + function TestComponent() { + useTranslation() + return null + } + + render( + React.createElement(TranslationProvider, null, React.createElement(TestComponent)) + ) + + expect(document.documentElement.dir).toBe('rtl') + }) + + it('FE-COMP-I18N-014: TranslationProvider sets dir=ltr for non-RTL language', () => { + seedStore(useSettingsStore, { settings: buildSettings({ language: 'en' }) }) + + function TestComponent() { + useTranslation() + return null + } + + render( + React.createElement(TranslationProvider, null, React.createElement(TestComponent)) + ) + + expect(document.documentElement.dir).toBe('ltr') + }) + + it('FE-COMP-I18N-015: t() falls back to English for unknown language', () => { + // Seed with a non-existent language to trigger fallback to English translations + seedStore(useSettingsStore, { settings: buildSettings({ language: 'xx' as any }) }) + + let translated = '' + + function TestComponent() { + const { t } = useTranslation() + translated = t('common.loading') + return null + } + + render( + React.createElement(TranslationProvider, null, React.createElement(TestComponent)) + ) + + // Should fall back to English translation (non-empty, not the key itself if key exists in en) + expect(typeof translated).toBe('string') + expect(translated.length).toBeGreaterThan(0) + }) +}) diff --git a/client/tests/unit/i18n/parity.test.ts b/client/tests/unit/i18n/parity.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..ec60b2eea1e4f1575e37d5983ebc6255b69483c7 --- /dev/null +++ b/client/tests/unit/i18n/parity.test.ts @@ -0,0 +1,64 @@ +import { describe, it, expect } from 'vitest' +import type { TranslationStrings } from '@trek/shared/i18n' +import en from '@trek/shared/i18n/en' +import de from '@trek/shared/i18n/de' +import es from '@trek/shared/i18n/es' +import fr from '@trek/shared/i18n/fr' +import hu from '@trek/shared/i18n/hu' +import itIT from '@trek/shared/i18n/it' +import tr from '@trek/shared/i18n/tr' +import ru from '@trek/shared/i18n/ru' +import zh from '@trek/shared/i18n/zh' +import zhTW from '@trek/shared/i18n/zh-TW' +import nl from '@trek/shared/i18n/nl' +import idID from '@trek/shared/i18n/id' +import ar from '@trek/shared/i18n/ar' +import br from '@trek/shared/i18n/br' +import cs from '@trek/shared/i18n/cs' +import pl from '@trek/shared/i18n/pl' +import ja from '@trek/shared/i18n/ja' +import ko from '@trek/shared/i18n/ko' +import uk from '@trek/shared/i18n/uk' +import gr from '@trek/shared/i18n/gr' + +// Runtime guard for the aggregated i18n bundles. `t()` resolves keys against the +// active locale's flat dot-key map (see TranslationContext), so a key that is +// present in en but missing in another locale silently falls back to English at +// runtime — easy to ship, hard to notice. This test fails loudly when any locale +// drifts away from the en key set so translators get an explicit, diagnostic list. +// +// The shared package also runs a file-level parity check (shared/scripts), but +// that one only inspects per-domain source files; this one asserts the *merged* +// export each locale actually serves to the app. + +const NON_EN_LOCALES: Record = { + de, es, fr, hu, it: itIT, tr, ru, zh, 'zh-TW': zhTW, nl, id: idID, + ar, br, cs, pl, ja, ko, uk, gr, +} + +const enKeys = new Set(Object.keys(en)) + +describe('i18n locale key parity', () => { + it('covers every non-en locale', () => { + // Keep the assertion set in lockstep with the supported language list minus en. + expect(Object.keys(NON_EN_LOCALES)).toHaveLength(19) + }) + + for (const [locale, strings] of Object.entries(NON_EN_LOCALES)) { + it(`${locale} has the exact same key set as en`, () => { + const localeKeys = new Set(Object.keys(strings)) + const missing = [...enKeys].filter((k) => !localeKeys.has(k)) + const extra = [...localeKeys].filter((k) => !enKeys.has(k)) + + const diagnostic = + `Locale "${locale}" key drift vs en — ` + + `missing ${missing.length}` + + (missing.length ? ` (${missing.slice(0, 10).join(', ')}${missing.length > 10 ? ', …' : ''})` : '') + + `; extra ${extra.length}` + + (extra.length ? ` (${extra.slice(0, 10).join(', ')}${extra.length > 10 ? ', …' : ''})` : '') + + expect(missing, diagnostic).toEqual([]) + expect(extra, diagnostic).toEqual([]) + }) + } +}) diff --git a/client/tests/unit/remoteEventHandler/assignments.test.ts b/client/tests/unit/remoteEventHandler/assignments.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..3da39f9779230f497c5a6bd250c287a3479873ac --- /dev/null +++ b/client/tests/unit/remoteEventHandler/assignments.test.ts @@ -0,0 +1,163 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import { useTripStore } from '../../../src/store/tripStore'; +import { resetAllStores } from '../../helpers/store'; +import { buildDay, buildAssignment, buildPlace } from '../../helpers/factories'; +import type { Assignment } from '../../../src/types'; + +beforeEach(() => { + resetAllStores(); +}); + +describe('remoteEventHandler > assignments', () => { + const seedData = () => { + useTripStore.setState({ + days: [buildDay({ id: 10 }), buildDay({ id: 20 })], + assignments: { + '10': [buildAssignment({ id: 100, day_id: 10 })], + '20': [], + }, + }); + }; + + it('FE-WSEVT-ASSIGN-001: assignment:created adds assignment to correct day', () => { + seedData(); + const newAssignment = buildAssignment({ id: 200, day_id: 20 }); + useTripStore.getState().handleRemoteEvent({ type: 'assignment:created', assignment: newAssignment }); + const { assignments } = useTripStore.getState(); + expect(assignments['20']).toHaveLength(1); + expect(assignments['20'][0].id).toBe(200); + expect(assignments['10']).toHaveLength(1); + }); + + it('FE-WSEVT-ASSIGN-002: assignment:created is idempotent — no duplicate if same ID', () => { + seedData(); + const duplicate = buildAssignment({ id: 100, day_id: 10 }); + useTripStore.getState().handleRemoteEvent({ type: 'assignment:created', assignment: duplicate }); + const { assignments } = useTripStore.getState(); + expect(assignments['10']).toHaveLength(1); + }); + + it('FE-WSEVT-ASSIGN-003: assignment:created replaces temp (negative) ID assignment with same place_id', () => { + const place = buildPlace({ id: 55 }); + const tempAssignment = buildAssignment({ id: -1, day_id: 10, place, place_id: place.id }); + useTripStore.setState({ + days: [buildDay({ id: 10 })], + assignments: { '10': [tempAssignment] }, + }); + const realAssignment = buildAssignment({ id: 500, day_id: 10, place, place_id: place.id }); + useTripStore.getState().handleRemoteEvent({ type: 'assignment:created', assignment: realAssignment }); + const { assignments } = useTripStore.getState(); + expect(assignments['10']).toHaveLength(1); + expect(assignments['10'][0].id).toBe(500); + }); + + it('FE-WSEVT-ASSIGN-003b: a second assignment of an already-present place is NOT suppressed (H11)', () => { + const place = buildPlace({ id: 55 }); + useTripStore.setState({ + days: [buildDay({ id: 10 })], + // A committed (positive-id) assignment of place 55 already on the day. + assignments: { '10': [buildAssignment({ id: 100, day_id: 10, place, place_id: place.id })] }, + }); + // A legitimately new, distinct assignment of the same place arrives. + const second = buildAssignment({ id: 300, day_id: 10, place, place_id: place.id }); + useTripStore.getState().handleRemoteEvent({ type: 'assignment:created', assignment: second }); + const { assignments } = useTripStore.getState(); + expect(assignments['10']).toHaveLength(2); + expect(assignments['10'].map(a => a.id).sort((x, y) => x - y)).toEqual([100, 300]); + }); + + it('FE-WSEVT-ASSIGN-003c: temp reconciliation replaces only the matching place, not a sibling temp (H11)', () => { + const place55 = buildPlace({ id: 55 }); + const place66 = buildPlace({ id: 66 }); + useTripStore.setState({ + days: [buildDay({ id: 10 })], + assignments: { + '10': [ + buildAssignment({ id: -1, day_id: 10, place: place55, place_id: 55 }), + buildAssignment({ id: -2, day_id: 10, place: place66, place_id: 66 }), + ], + }, + }); + const real = buildAssignment({ id: 500, day_id: 10, place: place55, place_id: 55 }); + useTripStore.getState().handleRemoteEvent({ type: 'assignment:created', assignment: real }); + const { assignments } = useTripStore.getState(); + const ids = assignments['10'].map(a => a.id); + expect(assignments['10']).toHaveLength(2); + expect(ids).toContain(500); // temp 55 reconciled to real + expect(ids).toContain(-2); // sibling temp 66 untouched + expect(ids).not.toContain(-1); + }); + + it('FE-WSEVT-ASSIGN-003d: place-less assignments do not collapse onto each other (H11)', () => { + // Defensive: a malformed event lacking place data must not let the + // `place?.id === placeId` reconciliation match undefined === undefined. + const placeless = (id: number): Assignment => + ({ ...buildAssignment({ id, day_id: 10 }), place: undefined, place_id: undefined } as unknown as Assignment); + useTripStore.setState({ + days: [buildDay({ id: 10 })], + assignments: { '10': [placeless(-1)] }, + }); + useTripStore.getState().handleRemoteEvent({ type: 'assignment:created', assignment: placeless(700) }); + const { assignments } = useTripStore.getState(); + // No placeId → no reconcile; both survive as distinct rows (no collapse). + expect(assignments['10']).toHaveLength(2); + }); + + it('FE-WSEVT-ASSIGN-004: assignment:updated merges updated data into correct day', () => { + seedData(); + const updated = buildAssignment({ id: 100, day_id: 10, notes: 'Updated notes' }); + useTripStore.getState().handleRemoteEvent({ type: 'assignment:updated', assignment: updated }); + const { assignments } = useTripStore.getState(); + expect(assignments['10'][0].notes).toBe('Updated notes'); + }); + + it('FE-WSEVT-ASSIGN-005: assignment:deleted removes assignment from day', () => { + seedData(); + useTripStore.getState().handleRemoteEvent({ type: 'assignment:deleted', assignmentId: 100, dayId: 10 }); + const { assignments } = useTripStore.getState(); + expect(assignments['10']).toHaveLength(0); + }); + + it('FE-WSEVT-ASSIGN-006: assignment:moved removes from old day and adds to new day', () => { + const movedAssignment = buildAssignment({ id: 100, day_id: 20 }); + useTripStore.setState({ + days: [buildDay({ id: 10 }), buildDay({ id: 20 })], + assignments: { + '10': [movedAssignment], + '20': [], + }, + }); + useTripStore.getState().handleRemoteEvent({ + type: 'assignment:moved', + assignment: movedAssignment, + oldDayId: 10, + newDayId: 20, + }); + const { assignments } = useTripStore.getState(); + expect(assignments['10']).toHaveLength(0); + expect(assignments['20']).toHaveLength(1); + expect(assignments['20'][0].id).toBe(100); + }); + + it('FE-WSEVT-ASSIGN-007: assignment:reordered updates order_index values', () => { + const a1 = buildAssignment({ id: 1, day_id: 10, order_index: 0 }); + const a2 = buildAssignment({ id: 2, day_id: 10, order_index: 1 }); + const a3 = buildAssignment({ id: 3, day_id: 10, order_index: 2 }); + useTripStore.setState({ + assignments: { '10': [a1, a2, a3] }, + }); + useTripStore.getState().handleRemoteEvent({ + type: 'assignment:reordered', + dayId: 10, + orderedIds: [3, 1, 2], + }); + const { assignments } = useTripStore.getState(); + const reordered = assignments['10']; + const item3 = reordered.find(a => a.id === 3); + const item1 = reordered.find(a => a.id === 1); + const item2 = reordered.find(a => a.id === 2); + expect(item3?.order_index).toBe(0); + expect(item1?.order_index).toBe(1); + expect(item2?.order_index).toBe(2); + }); +}); diff --git a/client/tests/unit/remoteEventHandler/budget.test.ts b/client/tests/unit/remoteEventHandler/budget.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..53e2b3f240711e4d5b86e96112cc027ae48a9a11 --- /dev/null +++ b/client/tests/unit/remoteEventHandler/budget.test.ts @@ -0,0 +1,93 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import { useTripStore } from '../../../src/store/tripStore'; +import { resetAllStores } from '../../helpers/store'; +import { buildBudgetItem } from '../../helpers/factories'; +import type { BudgetItemMember } from '../../../src/types'; + +beforeEach(() => { + resetAllStores(); +}); + +describe('remoteEventHandler > budget', () => { + const member1: BudgetItemMember = { user_id: 5, paid: 0, username: 'eve' }; + const member2: BudgetItemMember = { user_id: 6, paid: 1, username: 'frank' }; + + const seedData = () => { + useTripStore.setState({ + budgetItems: [ + buildBudgetItem({ id: 1, persons: 1, members: [{ ...member1 }] }), + buildBudgetItem({ id: 2, persons: 2, members: [{ ...member2 }] }), + ], + }); + }; + + it('FE-WSEVT-BUDGET-001: budget:created adds item to budgetItems', () => { + seedData(); + const newItem = buildBudgetItem({ id: 99, name: 'Hotel' }); + useTripStore.getState().handleRemoteEvent({ type: 'budget:created', item: newItem }); + const { budgetItems } = useTripStore.getState(); + expect(budgetItems).toHaveLength(3); + expect(budgetItems.find(i => i.id === 99)).toBeDefined(); + }); + + it('FE-WSEVT-BUDGET-002: budget:created is idempotent — no duplicate if same ID', () => { + seedData(); + const duplicate = buildBudgetItem({ id: 1, name: 'Duplicate' }); + useTripStore.getState().handleRemoteEvent({ type: 'budget:created', item: duplicate }); + const { budgetItems } = useTripStore.getState(); + expect(budgetItems).toHaveLength(2); + }); + + it('FE-WSEVT-BUDGET-003: budget:updated replaces item in array', () => { + seedData(); + const updated = buildBudgetItem({ id: 1, name: 'Updated Hotel', total_price: 500 }); + useTripStore.getState().handleRemoteEvent({ type: 'budget:updated', item: updated }); + const { budgetItems } = useTripStore.getState(); + const item = budgetItems.find(i => i.id === 1); + expect(item?.name).toBe('Updated Hotel'); + expect(item?.total_price).toBe(500); + }); + + it('FE-WSEVT-BUDGET-004: budget:deleted removes item by ID', () => { + seedData(); + useTripStore.getState().handleRemoteEvent({ type: 'budget:deleted', itemId: 1 }); + const { budgetItems } = useTripStore.getState(); + expect(budgetItems).toHaveLength(1); + expect(budgetItems.find(i => i.id === 1)).toBeUndefined(); + }); + + it('FE-WSEVT-BUDGET-005: budget:members-updated replaces entire members array and persons count', () => { + seedData(); + const newMembers: BudgetItemMember[] = [{ user_id: 7, paid: 1, username: 'grace' }, { user_id: 8, paid: 0, username: 'heidi' }]; + useTripStore.getState().handleRemoteEvent({ + type: 'budget:members-updated', + itemId: 1, + members: newMembers, + persons: 3, + }); + const { budgetItems } = useTripStore.getState(); + const item = budgetItems.find(i => i.id === 1); + expect(item?.members).toEqual(newMembers); + expect(item?.persons).toBe(3); + // Other item should be unchanged + const item2 = budgetItems.find(i => i.id === 2); + expect(item2?.members).toEqual([{ ...member2 }]); + }); + + it('FE-WSEVT-BUDGET-006: budget:member-paid-updated toggles specific member paid status', () => { + seedData(); + useTripStore.getState().handleRemoteEvent({ + type: 'budget:member-paid-updated', + itemId: 1, + userId: 5, + paid: true, + }); + const { budgetItems } = useTripStore.getState(); + const item = budgetItems.find(i => i.id === 1); + const m = item?.members?.find(m => m.user_id === 5); + expect(m?.paid).toBe(true); + // Other item members unchanged (member2 keeps its seeded paid value) + const item2 = budgetItems.find(i => i.id === 2); + expect(item2?.members?.[0].paid).toBe(1); + }); +}); diff --git a/client/tests/unit/remoteEventHandler/dayNotes.test.ts b/client/tests/unit/remoteEventHandler/dayNotes.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..1529680d7cc60bfcf6188fe0aa3474a5c5a38ffe --- /dev/null +++ b/client/tests/unit/remoteEventHandler/dayNotes.test.ts @@ -0,0 +1,60 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import { useTripStore } from '../../../src/store/tripStore'; +import { resetAllStores } from '../../helpers/store'; +import { buildDayNote } from '../../helpers/factories'; + +beforeEach(() => { + resetAllStores(); +}); + +describe('remoteEventHandler > dayNotes', () => { + const seedData = () => { + useTripStore.setState({ + dayNotes: { + '10': [buildDayNote({ id: 1, day_id: 10, text: 'Original' })], + '20': [], + }, + }); + }; + + it('FE-WSEVT-DAYNOTE-001: dayNote:created adds note to correct day', () => { + seedData(); + const newNote = buildDayNote({ id: 99, day_id: 10, text: 'New note' }); + useTripStore.getState().handleRemoteEvent({ type: 'dayNote:created', dayId: 10, note: newNote }); + const { dayNotes } = useTripStore.getState(); + expect(dayNotes['10']).toHaveLength(2); + expect(dayNotes['10'].find(n => n.id === 99)).toBeDefined(); + }); + + it('FE-WSEVT-DAYNOTE-002: dayNote:created is idempotent — no duplicate if same ID', () => { + seedData(); + const duplicate = buildDayNote({ id: 1, day_id: 10, text: 'Duplicate' }); + useTripStore.getState().handleRemoteEvent({ type: 'dayNote:created', dayId: 10, note: duplicate }); + const { dayNotes } = useTripStore.getState(); + expect(dayNotes['10']).toHaveLength(1); + expect(dayNotes['10'][0].text).toBe('Original'); + }); + + it('FE-WSEVT-DAYNOTE-003: dayNote:updated replaces note in correct day', () => { + seedData(); + const updated = buildDayNote({ id: 1, day_id: 10, text: 'Updated text' }); + useTripStore.getState().handleRemoteEvent({ type: 'dayNote:updated', dayId: 10, note: updated }); + const { dayNotes } = useTripStore.getState(); + expect(dayNotes['10'][0].text).toBe('Updated text'); + }); + + it('FE-WSEVT-DAYNOTE-004: dayNote:deleted removes note from correct day', () => { + seedData(); + useTripStore.getState().handleRemoteEvent({ type: 'dayNote:deleted', dayId: 10, noteId: 1 }); + const { dayNotes } = useTripStore.getState(); + expect(dayNotes['10']).toHaveLength(0); + }); + + it('FE-WSEVT-DAYNOTE-005: operations on day 10 do not affect day 20', () => { + seedData(); + const newNote = buildDayNote({ id: 50, day_id: 10, text: 'Day 10 note' }); + useTripStore.getState().handleRemoteEvent({ type: 'dayNote:created', dayId: 10, note: newNote }); + const { dayNotes } = useTripStore.getState(); + expect(dayNotes['20']).toHaveLength(0); + }); +}); diff --git a/client/tests/unit/remoteEventHandler/days.test.ts b/client/tests/unit/remoteEventHandler/days.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..df2282b20a43b86f5629f6e4b2d21f5891774165 --- /dev/null +++ b/client/tests/unit/remoteEventHandler/days.test.ts @@ -0,0 +1,80 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import { useTripStore } from '../../../src/store/tripStore'; +import { resetAllStores } from '../../helpers/store'; +import { buildDay, buildAssignment, buildDayNote } from '../../helpers/factories'; + +beforeEach(() => { + resetAllStores(); +}); + +describe('remoteEventHandler > days', () => { + const seedData = () => { + useTripStore.setState({ + days: [buildDay({ id: 10 }), buildDay({ id: 20 })], + assignments: { + '10': [buildAssignment({ id: 100, day_id: 10 })], + '20': [], + }, + dayNotes: { + '10': [buildDayNote({ id: 1, day_id: 10 })], + '20': [], + }, + }); + }; + + it('FE-WSEVT-DAY-001: day:created adds day to days array', () => { + seedData(); + const newDay = buildDay({ id: 30 }); + useTripStore.getState().handleRemoteEvent({ type: 'day:created', day: newDay }); + const { days } = useTripStore.getState(); + expect(days).toHaveLength(3); + expect(days.find(d => d.id === 30)).toBeDefined(); + }); + + it('FE-WSEVT-DAY-002: day:created is idempotent — no duplicate if same ID', () => { + seedData(); + const duplicate = buildDay({ id: 10 }); + useTripStore.getState().handleRemoteEvent({ type: 'day:created', day: duplicate }); + const { days } = useTripStore.getState(); + expect(days).toHaveLength(2); + }); + + it('FE-WSEVT-DAY-003: day:updated replaces day in days array', () => { + seedData(); + const updated = buildDay({ id: 10, title: 'New Title' }); + useTripStore.getState().handleRemoteEvent({ type: 'day:updated', day: updated }); + const { days } = useTripStore.getState(); + const day10 = days.find(d => d.id === 10); + expect(day10?.title).toBe('New Title'); + }); + + it('FE-WSEVT-DAY-004: day:deleted removes day from days array', () => { + seedData(); + useTripStore.getState().handleRemoteEvent({ type: 'day:deleted', dayId: 10 }); + const { days } = useTripStore.getState(); + expect(days).toHaveLength(1); + expect(days.find(d => d.id === 10)).toBeUndefined(); + }); + + it('FE-WSEVT-DAY-005: day:deleted removes the assignments key for deleted day', () => { + seedData(); + useTripStore.getState().handleRemoteEvent({ type: 'day:deleted', dayId: 10 }); + const { assignments } = useTripStore.getState(); + expect('10' in assignments).toBe(false); + }); + + it('FE-WSEVT-DAY-006: day:deleted removes the dayNotes key for deleted day', () => { + seedData(); + useTripStore.getState().handleRemoteEvent({ type: 'day:deleted', dayId: 10 }); + const { dayNotes } = useTripStore.getState(); + expect('10' in dayNotes).toBe(false); + }); + + it('FE-WSEVT-DAY-007: day:deleted does not remove other days assignments/dayNotes', () => { + seedData(); + useTripStore.getState().handleRemoteEvent({ type: 'day:deleted', dayId: 10 }); + const { assignments, dayNotes } = useTripStore.getState(); + expect('20' in assignments).toBe(true); + expect('20' in dayNotes).toBe(true); + }); +}); diff --git a/client/tests/unit/remoteEventHandler/files.test.ts b/client/tests/unit/remoteEventHandler/files.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..5623b1a3cde633fd682271f46e90ad21c71bf933 --- /dev/null +++ b/client/tests/unit/remoteEventHandler/files.test.ts @@ -0,0 +1,61 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import { useTripStore } from '../../../src/store/tripStore'; +import { resetAllStores } from '../../helpers/store'; +import { buildTripFile } from '../../helpers/factories'; + +beforeEach(() => { + resetAllStores(); +}); + +describe('remoteEventHandler > files', () => { + const seedData = () => { + useTripStore.setState({ + files: [buildTripFile({ id: 1, original_name: 'document.pdf' })], + }); + }; + + it('FE-WSEVT-FILE-001: file:created prepends new file to array', () => { + seedData(); + const newFile = buildTripFile({ id: 99, original_name: 'photo.jpg' }); + useTripStore.getState().handleRemoteEvent({ type: 'file:created', file: newFile }); + const { files } = useTripStore.getState(); + expect(files).toHaveLength(2); + expect(files[0].id).toBe(99); // prepended + }); + + it('FE-WSEVT-FILE-002: file:created is idempotent — no duplicate if same ID', () => { + seedData(); + const duplicate = buildTripFile({ id: 1, original_name: 'document_dup.pdf' }); + useTripStore.getState().handleRemoteEvent({ type: 'file:created', file: duplicate }); + const { files } = useTripStore.getState(); + expect(files).toHaveLength(1); + expect(files[0].original_name).toBe('document.pdf'); + }); + + it('FE-WSEVT-FILE-003: file:updated replaces file in array', () => { + seedData(); + const updated = buildTripFile({ id: 1, original_name: 'renamed.pdf' }); + useTripStore.getState().handleRemoteEvent({ type: 'file:updated', file: updated }); + const { files } = useTripStore.getState(); + expect(files[0].original_name).toBe('renamed.pdf'); + }); + + it('FE-WSEVT-FILE-004: file:deleted removes file by ID', () => { + seedData(); + useTripStore.getState().handleRemoteEvent({ type: 'file:deleted', fileId: 1 }); + const { files } = useTripStore.getState(); + expect(files).toHaveLength(0); + }); + + it('FE-WSEVT-FILE-005: file:created ordering — newest is first', () => { + seedData(); + const f2 = buildTripFile({ id: 2, original_name: 'second.pdf' }); + const f3 = buildTripFile({ id: 3, original_name: 'third.pdf' }); + useTripStore.getState().handleRemoteEvent({ type: 'file:created', file: f2 }); + useTripStore.getState().handleRemoteEvent({ type: 'file:created', file: f3 }); + const { files } = useTripStore.getState(); + expect(files[0].id).toBe(3); + expect(files[1].id).toBe(2); + expect(files[2].id).toBe(1); + }); +}); diff --git a/client/tests/unit/remoteEventHandler/memories.test.ts b/client/tests/unit/remoteEventHandler/memories.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..62b4e0ba577f726a41df1aee8efe10babb9c18b5 --- /dev/null +++ b/client/tests/unit/remoteEventHandler/memories.test.ts @@ -0,0 +1,57 @@ +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { useTripStore } from '../../../src/store/tripStore'; +import { resetAllStores } from '../../helpers/store'; +import { buildPlace } from '../../helpers/factories'; + +beforeEach(() => { + resetAllStores(); +}); + +describe('remoteEventHandler > memories', () => { + it('FE-WSEVT-MEM-001: memories:updated dispatches CustomEvent on window', () => { + const received: Event[] = []; + const handler = (e: Event) => received.push(e); + window.addEventListener('memories:updated', handler); + useTripStore.getState().handleRemoteEvent({ type: 'memories:updated', photos: [] }); + window.removeEventListener('memories:updated', handler); + expect(received).toHaveLength(1); + }); + + it('FE-WSEVT-MEM-002: memories:updated event type is correct', () => { + const received: Event[] = []; + const handler = (e: Event) => received.push(e); + window.addEventListener('memories:updated', handler); + useTripStore.getState().handleRemoteEvent({ type: 'memories:updated', photos: [] }); + window.removeEventListener('memories:updated', handler); + expect(received[0].type).toBe('memories:updated'); + }); + + it('FE-WSEVT-MEM-003: memories:updated event detail contains the payload', () => { + const received: CustomEvent[] = []; + const handler = (e: Event) => received.push(e as CustomEvent); + window.addEventListener('memories:updated', handler); + const payload = { photos: [{ id: 1, url: '/photo.jpg' }] }; + useTripStore.getState().handleRemoteEvent({ type: 'memories:updated', ...payload }); + window.removeEventListener('memories:updated', handler); + expect(received[0].detail).toMatchObject(payload); + }); + + it('FE-WSEVT-MEM-004: memories:updated does not modify store state', () => { + const places = [buildPlace({ id: 42, name: 'Eiffel Tower' })]; + useTripStore.setState({ places }); + useTripStore.getState().handleRemoteEvent({ type: 'memories:updated', photos: [] }); + const { places: afterPlaces } = useTripStore.getState(); + expect(afterPlaces).toHaveLength(1); + expect(afterPlaces[0].id).toBe(42); + }); + + it('FE-WSEVT-MEM-005: memories:updated fires exactly once per event', () => { + const received: Event[] = []; + const handler = (e: Event) => received.push(e); + window.addEventListener('memories:updated', handler); + useTripStore.getState().handleRemoteEvent({ type: 'memories:updated', photos: [] }); + useTripStore.getState().handleRemoteEvent({ type: 'memories:updated', photos: [] }); + window.removeEventListener('memories:updated', handler); + expect(received).toHaveLength(2); + }); +}); diff --git a/client/tests/unit/remoteEventHandler/packing.test.ts b/client/tests/unit/remoteEventHandler/packing.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..0c578233a951b4b049b393620cc9d249fc3251dd --- /dev/null +++ b/client/tests/unit/remoteEventHandler/packing.test.ts @@ -0,0 +1,49 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import { useTripStore } from '../../../src/store/tripStore'; +import { resetAllStores } from '../../helpers/store'; +import { buildPackingItem } from '../../helpers/factories'; + +beforeEach(() => { + resetAllStores(); +}); + +describe('remoteEventHandler > packing', () => { + const seedData = () => { + useTripStore.setState({ + packingItems: [buildPackingItem({ id: 1, name: 'Sunscreen' })], + }); + }; + + it('FE-WSEVT-PACK-001: packing:created adds item to packingItems', () => { + seedData(); + const newItem = buildPackingItem({ id: 99, name: 'Hat' }); + useTripStore.getState().handleRemoteEvent({ type: 'packing:created', item: newItem }); + const { packingItems } = useTripStore.getState(); + expect(packingItems).toHaveLength(2); + expect(packingItems.find(i => i.id === 99)).toBeDefined(); + }); + + it('FE-WSEVT-PACK-002: packing:created is idempotent — no duplicate if same ID', () => { + seedData(); + const duplicate = buildPackingItem({ id: 1, name: 'Sunscreen Duplicate' }); + useTripStore.getState().handleRemoteEvent({ type: 'packing:created', item: duplicate }); + const { packingItems } = useTripStore.getState(); + expect(packingItems).toHaveLength(1); + expect(packingItems[0].name).toBe('Sunscreen'); + }); + + it('FE-WSEVT-PACK-003: packing:updated replaces item in array', () => { + seedData(); + const updated = buildPackingItem({ id: 1, name: 'SPF 50 Sunscreen' }); + useTripStore.getState().handleRemoteEvent({ type: 'packing:updated', item: updated }); + const { packingItems } = useTripStore.getState(); + expect(packingItems[0].name).toBe('SPF 50 Sunscreen'); + }); + + it('FE-WSEVT-PACK-004: packing:deleted removes item by ID', () => { + seedData(); + useTripStore.getState().handleRemoteEvent({ type: 'packing:deleted', itemId: 1 }); + const { packingItems } = useTripStore.getState(); + expect(packingItems).toHaveLength(0); + }); +}); diff --git a/client/tests/unit/remoteEventHandler/places.test.ts b/client/tests/unit/remoteEventHandler/places.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..8584f0d28573c54ded59d4869588efb2e44be329 --- /dev/null +++ b/client/tests/unit/remoteEventHandler/places.test.ts @@ -0,0 +1,67 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import { useTripStore } from '../../../src/store/tripStore'; +import { resetAllStores } from '../../helpers/store'; +import { buildPlace, buildAssignment } from '../../helpers/factories'; + +beforeEach(() => { + resetAllStores(); +}); + +describe('remoteEventHandler > places', () => { + const seedData = () => { + const place = buildPlace({ id: 1, name: 'Original' }); + const assignment = buildAssignment({ id: 100, place, day_id: 10 }); + useTripStore.setState({ + places: [place], + assignments: { '10': [assignment] }, + }); + }; + + it('FE-WSEVT-PLACE-001: place:created prepends new place to places array', () => { + seedData(); + const newPlace = buildPlace({ id: 99, name: 'New Place' }); + useTripStore.getState().handleRemoteEvent({ type: 'place:created', place: newPlace }); + const { places } = useTripStore.getState(); + expect(places[0].id).toBe(99); + expect(places).toHaveLength(2); + }); + + it('FE-WSEVT-PLACE-002: place:created is idempotent — no duplicate if same ID', () => { + seedData(); + const duplicate = buildPlace({ id: 1, name: 'Duplicate' }); + useTripStore.getState().handleRemoteEvent({ type: 'place:created', place: duplicate }); + const { places } = useTripStore.getState(); + expect(places).toHaveLength(1); + expect(places[0].name).toBe('Original'); + }); + + it('FE-WSEVT-PLACE-003: place:updated updates place in places array', () => { + seedData(); + const updated = buildPlace({ id: 1, name: 'Updated Name' }); + useTripStore.getState().handleRemoteEvent({ type: 'place:updated', place: updated }); + const { places } = useTripStore.getState(); + expect(places[0].name).toBe('Updated Name'); + }); + + it('FE-WSEVT-PLACE-004: place:updated cascades into assignments nested place', () => { + seedData(); + const updated = buildPlace({ id: 1, name: 'Cascaded Update' }); + useTripStore.getState().handleRemoteEvent({ type: 'place:updated', place: updated }); + const { assignments } = useTripStore.getState(); + expect(assignments['10'][0].place?.name).toBe('Cascaded Update'); + }); + + it('FE-WSEVT-PLACE-005: place:deleted removes place from places array', () => { + seedData(); + useTripStore.getState().handleRemoteEvent({ type: 'place:deleted', placeId: 1 }); + const { places } = useTripStore.getState(); + expect(places).toHaveLength(0); + }); + + it('FE-WSEVT-PLACE-006: place:deleted cascades — assignments referencing that place are removed', () => { + seedData(); + useTripStore.getState().handleRemoteEvent({ type: 'place:deleted', placeId: 1 }); + const { assignments } = useTripStore.getState(); + expect(assignments['10']).toHaveLength(0); + }); +}); diff --git a/client/tests/unit/remoteEventHandler/reservations.test.ts b/client/tests/unit/remoteEventHandler/reservations.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..df9cc80b313727cccd0010f00bfa0216d3676771 --- /dev/null +++ b/client/tests/unit/remoteEventHandler/reservations.test.ts @@ -0,0 +1,61 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import { useTripStore } from '../../../src/store/tripStore'; +import { resetAllStores } from '../../helpers/store'; +import { buildReservation } from '../../helpers/factories'; + +beforeEach(() => { + resetAllStores(); +}); + +describe('remoteEventHandler > reservations', () => { + const seedData = () => { + useTripStore.setState({ + reservations: [buildReservation({ id: 1, title: 'Hotel Paris' })], + }); + }; + + it('FE-WSEVT-RESERV-001: reservation:created prepends new reservation to array', () => { + seedData(); + const newRes = buildReservation({ id: 99, title: 'Flight' }); + useTripStore.getState().handleRemoteEvent({ type: 'reservation:created', reservation: newRes }); + const { reservations } = useTripStore.getState(); + expect(reservations).toHaveLength(2); + expect(reservations[0].id).toBe(99); // prepended, so first + }); + + it('FE-WSEVT-RESERV-002: reservation:created is idempotent — no duplicate if same ID', () => { + seedData(); + const duplicate = buildReservation({ id: 1, title: 'Hotel Paris Dup' }); + useTripStore.getState().handleRemoteEvent({ type: 'reservation:created', reservation: duplicate }); + const { reservations } = useTripStore.getState(); + expect(reservations).toHaveLength(1); + expect(reservations[0].title).toBe('Hotel Paris'); + }); + + it('FE-WSEVT-RESERV-003: reservation:updated replaces reservation in array', () => { + seedData(); + const updated = buildReservation({ id: 1, title: 'Hotel Lyon' }); + useTripStore.getState().handleRemoteEvent({ type: 'reservation:updated', reservation: updated }); + const { reservations } = useTripStore.getState(); + expect(reservations[0].title).toBe('Hotel Lyon'); + }); + + it('FE-WSEVT-RESERV-004: reservation:deleted removes reservation by ID', () => { + seedData(); + useTripStore.getState().handleRemoteEvent({ type: 'reservation:deleted', reservationId: 1 }); + const { reservations } = useTripStore.getState(); + expect(reservations).toHaveLength(0); + }); + + it('FE-WSEVT-RESERV-005: reservation:created ordering — newest is first', () => { + seedData(); + const r2 = buildReservation({ id: 2, title: 'Second' }); + const r3 = buildReservation({ id: 3, title: 'Third' }); + useTripStore.getState().handleRemoteEvent({ type: 'reservation:created', reservation: r2 }); + useTripStore.getState().handleRemoteEvent({ type: 'reservation:created', reservation: r3 }); + const { reservations } = useTripStore.getState(); + expect(reservations[0].id).toBe(3); + expect(reservations[1].id).toBe(2); + expect(reservations[2].id).toBe(1); + }); +}); diff --git a/client/tests/unit/remoteEventHandler/todo.test.ts b/client/tests/unit/remoteEventHandler/todo.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..1c5c2a688025385cffe5cb19150971b920d24b32 --- /dev/null +++ b/client/tests/unit/remoteEventHandler/todo.test.ts @@ -0,0 +1,49 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import { useTripStore } from '../../../src/store/tripStore'; +import { resetAllStores } from '../../helpers/store'; +import { buildTodoItem } from '../../helpers/factories'; + +beforeEach(() => { + resetAllStores(); +}); + +describe('remoteEventHandler > todo', () => { + const seedData = () => { + useTripStore.setState({ + todoItems: [buildTodoItem({ id: 1, name: 'Book flights' })], + }); + }; + + it('FE-WSEVT-TODO-001: todo:created adds item to todoItems', () => { + seedData(); + const newItem = buildTodoItem({ id: 99, name: 'Pack bags' }); + useTripStore.getState().handleRemoteEvent({ type: 'todo:created', item: newItem }); + const { todoItems } = useTripStore.getState(); + expect(todoItems).toHaveLength(2); + expect(todoItems.find(i => i.id === 99)).toBeDefined(); + }); + + it('FE-WSEVT-TODO-002: todo:created is idempotent — no duplicate if same ID', () => { + seedData(); + const duplicate = buildTodoItem({ id: 1, name: 'Book flights duplicate' }); + useTripStore.getState().handleRemoteEvent({ type: 'todo:created', item: duplicate }); + const { todoItems } = useTripStore.getState(); + expect(todoItems).toHaveLength(1); + expect(todoItems[0].name).toBe('Book flights'); + }); + + it('FE-WSEVT-TODO-003: todo:updated replaces item in array', () => { + seedData(); + const updated = buildTodoItem({ id: 1, name: 'Book round-trip flights' }); + useTripStore.getState().handleRemoteEvent({ type: 'todo:updated', item: updated }); + const { todoItems } = useTripStore.getState(); + expect(todoItems[0].name).toBe('Book round-trip flights'); + }); + + it('FE-WSEVT-TODO-004: todo:deleted removes item by ID', () => { + seedData(); + useTripStore.getState().handleRemoteEvent({ type: 'todo:deleted', itemId: 1 }); + const { todoItems } = useTripStore.getState(); + expect(todoItems).toHaveLength(0); + }); +}); diff --git a/client/tests/unit/remoteEventHandler/trip.test.ts b/client/tests/unit/remoteEventHandler/trip.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..992c260b521959cd7b2fb7cbca25d9cd2ea26698 --- /dev/null +++ b/client/tests/unit/remoteEventHandler/trip.test.ts @@ -0,0 +1,32 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import { useTripStore } from '../../../src/store/tripStore'; +import { resetAllStores } from '../../helpers/store'; +import { buildTrip, buildPlace } from '../../helpers/factories'; + +beforeEach(() => { + resetAllStores(); +}); + +describe('remoteEventHandler > trip', () => { + it('FE-WSEVT-TRIP-001: trip:updated replaces trip in state', () => { + const originalTrip = buildTrip({ id: 1, title: 'Paris Trip' }); + useTripStore.setState({ trip: originalTrip }); + const updatedTrip = buildTrip({ id: 1, title: 'Paris & Lyon Trip' }); + useTripStore.getState().handleRemoteEvent({ type: 'trip:updated', trip: updatedTrip }); + const { trip } = useTripStore.getState(); + expect(trip?.title).toBe('Paris & Lyon Trip'); + }); + + it('FE-WSEVT-TRIP-002: trip:updated does not affect other state fields', () => { + const existingPlace = buildPlace({ id: 55, name: 'Eiffel Tower' }); + useTripStore.setState({ + trip: buildTrip({ id: 1, title: 'Original' }), + places: [existingPlace], + }); + const updatedTrip = buildTrip({ id: 1, title: 'Updated' }); + useTripStore.getState().handleRemoteEvent({ type: 'trip:updated', trip: updatedTrip }); + const { places } = useTripStore.getState(); + expect(places).toHaveLength(1); + expect(places[0].id).toBe(55); + }); +}); diff --git a/client/tests/unit/repo/packingRepo.test.ts b/client/tests/unit/repo/packingRepo.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..4c25ada2486ac1b57b20f4d60469b1f64bdac95b --- /dev/null +++ b/client/tests/unit/repo/packingRepo.test.ts @@ -0,0 +1,119 @@ +/** + * packingRepo unit tests. + * + * Online path: calls REST via MSW, writes result to Dexie. + * Offline path: returns Dexie cache, skips REST. + */ +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; +import 'fake-indexeddb/auto'; +import { server } from '../../helpers/msw/server'; +import { http, HttpResponse } from 'msw'; +import { packingRepo } from '../../../src/repo/packingRepo'; +import { offlineDb, clearAll } from '../../../src/db/offlineDb'; +import { buildPackingItem } from '../../helpers/factories'; + +beforeEach(async () => { + await clearAll(); + Object.defineProperty(navigator, 'onLine', { value: true, writable: true, configurable: true }); +}); + +afterEach(() => { + vi.restoreAllMocks(); +}); + +describe('packingRepo.list', () => { + it('online — fetches from REST and caches in Dexie', async () => { + const item = buildPackingItem({ trip_id: 1 }); + server.use( + http.get('/api/trips/1/packing', () => HttpResponse.json({ items: [item] })), + ); + + const result = await packingRepo.list(1); + expect(result.items).toHaveLength(1); + expect(result.items[0].id).toBe(item.id); + + await new Promise(r => setTimeout(r, 0)); + const cached = await offlineDb.packingItems.where('trip_id').equals(1).toArray(); + expect(cached).toHaveLength(1); + expect(cached[0].id).toBe(item.id); + }); + + it('offline — returns Dexie cache without REST call', async () => { + Object.defineProperty(navigator, 'onLine', { value: false }); + + const item = buildPackingItem({ trip_id: 1 }); + await offlineDb.packingItems.put(item); + + let restCalled = false; + server.use( + http.get('/api/trips/1/packing', () => { + restCalled = true; + return HttpResponse.json({ items: [] }); + }), + ); + + const result = await packingRepo.list(1); + expect(result.items).toHaveLength(1); + expect(result.items[0].id).toBe(item.id); + expect(restCalled).toBe(false); + }); + + it('offline — returns empty array when nothing cached', async () => { + Object.defineProperty(navigator, 'onLine', { value: false }); + const result = await packingRepo.list(99); + expect(result.items).toHaveLength(0); + }); +}); + +describe('packingRepo.create', () => { + it('calls REST and caches created item in Dexie', async () => { + const item = buildPackingItem({ trip_id: 1, name: 'Sunscreen' }); + server.use( + http.post('/api/trips/1/packing', () => HttpResponse.json({ item })), + ); + + const result = await packingRepo.create(1, { name: 'Sunscreen' }); + expect(result.item.name).toBe('Sunscreen'); + + await new Promise(r => setTimeout(r, 0)); + const cached = await offlineDb.packingItems.get(item.id); + expect(cached).toBeDefined(); + expect(cached!.name).toBe('Sunscreen'); + }); +}); + +describe('packingRepo.update', () => { + it('calls REST and updates Dexie cache', async () => { + const original = buildPackingItem({ trip_id: 1, name: 'Jacket', checked: 0 }); + await offlineDb.packingItems.put(original); + + const updated = { ...original, checked: 1 }; + server.use( + http.put(`/api/trips/1/packing/${original.id}`, () => HttpResponse.json({ item: updated })), + ); + + const result = await packingRepo.update(1, original.id, { checked: true }); + expect(result.item.checked).toBe(1); + + await new Promise(r => setTimeout(r, 0)); + const cached = await offlineDb.packingItems.get(original.id); + expect(cached!.checked).toBe(1); + }); +}); + +describe('packingRepo.delete', () => { + it('calls REST and removes from Dexie', async () => { + const item = buildPackingItem({ trip_id: 1 }); + await offlineDb.packingItems.put(item); + + server.use( + http.delete(`/api/trips/1/packing/${item.id}`, () => HttpResponse.json({ success: true })), + ); + + await packingRepo.delete(1, item.id); + + await new Promise(r => setTimeout(r, 0)); + const cached = await offlineDb.packingItems.get(item.id); + expect(cached).toBeUndefined(); + }); +}); diff --git a/client/tests/unit/repo/placeRepo.test.ts b/client/tests/unit/repo/placeRepo.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..4724ca56f839c66dea69785cf06128e65b8e6bc0 --- /dev/null +++ b/client/tests/unit/repo/placeRepo.test.ts @@ -0,0 +1,134 @@ +/** + * placeRepo unit tests. + * + * Online path: calls REST via MSW, writes result to Dexie. + * Offline path: returns Dexie cache, skips REST. + */ +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; +import 'fake-indexeddb/auto'; +import { server } from '../../helpers/msw/server'; +import { http, HttpResponse } from 'msw'; +import { placeRepo } from '../../../src/repo/placeRepo'; +import { offlineDb, clearAll } from '../../../src/db/offlineDb'; +import { buildPlace } from '../../helpers/factories'; + +beforeEach(async () => { + await clearAll(); + Object.defineProperty(navigator, 'onLine', { value: true, writable: true, configurable: true }); +}); + +afterEach(() => { + vi.restoreAllMocks(); +}); + +describe('placeRepo.list', () => { + it('online — fetches from REST and caches in Dexie', async () => { + const place = buildPlace({ trip_id: 1 }); + server.use( + http.get('/api/trips/1/places', () => HttpResponse.json({ places: [place] })), + ); + + const result = await placeRepo.list(1); + expect(result.places).toHaveLength(1); + expect(result.places[0].id).toBe(place.id); + + // Give fire-and-forget a tick to flush + await new Promise(r => setTimeout(r, 0)); + const cached = await offlineDb.places.where('trip_id').equals(1).toArray(); + expect(cached).toHaveLength(1); + expect(cached[0].id).toBe(place.id); + }); + + it('offline — returns Dexie cache without REST call', async () => { + Object.defineProperty(navigator, 'onLine', { value: false }); + + const place = buildPlace({ trip_id: 1 }); + await offlineDb.places.put(place); + + let restCalled = false; + server.use( + http.get('/api/trips/1/places', () => { + restCalled = true; + return HttpResponse.json({ places: [] }); + }), + ); + + const result = await placeRepo.list(1); + expect(result.places).toHaveLength(1); + expect(result.places[0].id).toBe(place.id); + expect(restCalled).toBe(false); + }); + + it('offline — returns empty array when nothing cached', async () => { + Object.defineProperty(navigator, 'onLine', { value: false }); + const result = await placeRepo.list(99); + expect(result.places).toHaveLength(0); + }); + + it('online but request fails — falls back to Dexie cache (captive portal)', async () => { + // navigator.onLine lies "true" on a captive portal; the request throws. + const place = buildPlace({ trip_id: 1 }); + await offlineDb.places.put(place); + + server.use( + http.get('/api/trips/1/places', () => HttpResponse.error()), + ); + + const result = await placeRepo.list(1); + expect(result.places).toHaveLength(1); + expect(result.places[0].id).toBe(place.id); + }); +}); + +describe('placeRepo.create', () => { + it('calls REST and caches created place in Dexie', async () => { + const place = buildPlace({ trip_id: 1, name: 'Eiffel Tower' }); + server.use( + http.post('/api/trips/1/places', () => HttpResponse.json({ place })), + ); + + const result = await placeRepo.create(1, { name: 'Eiffel Tower' }); + expect(result.place.name).toBe('Eiffel Tower'); + + await new Promise(r => setTimeout(r, 0)); + const cached = await offlineDb.places.get(place.id); + expect(cached).toBeDefined(); + expect(cached!.name).toBe('Eiffel Tower'); + }); +}); + +describe('placeRepo.update', () => { + it('calls REST and updates Dexie cache', async () => { + const original = buildPlace({ trip_id: 1, name: 'Old Name' }); + await offlineDb.places.put(original); + + const updated = { ...original, name: 'New Name' }; + server.use( + http.put(`/api/trips/1/places/${original.id}`, () => HttpResponse.json({ place: updated })), + ); + + const result = await placeRepo.update(1, original.id, { name: 'New Name' }); + expect(result.place.name).toBe('New Name'); + + await new Promise(r => setTimeout(r, 0)); + const cached = await offlineDb.places.get(original.id); + expect(cached!.name).toBe('New Name'); + }); +}); + +describe('placeRepo.delete', () => { + it('calls REST and removes from Dexie', async () => { + const place = buildPlace({ trip_id: 1 }); + await offlineDb.places.put(place); + + server.use( + http.delete(`/api/trips/1/places/${place.id}`, () => HttpResponse.json({ success: true })), + ); + + await placeRepo.delete(1, place.id); + + await new Promise(r => setTimeout(r, 0)); + const cached = await offlineDb.places.get(place.id); + expect(cached).toBeUndefined(); + }); +}); diff --git a/client/tests/unit/repo/withOfflineFallback.test.ts b/client/tests/unit/repo/withOfflineFallback.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..6a679df2c2673e1a5570b80e809aed2ea83c0109 --- /dev/null +++ b/client/tests/unit/repo/withOfflineFallback.test.ts @@ -0,0 +1,76 @@ +/** + * onlineThenCache — the read-through fallback shared by every repo (H2). + * + * Branches: + * - navigator offline → cache only (skip the request) + * - online but the request fails at the network level → fall back to cache + * - online but the server returns an HTTP error → rethrow (don't mask) + * - online and the request succeeds → return it, skip cache + */ +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; +import { onlineThenCache } from '../../../src/repo/withOfflineFallback'; + +beforeEach(() => { + Object.defineProperty(navigator, 'onLine', { value: true, writable: true, configurable: true }); +}); + +afterEach(() => { + vi.restoreAllMocks(); +}); + +describe('onlineThenCache', () => { + it('returns the online result when online', async () => { + const online = vi.fn().mockResolvedValue('online'); + const cache = vi.fn().mockResolvedValue('cache'); + + expect(await onlineThenCache(online, cache)).toBe('online'); + expect(online).toHaveBeenCalledOnce(); + expect(cache).not.toHaveBeenCalled(); + }); + + it('reads the cache without calling online when navigator is offline', async () => { + Object.defineProperty(navigator, 'onLine', { value: false }); + const online = vi.fn().mockResolvedValue('online'); + const cache = vi.fn().mockResolvedValue('cache'); + + expect(await onlineThenCache(online, cache)).toBe('cache'); + expect(online).not.toHaveBeenCalled(); + }); + + it('falls back to the cache on a network-level failure (no HTTP response)', async () => { + // Axios network error: the request never reached the server (captive portal). + const netErr = Object.assign(new Error('Network Error'), { isAxiosError: true, response: undefined }); + const online = vi.fn().mockRejectedValue(netErr); + const cache = vi.fn().mockResolvedValue('cache'); + + expect(await onlineThenCache(online, cache)).toBe('cache'); + expect(online).toHaveBeenCalledOnce(); + expect(cache).toHaveBeenCalledOnce(); + }); + + it('rethrows a genuine HTTP error (server responded) instead of masking it', async () => { + // 404/403/500 mean the server replied — callers must see it, not a stale cache. + const httpErr = Object.assign(new Error('Not Found'), { isAxiosError: true, response: { status: 404 } }); + const online = vi.fn().mockRejectedValue(httpErr); + const cache = vi.fn().mockResolvedValue('cache'); + + await expect(onlineThenCache(online, cache)).rejects.toThrow('Not Found'); + expect(cache).not.toHaveBeenCalled(); + }); + + it('rethrows a non-Axios error rather than swallowing it', async () => { + const online = vi.fn().mockRejectedValue(new Error('bug')); + const cache = vi.fn().mockResolvedValue('cache'); + + await expect(onlineThenCache(online, cache)).rejects.toThrow('bug'); + expect(cache).not.toHaveBeenCalled(); + }); + + it('propagates a cache error (e.g. nothing cached) when online also failed', async () => { + Object.defineProperty(navigator, 'onLine', { value: false }); + const online = vi.fn().mockResolvedValue('online'); + const cache = vi.fn().mockRejectedValue(new Error('No cached data')); + + await expect(onlineThenCache(online, cache)).rejects.toThrow('No cached data'); + }); +}); diff --git a/client/tests/unit/services/photoService.test.ts b/client/tests/unit/services/photoService.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..953078cf776dc9b2231bc1b8c06a78e58fc05e6e --- /dev/null +++ b/client/tests/unit/services/photoService.test.ts @@ -0,0 +1,343 @@ +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; + +// Module-level types for dynamic imports +type PhotoServiceModule = typeof import('../../../src/services/photoService'); +type ApiClientModule = typeof import('../../../src/api/client'); + +let svc: PhotoServiceModule; +let mockPlacePhoto: ReturnType; + +// ── Canvas mock helpers ──────────────────────────────────────────────────────── + +function setupCanvasMock(dataUrl = 'data:image/webp;base64,mock') { + vi.spyOn(HTMLCanvasElement.prototype, 'getContext').mockReturnValue({ + beginPath: vi.fn(), + arc: vi.fn(), + clip: vi.fn(), + drawImage: vi.fn(), + } as unknown as CanvasRenderingContext2D); + vi.spyOn(HTMLCanvasElement.prototype, 'toDataURL').mockReturnValue(dataUrl); +} + +// ── Image src interceptor ────────────────────────────────────────────────────── +// jsdom doesn't load images; we override the src setter so onload/onerror fire. + +function setupImageAutoLoad(succeed = true) { + Object.defineProperty(HTMLImageElement.prototype, 'src', { + configurable: true, + set(url: string) { + (this as HTMLImageElement & { _src: string })._src = url; + // Fire asynchronously so assignment completes before handler runs + Promise.resolve().then(() => { + if (succeed && typeof this.onload === 'function') { + this.onload(new Event('load')); + } else if (!succeed && typeof this.onerror === 'function') { + this.onerror(new Event('error')); + } + }); + }, + get() { + return (this as HTMLImageElement & { _src: string })._src ?? ''; + }, + }); +} + +function restoreImageSrc() { + // Remove override — jsdom's descriptor is on the prototype, restoring + // configurable property to original (no-op src) is sufficient for test isolation. + Object.defineProperty(HTMLImageElement.prototype, 'src', { + configurable: true, + set(_url: string) {}, + get() { return ''; }, + }); +} + +// ── Module reset helpers ─────────────────────────────────────────────────────── + +async function freshImports() { + vi.resetModules(); + vi.doMock('../../../src/api/client', () => ({ + mapsApi: { placePhoto: vi.fn() }, + })); + svc = await import('../../../src/services/photoService'); + const apiClient = await import('../../../src/api/client') as ApiClientModule; + mockPlacePhoto = vi.mocked(apiClient.mapsApi.placePhoto); +} + +// ── Flush all pending microtasks + macrotasks ────────────────────────────────── +const flush = () => new Promise(r => setTimeout(r, 0)); + +// ============================================================================== + +beforeEach(async () => { + await freshImports(); + setupCanvasMock(); + setupImageAutoLoad(true); // default: image loads succeed so urlToBase64 resolves and .finally() runs +}); + +afterEach(() => { + vi.restoreAllMocks(); + restoreImageSrc(); + vi.clearAllMocks(); +}); + +// ============================================================================== +// getCached / isLoading +// ============================================================================== + +describe('getCached', () => { + it('FE-COMP-PHOTO-001: returns undefined for an unknown key', () => { + expect(svc.getCached('missing')).toBeUndefined(); + }); +}); + +describe('isLoading', () => { + it('FE-COMP-PHOTO-002: returns false before any fetch', () => { + expect(svc.isLoading('key')).toBe(false); + }); +}); + +// ============================================================================== +// fetchPhoto — cache hit +// ============================================================================== + +describe('fetchPhoto — cache hit', () => { + it('FE-COMP-PHOTO-003: callback fires immediately on second call; API called only once', async () => { + mockPlacePhoto.mockResolvedValue({ photoUrl: 'https://example.com/photo.jpg' }); + + const cb1 = vi.fn(); + svc.fetchPhoto('k', 'pid', undefined, undefined, undefined, cb1); + await flush(); + + expect(mockPlacePhoto).toHaveBeenCalledTimes(1); + expect(cb1).toHaveBeenCalledWith(expect.objectContaining({ photoUrl: 'https://example.com/photo.jpg' })); + + const cb2 = vi.fn(); + svc.fetchPhoto('k', 'pid', undefined, undefined, undefined, cb2); + // Cache hit → synchronous call, no additional API request + expect(cb2).toHaveBeenCalledWith(expect.objectContaining({ photoUrl: 'https://example.com/photo.jpg' })); + expect(mockPlacePhoto).toHaveBeenCalledTimes(1); + }); +}); + +// ============================================================================== +// fetchPhoto — in-flight deduplication +// ============================================================================== + +describe('fetchPhoto — in-flight deduplication', () => { + it('FE-COMP-PHOTO-004: concurrent calls make only one API request; both callbacks receive result', async () => { + let resolve!: (v: { photoUrl: string }) => void; + mockPlacePhoto.mockReturnValue(new Promise<{ photoUrl: string }>(r => { resolve = r; })); + + const cb1 = vi.fn(); + const cb2 = vi.fn(); + svc.fetchPhoto('k', 'pid', undefined, undefined, undefined, cb1); + svc.fetchPhoto('k', 'pid', undefined, undefined, undefined, cb2); + + // acquireRequestSlot() is async (Promise.resolve), so flush microtasks before asserting + await flush(); + expect(mockPlacePhoto).toHaveBeenCalledTimes(1); + + resolve({ photoUrl: 'https://example.com/photo.jpg' }); + await flush(); + + expect(cb1).toHaveBeenCalledWith(expect.objectContaining({ photoUrl: 'https://example.com/photo.jpg' })); + expect(cb2).toHaveBeenCalledWith(expect.objectContaining({ photoUrl: 'https://example.com/photo.jpg' })); + }); +}); + +// ============================================================================== +// fetchPhoto — photoUrl present +// ============================================================================== + +describe('fetchPhoto — photoUrl present', () => { + it('FE-COMP-PHOTO-005: callback receives entry with photoUrl set and thumbDataUrl null at call time', async () => { + mockPlacePhoto.mockResolvedValue({ photoUrl: 'https://example.com/photo.jpg' }); + + // Capture a shallow clone at the moment of the call, before the entry is mutated by thumb generation + const snapshots: { photoUrl: string | null; thumbDataUrl: string | null }[] = []; + const cb = vi.fn((entry: { photoUrl: string | null; thumbDataUrl: string | null }) => { + snapshots.push({ ...entry }); + }); + + svc.fetchPhoto('k', 'pid', undefined, undefined, undefined, cb); + await flush(); + + expect(cb).toHaveBeenCalledTimes(1); + expect(snapshots[0]).toEqual({ photoUrl: 'https://example.com/photo.jpg', thumbDataUrl: null }); + }); + + it('FE-COMP-PHOTO-006: getCached returns the entry after fetch resolves', async () => { + mockPlacePhoto.mockResolvedValue({ photoUrl: 'https://example.com/photo.jpg' }); + + svc.fetchPhoto('k', 'pid'); + await flush(); + + const entry = svc.getCached('k'); + expect(entry).toBeDefined(); + expect(entry!.photoUrl).toBe('https://example.com/photo.jpg'); + }); + + it('FE-COMP-PHOTO-007: isLoading returns false after fetch completes', async () => { + mockPlacePhoto.mockResolvedValue({ photoUrl: 'https://example.com/photo.jpg' }); + + svc.fetchPhoto('k', 'pid'); + await flush(); + + expect(svc.isLoading('k')).toBe(false); + }); +}); + +// ============================================================================== +// fetchPhoto — photoUrl null +// ============================================================================== + +describe('fetchPhoto — photoUrl null', () => { + it('FE-COMP-PHOTO-008: callback receives null entry when API returns no photoUrl', async () => { + mockPlacePhoto.mockResolvedValue({}); + + const cb = vi.fn(); + svc.fetchPhoto('k', 'pid', undefined, undefined, undefined, cb); + await flush(); + + expect(cb).toHaveBeenCalledWith({ photoUrl: null, thumbDataUrl: null }); + expect(svc.getCached('k')).toEqual({ photoUrl: null, thumbDataUrl: null }); + }); +}); + +// ============================================================================== +// fetchPhoto — API error +// ============================================================================== + +describe('fetchPhoto — API error', () => { + it('FE-COMP-PHOTO-009: callback receives null entry on API rejection', async () => { + mockPlacePhoto.mockRejectedValue(new Error('Network error')); + + const cb = vi.fn(); + svc.fetchPhoto('k', 'pid', undefined, undefined, undefined, cb); + await flush(); + + expect(cb).toHaveBeenCalledWith({ photoUrl: null, thumbDataUrl: null }); + expect(svc.getCached('k')).toEqual({ photoUrl: null, thumbDataUrl: null }); + }); +}); + +// ============================================================================== +// onPhotoLoaded +// ============================================================================== + +describe('onPhotoLoaded', () => { + it('FE-COMP-PHOTO-010: listener fires once when photo is fetched', async () => { + mockPlacePhoto.mockResolvedValue({ photoUrl: 'https://example.com/photo.jpg' }); + + const fn = vi.fn(); + svc.onPhotoLoaded('k', fn); + svc.fetchPhoto('k', 'pid'); + await flush(); + + expect(fn).toHaveBeenCalledTimes(1); + expect(fn).toHaveBeenCalledWith(expect.objectContaining({ photoUrl: 'https://example.com/photo.jpg' })); + }); + + it('FE-COMP-PHOTO-011: unsubscribe prevents callback from being called', async () => { + mockPlacePhoto.mockResolvedValue({ photoUrl: 'https://example.com/photo.jpg' }); + + const fn = vi.fn(); + const unsub = svc.onPhotoLoaded('k', fn); + unsub(); + svc.fetchPhoto('k', 'pid'); + await flush(); + + expect(fn).not.toHaveBeenCalled(); + }); +}); + +// ============================================================================== +// onThumbReady +// ============================================================================== + +describe('onThumbReady', () => { + it('FE-COMP-PHOTO-012: fires when urlToBase64 produces a thumb', async () => { + mockPlacePhoto.mockResolvedValue({ photoUrl: 'https://example.com/img.jpg' }); + setupImageAutoLoad(true); // trigger img.onload → canvas path runs + vi.spyOn(HTMLCanvasElement.prototype, 'toDataURL').mockReturnValue('data:image/webp;base64,thumb'); + + const fn = vi.fn(); + svc.onThumbReady('k', fn); + svc.fetchPhoto('k', 'pid'); + + // flush microtasks + macrotasks to let urlToBase64 complete + await flush(); + await flush(); + + expect(fn).toHaveBeenCalledWith('data:image/webp;base64,thumb'); + expect(svc.getCached('k')?.thumbDataUrl).toBe('data:image/webp;base64,thumb'); + }); + + it('FE-COMP-PHOTO-013: unsubscribe prevents thumb callback', async () => { + mockPlacePhoto.mockResolvedValue({ photoUrl: 'https://example.com/img.jpg' }); + setupImageAutoLoad(true); + + const fn = vi.fn(); + const unsub = svc.onThumbReady('k', fn); + unsub(); + svc.fetchPhoto('k', 'pid'); + + await flush(); + await flush(); + + expect(fn).not.toHaveBeenCalled(); + }); +}); + +// ============================================================================== +// urlToBase64 +// ============================================================================== + +describe('urlToBase64', () => { + it('FE-COMP-PHOTO-014: returns null when image fails to load', async () => { + setupImageAutoLoad(false); // triggers onerror + const result = await svc.urlToBase64('https://bad-url.jpg'); + expect(result).toBeNull(); + }); + + it('FE-COMP-PHOTO-015: returns a data URL string on successful load', async () => { + setupImageAutoLoad(true); + vi.spyOn(HTMLCanvasElement.prototype, 'toDataURL').mockReturnValue('data:image/webp;base64,abc123'); + + const result = await svc.urlToBase64('https://example.com/img.jpg', 48); + expect(result).toBe('data:image/webp;base64,abc123'); + }); + + it('FE-COMP-PHOTO-016: canvas clip/draw path does not throw', async () => { + setupImageAutoLoad(true); + await expect(svc.urlToBase64('https://example.com/img.jpg')).resolves.not.toThrow(); + }); +}); + +// ============================================================================== +// getAllThumbs +// ============================================================================== + +describe('getAllThumbs', () => { + it('FE-COMP-PHOTO-017: returns only entries with a non-null thumbDataUrl', async () => { + // key1: photo with thumb + mockPlacePhoto.mockResolvedValueOnce({ photoUrl: 'https://example.com/img1.jpg' }); + // key2: no photo, no thumb + mockPlacePhoto.mockResolvedValueOnce({}); + + setupImageAutoLoad(true); + vi.spyOn(HTMLCanvasElement.prototype, 'toDataURL').mockReturnValue('data:image/webp;base64,thumb1'); + + svc.fetchPhoto('key1', 'pid1'); + svc.fetchPhoto('key2', 'pid2'); + + await flush(); + await flush(); + + const thumbs = svc.getAllThumbs(); + expect(Object.keys(thumbs)).toContain('key1'); + expect(thumbs['key1']).toBe('data:image/webp;base64,thumb1'); + expect(Object.keys(thumbs)).not.toContain('key2'); + }); +}); diff --git a/client/tests/unit/shared-contract.test.ts b/client/tests/unit/shared-contract.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..e82f5dd96667594b8dcf92fe08195de78031e6f5 --- /dev/null +++ b/client/tests/unit/shared-contract.test.ts @@ -0,0 +1,10 @@ +import { describe, it, expect } from 'vitest'; +// Smoke test: proves the client toolchain (vite / vitest) resolves @trek/shared. +import { idParamSchema, paginationQuerySchema } from '@trek/shared'; + +describe('@trek/shared resolves in the client toolchain', () => { + it('imports and uses a shared schema', () => { + expect(idParamSchema.parse('7')).toBe(7); + expect(paginationQuerySchema.parse({})).toEqual({ page: 1, perPage: 50 }); + }); +}); diff --git a/client/tests/unit/slices/assignmentsSlice.test.ts b/client/tests/unit/slices/assignmentsSlice.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..2978581884bd4daee8226843f645b67bd61bdb59 --- /dev/null +++ b/client/tests/unit/slices/assignmentsSlice.test.ts @@ -0,0 +1,222 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { http, HttpResponse } from 'msw'; +import { useTripStore } from '../../../src/store/tripStore'; +import { resetAllStores, seedStore } from '../../helpers/store'; +import { buildPlace, buildAssignment } from '../../helpers/factories'; +import { server } from '../../helpers/msw/server'; + +vi.mock('../../../src/api/websocket', () => ({ + connect: vi.fn(), + disconnect: vi.fn(), + getSocketId: vi.fn(() => null), + joinTrip: vi.fn(), + leaveTrip: vi.fn(), + addListener: vi.fn(), + removeListener: vi.fn(), + setRefetchCallback: vi.fn(), + setPreReconnectHook: vi.fn(), +})); + +beforeEach(() => { + resetAllStores(); +}); + +describe('assignmentsSlice', () => { + describe('assignPlaceToDay', () => { + it('FE-ASSIGN-001: assignPlaceToDay adds optimistic temp ID (negative) immediately', async () => { + const place = buildPlace({ id: 10, trip_id: 1 }); + seedStore(useTripStore, { + places: [place], + assignments: { '1': [] }, + }); + + // Don't await — check state mid-flight + let tempAdded = false; + server.use( + http.post('/api/trips/1/days/1/assignments', async () => { + const state = useTripStore.getState(); + const dayAssignments = state.assignments['1']; + if (dayAssignments.some(a => a.id < 0)) { + tempAdded = true; + } + const result = buildAssignment({ day_id: 1, place_id: 10, place }); + return HttpResponse.json({ assignment: result }); + }), + ); + + await useTripStore.getState().assignPlaceToDay(1, 1, 10); + expect(tempAdded).toBe(true); + }); + + it('FE-ASSIGN-002: after API success, temp ID is replaced with real assignment', async () => { + const place = buildPlace({ id: 10, trip_id: 1 }); + seedStore(useTripStore, { + places: [place], + assignments: { '1': [] }, + }); + + const realAssignment = buildAssignment({ id: 999, day_id: 1, place_id: 10, place }); + server.use( + http.post('/api/trips/1/days/1/assignments', () => + HttpResponse.json({ assignment: realAssignment }) + ), + ); + + await useTripStore.getState().assignPlaceToDay(1, 1, 10); + + const dayAssignments = useTripStore.getState().assignments['1']; + expect(dayAssignments).toHaveLength(1); + expect(dayAssignments[0].id).toBe(999); + expect(dayAssignments.every(a => a.id > 0)).toBe(true); + }); + + it('FE-ASSIGN-003: on API failure, temp assignment is removed (rollback)', async () => { + const place = buildPlace({ id: 10, trip_id: 1 }); + seedStore(useTripStore, { + places: [place], + assignments: { '1': [] }, + }); + + server.use( + http.post('/api/trips/1/days/1/assignments', () => + HttpResponse.json({ message: 'Error' }, { status: 500 }) + ), + ); + + await expect(useTripStore.getState().assignPlaceToDay(1, 1, 10)).rejects.toThrow(); + + const dayAssignments = useTripStore.getState().assignments['1']; + expect(dayAssignments).toHaveLength(0); + }); + + it('FE-ASSIGN-001b: returns undefined if place not found in store', async () => { + seedStore(useTripStore, { + places: [], // no places seeded + assignments: { '1': [] }, + }); + + const result = await useTripStore.getState().assignPlaceToDay(1, 1, 999); + expect(result).toBeUndefined(); + }); + }); + + describe('removeAssignment', () => { + it('FE-ASSIGN-004: removeAssignment is optimistically removed, re-added on failure', async () => { + const place = buildPlace({ id: 10, trip_id: 1 }); + const assignment = buildAssignment({ id: 100, day_id: 1, place }); + seedStore(useTripStore, { + assignments: { '1': [assignment] }, + }); + + server.use( + http.delete('/api/trips/1/days/1/assignments/100', () => + HttpResponse.json({ message: 'Error' }, { status: 500 }) + ), + ); + + await expect(useTripStore.getState().removeAssignment(1, 1, 100)).rejects.toThrow(); + + // Should be rolled back + const dayAssignments = useTripStore.getState().assignments['1']; + expect(dayAssignments).toHaveLength(1); + expect(dayAssignments[0].id).toBe(100); + }); + + it('FE-ASSIGN-004b: removeAssignment success removes from store', async () => { + const place = buildPlace({ id: 10, trip_id: 1 }); + const assignment = buildAssignment({ id: 100, day_id: 1, place }); + seedStore(useTripStore, { + assignments: { '1': [assignment] }, + }); + + await useTripStore.getState().removeAssignment(1, 1, 100); + + expect(useTripStore.getState().assignments['1']).toHaveLength(0); + }); + }); + + describe('reorderAssignments', () => { + it('FE-ASSIGN-005: reorderAssignments updates order_index of assignments', async () => { + const place1 = buildPlace({ id: 10 }); + const place2 = buildPlace({ id: 20 }); + const a1 = buildAssignment({ id: 1, day_id: 5, order_index: 0, place: place1 }); + const a2 = buildAssignment({ id: 2, day_id: 5, order_index: 1, place: place2 }); + seedStore(useTripStore, { + assignments: { '5': [a1, a2] }, + }); + + await useTripStore.getState().reorderAssignments(1, 5, [2, 1]); + + const dayAssignments = useTripStore.getState().assignments['5']; + const reorderedA2 = dayAssignments.find(a => a.id === 2); + const reorderedA1 = dayAssignments.find(a => a.id === 1); + expect(reorderedA2?.order_index).toBe(0); + expect(reorderedA1?.order_index).toBe(1); + }); + + it('FE-ASSIGN-005b: reorderAssignments rolls back on failure', async () => { + const place1 = buildPlace({ id: 10 }); + const place2 = buildPlace({ id: 20 }); + const a1 = buildAssignment({ id: 1, day_id: 5, order_index: 0, place: place1 }); + const a2 = buildAssignment({ id: 2, day_id: 5, order_index: 1, place: place2 }); + seedStore(useTripStore, { + assignments: { '5': [a1, a2] }, + }); + + server.use( + http.put('/api/trips/1/days/5/assignments/reorder', () => + HttpResponse.json({ message: 'Error' }, { status: 500 }) + ), + ); + + await expect(useTripStore.getState().reorderAssignments(1, 5, [2, 1])).rejects.toThrow(); + + const dayAssignments = useTripStore.getState().assignments['5']; + expect(dayAssignments.find(a => a.id === 1)?.order_index).toBe(0); + expect(dayAssignments.find(a => a.id === 2)?.order_index).toBe(1); + }); + }); + + describe('moveAssignment', () => { + it('FE-ASSIGN-006: moveAssignment removes from source day and adds to target day', async () => { + const place = buildPlace({ id: 10 }); + const assignment = buildAssignment({ id: 50, day_id: 1, order_index: 0, place }); + seedStore(useTripStore, { + assignments: { + '1': [assignment], + '2': [], + }, + }); + + await useTripStore.getState().moveAssignment(1, 50, 1, 2); + + expect(useTripStore.getState().assignments['1']).toHaveLength(0); + expect(useTripStore.getState().assignments['2']).toHaveLength(1); + expect(useTripStore.getState().assignments['2'][0].id).toBe(50); + }); + + it('FE-ASSIGN-007: moveAssignment rolls back on failure', async () => { + const place = buildPlace({ id: 10 }); + const assignment = buildAssignment({ id: 50, day_id: 1, order_index: 0, place }); + seedStore(useTripStore, { + assignments: { + '1': [assignment], + '2': [], + }, + }); + + server.use( + http.put('/api/trips/1/assignments/50/move', () => + HttpResponse.json({ message: 'Error' }, { status: 500 }) + ), + ); + + await expect(useTripStore.getState().moveAssignment(1, 50, 1, 2)).rejects.toThrow(); + + // Rolled back: assignment back in day 1 + expect(useTripStore.getState().assignments['1']).toHaveLength(1); + expect(useTripStore.getState().assignments['1'][0].id).toBe(50); + expect(useTripStore.getState().assignments['2']).toHaveLength(0); + }); + }); +}); diff --git a/client/tests/unit/slices/budgetSlice.test.ts b/client/tests/unit/slices/budgetSlice.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..3b428d1b4bc0c4f6d2f0b7ee9485833c429fa9f4 --- /dev/null +++ b/client/tests/unit/slices/budgetSlice.test.ts @@ -0,0 +1,176 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { http, HttpResponse } from 'msw'; +import { useTripStore } from '../../../src/store/tripStore'; +import { resetAllStores, seedStore } from '../../helpers/store'; +import { buildBudgetItem, buildReservation } from '../../helpers/factories'; +import { server } from '../../helpers/msw/server'; + +vi.mock('../../../src/api/websocket', () => ({ + connect: vi.fn(), + disconnect: vi.fn(), + getSocketId: vi.fn(() => null), + joinTrip: vi.fn(), + leaveTrip: vi.fn(), + addListener: vi.fn(), + removeListener: vi.fn(), + setRefetchCallback: vi.fn(), + setPreReconnectHook: vi.fn(), +})); + +beforeEach(() => { + resetAllStores(); +}); + +describe('budgetSlice', () => { + describe('loadBudgetItems', () => { + it('FE-BUDGET-001: loadBudgetItems fetches and replaces budgetItems', async () => { + seedStore(useTripStore, { budgetItems: [] }); + + const item = buildBudgetItem({ trip_id: 1 }); + server.use( + http.get('/api/trips/1/budget', () => HttpResponse.json({ items: [item] })), + ); + + await useTripStore.getState().loadBudgetItems(1); + + expect(useTripStore.getState().budgetItems).toHaveLength(1); + expect(useTripStore.getState().budgetItems[0].id).toBe(item.id); + }); + }); + + describe('addBudgetItem', () => { + it('FE-BUDGET-002: addBudgetItem appends to budgetItems', async () => { + const existing = buildBudgetItem({ trip_id: 1 }); + seedStore(useTripStore, { budgetItems: [existing] }); + + const result = await useTripStore.getState().addBudgetItem(1, { name: 'Hotel', total_price: 200 }); + + expect(result.name).toBe('Hotel'); + expect(useTripStore.getState().budgetItems).toHaveLength(2); + }); + + it('FE-BUDGET-003: addBudgetItem on failure throws', async () => { + server.use( + http.post('/api/trips/1/budget', () => + HttpResponse.json({ message: 'Error' }, { status: 500 }) + ), + ); + + await expect( + useTripStore.getState().addBudgetItem(1, { name: 'Fail' }) + ).rejects.toThrow(); + }); + }); + + describe('updateBudgetItem', () => { + it('FE-BUDGET-004: updateBudgetItem replaces item in array', async () => { + const item = buildBudgetItem({ id: 10, trip_id: 1, name: 'Old', total_price: 100 }); + seedStore(useTripStore, { budgetItems: [item] }); + + server.use( + http.put('/api/trips/1/budget/10', async ({ request }) => { + const body = await request.json() as Record; + return HttpResponse.json({ item: { ...item, ...body } }); + }), + ); + + const result = await useTripStore.getState().updateBudgetItem(1, 10, { name: 'Updated', total_price: 150 }); + + expect(result.name).toBe('Updated'); + expect(useTripStore.getState().budgetItems[0].name).toBe('Updated'); + }); + + it('FE-BUDGET-005: updateBudgetItem with total_price triggers loadReservations when reservation_id present', async () => { + const item = buildBudgetItem({ id: 10, trip_id: 1, total_price: 100 }); + const initialReservation = buildReservation({ trip_id: 1 }); + const newReservation = buildReservation({ trip_id: 1, title: 'Refreshed Reservation' }); + seedStore(useTripStore, { + budgetItems: [item], + reservations: [initialReservation], + }); + + server.use( + http.put('/api/trips/1/budget/10', async ({ request }) => { + const body = await request.json() as Record; + // Return item with reservation_id to trigger loadReservations + return HttpResponse.json({ item: { ...item, ...body, reservation_id: 42 } }); + }), + http.get('/api/trips/1/reservations', () => + HttpResponse.json({ reservations: [newReservation] }) + ), + ); + + await useTripStore.getState().updateBudgetItem(1, 10, { total_price: 200 } as Record); + + // Wait for the async loadReservations to complete + await new Promise(resolve => setTimeout(resolve, 50)); + + expect(useTripStore.getState().reservations).toHaveLength(1); + expect(useTripStore.getState().reservations[0].title).toBe('Refreshed Reservation'); + }); + }); + + describe('deleteBudgetItem', () => { + it('FE-BUDGET-006: deleteBudgetItem optimistically removes item, rolls back on failure', async () => { + const item = buildBudgetItem({ id: 10, trip_id: 1 }); + seedStore(useTripStore, { budgetItems: [item] }); + + server.use( + http.delete('/api/trips/1/budget/10', () => + HttpResponse.json({ message: 'Error' }, { status: 500 }) + ), + ); + + await expect(useTripStore.getState().deleteBudgetItem(1, 10)).rejects.toThrow(); + + expect(useTripStore.getState().budgetItems).toHaveLength(1); + expect(useTripStore.getState().budgetItems[0].id).toBe(10); + }); + + it('FE-BUDGET-006b: deleteBudgetItem success removes item', async () => { + const item1 = buildBudgetItem({ id: 10, trip_id: 1 }); + const item2 = buildBudgetItem({ id: 20, trip_id: 1 }); + seedStore(useTripStore, { budgetItems: [item1, item2] }); + + await useTripStore.getState().deleteBudgetItem(1, 10); + + expect(useTripStore.getState().budgetItems).toHaveLength(1); + expect(useTripStore.getState().budgetItems[0].id).toBe(20); + }); + }); + + describe('setBudgetItemMembers', () => { + it('FE-BUDGET-007: setBudgetItemMembers updates members array on item', async () => { + const item = buildBudgetItem({ id: 10, trip_id: 1, members: [] }); + seedStore(useTripStore, { budgetItems: [item] }); + + const members = [{ user_id: 1, paid: false }, { user_id: 2, paid: false }]; + server.use( + http.put('/api/trips/1/budget/10/members', () => + HttpResponse.json({ members, item: { ...item, persons: 2, members } }) + ), + ); + + const result = await useTripStore.getState().setBudgetItemMembers(1, 10, [1, 2]); + + expect(result.members).toHaveLength(2); + const updatedItem = useTripStore.getState().budgetItems.find(i => i.id === 10); + expect(updatedItem?.members).toHaveLength(2); + expect(updatedItem?.persons).toBe(2); + }); + }); + + describe('toggleBudgetMemberPaid', () => { + it('FE-BUDGET-008: toggleBudgetMemberPaid updates paid status after API success', async () => { + const member = { user_id: 5, paid: 0, username: 'dave' }; + const item = buildBudgetItem({ id: 10, trip_id: 1, members: [member] }); + seedStore(useTripStore, { budgetItems: [item] }); + + await useTripStore.getState().toggleBudgetMemberPaid(1, 10, 5, true); + + const updatedItem = useTripStore.getState().budgetItems.find(i => i.id === 10); + const updatedMember = updatedItem?.members.find(m => m.user_id === 5); + expect(updatedMember?.paid).toBe(true); + }); + }); +}); diff --git a/client/tests/unit/slices/dayNotesSlice.test.ts b/client/tests/unit/slices/dayNotesSlice.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..3d770ec0f659220aefa278cb4a93444ace18c2ec --- /dev/null +++ b/client/tests/unit/slices/dayNotesSlice.test.ts @@ -0,0 +1,177 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { http, HttpResponse } from 'msw'; +import { useTripStore } from '../../../src/store/tripStore'; +import { resetAllStores, seedStore } from '../../helpers/store'; +import { buildDay, buildDayNote } from '../../helpers/factories'; +import { server } from '../../helpers/msw/server'; + +vi.mock('../../../src/api/websocket', () => ({ + connect: vi.fn(), + disconnect: vi.fn(), + getSocketId: vi.fn(() => null), + joinTrip: vi.fn(), + leaveTrip: vi.fn(), + addListener: vi.fn(), + removeListener: vi.fn(), + setRefetchCallback: vi.fn(), + setPreReconnectHook: vi.fn(), +})); + +beforeEach(() => { + resetAllStores(); +}); + +describe('dayNotesSlice', () => { + describe('addDayNote', () => { + it('FE-DAYNOTES-001: addDayNote inserts temp note immediately, replaces on success', async () => { + seedStore(useTripStore, { dayNotes: { '1': [] } }); + + let tempAdded = false; + const realNote = buildDayNote({ id: 500, day_id: 1, text: 'New note' }); + + server.use( + http.post('/api/trips/1/days/1/notes', async () => { + const state = useTripStore.getState(); + const notes = state.dayNotes['1']; + if (notes.some(n => n.id < 0)) { + tempAdded = true; + } + return HttpResponse.json({ note: realNote }); + }), + ); + + const result = await useTripStore.getState().addDayNote(1, 1, { text: 'New note', sort_order: 0 }); + + expect(tempAdded).toBe(true); + expect(result.id).toBe(500); + const notes = useTripStore.getState().dayNotes['1']; + expect(notes).toHaveLength(1); + expect(notes[0].id).toBe(500); + }); + + it('FE-DAYNOTES-002: addDayNote on failure rolls back — temp note removed', async () => { + seedStore(useTripStore, { dayNotes: { '1': [] } }); + + server.use( + http.post('/api/trips/1/days/1/notes', () => + HttpResponse.json({ message: 'Error' }, { status: 500 }) + ), + ); + + await expect( + useTripStore.getState().addDayNote(1, 1, { text: 'Fail note', sort_order: 0 }) + ).rejects.toThrow(); + + expect(useTripStore.getState().dayNotes['1']).toHaveLength(0); + }); + }); + + describe('updateDayNote', () => { + it('FE-DAYNOTES-003: updateDayNote replaces note in map by id', async () => { + const note = buildDayNote({ id: 10, day_id: 1, text: 'Old text' }); + seedStore(useTripStore, { dayNotes: { '1': [note] } }); + + const updated = { ...note, text: 'Updated text' }; + server.use( + http.put('/api/trips/1/days/1/notes/10', () => + HttpResponse.json({ note: updated }) + ), + ); + + const result = await useTripStore.getState().updateDayNote(1, 1, 10, { text: 'Updated text' }); + + expect(result.text).toBe('Updated text'); + expect(useTripStore.getState().dayNotes['1'][0].text).toBe('Updated text'); + }); + }); + + describe('deleteDayNote', () => { + it('FE-DAYNOTES-004: deleteDayNote optimistically removes note, restores on failure', async () => { + const note = buildDayNote({ id: 10, day_id: 1 }); + seedStore(useTripStore, { dayNotes: { '1': [note] } }); + + server.use( + http.delete('/api/trips/1/days/1/notes/10', () => + HttpResponse.json({ message: 'Error' }, { status: 500 }) + ), + ); + + await expect(useTripStore.getState().deleteDayNote(1, 1, 10)).rejects.toThrow(); + + // Rolled back + expect(useTripStore.getState().dayNotes['1']).toHaveLength(1); + expect(useTripStore.getState().dayNotes['1'][0].id).toBe(10); + }); + + it('FE-DAYNOTES-004b: deleteDayNote success removes note from correct day', async () => { + const note1 = buildDayNote({ id: 10, day_id: 1 }); + const note2 = buildDayNote({ id: 20, day_id: 1 }); + seedStore(useTripStore, { dayNotes: { '1': [note1, note2] } }); + + await useTripStore.getState().deleteDayNote(1, 1, 10); + + const notes = useTripStore.getState().dayNotes['1']; + expect(notes).toHaveLength(1); + expect(notes[0].id).toBe(20); + }); + }); + + describe('moveDayNote', () => { + it('FE-DAYNOTES-005: moveDayNote removes from source, adds to target (delete+create)', async () => { + const note = buildDayNote({ id: 10, day_id: 1, text: 'Move me' }); + const newNote = buildDayNote({ id: 99, day_id: 2, text: 'Move me' }); + seedStore(useTripStore, { dayNotes: { '1': [note], '2': [] } }); + + server.use( + http.delete('/api/trips/1/days/1/notes/10', () => HttpResponse.json({ success: true })), + http.post('/api/trips/1/days/2/notes', () => HttpResponse.json({ note: newNote })), + ); + + await useTripStore.getState().moveDayNote(1, 1, 2, 10); + + expect(useTripStore.getState().dayNotes['1']).toHaveLength(0); + expect(useTripStore.getState().dayNotes['2']).toHaveLength(1); + expect(useTripStore.getState().dayNotes['2'][0].id).toBe(99); + }); + + it('FE-DAYNOTES-006: moveDayNote rolls back to source day on failure', async () => { + const note = buildDayNote({ id: 10, day_id: 1, text: 'Move me' }); + seedStore(useTripStore, { dayNotes: { '1': [note], '2': [] } }); + + server.use( + http.delete('/api/trips/1/days/1/notes/10', () => + HttpResponse.json({ message: 'Error' }, { status: 500 }) + ), + ); + + await expect(useTripStore.getState().moveDayNote(1, 1, 2, 10)).rejects.toThrow(); + + expect(useTripStore.getState().dayNotes['1']).toHaveLength(1); + expect(useTripStore.getState().dayNotes['1'][0].id).toBe(10); + }); + }); + + describe('updateDayNotes', () => { + it('FE-DAYNOTES-007: updateDayNotes persists notes text and updates days array', async () => { + const day = buildDay({ id: 1, trip_id: 1, notes: null }); + seedStore(useTripStore, { days: [day] }); + + await useTripStore.getState().updateDayNotes(1, 1, 'My travel notes'); + + const updatedDay = useTripStore.getState().days.find(d => d.id === 1); + expect(updatedDay?.notes).toBe('My travel notes'); + }); + }); + + describe('updateDayTitle', () => { + it('FE-DAYNOTES-008: updateDayTitle persists title and updates days array', async () => { + const day = buildDay({ id: 1, trip_id: 1, title: null }); + seedStore(useTripStore, { days: [day] }); + + await useTripStore.getState().updateDayTitle(1, 1, 'Day at the Beach'); + + const updatedDay = useTripStore.getState().days.find(d => d.id === 1); + expect(updatedDay?.title).toBe('Day at the Beach'); + }); + }); +}); diff --git a/client/tests/unit/slices/filesSlice.test.ts b/client/tests/unit/slices/filesSlice.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..7f5adc8c5999b57c22956109d9fbb947dda2ba10 --- /dev/null +++ b/client/tests/unit/slices/filesSlice.test.ts @@ -0,0 +1,119 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { http, HttpResponse } from 'msw'; +import { useTripStore } from '../../../src/store/tripStore'; +import { filesApi } from '../../../src/api/client'; +import { resetAllStores, seedStore } from '../../helpers/store'; +import { buildTripFile } from '../../helpers/factories'; +import { server } from '../../helpers/msw/server'; + +vi.mock('../../../src/api/websocket', () => ({ + connect: vi.fn(), + disconnect: vi.fn(), + getSocketId: vi.fn(() => null), + joinTrip: vi.fn(), + leaveTrip: vi.fn(), + addListener: vi.fn(), + removeListener: vi.fn(), + setRefetchCallback: vi.fn(), + setPreReconnectHook: vi.fn(), +})); + +beforeEach(() => { + resetAllStores(); +}); + +describe('filesSlice', () => { + describe('loadFiles', () => { + it('FE-FILES-001: loadFiles fetches and replaces files array', async () => { + const staleFile = buildTripFile({ trip_id: 1, filename: 'stale.pdf' }); + seedStore(useTripStore, { files: [staleFile] }); + + const freshFile = buildTripFile({ trip_id: 1, filename: 'fresh.pdf' }); + server.use( + http.get('/api/trips/1/files', () => HttpResponse.json({ files: [freshFile] })), + ); + + await useTripStore.getState().loadFiles(1); + + const files = useTripStore.getState().files; + expect(files).toHaveLength(1); + expect(files[0].filename).toBe('fresh.pdf'); + }); + + it('FE-FILES-002: loadFiles silently catches errors', async () => { + server.use( + http.get('/api/trips/1/files', () => + HttpResponse.json({ message: 'Error' }, { status: 500 }) + ), + ); + + // Should not throw + await useTripStore.getState().loadFiles(1); + }); + }); + + describe('addFile', () => { + it('FE-FILES-003: addFile uploads and prepends file to files array', async () => { + const existing = buildTripFile({ trip_id: 1, filename: 'existing.pdf' }); + seedStore(useTripStore, { files: [existing] }); + + const uploaded = buildTripFile({ trip_id: 1, filename: 'new-upload.pdf' }); + // FormData POST hangs on CI — mock at the API boundary instead of MSW. + const uploadSpy = vi.spyOn(filesApi, 'upload').mockResolvedValueOnce({ file: uploaded }); + + const formData = new FormData(); + formData.append('file', new Blob(['content'], { type: 'application/pdf' }), 'new-upload.pdf'); + + const result = await useTripStore.getState().addFile(1, formData); + uploadSpy.mockRestore(); + + expect(result.filename).toBe('new-upload.pdf'); + const files = useTripStore.getState().files; + expect(files).toHaveLength(2); + // prepends + expect(files[0].filename).toBe('new-upload.pdf'); + }); + + it('FE-FILES-004: addFile on failure throws', async () => { + server.use( + http.post('/api/trips/1/files', () => + HttpResponse.json({ message: 'Error' }, { status: 500 }) + ), + ); + + const formData = new FormData(); + + await expect(useTripStore.getState().addFile(1, formData)).rejects.toThrow(); + }); + }); + + describe('deleteFile', () => { + it('FE-FILES-005: deleteFile removes file from array after API success', async () => { + const file1 = buildTripFile({ id: 10, trip_id: 1 }); + const file2 = buildTripFile({ id: 20, trip_id: 1 }); + seedStore(useTripStore, { files: [file1, file2] }); + + await useTripStore.getState().deleteFile(1, 10); + + const files = useTripStore.getState().files; + expect(files).toHaveLength(1); + expect(files[0].id).toBe(20); + }); + + it('FE-FILES-006: deleteFile on failure throws', async () => { + const file = buildTripFile({ id: 10, trip_id: 1 }); + seedStore(useTripStore, { files: [file] }); + + server.use( + http.delete('/api/trips/1/files/10', () => + HttpResponse.json({ message: 'Error' }, { status: 500 }) + ), + ); + + await expect(useTripStore.getState().deleteFile(1, 10)).rejects.toThrow(); + + // File remains since server-first (only removes after success) + expect(useTripStore.getState().files).toHaveLength(1); + }); + }); +}); diff --git a/client/tests/unit/slices/packingSlice.test.ts b/client/tests/unit/slices/packingSlice.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..901c0a08223d32a0c42874cd96f19fdba7ad4227 --- /dev/null +++ b/client/tests/unit/slices/packingSlice.test.ts @@ -0,0 +1,135 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { http, HttpResponse } from 'msw'; +import { useTripStore } from '../../../src/store/tripStore'; +import { resetAllStores, seedStore } from '../../helpers/store'; +import { buildPackingItem } from '../../helpers/factories'; +import { server } from '../../helpers/msw/server'; + +vi.mock('../../../src/api/websocket', () => ({ + connect: vi.fn(), + disconnect: vi.fn(), + getSocketId: vi.fn(() => null), + joinTrip: vi.fn(), + leaveTrip: vi.fn(), + addListener: vi.fn(), + removeListener: vi.fn(), + setRefetchCallback: vi.fn(), + setPreReconnectHook: vi.fn(), +})); + +beforeEach(() => { + resetAllStores(); +}); + +describe('packingSlice', () => { + describe('addPackingItem', () => { + it('FE-PACKING-001: addPackingItem calls API and appends item to packingItems', async () => { + const existing = buildPackingItem({ trip_id: 1, name: 'Existing' }); + seedStore(useTripStore, { packingItems: [existing] }); + + const result = await useTripStore.getState().addPackingItem(1, { name: 'Toothbrush', quantity: 1 }); + + expect(result.name).toBe('Toothbrush'); + const items = useTripStore.getState().packingItems; + expect(items).toHaveLength(2); + // addPackingItem appends (not prepends) + expect(items[items.length - 1].name).toBe('Toothbrush'); + }); + + it('FE-PACKING-002: addPackingItem on failure throws', async () => { + server.use( + http.post('/api/trips/1/packing', () => + HttpResponse.json({ message: 'Error' }, { status: 500 }) + ), + ); + + await expect( + useTripStore.getState().addPackingItem(1, { name: 'Fail item' }) + ).rejects.toThrow(); + }); + }); + + describe('updatePackingItem', () => { + it('FE-PACKING-003: updatePackingItem replaces item in array by id', async () => { + const item = buildPackingItem({ id: 10, trip_id: 1, name: 'Old name', quantity: 1 }); + seedStore(useTripStore, { packingItems: [item] }); + + server.use( + http.put('/api/trips/1/packing/10', async ({ request }) => { + const body = await request.json() as Record; + return HttpResponse.json({ item: { ...item, ...body } }); + }), + ); + + const result = await useTripStore.getState().updatePackingItem(1, 10, { name: 'New name' }); + + expect(result.name).toBe('New name'); + expect(useTripStore.getState().packingItems[0].name).toBe('New name'); + }); + }); + + describe('deletePackingItem', () => { + it('FE-PACKING-004: deletePackingItem optimistically removes item, rollback on failure', async () => { + const item = buildPackingItem({ id: 10, trip_id: 1 }); + seedStore(useTripStore, { packingItems: [item] }); + + server.use( + http.delete('/api/trips/1/packing/10', () => + HttpResponse.json({ message: 'Error' }, { status: 500 }) + ), + ); + + await expect(useTripStore.getState().deletePackingItem(1, 10)).rejects.toThrow(); + + expect(useTripStore.getState().packingItems).toHaveLength(1); + expect(useTripStore.getState().packingItems[0].id).toBe(10); + }); + + it('FE-PACKING-004b: deletePackingItem success removes item', async () => { + const item1 = buildPackingItem({ id: 10, trip_id: 1 }); + const item2 = buildPackingItem({ id: 20, trip_id: 1 }); + seedStore(useTripStore, { packingItems: [item1, item2] }); + + await useTripStore.getState().deletePackingItem(1, 10); + + const items = useTripStore.getState().packingItems; + expect(items).toHaveLength(1); + expect(items[0].id).toBe(20); + }); + }); + + describe('togglePackingItem', () => { + it('FE-PACKING-005: togglePackingItem sets checked optimistically', async () => { + const item = buildPackingItem({ id: 10, trip_id: 1, checked: 0 }); + seedStore(useTripStore, { packingItems: [item] }); + + server.use( + http.put('/api/trips/1/packing/10', async ({ request }) => { + const body = await request.json() as Record; + return HttpResponse.json({ item: { ...item, ...body } }); + }), + ); + + await useTripStore.getState().togglePackingItem(1, 10, true); + + expect(useTripStore.getState().packingItems[0].checked).toBe(1); + }); + + it('FE-PACKING-006: togglePackingItem rolls back checked on API failure', async () => { + const item = buildPackingItem({ id: 10, trip_id: 1, checked: 0 }); + seedStore(useTripStore, { packingItems: [item] }); + + server.use( + http.put('/api/trips/1/packing/10', () => + HttpResponse.json({ message: 'Error' }, { status: 500 }) + ), + ); + + // toggle does NOT throw on error (silent rollback) + await useTripStore.getState().togglePackingItem(1, 10, true); + + // Should be rolled back to original value + expect(useTripStore.getState().packingItems[0].checked).toBe(0); + }); + }); +}); diff --git a/client/tests/unit/slices/placesSlice.test.ts b/client/tests/unit/slices/placesSlice.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..93a9310e190ab7b1fcbadb8ee13938a37b993717 --- /dev/null +++ b/client/tests/unit/slices/placesSlice.test.ts @@ -0,0 +1,151 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { http, HttpResponse } from 'msw'; +import { useTripStore } from '../../../src/store/tripStore'; +import { resetAllStores, seedStore } from '../../helpers/store'; +import { buildPlace, buildAssignment } from '../../helpers/factories'; +import { server } from '../../helpers/msw/server'; + +vi.mock('../../../src/api/websocket', () => ({ + connect: vi.fn(), + disconnect: vi.fn(), + getSocketId: vi.fn(() => null), + joinTrip: vi.fn(), + leaveTrip: vi.fn(), + addListener: vi.fn(), + removeListener: vi.fn(), + setRefetchCallback: vi.fn(), + setPreReconnectHook: vi.fn(), +})); + +beforeEach(() => { + resetAllStores(); +}); + +describe('placesSlice', () => { + describe('addPlace', () => { + it('FE-PLACES-001: addPlace calls API and prepends place to places array', async () => { + const existing = buildPlace({ trip_id: 1 }); + seedStore(useTripStore, { places: [existing] }); + + const result = await useTripStore.getState().addPlace(1, { name: 'New Place' }); + + expect(result.name).toBe('New Place'); + const places = useTripStore.getState().places; + expect(places).toHaveLength(2); + expect(places[0].name).toBe('New Place'); // prepended + }); + + it('FE-PLACES-002: addPlace on failure throws and places remain unchanged', async () => { + const existing = buildPlace({ trip_id: 1 }); + seedStore(useTripStore, { places: [existing] }); + + server.use( + http.post('/api/trips/:id/places', () => + HttpResponse.json({ message: 'Server error' }, { status: 500 }) + ), + ); + + await expect(useTripStore.getState().addPlace(1, { name: 'Fail' })).rejects.toThrow(); + expect(useTripStore.getState().places).toEqual([existing]); + }); + }); + + describe('updatePlace', () => { + it('FE-PLACES-003: updatePlace calls API and updates place in array', async () => { + const place = buildPlace({ id: 10, trip_id: 1, name: 'Old Name' }); + seedStore(useTripStore, { places: [place] }); + + server.use( + http.put('/api/trips/:id/places/:placeId', async ({ params, request }) => { + const body = await request.json() as Record; + return HttpResponse.json({ place: { ...place, ...body, id: Number(params.placeId) } }); + }), + ); + + const result = await useTripStore.getState().updatePlace(1, 10, { name: 'New Name' }); + + expect(result.name).toBe('New Name'); + const updated = useTripStore.getState().places.find(p => p.id === 10); + expect(updated?.name).toBe('New Name'); + }); + + it('FE-PLACES-004: updatePlace cascades to assignments map — assignment place field updated', async () => { + const place = buildPlace({ id: 10, trip_id: 1, name: 'Old Place' }); + const assignment = buildAssignment({ id: 100, day_id: 1, place }); + seedStore(useTripStore, { + places: [place], + assignments: { '1': [assignment] }, + }); + + server.use( + http.put('/api/trips/1/places/10', async ({ request }) => { + const body = await request.json() as Record; + return HttpResponse.json({ place: { ...place, ...body } }); + }), + ); + + await useTripStore.getState().updatePlace(1, 10, { name: 'Updated Place' }); + + const updatedAssignments = useTripStore.getState().assignments['1']; + expect(updatedAssignments[0].place.name).toBe('Updated Place'); + }); + }); + + describe('deletePlace', () => { + it('FE-PLACES-005: deletePlace removes place from places array', async () => { + const place1 = buildPlace({ id: 10, trip_id: 1 }); + const place2 = buildPlace({ id: 20, trip_id: 1 }); + seedStore(useTripStore, { places: [place1, place2], assignments: {} }); + + server.use( + http.delete('/api/trips/1/places/10', () => HttpResponse.json({ success: true })), + ); + + await useTripStore.getState().deletePlace(1, 10); + + const places = useTripStore.getState().places; + expect(places).toHaveLength(1); + expect(places[0].id).toBe(20); + }); + + it('FE-PLACES-006: deletePlace cascades — assignments referencing the place are removed', async () => { + const place = buildPlace({ id: 10, trip_id: 1 }); + const otherPlace = buildPlace({ id: 20, trip_id: 1 }); + const assignmentWithPlace = buildAssignment({ id: 100, day_id: 1, place }); + const assignmentOther = buildAssignment({ id: 200, day_id: 1, place: otherPlace }); + + seedStore(useTripStore, { + places: [place, otherPlace], + assignments: { '1': [assignmentWithPlace, assignmentOther] }, + }); + + server.use( + http.delete('/api/trips/1/places/10', () => HttpResponse.json({ success: true })), + ); + + await useTripStore.getState().deletePlace(1, 10); + + const dayAssignments = useTripStore.getState().assignments['1']; + expect(dayAssignments).toHaveLength(1); + expect(dayAssignments[0].id).toBe(200); + }); + }); + + describe('refreshPlaces', () => { + it('FE-PLACES-007: refreshPlaces re-fetches and replaces places array', async () => { + const stale = buildPlace({ id: 99, trip_id: 1, name: 'Stale' }); + seedStore(useTripStore, { places: [stale] }); + + const fresh = buildPlace({ trip_id: 1, name: 'Fresh' }); + server.use( + http.get('/api/trips/1/places', () => HttpResponse.json({ places: [fresh] })), + ); + + await useTripStore.getState().refreshPlaces(1); + + const places = useTripStore.getState().places; + expect(places).toHaveLength(1); + expect(places[0].name).toBe('Fresh'); + }); + }); +}); diff --git a/client/tests/unit/slices/reservationsSlice.test.ts b/client/tests/unit/slices/reservationsSlice.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..a95b91b77a96ef22a60ea95e7429bfbe73b39ab4 --- /dev/null +++ b/client/tests/unit/slices/reservationsSlice.test.ts @@ -0,0 +1,182 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { http, HttpResponse } from 'msw'; +import { useTripStore } from '../../../src/store/tripStore'; +import { resetAllStores, seedStore } from '../../helpers/store'; +import { buildReservation } from '../../helpers/factories'; +import { server } from '../../helpers/msw/server'; + +vi.mock('../../../src/api/websocket', () => ({ + connect: vi.fn(), + disconnect: vi.fn(), + getSocketId: vi.fn(() => null), + joinTrip: vi.fn(), + leaveTrip: vi.fn(), + addListener: vi.fn(), + removeListener: vi.fn(), + setRefetchCallback: vi.fn(), + setPreReconnectHook: vi.fn(), +})); + +beforeEach(() => { + resetAllStores(); +}); + +describe('reservationsSlice', () => { + describe('loadReservations', () => { + it('FE-RESERV-001: loadReservations fetches and replaces reservations', async () => { + seedStore(useTripStore, { reservations: [] }); + + const reservation = buildReservation({ trip_id: 1 }); + server.use( + http.get('/api/trips/1/reservations', () => + HttpResponse.json({ reservations: [reservation] }) + ), + ); + + await useTripStore.getState().loadReservations(1); + + expect(useTripStore.getState().reservations).toHaveLength(1); + expect(useTripStore.getState().reservations[0].id).toBe(reservation.id); + }); + }); + + describe('addReservation', () => { + it('FE-RESERV-002: addReservation prepends to reservations array', async () => { + const existing = buildReservation({ trip_id: 1, title: 'Existing' }); + seedStore(useTripStore, { reservations: [existing] }); + + const result = await useTripStore.getState().addReservation(1, { + title: 'New Hotel', + type: 'hotel', + status: 'pending', + }); + + expect(result.title).toBe('New Hotel'); + const reservations = useTripStore.getState().reservations; + expect(reservations).toHaveLength(2); + // addReservation prepends + expect(reservations[0].title).toBe('New Hotel'); + }); + + it('FE-RESERV-003: addReservation on failure throws', async () => { + server.use( + http.post('/api/trips/1/reservations', () => + HttpResponse.json({ message: 'Error' }, { status: 500 }) + ), + ); + + await expect( + useTripStore.getState().addReservation(1, { title: 'Fail' }) + ).rejects.toThrow(); + }); + }); + + describe('updateReservation', () => { + it('FE-RESERV-004: updateReservation replaces item in array by id', async () => { + const reservation = buildReservation({ id: 10, trip_id: 1, title: 'Old', status: 'pending' }); + seedStore(useTripStore, { reservations: [reservation] }); + + server.use( + http.put('/api/trips/1/reservations/10', async ({ request }) => { + const body = await request.json() as Record; + return HttpResponse.json({ reservation: { ...reservation, ...body } }); + }), + ); + + const result = await useTripStore.getState().updateReservation(1, 10, { title: 'Updated Hotel' }); + + expect(result.title).toBe('Updated Hotel'); + expect(useTripStore.getState().reservations[0].title).toBe('Updated Hotel'); + }); + }); + + describe('toggleReservationStatus', () => { + it('FE-RESERV-005: toggleReservationStatus flips confirmed to pending optimistically', async () => { + const reservation = buildReservation({ id: 10, trip_id: 1, status: 'confirmed' }); + seedStore(useTripStore, { reservations: [reservation] }); + + server.use( + http.put('/api/trips/1/reservations/10', async ({ request }) => { + const body = await request.json() as Record; + return HttpResponse.json({ reservation: { ...reservation, ...body } }); + }), + ); + + await useTripStore.getState().toggleReservationStatus(1, 10); + + expect(useTripStore.getState().reservations[0].status).toBe('pending'); + }); + + it('FE-RESERV-006: toggleReservationStatus flips pending to confirmed optimistically', async () => { + const reservation = buildReservation({ id: 10, trip_id: 1, status: 'pending' }); + seedStore(useTripStore, { reservations: [reservation] }); + + server.use( + http.put('/api/trips/1/reservations/10', async ({ request }) => { + const body = await request.json() as Record; + return HttpResponse.json({ reservation: { ...reservation, ...body } }); + }), + ); + + await useTripStore.getState().toggleReservationStatus(1, 10); + + expect(useTripStore.getState().reservations[0].status).toBe('confirmed'); + }); + + it('FE-RESERV-007: toggleReservationStatus rolls back and surfaces the error on API failure', async () => { + const reservation = buildReservation({ id: 10, trip_id: 1, status: 'confirmed' }); + seedStore(useTripStore, { reservations: [reservation] }); + + server.use( + http.put('/api/trips/1/reservations/10', () => + HttpResponse.json({ message: 'Error' }, { status: 500 }) + ), + ); + + // Rolls back the optimistic toggle AND rejects, so the caller's catch can + // show a toast (previously the failure was swallowed and the toast never fired). + await expect(useTripStore.getState().toggleReservationStatus(1, 10)).rejects.toThrow(); + + expect(useTripStore.getState().reservations[0].status).toBe('confirmed'); + }); + + it('FE-RESERV-008: toggleReservationStatus does nothing if reservation not found', async () => { + seedStore(useTripStore, { reservations: [] }); + + // Should not throw + await useTripStore.getState().toggleReservationStatus(1, 999); + + expect(useTripStore.getState().reservations).toHaveLength(0); + }); + }); + + describe('deleteReservation', () => { + it('FE-RESERV-009: deleteReservation removes from reservations after API success', async () => { + const r1 = buildReservation({ id: 10, trip_id: 1 }); + const r2 = buildReservation({ id: 20, trip_id: 1 }); + seedStore(useTripStore, { reservations: [r1, r2] }); + + await useTripStore.getState().deleteReservation(1, 10); + + const reservations = useTripStore.getState().reservations; + expect(reservations).toHaveLength(1); + expect(reservations[0].id).toBe(20); + }); + + it('FE-RESERV-010: deleteReservation on failure throws (no optimistic, server-first)', async () => { + const reservation = buildReservation({ id: 10, trip_id: 1 }); + seedStore(useTripStore, { reservations: [reservation] }); + + server.use( + http.delete('/api/trips/1/reservations/10', () => + HttpResponse.json({ message: 'Error' }, { status: 500 }) + ), + ); + + await expect(useTripStore.getState().deleteReservation(1, 10)).rejects.toThrow(); + + // Still in state since server-first (only removes after success) + expect(useTripStore.getState().reservations).toHaveLength(1); + }); + }); +}); diff --git a/client/tests/unit/slices/todoSlice.test.ts b/client/tests/unit/slices/todoSlice.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..123426bc3ba85a4bc0d6f9b5df81d1187c24dbdf --- /dev/null +++ b/client/tests/unit/slices/todoSlice.test.ts @@ -0,0 +1,150 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { http, HttpResponse } from 'msw'; +import { useTripStore } from '../../../src/store/tripStore'; +import { resetAllStores, seedStore } from '../../helpers/store'; +import { buildTodoItem } from '../../helpers/factories'; +import { server } from '../../helpers/msw/server'; + +vi.mock('../../../src/api/websocket', () => ({ + connect: vi.fn(), + disconnect: vi.fn(), + getSocketId: vi.fn(() => null), + joinTrip: vi.fn(), + leaveTrip: vi.fn(), + addListener: vi.fn(), + removeListener: vi.fn(), + setRefetchCallback: vi.fn(), + setPreReconnectHook: vi.fn(), +})); + +beforeEach(() => { + resetAllStores(); +}); + +describe('todoSlice', () => { + describe('addTodoItem', () => { + it('FE-TODO-001: addTodoItem calls API and appends item to todoItems', async () => { + const existing = buildTodoItem({ trip_id: 1 }); + seedStore(useTripStore, { todoItems: [existing] }); + + const result = await useTripStore.getState().addTodoItem(1, { name: 'Buy sunscreen', priority: 1 }); + + expect(result.name).toBe('Buy sunscreen'); + const items = useTripStore.getState().todoItems; + expect(items).toHaveLength(2); + }); + + it('FE-TODO-002: addTodoItem on failure throws', async () => { + server.use( + http.post('/api/trips/1/todo', () => + HttpResponse.json({ message: 'Error' }, { status: 500 }) + ), + ); + + await expect( + useTripStore.getState().addTodoItem(1, { name: 'Fail' }) + ).rejects.toThrow(); + }); + }); + + describe('updateTodoItem', () => { + it('FE-TODO-003: updateTodoItem replaces item and preserves priority field', async () => { + const item = buildTodoItem({ id: 10, trip_id: 1, name: 'Old', priority: 2, sort_order: 5 }); + seedStore(useTripStore, { todoItems: [item] }); + + server.use( + http.put('/api/trips/1/todo/10', async ({ request }) => { + const body = await request.json() as Record; + return HttpResponse.json({ item: { ...item, ...body } }); + }), + ); + + const result = await useTripStore.getState().updateTodoItem(1, 10, { name: 'Updated', priority: 2 }); + + expect(result.name).toBe('Updated'); + expect(result.priority).toBe(2); + expect(useTripStore.getState().todoItems[0].name).toBe('Updated'); + expect(useTripStore.getState().todoItems[0].priority).toBe(2); + }); + }); + + describe('deleteTodoItem', () => { + it('FE-TODO-004: deleteTodoItem optimistically removes item, rollback on failure', async () => { + const item = buildTodoItem({ id: 10, trip_id: 1 }); + seedStore(useTripStore, { todoItems: [item] }); + + server.use( + http.delete('/api/trips/1/todo/10', () => + HttpResponse.json({ message: 'Error' }, { status: 500 }) + ), + ); + + await expect(useTripStore.getState().deleteTodoItem(1, 10)).rejects.toThrow(); + + expect(useTripStore.getState().todoItems).toHaveLength(1); + expect(useTripStore.getState().todoItems[0].id).toBe(10); + }); + + it('FE-TODO-004b: deleteTodoItem success removes item from array', async () => { + const item1 = buildTodoItem({ id: 10, trip_id: 1 }); + const item2 = buildTodoItem({ id: 20, trip_id: 1 }); + seedStore(useTripStore, { todoItems: [item1, item2] }); + + await useTripStore.getState().deleteTodoItem(1, 10); + + const items = useTripStore.getState().todoItems; + expect(items).toHaveLength(1); + expect(items[0].id).toBe(20); + }); + }); + + describe('toggleTodoItem', () => { + it('FE-TODO-005: toggleTodoItem sets checked optimistically to 1', async () => { + const item = buildTodoItem({ id: 10, trip_id: 1, checked: 0 }); + seedStore(useTripStore, { todoItems: [item] }); + + server.use( + http.put('/api/trips/1/todo/10', async ({ request }) => { + const body = await request.json() as Record; + return HttpResponse.json({ item: { ...item, ...body } }); + }), + ); + + await useTripStore.getState().toggleTodoItem(1, 10, true); + + expect(useTripStore.getState().todoItems[0].checked).toBe(1); + }); + + it('FE-TODO-006: toggleTodoItem rolls back checked on API failure (silent)', async () => { + const item = buildTodoItem({ id: 10, trip_id: 1, checked: 0 }); + seedStore(useTripStore, { todoItems: [item] }); + + server.use( + http.put('/api/trips/1/todo/10', () => + HttpResponse.json({ message: 'Error' }, { status: 500 }) + ), + ); + + // Does NOT throw + await useTripStore.getState().toggleTodoItem(1, 10, true); + + expect(useTripStore.getState().todoItems[0].checked).toBe(0); + }); + + it('FE-TODO-007: toggleTodoItem preserves sort_order field', async () => { + const item = buildTodoItem({ id: 10, trip_id: 1, checked: 0, sort_order: 3 }); + seedStore(useTripStore, { todoItems: [item] }); + + server.use( + http.put('/api/trips/1/todo/10', async ({ request }) => { + const body = await request.json() as Record; + return HttpResponse.json({ item: { ...item, ...body } }); + }), + ); + + await useTripStore.getState().toggleTodoItem(1, 10, true); + + expect(useTripStore.getState().todoItems[0].sort_order).toBe(3); + }); + }); +}); diff --git a/client/tests/unit/stores/addonStore.test.ts b/client/tests/unit/stores/addonStore.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..d195b445640ce76ee26783ba2235bd9678e3c9a1 --- /dev/null +++ b/client/tests/unit/stores/addonStore.test.ts @@ -0,0 +1,65 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import { http, HttpResponse } from 'msw'; +import { server } from '../../helpers/msw/server'; +import { useAddonStore } from '../../../src/store/addonStore'; +import { resetAllStores } from '../../helpers/store'; + +beforeEach(() => { + resetAllStores(); +}); + +describe('addonStore', () => { + describe('FE-ADDON-001: loadAddons()', () => { + it('fetches and stores enabled addons', async () => { + await useAddonStore.getState().loadAddons(); + const state = useAddonStore.getState(); + + expect(state.loaded).toBe(true); + expect(state.addons.length).toBeGreaterThan(0); + expect(state.addons[0]).toHaveProperty('id'); + expect(state.addons[0]).toHaveProperty('enabled', true); + expect(state.bagTracking).toBe(false); + }); + + it('captures the global bagTracking flag from the response', async () => { + server.use( + http.get('/api/addons', () => + HttpResponse.json({ bagTracking: true, addons: [] }) + ) + ); + + await useAddonStore.getState().loadAddons(); + expect(useAddonStore.getState().bagTracking).toBe(true); + }); + }); + + describe('FE-ADDON-002: isEnabled returns true for known addon', () => { + it('returns true when addon is in the list and enabled', async () => { + await useAddonStore.getState().loadAddons(); + expect(useAddonStore.getState().isEnabled('vacay')).toBe(true); + }); + }); + + describe('FE-ADDON-003: isEnabled returns false for unknown addon', () => { + it('returns false when addon is not in the list', async () => { + await useAddonStore.getState().loadAddons(); + expect(useAddonStore.getState().isEnabled('nonexistent')).toBe(false); + }); + }); + + describe('FE-ADDON-004: API failure', () => { + it('sets loaded: true and keeps addons empty on API error', async () => { + server.use( + http.get('/api/addons', () => + HttpResponse.json({ error: 'Server error' }, { status: 500 }) + ) + ); + + await useAddonStore.getState().loadAddons(); + const state = useAddonStore.getState(); + + expect(state.loaded).toBe(true); + expect(state.addons).toEqual([]); + }); + }); +}); diff --git a/client/tests/unit/stores/authStore.test.ts b/client/tests/unit/stores/authStore.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..228697190e3a0bfaac0ad6eba1a7a86c03a77f35 --- /dev/null +++ b/client/tests/unit/stores/authStore.test.ts @@ -0,0 +1,491 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { http, HttpResponse } from 'msw'; +import { server } from '../../helpers/msw/server'; +import { useAuthStore } from '../../../src/store/authStore'; +import { authApi } from '../../../src/api/client'; +import { resetAllStores } from '../../helpers/store'; +import { buildUser } from '../../helpers/factories'; + +// The websocket module is already mocked globally in tests/setup.ts +import { connect, disconnect } from '../../../src/api/websocket'; + +beforeEach(() => { + resetAllStores(); + vi.clearAllMocks(); +}); + +describe('authStore', () => { + describe('FE-AUTH-001: Successful login', () => { + it('sets user, isAuthenticated: true, isLoading: false', async () => { + const user = buildUser(); + server.use( + http.post('/api/auth/login', () => + HttpResponse.json({ user, token: 'tok' }) + ) + ); + + await useAuthStore.getState().login(user.email, 'password'); + const state = useAuthStore.getState(); + + expect(state.user).toEqual(user); + expect(state.isAuthenticated).toBe(true); + expect(state.isLoading).toBe(false); + expect(state.error).toBeNull(); + }); + }); + + describe('FE-AUTH-002: Login failure', () => { + it('sets error and isAuthenticated: false', async () => { + server.use( + http.post('/api/auth/login', () => + HttpResponse.json({ error: 'Bad credentials' }, { status: 401 }) + ) + ); + + await expect( + useAuthStore.getState().login('bad@example.com', 'wrong') + ).rejects.toThrow(); + + const state = useAuthStore.getState(); + expect(state.error).toBe('Bad credentials'); + expect(state.isAuthenticated).toBe(false); + expect(state.isLoading).toBe(false); + }); + }); + + describe('FE-AUTH-003: Login calls connect()', () => { + it('calls connect from websocket module after successful login', async () => { + const user = buildUser(); + server.use( + http.post('/api/auth/login', () => + HttpResponse.json({ user, token: 'tok' }) + ) + ); + + await useAuthStore.getState().login(user.email, 'password'); + + expect(connect).toHaveBeenCalledOnce(); + }); + }); + + describe('FE-AUTH-004: loadUser with valid session', () => { + it('sets user state from /auth/me', async () => { + const user = buildUser(); + server.use( + http.get('/api/auth/me', () => HttpResponse.json({ user })) + ); + + await useAuthStore.getState().loadUser(); + const state = useAuthStore.getState(); + + expect(state.user).toEqual(user); + expect(state.isAuthenticated).toBe(true); + expect(state.isLoading).toBe(false); + }); + }); + + describe('FE-AUTH-005: loadUser with 401', () => { + it('clears auth state on 401', async () => { + server.use( + http.get('/api/auth/me', () => + HttpResponse.json({ error: 'Unauthorized' }, { status: 401 }) + ) + ); + + // Pre-seed as authenticated + useAuthStore.setState({ user: buildUser(), isAuthenticated: true }); + + await useAuthStore.getState().loadUser(); + const state = useAuthStore.getState(); + + expect(state.user).toBeNull(); + expect(state.isAuthenticated).toBe(false); + expect(state.isLoading).toBe(false); + }); + }); + + describe('FE-AUTH-006: logout', () => { + it('calls disconnect() and clears user state', async () => { + useAuthStore.setState({ user: buildUser(), isAuthenticated: true }); + + await useAuthStore.getState().logout(); + const state = useAuthStore.getState(); + + expect(disconnect).toHaveBeenCalledOnce(); + expect(state.user).toBeNull(); + expect(state.isAuthenticated).toBe(false); + }); + }); + + describe('FE-AUTH-007: Register success', () => { + it('sets user and authenticates', async () => { + const user = buildUser(); + server.use( + http.post('/api/auth/register', () => + HttpResponse.json({ user, token: 'tok' }) + ) + ); + + await useAuthStore.getState().register(user.username, user.email, 'password'); + const state = useAuthStore.getState(); + + expect(state.user).toEqual(user); + expect(state.isAuthenticated).toBe(true); + expect(state.isLoading).toBe(false); + }); + }); + + describe('FE-AUTH-008: authSequence guard', () => { + it('stale loadUser does not overwrite fresh login state', async () => { + let resolveStale!: (v: Response) => void; + const stalePromise = new Promise((res) => { resolveStale = res; }); + + // First call to /auth/me will hang until we resolve it manually + let callCount = 0; + server.use( + http.get('/api/auth/me', async () => { + callCount++; + if (callCount === 1) { + // Stale request — wait + await stalePromise; + return HttpResponse.json({ user: buildUser({ username: 'stale' }) }); + } + // Should not be called a second time in this test + return HttpResponse.json({ user: buildUser({ username: 'fresh' }) }); + }) + ); + + // Start loadUser but don't await yet + const staleLoad = useAuthStore.getState().loadUser(); + + // Meanwhile, perform a login (bumps authSequence) + const freshUser = buildUser({ username: 'freshlogin' }); + server.use( + http.post('/api/auth/login', () => + HttpResponse.json({ user: freshUser, token: 'tok' }) + ) + ); + await useAuthStore.getState().login(freshUser.email, 'password'); + + // Now resolve the stale loadUser response + resolveStale(new Response()); + await staleLoad; + + // The fresh login state must be preserved + const state = useAuthStore.getState(); + expect(state.user?.username).toBe('freshlogin'); + expect(state.isAuthenticated).toBe(true); + }); + }); + + describe('FE-AUTH-009: MFA-required state handling', () => { + it('returns mfa_required flag and does not set user as authenticated', async () => { + server.use( + http.post('/api/auth/login', () => + HttpResponse.json({ mfa_required: true, mfa_token: 'mfa-tok-123' }) + ) + ); + + const result = await useAuthStore.getState().login('user@example.com', 'password'); + + expect(result).toMatchObject({ mfa_required: true, mfa_token: 'mfa-tok-123' }); + const state = useAuthStore.getState(); + expect(state.isAuthenticated).toBe(false); + expect(state.user).toBeNull(); + }); + }); + + describe('FE-STORE-AUTH-010: completeMfaLogin success', () => { + it('sets user, isAuthenticated, and calls connect', async () => { + const user = buildUser(); + server.use( + http.post('/api/auth/mfa/verify-login', () => + HttpResponse.json({ user, token: 'mfa-session-tok' }) + ) + ); + + await useAuthStore.getState().completeMfaLogin('mfa-tok', '123456'); + const state = useAuthStore.getState(); + + expect(state.user).toEqual(user); + expect(state.isAuthenticated).toBe(true); + expect(state.isLoading).toBe(false); + expect(connect).toHaveBeenCalledOnce(); + }); + }); + + describe('FE-STORE-AUTH-011: completeMfaLogin failure', () => { + it('sets error and remains unauthenticated', async () => { + server.use( + http.post('/api/auth/mfa/verify-login', () => + HttpResponse.json({ error: 'Invalid code' }, { status: 401 }) + ) + ); + + await expect( + useAuthStore.getState().completeMfaLogin('mfa-tok', '000000') + ).rejects.toThrow(); + + const state = useAuthStore.getState(); + expect(state.error).toBeTruthy(); + expect(state.isAuthenticated).toBe(false); + expect(state.isLoading).toBe(false); + }); + }); + + describe('FE-STORE-AUTH-012: register failure', () => { + it('sets error on registration failure', async () => { + server.use( + http.post('/api/auth/register', () => + HttpResponse.json({ error: 'Email taken' }, { status: 400 }) + ) + ); + + await expect( + useAuthStore.getState().register('u', 'e@e.com', 'pw') + ).rejects.toThrow(); + + const state = useAuthStore.getState(); + expect(state.error).toBe('Email taken'); + expect(state.isAuthenticated).toBe(false); + }); + }); + + describe('FE-STORE-AUTH-013: loadUser silent mode', () => { + it('does not toggle isLoading when silent: true', async () => { + const user = buildUser(); + server.use( + http.get('/api/auth/me', () => HttpResponse.json({ user })) + ); + + useAuthStore.setState({ isLoading: false }); + + // isLoading should remain false immediately after calling (silent mode) + const loadPromise = useAuthStore.getState().loadUser({ silent: true }); + expect(useAuthStore.getState().isLoading).toBe(false); + + await loadPromise; + const state = useAuthStore.getState(); + expect(state.isAuthenticated).toBe(true); + expect(state.isLoading).toBe(false); + }); + }); + + describe('FE-STORE-AUTH-014: loadUser network error (non-401)', () => { + it('preserves auth state on network error', async () => { + server.use( + http.get('/api/auth/me', () => + HttpResponse.json({ error: 'Server error' }, { status: 500 }) + ) + ); + + useAuthStore.setState({ user: buildUser(), isAuthenticated: true }); + + await useAuthStore.getState().loadUser(); + const state = useAuthStore.getState(); + + expect(state.isAuthenticated).toBe(true); + expect(state.isLoading).toBe(false); + }); + }); + + describe('FE-STORE-AUTH-015: updateMapsKey', () => { + it('updates user maps_api_key', async () => { + server.use( + http.put('/api/auth/me/maps-key', () => + HttpResponse.json({ success: true }) + ) + ); + + useAuthStore.setState({ user: buildUser() }); + + await useAuthStore.getState().updateMapsKey('my-key'); + expect(useAuthStore.getState().user?.maps_api_key).toBe('my-key'); + }); + }); + + describe('FE-STORE-AUTH-016: updateMapsKey with null clears key', () => { + it('sets maps_api_key to null', async () => { + server.use( + http.put('/api/auth/me/maps-key', () => + HttpResponse.json({ success: true }) + ) + ); + + useAuthStore.setState({ user: buildUser({ maps_api_key: 'old-key' }) }); + + await useAuthStore.getState().updateMapsKey(null); + expect(useAuthStore.getState().user?.maps_api_key).toBeNull(); + }); + }); + + describe('FE-STORE-AUTH-017: updateApiKeys', () => { + it('updates user with returned data', async () => { + const updatedUser = buildUser({ username: 'apiuser' }); + server.use( + http.put('/api/auth/me/api-keys', () => + HttpResponse.json({ user: updatedUser }) + ) + ); + + useAuthStore.setState({ user: buildUser() }); + + await useAuthStore.getState().updateApiKeys({ some_api_key: 'val' }); + expect(useAuthStore.getState().user).toEqual(updatedUser); + }); + }); + + describe('FE-STORE-AUTH-018: updateProfile', () => { + it('updates user profile', async () => { + const updatedUser = buildUser({ username: 'updated' }); + server.use( + http.put('/api/auth/me/settings', () => + HttpResponse.json({ user: updatedUser }) + ) + ); + + useAuthStore.setState({ user: buildUser() }); + + await useAuthStore.getState().updateProfile({ username: 'updated' }); + expect(useAuthStore.getState().user?.username).toBe('updated'); + }); + }); + + describe('FE-STORE-AUTH-019: setDemoMode(true)', () => { + it('sets demoMode and localStorage', () => { + useAuthStore.getState().setDemoMode(true); + expect(useAuthStore.getState().demoMode).toBe(true); + expect(localStorage.getItem('demo_mode')).toBe('true'); + }); + }); + + describe('FE-STORE-AUTH-020: setDemoMode(false)', () => { + it('clears demoMode and localStorage', () => { + localStorage.setItem('demo_mode', 'true'); + useAuthStore.getState().setDemoMode(false); + expect(useAuthStore.getState().demoMode).toBe(false); + expect(localStorage.getItem('demo_mode')).toBeNull(); + }); + }); + + describe('FE-STORE-AUTH-021: demoLogin success', () => { + it('authenticates and sets demoMode', async () => { + const user = buildUser(); + server.use( + http.post('/api/auth/demo-login', () => + HttpResponse.json({ user, token: 'tok' }) + ) + ); + + await useAuthStore.getState().demoLogin(); + const state = useAuthStore.getState(); + + expect(state.isAuthenticated).toBe(true); + expect(state.demoMode).toBe(true); + expect(state.isLoading).toBe(false); + expect(connect).toHaveBeenCalled(); + }); + }); + + describe('FE-STORE-AUTH-022: simple setters', () => { + it('updates devMode, hasMapsKey, serverTimezone, appRequireMfa, tripRemindersEnabled', () => { + const { setDevMode, setHasMapsKey, setServerTimezone, setAppRequireMfa, setTripRemindersEnabled } = + useAuthStore.getState(); + + setDevMode(true); + expect(useAuthStore.getState().devMode).toBe(true); + + setHasMapsKey(true); + expect(useAuthStore.getState().hasMapsKey).toBe(true); + + setServerTimezone('Europe/Berlin'); + expect(useAuthStore.getState().serverTimezone).toBe('Europe/Berlin'); + + setAppRequireMfa(true); + expect(useAuthStore.getState().appRequireMfa).toBe(true); + + setTripRemindersEnabled(true); + expect(useAuthStore.getState().tripRemindersEnabled).toBe(true); + }); + }); + + describe('FE-STORE-AUTH-023: deleteAvatar', () => { + it('sets avatar_url to null', async () => { + server.use( + http.delete('/api/auth/avatar', () => + HttpResponse.json({ success: true }) + ) + ); + + useAuthStore.setState({ user: buildUser({ avatar_url: '/uploads/avatar.png' }) }); + + await useAuthStore.getState().deleteAvatar(); + expect(useAuthStore.getState().user?.avatar_url).toBeNull(); + }); + }); + + describe('FE-STORE-AUTH-UPLOAD: uploadAvatar', () => { + it('updates avatar_url from response', async () => { + // FormData POST hangs on CI — mock at the API boundary instead of MSW. + const uploadSpy = vi.spyOn(authApi, 'uploadAvatar').mockResolvedValueOnce({ avatar_url: '/uploads/avatar-new.png' }); + + useAuthStore.setState({ user: buildUser() }); + + const file = new File(['x'], 'avatar.png', { type: 'image/png' }); + const result = await useAuthStore.getState().uploadAvatar(file); + + expect(result.avatar_url).toBe('/uploads/avatar-new.png'); + expect(useAuthStore.getState().user?.avatar_url).toBe('/uploads/avatar-new.png'); + uploadSpy.mockRestore(); + }); + }); + + describe('FE-STORE-AUTH-PERSIST-001: logout resets persisted snapshot', () => { + it('snapshot has isAuthenticated:false after logout (PWA offline will redirect to login)', async () => { + useAuthStore.setState({ user: buildUser(), isAuthenticated: true }); + + await useAuthStore.getState().logout(); + + const snapshot = JSON.parse(localStorage.getItem('trek_auth_snapshot') ?? '{}'); + expect(snapshot?.state?.isAuthenticated).toBe(false); + expect(snapshot?.state?.user).toBeNull(); + }); + }); + + describe('FE-STORE-AUTH-PERSIST-002: 401 resets persisted snapshot', () => { + it('snapshot has isAuthenticated:false after 401 (expired session clears offline access)', async () => { + useAuthStore.setState({ user: buildUser(), isAuthenticated: true }); + + server.use( + http.get('/api/auth/me', () => + HttpResponse.json({ error: 'Unauthorized' }, { status: 401 }) + ) + ); + + await useAuthStore.getState().loadUser(); + + const snapshot = JSON.parse(localStorage.getItem('trek_auth_snapshot') ?? '{}'); + expect(snapshot?.state?.isAuthenticated).toBe(false); + expect(useAuthStore.getState().isAuthenticated).toBe(false); + }); + }); + + describe('FE-STORE-AUTH-PERSIST-003: network error preserves snapshot', () => { + it('snapshot retains isAuthenticated:true on network error (offline PWA skips login screen)', async () => { + useAuthStore.setState({ user: buildUser(), isAuthenticated: true }); + + server.use( + http.get('/api/auth/me', () => + HttpResponse.json({ error: 'Server error' }, { status: 500 }) + ) + ); + + await useAuthStore.getState().loadUser(); + + // Persist middleware writes the state; isAuthenticated must stay true + const snapshot = JSON.parse(localStorage.getItem('trek_auth_snapshot') ?? '{}'); + expect(snapshot?.state?.isAuthenticated).toBe(true); + expect(useAuthStore.getState().isAuthenticated).toBe(true); + }); + }); +}); diff --git a/client/tests/unit/stores/inAppNotificationStore.test.ts b/client/tests/unit/stores/inAppNotificationStore.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..ba6772272e23ca8e07736d2e5804089b8557f91a --- /dev/null +++ b/client/tests/unit/stores/inAppNotificationStore.test.ts @@ -0,0 +1,351 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import { http, HttpResponse } from 'msw'; +import { server } from '../../helpers/msw/server'; +import { useInAppNotificationStore } from '../../../src/store/inAppNotificationStore'; +import { resetAllStores } from '../../helpers/store'; + +// Raw notification factory matching the server shape (is_read as 0/1, params as strings) +function buildRawNotif(overrides: Record = {}) { + const id = Math.floor(Math.random() * 100000); + return { + id, + type: 'simple', + scope: 'trip', + target: 1, + sender_id: 2, + sender_username: 'alice', + sender_avatar: null, + recipient_id: 1, + title_key: 'notif.title', + title_params: '{}', + text_key: 'notif.text', + text_params: '{}', + positive_text_key: null, + negative_text_key: null, + response: null, + navigate_text_key: null, + navigate_target: null, + is_read: 0, + created_at: '2025-01-01T00:00:00.000Z', + ...overrides, + }; +} + +beforeEach(() => { + resetAllStores(); +}); + +describe('inAppNotificationStore', () => { + describe('FE-NOTIF-001: fetchNotifications() loads first page', () => { + it('populates notifications, total, and unreadCount', async () => { + await useInAppNotificationStore.getState().fetchNotifications(); + const state = useInAppNotificationStore.getState(); + + expect(state.notifications.length).toBeGreaterThan(0); + expect(state.total).toBeGreaterThan(0); + expect(state.unreadCount).toBe(5); + expect(state.isLoading).toBe(false); + }); + }); + + describe('FE-NOTIF-002: Pagination — loading more appends to list', () => { + it('appends additional notifications when fetchNotifications is called again', async () => { + // First page + await useInAppNotificationStore.getState().fetchNotifications(true); + const firstPageCount = useInAppNotificationStore.getState().notifications.length; + const total = useInAppNotificationStore.getState().total; + + // Only test pagination if there are more items + if (firstPageCount < total) { + await useInAppNotificationStore.getState().fetchNotifications(); + const state = useInAppNotificationStore.getState(); + expect(state.notifications.length).toBeGreaterThan(firstPageCount); + } else { + // All notifications fit in one page + expect(firstPageCount).toBe(total); + } + }); + }); + + describe('FE-NOTIF-003: markRead(id)', () => { + it('updates is_read to true for the notification', async () => { + // Seed with an unread notification + const unread = buildRawNotif({ id: 42, is_read: 0 }); + useInAppNotificationStore.setState({ + notifications: [{ ...unread, title_params: {}, text_params: {}, is_read: false }] as never, + unreadCount: 1, + }); + + await useInAppNotificationStore.getState().markRead(42); + const state = useInAppNotificationStore.getState(); + + const notif = state.notifications.find((n) => n.id === 42); + expect(notif?.is_read).toBe(true); + expect(state.unreadCount).toBe(0); + }); + }); + + describe('FE-NOTIF-004: handleNewNotification() prepends to list', () => { + it('adds a new notification at the start of the list', () => { + // Seed existing notifications + useInAppNotificationStore.setState({ + notifications: [{ ...buildRawNotif({ id: 1 }), title_params: {}, text_params: {}, is_read: false }] as never, + total: 1, + unreadCount: 1, + }); + + const newRaw = buildRawNotif({ id: 99 }); + useInAppNotificationStore.getState().handleNewNotification(newRaw as never); + + const state = useInAppNotificationStore.getState(); + expect(state.notifications[0].id).toBe(99); + expect(state.notifications.length).toBe(2); + expect(state.total).toBe(2); + expect(state.unreadCount).toBe(2); + }); + }); + + describe('FE-NOTIF-005: handleUpdatedNotification() updates existing notification', () => { + it('replaces the notification in the list', () => { + useInAppNotificationStore.setState({ + notifications: [{ ...buildRawNotif({ id: 7, is_read: 0 }), title_params: {}, text_params: {}, is_read: false }] as never, + total: 1, + unreadCount: 1, + }); + + const updated = buildRawNotif({ id: 7, is_read: 1 }); + useInAppNotificationStore.getState().handleUpdatedNotification(updated as never); + + const state = useInAppNotificationStore.getState(); + const notif = state.notifications.find((n) => n.id === 7); + expect(notif?.is_read).toBe(true); + }); + }); + + describe('FE-NOTIF-006: Unread count is correct', () => { + it('unreadCount matches the number of unread notifications', async () => { + await useInAppNotificationStore.getState().fetchNotifications(true); + const state = useInAppNotificationStore.getState(); + + // The mock returns 5 unread from the server + expect(state.unreadCount).toBe(5); + }); + }); + + describe('FE-STORE-NOTIF-007: fetchNotifications early-return when already loading', () => { + it('does not fetch when isLoading is true', async () => { + useInAppNotificationStore.setState({ isLoading: true }); + + await useInAppNotificationStore.getState().fetchNotifications(); + const state = useInAppNotificationStore.getState(); + + expect(state.notifications).toEqual([]); + expect(state.isLoading).toBe(true); + }); + }); + + describe('FE-STORE-NOTIF-008: fetchNotifications(reset=true) resets existing list', () => { + it('replaces seeded notifications with fresh data', async () => { + // Seed store with 3 notifications + useInAppNotificationStore.setState({ + notifications: [ + { ...buildRawNotif({ id: 901 }), title_params: {}, text_params: {}, is_read: false }, + { ...buildRawNotif({ id: 902 }), title_params: {}, text_params: {}, is_read: false }, + { ...buildRawNotif({ id: 903 }), title_params: {}, text_params: {}, is_read: false }, + ] as never, + total: 3, + }); + + await useInAppNotificationStore.getState().fetchNotifications(true); + const state = useInAppNotificationStore.getState(); + + // Should not contain seeded IDs + expect(state.notifications.find(n => n.id === 901)).toBeUndefined(); + expect(state.notifications.find(n => n.id === 902)).toBeUndefined(); + expect(state.notifications.find(n => n.id === 903)).toBeUndefined(); + // Should contain data from MSW (IDs 1-20) + expect(state.notifications.length).toBe(20); + expect(state.isLoading).toBe(false); + }); + }); + + describe('FE-STORE-NOTIF-009: hasMore is set correctly', () => { + it('hasMore is true when more items exist, false when all loaded', async () => { + // Default MSW returns 25 total, 20 per page + await useInAppNotificationStore.getState().fetchNotifications(true); + expect(useInAppNotificationStore.getState().hasMore).toBe(true); + + // Second page: offset=20, returns 5 items, total=25 => 25 >= 25 => hasMore=false + await useInAppNotificationStore.getState().fetchNotifications(); + expect(useInAppNotificationStore.getState().hasMore).toBe(false); + }); + }); + + describe('FE-STORE-NOTIF-010: fetchUnreadCount updates unreadCount', () => { + it('sets unreadCount from server response', async () => { + useInAppNotificationStore.setState({ unreadCount: 0 }); + + await useInAppNotificationStore.getState().fetchUnreadCount(); + expect(useInAppNotificationStore.getState().unreadCount).toBe(5); + }); + }); + + describe('FE-STORE-NOTIF-011: markUnread(id)', () => { + it('sets is_read to false and increments unreadCount', async () => { + useInAppNotificationStore.setState({ + notifications: [{ ...buildRawNotif({ id: 50, is_read: 1 }), title_params: {}, text_params: {}, is_read: true }] as never, + unreadCount: 0, + }); + + await useInAppNotificationStore.getState().markUnread(50); + const state = useInAppNotificationStore.getState(); + + expect(state.notifications.find(n => n.id === 50)?.is_read).toBe(false); + expect(state.unreadCount).toBe(1); + }); + }); + + describe('FE-STORE-NOTIF-012: markAllRead()', () => { + it('marks all notifications as read and sets unreadCount to 0', async () => { + useInAppNotificationStore.setState({ + notifications: [ + { ...buildRawNotif({ id: 60 }), title_params: {}, text_params: {}, is_read: false }, + { ...buildRawNotif({ id: 61 }), title_params: {}, text_params: {}, is_read: false }, + { ...buildRawNotif({ id: 62 }), title_params: {}, text_params: {}, is_read: false }, + ] as never, + unreadCount: 3, + }); + + await useInAppNotificationStore.getState().markAllRead(); + const state = useInAppNotificationStore.getState(); + + expect(state.notifications.every(n => n.is_read === true)).toBe(true); + expect(state.unreadCount).toBe(0); + }); + }); + + describe('FE-STORE-NOTIF-013: deleteNotification removes unread item and decrements counts', () => { + it('removes notification and decrements total and unreadCount', async () => { + useInAppNotificationStore.setState({ + notifications: [{ ...buildRawNotif({ id: 5 }), title_params: {}, text_params: {}, is_read: false }] as never, + total: 3, + unreadCount: 1, + }); + + await useInAppNotificationStore.getState().deleteNotification(5); + const state = useInAppNotificationStore.getState(); + + expect(state.notifications.find(n => n.id === 5)).toBeUndefined(); + expect(state.total).toBe(2); + expect(state.unreadCount).toBe(0); + }); + }); + + describe('FE-STORE-NOTIF-014: deleteNotification on read item does not decrement unreadCount', () => { + it('decrements total but not unreadCount', async () => { + useInAppNotificationStore.setState({ + notifications: [{ ...buildRawNotif({ id: 6, is_read: 1 }), title_params: {}, text_params: {}, is_read: true }] as never, + total: 2, + unreadCount: 0, + }); + + await useInAppNotificationStore.getState().deleteNotification(6); + const state = useInAppNotificationStore.getState(); + + expect(state.total).toBe(1); + expect(state.unreadCount).toBe(0); + }); + }); + + describe('FE-STORE-NOTIF-015: deleteAll clears all state', () => { + it('resets notifications, total, unreadCount, and hasMore', async () => { + useInAppNotificationStore.setState({ + notifications: [ + { ...buildRawNotif({ id: 70 }), title_params: {}, text_params: {}, is_read: false }, + { ...buildRawNotif({ id: 71 }), title_params: {}, text_params: {}, is_read: false }, + ] as never, + total: 2, + unreadCount: 2, + hasMore: true, + }); + + await useInAppNotificationStore.getState().deleteAll(); + const state = useInAppNotificationStore.getState(); + + expect(state.notifications).toEqual([]); + expect(state.total).toBe(0); + expect(state.unreadCount).toBe(0); + expect(state.hasMore).toBe(false); + }); + }); + + describe('FE-STORE-NOTIF-016: respondToBoolean updates notification', () => { + it('updates response and is_read from server', async () => { + useInAppNotificationStore.setState({ + notifications: [{ + ...buildRawNotif({ id: 10, type: 'boolean' }), + title_params: {}, + text_params: {}, + is_read: false, + }] as never, + unreadCount: 1, + }); + + await useInAppNotificationStore.getState().respondToBoolean(10, 'positive'); + const state = useInAppNotificationStore.getState(); + + const notif = state.notifications.find(n => n.id === 10); + expect(notif?.response).toBe('positive'); + expect(notif?.is_read).toBe(true); + }); + }); + + describe('FE-STORE-NOTIF-017: normalizeNotification coerces stringified params', () => { + it('parses JSON string params into objects', () => { + const raw = buildRawNotif({ + id: 200, + title_params: '{"trip":"Rome"}', + text_params: '{"user":"alice"}', + }); + + useInAppNotificationStore.getState().handleNewNotification(raw as never); + const notif = useInAppNotificationStore.getState().notifications.find(n => n.id === 200); + + expect(notif?.title_params).toEqual({ trip: 'Rome' }); + expect(notif?.text_params).toEqual({ user: 'alice' }); + }); + }); + + describe('FE-STORE-NOTIF-018: normalizeNotification handles already-parsed params', () => { + it('stores object params without error', () => { + const raw = buildRawNotif({ + id: 201, + title_params: {}, + text_params: { key: 'value' }, + }); + + expect(() => { + useInAppNotificationStore.getState().handleNewNotification(raw as never); + }).not.toThrow(); + + const notif = useInAppNotificationStore.getState().notifications.find(n => n.id === 201); + expect(notif?.title_params).toEqual({}); + expect(notif?.text_params).toEqual({ key: 'value' }); + }); + }); + + describe('FE-STORE-NOTIF-019: fetchUnreadCount is best-effort', () => { + it('does not throw on server error and preserves state', async () => { + useInAppNotificationStore.setState({ unreadCount: 3 }); + + server.use( + http.get('/api/notifications/in-app/unread-count', () => { + return new HttpResponse(null, { status: 500 }); + }), + ); + + await expect(useInAppNotificationStore.getState().fetchUnreadCount()).resolves.not.toThrow(); + expect(useInAppNotificationStore.getState().unreadCount).toBe(3); + }); + }); +}); diff --git a/client/tests/unit/stores/permissionsStore.test.ts b/client/tests/unit/stores/permissionsStore.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..7f5f88aa314c3d032189f149db104d31c3e3495b --- /dev/null +++ b/client/tests/unit/stores/permissionsStore.test.ts @@ -0,0 +1,110 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import { renderHook } from '@testing-library/react'; +import { usePermissionsStore, useCanDo } from '../../../src/store/permissionsStore'; +import { useAuthStore } from '../../../src/store/authStore'; +import { resetAllStores } from '../../helpers/store'; +import { buildUser, buildAdmin } from '../../helpers/factories'; + +beforeEach(() => { + resetAllStores(); +}); + +describe('permissionsStore', () => { + describe('FE-PERMS-001: setPermissions()', () => { + it('stores the permission map', () => { + const perms = { trip_create: 'everybody', file_upload: 'trip_member' } as const; + usePermissionsStore.getState().setPermissions(perms); + + expect(usePermissionsStore.getState().permissions).toEqual(perms); + }); + }); + + describe('FE-PERMS-002: useCanDo() — basic allow/deny', () => { + it('returns false when user is not authenticated', () => { + usePermissionsStore.getState().setPermissions({ trip_create: 'everybody' }); + + const { result } = renderHook(() => useCanDo()); + expect(result.current('trip_create')).toBe(false); + }); + + it('returns true for "everybody" when user is authenticated', () => { + useAuthStore.setState({ user: buildUser(), isAuthenticated: true }); + usePermissionsStore.getState().setPermissions({ trip_create: 'everybody' }); + + const { result } = renderHook(() => useCanDo()); + expect(result.current('trip_create')).toBe(true); + }); + + it('returns true when action has no configured permission (default allow)', () => { + useAuthStore.setState({ user: buildUser(), isAuthenticated: true }); + usePermissionsStore.getState().setPermissions({}); + + const { result } = renderHook(() => useCanDo()); + expect(result.current('unconfigured_action')).toBe(true); + }); + }); + + describe('Admin user', () => { + it('can do anything regardless of configured permissions', () => { + useAuthStore.setState({ user: buildAdmin(), isAuthenticated: true }); + usePermissionsStore.getState().setPermissions({ restricted_action: 'admin' }); + + const { result } = renderHook(() => useCanDo()); + expect(result.current('restricted_action')).toBe(true); + }); + }); + + describe('Owner permissions', () => { + it('trip_owner level: owner can act, member cannot', () => { + const user = buildUser({ id: 42 }); + useAuthStore.setState({ user, isAuthenticated: true }); + usePermissionsStore.getState().setPermissions({ delete_trip: 'trip_owner' }); + + const { result } = renderHook(() => useCanDo()); + const trip = { owner_id: 42 }; // user is owner + const otherTrip = { owner_id: 99 }; // user is not owner + + expect(result.current('delete_trip', trip)).toBe(true); + expect(result.current('delete_trip', otherTrip)).toBe(false); + }); + + it('trip_owner level: is_owner flag grants access', () => { + const user = buildUser({ id: 1 }); + useAuthStore.setState({ user, isAuthenticated: true }); + usePermissionsStore.getState().setPermissions({ delete_trip: 'trip_owner' }); + + const { result } = renderHook(() => useCanDo()); + expect(result.current('delete_trip', { is_owner: true })).toBe(true); + expect(result.current('delete_trip', { is_owner: false })).toBe(false); + }); + }); + + describe('Member permissions', () => { + it('trip_member level: members and owners can act, unauthenticated trip context cannot', () => { + const user = buildUser({ id: 1 }); + useAuthStore.setState({ user, isAuthenticated: true }); + usePermissionsStore.getState().setPermissions({ upload_file: 'trip_member' }); + + const { result } = renderHook(() => useCanDo()); + const asOwner = { owner_id: 1 }; // user is owner + const asMember = { owner_id: 99 }; // user is member (trip context provided, not owner) + const noTrip = null; // no trip context + + expect(result.current('upload_file', asOwner)).toBe(true); + expect(result.current('upload_file', asMember)).toBe(true); + expect(result.current('upload_file', noTrip)).toBe(false); + }); + }); + + describe('Nobody / admin-only level', () => { + it('admin level: regular user is denied even as trip owner', () => { + const user = buildUser({ id: 1 }); + useAuthStore.setState({ user, isAuthenticated: true }); + usePermissionsStore.getState().setPermissions({ admin_action: 'admin' }); + + const { result } = renderHook(() => useCanDo()); + expect(result.current('admin_action', { owner_id: 1 })).toBe(false); + expect(result.current('admin_action')).toBe(false); + }); + }); +}); diff --git a/client/tests/unit/stores/settingsStore.test.ts b/client/tests/unit/stores/settingsStore.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..cc020f5ffa2aed7203d568a2800af6cf7e790541 --- /dev/null +++ b/client/tests/unit/stores/settingsStore.test.ts @@ -0,0 +1,219 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import { http, HttpResponse } from 'msw'; +import { server } from '../../helpers/msw/server'; +import { useSettingsStore } from '../../../src/store/settingsStore'; +import { resetAllStores } from '../../helpers/store'; +import { buildSettings } from '../../helpers/factories'; + +beforeEach(() => { + resetAllStores(); +}); + +describe('settingsStore', () => { + describe('FE-SETTINGS-001: loadSettings()', () => { + it('fetches settings and updates store', async () => { + const settings = buildSettings({ default_currency: 'EUR', language: 'de' }); + server.use( + http.get('/api/settings', () => HttpResponse.json({ settings })) + ); + + await useSettingsStore.getState().loadSettings(); + const state = useSettingsStore.getState(); + + expect(state.settings.default_currency).toBe('EUR'); + expect(state.settings.language).toBe('de'); + expect(state.isLoaded).toBe(true); + }); + }); + + describe('FE-SETTINGS-002: updateSetting() optimistic update', () => { + it('immediately updates local state before API resolves', async () => { + // The store's set() is called synchronously before the first await (settingsApi.set) + // so state is visible without needing to await the full action. + const promise = useSettingsStore.getState().updateSetting('default_currency', 'GBP'); + + // Check optimistic state — no await needed here + expect(useSettingsStore.getState().settings.default_currency).toBe('GBP'); + + // Let the API call finish to avoid dangling promises + await promise; + }); + }); + + describe('FE-SETTINGS-003: updateSetting() reverts on API failure', () => { + it('throws when API fails', async () => { + server.use( + http.put('/api/settings', () => + HttpResponse.json({ error: 'Server error' }, { status: 500 }) + ) + ); + + // The store optimistically sets, then throws — the revert is a throw + await expect( + useSettingsStore.getState().updateSetting('default_currency', 'GBP') + ).rejects.toThrow(); + }); + }); + + describe('FE-SETTINGS-004: Language change', () => { + it('updates language field and localStorage', async () => { + await useSettingsStore.getState().updateSetting('language', 'fr'); + + const state = useSettingsStore.getState(); + expect(state.settings.language).toBe('fr'); + expect(localStorage.getItem('app_language')).toBe('fr'); + }); + }); + + describe('FE-SETTINGS-005: loadSettings failure', () => { + it('sets isLoaded: true even on API failure (graceful)', async () => { + server.use( + http.get('/api/settings', () => + HttpResponse.json({ error: 'Server error' }, { status: 500 }) + ) + ); + + await useSettingsStore.getState().loadSettings(); + const state = useSettingsStore.getState(); + + expect(state.isLoaded).toBe(true); + }); + }); + + describe('FE-STORE-SETTINGS-006: setLanguageLocal updates state and localStorage', () => { + it('sets language in state and localStorage without an API call', () => { + useSettingsStore.getState().setLanguageLocal('ja'); + + const state = useSettingsStore.getState(); + expect(state.settings.language).toBe('ja'); + expect(localStorage.getItem('app_language')).toBe('ja'); + }); + }); + + describe('FE-STORE-SETTINGS-007: setLanguageLocal without prior localStorage value', () => { + it('writes to localStorage even when no prior value exists', () => { + localStorage.clear(); + + useSettingsStore.getState().setLanguageLocal('ko'); + + const state = useSettingsStore.getState(); + expect(state.settings.language).toBe('ko'); + expect(localStorage.getItem('app_language')).toBe('ko'); + }); + }); + + describe('FE-STORE-SETTINGS-008: updateSettings bulk update', () => { + it('updates multiple settings keys and calls bulk API', async () => { + await useSettingsStore.getState().updateSettings({ dark_mode: true, default_currency: 'JPY' }); + + const state = useSettingsStore.getState(); + expect(state.settings.dark_mode).toBe(true); + expect(state.settings.default_currency).toBe('JPY'); + }); + }); + + describe('FE-STORE-SETTINGS-009: updateSettings optimistic update', () => { + it('updates state synchronously before API resolves', async () => { + const promise = useSettingsStore.getState().updateSettings({ dark_mode: true }); + + expect(useSettingsStore.getState().settings.dark_mode).toBe(true); + + await promise; + }); + }); + + describe('FE-STORE-SETTINGS-010: updateSettings API failure throws', () => { + it('throws when bulk API returns 500', async () => { + server.use( + http.post('/api/settings/bulk', () => + HttpResponse.json({ error: 'Server error' }, { status: 500 }) + ) + ); + + await expect( + useSettingsStore.getState().updateSettings({ dark_mode: true }) + ).rejects.toThrow(); + }); + }); + + describe('FE-STORE-SETTINGS-011: updateSetting non-language key does not write to localStorage', () => { + it('does not modify app_language in localStorage', async () => { + const before = localStorage.getItem('app_language'); + + await useSettingsStore.getState().updateSetting('dark_mode', true); + + expect(localStorage.getItem('app_language')).toBe(before); + }); + }); + + describe('FE-STORE-SETTINGS-012: loadSettings merges server values with defaults', () => { + it('preserves default keys not returned by server', async () => { + server.use( + http.get('/api/settings', () => + HttpResponse.json({ settings: { dark_mode: true } }) + ) + ); + + await useSettingsStore.getState().loadSettings(); + + const state = useSettingsStore.getState(); + expect(state.settings.dark_mode).toBe(true); + expect(state.settings.default_currency).toBe('USD'); + }); + }); + + describe('FE-STORE-SETTINGS-013: updateSetting for time_format', () => { + it('updates time_format in state', async () => { + await useSettingsStore.getState().updateSetting('time_format', '24h'); + + expect(useSettingsStore.getState().settings.time_format).toBe('24h'); + }); + }); + + describe('FE-STORE-SETTINGS-015: setLanguageTransient updates state without touching localStorage', () => { + it('sets language in state but does not write to localStorage', () => { + localStorage.clear(); + + useSettingsStore.getState().setLanguageTransient('fr'); + + expect(useSettingsStore.getState().settings.language).toBe('fr'); + expect(localStorage.getItem('app_language')).toBeNull(); + }); + }); + + describe('FE-STORE-SETTINGS-016: setLanguageTransient rejects unsupported language code', () => { + it('leaves state unchanged for an unknown code', () => { + const before = useSettingsStore.getState().settings.language; + + useSettingsStore.getState().setLanguageTransient('xx'); + + expect(useSettingsStore.getState().settings.language).toBe(before); + }); + }); + + describe('FE-STORE-SETTINGS-017: setLanguageTransient does not overwrite an explicit localStorage choice', () => { + it('localStorage remains unchanged after a transient set', () => { + localStorage.setItem('app_language', 'de'); + + useSettingsStore.getState().setLanguageTransient('es'); + + expect(localStorage.getItem('app_language')).toBe('de'); + }); + }); + + describe('FE-STORE-SETTINGS-014: updateSetting API failure leaves optimistic state', () => { + it('throws on API failure but keeps the optimistic state', async () => { + server.use( + http.put('/api/settings', () => + HttpResponse.json({ error: 'Server error' }, { status: 500 }) + ) + ); + + await expect( + useSettingsStore.getState().updateSetting('default_zoom', 15) + ).rejects.toThrow(); + + expect(useSettingsStore.getState().settings.default_zoom).toBe(15); + }); + }); +}); diff --git a/client/tests/unit/stores/vacayStore.test.ts b/client/tests/unit/stores/vacayStore.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..261c558be2c3770d0654195e7b62b53771628257 --- /dev/null +++ b/client/tests/unit/stores/vacayStore.test.ts @@ -0,0 +1,404 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import { http, HttpResponse } from 'msw'; +import { server } from '../../helpers/msw/server'; +import { useVacayStore } from '../../../src/store/vacayStore'; +import { resetAllStores } from '../../helpers/store'; + +beforeEach(() => { + resetAllStores(); +}); + +describe('vacayStore', () => { + describe('FE-VACAY-001: loadAll()', () => { + it('fetches plan, years, entries, and stats, updates state', async () => { + await useVacayStore.getState().loadAll(); + const state = useVacayStore.getState(); + + expect(state.plan).not.toBeNull(); + expect(state.plan?.id).toBe(1); + expect(state.years).toEqual([2025, 2026]); + expect(state.entries.length).toBeGreaterThan(0); + expect(state.stats.length).toBeGreaterThan(0); + expect(state.loading).toBe(false); + }); + }); + + describe('FE-VACAY-002: toggleEntry()', () => { + it('calls the toggle API then reloads entries and stats', async () => { + // Seed selected year + useVacayStore.setState({ selectedYear: 2025 }); + + let toggled = false; + server.use( + http.post('/api/addons/vacay/entries/toggle', () => { + toggled = true; + return HttpResponse.json({ success: true }); + }) + ); + + await useVacayStore.getState().toggleEntry('2025-06-20'); + + expect(toggled).toBe(true); + // After toggle, entries are refreshed from MSW (2 entries) + expect(useVacayStore.getState().entries.length).toBe(2); + }); + }); + + describe('FE-VACAY-003: loadHolidays() — holidays_enabled with calendars', () => { + it('populates holidays map when plan has holiday calendars', async () => { + // Set plan state with holidays_enabled and a simple (non-regional) calendar + useVacayStore.setState({ + selectedYear: 2025, + plan: { + id: 1, + holidays_enabled: true, + holidays_region: null, + holiday_calendars: [ + { id: 1, plan_id: 1, region: 'DE', label: 'Germany', color: '#ef4444', sort_order: 0 }, + ], + block_weekends: true, + carry_over_enabled: false, + company_holidays_enabled: false, + }, + }); + + // Override MSW to return non-regional holidays (no counties) + server.use( + http.get('/api/addons/vacay/holidays/:year/:country', () => + HttpResponse.json([ + { date: '2025-12-25', name: 'Christmas', localName: 'Weihnachten', global: true, counties: null }, + { date: '2025-01-01', name: 'New Year', localName: 'Neujahr', global: true, counties: null }, + ]) + ) + ); + + await useVacayStore.getState().loadHolidays(2025); + const state = useVacayStore.getState(); + + expect(Object.keys(state.holidays).length).toBeGreaterThan(0); + expect(state.holidays['2025-12-25']).toBeDefined(); + expect(state.holidays['2025-12-25'].name).toBe('Christmas'); + }); + }); + + describe('FE-VACAY-003b: loadHolidays() — holidays not enabled', () => { + it('sets holidays to empty map when holidays_enabled is false', async () => { + useVacayStore.setState({ + selectedYear: 2025, + plan: { + id: 1, + holidays_enabled: false, + holidays_region: null, + holiday_calendars: [], + block_weekends: true, + carry_over_enabled: false, + company_holidays_enabled: false, + }, + }); + + await useVacayStore.getState().loadHolidays(2025); + expect(useVacayStore.getState().holidays).toEqual({}); + }); + }); + + describe('FE-VACAY-004a: updatePlan()', () => { + it('updates plan and reloads entries, stats, holidays', async () => { + // Need existing plan for holiday check in loadHolidays + useVacayStore.setState({ + selectedYear: 2025, + plan: { + id: 1, + holidays_enabled: false, + holidays_region: null, + holiday_calendars: [], + block_weekends: true, + carry_over_enabled: false, + company_holidays_enabled: false, + }, + }); + + await useVacayStore.getState().updatePlan({ holidays_enabled: true }); + const state = useVacayStore.getState(); + + // The MSW handler for PUT /addons/vacay/plan returns holidays_enabled: true + expect(state.plan?.holidays_enabled).toBe(true); + }); + }); + + describe('FE-VACAY-004b: addYear()', () => { + it('adds a year and the years list is updated', async () => { + await useVacayStore.getState().addYear(2027); + expect(useVacayStore.getState().years).toContain(2027); + }); + }); + + describe('FE-VACAY-004c: removeYear()', () => { + it('removes a year and updates the years list', async () => { + useVacayStore.setState({ years: [2025, 2026], selectedYear: 2026 }); + + await useVacayStore.getState().removeYear(2026); + const state = useVacayStore.getState(); + + // MSW returns [2025] after delete + expect(state.years).toEqual([2025]); + // selectedYear should shift to the last remaining year + expect(state.selectedYear).toBe(2025); + }); + }); + + describe('FE-STORE-VACAY-005: setSelectedYear and setSelectedUserId', () => { + it('updates selectedYear state', () => { + useVacayStore.getState().setSelectedYear(2028); + expect(useVacayStore.getState().selectedYear).toBe(2028); + }); + + it('updates selectedUserId state', () => { + useVacayStore.getState().setSelectedUserId(42); + expect(useVacayStore.getState().selectedUserId).toBe(42); + }); + + it('sets selectedUserId to null', () => { + useVacayStore.setState({ selectedUserId: 42 }); + useVacayStore.getState().setSelectedUserId(null); + expect(useVacayStore.getState().selectedUserId).toBeNull(); + }); + }); + + describe('FE-STORE-VACAY-006: loadEntries() uses selectedYear when no year arg', () => { + it('falls back to selectedYear when called without argument', async () => { + useVacayStore.setState({ selectedYear: 2025 }); + await useVacayStore.getState().loadEntries(); + expect(useVacayStore.getState().entries.length).toBe(2); + }); + }); + + describe('FE-STORE-VACAY-007: loadStats() uses selectedYear when no year arg', () => { + it('falls back to selectedYear when called without argument', async () => { + useVacayStore.setState({ selectedYear: 2025 }); + await useVacayStore.getState().loadStats(); + expect(useVacayStore.getState().stats.length).toBe(1); + }); + }); + + describe('FE-STORE-VACAY-008: invite()', () => { + it('calls invite API and reloads plan', async () => { + let inviteCalled = false; + server.use( + http.post('/api/addons/vacay/invite', () => { + inviteCalled = true; + return HttpResponse.json({ success: true }); + }) + ); + + await useVacayStore.getState().invite(5); + const state = useVacayStore.getState(); + + expect(inviteCalled).toBe(true); + expect(state.plan).not.toBeNull(); + expect(state.plan?.id).toBe(1); + }); + }); + + describe('FE-STORE-VACAY-009: declineInvite()', () => { + it('calls decline API and reloads plan', async () => { + await useVacayStore.getState().declineInvite(2); + expect(useVacayStore.getState().plan?.id).toBe(1); + }); + }); + + describe('FE-STORE-VACAY-010: cancelInvite()', () => { + it('calls cancel API and reloads plan', async () => { + await useVacayStore.getState().cancelInvite(3); + const state = useVacayStore.getState(); + expect(state.plan).not.toBeNull(); + expect(state.plan?.id).toBe(1); + }); + }); + + describe('FE-STORE-VACAY-011: acceptInvite()', () => { + it('calls loadAll after accepting invite', async () => { + await useVacayStore.getState().acceptInvite(1); + const state = useVacayStore.getState(); + + expect(state.plan).not.toBeNull(); + expect(state.years).toEqual([2025, 2026]); + expect(state.loading).toBe(false); + }); + }); + + describe('FE-STORE-VACAY-012: dissolve()', () => { + it('calls loadAll after dissolving', async () => { + await useVacayStore.getState().dissolve(); + const state = useVacayStore.getState(); + + expect(state.plan).not.toBeNull(); + expect(state.loading).toBe(false); + }); + }); + + describe('FE-STORE-VACAY-013: updateColor()', () => { + it('reloads plan and entries after updating color', async () => { + server.use( + http.put('/api/addons/vacay/color', () => + HttpResponse.json({ success: true }) + ) + ); + + await useVacayStore.getState().updateColor('#ff0000'); + const state = useVacayStore.getState(); + + expect(state.plan?.id).toBe(1); + expect(state.entries.length).toBe(2); + }); + }); + + describe('FE-STORE-VACAY-014: toggleCompanyHoliday()', () => { + it('reloads entries and stats after toggling company holiday', async () => { + useVacayStore.setState({ selectedYear: 2025 }); + + server.use( + http.post('/api/addons/vacay/entries/company-holiday', () => + HttpResponse.json({ success: true }) + ) + ); + + await useVacayStore.getState().toggleCompanyHoliday('2025-12-26'); + const state = useVacayStore.getState(); + + expect(state.entries.length).toBe(2); + expect(state.stats.length).toBe(1); + }); + }); + + describe('FE-STORE-VACAY-015: updateVacationDays()', () => { + it('reloads stats for the given year', async () => { + await useVacayStore.getState().updateVacationDays(2025, 25); + expect(useVacayStore.getState().stats.length).toBe(1); + }); + }); + + describe('FE-STORE-VACAY-016: removeYear() when selectedYear is not the removed year', () => { + it('does not change selectedYear when a different year is removed', async () => { + useVacayStore.setState({ years: [2025, 2026], selectedYear: 2025 }); + + await useVacayStore.getState().removeYear(2026); + const state = useVacayStore.getState(); + + expect(state.years).toEqual([2025]); + expect(state.selectedYear).toBe(2025); + }); + }); + + describe('FE-STORE-VACAY-017: addHolidayCalendar()', () => { + it('reloads plan and holidays after adding a holiday calendar', async () => { + server.use( + http.post('/api/addons/vacay/plan/holiday-calendars', () => + HttpResponse.json({ + calendar: { id: 1, plan_id: 1, region: 'DE', label: null, color: '#ef4444', sort_order: 0 }, + }) + ) + ); + + await useVacayStore.getState().addHolidayCalendar({ region: 'DE', color: '#ef4444' }); + expect(useVacayStore.getState().plan?.id).toBe(1); + }); + }); + + describe('FE-STORE-VACAY-018: updateHolidayCalendar()', () => { + it('reloads plan and holidays after updating a holiday calendar', async () => { + server.use( + http.put('/api/addons/vacay/plan/holiday-calendars/:id', () => + HttpResponse.json({ + calendar: { id: 1, plan_id: 1, region: 'US', label: 'US Holidays', color: '#3b82f6', sort_order: 0 }, + }) + ) + ); + + await useVacayStore.getState().updateHolidayCalendar(1, { label: 'US Holidays' }); + expect(useVacayStore.getState().plan?.id).toBe(1); + }); + }); + + describe('FE-STORE-VACAY-019: deleteHolidayCalendar()', () => { + it('reloads plan and holidays after deleting a holiday calendar', async () => { + await useVacayStore.getState().deleteHolidayCalendar(1); + expect(useVacayStore.getState().plan?.id).toBe(1); + }); + }); + + describe('FE-STORE-VACAY-020: loadHolidays() with regional calendar includes matching counties', () => { + it('includes holidays matching the region county and excludes non-matching ones', async () => { + useVacayStore.setState({ + selectedYear: 2025, + plan: { + id: 1, + holidays_enabled: true, + holidays_region: null, + holiday_calendars: [ + { id: 1, plan_id: 1, region: 'DE-BY', label: null, color: '#ef4444', sort_order: 0 }, + ], + block_weekends: false, + carry_over_enabled: false, + company_holidays_enabled: false, + }, + }); + + server.use( + http.get('/api/addons/vacay/holidays/:year/:country', () => + HttpResponse.json([ + { date: '2025-11-01', name: 'All Saints Day', localName: 'Allerheiligen', global: false, counties: ['DE-BY', 'DE-BW'] }, + { date: '2025-08-15', name: 'Assumption Day', localName: 'Mariä Himmelfahrt', global: false, counties: ['DE-BY'] }, + { date: '2025-03-19', name: 'St. Joseph', localName: 'Sankt Joseph', global: false, counties: ['DE-NW'] }, + ]) + ) + ); + + await useVacayStore.getState().loadHolidays(2025); + const holidays = useVacayStore.getState().holidays; + + // DE-BY holidays should be included + expect(holidays['2025-11-01']).toBeDefined(); + expect(holidays['2025-08-15']).toBeDefined(); + // DE-NW only holiday should be excluded + expect(holidays['2025-03-19']).toBeUndefined(); + }); + }); + + describe('FE-STORE-VACAY-021: loadHolidays() skips regional calendar when data has no county breakdown', () => { + it('results in empty holidays map when all entries are global (no counties)', async () => { + useVacayStore.setState({ + selectedYear: 2025, + plan: { + id: 1, + holidays_enabled: true, + holidays_region: null, + holiday_calendars: [ + { id: 1, plan_id: 1, region: 'DE-BY', label: null, color: '#ef4444', sort_order: 0 }, + ], + block_weekends: false, + carry_over_enabled: false, + company_holidays_enabled: false, + }, + }); + + server.use( + http.get('/api/addons/vacay/holidays/:year/:country', () => + HttpResponse.json([ + { date: '2025-12-25', name: 'Christmas', localName: 'Weihnachten', global: true, counties: null }, + { date: '2025-01-01', name: 'New Year', localName: 'Neujahr', global: true, counties: null }, + ]) + ) + ); + + await useVacayStore.getState().loadHolidays(2025); + // hasRegions is false (no counties), region is 'DE-BY' (non-null) + // so the condition `hasRegions && !region` is false → proceeds to county filter + // h.global is true → all holidays are included despite region filter + // Actually: global=true entries are included by the `h.global` check in the forEach + // The test verifies behavior when counties: null + global: true + const holidays = useVacayStore.getState().holidays; + // Global holidays are included even for regional calendars when counties data is absent + expect(holidays['2025-12-25']).toBeDefined(); + }); + }); +}); diff --git a/client/tests/unit/sync/mutationQueue.test.ts b/client/tests/unit/sync/mutationQueue.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..8e7868acf3cdbeb80a4da1c9d8d4cf7eef16f63f --- /dev/null +++ b/client/tests/unit/sync/mutationQueue.test.ts @@ -0,0 +1,464 @@ +/** + * mutationQueue unit tests. + * + * Covers: enqueue, flush (2xx success, 4xx fail, network error), idempotency header, + * pending count, create temp-id reconciliation, delete Dexie cleanup. + */ +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; +import 'fake-indexeddb/auto'; +import { server } from '../../helpers/msw/server'; +import { http, HttpResponse } from 'msw'; +import { setAuthed } from '../../../src/sync/authGate'; +import { mutationQueue, generateUUID, nextTempId } from '../../../src/sync/mutationQueue'; +import { offlineDb, clearAll } from '../../../src/db/offlineDb'; +import { placeRepo } from '../../../src/repo/placeRepo'; +import { buildPlace, buildPackingItem } from '../../helpers/factories'; + +beforeEach(async () => { + await clearAll(); + mutationQueue._resetFlushing(); + setAuthed(true); + Object.defineProperty(navigator, 'onLine', { value: true, writable: true, configurable: true }); +}); + +afterEach(() => { + vi.restoreAllMocks(); + setAuthed(false); +}); + +// ── helpers ────────────────────────────────────────────────────────────────── + +function makeMutation(overrides: Partial[0]> = {}) { + return { + id: generateUUID(), + tripId: 1, + method: 'POST' as const, + url: '/trips/1/places', + body: { name: 'Eiffel Tower' }, + resource: 'places', + ...overrides, + }; +} + +// ── enqueue ─────────────────────────────────────────────────────────────────── + +describe('mutationQueue.enqueue', () => { + it('stores mutation with pending status', async () => { + const id = generateUUID(); + await mutationQueue.enqueue(makeMutation({ id })); + + const stored = await offlineDb.mutationQueue.get(id); + expect(stored).toBeDefined(); + expect(stored!.status).toBe('pending'); + expect(stored!.attempts).toBe(0); + }); + + it('returns the mutation id', async () => { + const id = generateUUID(); + const returned = await mutationQueue.enqueue(makeMutation({ id })); + expect(returned).toBe(id); + }); +}); + +// ── flush — success path ────────────────────────────────────────────────────── + +describe('mutationQueue.flush — 2xx success', () => { + it('removes mutation from queue and writes canonical entity to Dexie', async () => { + const place = buildPlace({ trip_id: 1, id: 42 }); + const id = generateUUID(); + await mutationQueue.enqueue(makeMutation({ id })); + + server.use( + http.post('/api/trips/1/places', () => HttpResponse.json({ place })), + ); + + await mutationQueue.flush(); + + const queued = await offlineDb.mutationQueue.get(id); + expect(queued).toBeUndefined(); + + const cached = await offlineDb.places.get(42); + expect(cached).toBeDefined(); + expect(cached!.name).toBe(place.name); + }); + + it('attaches X-Idempotency-Key header matching the mutation id', async () => { + const place = buildPlace({ trip_id: 1 }); + const id = generateUUID(); + await mutationQueue.enqueue(makeMutation({ id })); + + let capturedKey: string | null = null; + server.use( + http.post('/api/trips/1/places', ({ request }) => { + capturedKey = request.headers.get('X-Idempotency-Key'); + return HttpResponse.json({ place }); + }), + ); + + await mutationQueue.flush(); + expect(capturedKey).toBe(id); + }); + + it('removes temp entry and adds canonical entry on CREATE flush', async () => { + const tempId = -12345; + const place = buildPlace({ trip_id: 1, id: 99 }); + const id = generateUUID(); + + // Optimistic temp entry in Dexie + await offlineDb.places.put({ ...place, id: tempId }); + + await mutationQueue.enqueue(makeMutation({ id, tempId })); + + server.use( + http.post('/api/trips/1/places', () => HttpResponse.json({ place })), + ); + + await mutationQueue.flush(); + + expect(await offlineDb.places.get(tempId)).toBeUndefined(); + expect(await offlineDb.places.get(99)).toBeDefined(); + }); + + it('handles DELETE: removes entity from Dexie after flush', async () => { + const place = buildPlace({ trip_id: 1, id: 55 }); + await offlineDb.places.put(place); + + const id = generateUUID(); + await mutationQueue.enqueue({ + id, + tripId: 1, + method: 'DELETE', + url: '/trips/1/places/55', + body: undefined, + resource: 'places', + entityId: 55, + }); + + server.use( + http.delete('/api/trips/1/places/55', () => HttpResponse.json({ success: true })), + ); + + await mutationQueue.flush(); + + expect(await offlineDb.mutationQueue.get(id)).toBeUndefined(); + expect(await offlineDb.places.get(55)).toBeUndefined(); + }); +}); + +// ── flush — error paths ─────────────────────────────────────────────────────── + +describe('mutationQueue.flush — 4xx client error', () => { + it('marks mutation as failed and continues to next mutation', async () => { + const id1 = generateUUID(); + const id2 = generateUUID(); + const place = buildPlace({ trip_id: 1 }); + + // Enqueue in order + await mutationQueue.enqueue(makeMutation({ id: id1 })); + await mutationQueue.enqueue(makeMutation({ id: id2 })); + + let callCount = 0; + server.use( + http.post('/api/trips/1/places', () => { + callCount++; + if (callCount === 1) { + return HttpResponse.json({ error: 'Bad request' }, { status: 400 }); + } + return HttpResponse.json({ place }); + }), + ); + + await mutationQueue.flush(); + + const m1 = await offlineDb.mutationQueue.get(id1); + expect(m1).toBeDefined(); + expect(m1!.status).toBe('failed'); + + // Second mutation succeeded and was removed + expect(await offlineDb.mutationQueue.get(id2)).toBeUndefined(); + }); +}); + +describe('mutationQueue.flush — network error', () => { + it('resets to pending and stops flush without marking failed', async () => { + const id = generateUUID(); + await mutationQueue.enqueue(makeMutation({ id })); + + server.use( + http.post('/api/trips/1/places', () => HttpResponse.error()), + ); + + await mutationQueue.flush(); + + const m = await offlineDb.mutationQueue.get(id); + expect(m).toBeDefined(); + expect(m!.status).toBe('pending'); + expect(m!.attempts).toBe(1); + }); +}); + +// ── flush — offline guard ───────────────────────────────────────────────────── + +describe('mutationQueue.flush — offline guard', () => { + it('does nothing when offline', async () => { + Object.defineProperty(navigator, 'onLine', { value: false }); + const id = generateUUID(); + await mutationQueue.enqueue(makeMutation({ id })); + + let called = false; + server.use( + http.post('/api/trips/1/places', () => { + called = true; + return HttpResponse.json({ place: buildPlace({ trip_id: 1 }) }); + }), + ); + + await mutationQueue.flush(); + expect(called).toBe(false); + const m = await offlineDb.mutationQueue.get(id); + expect(m!.status).toBe('pending'); + }); + + it('does nothing when logged out (auth gate closed)', async () => { + setAuthed(false); + const id = generateUUID(); + await mutationQueue.enqueue(makeMutation({ id })); + + let called = false; + server.use( + http.post('/api/trips/1/places', () => { + called = true; + return HttpResponse.json({ place: buildPlace({ trip_id: 1 }) }); + }), + ); + + await mutationQueue.flush(); + expect(called).toBe(false); + const m = await offlineDb.mutationQueue.get(id); + expect(m!.status).toBe('pending'); + }); +}); + +// ── pending / pendingCount ──────────────────────────────────────────────────── + +describe('mutationQueue.pending', () => { + it('returns pending mutations for a trip', async () => { + const id1 = generateUUID(); + const id2 = generateUUID(); + await mutationQueue.enqueue(makeMutation({ id: id1, tripId: 1 })); + await mutationQueue.enqueue(makeMutation({ id: id2, tripId: 2 })); + + const trip1 = await mutationQueue.pending(1); + expect(trip1).toHaveLength(1); + expect(trip1[0].id).toBe(id1); + }); + + it('returns all pending when no tripId given', async () => { + await mutationQueue.enqueue(makeMutation({ id: generateUUID(), tripId: 1 })); + await mutationQueue.enqueue(makeMutation({ id: generateUUID(), tripId: 2 })); + + const all = await mutationQueue.pending(); + expect(all).toHaveLength(2); + }); + + it('excludes failed mutations', async () => { + const id = generateUUID(); + await mutationQueue.enqueue(makeMutation({ id })); + await offlineDb.mutationQueue.update(id, { status: 'failed' }); + + const pending = await mutationQueue.pending(1); + expect(pending).toHaveLength(0); + }); +}); + +describe('mutationQueue.pendingCount', () => { + it('returns zero for empty queue', async () => { + expect(await mutationQueue.pendingCount()).toBe(0); + }); + + it('counts pending and syncing, excludes failed', async () => { + const id1 = generateUUID(); + const id2 = generateUUID(); + const id3 = generateUUID(); + await mutationQueue.enqueue(makeMutation({ id: id1 })); + await mutationQueue.enqueue(makeMutation({ id: id2 })); + await mutationQueue.enqueue(makeMutation({ id: id3 })); + await offlineDb.mutationQueue.update(id3, { status: 'failed' }); + + expect(await mutationQueue.pendingCount()).toBe(2); + }); +}); + +describe('mutationQueue.failedCount', () => { + it('counts only failed mutations (not pending/syncing)', async () => { + const id1 = generateUUID(); + const id2 = generateUUID(); + await mutationQueue.enqueue(makeMutation({ id: id1 })); + await mutationQueue.enqueue(makeMutation({ id: id2 })); + await offlineDb.mutationQueue.update(id2, { status: 'failed' }); + + expect(await mutationQueue.failedCount()).toBe(1); + expect(await mutationQueue.pendingCount()).toBe(1); + }); +}); + +// ── B2: collision-free temp ids ──────────────────────────────────────────────── + +describe('nextTempId (B2)', () => { + it('returns distinct negative ids even within the same millisecond', () => { + mutationQueue._resetFlushing(); + const a = nextTempId(); + const b = nextTempId(); + const c = nextTempId(); + expect(a).toBeLessThan(0); + expect(new Set([a, b, c]).size).toBe(3); + }); + + it('two tight offline creates produce two distinct Dexie rows', async () => { + Object.defineProperty(navigator, 'onLine', { value: false }); + await placeRepo.create(1, { name: 'First' }); + await placeRepo.create(1, { name: 'Second' }); + + const rows = await offlineDb.places.where('trip_id').equals(1).toArray(); + expect(rows).toHaveLength(2); + expect(rows.map(r => r.name).sort()).toEqual(['First', 'Second']); + }); +}); + +// ── B1: temp-id → real-id remapping ───────────────────────────────────────────── + +describe('mutationQueue.flush — temp-id remapping (B1)', () => { + it('rewrites a dependent PUT/DELETE to the real id within one flush', async () => { + const tempId = -1; + await offlineDb.places.put({ ...buildPlace({ trip_id: 1 }), id: tempId }); + + const createId = generateUUID(); + const putId = generateUUID(); + const deleteId = generateUUID(); + + await mutationQueue.enqueue({ + id: createId, tripId: 1, method: 'POST', url: '/trips/1/places', + body: { name: 'Temp' }, resource: 'places', tempId, + }); + await mutationQueue.enqueue({ + id: putId, tripId: 1, method: 'PUT', url: '/trips/1/places/{id}', + body: { name: 'Edited' }, resource: 'places', entityId: tempId, tempEntityId: tempId, + }); + await mutationQueue.enqueue({ + id: deleteId, tripId: 1, method: 'DELETE', url: '/trips/1/places/{id}', + body: undefined, resource: 'places', entityId: tempId, tempEntityId: tempId, + }); + + const putUrls: string[] = []; + const deleteUrls: string[] = []; + server.use( + http.post('/api/trips/1/places', () => HttpResponse.json({ place: buildPlace({ trip_id: 1, id: 42 }) })), + http.put('/api/trips/1/places/:id', ({ params }) => { putUrls.push(String(params.id)); return HttpResponse.json({ place: buildPlace({ trip_id: 1, id: 42, name: 'Edited' }) }); }), + http.delete('/api/trips/1/places/:id', ({ params }) => { deleteUrls.push(String(params.id)); return HttpResponse.json({ success: true }); }), + ); + + await mutationQueue.flush(); + + expect(putUrls).toEqual(['42']); + expect(deleteUrls).toEqual(['42']); + expect(await mutationQueue.pendingCount()).toBe(0); + expect(await mutationQueue.failedCount()).toBe(0); + }); + + it('durably rewrites a still-queued dependent after the CREATE flushes alone', async () => { + const tempId = -7; + await offlineDb.places.put({ ...buildPlace({ trip_id: 1 }), id: tempId }); + + const createId = generateUUID(); + const putId = generateUUID(); + await mutationQueue.enqueue({ + id: createId, tripId: 1, method: 'POST', url: '/trips/1/places', + body: { name: 'Temp' }, resource: 'places', tempId, + }); + await mutationQueue.enqueue({ + id: putId, tripId: 1, method: 'PUT', url: '/trips/1/places/{id}', + body: { name: 'Edited' }, resource: 'places', entityId: tempId, tempEntityId: tempId, + }); + + // Only the CREATE succeeds this round; the PUT errors out (network) and stays queued. + let putAttempts = 0; + server.use( + http.post('/api/trips/1/places', () => HttpResponse.json({ place: buildPlace({ trip_id: 1, id: 88 }) })), + http.put('/api/trips/1/places/:id', () => { putAttempts++; return HttpResponse.error(); }), + ); + + await mutationQueue.flush(); + + const queuedPut = await offlineDb.mutationQueue.get(putId); + expect(queuedPut).toBeDefined(); + expect(queuedPut!.url).toBe('/trips/1/places/88'); + expect(queuedPut!.entityId).toBe(88); + expect(queuedPut!.tempEntityId).toBeUndefined(); + expect(putAttempts).toBeGreaterThanOrEqual(1); + }); + + it('marks an orphaned dependent (placeholder never resolved) as failed', async () => { + const putId = generateUUID(); + await mutationQueue.enqueue({ + id: putId, tripId: 1, method: 'PUT', url: '/trips/1/places/{id}', + body: { name: 'Edited' }, resource: 'places', entityId: -999, tempEntityId: -999, + }); + + await mutationQueue.flush(); + + const m = await offlineDb.mutationQueue.get(putId); + expect(m!.status).toBe('failed'); + }); +}); + +// ── B3: terminal rollback + retryable classification ──────────────────────────── + +describe('mutationQueue.flush — failure handling (B3)', () => { + it('rolls back the phantom optimistic row on a terminal 400 CREATE', async () => { + const tempId = -3; + await offlineDb.places.put({ ...buildPlace({ trip_id: 1 }), id: tempId }); + + const id = generateUUID(); + await mutationQueue.enqueue(makeMutation({ id, tempId })); + + server.use( + http.post('/api/trips/1/places', () => HttpResponse.json({ error: 'Bad' }, { status: 400 })), + ); + + await mutationQueue.flush(); + + expect(await offlineDb.places.get(tempId)).toBeUndefined(); + const m = await offlineDb.mutationQueue.get(id); + expect(m!.status).toBe('failed'); + }); + + it('treats 429 as retryable: resets to pending and stops the flush', async () => { + const id = generateUUID(); + await mutationQueue.enqueue(makeMutation({ id })); + + server.use( + http.post('/api/trips/1/places', () => HttpResponse.json({ error: 'slow down' }, { status: 429 })), + ); + + await mutationQueue.flush(); + + const m = await offlineDb.mutationQueue.get(id); + expect(m!.status).toBe('pending'); + expect(m!.attempts).toBe(1); + expect(await mutationQueue.failedCount()).toBe(0); + }); + + it('treats 401 as retryable rather than dropping the change', async () => { + const id = generateUUID(); + await mutationQueue.enqueue(makeMutation({ id })); + + server.use( + http.post('/api/trips/1/places', () => HttpResponse.json({ error: 'AUTH_REQUIRED' }, { status: 401 })), + ); + + await mutationQueue.flush(); + + const m = await offlineDb.mutationQueue.get(id); + expect(m!.status).toBe('pending'); + }); +}); diff --git a/client/tests/unit/sync/persistentStorage.test.ts b/client/tests/unit/sync/persistentStorage.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..722053f71b335c5ee858695358332e91bf2b1723 --- /dev/null +++ b/client/tests/unit/sync/persistentStorage.test.ts @@ -0,0 +1,47 @@ +/** + * requestPersistentStorage (H8 / M6) — best-effort persistent storage request + * so prefetched tiles / file blobs / IndexedDB aren't evicted under pressure. + */ +import { describe, it, expect, afterEach, vi } from 'vitest'; +import { requestPersistentStorage } from '../../../src/sync/persistentStorage'; + +const original = (navigator as Navigator & { storage?: StorageManager }).storage; + +afterEach(() => { + Object.defineProperty(navigator, 'storage', { value: original, configurable: true }); + vi.restoreAllMocks(); +}); + +function stubStorage(storage: unknown) { + Object.defineProperty(navigator, 'storage', { value: storage, configurable: true }); +} + +describe('requestPersistentStorage', () => { + it('requests persistence when not already granted', async () => { + const persist = vi.fn().mockResolvedValue(true); + const persisted = vi.fn().mockResolvedValue(false); + stubStorage({ persist, persisted }); + + expect(await requestPersistentStorage()).toBe(true); + expect(persist).toHaveBeenCalledOnce(); + }); + + it('skips the prompt when already persisted', async () => { + const persist = vi.fn().mockResolvedValue(true); + const persisted = vi.fn().mockResolvedValue(true); + stubStorage({ persist, persisted }); + + expect(await requestPersistentStorage()).toBe(true); + expect(persist).not.toHaveBeenCalled(); + }); + + it('returns false (no throw) when the API is unavailable', async () => { + stubStorage(undefined); + expect(await requestPersistentStorage()).toBe(false); + }); + + it('returns false (no throw) when persist rejects', async () => { + stubStorage({ persist: vi.fn().mockRejectedValue(new Error('denied')) }); + expect(await requestPersistentStorage()).toBe(false); + }); +}); diff --git a/client/tests/unit/sync/syncTriggers.test.ts b/client/tests/unit/sync/syncTriggers.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..ad3bc2656e1ae877d37d5aa3fb58e94011bc5e89 --- /dev/null +++ b/client/tests/unit/sync/syncTriggers.test.ts @@ -0,0 +1,76 @@ +/** + * syncTriggers — reconnect/online wiring (H1). + * + * Verifies the previously-dead refetch path is wired: on WS reconnect and on the + * `online` event the active trip's store is re-hydrated (after the queue flush). + */ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; + +const flush = vi.fn(() => Promise.resolve()); +const syncAll = vi.fn(() => Promise.resolve()); +const hydrate = vi.fn(() => Promise.resolve()); + +let refetchCb: ((tripId: string) => void) | null = null; +let preReconnect: (() => Promise) | null = null; + +vi.mock('../../../src/sync/mutationQueue', () => ({ + mutationQueue: { flush: () => flush() }, +})); +vi.mock('../../../src/sync/tripSyncManager', () => ({ + tripSyncManager: { syncAll: () => syncAll() }, +})); +vi.mock('../../../src/api/websocket', () => ({ + setPreReconnectHook: (fn: (() => Promise) | null) => { preReconnect = fn; }, + setRefetchCallback: (fn: ((tripId: string) => void) | null) => { refetchCb = fn; }, + getActiveTrips: () => ['7'], +})); +vi.mock('../../../src/store/tripStore', () => ({ + useTripStore: { getState: () => ({ hydrateActiveTrip: hydrate }) }, +})); + +import { registerSyncTriggers, unregisterSyncTriggers } from '../../../src/sync/syncTriggers'; + +const flushMicrotasks = async () => { + for (let i = 0; i < 5; i++) await Promise.resolve(); +}; + +beforeEach(() => { + flush.mockClear(); syncAll.mockClear(); hydrate.mockClear(); + refetchCb = null; preReconnect = null; + Object.defineProperty(navigator, 'onLine', { value: true, writable: true, configurable: true }); +}); + +afterEach(() => { + unregisterSyncTriggers(); +}); + +describe('syncTriggers', () => { + it('registers a refetch callback that hydrates the active trip', () => { + registerSyncTriggers(); + expect(refetchCb).toBeTypeOf('function'); + refetchCb!('7'); + expect(hydrate).toHaveBeenCalledWith('7'); + }); + + it('also registers the pre-reconnect flush hook', () => { + registerSyncTriggers(); + expect(preReconnect).toBeTypeOf('function'); + }); + + it('clears both reconnect hooks on unregister', () => { + registerSyncTriggers(); + unregisterSyncTriggers(); + expect(refetchCb).toBeNull(); + expect(preReconnect).toBeNull(); + }); + + it('online event flushes, then re-seeds Dexie and re-hydrates active trips', async () => { + registerSyncTriggers(); + window.dispatchEvent(new Event('online')); + await flushMicrotasks(); + + expect(flush).toHaveBeenCalled(); + expect(syncAll).toHaveBeenCalled(); + expect(hydrate).toHaveBeenCalledWith('7'); + }); +}); diff --git a/client/tests/unit/sync/tilePrefetcher.test.ts b/client/tests/unit/sync/tilePrefetcher.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..768283850ab62c97bf0f83c0f9e5f2687a9a5b62 --- /dev/null +++ b/client/tests/unit/sync/tilePrefetcher.test.ts @@ -0,0 +1,248 @@ +/** + * tilePrefetcher unit tests. + * + * Covers: bbox computation, tile math, URL building, size guard, + * offline/no-SW guard, syncMeta update. + */ +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; +import 'fake-indexeddb/auto'; +import { + computeBbox, + lngToTileX, + latToTileY, + buildTileUrl, + countTiles, + prefetchTiles, + prefetchTilesForTrip, + MAX_TILES, + type TileBbox, +} from '../../../src/sync/tilePrefetcher'; +import { offlineDb, clearAll, upsertSyncMeta } from '../../../src/db/offlineDb'; +import { buildPlace } from '../../helpers/factories'; + +beforeEach(async () => { + await clearAll(); + Object.defineProperty(navigator, 'onLine', { value: true, writable: true, configurable: true }); + // Stub fetch + serviceWorker so prefetch path is exercised + vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ ok: true })); + Object.defineProperty(navigator, 'serviceWorker', { + value: { controller: {} }, + writable: true, + configurable: true, + }); +}); + +afterEach(() => { + vi.restoreAllMocks(); + vi.unstubAllGlobals(); +}); + +// ── bbox computation ────────────────────────────────────────────────────────── + +describe('computeBbox', () => { + it('returns null when no places have coordinates', () => { + const places = [buildPlace({ lat: null, lng: null })]; + expect(computeBbox(places)).toBeNull(); + }); + + it('expands single-point bbox to at least 0.1° span', () => { + const place = buildPlace({ lat: 48.8566, lng: 2.3522 }); + const bbox = computeBbox([place])!; + expect(bbox.maxLat - bbox.minLat).toBeGreaterThan(0.09); + expect(bbox.maxLng - bbox.minLng).toBeGreaterThan(0.09); + }); + + it('computes multi-point bbox with padding', () => { + const places = [ + buildPlace({ lat: 48.8566, lng: 2.3522 }), // Paris + buildPlace({ lat: 51.5074, lng: -0.1278 }), // London + ]; + const bbox = computeBbox(places, 0.1)!; + // Padded bbox should extend beyond raw points + expect(bbox.minLat).toBeLessThan(48.8566); + expect(bbox.maxLat).toBeGreaterThan(51.5074); + expect(bbox.minLng).toBeLessThan(-0.1278); + expect(bbox.maxLng).toBeGreaterThan(2.3522); + }); + + it('clamps to valid Mercator lat bounds', () => { + const places = [buildPlace({ lat: 85.0, lng: 0 })]; + const bbox = computeBbox(places, 0.5)!; + expect(bbox.maxLat).toBeLessThanOrEqual(85.0511); + }); +}); + +// ── tile math ───────────────────────────────────────────────────────────────── + +describe('lngToTileX', () => { + it('returns 0 for lng=-180 at any zoom', () => { + expect(lngToTileX(-180, 10)).toBe(0); + }); + + it('returns max tile for lng=180 at zoom 1', () => { + // At zoom 1: 2^1 = 2 tiles, lng=180 → x = floor(360/360 * 2) = floor(2) = 2 + // But tile range is 0..1, so this is the "overflow" edge — that's fine + expect(lngToTileX(180, 1)).toBe(2); + }); + + it('increases with more easterly longitude', () => { + const x1 = lngToTileX(0, 10); + const x2 = lngToTileX(10, 10); + expect(x2).toBeGreaterThan(x1); + }); +}); + +describe('latToTileY', () => { + it('returns smaller y for higher latitude (north = top)', () => { + const yNorth = latToTileY(60, 10); + const ySouth = latToTileY(10, 10); + expect(yNorth).toBeLessThan(ySouth); + }); + + it('equator is roughly half the tile grid', () => { + const yEq = latToTileY(0, 1); + // zoom 1 → 2 rows, equator ≈ row 1 + expect(yEq).toBe(1); + }); +}); + +// ── URL building ─────────────────────────────────────────────────────────────── + +describe('buildTileUrl', () => { + it('replaces {z}, {x}, {y}, {r} correctly', () => { + const tmpl = 'https://tile.example.com/{z}/{x}/{y}.png'; + const url = buildTileUrl(tmpl, 10, 500, 300); + expect(url).toBe('https://tile.example.com/10/500/300.png'); + }); + + it('replaces {s} with a subdomain character', () => { + const tmpl = 'https://{s}.tiles.example.com/{z}/{x}/{y}.png'; + const url = buildTileUrl(tmpl, 10, 0, 0); + expect(url).toMatch(/^https:\/\/[abcd]\.tiles\.example\.com\/10\/0\/0\.png$/); + }); + + it('removes {r} (retina placeholder)', () => { + const tmpl = 'https://tiles.example.com/{z}/{x}/{y}{r}.png'; + const url = buildTileUrl(tmpl, 10, 0, 0); + expect(url).toBe('https://tiles.example.com/10/0/0.png'); + }); +}); + +// ── countTiles ──────────────────────────────────────────────────────────────── + +describe('countTiles', () => { + it('returns more tiles at higher zoom levels', () => { + const bbox: TileBbox = { minLat: 48.7, maxLat: 49.0, minLng: 2.2, maxLng: 2.5 }; + const low = countTiles(bbox, 10, 10); + const high = countTiles(bbox, 12, 12); + expect(high).toBeGreaterThan(low); + }); + + it('stops counting after exceeding MAX_TILES', () => { + // Very large bbox — should hit cap quickly at high zooms + const bbox: TileBbox = { minLat: -60, maxLat: 60, minLng: -180, maxLng: 180 }; + const count = countTiles(bbox, 10, 16); + expect(count).toBeGreaterThan(MAX_TILES); + }); +}); + +// ── prefetchTiles guards ─────────────────────────────────────────────────────── + +describe('prefetchTiles — offline guard', () => { + it('returns 0 and does not fetch when offline', async () => { + Object.defineProperty(navigator, 'onLine', { value: false }); + const bbox: TileBbox = { minLat: 48.8, maxLat: 48.9, minLng: 2.3, maxLng: 2.4 }; + const count = await prefetchTiles(bbox, 'https://{s}.example.com/{z}/{x}/{y}.png', 10, 10); + expect(count).toBe(0); + expect(vi.mocked(fetch)).not.toHaveBeenCalled(); + }); + + it('returns 0 when no service worker controller', async () => { + Object.defineProperty(navigator, 'serviceWorker', { + value: { controller: null }, + configurable: true, + }); + const bbox: TileBbox = { minLat: 48.8, maxLat: 48.9, minLng: 2.3, maxLng: 2.4 }; + const count = await prefetchTiles(bbox, 'https://{s}.example.com/{z}/{x}/{y}.png', 10, 10); + expect(count).toBe(0); + }); +}); + +describe('prefetchTiles — normal operation', () => { + it('fetches tiles and returns count', async () => { + const bbox: TileBbox = { minLat: 48.84, maxLat: 48.87, minLng: 2.33, maxLng: 2.37 }; + const count = await prefetchTiles(bbox, 'https://{s}.example.com/{z}/{x}/{y}.png', 10, 11); + expect(count).toBeGreaterThan(0); + expect(vi.mocked(fetch)).toHaveBeenCalled(); + }); + + it('stops at zoom level where cap is exceeded', async () => { + // Use a very small MAX_TILES override by using a huge bbox + const bbox: TileBbox = { minLat: -80, maxLat: 80, minLng: -170, maxLng: 170 }; + // This bbox at zoom 10 alone has thousands of tiles — should trigger early stop + const count = await prefetchTiles(bbox, 'https://{s}.example.com/{z}/{x}/{y}.png', 10, 16); + expect(count).toBeLessThanOrEqual(MAX_TILES); + }); +}); + +// ── prefetchTilesForTrip ────────────────────────────────────────────────────── + +describe('prefetchTilesForTrip', () => { + it('no-ops when no places have coordinates', async () => { + const places = [buildPlace({ lat: null, lng: null })]; + await prefetchTilesForTrip(1, places); + expect(vi.mocked(fetch)).not.toHaveBeenCalled(); + }); + + it('updates syncMeta tilesBbox after prefetch', async () => { + await upsertSyncMeta({ tripId: 1, lastSyncedAt: Date.now(), status: 'idle', tilesBbox: null, filesCachedCount: 0 }); + + const places = [ + buildPlace({ trip_id: 1, lat: 48.8566, lng: 2.3522 }), + ]; + await prefetchTilesForTrip(1, places, 'https://{s}.example.com/{z}/{x}/{y}.png'); + + const meta = await offlineDb.syncMeta.get(1); + expect(meta!.tilesBbox).not.toBeNull(); + expect(meta!.tilesBbox).toHaveLength(4); + }); + + it('zoom-clamps instead of skipping when the bbox exceeds MAX_TILES', async () => { + await upsertSyncMeta({ tripId: 1, lastSyncedAt: Date.now(), status: 'idle', tilesBbox: null, filesCachedCount: 0 }); + + // ~4° road-trip span: low zooms fit the budget, high zooms (z14+) blow past + // it. The old guard skipped the whole trip; now we keep what fits. + const places = [ + buildPlace({ trip_id: 1, lat: 45.0, lng: 0.0 }), + buildPlace({ trip_id: 1, lat: 49.0, lng: 4.0 }), + ]; + await prefetchTilesForTrip(1, places, 'https://{s}.example.com/{z}/{x}/{y}.png'); + + // Previously this skipped entirely; now it prefetches a clamped subset. + const calls = vi.mocked(fetch).mock.calls.length; + expect(calls).toBeGreaterThan(0); + expect(calls).toBeLessThanOrEqual(MAX_TILES); + }); + + it('prefetches a region-sized (0.5°) trip that the old all-or-nothing guard would have skipped', async () => { + await upsertSyncMeta({ tripId: 1, lastSyncedAt: Date.now(), status: 'idle', tilesBbox: null, filesCachedCount: 0 }); + + const places = [ + buildPlace({ trip_id: 1, lat: 48.6, lng: 2.1 }), + buildPlace({ trip_id: 1, lat: 49.1, lng: 2.6 }), + ]; + await prefetchTilesForTrip(1, places, 'https://{s}.example.com/{z}/{x}/{y}.png'); + + const calls = vi.mocked(fetch).mock.calls.length; + expect(calls).toBeGreaterThan(0); + expect(calls).toBeLessThanOrEqual(MAX_TILES); + }); +}); + +// ── cap coherence ─────────────────────────────────────────────────────────────── + +describe('MAX_TILES budget', () => { + it('matches the Workbox map-tiles maxEntries in vite.config.js (drift guard)', () => { + expect(MAX_TILES).toBe(12288); + }); +}); diff --git a/client/tests/unit/sync/tripSyncManager.test.ts b/client/tests/unit/sync/tripSyncManager.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..46ff38291ed8842c6100532921f69300d2a0873d --- /dev/null +++ b/client/tests/unit/sync/tripSyncManager.test.ts @@ -0,0 +1,285 @@ +/** + * tripSyncManager unit tests. + * + * Covers: trip filtering (shouldCache/isStale), bundle fetch → Dexie upsert, + * stale trip eviction, offline guard, file blob caching. + */ +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; +import 'fake-indexeddb/auto'; +import { server } from '../../helpers/msw/server'; +import { http, HttpResponse } from 'msw'; +import { tripSyncManager } from '../../../src/sync/tripSyncManager'; +import { setAuthed } from '../../../src/sync/authGate'; +import { offlineDb, clearAll, upsertTrip } from '../../../src/db/offlineDb'; +import { + buildTrip, + buildDay, + buildPlace, + buildPackingItem, + buildTodoItem, + buildBudgetItem, + buildReservation, + buildTripFile, +} from '../../helpers/factories'; + +// Helper to get today ± N days as YYYY-MM-DD +function dateOffset(days: number): string { + const d = new Date(); + d.setDate(d.getDate() + days); + return d.toISOString().slice(0, 10); +} + +function makeBundle(tripId: number) { + const trip = buildTrip({ id: tripId, end_date: dateOffset(3) }); + return { + trip, + days: [buildDay({ trip_id: tripId, assignments: [], notes_items: [] })], + places: [buildPlace({ trip_id: tripId })], + packingItems: [buildPackingItem({ trip_id: tripId })], + todoItems: [buildTodoItem({ trip_id: tripId })], + budgetItems: [buildBudgetItem({ trip_id: tripId })], + reservations: [buildReservation({ trip_id: tripId })], + files: [buildTripFile({ trip_id: tripId, url: `/api/trips/${tripId}/files/99/download`, mime_type: 'application/pdf' })], + }; +} + +beforeEach(async () => { + await clearAll(); + tripSyncManager._resetSyncing(); + setAuthed(true); + Object.defineProperty(navigator, 'onLine', { value: true, writable: true, configurable: true }); + // Stub fetch for blob caching (used by cacheFilesForTrip) + vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ + ok: true, + blob: async () => new Blob(['data'], { type: 'application/pdf' }), + })); +}); + +afterEach(() => { + vi.restoreAllMocks(); + vi.unstubAllGlobals(); + setAuthed(false); +}); + +describe('tripSyncManager.syncAll — auth gate (B4)', () => { + it('no-ops when logged out (gate closed)', async () => { + setAuthed(false); + let called = false; + server.use( + http.get('/api/trips', () => { called = true; return HttpResponse.json({ trips: [] }); }), + ); + await tripSyncManager.syncAll(); + expect(called).toBe(false); + }); +}); + +// ── offline guard ───────────────────────────────────────────────────────────── + +describe('tripSyncManager.syncAll — offline guard', () => { + it('does nothing when offline', async () => { + Object.defineProperty(navigator, 'onLine', { value: false }); + + let listed = false; + server.use( + http.get('/api/trips', () => { listed = true; return HttpResponse.json({ trips: [] }); }), + ); + + await tripSyncManager.syncAll(); + expect(listed).toBe(false); + }); +}); + +// ── trip filtering ───────────────────────────────────────────────────────────── + +describe('tripSyncManager.syncAll — trip filtering', () => { + it('caches ongoing trips (end_date >= today)', async () => { + const tripId = 100; + const bundle = makeBundle(tripId); + + server.use( + http.get('/api/trips', () => + HttpResponse.json({ trips: [buildTrip({ id: tripId, end_date: dateOffset(2) })] }), + ), + http.get(`/api/trips/${tripId}/bundle`, () => HttpResponse.json(bundle)), + ); + + await tripSyncManager.syncAll(); + + const cached = await offlineDb.trips.get(tripId); + expect(cached).toBeDefined(); + expect(cached!.id).toBe(tripId); + }); + + it('caches trips with no end_date', async () => { + const tripId = 101; + const bundle = makeBundle(tripId); + const trip = buildTrip({ id: tripId, end_date: null as unknown as string }); + + server.use( + http.get('/api/trips', () => HttpResponse.json({ trips: [trip] })), + http.get(`/api/trips/${tripId}/bundle`, () => HttpResponse.json({ ...bundle, trip })), + ); + + await tripSyncManager.syncAll(); + expect(await offlineDb.trips.get(tripId)).toBeDefined(); + }); + + it('does not cache past trips (end_date < today)', async () => { + const tripId = 102; + + server.use( + http.get('/api/trips', () => + HttpResponse.json({ trips: [buildTrip({ id: tripId, end_date: dateOffset(-1) })] }), + ), + ); + + // Bundle should NOT be called for past trips + let bundleCalled = false; + server.use( + http.get(`/api/trips/${tripId}/bundle`, () => { + bundleCalled = true; + return HttpResponse.json({}); + }), + ); + + await tripSyncManager.syncAll(); + expect(bundleCalled).toBe(false); + expect(await offlineDb.trips.get(tripId)).toBeUndefined(); + }); +}); + +// ── stale eviction ───────────────────────────────────────────────────────────── + +describe('tripSyncManager.syncAll — stale eviction', () => { + it('evicts trips that ended more than 7 days ago', async () => { + const staleId = 200; + // Seed Dexie as if previously cached + await upsertTrip(buildTrip({ id: staleId, end_date: dateOffset(-8) })); + + server.use( + http.get('/api/trips', () => + HttpResponse.json({ trips: [buildTrip({ id: staleId, end_date: dateOffset(-8) })] }), + ), + ); + + await tripSyncManager.syncAll(); + expect(await offlineDb.trips.get(staleId)).toBeUndefined(); + }); + + it('does NOT evict trips that ended exactly 6 days ago', async () => { + const recentId = 201; + const bundle = makeBundle(recentId); + const trip = buildTrip({ id: recentId, end_date: dateOffset(-6) }); + + server.use( + http.get('/api/trips', () => HttpResponse.json({ trips: [trip] })), + http.get(`/api/trips/${recentId}/bundle`, () => HttpResponse.json({ ...bundle, trip })), + ); + + await tripSyncManager.syncAll(); + // end_date = -6 days: still within 7d window, but < today so not cached + // i.e., shouldCache is false (end_date < today) so won't be fetched + // but also isStale is false (end_date = -6 >= cutoff -7), so won't be evicted + // → trip should simply not appear in Dexie (not cached, not evicted pre-seeded data) + expect(await offlineDb.trips.get(recentId)).toBeUndefined(); + }); +}); + +// ── bundle upsert ────────────────────────────────────────────────────────────── + +describe('tripSyncManager.syncAll — bundle upsert', () => { + it('writes all bundle entities to Dexie', async () => { + const tripId = 300; + const bundle = makeBundle(tripId); + + server.use( + http.get('/api/trips', () => + HttpResponse.json({ trips: [buildTrip({ id: tripId, end_date: dateOffset(5) })] }), + ), + http.get(`/api/trips/${tripId}/bundle`, () => HttpResponse.json(bundle)), + ); + + await tripSyncManager.syncAll(); + + expect(await offlineDb.trips.get(tripId)).toBeDefined(); + expect(await offlineDb.days.where('trip_id').equals(tripId).count()).toBe(1); + expect(await offlineDb.places.where('trip_id').equals(tripId).count()).toBe(1); + expect(await offlineDb.packingItems.where('trip_id').equals(tripId).count()).toBe(1); + expect(await offlineDb.todoItems.where('trip_id').equals(tripId).count()).toBe(1); + expect(await offlineDb.budgetItems.where('trip_id').equals(tripId).count()).toBe(1); + expect(await offlineDb.reservations.where('trip_id').equals(tripId).count()).toBe(1); + expect(await offlineDb.tripFiles.where('trip_id').equals(tripId).count()).toBe(1); + }); + + it('writes syncMeta with lastSyncedAt', async () => { + const tripId = 301; + const bundle = makeBundle(tripId); + + server.use( + http.get('/api/trips', () => + HttpResponse.json({ trips: [buildTrip({ id: tripId, end_date: dateOffset(5) })] }), + ), + http.get(`/api/trips/${tripId}/bundle`, () => HttpResponse.json(bundle)), + ); + + const before = Date.now(); + await tripSyncManager.syncAll(); + const after = Date.now(); + + const meta = await offlineDb.syncMeta.get(tripId); + expect(meta).toBeDefined(); + expect(meta!.lastSyncedAt).toBeGreaterThanOrEqual(before); + expect(meta!.lastSyncedAt).toBeLessThanOrEqual(after); + }); +}); + +// ── file blob caching ────────────────────────────────────────────────────────── + +describe('tripSyncManager — file blob caching', () => { + it('caches non-photo files after bundle sync', async () => { + const tripId = 400; + const bundle = makeBundle(tripId); + + server.use( + http.get('/api/trips', () => + HttpResponse.json({ trips: [buildTrip({ id: tripId, end_date: dateOffset(5) })] }), + ), + http.get(`/api/trips/${tripId}/bundle`, () => HttpResponse.json(bundle)), + ); + + await tripSyncManager.syncAll(); + + // Give fire-and-forget a tick + await new Promise(r => setTimeout(r, 50)); + + const cached = await offlineDb.blobCache.toArray(); + expect(cached.length).toBeGreaterThan(0); + expect(cached[0].url).toContain('/download'); + }); + + it('does not cache photo files (image/* MIME)', async () => { + const tripId = 401; + const photoFile = buildTripFile({ + trip_id: tripId, + mime_type: 'image/jpeg', + url: `/api/trips/${tripId}/files/77/download`, + }); + const bundle = { + ...makeBundle(tripId), + files: [photoFile], + }; + + server.use( + http.get('/api/trips', () => + HttpResponse.json({ trips: [buildTrip({ id: tripId, end_date: dateOffset(5) })] }), + ), + http.get(`/api/trips/${tripId}/bundle`, () => HttpResponse.json(bundle)), + ); + + await tripSyncManager.syncAll(); + await new Promise(r => setTimeout(r, 50)); + + const cached = await offlineDb.blobCache.toArray(); + expect(cached.length).toBe(0); + }); +}); diff --git a/client/tests/unit/tripStore.test.ts b/client/tests/unit/tripStore.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..8f8259a26d7469a6b4292f94a30d54c39174dc79 --- /dev/null +++ b/client/tests/unit/tripStore.test.ts @@ -0,0 +1,372 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { http, HttpResponse } from 'msw'; +import { useTripStore } from '../../src/store/tripStore'; +import { resetAllStores } from '../helpers/store'; +import { buildTrip, buildDay, buildPlace, buildPackingItem, buildTodoItem, buildTag, buildCategory, buildAssignment, buildDayNote, buildBudgetItem, buildReservation, buildTripFile } from '../helpers/factories'; +import { server } from '../helpers/msw/server'; + +vi.mock('../../src/api/websocket', () => ({ + connect: vi.fn(), + disconnect: vi.fn(), + getSocketId: vi.fn(() => null), + joinTrip: vi.fn(), + leaveTrip: vi.fn(), + addListener: vi.fn(), + removeListener: vi.fn(), + setRefetchCallback: vi.fn(), + setPreReconnectHook: vi.fn(), +})); + +beforeEach(() => { + resetAllStores(); +}); + +/** Full set of MSW handlers for one trip's loadTrip fan-out. */ +function tripHandlers( + id: number, + data: { + budget?: unknown[]; reservations?: unknown[]; files?: unknown[]; + tags?: unknown[]; categories?: unknown[]; + }, +) { + return [ + http.get(`/api/trips/${id}`, () => HttpResponse.json({ trip: buildTrip({ id }) })), + http.get(`/api/trips/${id}/days`, () => HttpResponse.json({ days: [] })), + http.get(`/api/trips/${id}/places`, () => HttpResponse.json({ places: [] })), + http.get(`/api/trips/${id}/packing`, () => HttpResponse.json({ items: [] })), + http.get(`/api/trips/${id}/todo`, () => HttpResponse.json({ items: [] })), + http.get(`/api/trips/${id}/budget`, () => HttpResponse.json({ items: data.budget ?? [] })), + http.get(`/api/trips/${id}/reservations`, () => HttpResponse.json({ reservations: data.reservations ?? [] })), + http.get(`/api/trips/${id}/files`, () => HttpResponse.json({ files: data.files ?? [] })), + http.get('/api/tags', () => HttpResponse.json({ tags: data.tags ?? [] })), + http.get('/api/categories', () => HttpResponse.json({ categories: data.categories ?? [] })), + ]; +} + +describe('tripStore', () => { + describe('loadTrip', () => { + it('FE-TRIP-001: fires parallel API calls for trips, days, places, packing, todo, tags, categories', async () => { + const calledUrls: string[] = []; + server.use( + http.get('/api/trips/:id', ({ params }) => { + calledUrls.push(`/api/trips/${params.id}`); + return HttpResponse.json({ trip: buildTrip({ id: Number(params.id) }) }); + }), + http.get('/api/trips/:id/days', ({ params }) => { + calledUrls.push(`/api/trips/${params.id}/days`); + return HttpResponse.json({ days: [] }); + }), + http.get('/api/trips/:id/places', ({ params }) => { + calledUrls.push(`/api/trips/${params.id}/places`); + return HttpResponse.json({ places: [] }); + }), + http.get('/api/trips/:id/packing', ({ params }) => { + calledUrls.push(`/api/trips/${params.id}/packing`); + return HttpResponse.json({ items: [] }); + }), + http.get('/api/trips/:id/todo', ({ params }) => { + calledUrls.push(`/api/trips/${params.id}/todo`); + return HttpResponse.json({ items: [] }); + }), + http.get('/api/tags', () => { + calledUrls.push('/api/tags'); + return HttpResponse.json({ tags: [] }); + }), + http.get('/api/categories', () => { + calledUrls.push('/api/categories'); + return HttpResponse.json({ categories: [] }); + }), + ); + + await useTripStore.getState().loadTrip(1); + + expect(calledUrls).toContain('/api/trips/1'); + expect(calledUrls).toContain('/api/trips/1/days'); + expect(calledUrls).toContain('/api/trips/1/places'); + expect(calledUrls).toContain('/api/trips/1/packing'); + expect(calledUrls).toContain('/api/trips/1/todo'); + expect(calledUrls).toContain('/api/tags'); + expect(calledUrls).toContain('/api/categories'); + }); + + it('FE-TRIP-002: after loadTrip, all store fields are populated', async () => { + const trip = buildTrip({ id: 1 }); + const place = buildPlace({ trip_id: 1 }); + const packingItem = buildPackingItem({ trip_id: 1 }); + const todoItem = buildTodoItem({ trip_id: 1 }); + const tag = buildTag(); + const category = buildCategory(); + + server.use( + http.get('/api/trips/1', () => HttpResponse.json({ trip })), + http.get('/api/trips/1/days', () => HttpResponse.json({ days: [] })), + http.get('/api/trips/1/places', () => HttpResponse.json({ places: [place] })), + http.get('/api/trips/1/packing', () => HttpResponse.json({ items: [packingItem] })), + http.get('/api/trips/1/todo', () => HttpResponse.json({ items: [todoItem] })), + http.get('/api/tags', () => HttpResponse.json({ tags: [tag] })), + http.get('/api/categories', () => HttpResponse.json({ categories: [category] })), + ); + + await useTripStore.getState().loadTrip(1); + const state = useTripStore.getState(); + + expect(state.trip).toEqual(trip); + expect(state.places).toEqual([place]); + expect(state.packingItems).toEqual([packingItem]); + expect(state.todoItems).toEqual([todoItem]); + expect(state.tags).toEqual([tag]); + expect(state.categories).toEqual([category]); + }); + + it('FE-TRIP-003: loadTrip extracts assignments map from days response', async () => { + const assignment = buildAssignment({ day_id: 10, order_index: 0 }); + const day = buildDay({ id: 10, assignments: [assignment], notes_items: [] }); + + server.use( + http.get('/api/trips/1', () => HttpResponse.json({ trip: buildTrip({ id: 1 }) })), + http.get('/api/trips/1/days', () => HttpResponse.json({ days: [day] })), + http.get('/api/trips/1/places', () => HttpResponse.json({ places: [] })), + http.get('/api/trips/1/packing', () => HttpResponse.json({ items: [] })), + http.get('/api/trips/1/todo', () => HttpResponse.json({ items: [] })), + http.get('/api/tags', () => HttpResponse.json({ tags: [] })), + http.get('/api/categories', () => HttpResponse.json({ categories: [] })), + ); + + await useTripStore.getState().loadTrip(1); + const { assignments } = useTripStore.getState(); + + expect(assignments['10']).toBeDefined(); + expect(assignments['10']).toEqual([assignment]); + }); + + it('FE-TRIP-004: loadTrip extracts dayNotes map from days response', async () => { + const note = buildDayNote({ day_id: 10 }); + const day = buildDay({ id: 10, assignments: [], notes_items: [note] }); + + server.use( + http.get('/api/trips/1', () => HttpResponse.json({ trip: buildTrip({ id: 1 }) })), + http.get('/api/trips/1/days', () => HttpResponse.json({ days: [day] })), + http.get('/api/trips/1/places', () => HttpResponse.json({ places: [] })), + http.get('/api/trips/1/packing', () => HttpResponse.json({ items: [] })), + http.get('/api/trips/1/todo', () => HttpResponse.json({ items: [] })), + http.get('/api/tags', () => HttpResponse.json({ tags: [] })), + http.get('/api/categories', () => HttpResponse.json({ categories: [] })), + ); + + await useTripStore.getState().loadTrip(1); + const { dayNotes } = useTripStore.getState(); + + expect(dayNotes['10']).toBeDefined(); + expect(dayNotes['10']).toEqual([note]); + }); + + it('FE-TRIP-005: loadTrip sets isLoading true during, false after', async () => { + let wasLoadingDuringFetch = false; + + server.use( + http.get('/api/trips/1', () => { + wasLoadingDuringFetch = useTripStore.getState().isLoading; + return HttpResponse.json({ trip: buildTrip({ id: 1 }) }); + }), + http.get('/api/trips/1/days', () => HttpResponse.json({ days: [] })), + http.get('/api/trips/1/places', () => HttpResponse.json({ places: [] })), + http.get('/api/trips/1/packing', () => HttpResponse.json({ items: [] })), + http.get('/api/trips/1/todo', () => HttpResponse.json({ items: [] })), + http.get('/api/tags', () => HttpResponse.json({ tags: [] })), + http.get('/api/categories', () => HttpResponse.json({ categories: [] })), + ); + + const promise = useTripStore.getState().loadTrip(1); + expect(useTripStore.getState().isLoading).toBe(true); + await promise; + expect(wasLoadingDuringFetch).toBe(true); + expect(useTripStore.getState().isLoading).toBe(false); + }); + + it('FE-TRIP-006: loadTrip on API failure sets error and isLoading: false', async () => { + server.use( + http.get('/api/trips/1', () => HttpResponse.json({ message: 'Not found' }, { status: 404 })), + http.get('/api/trips/1/days', () => HttpResponse.json({ days: [] })), + http.get('/api/trips/1/places', () => HttpResponse.json({ places: [] })), + http.get('/api/trips/1/packing', () => HttpResponse.json({ items: [] })), + http.get('/api/trips/1/todo', () => HttpResponse.json({ items: [] })), + http.get('/api/tags', () => HttpResponse.json({ tags: [] })), + http.get('/api/categories', () => HttpResponse.json({ categories: [] })), + ); + + await expect(useTripStore.getState().loadTrip(1)).rejects.toThrow(); + + const state = useTripStore.getState(); + expect(state.isLoading).toBe(false); + expect(state.error).not.toBeNull(); + }); + + it('FE-TRIP-H5: loadTrip uniformly hydrates budget, reservations and files', async () => { + const budgetItem = buildBudgetItem({ trip_id: 1 }); + const reservation = buildReservation({ trip_id: 1 }); + const file = buildTripFile({ trip_id: 1 }); + server.use(...tripHandlers(1, { budget: [budgetItem], reservations: [reservation], files: [file] })); + + await useTripStore.getState().loadTrip(1); + const state = useTripStore.getState(); + + expect(state.budgetItems).toEqual([budgetItem]); + expect(state.reservations).toEqual([reservation]); + expect(state.files).toEqual([file]); + }); + + it('FE-TRIP-H4: switching trips does not leak budget/reservations/files from the previous trip', async () => { + // Trip 1 has budget/reservations/files; trip 2 has none. + server.use(...tripHandlers(1, { + budget: [buildBudgetItem({ trip_id: 1 })], + reservations: [buildReservation({ trip_id: 1 })], + files: [buildTripFile({ trip_id: 1 })], + })); + await useTripStore.getState().loadTrip(1); + expect(useTripStore.getState().budgetItems).toHaveLength(1); + + server.use(...tripHandlers(2, {})); + await useTripStore.getState().loadTrip(2); + const state = useTripStore.getState(); + + expect(state.trip!.id).toBe(2); + expect(state.budgetItems).toEqual([]); + expect(state.reservations).toEqual([]); + expect(state.files).toEqual([]); + }); + + it('FE-TRIP-H4b: resetTrip clears every trip-scoped slice but keeps tags/categories', async () => { + server.use(...tripHandlers(1, { + budget: [buildBudgetItem({ trip_id: 1 })], + reservations: [buildReservation({ trip_id: 1 })], + files: [buildTripFile({ trip_id: 1 })], + tags: [buildTag()], + })); + await useTripStore.getState().loadTrip(1); + expect(useTripStore.getState().budgetItems).toHaveLength(1); + + useTripStore.getState().resetTrip(); + const state = useTripStore.getState(); + + expect(state.trip).toBeNull(); + expect(state.places).toEqual([]); + expect(state.budgetItems).toEqual([]); + expect(state.reservations).toEqual([]); + expect(state.files).toEqual([]); + expect(state.selectedDayId).toBeNull(); + // Global lookups survive a trip reset. + expect(state.tags).toHaveLength(1); + }); + }); + + describe('hydrateActiveTrip', () => { + const loadHandlers = (places: unknown[] = [], budget: unknown[] = []) => [ + http.get('/api/trips/1', () => HttpResponse.json({ trip: buildTrip({ id: 1 }) })), + http.get('/api/trips/1/days', () => HttpResponse.json({ days: [] })), + http.get('/api/trips/1/places', () => HttpResponse.json({ places })), + http.get('/api/trips/1/packing', () => HttpResponse.json({ items: [] })), + http.get('/api/trips/1/todo', () => HttpResponse.json({ items: [] })), + http.get('/api/trips/1/budget', () => HttpResponse.json({ items: budget })), + http.get('/api/trips/1/reservations', () => HttpResponse.json({ reservations: [] })), + http.get('/api/trips/1/files', () => HttpResponse.json({ files: [] })), + http.get('/api/tags', () => HttpResponse.json({ tags: [] })), + http.get('/api/categories', () => HttpResponse.json({ categories: [] })), + ]; + + it('FE-TRIP-H1: silently refreshes resources without resetting or splashing', async () => { + server.use(...loadHandlers()); + await useTripStore.getState().loadTrip(1); + expect(useTripStore.getState().trip!.id).toBe(1); + + // New collaborative state arrives (as if edited by someone while we were offline). + const place = buildPlace({ trip_id: 1 }); + const budgetItem = buildBudgetItem({ trip_id: 1 }); + server.use(...loadHandlers([place], [budgetItem])); + + await useTripStore.getState().hydrateActiveTrip(1); + const state = useTripStore.getState(); + + expect(state.places).toEqual([place]); + expect(state.budgetItems).toEqual([budgetItem]); + expect(state.trip!.id).toBe(1); // trip not reset + expect(state.isLoading).toBe(false); // no splash toggled + }); + }); + + describe('refreshDays', () => { + it('FE-TRIP-007: refreshDays re-fetches days and rebuilds assignments/dayNotes maps', async () => { + const assignment = buildAssignment({ day_id: 20, order_index: 0 }); + const note = buildDayNote({ day_id: 20 }); + const day = buildDay({ id: 20, assignments: [assignment], notes_items: [note] }); + + server.use( + http.get('/api/trips/1/days', () => HttpResponse.json({ days: [day] })), + ); + + await useTripStore.getState().refreshDays(1); + const state = useTripStore.getState(); + + expect(state.days).toHaveLength(1); + expect(state.assignments['20']).toEqual([assignment]); + expect(state.dayNotes['20']).toEqual([note]); + }); + }); + + describe('updateTrip', () => { + it('FE-TRIP-008: updateTrip persists and refreshes trip + days', async () => { + const updatedTrip = buildTrip({ id: 1, title: 'Updated Trip' }); + + server.use( + http.put('/api/trips/1', () => HttpResponse.json({ trip: updatedTrip })), + http.get('/api/trips/1/days', () => HttpResponse.json({ days: [] })), + ); + + const result = await useTripStore.getState().updateTrip(1, { title: 'Updated Trip' }); + + expect(result).toEqual(updatedTrip); + expect(useTripStore.getState().trip).toEqual(updatedTrip); + }); + }); + + describe('setSelectedDay', () => { + it('FE-TRIP-009: setSelectedDay updates selectedDayId', () => { + useTripStore.getState().setSelectedDay(42); + expect(useTripStore.getState().selectedDayId).toBe(42); + + useTripStore.getState().setSelectedDay(null); + expect(useTripStore.getState().selectedDayId).toBeNull(); + }); + }); + + describe('addTag', () => { + it('FE-TRIP-010: addTag creates tag and appends to tags', async () => { + const existingTag = buildTag(); + useTripStore.setState({ tags: [existingTag] }); + + const newTagData = { name: 'New Tag', color: '#00ff00' }; + + const result = await useTripStore.getState().addTag(newTagData); + + expect(result.name).toBe('New Tag'); + const tags = useTripStore.getState().tags; + expect(tags).toHaveLength(2); + expect(tags[tags.length - 1].name).toBe('New Tag'); + }); + }); + + describe('addCategory', () => { + it('FE-TRIP-011: addCategory creates category and appends to categories', async () => { + const existingCategory = buildCategory(); + useTripStore.setState({ categories: [existingCategory] }); + + const newCategoryData = { name: 'New Category', icon: 'hotel' }; + + const result = await useTripStore.getState().addCategory(newCategoryData); + + expect(result.name).toBe('New Category'); + const categories = useTripStore.getState().categories; + expect(categories).toHaveLength(2); + expect(categories[categories.length - 1].name).toBe('New Category'); + }); + }); +}); diff --git a/client/tests/unit/utils/fileDownload.test.ts b/client/tests/unit/utils/fileDownload.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..2045d2175d67705ca681e515a3fb31cec37b21e5 --- /dev/null +++ b/client/tests/unit/utils/fileDownload.test.ts @@ -0,0 +1,225 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' +import { downloadFile, openFile } from '../../../src/utils/fileDownload' +import { getCachedBlob } from '../../../src/db/offlineDb' + +// Mock the offline DB so these tests never touch Dexie/IndexedDB. +vi.mock('../../../src/db/offlineDb', () => ({ getCachedBlob: vi.fn() })) + +function makeFetchMock(status: number, blob: Blob = new Blob(['data'], { type: 'application/pdf' })) { + return vi.fn().mockResolvedValue({ + status, + ok: status >= 200 && status < 300, + blob: () => Promise.resolve(blob), + }) +} + +beforeEach(() => { + vi.spyOn(URL, 'createObjectURL').mockReturnValue('blob:mock-url') + vi.spyOn(URL, 'revokeObjectURL').mockImplementation(() => {}) + vi.spyOn(document.body, 'appendChild').mockImplementation((el) => el) + vi.spyOn(document.body, 'removeChild').mockImplementation((el) => el) + vi.useFakeTimers() +}) + +afterEach(() => { + vi.restoreAllMocks() + vi.useRealTimers() +}) + +describe('assertRelativeUrl (URL guard)', () => { + it('rejects absolute http URLs', async () => { + await expect(downloadFile('https://evil.com/x')).rejects.toThrow('Refusing to fetch non-relative URL') + }) + it('rejects protocol-relative URLs', async () => { + await expect(downloadFile('//evil.com/x')).rejects.toThrow('Refusing to fetch non-relative URL') + }) + it('allows relative paths', async () => { + vi.stubGlobal('fetch', makeFetchMock(200)) + vi.spyOn(HTMLAnchorElement.prototype, 'click').mockImplementation(() => {}) + await expect(downloadFile('/trips/1/files/2/download')).resolves.toBeUndefined() + }) +}) + +describe('downloadFile', () => { + it('fetches with credentials:include and triggers anchor download', async () => { + const fetchMock = makeFetchMock(200) + vi.stubGlobal('fetch', fetchMock) + + const clickSpy = vi.spyOn(HTMLAnchorElement.prototype, 'click').mockImplementation(() => {}) + + await downloadFile('/uploads/files/test.pdf', 'test.pdf') + + expect(fetchMock).toHaveBeenCalledWith('/uploads/files/test.pdf', { credentials: 'include' }) + expect(URL.createObjectURL).toHaveBeenCalled() + expect(clickSpy).toHaveBeenCalled() + + // Revoke happens after setTimeout(100) + vi.runAllTimers() + expect(URL.revokeObjectURL).toHaveBeenCalledWith('blob:mock-url') + }) + + it('sets download attribute to filename when provided', async () => { + vi.stubGlobal('fetch', makeFetchMock(200)) + vi.spyOn(HTMLAnchorElement.prototype, 'click').mockImplementation(() => {}) + + await downloadFile('/uploads/files/report.pdf', 'report.pdf') + + // Check anchor was created with download attribute + const appendCalls = (document.body.appendChild as ReturnType).mock.calls + const anchor = appendCalls[0]?.[0] as HTMLAnchorElement + expect(anchor.download).toBe('report.pdf') + }) + + it('throws on 401 response', async () => { + vi.stubGlobal('fetch', makeFetchMock(401)) + await expect(downloadFile('/uploads/files/secret.pdf')).rejects.toThrow('Unauthorized') + expect(URL.createObjectURL).not.toHaveBeenCalled() + }) +}) + +describe('openFile', () => { + it('fetches with credentials:include and opens blob URL via target=_blank anchor', async () => { + vi.stubGlobal('fetch', makeFetchMock(200)) + const openSpy = vi.spyOn(window, 'open').mockReturnValue(null) + const clickSpy = vi.spyOn(HTMLAnchorElement.prototype, 'click').mockImplementation(() => {}) + + await openFile('/uploads/files/doc.pdf') + + expect(window.fetch).toHaveBeenCalledWith('/uploads/files/doc.pdf', { credentials: 'include' }) + expect(URL.createObjectURL).toHaveBeenCalled() + // Must NOT call window.open — that path returns null when noreferrer is + // set, which previously caused the file to also open in the current tab. + expect(openSpy).not.toHaveBeenCalled() + expect(clickSpy).toHaveBeenCalledTimes(1) + + // The anchor used to open the new tab must be target=_blank, must NOT + // carry a `download` attribute (otherwise it would download in-page + // instead of opening), and must use rel=noopener noreferrer. + const appendCalls = (document.body.appendChild as ReturnType).mock.calls + const anchor = appendCalls[0]?.[0] as HTMLAnchorElement + expect(anchor.target).toBe('_blank') + expect(anchor.rel).toBe('noopener noreferrer') + expect(anchor.hasAttribute('download')).toBe(false) + + // Revoke happens after 30s timeout + vi.runAllTimers() + expect(URL.revokeObjectURL).toHaveBeenCalledWith('blob:mock-url') + }) + + it('does not trigger a second in-page action for safe inline types (regression: no double-open)', async () => { + vi.stubGlobal('fetch', makeFetchMock(200)) + const clickSpy = vi.spyOn(HTMLAnchorElement.prototype, 'click').mockImplementation(() => {}) + + await openFile('/uploads/files/doc.pdf', 'doc.pdf') + + // Exactly ONE anchor click — opening the new tab. No fallback download. + expect(clickSpy).toHaveBeenCalledTimes(1) + }) + + it('throws on 401 response', async () => { + vi.stubGlobal('fetch', makeFetchMock(401, new Blob([], { type: 'application/pdf' }))) + await expect(openFile('/uploads/files/secret.pdf')).rejects.toThrow('Unauthorized') + expect(URL.createObjectURL).not.toHaveBeenCalled() + }) + + it('forces download for unsafe MIME types (HTML) instead of opening inline', async () => { + const htmlBlob = new Blob([''], { type: 'text/html' }) + vi.stubGlobal('fetch', makeFetchMock(200, htmlBlob)) + const openSpy = vi.spyOn(window, 'open').mockReturnValue({} as Window) + const clickSpy = vi.spyOn(HTMLAnchorElement.prototype, 'click').mockImplementation(() => {}) + + await openFile('/uploads/files/malicious.html', 'malicious.html') + + // Must NOT open inline — download anchor clicked instead + expect(openSpy).not.toHaveBeenCalled() + expect(clickSpy).toHaveBeenCalledTimes(1) + + const appendCalls = (document.body.appendChild as ReturnType).mock.calls + const anchor = appendCalls[0]?.[0] as HTMLAnchorElement + expect(anchor.download).toBe('malicious.html') + }) + + it('forces download for SVG MIME type', async () => { + const svgBlob = new Blob([''], { type: 'image/svg+xml' }) + vi.stubGlobal('fetch', makeFetchMock(200, svgBlob)) + const openSpy = vi.spyOn(window, 'open').mockReturnValue({} as Window) + const clickSpy = vi.spyOn(HTMLAnchorElement.prototype, 'click').mockImplementation(() => {}) + + await openFile('/uploads/files/malicious.svg') + + expect(openSpy).not.toHaveBeenCalled() + expect(clickSpy).toHaveBeenCalledTimes(1) + }) + + it('falls back to download in iOS PWA standalone mode (blob URL inaccessible to Safari)', async () => { + vi.stubGlobal('fetch', makeFetchMock(200)) + const clickSpy = vi.spyOn(HTMLAnchorElement.prototype, 'click').mockImplementation(() => {}) + // Simulate iOS PWA (Add-to-Home-Screen) context + Object.defineProperty(navigator, 'standalone', { configurable: true, value: true }) + + try { + await openFile('/uploads/files/doc.pdf', 'doc.pdf') + + // Single anchor click — and it must be a DOWNLOAD anchor (no target=_blank), + // because target="_blank" in iOS PWA would hand off to Safari which cannot + // read the in-WebView blob URL. + expect(clickSpy).toHaveBeenCalledTimes(1) + const appendCalls = (document.body.appendChild as ReturnType).mock.calls + const anchor = appendCalls[0]?.[0] as HTMLAnchorElement + expect(anchor.target).toBe('') + expect(anchor.download).toBe('doc.pdf') + } finally { + // Clean up the non-standard iOS-only property we forced above. + delete (navigator as any).standalone + } + }) +}) + +describe('offline fallback (#1046)', () => { + function setOnline(value: boolean) { + Object.defineProperty(navigator, 'onLine', { value, configurable: true }) + } + beforeEach(() => vi.mocked(getCachedBlob).mockReset()) + afterEach(() => setOnline(true)) + + it('serves the cached blob without a network call when offline', async () => { + setOnline(false) + const blob = new Blob(['x'], { type: 'application/pdf' }) + vi.mocked(getCachedBlob).mockResolvedValue(blob) + const fetchSpy = vi.fn() + vi.stubGlobal('fetch', fetchSpy) + vi.spyOn(HTMLAnchorElement.prototype, 'click').mockImplementation(() => {}) + + await downloadFile('/uploads/files/cached.pdf') + + expect(fetchSpy).not.toHaveBeenCalled() + expect(getCachedBlob).toHaveBeenCalledWith('/uploads/files/cached.pdf') + expect(URL.createObjectURL).toHaveBeenCalledWith(blob) + }) + + it('falls back to the cache when a live fetch rejects (network error) while online', async () => { + setOnline(true) + vi.stubGlobal('fetch', vi.fn().mockRejectedValue(new Error('network down'))) + const blob = new Blob(['x'], { type: 'application/pdf' }) + vi.mocked(getCachedBlob).mockResolvedValue(blob) + vi.spyOn(HTMLAnchorElement.prototype, 'click').mockImplementation(() => {}) + + await downloadFile('/uploads/files/cached.pdf') + + expect(getCachedBlob).toHaveBeenCalledWith('/uploads/files/cached.pdf') + expect(URL.createObjectURL).toHaveBeenCalledWith(blob) + }) + + it('throws when offline and the file was never cached', async () => { + setOnline(false) + vi.mocked(getCachedBlob).mockResolvedValue(null) + await expect(downloadFile('/uploads/files/missing.pdf')).rejects.toThrow(/offline/i) + }) + + it('does not consult the cache on an HTTP error — a 401 still surfaces', async () => { + setOnline(true) + vi.stubGlobal('fetch', makeFetchMock(401)) + await expect(downloadFile('/uploads/files/secret.pdf')).rejects.toThrow('Unauthorized') + expect(getCachedBlob).not.toHaveBeenCalled() + }) +}) diff --git a/client/tests/unit/utils/formatters.test.ts b/client/tests/unit/utils/formatters.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..961070028bea576705ecdf0392615b6eb6fafb7b --- /dev/null +++ b/client/tests/unit/utils/formatters.test.ts @@ -0,0 +1,107 @@ +import { describe, it, expect } from 'vitest'; +import { formatDate, formatTime, dayTotalCost, currencyDecimals } from '../../../src/utils/formatters'; +import type { AssignmentsMap } from '../../../src/types'; + +// dayTotalCost intentionally exercises edge-case price inputs (string / non-numeric), +// which are looser than the canonical AssignmentsMap shape — hence the casts below. +const asMap = (m: unknown): AssignmentsMap => m as AssignmentsMap; + +describe('currencyDecimals', () => { + it('returns 0 for zero-decimal currencies', () => { + expect(currencyDecimals('JPY')).toBe(0); + expect(currencyDecimals('KRW')).toBe(0); + expect(currencyDecimals('jpy')).toBe(0); // case-insensitive + }); + + it('returns 2 for standard currencies', () => { + expect(currencyDecimals('EUR')).toBe(2); + expect(currencyDecimals('USD')).toBe(2); + expect(currencyDecimals('GBP')).toBe(2); + }); +}); + +describe('formatDate', () => { + it('returns null for null/undefined input', () => { + expect(formatDate(null, 'en-US')).toBeNull(); + expect(formatDate(undefined, 'en-US')).toBeNull(); + }); + + it('formats a date string and returns a non-empty string', () => { + const result = formatDate('2025-06-01', 'en-US'); + expect(result).not.toBeNull(); + expect(typeof result).toBe('string'); + expect(result!.length).toBeGreaterThan(0); + }); + + it('accepts an optional timeZone parameter without throwing', () => { + const result = formatDate('2025-06-01', 'en-US', 'America/New_York'); + expect(result).not.toBeNull(); + }); +}); + +describe('formatTime', () => { + it('returns empty string for null/undefined', () => { + expect(formatTime(null, 'en-US', '24h')).toBe(''); + expect(formatTime(undefined, 'en-US', '24h')).toBe(''); + }); + + it('formats 24h time', () => { + expect(formatTime('14:30', 'en-US', '24h')).toBe('14:30'); + expect(formatTime('09:05', 'en-US', '24h')).toBe('09:05'); + }); + + it('appends Uhr suffix for German locale in 24h mode', () => { + expect(formatTime('14:30', 'de-DE', '24h')).toBe('14:30 Uhr'); + }); + + it('formats 12h time', () => { + expect(formatTime('14:30', 'en-US', '12h')).toBe('2:30 PM'); + expect(formatTime('00:00', 'en-US', '12h')).toBe('12:00 AM'); + expect(formatTime('12:00', 'en-US', '12h')).toBe('12:00 PM'); + expect(formatTime('01:00', 'en-US', '12h')).toBe('1:00 AM'); + }); +}); + +describe('dayTotalCost', () => { + it('returns null when there are no assignments', () => { + expect(dayTotalCost(1, {}, 'EUR')).toBeNull(); + }); + + it('returns null when no places have prices', () => { + const assignments = { + '1': [ + { id: 1, day_id: 1, order_index: 0, notes: null, place: { id: 1, trip_id: 1, name: 'P', lat: null, lng: null, description: null, address: null, category_id: null, icon: null, price: null, image_url: null, google_place_id: null, osm_id: null, route_geometry: null, place_time: null, end_time: null, created_at: '' } }, + ], + }; + expect(dayTotalCost(1, asMap(assignments), 'EUR')).toBeNull(); + }); + + it('sums prices across assignments', () => { + const assignments = { + '1': [ + { id: 1, day_id: 1, order_index: 0, notes: null, place: { id: 1, trip_id: 1, name: 'A', lat: null, lng: null, description: null, address: null, category_id: null, icon: null, price: '20', image_url: null, google_place_id: null, osm_id: null, route_geometry: null, place_time: null, end_time: null, created_at: '' } }, + { id: 2, day_id: 1, order_index: 1, notes: null, place: { id: 2, trip_id: 1, name: 'B', lat: null, lng: null, description: null, address: null, category_id: null, icon: null, price: '30', image_url: null, google_place_id: null, osm_id: null, route_geometry: null, place_time: null, end_time: null, created_at: '' } }, + ], + }; + expect(dayTotalCost(1, asMap(assignments), 'EUR')).toBe('50 EUR'); + }); + + it('ignores non-numeric price strings', () => { + const assignments = { + '1': [ + { id: 1, day_id: 1, order_index: 0, notes: null, place: { id: 1, trip_id: 1, name: 'A', lat: null, lng: null, description: null, address: null, category_id: null, icon: null, price: 'free', image_url: null, google_place_id: null, osm_id: null, route_geometry: null, place_time: null, end_time: null, created_at: '' } }, + ], + }; + expect(dayTotalCost(1, asMap(assignments), 'EUR')).toBeNull(); + }); + + it('uses the dayId key to look up assignments', () => { + const assignments = { + '2': [ + { id: 3, day_id: 2, order_index: 0, notes: null, place: { id: 3, trip_id: 1, name: 'C', lat: null, lng: null, description: null, address: null, category_id: null, icon: null, price: '10', image_url: null, google_place_id: null, osm_id: null, route_geometry: null, place_time: null, end_time: null, created_at: '' } }, + ], + }; + expect(dayTotalCost(1, asMap(assignments), 'USD')).toBeNull(); + expect(dayTotalCost(2, asMap(assignments), 'USD')).toBe('10 USD'); + }); +}); diff --git a/client/tsconfig.json b/client/tsconfig.json new file mode 100644 index 0000000000000000000000000000000000000000..d3c51f17e37c152ed6b23feb1e6b1b4c2f4c2bad --- /dev/null +++ b/client/tsconfig.json @@ -0,0 +1,30 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "baseUrl": ".", + "ignoreDeprecations": "6.0", + "paths": { + "@trek/shared": ["../shared/src/index.ts"], + "@trek/shared/*": ["../shared/src/*"] + }, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + "strict": false, + "noUnusedLocals": false, + "noUnusedParameters": false, + "noFallthroughCasesInSwitch": true, + "allowJs": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true + }, + "include": ["src", "tests"] +} diff --git a/client/vite.config.js b/client/vite.config.js new file mode 100644 index 0000000000000000000000000000000000000000..b08517b0034e1b4acd8785916ce5dcc321ebca4c --- /dev/null +++ b/client/vite.config.js @@ -0,0 +1,157 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' +import { VitePWA } from 'vite-plugin-pwa' + +export default defineConfig({ + plugins: [ + react(), + VitePWA({ + registerType: 'autoUpdate', + workbox: { + maximumFileSizeToCacheInBytes: 10 * 1024 * 1024, + globPatterns: ['**/*.{js,css,html,svg,png,woff,woff2,ttf}'], + navigateFallback: 'index.html', + navigateFallbackDenylist: [/^\/api/, /^\/uploads/, /^\/mcp/, /^\/oauth\//, /^\/.well-known\//], + runtimeCaching: [ + { + // Carto map tiles (default provider) + // maxEntries MUST stay >= MAX_TILES in src/sync/tilePrefetcher.ts + // (both are 12288) so prefetched tiles aren't evicted on arrival. + urlPattern: /^https:\/\/[a-d]\.basemaps\.cartocdn\.com\/.*/i, + handler: 'CacheFirst', + options: { + cacheName: 'map-tiles', + expiration: { maxEntries: 12288, maxAgeSeconds: 30 * 24 * 60 * 60 }, + cacheableResponse: { statuses: [0, 200] }, + }, + }, + { + // OpenStreetMap tiles (fallback / alternative) + // Shares the 'map-tiles' cache; keep maxEntries equal to the Carto + // rule above and MAX_TILES in src/sync/tilePrefetcher.ts (12288). + urlPattern: /^https:\/\/[a-c]\.tile\.openstreetmap\.org\/.*/i, + handler: 'CacheFirst', + options: { + cacheName: 'map-tiles', + expiration: { maxEntries: 12288, maxAgeSeconds: 30 * 24 * 60 * 60 }, + cacheableResponse: { statuses: [0, 200] }, + }, + }, + { + // Leaflet CSS/JS from unpkg CDN + urlPattern: /^https:\/\/unpkg\.com\/.*/i, + handler: 'CacheFirst', + options: { + cacheName: 'cdn-libs', + expiration: { maxEntries: 30, maxAgeSeconds: 365 * 24 * 60 * 60 }, + cacheableResponse: { statuses: [0, 200] }, + }, + }, + { + // Mapbox GL style, glyphs, sprites and vector tiles. Best-effort + // offline only: opportunistically caches what the user has already + // viewed online. Full pre-download offline maps require the Leaflet + // renderer (raster prefetch in tilePrefetcher.ts) — the GL vector + // pipeline is not prefetched. StaleWhileRevalidate keeps the basemap + // fresh online while still serving from cache when offline. Mapbox + // sends CORS, so responses are non-opaque (real 200s, no quota pad). + urlPattern: /^https:\/\/(api\.mapbox\.com|[a-d]\.tiles\.mapbox\.com)\/.*/i, + handler: 'StaleWhileRevalidate', + options: { + cacheName: 'mapbox-tiles', + expiration: { maxEntries: 3000, maxAgeSeconds: 30 * 24 * 60 * 60 }, + cacheableResponse: { statuses: [200] }, + }, + }, + { + // API calls — network only. We deliberately do NOT cache API + // responses in the Service Worker: Workbox keys entries by URL and + // cannot vary on the httpOnly session cookie, so a shared device + // could serve one user's cached data to the next (cross-user leak). + // Offline reads are served from the per-user IndexedDB cache via the + // repo layer instead. The urlPattern is kept so these requests still + // bypass the SPA navigation fallback. + urlPattern: /\/api\/(?!auth|admin|backup|settings|health).*/i, + handler: 'NetworkOnly', + }, + { + // Uploaded files (photos, covers — public assets only) + urlPattern: /\/uploads\/(?:covers|avatars)\/.*/i, + handler: 'CacheFirst', + options: { + cacheName: 'user-uploads', + expiration: { maxEntries: 300, maxAgeSeconds: 7 * 24 * 60 * 60 }, + cacheableResponse: { statuses: [200] }, + }, + }, + ], + }, + manifest: { + name: 'TREK \u2014 Travel Planner', + short_name: 'TREK', + description: 'Travel Resource & Exploration Kit', + theme_color: '#111827', + background_color: '#0f172a', + display: 'standalone', + scope: '/', + start_url: '/', + categories: ['travel', 'navigation'], + icons: [ + { src: 'icons/apple-touch-icon-180x180.png', sizes: '180x180', type: 'image/png' }, + { src: 'icons/icon-192x192.png', sizes: '192x192', type: 'image/png' }, + { src: 'icons/icon-512x512.png', sizes: '512x512', type: 'image/png' }, + { src: 'icons/icon-512x512.png', sizes: '512x512', type: 'image/png', purpose: 'maskable' }, + { src: 'icons/icon.svg', sizes: 'any', type: 'image/svg+xml' }, + ], + }, + }), + ], + build: { + sourcemap: false, + modulePreload: { polyfill: true }, + }, + server: { + port: 5173, + proxy: { + '/api': { + target: 'http://localhost:3001', + changeOrigin: true, + }, + '/uploads': { + target: 'http://localhost:3001', + changeOrigin: true, + }, + '/ws': { + target: 'http://localhost:3001', + ws: true, + }, + '/mcp': { + target: 'http://localhost:3001', + changeOrigin: true, + }, + // OAuth 2.1 endpoints handled by backend (SDK authorize handler + token/revoke) + // /oauth/authorize goes to backend so the SDK can redirect to /oauth/consent + // /oauth/consent is served by Vite as a SPA route (no proxy entry needed) + '/oauth/authorize': { + target: 'http://localhost:3001', + changeOrigin: true, + }, + '/oauth/token': { + target: 'http://localhost:3001', + changeOrigin: true, + }, + '/oauth/register': { + target: 'http://localhost:3001', + changeOrigin: true, + }, + '/oauth/revoke': { + target: 'http://localhost:3001', + changeOrigin: true, + }, + '/.well-known': { + target: 'http://localhost:3001', + changeOrigin: true, + }, + } + } +}) diff --git a/client/vitest.config.ts b/client/vitest.config.ts new file mode 100644 index 0000000000000000000000000000000000000000..97fdc1b06a36cc4492b05ee043a39b98bfb4076e --- /dev/null +++ b/client/vitest.config.ts @@ -0,0 +1,29 @@ +import { defineConfig } from 'vitest/config'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + plugins: [react()], + test: { + root: '.', + globals: true, + environment: './tests/environment/jsdom-native-abort.ts', + include: [ + 'tests/**/*.test.{ts,tsx}', + 'src/**/*.test.{ts,tsx}', + ], + setupFiles: ['tests/setup.ts'], + testTimeout: 15000, + hookTimeout: 15000, + pool: 'forks', + silent: false, + reporters: ['verbose'], + coverage: { + provider: 'v8', + reporter: ['lcov', 'text'], + reportsDirectory: './coverage', + include: ['src/**/*.{ts,tsx}'], + exclude: ['src/main.tsx', 'src/vite-env.d.ts'], + }, + css: false, + }, +}); diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..17cbdcae74e931cb8a969314ef796188cecaa301 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,57 @@ +services: + app: + image: mauriceboe/trek:dev + container_name: trek + read_only: true + security_opt: + - no-new-privileges:true + cap_drop: + - ALL + cap_add: + - CHOWN + - SETUID + - SETGID + tmpfs: + - /tmp:noexec,nosuid,size=128m + ports: + - "3000:3000" + environment: + - NODE_ENV=production + - PORT=3000 + - ENCRYPTION_KEY=${ENCRYPTION_KEY:-} # Recommended. Generate with: openssl rand -hex 32. If unset, falls back to data/.jwt_secret (existing installs) or auto-generates a key (fresh installs). + - TZ=${TZ:-UTC} # Timezone for logs, reminders and scheduled tasks (e.g. Europe/Berlin) + - LOG_LEVEL=${LOG_LEVEL:-info} # info = concise user actions; debug = verbose admin-level details +# - DEFAULT_LANGUAGE=en # Default language on the login page for users with no saved preference. Browser/OS language is auto-detected first; this is the fallback. Supported: de, en, es, fr, hu, nl, br, cs, pl, ru, zh, zh-TW, it, ar +# - SESSION_DURATION=30d # How long users stay logged in (trek_session JWT + cookie maxAge). Accepts: 1h | 12h | 7d | 30d | 90d. Default: 24h +# - SESSION_DURATION_REMEMBER=30d # Session length when "Remember me" is ticked at login: longer-lived JWT + persistent cookie that survives browser restarts. Same format as SESSION_DURATION. Default: 30d + - ALLOWED_ORIGINS=${ALLOWED_ORIGINS:-} # Comma-separated origins for CORS and email notification links +# - FORCE_HTTPS=true # Optional. Enables HTTPS redirect, HSTS, CSP upgrade-insecure-requests, and secure cookies behind a TLS proxy +# - HSTS_INCLUDE_SUBDOMAINS=false # When true: adds includeSubDomains to the HSTS header. Only effective when HSTS is active. Leave false if sibling subdomains still run over plain HTTP. +# - COOKIE_SECURE=false # Escape hatch: force session cookies over plain HTTP even in production. Not recommended. +# - TRUST_PROXY=1 # Trusted proxy count for X-Forwarded-For / X-Forwarded-Proto. Required for FORCE_HTTPS to work. +# - ALLOW_INTERNAL_NETWORK=false # Set to true if Immich or other services are hosted on your local network (RFC-1918 IPs). Loopback and link-local addresses remain blocked regardless. +# - APP_URL=https://trek.example.com # Public base URL — required when OIDC is enabled (must match the redirect URI registered with your IdP); also used as base URL for links in email notifications +# - OIDC_ISSUER=https://auth.example.com # OpenID Connect provider URL +# - OIDC_CLIENT_ID=trek # OpenID Connect client ID +# - OIDC_CLIENT_SECRET=supersecret # OpenID Connect client secret +# - OIDC_DISPLAY_NAME=SSO # Label shown on the SSO login button +# - OIDC_ONLY=false # Set true to force SSO-only mode: disables password login and registration, overrides Admin > Settings toggles, cannot be changed at runtime +# - OIDC_ADMIN_CLAIM=groups # OIDC claim used to identify admin users +# - OIDC_ADMIN_VALUE=app-trek-admins # Value of the OIDC claim that grants admin role +# - OIDC_SCOPE=openid email profile # Fully overrides the default. Add extra scopes as needed (e.g. add groups if using OIDC_ADMIN_CLAIM) +# - OIDC_DISCOVERY_URL= # Override the OIDC discovery endpoint for providers with non-standard paths (e.g. Authentik) +# - ADMIN_EMAIL=admin@trek.local # Initial admin e-mail — only used on first boot when no users exist +# - ADMIN_PASSWORD=changeme # Initial admin password — only used on first boot when no users exist +# - MCP_RATE_LIMIT=300 # Max MCP API requests per user per minute (default: 300) +# - MCP_MAX_SESSION_PER_USER=20 # Max concurrent MCP sessions per user (default: 20) +# - KITINERARY_EXTRACTOR_PATH= # Optional. Full path to kitinerary-extractor binary. Auto-detected from PATH and /usr/lib/*/libexec/kf6/ when unset. + volumes: + - ./data:/app/data + - ./uploads:/app/uploads + restart: unless-stopped + healthcheck: + test: ["CMD", "wget", "-qO-", "http://localhost:3000/api/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 15s diff --git a/docs/TREK-Generated-by-MCP.pdf b/docs/TREK-Generated-by-MCP.pdf new file mode 100644 index 0000000000000000000000000000000000000000..5112ed43e5d8d54262147f7705d8dcd6bd36b71b Binary files /dev/null and b/docs/TREK-Generated-by-MCP.pdf differ diff --git a/docs/logo-trek-dark.gif b/docs/logo-trek-dark.gif new file mode 100644 index 0000000000000000000000000000000000000000..3fa5cb56639b4f1ebbc69d5eadea8c5f85af5914 Binary files /dev/null and b/docs/logo-trek-dark.gif differ diff --git a/docs/logo-trek-light.gif b/docs/logo-trek-light.gif new file mode 100644 index 0000000000000000000000000000000000000000..a7b7f60703f68cc1e6a2aaa1d846c91d752794fc Binary files /dev/null and b/docs/logo-trek-light.gif differ diff --git a/docs/screenshots/admin.png b/docs/screenshots/admin.png new file mode 100644 index 0000000000000000000000000000000000000000..8797275b683338f5fd3df1c0c7fbad1526b8c9c0 Binary files /dev/null and b/docs/screenshots/admin.png differ diff --git a/docs/screenshots/atlas.png b/docs/screenshots/atlas.png new file mode 100644 index 0000000000000000000000000000000000000000..bb420dbdd059847cddce394461b089aab31744b6 Binary files /dev/null and b/docs/screenshots/atlas.png differ diff --git a/docs/screenshots/budget.png b/docs/screenshots/budget.png new file mode 100644 index 0000000000000000000000000000000000000000..a1bd19dedd96ce7d934032976dd6ed7d5d4afff0 Binary files /dev/null and b/docs/screenshots/budget.png differ diff --git a/docs/screenshots/dashboard.png b/docs/screenshots/dashboard.png new file mode 100644 index 0000000000000000000000000000000000000000..b2a6ad82a774decd20c05fa49af8139faaa33946 Binary files /dev/null and b/docs/screenshots/dashboard.png differ diff --git a/docs/screenshots/journey.png b/docs/screenshots/journey.png new file mode 100644 index 0000000000000000000000000000000000000000..f0deb0de09eb1e8f5403e431ab4738efff43bd8e Binary files /dev/null and b/docs/screenshots/journey.png differ diff --git a/docs/screenshots/trip-iceland.png b/docs/screenshots/trip-iceland.png new file mode 100644 index 0000000000000000000000000000000000000000..cdbdaa4ab8d0e957bf1a12c92307e0c5818bfa25 Binary files /dev/null and b/docs/screenshots/trip-iceland.png differ diff --git a/docs/screenshots/trip-planner.png b/docs/screenshots/trip-planner.png new file mode 100644 index 0000000000000000000000000000000000000000..f95bcb746dd9e355ad15837200d1c296420f2d53 Binary files /dev/null and b/docs/screenshots/trip-planner.png differ diff --git a/docs/screenshots/vacay.png b/docs/screenshots/vacay.png new file mode 100644 index 0000000000000000000000000000000000000000..af76a6f4acce7e4f733a1bccdf0b1b72f1d732d5 Binary files /dev/null and b/docs/screenshots/vacay.png differ diff --git a/docs/subtitle-dark.png b/docs/subtitle-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..73a09f70170ba26c0c6b78714818a344a7a3d87c Binary files /dev/null and b/docs/subtitle-dark.png differ diff --git a/docs/subtitle-light.png b/docs/subtitle-light.png new file mode 100644 index 0000000000000000000000000000000000000000..07c5ba8141497708c7ba4e7bf4cdf6aa8c9fbb70 Binary files /dev/null and b/docs/subtitle-light.png differ diff --git a/docs/system-notices.md b/docs/system-notices.md new file mode 100644 index 0000000000000000000000000000000000000000..612a9ef3fa4ab6f4c2ef228b0e7f0209aaec4d63 --- /dev/null +++ b/docs/system-notices.md @@ -0,0 +1,765 @@ +# System Notices — Technical Documentation & Dev Guide + +System notices are server-evaluated, user-targeted messages shown in the TREK UI as modals, banners, or toasts. They are used for onboarding, upgrade announcements, breaking change warnings, and time-boxed campaigns. Every aspect — targeting, display, copy, and dismissal — is controlled from one place: the server-side registry. + +--- + +## Table of Contents + +1. [Architecture overview](#1-architecture-overview) +2. [Data flow](#2-data-flow) +3. [Database schema](#3-database-schema) +4. [The notice registry](#4-the-notice-registry) +5. [Notice fields reference](#5-notice-fields-reference) +6. [Condition system](#6-condition-system) +7. [Display types](#7-display-types) +8. [CTAs (call to action)](#8-ctas-call-to-action) +9. [i18n — translation keys](#9-i18n--translation-keys) +10. [Client store & dismissal](#10-client-store--dismissal) +11. [Sorting & priority](#11-sorting--priority) +12. [How-to recipes](#12-how-to-recipes) +13. [Testing](#13-testing) +14. [Rules & constraints](#14-rules--constraints) + +--- + +## 1. Architecture overview + +``` +server/src/systemNotices/ +├── types.ts — TypeScript types (SystemNotice, NoticeCondition, …) +├── registry.ts — Authoritative list of all notices (edit here to add/change/remove) +├── conditions.ts — Condition evaluators + custom predicate registry +└── service.ts — Queries DB, evaluates conditions, sorts, strips server-only fields + +server/src/routes/systemNotices.ts — REST endpoints + +client/src/store/systemNoticeStore.ts — Zustand store (fetch + optimistic dismiss) +client/src/components/SystemNotices/ +├── SystemNoticeHost.tsx — Renders all three channels (modal / banner / toast) +├── SystemNoticeModal.tsx — Modal renderer (pager, animations, keyboard nav) +├── SystemNoticeBanner.tsx — Banner + toast renderers +└── noticeActions.ts — Client-side action registry for action-kind CTAs + +client/src/pages/Trips/noticeActions.ts — Example domain action registration +``` + +There are **no database rows for notice definitions**. The registry is code-only. The database only stores which notices a user has dismissed. + +--- + +## 2. Data flow + +``` +1. User authenticates + │ + ▼ +2. authStore.loadUser() completes + │ + ▼ +3. SystemNoticeHost mounts → calls useSystemNoticeStore.fetch() + │ (also triggered on cold page reload if store not yet loaded) + ▼ +4. GET /api/system-notices/active + │ + ▼ +5. service.getActiveNoticesFor(userId) + ├── reads user row (login_count, first_seen_version, role) + ├── counts user trips + ├── reads user_notice_dismissals + ├── filters SYSTEM_NOTICES: + │ – not dismissed + │ – within [minVersion, maxVersion) range for the running app version + │ – all conditions pass (AND logic) + ├── sorts by priority → severity → publishedAt (desc) + └── strips server-only fields (conditions, publishedAt, minVersion, maxVersion, priority) + │ + ▼ +6. Client receives SystemNoticeDTO[] + │ + ▼ +7. SystemNoticeHost partitions by display type + ├── modal → ModalRenderer (multi-page pager, slide transitions) + ├── banner → BannerRenderer (sticky top bar, max 2) + └── toast → ToastRenderer (fires window.__addToast, auto-dismisses) + │ + ▼ +8. User dismisses → POST /api/system-notices/:id/dismiss + ├── Server: INSERT OR IGNORE into user_notice_dismissals + └── Client: optimistic remove from store (retry once on failure) +``` + +--- + +## 3. Database schema + +Added in **migration 101** (`server/src/db/migrations.ts`). + +### `users` columns (added by migration 101) + +| Column | Type | Default | Purpose | +|---|---|---|---| +| `first_seen_version` | `TEXT` | `'0.0.0'` | App version at account creation. Used by `existingUserBeforeVersion` condition. Backfilled users get `'0.0.0'`. | +| `login_count` | `INTEGER` | `0` | Incremented on each successful login. Used by `firstLogin` condition. | + +### `user_notice_dismissals` + +| Column | Type | Notes | +|---|---|---| +| `user_id` | `INTEGER` | FK → `users.id` CASCADE DELETE | +| `notice_id` | `TEXT` | Matches `SystemNotice.id` from registry | +| `dismissed_at` | `INTEGER` | Unix ms timestamp | + +Primary key: `(user_id, notice_id)` — dismissals are idempotent. + +--- + +## 4. The notice registry + +**`server/src/systemNotices/registry.ts`** is the single source of truth. Add, change, or retire notices here. + +```typescript +export const SYSTEM_NOTICES: SystemNotice[] = [ + { + id: 'my-notice', // ← globally unique, never reuse + display: 'modal', + severity: 'info', + titleKey: 'system_notice.my_notice.title', + bodyKey: 'system_notice.my_notice.body', + dismissible: true, + conditions: [{ kind: 'firstLogin' }], + publishedAt: '2026-05-01T00:00:00Z', + priority: 50, + }, +]; +``` + +### The golden rule for IDs + +**Never remove or renumber an entry. Never reuse an ID.** + +Dismissals are stored in the database keyed by `id`. Removing an entry means dismissed users would see it again if you ever add a notice with the same ID. If a notice is no longer needed, set `maxVersion` to the upper version on which it should appear (e.g. `4.0.0` means show notice until `4.0.0` is reached) — do not delete the entry. + +--- + +## 5. Notice fields reference + +### Required fields + +| Field | Type | Description | +|---|---|---| +| `id` | `string` | Globally unique, stable identifier. Use kebab-case, descriptive, version-scoped when appropriate (`v3-photos`, `welcome-v1`). Max recommended length: 40 chars. | +| `display` | `'modal' \| 'banner' \| 'toast'` | How the notice is rendered. See [§7 Display types](#7-display-types). | +| `severity` | `'info' \| 'warn' \| 'critical'` | Affects colour scheme and accessibility role. `critical` notices cannot be toasts. | +| `titleKey` | `string` | i18n key for the title. | +| `bodyKey` | `string` | i18n key for the body. Markdown supported in modals; plain text only in banners/toasts. | +| `dismissible` | `boolean` | If `false`, the X button and ESC key are hidden/blocked. Use only for `critical` notices that require action before proceeding. | +| `conditions` | `NoticeCondition[]` | Empty array (`[]`) means always shown (same as `[{ kind: 'always' }]`). All conditions must pass (AND logic). | +| `publishedAt` | `string` | ISO 8601 date. Used as a tiebreaker in sorting. Set to the deployment date. | + +### Optional fields + +| Field | Type | Description | +|---|---|---| +| `priority` | `number` | Higher number = shown first. Primary sort key. Default: `0`. | +| `minVersion` | `string` | Lowest app version (inclusive, semver) that should show this notice. Omit for no lower bound. | +| `maxVersion` | `string` | Upper bound (exclusive, semver) — notice is hidden once this version ships. `maxVersion: '4.0.0'` means shown on `< 4.0.0`. Omit for no upper bound. | +| `icon` | `string` | Lucide icon name (e.g. `'Sparkles'`, `'ImageOff'`). Shown in the modal's severity icon circle. Falls back to the severity default icon if absent or unrecognised. | +| `bodyParams` | `Record` | Interpolation parameters for `bodyKey`. Values replace `{key}` placeholders in the translated string. **Never hardcode version numbers or dates directly in translation strings — use this instead.** | +| `media` | `NoticeMedia` | Image to display in the modal. See below. | +| `highlights` | `Array<{ labelKey: string; iconName?: string }>` | Bullet-point feature list rendered below the body in modals. Each entry is a translation key + optional Lucide icon name. | +| `cta` | `NoticeCta` | Primary action button. See [§8 CTAs](#8-ctas-call-to-action). | + +> **Version bounds:** The range is `[minVersion, maxVersion)` — lower bound inclusive, upper bound exclusive. So `maxVersion: '4.0.0'` hides the notice once the app reaches 4.0.0. Both bounds are compared after stripping prerelease/build metadata via `semver.coerce`, so a server running `3.0.0-pre.42` is treated as `3.0.0` — consistent with `existingUserBeforeVersion` and staging environments behave like production. + +### `NoticeMedia` + +```typescript +interface NoticeMedia { + src: string; // URL or path + srcDark?: string; // Optional dark-mode variant + altKey: string; // i18n key for alt text + placement?: 'hero' | 'inline'; // default: 'hero' (full-width above body) + aspectRatio?: string; // CSS aspect-ratio value, default '16/9' +} +``` + +### Character limits + +| Field | Modal | Banner | Toast | +|---|---|---|---| +| Title | ≤ 40 chars | ≤ 40 chars | ≤ 40 chars | +| Body | ≤ 400 chars (markdown) | ≤ 140 chars (plain) | ≤ 80 chars (plain) | +| CTA label | ≤ 20 chars, a verb | ≤ 20 chars | ≤ 20 chars | + +--- + +## 6. Condition system + +Conditions are evaluated **server-side** on every `GET /api/system-notices/active` call. The client never sees conditions — only the filtered result. + +All conditions in `conditions[]` must pass (AND logic). To implement OR logic, create multiple notices with overlapping IDs is not possible — instead use a `custom` predicate with internal OR logic. + +### Built-in conditions + +#### `always` +```typescript +{ kind: 'always' } +``` +Always passes. Equivalent to an empty `conditions` array. + +--- + +#### `firstLogin` +```typescript +{ kind: 'firstLogin' } +``` +Passes when `users.login_count <= 1`. The counter is incremented during login, so this fires on the first fetch after the very first login. Useful for onboarding notices. + +--- + +#### `noTrips` +```typescript +{ kind: 'noTrips' } +``` +Passes when the user has zero trips. Often combined with `firstLogin`. + +--- + +#### `existingUserBeforeVersion` +```typescript +{ kind: 'existingUserBeforeVersion', version: '3.0.0' } +``` +Passes when: +- `users.first_seen_version < version` (user existed before this version) +- AND the running app version `>= version` (the version has been deployed) + +Backfilled/legacy users have `first_seen_version = '0.0.0'` and always pass the first condition. Use this for upgrade announcements targeting users who were around before a breaking change. + +--- + +#### `dateWindow` +```typescript +{ kind: 'dateWindow', startsAt: '2026-06-01T00:00:00Z', endsAt: '2026-07-01T00:00:00Z' } +``` +Passes when the current server time is inside `[startsAt, endsAt]`. `endsAt` is optional (open-ended). Use for campaigns, maintenance banners, and time-limited promotions. + +--- + +#### `role` +```typescript +{ kind: 'role', roles: ['admin'] } +// or both roles: +{ kind: 'role', roles: ['admin', 'user'] } +``` +Passes when the user's role is in the given list. + +--- + +#### `addonEnabled` +```typescript +{ kind: 'addonEnabled', addonId: 'journey' } +``` +Passes when the named addon is enabled in admin settings. Addon IDs are the string values in `server/src/addons.ts` (`ADDON_IDS`). Use this to gate notices that promote features behind an addon. + +--- + +#### `custom` +```typescript +{ kind: 'custom', id: 'my-predicate-id' } +``` +Delegates evaluation to a predicate registered server-side with `registerPredicate`. This is the escape hatch for logic not covered by the built-in conditions. + +**Registering a custom predicate:** + +```typescript +// server/src/systemNotices/conditions.ts exports registerPredicate +import { registerPredicate } from '../systemNotices/conditions.js'; + +registerPredicate('has-immich-configured', (ctx) => { + // ctx.user = { login_count, first_seen_version, role, noTrips } + // ctx.currentAppVersion = string + // ctx.now = Date + return someDbCheck(ctx.user); +}); +``` + +Register predicates at application startup before the first `getActiveNoticesFor` call. + +--- + +### Combining conditions (AND) + +```typescript +conditions: [ + { kind: 'existingUserBeforeVersion', version: '3.0.0' }, + { kind: 'addonEnabled', addonId: 'journey' }, +] +// Only shows to pre-3.0 users AND only if the journey addon is enabled. +``` + +--- + +## 7. Display types + +### `modal` + +Full-screen overlay with backdrop. On mobile: bottom sheet with drag-to-dismiss. On desktop: centered card. + +**Features:** +- Markdown body (via `react-markdown` + `remark-gfm` + `rehype-sanitize`) +- Optional hero or inline image +- Optional highlights list (icon + label bullets) +- Optional CTA button + "Not now" link +- OK button when no CTA is defined +- **Multi-page pager**: when multiple modal notices are active simultaneously, they are rendered as a paginated single modal with prev/next arrows, dot indicators, `N / M` counter, and keyboard arrow navigation +- Slide transition between pages +- ESC to dismiss all (if current notice is dismissible) +- CTA and OK dismiss **all** active modal notices, not just the current page +- "Not now" dismisses only the current page + +**Non-dismissible modals** (`dismissible: false`): X button, ESC key, and pager navigation are all disabled until the user acts on the CTA. Use only for `critical` severity. + +--- + +### `banner` + +Sticky top bar below the navigation. Slides in with a translate-Y animation. + +**Constraints:** +- Maximum 2 banners shown simultaneously (the 2 highest-priority active banners) +- Plain text only (no markdown) +- RTL-aware left-border accent +- Reports its height via a CSS variable `--banner-stack-h` for layout reflow + +--- + +### `toast` + +Fires the global `window.__addToast` toast system. Auto-dismisses after 6 s (`info`) or 9 s (`warn`). The notice is dismissed from the store after the toast expires. + +**Constraints:** +- `critical` severity is not allowed as a toast — the renderer logs a warning and auto-dismisses it instead +- Plain text only +- No interaction (no CTA rendered via toast) + +--- + +## 8. CTAs (call to action) + +A CTA renders as the primary blue button in modals and as an underline link in banners. There are two kinds. + +### `nav` — navigate to a route + +```typescript +cta: { + kind: 'nav', + labelKey: 'system_notice.my_notice.cta_label', + href: '/journey', +} +``` + +On click: navigates to `href` using React Router, then **dismisses all active modal notices** (or the current banner notice). The label is resolved through the i18n system. + +--- + +### `action` — run a registered client-side handler + +```typescript +cta: { + kind: 'action', + labelKey: 'system_notice.my_notice.cta_label', + actionId: 'open:trip-create', + dismissOnAction: true, // default true — set false to keep notice open after action +} +``` + +On click: looks up `actionId` in the client-side action registry and calls the handler, then **dismisses all active modal notices**. + +**To add a new action:** + +1. Create (or extend) a `noticeActions.ts` file in the relevant feature directory: + +```typescript +// client/src/pages/MyFeature/noticeActions.ts +import { registerNoticeAction } from '../../components/SystemNotices/noticeActions.js'; + +registerNoticeAction('open:my-feature', ({ navigate }) => { + navigate('/my-feature?from=notice'); +}); +``` + +2. Import it as a side-effect in `client/src/App.tsx`: + +```typescript +import './pages/MyFeature/noticeActions.js' +``` + +3. The registry integrity test (`server/tests/unit/systemNotices/registry.test.ts`) automatically scans all `noticeActions.ts` files and verifies that every `actionId` in the registry is registered. The test will fail if you add an `actionId` to the registry without registering it on the client. + +**Action handler signature:** + +```typescript +(ctx: NoticeActionContext) => void | Promise + +interface NoticeActionContext { + navigate: NavigateFunction; // React Router navigate function +} +``` + +### Dismiss behaviour summary + +| Trigger | What is dismissed | +|---|---| +| X button (modal) | All active modal notices | +| ESC key | All active modal notices (if current is dismissible) | +| CTA button | All active modal notices | +| OK button (no CTA) | All active modal notices | +| "Not now" link | Current page only | +| Banner dismiss (X) | That banner only | +| Backdrop click (modal) | Current page only | +| Swipe down (mobile) | Current page only | +| Toast expires | That toast only | + +--- + +## 9. i18n — translation keys + +Every notice field that is user-visible (`titleKey`, `bodyKey`, CTA `labelKey`, highlight `labelKey`, media `altKey`) is an i18n key resolved through `useTranslation().t()`. The key string is what gets stored in the registry; the display value lives in the translation files. + +**Translation files location:** `client/src/i18n/translations/` (15 files: `en`, `de`, `fr`, `es`, `it`, `nl`, `pl`, `cs`, `hu`, `ru`, `zh`, `zhTw`, `ar`, `br`, `id`) + +### Key naming convention + +``` +system_notice.. +``` + +Examples: +``` +system_notice.welcome_v1.title +system_notice.welcome_v1.body +system_notice.welcome_v1.cta_label +system_notice.welcome_v1.highlight_plan +system_notice.welcome_v1.hero_alt +``` + +### Adding keys + +Add the English key to `client/src/i18n/translations/en.ts` first, then replicate to the other 14 files. Group related notice keys together with a comment: + +```typescript +// System notices — my feature +'system_notice.my_notice.title': 'My feature is here', +'system_notice.my_notice.body': 'Here is what changed.', +'system_notice.my_notice.cta_label': 'Explore', +``` + +### `bodyParams` interpolation + +For values that vary at runtime (version numbers, dates, counts), use `{placeholder}` syntax in the translation string and pass `bodyParams` in the registry entry: + +```typescript +// In registry: +bodyKey: 'system_notice.my_notice.body', +bodyParams: { version: '3.1.0', date: '1 May 2026' }, + +// In en.ts: +'system_notice.my_notice.body': 'TREK {version} was released on {date}.', +``` + +**Never hardcode dynamic values directly in translation strings.** The interpolation runs client-side in `ModalRenderer` before rendering. + +### Multiline bodies (modals only) + +Use `\n\n` (escaped, not literal newlines) for paragraph breaks in modal body strings: + +```typescript +'system_notice.my_notice.body': 'First paragraph.\n\nSecond paragraph.', +``` + +Literal newlines in single-quoted TypeScript strings cause a parse error. + +### Pager i18n keys + +The pager UI uses its own keys (already present in all 15 files): + +``` +system_notice.pager.prev → "Previous notice" +system_notice.pager.next → "Next notice" +system_notice.pager.counter → "{current} / {total}" +system_notice.pager.goto → "Go to notice {n}" +system_notice.pager.position → "Notice {current} of {total}" (aria-live) +``` + +--- + +## 10. Client store & dismissal + +`client/src/store/systemNoticeStore.ts` (Zustand, no persistence). + +| Action | Behaviour | +|---|---| +| `fetch()` | `GET /api/system-notices/active`. Fails silently (non-critical). Sets `loaded = true` regardless. | +| `dismiss(id)` | Optimistic: removes notice from store immediately. POSTs to `/api/system-notices/{id}/dismiss` in background with one retry on failure. | + +`SystemNoticeHost` triggers `fetch()` on mount if `loaded === false`. Auth store also triggers it after login, so on a fresh login the fetch happens exactly once. + +--- + +## 11. Sorting & priority + +Notices are sorted before being sent to the client. The sort order is: + +1. **`priority`** (descending) — primary key. Higher number appears first. +2. **`severity`** (descending) — tiebreaker: `critical` (2) > `warn` (1) > `info` (0). +3. **`publishedAt`** (descending) — final tiebreaker: more recent notices first. + +This means `priority` always wins over severity. Assign priorities deliberately so the intended reading order is preserved when multiple notices are active simultaneously. + +Current priority allocations in the registry: + +| Range | Use | +|---|---| +| 100 | Onboarding / first-login | +| 80–90 | Major version upgrade notices | +| 50–70 | Feature announcements | +| 10–40 | Campaigns, banners | +| 0 (default) | Miscellaneous | + +--- + +## 12. How-to recipes + +### Add a new modal notice + +1. **Registry** — add an entry to `SYSTEM_NOTICES` in `server/src/systemNotices/registry.ts`: + +```typescript +{ + id: 'my-feature-v2', + display: 'modal', + severity: 'info', + icon: 'Zap', + titleKey: 'system_notice.my_feature_v2.title', + bodyKey: 'system_notice.my_feature_v2.body', + highlights: [ + { labelKey: 'system_notice.my_feature_v2.highlight_one', iconName: 'Check' }, + ], + cta: { + kind: 'nav', + labelKey: 'system_notice.my_feature_v2.cta_label', + href: '/my-feature', + }, + dismissible: true, + conditions: [{ kind: 'existingUserBeforeVersion', version: '2.0.0' }], + publishedAt: '2026-06-01T00:00:00Z', + priority: 60, +}, +``` + +2. **i18n** — add keys to `client/src/i18n/translations/en.ts` and the 14 other language files. + +3. **Test** — run `cd server && npx vitest run tests/unit/systemNotices/` to verify registry integrity. + +--- + +### Add a notice with an action CTA + +1. Create the action handler in the relevant feature directory: + +```typescript +// client/src/pages/MyFeature/noticeActions.ts +import { registerNoticeAction } from '../../components/SystemNotices/noticeActions.js'; + +registerNoticeAction('open:my-feature-dialog', ({ navigate }) => { + navigate('/my-feature?dialog=welcome'); +}); +``` + +2. Import it in `client/src/App.tsx`: + +```typescript +import './pages/MyFeature/noticeActions.js' +``` + +3. Reference the `actionId` in the registry: + +```typescript +cta: { + kind: 'action', + labelKey: 'system_notice.my_notice.cta_label', + actionId: 'open:my-feature-dialog', +}, +``` + +The registry integrity test will catch any `actionId` that appears in the registry but lacks a `registerNoticeAction` call. + +--- + +### Retire a notice (stop showing it) + +**Do not delete the entry.** Set `maxVersion` to the last app version on which the notice should appear. Once the app is upgraded past that version, the service filters it out automatically. The database row for dismissed users remains harmless. + +```typescript +{ + id: 'old-campaign', + // ... all existing fields unchanged ... + maxVersion: '3.1.0', // hidden once 3.1.0 ships (exclusive upper bound) +} +``` + +To scope a notice to a specific version window (e.g. a v3-only announcement), combine both bounds: + +```typescript +{ + id: 'v3-only', + minVersion: '3.0.0', + maxVersion: '4.0.0', // shown on >= 3.0.0 and < 4.0.0 +} +``` + +--- + +### Show a notice only during a campaign window + +Combine `dateWindow` with any other targeting conditions: + +```typescript +conditions: [ + { kind: 'dateWindow', startsAt: '2026-06-15T00:00:00Z', endsAt: '2026-06-30T23:59:59Z' }, + { kind: 'role', roles: ['admin'] }, +], +``` + +--- + +### Show a notice only if an addon is enabled + +```typescript +conditions: [ + { kind: 'addonEnabled', addonId: 'journey' }, +], +``` + +Addon IDs are the string values in `server/src/addons.ts` → `ADDON_IDS`. + +--- + +### Add a custom condition + +```typescript +// server/src/startup.ts (or wherever your bootstrap code runs) +import { registerPredicate } from './systemNotices/conditions.js'; + +registerPredicate('has-no-profile-photo', (ctx) => { + const row = db.prepare('SELECT avatar FROM users WHERE id = ?').get(ctx.user.id); + return !row?.avatar; +}); +``` + +Then reference it in the registry: + +```typescript +conditions: [{ kind: 'custom', id: 'has-no-profile-photo' }], +``` + +--- + +### Create a multipage upgrade announcement + +Give multiple notices the same `conditions` and adjacent `priority` values. The pager groups all active modal notices together automatically — no extra wiring required. + +```typescript +// Page 1 — breaking change (higher priority, warn severity) +{ id: 'v4-breaking', priority: 90, severity: 'warn', conditions: [{ kind: 'existingUserBeforeVersion', version: '4.0.0' }], ... }, + +// Page 2 — new feature (lower priority, info severity) +{ id: 'v4-feature', priority: 80, severity: 'info', conditions: [{ kind: 'existingUserBeforeVersion', version: '4.0.0' }], ... }, +``` + +Users who have already dismissed page 1 will only see page 2 on their next session. + +--- + +## 13. Testing + +### Server unit tests + +**`server/tests/unit/systemNotices/conditions.test.ts`** + +Tests each condition kind in isolation using `evaluate()` directly. No DB required. + +**`server/tests/unit/systemNotices/registry.test.ts`** + +Validates registry integrity: +- No duplicate `id` values +- All `action` CTA `actionId`s have a corresponding `registerNoticeAction()` call in the client source (scanned via regex — no JSON file needed) +- All `publishedAt` values parse as valid ISO dates + +Run: `cd server && npx vitest run tests/unit/systemNotices/` + +**`server/tests/integration/systemNotices.test.ts`** + +Integration tests against a real in-memory SQLite database: +- `GET /api/system-notices/active` returns 401 without auth, returns correct notices per user state +- `POST /api/system-notices/:id/dismiss` stores the dismissal and filters on subsequent requests +- Dismissing an unknown ID returns 404 + +Run: `cd server && npx vitest run tests/integration/systemNotices.test.ts` + +--- + +### Client unit tests + +**`client/src/components/SystemNotices/SystemNoticeModal.test.tsx`** + +Tests `ModalRenderer` with fake timers (`vi.useFakeTimers()`) and MSW for the dismiss endpoint. Key helpers: + +```typescript +// Flush the 500 ms grace delay that gates the modal's visible state +async function flushGraceDelay() { + await act(async () => { vi.runAllTimers(); }); +} + +// Minimal notice factory +function makeNotice(overrides?: Partial): SystemNoticeDTO +``` + +Covered cases (FE-SN-MODAL-001 to 018): +- Grace delay before visibility +- Dismiss button, X button, ESC key +- Non-dismissible notices (all affordances blocked) +- CTA nav button — dismisses all notices +- Body param interpolation +- Pager: counter, dots, prev/next buttons, keyboard arrows, dot click, non-dismissible lock +- Dismiss-does-not-skip regression +- X and ESC dismiss all in multipage scenario +- Last notice close + +Run: `cd client && npm run test -- SystemNoticeModal` + +--- + +### Running all notice tests + +```bash +cd server && npx vitest run tests/unit/systemNotices/ tests/integration/systemNotices.test.ts +cd client && npm run test -- SystemNoticeModal +``` + +--- + +## 14. Rules & constraints + +| Rule | Reason | +|---|---| +| Never delete or reuse a notice `id` | Dismissal records are keyed by `id`. Deletion causes dismissed users to see the notice again. | +| Never use literal newlines in translation strings | Single-quoted TS strings with literal newlines cause esbuild parse errors. Use `\n\n` (escaped). | +| Never hardcode version numbers or dates in translation strings | Use `bodyParams` so strings stay translatable without retranslation per release. | +| `critical` severity must have `dismissible: false` | `critical` toasts are auto-dismissed with a warning; a dismissible critical modal is inconsistent UX. | +| `critical` must not use `display: 'toast'` | The toast renderer logs a warning and auto-dismisses critical toasts rather than showing them. | +| CTA labels ≤ 20 chars, sentence case, a verb | Consistent button copy across the app. | +| Priorities must be set explicitly for upgrade notices | Adjacent notices form a multipage group; ordering matters for the reading flow. | +| `action` CTA `actionId` must be registered client-side | The registry integrity test enforces this. Add both the registry entry and the `registerNoticeAction` call in the same PR. | +| `maxVersion` over deletion for retiring notices | See §12. Deletion would cause dismissed users to re-see the notice if the ID were ever reused. | diff --git a/docs/tiles/grid-desktop.svg b/docs/tiles/grid-desktop.svg new file mode 100644 index 0000000000000000000000000000000000000000..5070a59ae081a213bbec6a6ea3e0fb2497c6dc96 --- /dev/null +++ b/docs/tiles/grid-desktop.svg @@ -0,0 +1,146 @@ + + + + + + + + + + + + TRIP PLANNER + Drag and drop + day by day + Reorder, move across days, optimise + + + + + + + + + + + + + MAPS + See it all + on the map + Leaflet + Mapbox GL, 3D buildings + + + + + + + + + + + + + COLLAB + Plan together + in real time + WebSocket sync, chat, polls, notes + + + + + + + + + + + + + BUDGET + Track costs + per person + Pie chart, multi-currency, splits + + + + + + + + + + + + + PACKING + Lists, sorted. + by category + Templates, bag tracking, weights + + + + + + + + + + + + + JOURNEY + A journal for + every trip + Magazine entries, photos, maps + + + + + + + + + + + + + VACAY + Vacation days, + visualised + Calendar, 100+ country holidays + + + + + + + + + + + + + AI / MCP + Let AI plan + your trips + 80+ tools, OAuth 2.1, Claude-ready + + + + + + + + + + + + + SELF-HOSTED + Runs on + your server + Docker, SQLite, AGPL — your data, yours + + + \ No newline at end of file diff --git a/docs/tiles/grid-mobile.svg b/docs/tiles/grid-mobile.svg new file mode 100644 index 0000000000000000000000000000000000000000..e4d74d1bf9635edf7d14294ff0ba5ceb2d9ba46a --- /dev/null +++ b/docs/tiles/grid-mobile.svg @@ -0,0 +1,130 @@ + + + + + + + + + + + + TRIP PLANNER + Drag and drop + day by day + Reorder, move across days, optimise + + + + + + + + + + + + + MAPS + See it all + on the map + Leaflet + Mapbox GL, 3D buildings + + + + + + + + + + + + + COLLAB + Plan together + in real time + WebSocket sync, chat, polls, notes + + + + + + + + + + + + + BUDGET + Track costs + per person + Pie chart, multi-currency, splits + + + + + + + + + + + + + PACKING + Lists, sorted. + by category + Templates, bag tracking, weights + + + + + + + + + + + + + JOURNEY + A journal for + every trip + Magazine entries, photos, maps + + + + + + + + + + + + + VACAY + Vacation days, + visualised + Calendar, 100+ country holidays + + + + + + + + + + + + + AI / MCP + Let AI plan + your trips + 80+ tools, OAuth 2.1, Claude-ready + + + \ No newline at end of file diff --git a/docs/trek-icon.png b/docs/trek-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..b6f059ce428c2cbec437bb4fa9610868abeea61a Binary files /dev/null and b/docs/trek-icon.png differ diff --git a/node_modules/.bin/acorn b/node_modules/.bin/acorn new file mode 100644 index 0000000000000000000000000000000000000000..3ef3c124b08bd4fb0f27cd13301dfeeae4a6464c --- /dev/null +++ b/node_modules/.bin/acorn @@ -0,0 +1,4 @@ +#!/usr/bin/env node +"use strict" + +require("../dist/bin.js") diff --git a/node_modules/.bin/baseline-browser-mapping b/node_modules/.bin/baseline-browser-mapping new file mode 100644 index 0000000000000000000000000000000000000000..b52b3b0930145d6628f6c8fa770cdac7a5857684 --- /dev/null +++ b/node_modules/.bin/baseline-browser-mapping @@ -0,0 +1,2 @@ +#!/usr/bin/env node +"use strict";const{getCompatibleVersions:e}=require("./index.cjs"),a=process.argv.slice(2),s={};for(let e=0;e use integer seed as starting value (rolling CRC)", +" -H, --hex-seed= use hex seed as starting value (rolling CRC)", +" -d, --signed print result with format `%d` (default)", +" -u, --unsigned print result with format `%u`", +" -x, --hex print result with format `%0.8x`", +" -X, --HEX print result with format `%0.8X`", +" -c, --crc32c use CRC32C (Castagnoli)", +" -F, --format= use specified printf format", +"", +"Set filename = '-' or pipe data into crc32 to read from stdin", +"Default output mode is signed (-d)", +"" +].forEach(function(l) { console.log(l); }); + return 0; +} + +function version()/*:number*/ { console.log(X.version); return 0; } + +var fs = require('fs'); +try { require('exit-on-epipe'); } catch(e) {} + +function die(msg/*:string*/, ec/*:?number*/)/*:void*/ { console.error(msg); process.exit(ec || 0); } + +var args/*:Array*/ = process.argv.slice(2); +var filename/*:string*/ = ""; +var fmt/*:string*/ = ""; +var seed = 0, r = 10; + +for(var i = 0; i < args.length; ++i) { + var arg = args[i]; + if(arg.charCodeAt(0) != 45) { if(filename === "") filename = arg; continue; } + var m = arg.indexOf("=") == -1 ? arg : arg.substr(0, arg.indexOf("=")); + switch(m) { + case "-": filename = "-"; break; + + case "--help": case "-h": process.exit(help()); break; + case "--version": case "-V": process.exit(version()); break; + + case "--crc32c": case "-c": try { X = require('../crc32c'); } catch(e) { X = require('crc-32/crc32c'); } break; + + case "--signed": case "-d": fmt = "%d"; break; + case "--unsigned": case "-u": fmt = "%u"; break; + case "--hex": case "-x": fmt = "%0.8x"; break; + case "--HEX": case "-X": fmt = "%0.8X"; break; + case "--format": case "-F": + try { + require("printj"); + fmt = ((m!=arg) ? arg.substr(m.length+1) : args[++i])||""; + } catch(e) { + console.error("The `crc-32` module removed the `printj` dependency for formatting"); + console.error("Use the `crc32-cli` module instead:"); + console.error(" $ npx crc32-cli [options] [filename]"); + } break; + + case "--hex-seed": case "-H": r = 16; + /* falls through */ + case "--seed": case "-S": + seed=parseInt((m!=arg) ? arg.substr(m.length+1) : args[++i], r)||0; break; + + default: die("crc32: unrecognized option `" + arg + "'", 22); + } +} + +if(!process.stdin.isTTY) filename = filename || "-"; +if(filename.length===0) die("crc32: must specify a filename ('-' for stdin)",1); + +var crc32 = seed; +// $FlowIgnore -- Writable is callable but type sig disagrees +var writable = require('stream').Writable(); +writable._write = function(chunk, e, cb) { crc32 = X.buf(chunk, crc32); cb(); }; +writable._writev = function(chunks, cb) { + chunks.forEach(function(c) { crc32 = X.buf(c.chunk, crc32);}); + cb(); +}; +writable.on('finish', function() { + if(fmt === "") console.log(crc32); + else try { console.log(require("printj").sprintf(fmt, crc32)); } catch(e) { + switch(fmt) { + case "%d": console.log(crc32); break; + case "%u": console.log(crc32 >>> 0); break; + case "%0.8x": console.log((crc32 >>> 0).toString(16).padStart(8, "0").toLowerCase()); break; + case "%0.8X": console.log((crc32 >>> 0).toString(16).padStart(8, "0").toUpperCase()); break; + } + } +}); + +if(filename === "-") process.stdin.pipe(writable); +else if(fs.existsSync(filename)) fs.createReadStream(filename).pipe(writable); +else die("crc32: " + filename + ": No such file or directory", 2); diff --git a/node_modules/.bin/eslint b/node_modules/.bin/eslint new file mode 100644 index 0000000000000000000000000000000000000000..6e4858186ebf9f3f0d9a43487fa090f8723b5053 --- /dev/null +++ b/node_modules/.bin/eslint @@ -0,0 +1,211 @@ +#!/usr/bin/env node + +/** + * @fileoverview Main CLI that is run via the eslint command. + * @author Nicholas C. Zakas + */ + +/* eslint no-console:off -- CLI */ + +"use strict"; + +const mod = require("node:module"); + +// to use V8's code cache to speed up instantiation time +mod.enableCompileCache?.(); + +// must do this initialization *before* other requires in order to work +if (process.argv.includes("--debug")) { + require("debug").enable("eslint:*,-eslint:code-path,eslintrc:*"); +} + +//------------------------------------------------------------------------------ +// Helpers +//------------------------------------------------------------------------------ + +/** + * Read data from stdin til the end. + * + * Note: See + * - https://github.com/nodejs/node/blob/master/doc/api/process.md#processstdin + * - https://github.com/nodejs/node/blob/master/doc/api/process.md#a-note-on-process-io + * - https://lists.gnu.org/archive/html/bug-gnu-emacs/2016-01/msg00419.html + * - https://github.com/nodejs/node/issues/7439 (historical) + * + * On Windows using `fs.readFileSync(STDIN_FILE_DESCRIPTOR, "utf8")` seems + * to read 4096 bytes before blocking and never drains to read further data. + * + * The investigation on the Emacs thread indicates: + * + * > Emacs on MS-Windows uses pipes to communicate with subprocesses; a + * > pipe on Windows has a 4K buffer. So as soon as Emacs writes more than + * > 4096 bytes to the pipe, the pipe becomes full, and Emacs then waits for + * > the subprocess to read its end of the pipe, at which time Emacs will + * > write the rest of the stuff. + * @returns {Promise} The read text. + */ +function readStdin() { + return new Promise((resolve, reject) => { + let content = ""; + let chunk = ""; + + process.stdin + .setEncoding("utf8") + .on("readable", () => { + while ((chunk = process.stdin.read()) !== null) { + content += chunk; + } + }) + .on("end", () => resolve(content)) + .on("error", reject); + }); +} + +/** + * Spawns an external command and propagates its exit status. + * @param {string} command The command to run. + * @param {string[]} args The command arguments. + * @throws {Error} If the command cannot be spawned. + * @returns {void} + */ +function spawnExternalCommand(command, args) { + const spawn = require("cross-spawn"); + const result = spawn.sync(command, args, { + encoding: "utf8", + stdio: "inherit", + }); + + if (result.error) { + throw result.error; + } + + if (result.signal) { + process.kill(process.pid, result.signal); + return; + } + + process.exitCode = result.status ?? 0; +} + +/** + * Get the error message of a given value. + * @param {any} error The value to get. + * @returns {string} The error message. + */ +function getErrorMessage(error) { + // Lazy loading because this is used only if an error happened. + const util = require("node:util"); + + // Foolproof -- third-party module might throw non-object. + if (typeof error !== "object" || error === null) { + return String(error); + } + + // Use templates if `error.messageTemplate` is present. + if (typeof error.messageTemplate === "string") { + try { + const template = require(`../messages/${error.messageTemplate}.js`); + + return template(error.messageData || {}); + } catch { + // Ignore template error then fallback to use `error.stack`. + } + } + + // Use the stacktrace if it's an error object. + if (typeof error.stack === "string") { + return error.stack; + } + + // Otherwise, dump the object. + return util.format("%o", error); +} + +/** + * Tracks error messages that are shown to the user so we only ever show the + * same message once. + * @type {Set} + */ +const displayedErrors = new Set(); + +/** + * Tracks whether an unexpected error was caught + * @type {boolean} + */ +let hadFatalError = false; + +/** + * Catch and report unexpected error. + * @param {any} error The thrown error object. + * @returns {void} + */ +function onFatalError(error) { + process.exitCode = 2; + hadFatalError = true; + + const { version } = require("../package.json"); + const message = ` +Oops! Something went wrong! :( + +ESLint: ${version} + +${getErrorMessage(error)}`; + + if (!displayedErrors.has(message)) { + console.error(message); + displayedErrors.add(message); + } +} + +//------------------------------------------------------------------------------ +// Execution +//------------------------------------------------------------------------------ + +(async function main() { + process.on("uncaughtException", onFatalError); + process.on("unhandledRejection", onFatalError); + + // Call the config initializer if `--init` is present. + if (process.argv.includes("--init")) { + // `eslint --init` has been moved to `@eslint/create-config` + console.warn( + "You can also run this command directly using 'npm init @eslint/config@latest'.", + ); + + spawnExternalCommand("npm", ["init", "@eslint/config@latest"]); + return; + } + + // start the MCP server if `--mcp` is present + if (process.argv.includes("--mcp")) { + console.warn( + "You can also run this command directly using 'npx @eslint/mcp@latest'.", + ); + + spawnExternalCommand("npx", ["@eslint/mcp@latest"]); + return; + } + + // Otherwise, call the CLI. + const cli = require("../lib/cli"); + const exitCode = await cli.execute( + process.argv, + process.argv.includes("--stdin") ? await readStdin() : void 0, + ); + + /* + * If an uncaught exception or unhandled rejection was detected in the meantime, + * keep the fatal exit code 2 that is already assigned to `process.exitCode`. + * Without this condition, exit code 2 (unsuccessful execution) could be overwritten with + * 1 (successful execution, lint problems found) or even 0 (successful execution, no lint problems found). + * This ensures that unexpected errors that seemingly don't affect the success + * of the execution will still cause a non-zero exit code, as it's a common + * practice and the default behavior of Node.js to exit with non-zero + * in case of an uncaught exception or unhandled rejection. + * + * Otherwise, assign the exit code returned from CLI. + */ + if (!hadFatalError) { + process.exitCode = exitCode; + } +})().catch(onFatalError); diff --git a/node_modules/.bin/eslint-config-prettier b/node_modules/.bin/eslint-config-prettier new file mode 100644 index 0000000000000000000000000000000000000000..da85433640340258448a748ac62b830855f8a668 --- /dev/null +++ b/node_modules/.bin/eslint-config-prettier @@ -0,0 +1,240 @@ +#!/usr/bin/env node + +"use strict"; + +const validators = require("./validators"); +const config = require(".."); +const prettier = require("../prettier"); + +// Require locally installed eslint, for `npx eslint-config-prettier` support +// with no local eslint-config-prettier installation. +const localRequire = (request) => + require( + require.resolve(request, { + paths: [process.cwd(), ...require.resolve.paths("eslint")], + }) + ); + +let experimentalApi = {}; +try { + experimentalApi = localRequire("eslint/use-at-your-own-risk"); +} catch (_error) {} + +const { ESLint, FlatESLint = experimentalApi.FlatESLint } = + localRequire("eslint"); + +const SPECIAL_RULES_URL = + "https://github.com/prettier/eslint-config-prettier#special-rules"; + +const PRETTIER_RULES_URL = + "https://github.com/prettier/eslint-config-prettier#arrow-body-style-and-prefer-arrow-callback"; + +if (module === require.main) { + const args = process.argv.slice(2); + + if (args.length === 0) { + console.error(help()); + process.exit(1); + } + + const eslint = new ESLint(); + const flatESLint = FlatESLint === undefined ? undefined : new FlatESLint(); + + Promise.all( + args.map((file) => { + switch (process.env.ESLINT_USE_FLAT_CONFIG) { + case "true": { + return flatESLint.calculateConfigForFile(file); + } + case "false": { + return eslint.calculateConfigForFile(file); + } + default: { + // This turns synchronous errors (such as `.calculateConfigForFile` not existing) + // and turns them into promise rejections. + return Promise.resolve() + .then(() => flatESLint.calculateConfigForFile(file)) + .catch(() => eslint.calculateConfigForFile(file)); + } + } + }) + ) + .then((configs) => { + const rules = configs.flatMap( + ( + // Initializing config and rules for files that aren't included in the flat config, + // which is a very unlikely scenario, but should still be treated as a success + { rules = {} } = {}, + index + ) => Object.entries(rules).map((entry) => [...entry, args[index]]) + ); + const result = processRules(rules); + if (result.stderr) { + console.error(result.stderr); + } + if (result.stdout) { + console.error(result.stdout); + } + process.exit(result.code); + }) + .catch((error) => { + console.error(error.message); + process.exit(1); + }); +} + +function help() { + return ` +Usage: npx eslint-config-prettier FILE... + +Resolves an ESLint configuration for every given FILE and checks if they +contain rules that are unnecessary or conflict with Prettier. Example: + + npx eslint-config-prettier index.js test/index.js other/file/to/check.js + +Exit codes: + +0: No automatically detectable problems found. +1: General error. +2: Conflicting rules found. + +For more information, see: +https://github.com/prettier/eslint-config-prettier#cli-helper-tool + `.trim(); +} + +function processRules(configRules) { + const regularRules = filterRules(config.rules, (_, value) => value === "off"); + const optionsRules = filterRules( + config.rules, + (ruleName, value) => value === 0 && ruleName in validators + ); + const specialRules = filterRules( + config.rules, + (ruleName, value) => value === 0 && !(ruleName in validators) + ); + + const enabledRules = configRules + .map(([ruleName, value, source]) => { + const arrayValue = Array.isArray(value) ? value : [value]; + const [level, ...options] = arrayValue; + const isOff = level === "off" || level === 0; + return isOff ? null : { ruleName, options, source }; + }) + .filter(Boolean); + + const flaggedRules = enabledRules.filter( + ({ ruleName }) => ruleName in config.rules + ); + + const regularFlaggedRuleNames = filterRuleNames( + flaggedRules, + ({ ruleName }) => ruleName in regularRules + ); + const optionsFlaggedRuleNames = filterRuleNames( + flaggedRules, + ({ ruleName, ...rule }) => + ruleName in optionsRules && !validators[ruleName](rule) + ); + const specialFlaggedRuleNames = filterRuleNames( + flaggedRules, + ({ ruleName }) => ruleName in specialRules + ); + const prettierFlaggedRuleNames = filterRuleNames( + enabledRules, + ({ ruleName, source }) => + ruleName in prettier.rules && + enabledRules.some( + (rule) => + rule.ruleName === "prettier/prettier" && rule.source === source + ) + ); + + const regularMessage = [ + "The following rules are unnecessary or might conflict with Prettier:", + "", + printRuleNames(regularFlaggedRuleNames), + ].join("\n"); + + const optionsMessage = [ + "The following rules are enabled with config that might conflict with Prettier. See:", + SPECIAL_RULES_URL, + "", + printRuleNames(optionsFlaggedRuleNames), + ].join("\n"); + + const specialMessage = [ + "The following rules are enabled but cannot be automatically checked. See:", + SPECIAL_RULES_URL, + "", + printRuleNames(specialFlaggedRuleNames), + ].join("\n"); + + const prettierMessage = [ + "The following rules can cause issues when using eslint-plugin-prettier at the same time.", + "Only enable them if you know what you are doing! See:", + PRETTIER_RULES_URL, + "", + printRuleNames(prettierFlaggedRuleNames), + ].join("\n"); + + if ( + regularFlaggedRuleNames.length === 0 && + optionsFlaggedRuleNames.length === 0 + ) { + const message = + specialFlaggedRuleNames.length === 0 && + prettierFlaggedRuleNames.length === 0 + ? "No rules that are unnecessary or conflict with Prettier were found." + : [ + specialFlaggedRuleNames.length === 0 ? null : specialMessage, + prettierFlaggedRuleNames.length === 0 ? null : prettierMessage, + "Other than that, no rules that are unnecessary or conflict with Prettier were found.", + ] + .filter(Boolean) + .join("\n\n"); + + return { + stdout: message, + code: 0, + }; + } + + const message = [ + regularFlaggedRuleNames.length === 0 ? null : regularMessage, + optionsFlaggedRuleNames.length === 0 ? null : optionsMessage, + specialFlaggedRuleNames.length === 0 ? null : specialMessage, + prettierFlaggedRuleNames.length === 0 ? null : prettierMessage, + ] + .filter(Boolean) + .join("\n\n"); + + return { + stdout: message, + code: 2, + }; +} + +function filterRules(rules, fn) { + return Object.fromEntries( + Object.entries(rules) + .filter(([ruleName, value]) => fn(ruleName, value)) + .map(([ruleName]) => [ruleName, true]) + ); +} + +function filterRuleNames(rules, fn) { + return [ + ...new Set(rules.filter((rule) => fn(rule)).map((rule) => rule.ruleName)), + ]; +} + +function printRuleNames(ruleNames) { + return ruleNames + .slice() + .sort() + .map((ruleName) => `- ${ruleName}`) + .join("\n"); +} + +exports.processRules = processRules; diff --git a/node_modules/.bin/fxparser b/node_modules/.bin/fxparser new file mode 100644 index 0000000000000000000000000000000000000000..b4773e40362211ccd6b227bcfa836b4a4f5ba8e3 --- /dev/null +++ b/node_modules/.bin/fxparser @@ -0,0 +1,97 @@ +#!/usr/bin/env node +'use strict'; +/*eslint-disable no-console*/ +import fs from 'fs'; +import { resolve } from 'path'; +import {XMLParser, XMLValidator} from "../fxp.js"; +import ReadToEnd from './read.js'; +import cmdDetail from "./man.js" + +console.warn("\x1b[33m%s\x1b[0m", "⚠️ Warning: The built-in CLI interface is now deprecated."); +console.warn("Please install the dedicated CLI package instead:"); +console.warn(" npm install -g fxp-cli"); + +if (process.argv[2] === '--help' || process.argv[2] === '-h') { + console.log(cmdDetail); +} else if (process.argv[2] === '--version') { + const packageJsonPath = resolve(process.cwd(), 'package.json'); + const version = JSON.parse(fs.readFileSync(packageJsonPath).toString()).version; + console.log(version); +} else { + const options = { + removeNSPrefix: true, + ignoreAttributes: false, + parseTagValue: true, + parseAttributeValue: true, + }; + let fileName = ''; + let outputFileName; + let validate = false; + let validateOnly = false; + for (let i = 2; i < process.argv.length; i++) { + if (process.argv[i] === '-ns') { + options.removeNSPrefix = false; + } else if (process.argv[i] === '-a') { + options.ignoreAttributes = true; + } else if (process.argv[i] === '-c') { + options.parseTagValue = false; + options.parseAttributeValue = false; + } else if (process.argv[i] === '-o') { + outputFileName = process.argv[++i]; + } else if (process.argv[i] === '-v') { + validate = true; + } else if (process.argv[i] === '-V') { + validateOnly = true; + } else { + //filename + fileName = process.argv[i]; + } + } + + const callback = function(xmlData) { + let output = ''; + if (validateOnly) { + output = XMLValidator.validate(xmlData); + process.exitCode = output === true ? 0 : 1; + } else { + const parser = new XMLParser(options); + output = JSON.stringify(parser.parse(xmlData,validate), null, 4); + } + if (outputFileName) { + writeToFile(outputFileName, output); + } else { + console.log(output); + } + }; + + + try { + + if (!fileName) { + ReadToEnd.readToEnd(process.stdin, function(err, data) { + if (err) { + throw err; + } + callback(data.toString()); + }); + } else { + fs.readFile(fileName, function(err, data) { + if (err) { + throw err; + } + callback(data.toString()); + }); + } + } catch (e) { + console.log('Seems an invalid file or stream.' + e); + } +} + +function writeToFile(fileName, data) { + fs.writeFile(fileName, data, function(err) { + if (err) { + throw err; + } + console.log('JSON output has been written to ' + fileName); + }); +} diff --git a/node_modules/.bin/jiti b/node_modules/.bin/jiti new file mode 100644 index 0000000000000000000000000000000000000000..2867c64676c200685520975b43698493766fb2ba --- /dev/null +++ b/node_modules/.bin/jiti @@ -0,0 +1,16 @@ +#!/usr/bin/env node + +const { resolve } = require("node:path"); + +const script = process.argv.splice(2, 1)[0]; + +if (!script) { + + console.error("Usage: jiti [...arguments]"); + process.exit(1); +} + +const pwd = process.cwd(); +const jiti = require("..")(pwd); +const resolved = (process.argv[1] = jiti.resolve(resolve(pwd, script))); +jiti(resolved); diff --git a/node_modules/.bin/js-yaml b/node_modules/.bin/js-yaml new file mode 100644 index 0000000000000000000000000000000000000000..abcd7e000bd312d4c03f3faf85f75b7a82218d5a --- /dev/null +++ b/node_modules/.bin/js-yaml @@ -0,0 +1,117 @@ +#!/usr/bin/env node + +'use strict' + +const fs = require('fs') +const argparse = require('argparse') +const yaml = require('..') + +/// ///////////////////////////////////////////////////////////////////////////// + +const cli = new argparse.ArgumentParser({ + prog: 'js-yaml', + add_help: true +}) + +cli.add_argument('-v', '--version', { + action: 'version', + version: require('../package.json').version +}) + +cli.add_argument('-c', '--compact', { + help: 'Display errors in compact mode', + action: 'store_true' +}) + +// deprecated (not needed after we removed output colors) +// option suppressed, but not completely removed for compatibility +cli.add_argument('-j', '--to-json', { + help: argparse.SUPPRESS, + dest: 'json', + action: 'store_true' +}) + +cli.add_argument('-t', '--trace', { + help: 'Show stack trace on error', + action: 'store_true' +}) + +cli.add_argument('file', { + help: 'File to read, utf-8 encoded without BOM', + nargs: '?', + default: '-' +}) + +/// ///////////////////////////////////////////////////////////////////////////// + +const options = cli.parse_args() + +/// ///////////////////////////////////////////////////////////////////////////// + +function readFile (filename, encoding, callback) { + if (options.file === '-') { + // read from stdin + + const chunks = [] + + process.stdin.on('data', function (chunk) { + chunks.push(chunk) + }) + + process.stdin.on('end', function () { + return callback(null, Buffer.concat(chunks).toString(encoding)) + }) + } else { + fs.readFile(filename, encoding, callback) + } +} + +readFile(options.file, 'utf8', function (error, input) { + let output + let isYaml + + if (error) { + if (error.code === 'ENOENT') { + console.error('File not found: ' + options.file) + process.exit(2) + } + + console.error( + (options.trace && error.stack) || + error.message || + String(error)) + + process.exit(1) + } + + try { + output = JSON.parse(input) + isYaml = false + } catch (err) { + if (err instanceof SyntaxError) { + try { + output = [] + yaml.loadAll(input, function (doc) { output.push(doc) }, {}) + isYaml = true + + if (output.length === 0) output = null + else if (output.length === 1) output = output[0] + } catch (e) { + if (options.trace && err.stack) console.error(e.stack) + else console.error(e.toString(options.compact)) + + process.exit(1) + } + } else { + console.error( + (options.trace && err.stack) || + err.message || + String(err)) + + process.exit(1) + } + } + + if (isYaml) console.log(JSON.stringify(output, null, ' ')) + else console.log(yaml.dump(output)) +}) diff --git a/node_modules/.bin/jsesc b/node_modules/.bin/jsesc new file mode 100644 index 0000000000000000000000000000000000000000..e9a541db322abe86e96da58a3ab528de98b90ce9 --- /dev/null +++ b/node_modules/.bin/jsesc @@ -0,0 +1,148 @@ +#!/usr/bin/env node +(function() { + + var fs = require('fs'); + var stringEscape = require('../jsesc.js'); + var strings = process.argv.splice(2); + var stdin = process.stdin; + var data; + var timeout; + var isObject = false; + var options = {}; + var log = console.log; + + var main = function() { + var option = strings[0]; + + if (/^(?:-h|--help|undefined)$/.test(option)) { + log( + 'jsesc v%s - https://mths.be/jsesc', + stringEscape.version + ); + log([ + '\nUsage:\n', + '\tjsesc [string]', + '\tjsesc [-s | --single-quotes] [string]', + '\tjsesc [-d | --double-quotes] [string]', + '\tjsesc [-w | --wrap] [string]', + '\tjsesc [-e | --escape-everything] [string]', + '\tjsesc [-t | --escape-etago] [string]', + '\tjsesc [-6 | --es6] [string]', + '\tjsesc [-l | --lowercase-hex] [string]', + '\tjsesc [-j | --json] [string]', + '\tjsesc [-o | --object] [stringified_object]', // `JSON.parse()` the argument + '\tjsesc [-p | --pretty] [string]', // `compact: false` + '\tjsesc [-v | --version]', + '\tjsesc [-h | --help]', + '\nExamples:\n', + '\tjsesc \'f\xF6o \u2665 b\xE5r \uD834\uDF06 baz\'', + '\tjsesc --json \'f\xF6o \u2665 b\xE5r \uD834\uDF06 baz\'', + '\tjsesc --json --escape-everything \'f\xF6o \u2665 b\xE5r \uD834\uDF06 baz\'', + '\tjsesc --double-quotes --wrap \'f\xF6o \u2665 b\xE5r \uD834\uDF06 baz\'', + '\techo \'f\xF6o \u2665 b\xE5r \uD834\uDF06 baz\' | jsesc' + ].join('\n')); + return process.exit(1); + } + + if (/^(?:-v|--version)$/.test(option)) { + log('v%s', stringEscape.version); + return process.exit(1); + } + + strings.forEach(function(string) { + // Process options + if (/^(?:-s|--single-quotes)$/.test(string)) { + options.quotes = 'single'; + return; + } + if (/^(?:-d|--double-quotes)$/.test(string)) { + options.quotes = 'double'; + return; + } + if (/^(?:-w|--wrap)$/.test(string)) { + options.wrap = true; + return; + } + if (/^(?:-e|--escape-everything)$/.test(string)) { + options.escapeEverything = true; + return; + } + if (/^(?:-t|--escape-etago)$/.test(string)) { + options.escapeEtago = true; + return; + } + if (/^(?:-6|--es6)$/.test(string)) { + options.es6 = true; + return; + } + if (/^(?:-l|--lowercase-hex)$/.test(string)) { + options.lowercaseHex = true; + return; + } + if (/^(?:-j|--json)$/.test(string)) { + options.json = true; + return; + } + if (/^(?:-o|--object)$/.test(string)) { + isObject = true; + return; + } + if (/^(?:-p|--pretty)$/.test(string)) { + isObject = true; + options.compact = false; + return; + } + + // Process string(s) + var result; + try { + if (isObject) { + string = JSON.parse(string); + } + result = stringEscape(string, options); + log(result); + } catch(error) { + log(error.message + '\n'); + log('Error: failed to escape.'); + log('If you think this is a bug in jsesc, please report it:'); + log('https://github.com/mathiasbynens/jsesc/issues/new'); + log( + '\nStack trace using jsesc@%s:\n', + stringEscape.version + ); + log(error.stack); + return process.exit(1); + } + }); + // Return with exit status 0 outside of the `forEach` loop, in case + // multiple strings were passed in. + return process.exit(0); + + }; + + if (stdin.isTTY) { + // handle shell arguments + main(); + } else { + // Either the script is called from within a non-TTY context, + // or `stdin` content is being piped in. + if (!process.stdout.isTTY) { // called from a non-TTY context + timeout = setTimeout(function() { + // if no piped data arrived after a while, handle shell arguments + main(); + }, 250); + } + + data = ''; + stdin.on('data', function(chunk) { + clearTimeout(timeout); + data += chunk; + }); + stdin.on('end', function() { + strings.push(data.trim()); + main(); + }); + stdin.resume(); + } + +}()); diff --git a/node_modules/.bin/json5 b/node_modules/.bin/json5 new file mode 100644 index 0000000000000000000000000000000000000000..93cb80921e21aed8acfdd81bf5c096138f2e228f --- /dev/null +++ b/node_modules/.bin/json5 @@ -0,0 +1,152 @@ +#!/usr/bin/env node + +const fs = require('fs') +const path = require('path') +const pkg = require('../package.json') +const JSON5 = require('./') + +const argv = parseArgs() + +if (argv.version) { + version() +} else if (argv.help) { + usage() +} else { + const inFilename = argv.defaults[0] + + let readStream + if (inFilename) { + readStream = fs.createReadStream(inFilename) + } else { + readStream = process.stdin + } + + let json5 = '' + readStream.on('data', data => { + json5 += data + }) + + readStream.on('end', () => { + let space + if (argv.space === 't' || argv.space === 'tab') { + space = '\t' + } else { + space = Number(argv.space) + } + + let value + try { + value = JSON5.parse(json5) + if (!argv.validate) { + const json = JSON.stringify(value, null, space) + + let writeStream + + // --convert is for backward compatibility with v0.5.1. If + // specified with and not --out-file, then a file with + // the same name but with a .json extension will be written. + if (argv.convert && inFilename && !argv.outFile) { + const parsedFilename = path.parse(inFilename) + const outFilename = path.format( + Object.assign( + parsedFilename, + {base: path.basename(parsedFilename.base, parsedFilename.ext) + '.json'} + ) + ) + + writeStream = fs.createWriteStream(outFilename) + } else if (argv.outFile) { + writeStream = fs.createWriteStream(argv.outFile) + } else { + writeStream = process.stdout + } + + writeStream.write(json) + } + } catch (err) { + console.error(err.message) + process.exit(1) + } + }) +} + +function parseArgs () { + let convert + let space + let validate + let outFile + let version + let help + const defaults = [] + + const args = process.argv.slice(2) + for (let i = 0; i < args.length; i++) { + const arg = args[i] + switch (arg) { + case '--convert': + case '-c': + convert = true + break + + case '--space': + case '-s': + space = args[++i] + break + + case '--validate': + case '-v': + validate = true + break + + case '--out-file': + case '-o': + outFile = args[++i] + break + + case '--version': + case '-V': + version = true + break + + case '--help': + case '-h': + help = true + break + + default: + defaults.push(arg) + break + } + } + + return { + convert, + space, + validate, + outFile, + version, + help, + defaults, + } +} + +function version () { + console.log(pkg.version) +} + +function usage () { + console.log( + ` + Usage: json5 [options] + + If is not provided, then STDIN is used. + + Options: + + -s, --space The number of spaces to indent or 't' for tabs + -o, --out-file [file] Output to the specified file, otherwise STDOUT + -v, --validate Validate JSON5 but do not output JSON + -V, --version Output the version number + -h, --help Output usage information` + ) +} diff --git a/node_modules/.bin/mime b/node_modules/.bin/mime new file mode 100644 index 0000000000000000000000000000000000000000..ab70a49c41aeaee64a44a3667094710dbad2efb0 --- /dev/null +++ b/node_modules/.bin/mime @@ -0,0 +1,46 @@ +#!/usr/bin/env node + +'use strict'; + +process.title = 'mime'; +let mime = require('.'); +let pkg = require('./package.json'); +let args = process.argv.splice(2); + +if (args.includes('--version') || args.includes('-v') || args.includes('--v')) { + console.log(pkg.version); + process.exit(0); +} else if (args.includes('--name') || args.includes('-n') || args.includes('--n')) { + console.log(pkg.name); + process.exit(0); +} else if (args.includes('--help') || args.includes('-h') || args.includes('--h')) { + console.log(pkg.name + ' - ' + pkg.description + '\n'); + console.log(`Usage: + + mime [flags] [path_or_extension] + + Flags: + --help, -h Show this message + --version, -v Display the version + --name, -n Print the name of the program + + Note: the command will exit after it executes if a command is specified + The path_or_extension is the path to the file or the extension of the file. + + Examples: + mime --help + mime --version + mime --name + mime -v + mime src/log.js + mime new.py + mime foo.sh + `); + process.exit(0); +} + +let file = args[0]; +let type = mime.getType(file); + +process.stdout.write(type + '\n'); + diff --git a/node_modules/.bin/msw b/node_modules/.bin/msw new file mode 100644 index 0000000000000000000000000000000000000000..e22bd0e916cb1116c10b5294204bd1350999dc5d --- /dev/null +++ b/node_modules/.bin/msw @@ -0,0 +1,35 @@ +#!/usr/bin/env node +import yargs from 'yargs' +import { init } from './init.js' + +// eslint-disable-next-line @typescript-eslint/no-unused-expressions +yargs(process.argv.slice(2)) + .usage('$0 [args]') + .command( + 'init', + 'Initializes Mock Service Worker at the specified directory', + (yargs) => { + yargs + .positional('publicDir', { + type: 'string', + description: 'Relative path to the public directory', + demandOption: false, + normalize: true, + }) + .option('save', { + type: 'boolean', + description: 'Save the worker directory in your package.json', + }) + .option('cwd', { + type: 'string', + description: 'Custom current worker directory', + normalize: true, + }) + .example('msw init') + .example('msw init ./public') + .example('msw init ./static --save') + }, + init, + ) + .demandCommand() + .help().argv diff --git a/node_modules/.bin/nanoid b/node_modules/.bin/nanoid new file mode 100644 index 0000000000000000000000000000000000000000..c76db0faa81f6d683c4a18a212f804fb70cf8143 --- /dev/null +++ b/node_modules/.bin/nanoid @@ -0,0 +1,55 @@ +#!/usr/bin/env node + +let { nanoid, customAlphabet } = require('..') + +function print(msg) { + process.stdout.write(msg + '\n') +} + +function error(msg) { + process.stderr.write(msg + '\n') + process.exit(1) +} + +if (process.argv.includes('--help') || process.argv.includes('-h')) { + print(` + Usage + $ nanoid [options] + + Options + -s, --size Generated ID size + -a, --alphabet Alphabet to use + -h, --help Show this help + + Examples + $ nanoid --s 15 + S9sBF77U6sDB8Yg + + $ nanoid --size 10 --alphabet abc + bcabababca`) + process.exit() +} + +let alphabet, size +for (let i = 2; i < process.argv.length; i++) { + let arg = process.argv[i] + if (arg === '--size' || arg === '-s') { + size = Number(process.argv[i + 1]) + i += 1 + if (Number.isNaN(size) || size <= 0) { + error('Size must be positive integer') + } + } else if (arg === '--alphabet' || arg === '-a') { + alphabet = process.argv[i + 1] + i += 1 + } else { + error('Unknown argument ' + arg) + } +} + +if (alphabet) { + let customNanoid = customAlphabet(alphabet, size) + print(customNanoid()) +} else { + print(nanoid(size)) +} diff --git a/node_modules/.bin/node-which b/node_modules/.bin/node-which new file mode 100644 index 0000000000000000000000000000000000000000..7cee3729eebdd09e39bd891aa4d17483ea4a757a --- /dev/null +++ b/node_modules/.bin/node-which @@ -0,0 +1,52 @@ +#!/usr/bin/env node +var which = require("../") +if (process.argv.length < 3) + usage() + +function usage () { + console.error('usage: which [-as] program ...') + process.exit(1) +} + +var all = false +var silent = false +var dashdash = false +var args = process.argv.slice(2).filter(function (arg) { + if (dashdash || !/^-/.test(arg)) + return true + + if (arg === '--') { + dashdash = true + return false + } + + var flags = arg.substr(1).split('') + for (var f = 0; f < flags.length; f++) { + var flag = flags[f] + switch (flag) { + case 's': + silent = true + break + case 'a': + all = true + break + default: + console.error('which: illegal option -- ' + flag) + usage() + } + } + return false +}) + +process.exit(args.reduce(function (pv, current) { + try { + var f = which.sync(current, { all: all }) + if (all) + f = f.join('\n') + if (!silent) + console.log(f) + return pv; + } catch (e) { + return 1; + } +}, 0)) diff --git a/node_modules/.bin/nodemon b/node_modules/.bin/nodemon new file mode 100644 index 0000000000000000000000000000000000000000..3d490f140dd264f98dfb50ab820456c88fe92a64 --- /dev/null +++ b/node_modules/.bin/nodemon @@ -0,0 +1,16 @@ +#!/usr/bin/env node + +const cli = require('../lib/cli'); +const nodemon = require('../lib/'); +const options = cli.parse(process.argv); + +nodemon(options); + +const fs = require('fs'); + +// checks for available update and returns an instance +const pkg = JSON.parse(fs.readFileSync(__dirname + '/../package.json')); + +if (pkg.version.indexOf('0.0.0') !== 0 && options.noUpdateNotifier !== true) { + require('simple-update-notifier')({ pkg }); +} diff --git a/node_modules/.bin/nodetouch b/node_modules/.bin/nodetouch new file mode 100644 index 0000000000000000000000000000000000000000..f78f0829d1fa9c09fc6565a51ceefc4a1bc80cda --- /dev/null +++ b/node_modules/.bin/nodetouch @@ -0,0 +1,112 @@ +#!/usr/bin/env node +const touch = require("../index.js") + +const usage = code => { + console[code ? 'error' : 'log']( + 'usage:\n' + + 'touch [-acfm] [-r file] [-t [[CC]YY]MMDDhhmm[.SS]] file ...' + ) + process.exit(code) +} + +const singleFlags = { + a: 'atime', + m: 'mtime', + c: 'nocreate', + f: 'force' +} + +const singleOpts = { + r: 'ref', + t: 'time' +} + +const files = [] +const args = process.argv.slice(2) +const options = {} +for (let i = 0; i < args.length; i++) { + const arg = args[i] + if (!arg.match(/^-/)) { + files.push(arg) + continue + } + + // expand shorthands + if (arg.charAt(1) !== '-') { + const expand = [] + for (let f = 1; f < arg.length; f++) { + const fc = arg.charAt(f) + const sf = singleFlags[fc] + const so = singleOpts[fc] + if (sf) + expand.push('--' + sf) + else if (so) { + const soslice = arg.slice(f + 1) + const soval = soslice.charAt(0) === '=' ? soslice : '=' + soslice + expand.push('--' + so + soval) + f = arg.length + } else if (arg !== '-' + fc) + expand.push('-' + fc) + } + if (expand.length) { + args.splice.apply(args, [i, 1].concat(expand)) + i-- + continue + } + } + + const argsplit = arg.split('=') + const key = argsplit.shift().replace(/^\-\-/, '') + const val = argsplit.length ? argsplit.join('=') : null + + switch (key) { + case 'time': + const timestr = val || args[++i] + // [-t [[CC]YY]MMDDhhmm[.SS]] + const parsedtime = timestr.match( + /^(([0-9]{2})?([0-9]{2}))?([0-9]{2})([0-9]{2})([0-9]{2})([0-9]{2})(\.([0-9]{2}))?$/ + ) + if (!parsedtime) { + console.error('touch: out of range or illegal ' + + 'time specification: ' + + '[[CC]YY]MMDDhhmm[.SS]') + process.exit(1) + } else { + const y = +parsedtime[1] + const year = parsedtime[2] ? y + : y <= 68 ? 2000 + y + : 1900 + y + + const MM = +parsedtime[4] - 1 + const dd = +parsedtime[5] + const hh = +parsedtime[6] + const mm = +parsedtime[7] + const ss = +parsedtime[8] + + options.time = new Date(Date.UTC(year, MM, dd, hh, mm, ss)) + } + continue + + case 'ref': + options.ref = val || args[++i] + continue + + case 'mtime': + case 'nocreate': + case 'atime': + case 'force': + options[key] = true + continue + + default: + console.error('touch: illegal option -- ' + arg) + usage(1) + } +} + +if (!files.length) + usage() + +process.exitCode = 0 +Promise.all(files.map(f => touch(f, options))) + .catch(er => process.exitCode = 1) diff --git a/node_modules/.bin/parser b/node_modules/.bin/parser new file mode 100644 index 0000000000000000000000000000000000000000..4808c5ee86f71fc4452fa6bb50580669fe52e8f2 --- /dev/null +++ b/node_modules/.bin/parser @@ -0,0 +1,15 @@ +#!/usr/bin/env node +/* eslint-disable no-var, unicorn/prefer-node-protocol */ + +var parser = require(".."); +var fs = require("fs"); + +var filename = process.argv[2]; +if (!filename) { + console.error("no filename specified"); +} else { + var file = fs.readFileSync(filename, "utf8"); + var ast = parser.parse(file); + + console.log(JSON.stringify(ast, null, " ")); +} diff --git a/node_modules/.bin/pixelmatch b/node_modules/.bin/pixelmatch new file mode 100644 index 0000000000000000000000000000000000000000..85bbb1a788ab6de50e76a4bbbcc4583b62e8ff63 --- /dev/null +++ b/node_modules/.bin/pixelmatch @@ -0,0 +1,42 @@ +#!/usr/bin/env node +/* eslint-disable no-process-exit */ + +'use strict'; + +const PNG = require('pngjs').PNG; +const fs = require('fs'); +const match = require('../.'); + +if (process.argv.length < 4) { + console.log('Usage: pixelmatch image1.png image2.png [diff.png] [threshold] [includeAA]'); + process.exit(64); +} + +const [,, img1Path, img2Path, diffPath, threshold, includeAA] = process.argv; +const options = {}; +if (threshold !== undefined) options.threshold = +threshold; +if (includeAA !== undefined) options.includeAA = includeAA !== 'false'; + +const img1 = PNG.sync.read(fs.readFileSync(img1Path)); +const img2 = PNG.sync.read(fs.readFileSync(img2Path)); + +const {width, height} = img1; + +if (img2.width !== width || img2.height !== height) { + console.log(`Image dimensions do not match: ${width}x${height} vs ${img2.width}x${img2.height}`); + process.exit(65); +} + +const diff = diffPath ? new PNG({width, height}) : null; + +console.time('matched in'); +const diffs = match(img1.data, img2.data, diff ? diff.data : null, width, height, options); +console.timeEnd('matched in'); + +console.log(`different pixels: ${diffs}`); +console.log(`error: ${Math.round(100 * 100 * diffs / (width * height)) / 100}%`); + +if (diff) { + fs.writeFileSync(diffPath, PNG.sync.write(diff)); +} +process.exit(diffs ? 66 : 0); diff --git a/node_modules/.bin/prebuild-install b/node_modules/.bin/prebuild-install new file mode 100644 index 0000000000000000000000000000000000000000..e5260cce5707aba8272a4bfa9658bd5dc5d7ce8c --- /dev/null +++ b/node_modules/.bin/prebuild-install @@ -0,0 +1,78 @@ +#!/usr/bin/env node + +const path = require('path') +const fs = require('fs') +const napi = require('napi-build-utils') + +const pkg = require(path.resolve('package.json')) +const rc = require('./rc')(pkg) +const log = require('./log')(rc, process.env) +const download = require('./download') +const asset = require('./asset') +const util = require('./util') + +const prebuildClientVersion = require('./package.json').version +if (rc.version) { + console.log(prebuildClientVersion) + process.exit(0) +} + +if (rc.path) process.chdir(rc.path) + +if (rc.runtime === 'electron' && rc.target[0] === '4' && rc.abi === '64') { + log.error(`Electron version ${rc.target} found - skipping prebuild-install work due to known ABI issue`) + log.error('More information about this issue can be found at https://github.com/lgeiger/node-abi/issues/54') + process.exit(1) +} + +if (!fs.existsSync('package.json')) { + log.error('setup', 'No package.json found. Aborting...') + process.exit(1) +} + +if (rc.help) { + console.error(fs.readFileSync(path.join(__dirname, 'help.txt'), 'utf-8')) + process.exit(0) +} + +log.info('begin', 'Prebuild-install version', prebuildClientVersion) + +const opts = Object.assign({}, rc, { pkg: pkg, log: log }) + +if (napi.isNapiRuntime(rc.runtime)) napi.logUnsupportedVersion(rc.target, log) + +const origin = util.packageOrigin(process.env, pkg) + +if (opts.force) { + log.warn('install', 'prebuilt binaries enforced with --force!') + log.warn('install', 'prebuilt binaries may be out of date!') +} else if (origin && origin.length > 4 && origin.substr(0, 4) === 'git+') { + log.info('install', 'installing from git repository, skipping download.') + process.exit(1) +} else if (opts.buildFromSource) { + log.info('install', '--build-from-source specified, not attempting download.') + process.exit(1) +} + +const startDownload = function (downloadUrl) { + download(downloadUrl, opts, function (err) { + if (err) { + log.warn('install', err.message) + return process.exit(1) + } + log.info('install', 'Successfully installed prebuilt binary!') + }) +} + +if (opts.token) { + asset(opts, function (err, assetId) { + if (err) { + log.warn('install', err.message) + return process.exit(1) + } + + startDownload(util.getAssetUrl(opts, assetId)) + }) +} else { + startDownload(util.getDownloadUrl(opts)) +} diff --git a/node_modules/.bin/prettier b/node_modules/.bin/prettier new file mode 100644 index 0000000000000000000000000000000000000000..642c99dd135f7b9ea845f6d4faacef76e195e5aa --- /dev/null +++ b/node_modules/.bin/prettier @@ -0,0 +1,80 @@ +#!/usr/bin/env node +"use strict"; +var __getOwnPropNames = Object.getOwnPropertyNames; +var __commonJS = function(cb, mod) { + return function __require() { + return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports; + }; +}; + +// node_modules/semver-compare/index.js +var require_semver_compare = __commonJS({ + "node_modules/semver-compare/index.js": function(exports2, module2) { + module2.exports = function cmp(a, b) { + var pa = a.split("."); + var pb = b.split("."); + for (var i = 0; i < 3; i++) { + var na = Number(pa[i]); + var nb = Number(pb[i]); + if (na > nb) return 1; + if (nb > na) return -1; + if (!isNaN(na) && isNaN(nb)) return 1; + if (isNaN(na) && !isNaN(nb)) return -1; + } + return 0; + }; + } +}); + +// node_modules/please-upgrade-node/index.js +var require_please_upgrade_node = __commonJS({ + "node_modules/please-upgrade-node/index.js": function(exports2, module2) { + var semverCompare = require_semver_compare(); + module2.exports = function pleaseUpgradeNode2(pkg, opts) { + var opts = opts || {}; + var requiredVersion = pkg.engines.node.replace(">=", ""); + var currentVersion = process.version.replace("v", ""); + if (semverCompare(currentVersion, requiredVersion) === -1) { + if (opts.message) { + console.error(opts.message(requiredVersion)); + } else { + console.error( + pkg.name + " requires at least version " + requiredVersion + " of Node, please upgrade" + ); + } + if (opts.hasOwnProperty("exitCode")) { + process.exit(opts.exitCode); + } else { + process.exit(1); + } + } + }; + } +}); + +// bin/prettier.cjs +var nodeModule = require("module"); +if (typeof nodeModule.enableCompileCache === "function") { + nodeModule.enableCompileCache(); +} +var pleaseUpgradeNode = require_please_upgrade_node(); +var packageJson = require("../package.json"); +pleaseUpgradeNode(packageJson); +var dynamicImport = new Function("module", "return import(module)"); +var promise; +var index = process.argv.indexOf("--experimental-cli"); +if (process.env.PRETTIER_EXPERIMENTAL_CLI || index !== -1) { + if (index !== -1) { + process.argv.splice(index, 1); + } + promise = dynamicImport("../internal/experimental-cli.mjs").then( + function(cli) { + return cli.__promise; + } + ); +} else { + promise = dynamicImport("../internal/legacy-cli.mjs").then(function runCli(cli) { + return cli.run(); + }); +} +module.exports.__promise = promise; diff --git a/node_modules/.bin/qrcode b/node_modules/.bin/qrcode new file mode 100644 index 0000000000000000000000000000000000000000..dd990b63cdbfcb266263496363f47ac8f86601d7 --- /dev/null +++ b/node_modules/.bin/qrcode @@ -0,0 +1,159 @@ +#!/usr/bin/env node +var yargs = require('yargs') +var qr = require('../lib') + +function save (file, text, options) { + qr.toFile(file, text, options, function (err, data) { + if (err) { + console.error('Error:', err.message) + process.exit(1) + } + + console.log('saved qrcode to: ' + file + '\n') + }) +} + +function print (text, options) { + options.type = 'terminal' + qr.toString(text, options, function (err, text) { + if (err) { + console.error('Error:', err.message) + process.exit(1) + } + + console.log(text) + }) +} + +function parseOptions (args) { + return { + version: args.qversion, + errorCorrectionLevel: args.error, + type: args.type, + small: !!args.small, + inverse: !!args.inverse, + maskPattern: args.mask, + margin: args.qzone, + width: args.width, + scale: args.scale, + color: { + light: args.lightcolor, + dark: args.darkcolor + } + } +} + +function processInputs (text, opts) { + if (!text.length) { + yargs.showHelp() + process.exit(1) + } + + if (opts.output) { + save(opts.output, text, parseOptions(opts)) + } else { + print(text, parseOptions(opts)) + } +} + +var argv = yargs + .detectLocale(false) + .usage('Usage: $0 [options] ') + .option('v', { + alias: 'qversion', + description: 'QR Code symbol version (1 - 40)', + group: 'QR Code options:', + type: 'number' + }) + .option('e', { + alias: 'error', + description: 'Error correction level', + choices: ['L', 'M', 'Q', 'H'], + group: 'QR Code options:' + }) + .option('m', { + alias: 'mask', + description: 'Mask pattern (0 - 7)', + group: 'QR Code options:', + type: 'number' + }) + .option('t', { + alias: 'type', + description: 'Output type', + choices: ['png', 'svg', 'utf8'], + implies: 'output', + group: 'Renderer options:' + }) + .option('i', { + alias: 'inverse', + type: 'boolean', + description: 'Invert colors', + group: 'Renderer options:' + }) + .option('w', { + alias: 'width', + description: 'Image width (px)', + conflicts: 'scale', + group: 'Renderer options:', + type: 'number' + }) + .option('s', { + alias: 'scale', + description: 'Scale factor', + conflicts: 'width', + group: 'Renderer options:', + type: 'number' + }) + .option('q', { + alias: 'qzone', + description: 'Quiet zone size', + group: 'Renderer options:', + type: 'number' + }) + .option('l', { + alias: 'lightcolor', + description: 'Light RGBA hex color', + group: 'Renderer options:' + }) + .option('d', { + alias: 'darkcolor', + description: 'Dark RGBA hex color', + group: 'Renderer options:' + }) + .option('small', { + type: 'boolean', + description: 'Output smaller QR code to terminal', + conflicts: 'type', + group: 'Renderer options:' + }) + .option('o', { + alias: 'output', + description: 'Output file' + }) + .help('h') + .alias('h', 'help') + .version() + .example('$0 "some text"', 'Draw in terminal window') + .example('$0 -o out.png "some text"', 'Save as png image') + .example('$0 -d F00 -o out.png "some text"', 'Use red as foreground color') + .parserConfiguration({'parse-numbers': false}) + .argv + +if (process.stdin.isTTY) { + processInputs(argv._.join(' '), argv) +} else { + var text = '' + process.stdin.setEncoding('utf8') + process.stdin.on('readable', function () { + var chunk = process.stdin.read() + if (chunk !== null) { + text += chunk + } + }) + + process.stdin.on('end', function () { + // this process can be run as a command outside of a tty so if there was no + // data on stdin read from argv + processInputs(text.length?text:argv._.join(' '), argv) + }) +} diff --git a/node_modules/.bin/rc b/node_modules/.bin/rc new file mode 100644 index 0000000000000000000000000000000000000000..ab05b6072b01701227f5e2765b327a8e409c6553 --- /dev/null +++ b/node_modules/.bin/rc @@ -0,0 +1,4 @@ +#! /usr/bin/env node +var rc = require('./index') + +console.log(JSON.stringify(rc(process.argv[2]), false, 2)) diff --git a/node_modules/.bin/rolldown b/node_modules/.bin/rolldown new file mode 100644 index 0000000000000000000000000000000000000000..814234a5ca3ad686aa259095d82e9212195b9664 --- /dev/null +++ b/node_modules/.bin/rolldown @@ -0,0 +1,2 @@ +#!/usr/bin/env node +import '../dist/cli.mjs'; diff --git a/node_modules/.bin/rollup b/node_modules/.bin/rollup new file mode 100644 index 0000000000000000000000000000000000000000..fc55f8b18eb22b0044b4a892cf06ea2efc91088e --- /dev/null +++ b/node_modules/.bin/rollup @@ -0,0 +1,1912 @@ +#!/usr/bin/env node +/* + @license + Rollup.js v4.62.0 + Sat, 13 Jun 2026 08:30:18 GMT - commit 5e0066d92defee0097f10fb814e63f60b2a7b612 + + https://github.com/rollup/rollup + + Released under the MIT License. +*/ +'use strict'; + +Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); + +const process$1 = require('node:process'); +const rollup = require('../shared/rollup.js'); +const require$$2 = require('util'); +const require$$0 = require('path'); +const require$$0$1 = require('fs'); +const parseAst_js = require('../shared/parseAst.js'); +const fseventsImporter = require('../shared/fsevents-importer.js'); +const promises = require('node:fs/promises'); +const path = require('node:path'); +const loadConfigFile_js = require('../shared/loadConfigFile.js'); +require('../native.js'); +require('node:perf_hooks'); +require('node:url'); +require('../getLogFilter.js'); + +const help = "rollup version 4.62.0\n=====================================\n\nUsage: rollup [options] \n\nOptions:\n\n-c, --config Use this config file (if argument is used but value\n is unspecified, Rollup will try to load configuration files in\n the following order:\n rollup.config.mjs -> rollup.config.cjs -> rollup.config.js)\n-d, --dir Directory for chunks (if absent, prints to stdout)\n-e, --external Comma-separate list of module IDs to exclude\n-f, --format Type of output (amd, cjs, es, iife, umd, system)\n-g, --globals Comma-separate list of `moduleID:Global` pairs\n-h, --help Show this help message\n-i, --input Input (alternative to )\n-m, --sourcemap Generate sourcemap (`-m inline` for inline map)\n-n, --name Name for UMD export\n-o, --file Single output file (if absent, prints to stdout)\n-p, --plugin Use the plugin specified (may be repeated)\n-v, --version Show version number\n-w, --watch Watch files in bundle and rebuild on changes\n--amd.autoId Generate the AMD ID based off the chunk name\n--amd.basePath Path to prepend to auto generated AMD ID\n--amd.define Function to use in place of `define`\n--amd.forceJsExtensionForImports Use `.js` extension in AMD imports\n--amd.id ID for AMD module (default is anonymous)\n--assetFileNames Name pattern for emitted assets\n--banner Code to insert at top of bundle (outside wrapper)\n--chunkFileNames Name pattern for emitted secondary chunks\n--compact Minify wrapper code\n--context Specify top-level `this` value\n--no-dynamicImportInCjs Write external dynamic CommonJS imports as require\n--entryFileNames Name pattern for emitted entry chunks\n--environment Settings passed to config file (see example)\n--no-esModule Do not add __esModule property\n--exports Specify export mode (auto, default, named, none)\n--extend Extend global variable defined by --name\n--no-externalImportAttributes Omit import attributes in \"es\" output\n--no-externalLiveBindings Do not generate code to support live bindings\n--failAfterWarnings Exit with an error if the build produced warnings\n--filterLogs Filter log messages\n--footer Code to insert at end of bundle (outside wrapper)\n--forceExit Force exit the process when done\n--no-freeze Do not freeze namespace objects\n--generatedCode Which code features to use (es5/es2015)\n--generatedCode.arrowFunctions Use arrow functions in generated code\n--generatedCode.constBindings Use \"const\" in generated code\n--generatedCode.objectShorthand Use shorthand properties in generated code\n--no-generatedCode.reservedNamesAsProps Always quote reserved names as props\n--generatedCode.symbols Use symbols in generated code\n--hashCharacters Use the specified character set for file hashes\n--no-hoistTransitiveImports Do not hoist transitive imports into entry chunks\n--importAttributesKey Use the specified keyword for import attributes\n--no-indent Don't indent result\n--inlineDynamicImports Create single bundle when using dynamic imports\n--no-interop Do not include interop block\n--intro Code to insert at top of bundle (inside wrapper)\n--logLevel Which kind of logs to display\n--no-makeAbsoluteExternalsRelative Prevent normalization of external imports\n--maxParallelFileOps How many files to read in parallel\n--minifyInternalExports Force or disable minification of internal exports\n--noConflict Generate a noConflict method for UMD globals\n--outro Code to insert at end of bundle (inside wrapper)\n--perf Display performance timings\n--no-preserveEntrySignatures Avoid facade chunks for entry points\n--preserveModules Preserve module structure\n--preserveModulesRoot Put preserved modules under this path at root level\n--preserveSymlinks Do not follow symlinks when resolving files\n--no-reexportProtoFromExternal Ignore `__proto__` in star re-exports\n--no-sanitizeFileName Do not replace invalid characters in file names\n--shimMissingExports Create shim variables for missing exports\n--silent Don't print warnings\n--sourcemapBaseUrl Emit absolute sourcemap URLs with given base\n--sourcemapDebugIds Emit unique debug ids in source and sourcemaps\n--sourcemapExcludeSources Do not include source code in source maps\n--sourcemapFile Specify bundle position for source maps\n--sourcemapFileNames Name pattern for emitted sourcemaps\n--stdin=ext Specify file extension used for stdin input\n--no-stdin Do not read \"-\" from stdin\n--no-strict Don't emit `\"use strict\";` in the generated modules\n--strictDeprecations Throw errors for deprecated features\n--no-systemNullSetters Do not replace empty SystemJS setters with `null`\n--no-treeshake Disable tree-shaking optimisations\n--no-treeshake.annotations Ignore pure call annotations\n--treeshake.correctVarValueBeforeDeclaration Deoptimize variables until declared\n--treeshake.manualPureFunctions Manually declare functions as pure\n--no-treeshake.moduleSideEffects Assume modules have no side effects\n--no-treeshake.propertyReadSideEffects Ignore property access side effects\n--no-treeshake.tryCatchDeoptimization Do not turn off try-catch-tree-shaking\n--no-treeshake.unknownGlobalSideEffects Assume unknown globals do not throw\n--validate Validate output\n--waitForBundleInput Wait for bundle input files\n--watch.allowInputInsideOutputPath Whether the input path is allowed to be a\n subpath of the output path\n--watch.buildDelay Throttle watch rebuilds\n--no-watch.clearScreen Do not clear the screen when rebuilding\n--watch.exclude Exclude files from being watched\n--watch.include Limit watching to specified files\n--watch.onBundleEnd Shell command to run on `\"BUNDLE_END\"` event\n--watch.onBundleStart Shell command to run on `\"BUNDLE_START\"` event\n--watch.onEnd Shell command to run on `\"END\"` event\n--watch.onError Shell command to run on `\"ERROR\"` event\n--watch.onStart Shell command to run on `\"START\"` event\n--watch.skipWrite Do not write files to disk when watching\n\nExamples:\n\n# use settings in config file\nrollup -c\n\n# in config file, process.env.INCLUDE_DEPS === 'true'\n# and process.env.BUILD === 'production'\nrollup -c --environment INCLUDE_DEPS,BUILD:production\n\n# create CommonJS bundle.js from src/main.js\nrollup --format=cjs --file=bundle.js -- src/main.js\n\n# create self-executing IIFE using `window.jQuery`\n# and `window._` as external globals\nrollup -f iife --globals jquery:jQuery,lodash:_ \\\n -i src/app.js -o build/app.js -m build/app.js.map\n\nNotes:\n\n* When piping to stdout, only inline sourcemaps are permitted\n\nFor more information visit https://rollupjs.org\n"; + +/** + * @license + * Copyright (c) 2016, Contributors + * SPDX-License-Identifier: ISC + */ +function camelCase(str) { + // Handle the case where an argument is provided as camel case, e.g., fooBar. + // by ensuring that the string isn't already mixed case: + const isCamelCase = str !== str.toLowerCase() && str !== str.toUpperCase(); + if (!isCamelCase) { + str = str.toLowerCase(); + } + if (str.indexOf('-') === -1 && str.indexOf('_') === -1) { + return str; + } + else { + let camelcase = ''; + let nextChrUpper = false; + const leadingHyphens = str.match(/^-+/); + for (let i = leadingHyphens ? leadingHyphens[0].length : 0; i < str.length; i++) { + let chr = str.charAt(i); + if (nextChrUpper) { + nextChrUpper = false; + chr = chr.toUpperCase(); + } + if (i !== 0 && (chr === '-' || chr === '_')) { + nextChrUpper = true; + } + else if (chr !== '-' && chr !== '_') { + camelcase += chr; + } + } + return camelcase; + } +} +function decamelize(str, joinString) { + const lowercase = str.toLowerCase(); + joinString = joinString || '-'; + let notCamelcase = ''; + for (let i = 0; i < str.length; i++) { + const chrLower = lowercase.charAt(i); + const chrString = str.charAt(i); + if (chrLower !== chrString && i > 0) { + notCamelcase += `${joinString}${lowercase.charAt(i)}`; + } + else { + notCamelcase += chrString; + } + } + return notCamelcase; +} +function looksLikeNumber(x) { + if (x === null || x === undefined) + return false; + // if loaded from config, may already be a number. + if (typeof x === 'number') + return true; + // hexadecimal. + if (/^0x[0-9a-f]+$/i.test(x)) + return true; + // don't treat 0123 as a number; as it drops the leading '0'. + if (/^0[^.]/.test(x)) + return false; + return /^[-]?(?:\d+(?:\.\d*)?|\.\d+)(e[-+]?\d+)?$/.test(x); +} + +/** + * @license + * Copyright (c) 2016, Contributors + * SPDX-License-Identifier: ISC + */ +// take an un-split argv string and tokenize it. +function tokenizeArgString(argString) { + if (Array.isArray(argString)) { + return argString.map(e => typeof e !== 'string' ? e + '' : e); + } + argString = argString.trim(); + let i = 0; + let prevC = null; + let c = null; + let opening = null; + const args = []; + for (let ii = 0; ii < argString.length; ii++) { + prevC = c; + c = argString.charAt(ii); + // split on spaces unless we're in quotes. + if (c === ' ' && !opening) { + if (!(prevC === ' ')) { + i++; + } + continue; + } + // don't split the string if we're in matching + // opening or closing single and double quotes. + if (c === opening) { + opening = null; + } + else if ((c === "'" || c === '"') && !opening) { + opening = c; + } + if (!args[i]) + args[i] = ''; + args[i] += c; + } + return args; +} + +/** + * @license + * Copyright (c) 2016, Contributors + * SPDX-License-Identifier: ISC + */ +var DefaultValuesForTypeKey; +(function (DefaultValuesForTypeKey) { + DefaultValuesForTypeKey["BOOLEAN"] = "boolean"; + DefaultValuesForTypeKey["STRING"] = "string"; + DefaultValuesForTypeKey["NUMBER"] = "number"; + DefaultValuesForTypeKey["ARRAY"] = "array"; +})(DefaultValuesForTypeKey || (DefaultValuesForTypeKey = {})); + +/** + * @license + * Copyright (c) 2016, Contributors + * SPDX-License-Identifier: ISC + */ +let mixin; +class YargsParser { + constructor(_mixin) { + mixin = _mixin; + } + parse(argsInput, options) { + const opts = Object.assign({ + alias: undefined, + array: undefined, + boolean: undefined, + config: undefined, + configObjects: undefined, + configuration: undefined, + coerce: undefined, + count: undefined, + default: undefined, + envPrefix: undefined, + narg: undefined, + normalize: undefined, + string: undefined, + number: undefined, + __: undefined, + key: undefined + }, options); + // allow a string argument to be passed in rather + // than an argv array. + const args = tokenizeArgString(argsInput); + // tokenizeArgString adds extra quotes to args if argsInput is a string + // only strip those extra quotes in processValue if argsInput is a string + const inputIsString = typeof argsInput === 'string'; + // aliases might have transitive relationships, normalize this. + const aliases = combineAliases(Object.assign(Object.create(null), opts.alias)); + const configuration = Object.assign({ + 'boolean-negation': true, + 'camel-case-expansion': true, + 'combine-arrays': false, + 'dot-notation': true, + 'duplicate-arguments-array': true, + 'flatten-duplicate-arrays': true, + 'greedy-arrays': true, + 'halt-at-non-option': false, + 'nargs-eats-options': false, + 'negation-prefix': 'no-', + 'parse-numbers': true, + 'parse-positional-numbers': true, + 'populate--': false, + 'set-placeholder-key': false, + 'short-option-groups': true, + 'strip-aliased': false, + 'strip-dashed': false, + 'unknown-options-as-args': false + }, opts.configuration); + const defaults = Object.assign(Object.create(null), opts.default); + const configObjects = opts.configObjects || []; + const envPrefix = opts.envPrefix; + const notFlagsOption = configuration['populate--']; + const notFlagsArgv = notFlagsOption ? '--' : '_'; + const newAliases = Object.create(null); + const defaulted = Object.create(null); + // allow a i18n handler to be passed in, default to a fake one (util.format). + const __ = opts.__ || mixin.format; + const flags = { + aliases: Object.create(null), + arrays: Object.create(null), + bools: Object.create(null), + strings: Object.create(null), + numbers: Object.create(null), + counts: Object.create(null), + normalize: Object.create(null), + configs: Object.create(null), + nargs: Object.create(null), + coercions: Object.create(null), + keys: [] + }; + const negative = /^-([0-9]+(\.[0-9]+)?|\.[0-9]+)$/; + const negatedBoolean = new RegExp('^--' + configuration['negation-prefix'] + '(.+)'); + [].concat(opts.array || []).filter(Boolean).forEach(function (opt) { + const key = typeof opt === 'object' ? opt.key : opt; + // assign to flags[bools|strings|numbers] + const assignment = Object.keys(opt).map(function (key) { + const arrayFlagKeys = { + boolean: 'bools', + string: 'strings', + number: 'numbers' + }; + return arrayFlagKeys[key]; + }).filter(Boolean).pop(); + // assign key to be coerced + if (assignment) { + flags[assignment][key] = true; + } + flags.arrays[key] = true; + flags.keys.push(key); + }); + [].concat(opts.boolean || []).filter(Boolean).forEach(function (key) { + flags.bools[key] = true; + flags.keys.push(key); + }); + [].concat(opts.string || []).filter(Boolean).forEach(function (key) { + flags.strings[key] = true; + flags.keys.push(key); + }); + [].concat(opts.number || []).filter(Boolean).forEach(function (key) { + flags.numbers[key] = true; + flags.keys.push(key); + }); + [].concat(opts.count || []).filter(Boolean).forEach(function (key) { + flags.counts[key] = true; + flags.keys.push(key); + }); + [].concat(opts.normalize || []).filter(Boolean).forEach(function (key) { + flags.normalize[key] = true; + flags.keys.push(key); + }); + if (typeof opts.narg === 'object') { + Object.entries(opts.narg).forEach(([key, value]) => { + if (typeof value === 'number') { + flags.nargs[key] = value; + flags.keys.push(key); + } + }); + } + if (typeof opts.coerce === 'object') { + Object.entries(opts.coerce).forEach(([key, value]) => { + if (typeof value === 'function') { + flags.coercions[key] = value; + flags.keys.push(key); + } + }); + } + if (typeof opts.config !== 'undefined') { + if (Array.isArray(opts.config) || typeof opts.config === 'string') { + [].concat(opts.config).filter(Boolean).forEach(function (key) { + flags.configs[key] = true; + }); + } + else if (typeof opts.config === 'object') { + Object.entries(opts.config).forEach(([key, value]) => { + if (typeof value === 'boolean' || typeof value === 'function') { + flags.configs[key] = value; + } + }); + } + } + // create a lookup table that takes into account all + // combinations of aliases: {f: ['foo'], foo: ['f']} + extendAliases(opts.key, aliases, opts.default, flags.arrays); + // apply default values to all aliases. + Object.keys(defaults).forEach(function (key) { + (flags.aliases[key] || []).forEach(function (alias) { + defaults[alias] = defaults[key]; + }); + }); + let error = null; + checkConfiguration(); + let notFlags = []; + const argv = Object.assign(Object.create(null), { _: [] }); + // TODO(bcoe): for the first pass at removing object prototype we didn't + // remove all prototypes from objects returned by this API, we might want + // to gradually move towards doing so. + const argvReturn = {}; + for (let i = 0; i < args.length; i++) { + const arg = args[i]; + const truncatedArg = arg.replace(/^-{3,}/, '---'); + let broken; + let key; + let letters; + let m; + let next; + let value; + // any unknown option (except for end-of-options, "--") + if (arg !== '--' && /^-/.test(arg) && isUnknownOptionAsArg(arg)) { + pushPositional(arg); + // ---, ---=, ----, etc, + } + else if (truncatedArg.match(/^---+(=|$)/)) { + // options without key name are invalid. + pushPositional(arg); + continue; + // -- separated by = + } + else if (arg.match(/^--.+=/) || (!configuration['short-option-groups'] && arg.match(/^-.+=/))) { + // Using [\s\S] instead of . because js doesn't support the + // 'dotall' regex modifier. See: + // http://stackoverflow.com/a/1068308/13216 + m = arg.match(/^--?([^=]+)=([\s\S]*)$/); + // arrays format = '--f=a b c' + if (m !== null && Array.isArray(m) && m.length >= 3) { + if (checkAllAliases(m[1], flags.arrays)) { + i = eatArray(i, m[1], args, m[2]); + } + else if (checkAllAliases(m[1], flags.nargs) !== false) { + // nargs format = '--f=monkey washing cat' + i = eatNargs(i, m[1], args, m[2]); + } + else { + setArg(m[1], m[2], true); + } + } + } + else if (arg.match(negatedBoolean) && configuration['boolean-negation']) { + m = arg.match(negatedBoolean); + if (m !== null && Array.isArray(m) && m.length >= 2) { + key = m[1]; + setArg(key, checkAllAliases(key, flags.arrays) ? [false] : false); + } + // -- separated by space. + } + else if (arg.match(/^--.+/) || (!configuration['short-option-groups'] && arg.match(/^-[^-]+/))) { + m = arg.match(/^--?(.+)/); + if (m !== null && Array.isArray(m) && m.length >= 2) { + key = m[1]; + if (checkAllAliases(key, flags.arrays)) { + // array format = '--foo a b c' + i = eatArray(i, key, args); + } + else if (checkAllAliases(key, flags.nargs) !== false) { + // nargs format = '--foo a b c' + // should be truthy even if: flags.nargs[key] === 0 + i = eatNargs(i, key, args); + } + else { + next = args[i + 1]; + if (next !== undefined && (!next.match(/^-/) || + next.match(negative)) && + !checkAllAliases(key, flags.bools) && + !checkAllAliases(key, flags.counts)) { + setArg(key, next); + i++; + } + else if (/^(true|false)$/.test(next)) { + setArg(key, next); + i++; + } + else { + setArg(key, defaultValue(key)); + } + } + } + // dot-notation flag separated by '='. + } + else if (arg.match(/^-.\..+=/)) { + m = arg.match(/^-([^=]+)=([\s\S]*)$/); + if (m !== null && Array.isArray(m) && m.length >= 3) { + setArg(m[1], m[2]); + } + // dot-notation flag separated by space. + } + else if (arg.match(/^-.\..+/) && !arg.match(negative)) { + next = args[i + 1]; + m = arg.match(/^-(.\..+)/); + if (m !== null && Array.isArray(m) && m.length >= 2) { + key = m[1]; + if (next !== undefined && !next.match(/^-/) && + !checkAllAliases(key, flags.bools) && + !checkAllAliases(key, flags.counts)) { + setArg(key, next); + i++; + } + else { + setArg(key, defaultValue(key)); + } + } + } + else if (arg.match(/^-[^-]+/) && !arg.match(negative)) { + letters = arg.slice(1, -1).split(''); + broken = false; + for (let j = 0; j < letters.length; j++) { + next = arg.slice(j + 2); + if (letters[j + 1] && letters[j + 1] === '=') { + value = arg.slice(j + 3); + key = letters[j]; + if (checkAllAliases(key, flags.arrays)) { + // array format = '-f=a b c' + i = eatArray(i, key, args, value); + } + else if (checkAllAliases(key, flags.nargs) !== false) { + // nargs format = '-f=monkey washing cat' + i = eatNargs(i, key, args, value); + } + else { + setArg(key, value); + } + broken = true; + break; + } + if (next === '-') { + setArg(letters[j], next); + continue; + } + // current letter is an alphabetic character and next value is a number + if (/[A-Za-z]/.test(letters[j]) && + /^-?\d+(\.\d*)?(e-?\d+)?$/.test(next) && + checkAllAliases(next, flags.bools) === false) { + setArg(letters[j], next); + broken = true; + break; + } + if (letters[j + 1] && letters[j + 1].match(/\W/)) { + setArg(letters[j], next); + broken = true; + break; + } + else { + setArg(letters[j], defaultValue(letters[j])); + } + } + key = arg.slice(-1)[0]; + if (!broken && key !== '-') { + if (checkAllAliases(key, flags.arrays)) { + // array format = '-f a b c' + i = eatArray(i, key, args); + } + else if (checkAllAliases(key, flags.nargs) !== false) { + // nargs format = '-f a b c' + // should be truthy even if: flags.nargs[key] === 0 + i = eatNargs(i, key, args); + } + else { + next = args[i + 1]; + if (next !== undefined && (!/^(-|--)[^-]/.test(next) || + next.match(negative)) && + !checkAllAliases(key, flags.bools) && + !checkAllAliases(key, flags.counts)) { + setArg(key, next); + i++; + } + else if (/^(true|false)$/.test(next)) { + setArg(key, next); + i++; + } + else { + setArg(key, defaultValue(key)); + } + } + } + } + else if (arg.match(/^-[0-9]$/) && + arg.match(negative) && + checkAllAliases(arg.slice(1), flags.bools)) { + // single-digit boolean alias, e.g: xargs -0 + key = arg.slice(1); + setArg(key, defaultValue(key)); + } + else if (arg === '--') { + notFlags = args.slice(i + 1); + break; + } + else if (configuration['halt-at-non-option']) { + notFlags = args.slice(i); + break; + } + else { + pushPositional(arg); + } + } + // order of precedence: + // 1. command line arg + // 2. value from env var + // 3. value from config file + // 4. value from config objects + // 5. configured default value + applyEnvVars(argv, true); // special case: check env vars that point to config file + applyEnvVars(argv, false); + setConfig(argv); + setConfigObjects(); + applyDefaultsAndAliases(argv, flags.aliases, defaults, true); + applyCoercions(argv); + if (configuration['set-placeholder-key']) + setPlaceholderKeys(argv); + // for any counts either not in args or without an explicit default, set to 0 + Object.keys(flags.counts).forEach(function (key) { + if (!hasKey(argv, key.split('.'))) + setArg(key, 0); + }); + // '--' defaults to undefined. + if (notFlagsOption && notFlags.length) + argv[notFlagsArgv] = []; + notFlags.forEach(function (key) { + argv[notFlagsArgv].push(key); + }); + if (configuration['camel-case-expansion'] && configuration['strip-dashed']) { + Object.keys(argv).filter(key => key !== '--' && key.includes('-')).forEach(key => { + delete argv[key]; + }); + } + if (configuration['strip-aliased']) { + [].concat(...Object.keys(aliases).map(k => aliases[k])).forEach(alias => { + if (configuration['camel-case-expansion'] && alias.includes('-')) { + delete argv[alias.split('.').map(prop => camelCase(prop)).join('.')]; + } + delete argv[alias]; + }); + } + // Push argument into positional array, applying numeric coercion: + function pushPositional(arg) { + const maybeCoercedNumber = maybeCoerceNumber('_', arg); + if (typeof maybeCoercedNumber === 'string' || typeof maybeCoercedNumber === 'number') { + argv._.push(maybeCoercedNumber); + } + } + // how many arguments should we consume, based + // on the nargs option? + function eatNargs(i, key, args, argAfterEqualSign) { + let ii; + let toEat = checkAllAliases(key, flags.nargs); + // NaN has a special meaning for the array type, indicating that one or + // more values are expected. + toEat = typeof toEat !== 'number' || isNaN(toEat) ? 1 : toEat; + if (toEat === 0) { + if (!isUndefined(argAfterEqualSign)) { + error = Error(__('Argument unexpected for: %s', key)); + } + setArg(key, defaultValue(key)); + return i; + } + let available = isUndefined(argAfterEqualSign) ? 0 : 1; + if (configuration['nargs-eats-options']) { + // classic behavior, yargs eats positional and dash arguments. + if (args.length - (i + 1) + available < toEat) { + error = Error(__('Not enough arguments following: %s', key)); + } + available = toEat; + } + else { + // nargs will not consume flag arguments, e.g., -abc, --foo, + // and terminates when one is observed. + for (ii = i + 1; ii < args.length; ii++) { + if (!args[ii].match(/^-[^0-9]/) || args[ii].match(negative) || isUnknownOptionAsArg(args[ii])) + available++; + else + break; + } + if (available < toEat) + error = Error(__('Not enough arguments following: %s', key)); + } + let consumed = Math.min(available, toEat); + if (!isUndefined(argAfterEqualSign) && consumed > 0) { + setArg(key, argAfterEqualSign); + consumed--; + } + for (ii = i + 1; ii < (consumed + i + 1); ii++) { + setArg(key, args[ii]); + } + return (i + consumed); + } + // if an option is an array, eat all non-hyphenated arguments + // following it... YUM! + // e.g., --foo apple banana cat becomes ["apple", "banana", "cat"] + function eatArray(i, key, args, argAfterEqualSign) { + let argsToSet = []; + let next = argAfterEqualSign || args[i + 1]; + // If both array and nargs are configured, enforce the nargs count: + const nargsCount = checkAllAliases(key, flags.nargs); + if (checkAllAliases(key, flags.bools) && !(/^(true|false)$/.test(next))) { + argsToSet.push(true); + } + else if (isUndefined(next) || + (isUndefined(argAfterEqualSign) && /^-/.test(next) && !negative.test(next) && !isUnknownOptionAsArg(next))) { + // for keys without value ==> argsToSet remains an empty [] + // set user default value, if available + if (defaults[key] !== undefined) { + const defVal = defaults[key]; + argsToSet = Array.isArray(defVal) ? defVal : [defVal]; + } + } + else { + // value in --option=value is eaten as is + if (!isUndefined(argAfterEqualSign)) { + argsToSet.push(processValue(key, argAfterEqualSign, true)); + } + for (let ii = i + 1; ii < args.length; ii++) { + if ((!configuration['greedy-arrays'] && argsToSet.length > 0) || + (nargsCount && typeof nargsCount === 'number' && argsToSet.length >= nargsCount)) + break; + next = args[ii]; + if (/^-/.test(next) && !negative.test(next) && !isUnknownOptionAsArg(next)) + break; + i = ii; + argsToSet.push(processValue(key, next, inputIsString)); + } + } + // If both array and nargs are configured, create an error if less than + // nargs positionals were found. NaN has special meaning, indicating + // that at least one value is required (more are okay). + if (typeof nargsCount === 'number' && ((nargsCount && argsToSet.length < nargsCount) || + (isNaN(nargsCount) && argsToSet.length === 0))) { + error = Error(__('Not enough arguments following: %s', key)); + } + setArg(key, argsToSet); + return i; + } + function setArg(key, val, shouldStripQuotes = inputIsString) { + if (/-/.test(key) && configuration['camel-case-expansion']) { + const alias = key.split('.').map(function (prop) { + return camelCase(prop); + }).join('.'); + addNewAlias(key, alias); + } + const value = processValue(key, val, shouldStripQuotes); + const splitKey = key.split('.'); + setKey(argv, splitKey, value); + // handle populating aliases of the full key + if (flags.aliases[key]) { + flags.aliases[key].forEach(function (x) { + const keyProperties = x.split('.'); + setKey(argv, keyProperties, value); + }); + } + // handle populating aliases of the first element of the dot-notation key + if (splitKey.length > 1 && configuration['dot-notation']) { + (flags.aliases[splitKey[0]] || []).forEach(function (x) { + let keyProperties = x.split('.'); + // expand alias with nested objects in key + const a = [].concat(splitKey); + a.shift(); // nuke the old key. + keyProperties = keyProperties.concat(a); + // populate alias only if is not already an alias of the full key + // (already populated above) + if (!(flags.aliases[key] || []).includes(keyProperties.join('.'))) { + setKey(argv, keyProperties, value); + } + }); + } + // Set normalize getter and setter when key is in 'normalize' but isn't an array + if (checkAllAliases(key, flags.normalize) && !checkAllAliases(key, flags.arrays)) { + const keys = [key].concat(flags.aliases[key] || []); + keys.forEach(function (key) { + Object.defineProperty(argvReturn, key, { + enumerable: true, + get() { + return val; + }, + set(value) { + val = typeof value === 'string' ? mixin.normalize(value) : value; + } + }); + }); + } + } + function addNewAlias(key, alias) { + if (!(flags.aliases[key] && flags.aliases[key].length)) { + flags.aliases[key] = [alias]; + newAliases[alias] = true; + } + if (!(flags.aliases[alias] && flags.aliases[alias].length)) { + addNewAlias(alias, key); + } + } + function processValue(key, val, shouldStripQuotes) { + // strings may be quoted, clean this up as we assign values. + if (shouldStripQuotes) { + val = stripQuotes(val); + } + // handle parsing boolean arguments --foo=true --bar false. + if (checkAllAliases(key, flags.bools) || checkAllAliases(key, flags.counts)) { + if (typeof val === 'string') + val = val === 'true'; + } + let value = Array.isArray(val) + ? val.map(function (v) { return maybeCoerceNumber(key, v); }) + : maybeCoerceNumber(key, val); + // increment a count given as arg (either no value or value parsed as boolean) + if (checkAllAliases(key, flags.counts) && (isUndefined(value) || typeof value === 'boolean')) { + value = increment(); + } + // Set normalized value when key is in 'normalize' and in 'arrays' + if (checkAllAliases(key, flags.normalize) && checkAllAliases(key, flags.arrays)) { + if (Array.isArray(val)) + value = val.map((val) => { return mixin.normalize(val); }); + else + value = mixin.normalize(val); + } + return value; + } + function maybeCoerceNumber(key, value) { + if (!configuration['parse-positional-numbers'] && key === '_') + return value; + if (!checkAllAliases(key, flags.strings) && !checkAllAliases(key, flags.bools) && !Array.isArray(value)) { + const shouldCoerceNumber = looksLikeNumber(value) && configuration['parse-numbers'] && (Number.isSafeInteger(Math.floor(parseFloat(`${value}`)))); + if (shouldCoerceNumber || (!isUndefined(value) && checkAllAliases(key, flags.numbers))) { + value = Number(value); + } + } + return value; + } + // set args from config.json file, this should be + // applied last so that defaults can be applied. + function setConfig(argv) { + const configLookup = Object.create(null); + // expand defaults/aliases, in-case any happen to reference + // the config.json file. + applyDefaultsAndAliases(configLookup, flags.aliases, defaults); + Object.keys(flags.configs).forEach(function (configKey) { + const configPath = argv[configKey] || configLookup[configKey]; + if (configPath) { + try { + let config = null; + const resolvedConfigPath = mixin.resolve(mixin.cwd(), configPath); + const resolveConfig = flags.configs[configKey]; + if (typeof resolveConfig === 'function') { + try { + config = resolveConfig(resolvedConfigPath); + } + catch (e) { + config = e; + } + if (config instanceof Error) { + error = config; + return; + } + } + else { + config = mixin.require(resolvedConfigPath); + } + setConfigObject(config); + } + catch (ex) { + // Deno will receive a PermissionDenied error if an attempt is + // made to load config without the --allow-read flag: + if (ex.name === 'PermissionDenied') + error = ex; + else if (argv[configKey]) + error = Error(__('Invalid JSON config file: %s', configPath)); + } + } + }); + } + // set args from config object. + // it recursively checks nested objects. + function setConfigObject(config, prev) { + Object.keys(config).forEach(function (key) { + const value = config[key]; + const fullKey = prev ? prev + '.' + key : key; + // if the value is an inner object and we have dot-notation + // enabled, treat inner objects in config the same as + // heavily nested dot notations (foo.bar.apple). + if (typeof value === 'object' && value !== null && !Array.isArray(value) && configuration['dot-notation']) { + // if the value is an object but not an array, check nested object + setConfigObject(value, fullKey); + } + else { + // setting arguments via CLI takes precedence over + // values within the config file. + if (!hasKey(argv, fullKey.split('.')) || (checkAllAliases(fullKey, flags.arrays) && configuration['combine-arrays'])) { + setArg(fullKey, value); + } + } + }); + } + // set all config objects passed in opts + function setConfigObjects() { + if (typeof configObjects !== 'undefined') { + configObjects.forEach(function (configObject) { + setConfigObject(configObject); + }); + } + } + function applyEnvVars(argv, configOnly) { + if (typeof envPrefix === 'undefined') + return; + const prefix = typeof envPrefix === 'string' ? envPrefix : ''; + const env = mixin.env(); + Object.keys(env).forEach(function (envVar) { + if (prefix === '' || envVar.lastIndexOf(prefix, 0) === 0) { + // get array of nested keys and convert them to camel case + const keys = envVar.split('__').map(function (key, i) { + if (i === 0) { + key = key.substring(prefix.length); + } + return camelCase(key); + }); + if (((configOnly && flags.configs[keys.join('.')]) || !configOnly) && !hasKey(argv, keys)) { + setArg(keys.join('.'), env[envVar]); + } + } + }); + } + function applyCoercions(argv) { + let coerce; + const applied = new Set(); + Object.keys(argv).forEach(function (key) { + if (!applied.has(key)) { // If we haven't already coerced this option via one of its aliases + coerce = checkAllAliases(key, flags.coercions); + if (typeof coerce === 'function') { + try { + const value = maybeCoerceNumber(key, coerce(argv[key])); + ([].concat(flags.aliases[key] || [], key)).forEach(ali => { + applied.add(ali); + argv[ali] = value; + }); + } + catch (err) { + error = err; + } + } + } + }); + } + function setPlaceholderKeys(argv) { + flags.keys.forEach((key) => { + // don't set placeholder keys for dot notation options 'foo.bar'. + if (~key.indexOf('.')) + return; + if (typeof argv[key] === 'undefined') + argv[key] = undefined; + }); + return argv; + } + function applyDefaultsAndAliases(obj, aliases, defaults, canLog = false) { + Object.keys(defaults).forEach(function (key) { + if (!hasKey(obj, key.split('.'))) { + setKey(obj, key.split('.'), defaults[key]); + if (canLog) + defaulted[key] = true; + (aliases[key] || []).forEach(function (x) { + if (hasKey(obj, x.split('.'))) + return; + setKey(obj, x.split('.'), defaults[key]); + }); + } + }); + } + function hasKey(obj, keys) { + let o = obj; + if (!configuration['dot-notation']) + keys = [keys.join('.')]; + keys.slice(0, -1).forEach(function (key) { + o = (o[key] || {}); + }); + const key = keys[keys.length - 1]; + if (typeof o !== 'object') + return false; + else + return key in o; + } + function setKey(obj, keys, value) { + let o = obj; + if (!configuration['dot-notation']) + keys = [keys.join('.')]; + keys.slice(0, -1).forEach(function (key) { + // TODO(bcoe): in the next major version of yargs, switch to + // Object.create(null) for dot notation: + key = sanitizeKey(key); + if (typeof o === 'object' && o[key] === undefined) { + o[key] = {}; + } + if (typeof o[key] !== 'object' || Array.isArray(o[key])) { + // ensure that o[key] is an array, and that the last item is an empty object. + if (Array.isArray(o[key])) { + o[key].push({}); + } + else { + o[key] = [o[key], {}]; + } + // we want to update the empty object at the end of the o[key] array, so set o to that object + o = o[key][o[key].length - 1]; + } + else { + o = o[key]; + } + }); + // TODO(bcoe): in the next major version of yargs, switch to + // Object.create(null) for dot notation: + const key = sanitizeKey(keys[keys.length - 1]); + const isTypeArray = checkAllAliases(keys.join('.'), flags.arrays); + const isValueArray = Array.isArray(value); + let duplicate = configuration['duplicate-arguments-array']; + // nargs has higher priority than duplicate + if (!duplicate && checkAllAliases(key, flags.nargs)) { + duplicate = true; + if ((!isUndefined(o[key]) && flags.nargs[key] === 1) || (Array.isArray(o[key]) && o[key].length === flags.nargs[key])) { + o[key] = undefined; + } + } + if (value === increment()) { + o[key] = increment(o[key]); + } + else if (Array.isArray(o[key])) { + if (duplicate && isTypeArray && isValueArray) { + o[key] = configuration['flatten-duplicate-arrays'] ? o[key].concat(value) : (Array.isArray(o[key][0]) ? o[key] : [o[key]]).concat([value]); + } + else if (!duplicate && Boolean(isTypeArray) === Boolean(isValueArray)) { + o[key] = value; + } + else { + o[key] = o[key].concat([value]); + } + } + else if (o[key] === undefined && isTypeArray) { + o[key] = isValueArray ? value : [value]; + } + else if (duplicate && !(o[key] === undefined || + checkAllAliases(key, flags.counts) || + checkAllAliases(key, flags.bools))) { + o[key] = [o[key], value]; + } + else { + o[key] = value; + } + } + // extend the aliases list with inferred aliases. + function extendAliases(...args) { + args.forEach(function (obj) { + Object.keys(obj || {}).forEach(function (key) { + // short-circuit if we've already added a key + // to the aliases array, for example it might + // exist in both 'opts.default' and 'opts.key'. + if (flags.aliases[key]) + return; + flags.aliases[key] = [].concat(aliases[key] || []); + // For "--option-name", also set argv.optionName + flags.aliases[key].concat(key).forEach(function (x) { + if (/-/.test(x) && configuration['camel-case-expansion']) { + const c = camelCase(x); + if (c !== key && flags.aliases[key].indexOf(c) === -1) { + flags.aliases[key].push(c); + newAliases[c] = true; + } + } + }); + // For "--optionName", also set argv['option-name'] + flags.aliases[key].concat(key).forEach(function (x) { + if (x.length > 1 && /[A-Z]/.test(x) && configuration['camel-case-expansion']) { + const c = decamelize(x, '-'); + if (c !== key && flags.aliases[key].indexOf(c) === -1) { + flags.aliases[key].push(c); + newAliases[c] = true; + } + } + }); + flags.aliases[key].forEach(function (x) { + flags.aliases[x] = [key].concat(flags.aliases[key].filter(function (y) { + return x !== y; + })); + }); + }); + }); + } + function checkAllAliases(key, flag) { + const toCheck = [].concat(flags.aliases[key] || [], key); + const keys = Object.keys(flag); + const setAlias = toCheck.find(key => keys.includes(key)); + return setAlias ? flag[setAlias] : false; + } + function hasAnyFlag(key) { + const flagsKeys = Object.keys(flags); + const toCheck = [].concat(flagsKeys.map(k => flags[k])); + return toCheck.some(function (flag) { + return Array.isArray(flag) ? flag.includes(key) : flag[key]; + }); + } + function hasFlagsMatching(arg, ...patterns) { + const toCheck = [].concat(...patterns); + return toCheck.some(function (pattern) { + const match = arg.match(pattern); + return match && hasAnyFlag(match[1]); + }); + } + // based on a simplified version of the short flag group parsing logic + function hasAllShortFlags(arg) { + // if this is a negative number, or doesn't start with a single hyphen, it's not a short flag group + if (arg.match(negative) || !arg.match(/^-[^-]+/)) { + return false; + } + let hasAllFlags = true; + let next; + const letters = arg.slice(1).split(''); + for (let j = 0; j < letters.length; j++) { + next = arg.slice(j + 2); + if (!hasAnyFlag(letters[j])) { + hasAllFlags = false; + break; + } + if ((letters[j + 1] && letters[j + 1] === '=') || + next === '-' || + (/[A-Za-z]/.test(letters[j]) && /^-?\d+(\.\d*)?(e-?\d+)?$/.test(next)) || + (letters[j + 1] && letters[j + 1].match(/\W/))) { + break; + } + } + return hasAllFlags; + } + function isUnknownOptionAsArg(arg) { + return configuration['unknown-options-as-args'] && isUnknownOption(arg); + } + function isUnknownOption(arg) { + arg = arg.replace(/^-{3,}/, '--'); + // ignore negative numbers + if (arg.match(negative)) { + return false; + } + // if this is a short option group and all of them are configured, it isn't unknown + if (hasAllShortFlags(arg)) { + return false; + } + // e.g. '--count=2' + const flagWithEquals = /^-+([^=]+?)=[\s\S]*$/; + // e.g. '-a' or '--arg' + const normalFlag = /^-+([^=]+?)$/; + // e.g. '-a-' + const flagEndingInHyphen = /^-+([^=]+?)-$/; + // e.g. '-abc123' + const flagEndingInDigits = /^-+([^=]+?\d+)$/; + // e.g. '-a/usr/local' + const flagEndingInNonWordCharacters = /^-+([^=]+?)\W+.*$/; + // check the different types of flag styles, including negatedBoolean, a pattern defined near the start of the parse method + return !hasFlagsMatching(arg, flagWithEquals, negatedBoolean, normalFlag, flagEndingInHyphen, flagEndingInDigits, flagEndingInNonWordCharacters); + } + // make a best effort to pick a default value + // for an option based on name and type. + function defaultValue(key) { + if (!checkAllAliases(key, flags.bools) && + !checkAllAliases(key, flags.counts) && + `${key}` in defaults) { + return defaults[key]; + } + else { + return defaultForType(guessType(key)); + } + } + // return a default value, given the type of a flag., + function defaultForType(type) { + const def = { + [DefaultValuesForTypeKey.BOOLEAN]: true, + [DefaultValuesForTypeKey.STRING]: '', + [DefaultValuesForTypeKey.NUMBER]: undefined, + [DefaultValuesForTypeKey.ARRAY]: [] + }; + return def[type]; + } + // given a flag, enforce a default type. + function guessType(key) { + let type = DefaultValuesForTypeKey.BOOLEAN; + if (checkAllAliases(key, flags.strings)) + type = DefaultValuesForTypeKey.STRING; + else if (checkAllAliases(key, flags.numbers)) + type = DefaultValuesForTypeKey.NUMBER; + else if (checkAllAliases(key, flags.bools)) + type = DefaultValuesForTypeKey.BOOLEAN; + else if (checkAllAliases(key, flags.arrays)) + type = DefaultValuesForTypeKey.ARRAY; + return type; + } + function isUndefined(num) { + return num === undefined; + } + // check user configuration settings for inconsistencies + function checkConfiguration() { + // count keys should not be set as array/narg + Object.keys(flags.counts).find(key => { + if (checkAllAliases(key, flags.arrays)) { + error = Error(__('Invalid configuration: %s, opts.count excludes opts.array.', key)); + return true; + } + else if (checkAllAliases(key, flags.nargs)) { + error = Error(__('Invalid configuration: %s, opts.count excludes opts.narg.', key)); + return true; + } + return false; + }); + } + return { + aliases: Object.assign({}, flags.aliases), + argv: Object.assign(argvReturn, argv), + configuration: configuration, + defaulted: Object.assign({}, defaulted), + error: error, + newAliases: Object.assign({}, newAliases) + }; + } +} +// if any aliases reference each other, we should +// merge them together. +function combineAliases(aliases) { + const aliasArrays = []; + const combined = Object.create(null); + let change = true; + // turn alias lookup hash {key: ['alias1', 'alias2']} into + // a simple array ['key', 'alias1', 'alias2'] + Object.keys(aliases).forEach(function (key) { + aliasArrays.push([].concat(aliases[key], key)); + }); + // combine arrays until zero changes are + // made in an iteration. + while (change) { + change = false; + for (let i = 0; i < aliasArrays.length; i++) { + for (let ii = i + 1; ii < aliasArrays.length; ii++) { + const intersect = aliasArrays[i].filter(function (v) { + return aliasArrays[ii].indexOf(v) !== -1; + }); + if (intersect.length) { + aliasArrays[i] = aliasArrays[i].concat(aliasArrays[ii]); + aliasArrays.splice(ii, 1); + change = true; + break; + } + } + } + } + // map arrays back to the hash-lookup (de-dupe while + // we're at it). + aliasArrays.forEach(function (aliasArray) { + aliasArray = aliasArray.filter(function (v, i, self) { + return self.indexOf(v) === i; + }); + const lastAlias = aliasArray.pop(); + if (lastAlias !== undefined && typeof lastAlias === 'string') { + combined[lastAlias] = aliasArray; + } + }); + return combined; +} +// this function should only be called when a count is given as an arg +// it is NOT called to set a default value +// thus we can start the count at 1 instead of 0 +function increment(orig) { + return orig !== undefined ? orig + 1 : 1; +} +// TODO(bcoe): in the next major version of yargs, switch to +// Object.create(null) for dot notation: +function sanitizeKey(key) { + if (key === '__proto__') + return '___proto___'; + return key; +} +function stripQuotes(val) { + return (typeof val === 'string' && + (val[0] === "'" || val[0] === '"') && + val[val.length - 1] === val[0]) + ? val.substring(1, val.length - 1) + : val; +} + +/** + * @fileoverview Main entrypoint for libraries using yargs-parser in Node.js + * CJS and ESM environments. + * + * @license + * Copyright (c) 2016, Contributors + * SPDX-License-Identifier: ISC + */ +var _a, _b, _c; +// See https://github.com/yargs/yargs-parser#supported-nodejs-versions for our +// version support policy. The YARGS_MIN_NODE_VERSION is used for testing only. +const minNodeVersion = (process && process.env && process.env.YARGS_MIN_NODE_VERSION) + ? Number(process.env.YARGS_MIN_NODE_VERSION) + : 12; +const nodeVersion = (_b = (_a = process === null || process === void 0 ? void 0 : process.versions) === null || _a === void 0 ? void 0 : _a.node) !== null && _b !== void 0 ? _b : (_c = process === null || process === void 0 ? void 0 : process.version) === null || _c === void 0 ? void 0 : _c.slice(1); +if (nodeVersion) { + const major = Number(nodeVersion.match(/^([^.]+)/)[1]); + if (major < minNodeVersion) { + throw Error(`yargs parser supports a minimum Node.js version of ${minNodeVersion}. Read our version support policy: https://github.com/yargs/yargs-parser#supported-nodejs-versions`); + } +} +// Creates a yargs-parser instance using Node.js standard libraries: +const env = process ? process.env : {}; +const parser = new YargsParser({ + cwd: process.cwd, + env: () => { + return env; + }, + format: require$$2.format, + normalize: require$$0.normalize, + resolve: require$$0.resolve, + // TODO: figure out a way to combine ESM and CJS coverage, such that + // we can exercise all the lines below: + require: (path) => { + if (typeof require !== 'undefined') { + return require(path); + } + else if (path.match(/\.json$/)) { + // Addresses: https://github.com/yargs/yargs/issues/2040 + return JSON.parse(require$$0$1.readFileSync(path, 'utf8')); + } + else { + throw Error('only .json config files are supported in ESM'); + } + } +}); +const yargsParser = function Parser(args, opts) { + const result = parser.parse(args.slice(), opts); + return result.argv; +}; +yargsParser.detailed = function (args, opts) { + return parser.parse(args.slice(), opts); +}; +yargsParser.camelCase = camelCase; +yargsParser.decamelize = decamelize; +yargsParser.looksLikeNumber = looksLikeNumber; + +/****************************************************************************** +Copyright (c) Microsoft Corporation. + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. +***************************************************************************** */ +/* global Reflect, Promise, SuppressedError, Symbol, Iterator */ + + +function __addDisposableResource(env, value, async) { + if (value !== null && value !== void 0) { + if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected."); + var dispose, inner; + if (async) { + if (!Symbol.asyncDispose) throw new TypeError("Symbol.asyncDispose is not defined."); + dispose = value[Symbol.asyncDispose]; + } + if (dispose === void 0) { + if (!Symbol.dispose) throw new TypeError("Symbol.dispose is not defined."); + dispose = value[Symbol.dispose]; + if (async) inner = dispose; + } + if (typeof dispose !== "function") throw new TypeError("Object not disposable."); + if (inner) dispose = function() { try { inner.call(this); } catch (e) { return Promise.reject(e); } }; + env.stack.push({ value: value, dispose: dispose, async: async }); + } + else if (async) { + env.stack.push({ async: true }); + } + return value; + +} + +var _SuppressedError = typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) { + var e = new Error(message); + return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e; +}; + +function __disposeResources(env) { + function fail(e) { + env.error = env.hasError ? new _SuppressedError(e, env.error, "An error was suppressed during disposal.") : e; + env.hasError = true; + } + var r, s = 0; + function next() { + while (r = env.stack.pop()) { + try { + if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next); + if (r.dispose) { + var result = r.dispose.call(r.value); + if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) { fail(e); return next(); }); + } + else s |= 1; + } + catch (e) { + fail(e); + } + } + if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve(); + if (env.hasError) throw env.error; + } + return next(); +} + +const toZeroIfInfinity = value => Number.isFinite(value) ? value : 0; + +function parseNumber(milliseconds) { + return { + days: Math.trunc(milliseconds / 86_400_000), + hours: Math.trunc(milliseconds / 3_600_000 % 24), + minutes: Math.trunc(milliseconds / 60_000 % 60), + seconds: Math.trunc(milliseconds / 1000 % 60), + milliseconds: Math.trunc(milliseconds % 1000), + microseconds: Math.trunc(toZeroIfInfinity(milliseconds * 1000) % 1000), + nanoseconds: Math.trunc(toZeroIfInfinity(milliseconds * 1e6) % 1000), + }; +} + +function parseBigint(milliseconds) { + return { + days: milliseconds / 86_400_000n, + hours: milliseconds / 3_600_000n % 24n, + minutes: milliseconds / 60_000n % 60n, + seconds: milliseconds / 1000n % 60n, + milliseconds: milliseconds % 1000n, + microseconds: 0n, + nanoseconds: 0n, + }; +} + +function parseMilliseconds(milliseconds) { + switch (typeof milliseconds) { + case 'number': { + if (Number.isFinite(milliseconds)) { + return parseNumber(milliseconds); + } + + break; + } + + case 'bigint': { + return parseBigint(milliseconds); + } + + // No default + } + + throw new TypeError('Expected a finite number or bigint'); +} + +const isZero = value => value === 0 || value === 0n; +const pluralize = (word, count) => (count === 1 || count === 1n) ? word : `${word}s`; + +const SECOND_ROUNDING_EPSILON = 0.000_000_1; +const ONE_DAY_IN_MILLISECONDS = 24n * 60n * 60n * 1000n; + +function prettyMilliseconds(milliseconds, options) { + const isBigInt = typeof milliseconds === 'bigint'; + if (!isBigInt && !Number.isFinite(milliseconds)) { + throw new TypeError('Expected a finite number or bigint'); + } + + options = {...options}; + + const sign = milliseconds < 0 ? '-' : ''; + milliseconds = milliseconds < 0 ? -milliseconds : milliseconds; // Cannot use `Math.abs()` because of BigInt support. + + if (options.colonNotation) { + options.compact = false; + options.formatSubMilliseconds = false; + options.separateMilliseconds = false; + options.verbose = false; + } + + if (options.compact) { + options.unitCount = 1; + options.secondsDecimalDigits = 0; + options.millisecondsDecimalDigits = 0; + } + + let result = []; + + const floorDecimals = (value, decimalDigits) => { + const flooredInterimValue = Math.floor((value * (10 ** decimalDigits)) + SECOND_ROUNDING_EPSILON); + const flooredValue = Math.round(flooredInterimValue) / (10 ** decimalDigits); + return flooredValue.toFixed(decimalDigits); + }; + + const add = (value, long, short, valueString) => { + if ( + (result.length === 0 || !options.colonNotation) + && isZero(value) + && !(options.colonNotation && short === 'm')) { + return; + } + + valueString ??= String(value); + if (options.colonNotation) { + const wholeDigits = valueString.includes('.') ? valueString.split('.')[0].length : valueString.length; + const minLength = result.length > 0 ? 2 : 1; + valueString = '0'.repeat(Math.max(0, minLength - wholeDigits)) + valueString; + } else { + valueString += options.verbose ? ' ' + pluralize(long, value) : short; + } + + result.push(valueString); + }; + + const parsed = parseMilliseconds(milliseconds); + const days = BigInt(parsed.days); + + if (options.hideYearAndDays) { + add((BigInt(days) * 24n) + BigInt(parsed.hours), 'hour', 'h'); + } else { + if (options.hideYear) { + add(days, 'day', 'd'); + } else { + add(days / 365n, 'year', 'y'); + add(days % 365n, 'day', 'd'); + } + + add(Number(parsed.hours), 'hour', 'h'); + } + + add(Number(parsed.minutes), 'minute', 'm'); + + if (!options.hideSeconds) { + if ( + options.separateMilliseconds + || options.formatSubMilliseconds + || (!options.colonNotation && milliseconds < 1000 && !options.subSecondsAsDecimals) + ) { + const seconds = Number(parsed.seconds); + const milliseconds = Number(parsed.milliseconds); + const microseconds = Number(parsed.microseconds); + const nanoseconds = Number(parsed.nanoseconds); + + add(seconds, 'second', 's'); + + if (options.formatSubMilliseconds) { + add(milliseconds, 'millisecond', 'ms'); + add(microseconds, 'microsecond', 'µs'); + add(nanoseconds, 'nanosecond', 'ns'); + } else { + const millisecondsAndBelow + = milliseconds + + (microseconds / 1000) + + (nanoseconds / 1e6); + + const millisecondsDecimalDigits + = typeof options.millisecondsDecimalDigits === 'number' + ? options.millisecondsDecimalDigits + : 0; + + const roundedMilliseconds = millisecondsAndBelow >= 1 + ? Math.round(millisecondsAndBelow) + : Math.ceil(millisecondsAndBelow); + + const millisecondsString = millisecondsDecimalDigits + ? millisecondsAndBelow.toFixed(millisecondsDecimalDigits) + : roundedMilliseconds; + + add( + Number.parseFloat(millisecondsString), + 'millisecond', + 'ms', + millisecondsString, + ); + } + } else { + const seconds = ( + (isBigInt ? Number(milliseconds % ONE_DAY_IN_MILLISECONDS) : milliseconds) + / 1000 + ) % 60; + const secondsDecimalDigits + = typeof options.secondsDecimalDigits === 'number' + ? options.secondsDecimalDigits + : 1; + const secondsFixed = floorDecimals(seconds, secondsDecimalDigits); + const secondsString = options.keepDecimalsOnWholeSeconds + ? secondsFixed + : secondsFixed.replace(/\.0+$/, ''); + add(Number.parseFloat(secondsString), 'second', 's', secondsString); + } + } + + if (result.length === 0) { + return sign + '0' + (options.verbose ? ' milliseconds' : 'ms'); + } + + const separator = options.colonNotation ? ':' : ' '; + if (typeof options.unitCount === 'number') { + result = result.slice(0, Math.max(options.unitCount, 1)); + } + + return sign + result.join(separator); +} + +const BYTE_UNITS = [ + 'B', + 'kB', + 'MB', + 'GB', + 'TB', + 'PB', + 'EB', + 'ZB', + 'YB', +]; + +const BIBYTE_UNITS = [ + 'B', + 'KiB', + 'MiB', + 'GiB', + 'TiB', + 'PiB', + 'EiB', + 'ZiB', + 'YiB', +]; + +const BIT_UNITS = [ + 'b', + 'kbit', + 'Mbit', + 'Gbit', + 'Tbit', + 'Pbit', + 'Ebit', + 'Zbit', + 'Ybit', +]; + +const BIBIT_UNITS = [ + 'b', + 'kibit', + 'Mibit', + 'Gibit', + 'Tibit', + 'Pibit', + 'Eibit', + 'Zibit', + 'Yibit', +]; + +/* +Formats the given number using `Number#toLocaleString`. +- If locale is a string, the value is expected to be a locale-key (for example: `de`). +- If locale is true, the system default locale is used for translation. +- If no value for locale is specified, the number is returned unmodified. +*/ +const toLocaleString = (number, locale, options) => { + let result = number; + if (typeof locale === 'string' || Array.isArray(locale)) { + result = number.toLocaleString(locale, options); + } else if (locale === true || options !== undefined) { + result = number.toLocaleString(undefined, options); + } + + return result; +}; + +const log10 = numberOrBigInt => { + if (typeof numberOrBigInt === 'number') { + return Math.log10(numberOrBigInt); + } + + const string = numberOrBigInt.toString(10); + + return string.length + Math.log10(`0.${string.slice(0, 15)}`); +}; + +const log = numberOrBigInt => { + if (typeof numberOrBigInt === 'number') { + return Math.log(numberOrBigInt); + } + + return log10(numberOrBigInt) * Math.log(10); +}; + +const divide = (numberOrBigInt, divisor) => { + if (typeof numberOrBigInt === 'number') { + return numberOrBigInt / divisor; + } + + const integerPart = numberOrBigInt / BigInt(divisor); + const remainder = numberOrBigInt % BigInt(divisor); + return Number(integerPart) + (Number(remainder) / divisor); +}; + +const applyFixedWidth = (result, fixedWidth) => { + if (fixedWidth === undefined) { + return result; + } + + if (typeof fixedWidth !== 'number' || !Number.isSafeInteger(fixedWidth) || fixedWidth < 0) { + throw new TypeError(`Expected fixedWidth to be a non-negative integer, got ${typeof fixedWidth}: ${fixedWidth}`); + } + + if (fixedWidth === 0) { + return result; + } + + return result.length < fixedWidth ? result.padStart(fixedWidth, ' ') : result; +}; + +const buildLocaleOptions = options => { + const {minimumFractionDigits, maximumFractionDigits} = options; + + if (minimumFractionDigits === undefined && maximumFractionDigits === undefined) { + return undefined; + } + + return { + ...(minimumFractionDigits !== undefined && {minimumFractionDigits}), + ...(maximumFractionDigits !== undefined && {maximumFractionDigits}), + roundingMode: 'trunc', + }; +}; + +function prettyBytes(number, options) { + if (typeof number !== 'bigint' && !Number.isFinite(number)) { + throw new TypeError(`Expected a finite number, got ${typeof number}: ${number}`); + } + + options = { + bits: false, + binary: false, + space: true, + nonBreakingSpace: false, + ...options, + }; + + const UNITS = options.bits + ? (options.binary ? BIBIT_UNITS : BIT_UNITS) + : (options.binary ? BIBYTE_UNITS : BYTE_UNITS); + + const separator = options.space ? (options.nonBreakingSpace ? '\u00A0' : ' ') : ''; + + // Handle signed zero case + const isZero = typeof number === 'number' ? number === 0 : number === 0n; + if (options.signed && isZero) { + const result = ` 0${separator}${UNITS[0]}`; + return applyFixedWidth(result, options.fixedWidth); + } + + const isNegative = number < 0; + const prefix = isNegative ? '-' : (options.signed ? '+' : ''); + + if (isNegative) { + number = -number; + } + + const localeOptions = buildLocaleOptions(options); + let result; + + if (number < 1) { + const numberString = toLocaleString(number, options.locale, localeOptions); + result = prefix + numberString + separator + UNITS[0]; + } else { + const exponent = Math.min(Math.floor(options.binary ? log(number) / Math.log(1024) : log10(number) / 3), UNITS.length - 1); + number = divide(number, (options.binary ? 1024 : 1000) ** exponent); + + if (!localeOptions) { + const minPrecision = Math.max(3, Math.floor(number).toString().length); + number = number.toPrecision(minPrecision); + } + + const numberString = toLocaleString(Number(number), options.locale, localeOptions); + const unit = UNITS[exponent]; + result = prefix + numberString + separator + unit; + } + + return applyFixedWidth(result, options.fixedWidth); +} + +function printTimings(timings) { + for (const [label, [time, memory, total]] of Object.entries(timings)) { + const appliedColor = label[0] === '#' ? (label[1] === '#' ? rollup.bold : rollup.underline) : (text) => text; + const row = `${label}: ${time.toFixed(0)}ms, ${prettyBytes(memory)} / ${prettyBytes(total)}`; + console.info(appliedColor(row)); + } +} + +async function build(inputOptions, warnings, silent = false) { + const env_1 = { stack: [], error: void 0, hasError: false }; + try { + const outputOptions = inputOptions.output; + const useStdout = !outputOptions[0].file && !outputOptions[0].dir; + const start = Date.now(); + const files = useStdout ? ['stdout'] : outputOptions.map(t => parseAst_js.relativeId(t.file || t.dir)); + if (!silent) { + let inputFiles; + if (typeof inputOptions.input === 'string') { + inputFiles = inputOptions.input; + } + else if (Array.isArray(inputOptions.input)) { + inputFiles = inputOptions.input.join(', '); + } + else if (typeof inputOptions.input === 'object' && inputOptions.input !== null) { + inputFiles = Object.values(inputOptions.input).join(', '); + } + rollup.stderr(rollup.cyan(`\n${rollup.bold(inputFiles)} → ${rollup.bold(files.join(', '))}...`)); + } + const bundle = __addDisposableResource(env_1, await rollup.rollup(inputOptions), true); + if (useStdout) { + const output = outputOptions[0]; + if (output.sourcemap && output.sourcemap !== 'inline') { + rollup.handleError(parseAst_js.logOnlyInlineSourcemapsForStdout()); + } + const { output: outputs } = await bundle.generate(output); + for (const file of outputs) { + if (outputs.length > 1) + process$1.stdout.write(`\n${rollup.cyan(rollup.bold(`//→ ${file.fileName}:`))}\n`); + process$1.stdout.write(file.type === 'asset' ? file.source : file.code); + } + if (!silent) { + warnings.flush(); + } + return; + } + await Promise.all(outputOptions.map(bundle.write)); + if (!silent) { + warnings.flush(); + rollup.stderr(rollup.green(`created ${rollup.bold(files.join(', '))} in ${rollup.bold(prettyMilliseconds(Date.now() - start))}`)); + if (bundle && bundle.getTimings) { + printTimings(bundle.getTimings()); + } + } + } + catch (e_1) { + env_1.error = e_1; + env_1.hasError = true; + } + finally { + const result_1 = __disposeResources(env_1); + if (result_1) + await result_1; + } +} + +const DEFAULT_CONFIG_BASE = 'rollup.config'; +async function getConfigPath(commandConfig) { + if (commandConfig === true) { + return path.resolve(await findConfigFileNameInCwd()); + } + if (commandConfig.slice(0, 5) === 'node:') { + const packageName = commandConfig.slice(5); + try { + return require.resolve(`rollup-config-${packageName}`, { paths: [process$1.cwd()] }); + } + catch { + try { + return require.resolve(packageName, { paths: [process$1.cwd()] }); + } + catch (error) { + if (error.code === 'MODULE_NOT_FOUND') { + rollup.handleError(parseAst_js.logMissingExternalConfig(commandConfig)); + } + throw error; + } + } + } + return path.resolve(commandConfig); +} +async function findConfigFileNameInCwd() { + const filesInWorkingDirectory = new Set(await promises.readdir(process$1.cwd())); + for (const extension of ['mjs', 'cjs', 'ts']) { + const fileName = `${DEFAULT_CONFIG_BASE}.${extension}`; + if (filesInWorkingDirectory.has(fileName)) + return fileName; + } + return `${DEFAULT_CONFIG_BASE}.js`; +} + +async function loadConfigFromCommand(commandOptions, watchMode) { + const warnings = loadConfigFile_js.batchWarnings(commandOptions); + if (!commandOptions.input && (commandOptions.stdin || !process$1.stdin.isTTY)) { + commandOptions.input = loadConfigFile_js.stdinName; + } + const options = await rollup.mergeOptions({ input: [] }, watchMode, commandOptions, warnings.log); + await loadConfigFile_js.addCommandPluginsToInputOptions(options, commandOptions); + return { options: [options], warnings }; +} + +async function runRollup(command) { + let inputSource; + if (command._.length > 0) { + if (command.input) { + rollup.handleError(parseAst_js.logDuplicateImportOptions()); + } + inputSource = command._; + } + else if (typeof command.input === 'string') { + inputSource = [command.input]; + } + else { + inputSource = command.input; + } + if (inputSource && inputSource.length > 0) { + if (inputSource.some((input) => input.includes('='))) { + command.input = {}; + for (const input of inputSource) { + const equalsIndex = input.indexOf('='); + const value = input.slice(Math.max(0, equalsIndex + 1)); + const key = input.slice(0, Math.max(0, equalsIndex)) || parseAst_js.getAliasName(input); + command.input[key] = value; + } + } + else { + command.input = inputSource; + } + } + if (command.environment) { + const environment = Array.isArray(command.environment) + ? command.environment + : [command.environment]; + for (const argument of environment) { + for (const pair of argument.split(',')) { + const [key, ...value] = pair.split(':'); + process$1.env[key] = value.length === 0 ? String(true) : value.join(':'); + } + } + } + if (rollup.isWatchEnabled(command.watch)) { + await fseventsImporter.loadFsEvents(); + const { watch } = await Promise.resolve().then(() => require('../shared/watch-cli.js')); + await watch(command); + } + else { + try { + const { options, warnings } = await getConfigs(command); + try { + for (const inputOptions of options) { + if (!inputOptions.cache) { + // We explicitly disable the cache when unused as the CLI will not + // use the cache object on the bundle when not in watch mode. This + // improves performance as the cache is not generated. + inputOptions.cache = false; + } + await build(inputOptions, warnings, command.silent); + } + if (command.failAfterWarnings && warnings.warningOccurred) { + warnings.flush(); + rollup.handleError(parseAst_js.logFailAfterWarnings()); + } + } + catch (error) { + warnings.flush(); + rollup.handleError(error); + } + } + catch (error) { + rollup.handleError(error); + } + } +} +async function getConfigs(command) { + if (command.config) { + const configFile = await getConfigPath(command.config); + const { options, warnings } = await loadConfigFile_js.loadConfigFile(configFile, command, false); + return { options, warnings }; + } + return await loadConfigFromCommand(command, false); +} + +const command = yargsParser(process$1.argv.slice(2), { + alias: rollup.commandAliases, + configuration: { 'camel-case-expansion': false } +}); +if (command.help || (process$1.argv.length <= 2 && process$1.stdin.isTTY)) { + console.log(`\n${help}\n`); +} +else if (command.version) { + console.log(`rollup v${rollup.version}`); +} +else { + try { + // eslint-disable-next-line @typescript-eslint/no-require-imports + require('source-map-support').install(); + } + catch { + // do nothing + } + const promise = runRollup(command); + if (command.forceExit) { + promise.then(() => process$1.exit()); + } +} + +exports.getConfigPath = getConfigPath; +exports.loadConfigFromCommand = loadConfigFromCommand; +exports.prettyMilliseconds = prettyMilliseconds; +exports.printTimings = printTimings; +//# sourceMappingURL=rollup.map diff --git a/node_modules/.bin/semver b/node_modules/.bin/semver new file mode 100644 index 0000000000000000000000000000000000000000..9ae8aadb95fbd9a67a0e8da3a1c8eacc86e86201 --- /dev/null +++ b/node_modules/.bin/semver @@ -0,0 +1,195 @@ +#!/usr/bin/env node +// Standalone semver comparison program. +// Exits successfully and prints matching version(s) if +// any supplied version is valid and passes all tests. + +'use strict' + +const argv = process.argv.slice(2) + +let versions = [] + +const range = [] + +let inc = null + +const version = require('../package.json').version + +let loose = false + +let includePrerelease = false + +let coerce = false + +let rtl = false + +let identifier + +let identifierBase + +const semver = require('../') +const parseOptions = require('../internal/parse-options') + +let reverse = false + +let options = {} + +const main = () => { + if (!argv.length) { + return help() + } + while (argv.length) { + let a = argv.shift() + const indexOfEqualSign = a.indexOf('=') + if (indexOfEqualSign !== -1) { + const value = a.slice(indexOfEqualSign + 1) + a = a.slice(0, indexOfEqualSign) + argv.unshift(value) + } + + switch (a) { + case '-rv': case '-rev': case '--rev': case '--reverse': + reverse = true + break + case '-l': case '--loose': + loose = true + break + case '-p': case '--include-prerelease': + includePrerelease = true + break + case '-v': case '--version': + versions.push(argv.shift()) + break + case '-i': case '--inc': case '--increment': + if (semver.RELEASE_TYPES.includes(argv[0]) || (argv[0] === 'release')) { + inc = { value: argv.shift(), maybeErrantValue: null, option: a } + } else { + inc = { value: 'patch', maybeErrantValue: argv[0], option: a } + } + break + case '--preid': + identifier = argv.shift() + break + case '-r': case '--range': + range.push(argv.shift()) + break + case '-n': + identifierBase = argv.shift() + if (identifierBase === 'false') { + identifierBase = false + } + break + case '-c': case '--coerce': + coerce = true + break + case '--rtl': + rtl = true + break + case '--ltr': + rtl = false + break + case '-h': case '--help': case '-?': + return help() + default: + versions.push(a) + break + } + } + + options = parseOptions({ loose, includePrerelease, rtl }) + + if ( + inc && + versions.includes(inc.maybeErrantValue) && + !semver.valid(inc.maybeErrantValue, options) + ) { + console.warn(`Invalid value for ${inc.option}; defaulting to 'patch'. This may become a failure in future major versions.`) + } + + versions = versions.map((v) => { + return coerce ? (semver.coerce(v, options) || { version: v }).version : v + }).filter((v) => { + return semver.valid(v, options) + }) + if (!versions.length) { + return fail() + } + if (inc && (versions.length !== 1 || range.length)) { + return failInc() + } + + for (let i = 0, l = range.length; i < l; i++) { + versions = versions.filter((v) => { + return semver.satisfies(v, range[i], options) + }) + if (!versions.length) { + return fail() + } + } + versions + .sort((a, b) => semver[reverse ? 'rcompare' : 'compare'](a, b, options)) + .map(v => semver.clean(v, options)) + .map(v => inc ? semver.inc(v, inc.value, options, identifier, identifierBase) : v) + .forEach(v => console.log(v)) +} + +const failInc = () => { + console.error('--inc can only be used on a single version with no range') + fail() +} + +const fail = () => process.exit(1) + +const help = () => console.log( +`SemVer ${version} + +A JavaScript implementation of the https://semver.org/ specification +Copyright Isaac Z. Schlueter + +Usage: semver [options] [ [...]] +Prints valid versions sorted by SemVer precedence + +Options: +-r --range + Print versions that match the specified range. + +-i --increment [] + Increment a version by the specified level. Level can + be one of: major, minor, patch, premajor, preminor, + prepatch, prerelease, or release. Default level is 'patch'. + Only one version may be specified. + +--preid + Identifier to be used to prefix premajor, preminor, + prepatch or prerelease version increments. + +-l --loose + Interpret versions and ranges loosely + +-p --include-prerelease + Always include prerelease versions in range matching + +-c --coerce + Coerce a string into SemVer if possible + (does not imply --loose) + +--rtl + Coerce version strings right to left + +--ltr + Coerce version strings left to right (default) + +-n + Base number to be used for the prerelease identifier. + Can be either 0 or 1, or false to omit the number altogether. + Defaults to 0. + +Program exits successfully if any valid version satisfies +all supplied ranges, and prints all satisfying versions. + +If no satisfying versions are found, then exits failure. + +Versions are printed in ascending order, so supplying +multiple versions to the utility will just sort them.`) + +main() diff --git a/node_modules/.bin/specificity b/node_modules/.bin/specificity new file mode 100644 index 0000000000000000000000000000000000000000..9d26b686fb92b85c03cb0cfcc3eeec0c09e9015b --- /dev/null +++ b/node_modules/.bin/specificity @@ -0,0 +1,14 @@ +#!/usr/bin/env node +import Specificity from '../dist/index.js'; + +if (!process.argv[2]) { + console.error('❌ Missing selector argument'); + process.exit(1); +} + +try { + const specificities = Specificity.calculate(process.argv[2]); + console.log(specificities.map((specificity) => `${specificity}`).join('\n')); +} catch (e) { + console.error(`❌ ${e.message}`); +} diff --git a/node_modules/.bin/terser b/node_modules/.bin/terser new file mode 100644 index 0000000000000000000000000000000000000000..b0cdc7c570e3149b05660695f17f7427863dde96 --- /dev/null +++ b/node_modules/.bin/terser @@ -0,0 +1,21 @@ +#!/usr/bin/env node + +"use strict"; + +require("../tools/exit.cjs"); + +try { + require("source-map-support").install(); +} catch (err) {} + +const fs = require("fs"); +const path = require("path"); +const program = require("commander"); + +const packageJson = require("../package.json"); +const { _run_cli: run_cli } = require(".."); + +run_cli({ program, packageJson, fs, path }).catch((error) => { + console.error(error); + process.exitCode = 1; +}); diff --git a/node_modules/.bin/tldts b/node_modules/.bin/tldts new file mode 100644 index 0000000000000000000000000000000000000000..9d7e9d4cd901a523812c7cd39fedb035ca1e4415 --- /dev/null +++ b/node_modules/.bin/tldts @@ -0,0 +1,21 @@ +#!/usr/bin/env node + +'use strict'; + +const { parse } = require('..'); +const readline = require('readline'); + +if (process.argv.length > 2) { + // URL(s) was specified in the command arguments + console.log( + JSON.stringify(parse(process.argv[process.argv.length - 1]), null, 2), + ); +} else { + // No arguments were specified, read URLs from each line of STDIN + const rlInterface = readline.createInterface({ + input: process.stdin, + }); + rlInterface.on('line', function (line) { + console.log(JSON.stringify(parse(line), null, 2)); + }); +} diff --git a/node_modules/.bin/tree-kill b/node_modules/.bin/tree-kill new file mode 100644 index 0000000000000000000000000000000000000000..1acb8158959fb9634e6ac5c965370a101f337bda --- /dev/null +++ b/node_modules/.bin/tree-kill @@ -0,0 +1,14 @@ +#!/usr/bin/env node +kill = require('.') +try { + kill(process.argv[2], process.argv[3], function(err){ + if (err) { + console.log(err.message) + process.exit(1) + } + }) +} +catch (err) { + console.log(err.message) + process.exit(1) +} diff --git a/node_modules/.bin/tsc b/node_modules/.bin/tsc new file mode 100644 index 0000000000000000000000000000000000000000..19c62bf7a0004aab7bd188aae51ff2564fdfc18d --- /dev/null +++ b/node_modules/.bin/tsc @@ -0,0 +1,2 @@ +#!/usr/bin/env node +require('../lib/tsc.js') diff --git a/node_modules/.bin/tsserver b/node_modules/.bin/tsserver new file mode 100644 index 0000000000000000000000000000000000000000..7143b6a73ab8a901ccf93752cc36f8e9f8191d93 --- /dev/null +++ b/node_modules/.bin/tsserver @@ -0,0 +1,2 @@ +#!/usr/bin/env node +require('../lib/tsserver.js') diff --git a/node_modules/.bin/tsx b/node_modules/.bin/tsx new file mode 100644 index 0000000000000000000000000000000000000000..c3e3c1931d79cb2268ae9b8f4509fcf18110aea5 --- /dev/null +++ b/node_modules/.bin/tsx @@ -0,0 +1,55 @@ +#!/usr/bin/env node +var Rn=Object.defineProperty;var a=(t,e)=>Rn(t,"name",{value:e,configurable:!0});import{constants as lt}from"node:os";import bn from"tty";import{transformSync as vn}from"esbuild";import{v as Sn}from"./package-Bj47PlGH.mjs";import{r as Ie,g as Bn,i as $n}from"./get-pipe-path-_tAJyU_v.mjs";import{pathToFileURL as Tn,fileURLToPath as xn}from"node:url";import On from"child_process";import z from"path";import De from"fs";import{i as mu,a as Nn,t as Hn}from"./node-features-B9BBLzwu.mjs";import Pn from"node:path";import Ln from"events";import ge from"util";import In from"stream";import _u from"os";import{g as kn,l as Mn,e as Gn,f as Wn,y as me}from"./index-gbaejti9.mjs";import jn from"node:net";import ct from"node:fs";import{t as Un}from"./temporary-directory-BDDVQOvU.mjs";import"module";const Kn="known-flag",Vn="unknown-flag",zn="argument",{stringify:_e}=JSON,Yn=/\B([A-Z])/g,qn=a(t=>t.replace(Yn,"-$1").toLowerCase(),"v$1"),{hasOwnProperty:Xn}=Object.prototype,Ae=a((t,e)=>Xn.call(t,e),"w$2"),Qn=a(t=>Array.isArray(t),"L$2"),Au=a(t=>typeof t=="function"?[t,!1]:Qn(t)?[t[0],!0]:Au(t.type),"b$2"),Zn=a((t,e)=>t===Boolean?e!=="false":e,"d$2"),Jn=a((t,e)=>typeof e=="boolean"?e:t===Number&&e===""?Number.NaN:t(e),"m$1"),er=/[\s.:=]/,tr=a(t=>{const e=`Flag name ${_e(t)}`;if(t.length===0)throw new Error(`${e} cannot be empty`);if(t.length===1)throw new Error(`${e} must be longer than a character`);const u=t.match(er);if(u)throw new Error(`${e} cannot contain ${_e(u?.[0])}`)},"B"),ur=a(t=>{const e={},u=a((s,n)=>{if(Ae(e,s))throw new Error(`Duplicate flags named ${_e(s)}`);e[s]=n},"r");for(const s in t){if(!Ae(t,s))continue;tr(s);const n=t[s],r=[[],...Au(n),n];u(s,r);const i=qn(s);if(s!==i&&u(i,r),"alias"in n&&typeof n.alias=="string"){const{alias:D}=n,o=`Flag alias ${_e(D)} for flag ${_e(s)}`;if(D.length===0)throw new Error(`${o} cannot be empty`);if(D.length>1)throw new Error(`${o} must be a single character`);u(D,r)}}return e},"K$1"),sr=a((t,e)=>{const u={};for(const s in t){if(!Ae(t,s))continue;const[n,,r,i]=e[s];if(n.length===0&&"default"in i){let{default:D}=i;typeof D=="function"&&(D=D()),u[s]=D}else u[s]=r?n:n.pop()}return u},"_$2"),ke="--",nr=/[.:=]/,rr=/^-{1,2}\w/,ir=a(t=>{if(!rr.test(t))return;const e=!t.startsWith(ke);let u=t.slice(e?1:2),s;const n=u.match(nr);if(n){const{index:r}=n;s=u.slice(r+1),u=u.slice(0,r)}return[u,s,e]},"N"),Dr=a((t,{onFlag:e,onArgument:u})=>{let s;const n=a((r,i)=>{if(typeof s!="function")return!0;s(r,i),s=void 0},"o");for(let r=0;r{for(const[u,s,n]of e.reverse()){if(s){const r=t[u];let i=r.slice(0,s);if(n||(i+=r.slice(s+1)),i!=="-"){t[u]=i;continue}}t.splice(u,1)}},"E"),yu=a((t,e=process.argv.slice(2),{ignore:u}={})=>{const s=[],n=ur(t),r={},i=[];return i[ke]=[],Dr(e,{onFlag(D,o,c){const f=Ae(n,D);if(!u?.(f?Kn:Vn,D,o)){if(f){const[h,l]=n[D],p=Zn(l,o),C=a((g,y)=>{s.push(c),y&&s.push(y),h.push(Jn(l,g||""))},"p");return p===void 0?C:C(p)}Ae(r,D)||(r[D]=[]),r[D].push(o===void 0?!0:o),s.push(c)}},onArgument(D,o,c){u?.(zn,e[o[0]])||(i.push(...D),c?(i[ke]=D,e.splice(o[0])):s.push(o))}}),or(e,s),{flags:sr(t,n),unknownFlags:r,_:i}},"U$2");var ar=Object.create,Me=Object.defineProperty,lr=Object.defineProperties,cr=Object.getOwnPropertyDescriptor,fr=Object.getOwnPropertyDescriptors,hr=Object.getOwnPropertyNames,wu=Object.getOwnPropertySymbols,dr=Object.getPrototypeOf,Ru=Object.prototype.hasOwnProperty,Er=Object.prototype.propertyIsEnumerable,bu=a((t,e,u)=>e in t?Me(t,e,{enumerable:!0,configurable:!0,writable:!0,value:u}):t[e]=u,"W$1"),Ge=a((t,e)=>{for(var u in e||(e={}))Ru.call(e,u)&&bu(t,u,e[u]);if(wu)for(var u of wu(e))Er.call(e,u)&&bu(t,u,e[u]);return t},"p"),ft=a((t,e)=>lr(t,fr(e)),"c"),pr=a(t=>Me(t,"__esModule",{value:!0}),"nD"),Cr=a((t,e)=>()=>(t&&(e=t(t=0)),e),"rD"),Fr=a((t,e)=>()=>(e||t((e={exports:{}}).exports,e),e.exports),"iD"),gr=a((t,e,u,s)=>{if(e&&typeof e=="object"||typeof e=="function")for(let n of hr(e))!Ru.call(t,n)&&n!=="default"&&Me(t,n,{get:a(()=>e[n],"get"),enumerable:!(s=cr(e,n))||s.enumerable});return t},"oD"),mr=a((t,e)=>gr(pr(Me(t!=null?ar(dr(t)):{},"default",{value:t,enumerable:!0})),t),"BD"),K=Cr(()=>{}),_r=Fr((t,e)=>{K(),e.exports=function(){return/\uD83C\uDFF4\uDB40\uDC67\uDB40\uDC62(?:\uDB40\uDC77\uDB40\uDC6C\uDB40\uDC73|\uDB40\uDC73\uDB40\uDC63\uDB40\uDC74|\uDB40\uDC65\uDB40\uDC6E\uDB40\uDC67)\uDB40\uDC7F|(?:\uD83E\uDDD1\uD83C\uDFFF\u200D\u2764\uFE0F\u200D(?:\uD83D\uDC8B\u200D)?\uD83E\uDDD1|\uD83D\uDC69\uD83C\uDFFF\u200D\uD83E\uDD1D\u200D(?:\uD83D[\uDC68\uDC69]))(?:\uD83C[\uDFFB-\uDFFE])|(?:\uD83E\uDDD1\uD83C\uDFFE\u200D\u2764\uFE0F\u200D(?:\uD83D\uDC8B\u200D)?\uD83E\uDDD1|\uD83D\uDC69\uD83C\uDFFE\u200D\uD83E\uDD1D\u200D(?:\uD83D[\uDC68\uDC69]))(?:\uD83C[\uDFFB-\uDFFD\uDFFF])|(?:\uD83E\uDDD1\uD83C\uDFFD\u200D\u2764\uFE0F\u200D(?:\uD83D\uDC8B\u200D)?\uD83E\uDDD1|\uD83D\uDC69\uD83C\uDFFD\u200D\uD83E\uDD1D\u200D(?:\uD83D[\uDC68\uDC69]))(?:\uD83C[\uDFFB\uDFFC\uDFFE\uDFFF])|(?:\uD83E\uDDD1\uD83C\uDFFC\u200D\u2764\uFE0F\u200D(?:\uD83D\uDC8B\u200D)?\uD83E\uDDD1|\uD83D\uDC69\uD83C\uDFFC\u200D\uD83E\uDD1D\u200D(?:\uD83D[\uDC68\uDC69]))(?:\uD83C[\uDFFB\uDFFD-\uDFFF])|(?:\uD83E\uDDD1\uD83C\uDFFB\u200D\u2764\uFE0F\u200D(?:\uD83D\uDC8B\u200D)?\uD83E\uDDD1|\uD83D\uDC69\uD83C\uDFFB\u200D\uD83E\uDD1D\u200D(?:\uD83D[\uDC68\uDC69]))(?:\uD83C[\uDFFC-\uDFFF])|\uD83D\uDC68(?:\uD83C\uDFFB(?:\u200D(?:\u2764\uFE0F\u200D(?:\uD83D\uDC8B\u200D\uD83D\uDC68(?:\uD83C[\uDFFB-\uDFFF])|\uD83D\uDC68(?:\uD83C[\uDFFB-\uDFFF]))|\uD83E\uDD1D\u200D\uD83D\uDC68(?:\uD83C[\uDFFC-\uDFFF])|[\u2695\u2696\u2708]\uFE0F|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD]))?|(?:\uD83C[\uDFFC-\uDFFF])\u200D\u2764\uFE0F\u200D(?:\uD83D\uDC8B\u200D\uD83D\uDC68(?:\uD83C[\uDFFB-\uDFFF])|\uD83D\uDC68(?:\uD83C[\uDFFB-\uDFFF]))|\u200D(?:\u2764\uFE0F\u200D(?:\uD83D\uDC8B\u200D)?\uD83D\uDC68|(?:\uD83D[\uDC68\uDC69])\u200D(?:\uD83D\uDC66\u200D\uD83D\uDC66|\uD83D\uDC67\u200D(?:\uD83D[\uDC66\uDC67]))|\uD83D\uDC66\u200D\uD83D\uDC66|\uD83D\uDC67\u200D(?:\uD83D[\uDC66\uDC67])|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C\uDFFF\u200D(?:\uD83E\uDD1D\u200D\uD83D\uDC68(?:\uD83C[\uDFFB-\uDFFE])|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C\uDFFE\u200D(?:\uD83E\uDD1D\u200D\uD83D\uDC68(?:\uD83C[\uDFFB-\uDFFD\uDFFF])|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C\uDFFD\u200D(?:\uD83E\uDD1D\u200D\uD83D\uDC68(?:\uD83C[\uDFFB\uDFFC\uDFFE\uDFFF])|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C\uDFFC\u200D(?:\uD83E\uDD1D\u200D\uD83D\uDC68(?:\uD83C[\uDFFB\uDFFD-\uDFFF])|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|(?:\uD83C\uDFFF\u200D[\u2695\u2696\u2708]|\uD83C\uDFFE\u200D[\u2695\u2696\u2708]|\uD83C\uDFFD\u200D[\u2695\u2696\u2708]|\uD83C\uDFFC\u200D[\u2695\u2696\u2708]|\u200D[\u2695\u2696\u2708])\uFE0F|\u200D(?:(?:\uD83D[\uDC68\uDC69])\u200D(?:\uD83D[\uDC66\uDC67])|\uD83D[\uDC66\uDC67])|\uD83C\uDFFF|\uD83C\uDFFE|\uD83C\uDFFD|\uD83C\uDFFC)?|(?:\uD83D\uDC69(?:\uD83C\uDFFB\u200D\u2764\uFE0F\u200D(?:\uD83D\uDC8B\u200D(?:\uD83D[\uDC68\uDC69])|\uD83D[\uDC68\uDC69])|(?:\uD83C[\uDFFC-\uDFFF])\u200D\u2764\uFE0F\u200D(?:\uD83D\uDC8B\u200D(?:\uD83D[\uDC68\uDC69])|\uD83D[\uDC68\uDC69]))|\uD83E\uDDD1(?:\uD83C[\uDFFB-\uDFFF])\u200D\uD83E\uDD1D\u200D\uD83E\uDDD1)(?:\uD83C[\uDFFB-\uDFFF])|\uD83D\uDC69\u200D\uD83D\uDC69\u200D(?:\uD83D\uDC66\u200D\uD83D\uDC66|\uD83D\uDC67\u200D(?:\uD83D[\uDC66\uDC67]))|\uD83D\uDC69(?:\u200D(?:\u2764\uFE0F\u200D(?:\uD83D\uDC8B\u200D(?:\uD83D[\uDC68\uDC69])|\uD83D[\uDC68\uDC69])|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C\uDFFF\u200D(?:\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C\uDFFE\u200D(?:\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C\uDFFD\u200D(?:\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C\uDFFC\u200D(?:\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C\uDFFB\u200D(?:\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD]))|\uD83E\uDDD1(?:\u200D(?:\uD83E\uDD1D\u200D\uD83E\uDDD1|\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C\uDFFF\u200D(?:\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C\uDFFE\u200D(?:\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C\uDFFD\u200D(?:\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C\uDFFC\u200D(?:\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C\uDFFB\u200D(?:\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD]))|\uD83D\uDC69\u200D\uD83D\uDC66\u200D\uD83D\uDC66|\uD83D\uDC69\u200D\uD83D\uDC69\u200D(?:\uD83D[\uDC66\uDC67])|\uD83D\uDC69\u200D\uD83D\uDC67\u200D(?:\uD83D[\uDC66\uDC67])|(?:\uD83D\uDC41\uFE0F\u200D\uD83D\uDDE8|\uD83E\uDDD1(?:\uD83C\uDFFF\u200D[\u2695\u2696\u2708]|\uD83C\uDFFE\u200D[\u2695\u2696\u2708]|\uD83C\uDFFD\u200D[\u2695\u2696\u2708]|\uD83C\uDFFC\u200D[\u2695\u2696\u2708]|\uD83C\uDFFB\u200D[\u2695\u2696\u2708]|\u200D[\u2695\u2696\u2708])|\uD83D\uDC69(?:\uD83C\uDFFF\u200D[\u2695\u2696\u2708]|\uD83C\uDFFE\u200D[\u2695\u2696\u2708]|\uD83C\uDFFD\u200D[\u2695\u2696\u2708]|\uD83C\uDFFC\u200D[\u2695\u2696\u2708]|\uD83C\uDFFB\u200D[\u2695\u2696\u2708]|\u200D[\u2695\u2696\u2708])|\uD83D\uDE36\u200D\uD83C\uDF2B|\uD83C\uDFF3\uFE0F\u200D\u26A7|\uD83D\uDC3B\u200D\u2744|(?:(?:\uD83C[\uDFC3\uDFC4\uDFCA]|\uD83D[\uDC6E\uDC70\uDC71\uDC73\uDC77\uDC81\uDC82\uDC86\uDC87\uDE45-\uDE47\uDE4B\uDE4D\uDE4E\uDEA3\uDEB4-\uDEB6]|\uD83E[\uDD26\uDD35\uDD37-\uDD39\uDD3D\uDD3E\uDDB8\uDDB9\uDDCD-\uDDCF\uDDD4\uDDD6-\uDDDD])(?:\uD83C[\uDFFB-\uDFFF])|\uD83D\uDC6F|\uD83E[\uDD3C\uDDDE\uDDDF])\u200D[\u2640\u2642]|(?:\u26F9|\uD83C[\uDFCB\uDFCC]|\uD83D\uDD75)(?:\uFE0F|\uD83C[\uDFFB-\uDFFF])\u200D[\u2640\u2642]|\uD83C\uDFF4\u200D\u2620|(?:\uD83C[\uDFC3\uDFC4\uDFCA]|\uD83D[\uDC6E\uDC70\uDC71\uDC73\uDC77\uDC81\uDC82\uDC86\uDC87\uDE45-\uDE47\uDE4B\uDE4D\uDE4E\uDEA3\uDEB4-\uDEB6]|\uD83E[\uDD26\uDD35\uDD37-\uDD39\uDD3D\uDD3E\uDDB8\uDDB9\uDDCD-\uDDCF\uDDD4\uDDD6-\uDDDD])\u200D[\u2640\u2642]|[\xA9\xAE\u203C\u2049\u2122\u2139\u2194-\u2199\u21A9\u21AA\u2328\u23CF\u23ED-\u23EF\u23F1\u23F2\u23F8-\u23FA\u24C2\u25AA\u25AB\u25B6\u25C0\u25FB\u25FC\u2600-\u2604\u260E\u2611\u2618\u2620\u2622\u2623\u2626\u262A\u262E\u262F\u2638-\u263A\u2640\u2642\u265F\u2660\u2663\u2665\u2666\u2668\u267B\u267E\u2692\u2694-\u2697\u2699\u269B\u269C\u26A0\u26A7\u26B0\u26B1\u26C8\u26CF\u26D1\u26D3\u26E9\u26F0\u26F1\u26F4\u26F7\u26F8\u2702\u2708\u2709\u270F\u2712\u2714\u2716\u271D\u2721\u2733\u2734\u2744\u2747\u2763\u27A1\u2934\u2935\u2B05-\u2B07\u3030\u303D\u3297\u3299]|\uD83C[\uDD70\uDD71\uDD7E\uDD7F\uDE02\uDE37\uDF21\uDF24-\uDF2C\uDF36\uDF7D\uDF96\uDF97\uDF99-\uDF9B\uDF9E\uDF9F\uDFCD\uDFCE\uDFD4-\uDFDF\uDFF5\uDFF7]|\uD83D[\uDC3F\uDCFD\uDD49\uDD4A\uDD6F\uDD70\uDD73\uDD76-\uDD79\uDD87\uDD8A-\uDD8D\uDDA5\uDDA8\uDDB1\uDDB2\uDDBC\uDDC2-\uDDC4\uDDD1-\uDDD3\uDDDC-\uDDDE\uDDE1\uDDE3\uDDE8\uDDEF\uDDF3\uDDFA\uDECB\uDECD-\uDECF\uDEE0-\uDEE5\uDEE9\uDEF0\uDEF3])\uFE0F|\uD83C\uDFF3\uFE0F\u200D\uD83C\uDF08|\uD83D\uDC69\u200D\uD83D\uDC67|\uD83D\uDC69\u200D\uD83D\uDC66|\uD83D\uDE35\u200D\uD83D\uDCAB|\uD83D\uDE2E\u200D\uD83D\uDCA8|\uD83D\uDC15\u200D\uD83E\uDDBA|\uD83E\uDDD1(?:\uD83C\uDFFF|\uD83C\uDFFE|\uD83C\uDFFD|\uD83C\uDFFC|\uD83C\uDFFB)?|\uD83D\uDC69(?:\uD83C\uDFFF|\uD83C\uDFFE|\uD83C\uDFFD|\uD83C\uDFFC|\uD83C\uDFFB)?|\uD83C\uDDFD\uD83C\uDDF0|\uD83C\uDDF6\uD83C\uDDE6|\uD83C\uDDF4\uD83C\uDDF2|\uD83D\uDC08\u200D\u2B1B|\u2764\uFE0F\u200D(?:\uD83D\uDD25|\uD83E\uDE79)|\uD83D\uDC41\uFE0F|\uD83C\uDFF3\uFE0F|\uD83C\uDDFF(?:\uD83C[\uDDE6\uDDF2\uDDFC])|\uD83C\uDDFE(?:\uD83C[\uDDEA\uDDF9])|\uD83C\uDDFC(?:\uD83C[\uDDEB\uDDF8])|\uD83C\uDDFB(?:\uD83C[\uDDE6\uDDE8\uDDEA\uDDEC\uDDEE\uDDF3\uDDFA])|\uD83C\uDDFA(?:\uD83C[\uDDE6\uDDEC\uDDF2\uDDF3\uDDF8\uDDFE\uDDFF])|\uD83C\uDDF9(?:\uD83C[\uDDE6\uDDE8\uDDE9\uDDEB-\uDDED\uDDEF-\uDDF4\uDDF7\uDDF9\uDDFB\uDDFC\uDDFF])|\uD83C\uDDF8(?:\uD83C[\uDDE6-\uDDEA\uDDEC-\uDDF4\uDDF7-\uDDF9\uDDFB\uDDFD-\uDDFF])|\uD83C\uDDF7(?:\uD83C[\uDDEA\uDDF4\uDDF8\uDDFA\uDDFC])|\uD83C\uDDF5(?:\uD83C[\uDDE6\uDDEA-\uDDED\uDDF0-\uDDF3\uDDF7-\uDDF9\uDDFC\uDDFE])|\uD83C\uDDF3(?:\uD83C[\uDDE6\uDDE8\uDDEA-\uDDEC\uDDEE\uDDF1\uDDF4\uDDF5\uDDF7\uDDFA\uDDFF])|\uD83C\uDDF2(?:\uD83C[\uDDE6\uDDE8-\uDDED\uDDF0-\uDDFF])|\uD83C\uDDF1(?:\uD83C[\uDDE6-\uDDE8\uDDEE\uDDF0\uDDF7-\uDDFB\uDDFE])|\uD83C\uDDF0(?:\uD83C[\uDDEA\uDDEC-\uDDEE\uDDF2\uDDF3\uDDF5\uDDF7\uDDFC\uDDFE\uDDFF])|\uD83C\uDDEF(?:\uD83C[\uDDEA\uDDF2\uDDF4\uDDF5])|\uD83C\uDDEE(?:\uD83C[\uDDE8-\uDDEA\uDDF1-\uDDF4\uDDF6-\uDDF9])|\uD83C\uDDED(?:\uD83C[\uDDF0\uDDF2\uDDF3\uDDF7\uDDF9\uDDFA])|\uD83C\uDDEC(?:\uD83C[\uDDE6\uDDE7\uDDE9-\uDDEE\uDDF1-\uDDF3\uDDF5-\uDDFA\uDDFC\uDDFE])|\uD83C\uDDEB(?:\uD83C[\uDDEE-\uDDF0\uDDF2\uDDF4\uDDF7])|\uD83C\uDDEA(?:\uD83C[\uDDE6\uDDE8\uDDEA\uDDEC\uDDED\uDDF7-\uDDFA])|\uD83C\uDDE9(?:\uD83C[\uDDEA\uDDEC\uDDEF\uDDF0\uDDF2\uDDF4\uDDFF])|\uD83C\uDDE8(?:\uD83C[\uDDE6\uDDE8\uDDE9\uDDEB-\uDDEE\uDDF0-\uDDF5\uDDF7\uDDFA-\uDDFF])|\uD83C\uDDE7(?:\uD83C[\uDDE6\uDDE7\uDDE9-\uDDEF\uDDF1-\uDDF4\uDDF6-\uDDF9\uDDFB\uDDFC\uDDFE\uDDFF])|\uD83C\uDDE6(?:\uD83C[\uDDE8-\uDDEC\uDDEE\uDDF1\uDDF2\uDDF4\uDDF6-\uDDFA\uDDFC\uDDFD\uDDFF])|[#\*0-9]\uFE0F\u20E3|\u2764\uFE0F|(?:\uD83C[\uDFC3\uDFC4\uDFCA]|\uD83D[\uDC6E\uDC70\uDC71\uDC73\uDC77\uDC81\uDC82\uDC86\uDC87\uDE45-\uDE47\uDE4B\uDE4D\uDE4E\uDEA3\uDEB4-\uDEB6]|\uD83E[\uDD26\uDD35\uDD37-\uDD39\uDD3D\uDD3E\uDDB8\uDDB9\uDDCD-\uDDCF\uDDD4\uDDD6-\uDDDD])(?:\uD83C[\uDFFB-\uDFFF])|(?:\u26F9|\uD83C[\uDFCB\uDFCC]|\uD83D\uDD75)(?:\uFE0F|\uD83C[\uDFFB-\uDFFF])|\uD83C\uDFF4|(?:[\u270A\u270B]|\uD83C[\uDF85\uDFC2\uDFC7]|\uD83D[\uDC42\uDC43\uDC46-\uDC50\uDC66\uDC67\uDC6B-\uDC6D\uDC72\uDC74-\uDC76\uDC78\uDC7C\uDC83\uDC85\uDC8F\uDC91\uDCAA\uDD7A\uDD95\uDD96\uDE4C\uDE4F\uDEC0\uDECC]|\uD83E[\uDD0C\uDD0F\uDD18-\uDD1C\uDD1E\uDD1F\uDD30-\uDD34\uDD36\uDD77\uDDB5\uDDB6\uDDBB\uDDD2\uDDD3\uDDD5])(?:\uD83C[\uDFFB-\uDFFF])|(?:[\u261D\u270C\u270D]|\uD83D[\uDD74\uDD90])(?:\uFE0F|\uD83C[\uDFFB-\uDFFF])|[\u270A\u270B]|\uD83C[\uDF85\uDFC2\uDFC7]|\uD83D[\uDC08\uDC15\uDC3B\uDC42\uDC43\uDC46-\uDC50\uDC66\uDC67\uDC6B-\uDC6D\uDC72\uDC74-\uDC76\uDC78\uDC7C\uDC83\uDC85\uDC8F\uDC91\uDCAA\uDD7A\uDD95\uDD96\uDE2E\uDE35\uDE36\uDE4C\uDE4F\uDEC0\uDECC]|\uD83E[\uDD0C\uDD0F\uDD18-\uDD1C\uDD1E\uDD1F\uDD30-\uDD34\uDD36\uDD77\uDDB5\uDDB6\uDDBB\uDDD2\uDDD3\uDDD5]|\uD83C[\uDFC3\uDFC4\uDFCA]|\uD83D[\uDC6E\uDC70\uDC71\uDC73\uDC77\uDC81\uDC82\uDC86\uDC87\uDE45-\uDE47\uDE4B\uDE4D\uDE4E\uDEA3\uDEB4-\uDEB6]|\uD83E[\uDD26\uDD35\uDD37-\uDD39\uDD3D\uDD3E\uDDB8\uDDB9\uDDCD-\uDDCF\uDDD4\uDDD6-\uDDDD]|\uD83D\uDC6F|\uD83E[\uDD3C\uDDDE\uDDDF]|[\u231A\u231B\u23E9-\u23EC\u23F0\u23F3\u25FD\u25FE\u2614\u2615\u2648-\u2653\u267F\u2693\u26A1\u26AA\u26AB\u26BD\u26BE\u26C4\u26C5\u26CE\u26D4\u26EA\u26F2\u26F3\u26F5\u26FA\u26FD\u2705\u2728\u274C\u274E\u2753-\u2755\u2757\u2795-\u2797\u27B0\u27BF\u2B1B\u2B1C\u2B50\u2B55]|\uD83C[\uDC04\uDCCF\uDD8E\uDD91-\uDD9A\uDE01\uDE1A\uDE2F\uDE32-\uDE36\uDE38-\uDE3A\uDE50\uDE51\uDF00-\uDF20\uDF2D-\uDF35\uDF37-\uDF7C\uDF7E-\uDF84\uDF86-\uDF93\uDFA0-\uDFC1\uDFC5\uDFC6\uDFC8\uDFC9\uDFCF-\uDFD3\uDFE0-\uDFF0\uDFF8-\uDFFF]|\uD83D[\uDC00-\uDC07\uDC09-\uDC14\uDC16-\uDC3A\uDC3C-\uDC3E\uDC40\uDC44\uDC45\uDC51-\uDC65\uDC6A\uDC79-\uDC7B\uDC7D-\uDC80\uDC84\uDC88-\uDC8E\uDC90\uDC92-\uDCA9\uDCAB-\uDCFC\uDCFF-\uDD3D\uDD4B-\uDD4E\uDD50-\uDD67\uDDA4\uDDFB-\uDE2D\uDE2F-\uDE34\uDE37-\uDE44\uDE48-\uDE4A\uDE80-\uDEA2\uDEA4-\uDEB3\uDEB7-\uDEBF\uDEC1-\uDEC5\uDED0-\uDED2\uDED5-\uDED7\uDEEB\uDEEC\uDEF4-\uDEFC\uDFE0-\uDFEB]|\uD83E[\uDD0D\uDD0E\uDD10-\uDD17\uDD1D\uDD20-\uDD25\uDD27-\uDD2F\uDD3A\uDD3F-\uDD45\uDD47-\uDD76\uDD78\uDD7A-\uDDB4\uDDB7\uDDBA\uDDBC-\uDDCB\uDDD0\uDDE0-\uDDFF\uDE70-\uDE74\uDE78-\uDE7A\uDE80-\uDE86\uDE90-\uDEA8\uDEB0-\uDEB6\uDEC0-\uDEC2\uDED0-\uDED6]|(?:[\u231A\u231B\u23E9-\u23EC\u23F0\u23F3\u25FD\u25FE\u2614\u2615\u2648-\u2653\u267F\u2693\u26A1\u26AA\u26AB\u26BD\u26BE\u26C4\u26C5\u26CE\u26D4\u26EA\u26F2\u26F3\u26F5\u26FA\u26FD\u2705\u270A\u270B\u2728\u274C\u274E\u2753-\u2755\u2757\u2795-\u2797\u27B0\u27BF\u2B1B\u2B1C\u2B50\u2B55]|\uD83C[\uDC04\uDCCF\uDD8E\uDD91-\uDD9A\uDDE6-\uDDFF\uDE01\uDE1A\uDE2F\uDE32-\uDE36\uDE38-\uDE3A\uDE50\uDE51\uDF00-\uDF20\uDF2D-\uDF35\uDF37-\uDF7C\uDF7E-\uDF93\uDFA0-\uDFCA\uDFCF-\uDFD3\uDFE0-\uDFF0\uDFF4\uDFF8-\uDFFF]|\uD83D[\uDC00-\uDC3E\uDC40\uDC42-\uDCFC\uDCFF-\uDD3D\uDD4B-\uDD4E\uDD50-\uDD67\uDD7A\uDD95\uDD96\uDDA4\uDDFB-\uDE4F\uDE80-\uDEC5\uDECC\uDED0-\uDED2\uDED5-\uDED7\uDEEB\uDEEC\uDEF4-\uDEFC\uDFE0-\uDFEB]|\uD83E[\uDD0C-\uDD3A\uDD3C-\uDD45\uDD47-\uDD78\uDD7A-\uDDCB\uDDCD-\uDDFF\uDE70-\uDE74\uDE78-\uDE7A\uDE80-\uDE86\uDE90-\uDEA8\uDEB0-\uDEB6\uDEC0-\uDEC2\uDED0-\uDED6])|(?:[#\*0-9\xA9\xAE\u203C\u2049\u2122\u2139\u2194-\u2199\u21A9\u21AA\u231A\u231B\u2328\u23CF\u23E9-\u23F3\u23F8-\u23FA\u24C2\u25AA\u25AB\u25B6\u25C0\u25FB-\u25FE\u2600-\u2604\u260E\u2611\u2614\u2615\u2618\u261D\u2620\u2622\u2623\u2626\u262A\u262E\u262F\u2638-\u263A\u2640\u2642\u2648-\u2653\u265F\u2660\u2663\u2665\u2666\u2668\u267B\u267E\u267F\u2692-\u2697\u2699\u269B\u269C\u26A0\u26A1\u26A7\u26AA\u26AB\u26B0\u26B1\u26BD\u26BE\u26C4\u26C5\u26C8\u26CE\u26CF\u26D1\u26D3\u26D4\u26E9\u26EA\u26F0-\u26F5\u26F7-\u26FA\u26FD\u2702\u2705\u2708-\u270D\u270F\u2712\u2714\u2716\u271D\u2721\u2728\u2733\u2734\u2744\u2747\u274C\u274E\u2753-\u2755\u2757\u2763\u2764\u2795-\u2797\u27A1\u27B0\u27BF\u2934\u2935\u2B05-\u2B07\u2B1B\u2B1C\u2B50\u2B55\u3030\u303D\u3297\u3299]|\uD83C[\uDC04\uDCCF\uDD70\uDD71\uDD7E\uDD7F\uDD8E\uDD91-\uDD9A\uDDE6-\uDDFF\uDE01\uDE02\uDE1A\uDE2F\uDE32-\uDE3A\uDE50\uDE51\uDF00-\uDF21\uDF24-\uDF93\uDF96\uDF97\uDF99-\uDF9B\uDF9E-\uDFF0\uDFF3-\uDFF5\uDFF7-\uDFFF]|\uD83D[\uDC00-\uDCFD\uDCFF-\uDD3D\uDD49-\uDD4E\uDD50-\uDD67\uDD6F\uDD70\uDD73-\uDD7A\uDD87\uDD8A-\uDD8D\uDD90\uDD95\uDD96\uDDA4\uDDA5\uDDA8\uDDB1\uDDB2\uDDBC\uDDC2-\uDDC4\uDDD1-\uDDD3\uDDDC-\uDDDE\uDDE1\uDDE3\uDDE8\uDDEF\uDDF3\uDDFA-\uDE4F\uDE80-\uDEC5\uDECB-\uDED2\uDED5-\uDED7\uDEE0-\uDEE5\uDEE9\uDEEB\uDEEC\uDEF0\uDEF3-\uDEFC\uDFE0-\uDFEB]|\uD83E[\uDD0C-\uDD3A\uDD3C-\uDD45\uDD47-\uDD78\uDD7A-\uDDCB\uDDCD-\uDDFF\uDE70-\uDE74\uDE78-\uDE7A\uDE80-\uDE86\uDE90-\uDEA8\uDEB0-\uDEB6\uDEC0-\uDEC2\uDED0-\uDED6])\uFE0F|(?:[\u261D\u26F9\u270A-\u270D]|\uD83C[\uDF85\uDFC2-\uDFC4\uDFC7\uDFCA-\uDFCC]|\uD83D[\uDC42\uDC43\uDC46-\uDC50\uDC66-\uDC78\uDC7C\uDC81-\uDC83\uDC85-\uDC87\uDC8F\uDC91\uDCAA\uDD74\uDD75\uDD7A\uDD90\uDD95\uDD96\uDE45-\uDE47\uDE4B-\uDE4F\uDEA3\uDEB4-\uDEB6\uDEC0\uDECC]|\uD83E[\uDD0C\uDD0F\uDD18-\uDD1F\uDD26\uDD30-\uDD39\uDD3C-\uDD3E\uDD77\uDDB5\uDDB6\uDDB8\uDDB9\uDDBB\uDDCD-\uDDCF\uDDD1-\uDDDD])/g}});K(),K(),K();var Ar=a(t=>{var e,u,s;let n=(e=process.stdout.columns)!=null?e:Number.POSITIVE_INFINITY;return typeof t=="function"&&(t=t(n)),t||(t={}),Array.isArray(t)?{columns:t,stdoutColumns:n}:{columns:(u=t.columns)!=null?u:[],stdoutColumns:(s=t.stdoutColumns)!=null?s:n}},"v");K(),K(),K(),K(),K();function yr({onlyFirst:t=!1}={}){let e=["[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]+)*|[a-zA-Z\\d]+(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)","(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-ntqry=><~]))"].join("|");return new RegExp(e,t?void 0:"g")}a(yr,"w$1");function vu(t){if(typeof t!="string")throw new TypeError(`Expected a \`string\`, got \`${typeof t}\``);return t.replace(yr(),"")}a(vu,"d$1"),K();function wr(t){return Number.isInteger(t)?t>=4352&&(t<=4447||t===9001||t===9002||11904<=t&&t<=12871&&t!==12351||12880<=t&&t<=19903||19968<=t&&t<=42182||43360<=t&&t<=43388||44032<=t&&t<=55203||63744<=t&&t<=64255||65040<=t&&t<=65049||65072<=t&&t<=65131||65281<=t&&t<=65376||65504<=t&&t<=65510||110592<=t&&t<=110593||127488<=t&&t<=127569||131072<=t&&t<=262141):!1}a(wr,"y$1");var Rr=mr(_r());function oe(t){if(typeof t!="string"||t.length===0||(t=vu(t),t.length===0))return 0;t=t.replace((0,Rr.default)()," ");let e=0;for(let u=0;u=127&&s<=159||s>=768&&s<=879||(s>65535&&u++,e+=wr(s)?2:1)}return e}a(oe,"g");var Su=a(t=>Math.max(...t.split(` +`).map(oe)),"b$1"),br=a(t=>{let e=[];for(let u of t){let{length:s}=u,n=s-e.length;for(let r=0;re[r]&&(e[r]=i)}}return e},"k$1");K();var Bu=/^\d+%$/,$u={width:"auto",align:"left",contentWidth:0,paddingLeft:0,paddingRight:0,paddingTop:0,paddingBottom:0,horizontalPadding:0,paddingLeftString:"",paddingRightString:""},vr=a((t,e)=>{var u;let s=[];for(let n=0;n=e){let o=i-e,c=Math.ceil(u.paddingLeft/n*o),f=o-c;u.paddingLeft-=c,u.paddingRight-=f,u.horizontalPadding=u.paddingLeft+u.paddingRight}u.paddingLeftString=u.paddingLeft?" ".repeat(u.paddingLeft):"",u.paddingRightString=u.paddingRight?" ".repeat(u.paddingRight):"";let D=e-u.horizontalPadding;u.width=Math.max(Math.min(u.width,D),r)}}a(Sr,"aD");var Tu=a(()=>Object.assign([],{columns:0}),"G$1");function Br(t,e){let u=[Tu()],[s]=u;for(let n of t){let r=n.width+n.horizontalPadding;s.columns+r>e&&(s=Tu(),u.push(s)),s.push(n),s.columns+=r}for(let n of u){let r=n.reduce((l,p)=>l+p.width+p.horizontalPadding,0),i=e-r;if(i===0)continue;let D=n.filter(l=>"autoOverflow"in l),o=D.filter(l=>l.autoOverflow>0),c=o.reduce((l,p)=>l+p.autoOverflow,0),f=Math.min(c,i);for(let l of o){let p=Math.floor(l.autoOverflow/c*f);l.width+=p,i-=p}let h=Math.floor(i/D.length);for(let l=0;le=>`\x1B[${e+t}m`,"U$1"),Ou=a((t=0)=>e=>`\x1B[${38+t};5;${e}m`,"V$1"),Nu=a((t=0)=>(e,u,s)=>`\x1B[${38+t};2;${e};${u};${s}m`,"Y");function Tr(){let t=new Map,e={modifier:{reset:[0,0],bold:[1,22],dim:[2,22],italic:[3,23],underline:[4,24],overline:[53,55],inverse:[7,27],hidden:[8,28],strikethrough:[9,29]},color:{black:[30,39],red:[31,39],green:[32,39],yellow:[33,39],blue:[34,39],magenta:[35,39],cyan:[36,39],white:[37,39],blackBright:[90,39],redBright:[91,39],greenBright:[92,39],yellowBright:[93,39],blueBright:[94,39],magentaBright:[95,39],cyanBright:[96,39],whiteBright:[97,39]},bgColor:{bgBlack:[40,49],bgRed:[41,49],bgGreen:[42,49],bgYellow:[43,49],bgBlue:[44,49],bgMagenta:[45,49],bgCyan:[46,49],bgWhite:[47,49],bgBlackBright:[100,49],bgRedBright:[101,49],bgGreenBright:[102,49],bgYellowBright:[103,49],bgBlueBright:[104,49],bgMagentaBright:[105,49],bgCyanBright:[106,49],bgWhiteBright:[107,49]}};e.color.gray=e.color.blackBright,e.bgColor.bgGray=e.bgColor.bgBlackBright,e.color.grey=e.color.blackBright,e.bgColor.bgGrey=e.bgColor.bgBlackBright;for(let[u,s]of Object.entries(e)){for(let[n,r]of Object.entries(s))e[n]={open:`\x1B[${r[0]}m`,close:`\x1B[${r[1]}m`},s[n]=e[n],t.set(r[0],r[1]);Object.defineProperty(e,u,{value:s,enumerable:!1})}return Object.defineProperty(e,"codes",{value:t,enumerable:!1}),e.color.close="\x1B[39m",e.bgColor.close="\x1B[49m",e.color.ansi=xu(),e.color.ansi256=Ou(),e.color.ansi16m=Nu(),e.bgColor.ansi=xu(ht),e.bgColor.ansi256=Ou(ht),e.bgColor.ansi16m=Nu(ht),Object.defineProperties(e,{rgbToAnsi256:{value:a((u,s,n)=>u===s&&s===n?u<8?16:u>248?231:Math.round((u-8)/247*24)+232:16+36*Math.round(u/255*5)+6*Math.round(s/255*5)+Math.round(n/255*5),"value"),enumerable:!1},hexToRgb:{value:a(u=>{let s=/(?[a-f\d]{6}|[a-f\d]{3})/i.exec(u.toString(16));if(!s)return[0,0,0];let{colorString:n}=s.groups;n.length===3&&(n=n.split("").map(i=>i+i).join(""));let r=Number.parseInt(n,16);return[r>>16&255,r>>8&255,r&255]},"value"),enumerable:!1},hexToAnsi256:{value:a(u=>e.rgbToAnsi256(...e.hexToRgb(u)),"value"),enumerable:!1},ansi256ToAnsi:{value:a(u=>{if(u<8)return 30+u;if(u<16)return 90+(u-8);let s,n,r;if(u>=232)s=((u-232)*10+8)/255,n=s,r=s;else{u-=16;let o=u%36;s=Math.floor(u/36)/5,n=Math.floor(o/6)/5,r=o%6/5}let i=Math.max(s,n,r)*2;if(i===0)return 30;let D=30+(Math.round(r)<<2|Math.round(n)<<1|Math.round(s));return i===2&&(D+=60),D},"value"),enumerable:!1},rgbToAnsi:{value:a((u,s,n)=>e.ansi256ToAnsi(e.rgbToAnsi256(u,s,n)),"value"),enumerable:!1},hexToAnsi:{value:a(u=>e.ansi256ToAnsi(e.hexToAnsi256(u)),"value"),enumerable:!1}}),e}a(Tr,"AD");var xr=Tr(),Or=xr,We=new Set(["\x1B","\x9B"]),Nr=39,dt="\x07",Hu="[",Hr="]",Pu="m",Et=`${Hr}8;;`,Lu=a(t=>`${We.values().next().value}${Hu}${t}${Pu}`,"J$1"),Iu=a(t=>`${We.values().next().value}${Et}${t}${dt}`,"Q"),Pr=a(t=>t.split(" ").map(e=>oe(e)),"hD"),pt=a((t,e,u)=>{let s=[...e],n=!1,r=!1,i=oe(vu(t[t.length-1]));for(let[D,o]of s.entries()){let c=oe(o);if(i+c<=u?t[t.length-1]+=o:(t.push(o),i=0),We.has(o)&&(n=!0,r=s.slice(D+1).join("").startsWith(Et)),n){r?o===dt&&(n=!1,r=!1):o===Pu&&(n=!1);continue}i+=c,i===u&&D0&&t.length>1&&(t[t.length-2]+=t.pop())},"S$1"),Lr=a(t=>{let e=t.split(" "),u=e.length;for(;u>0&&!(oe(e[u-1])>0);)u--;return u===e.length?t:e.slice(0,u).join(" ")+e.slice(u).join("")},"cD"),Ir=a((t,e,u={})=>{if(u.trim!==!1&&t.trim()==="")return"";let s="",n,r,i=Pr(t),D=[""];for(let[c,f]of t.split(" ").entries()){u.trim!==!1&&(D[D.length-1]=D[D.length-1].trimStart());let h=oe(D[D.length-1]);if(c!==0&&(h>=e&&(u.wordWrap===!1||u.trim===!1)&&(D.push(""),h=0),(h>0||u.trim===!1)&&(D[D.length-1]+=" ",h++)),u.hard&&i[c]>e){let l=e-h,p=1+Math.floor((i[c]-l-1)/e);Math.floor((i[c]-1)/e)e&&h>0&&i[c]>0){if(u.wordWrap===!1&&he&&u.wordWrap===!1){pt(D,f,e);continue}D[D.length-1]+=f}u.trim!==!1&&(D=D.map(c=>Lr(c)));let o=[...D.join(` +`)];for(let[c,f]of o.entries()){if(s+=f,We.has(f)){let{groups:l}=new RegExp(`(?:\\${Hu}(?\\d+)m|\\${Et}(?.*)${dt})`).exec(o.slice(c).join(""))||{groups:{}};if(l.code!==void 0){let p=Number.parseFloat(l.code);n=p===Nr?void 0:p}else l.uri!==void 0&&(r=l.uri.length===0?void 0:l.uri)}let h=Or.codes.get(Number(n));o[c+1]===` +`?(r&&(s+=Iu("")),n&&h&&(s+=Lu(h))):f===` +`&&(n&&h&&(s+=Lu(n)),r&&(s+=Iu(r)))}return s},"dD");function kr(t,e,u){return String(t).normalize().replace(/\r\n/g,` +`).split(` +`).map(s=>Ir(s,e,u)).join(` +`)}a(kr,"T$1");var ku=a(t=>Array.from({length:t}).fill(""),"X");function Mr(t,e){let u=[],s=0;for(let n of t){let r=0,i=n.map(o=>{var c;let f=(c=e[s])!=null?c:"";s+=1,o.preprocess&&(f=o.preprocess(f)),Su(f)>o.width&&(f=kr(f,o.width,{hard:!0}));let h=f.split(` +`);if(o.postprocess){let{postprocess:l}=o;h=h.map((p,C)=>l.call(o,p,C))}return o.paddingTop&&h.unshift(...ku(o.paddingTop)),o.paddingBottom&&h.push(...ku(o.paddingBottom)),h.length>r&&(r=h.length),ft(Ge({},o),{lines:h})}),D=[];for(let o=0;o{var h;let l=(h=f.lines[o])!=null?h:"",p=Number.isFinite(f.width)?" ".repeat(f.width-oe(l)):"",C=f.paddingLeftString;return f.align==="right"&&(C+=p),C+=l,f.align==="left"&&(C+=p),C+f.paddingRightString}).join("");D.push(c)}u.push(D.join(` +`))}return u.join(` +`)}a(Mr,"P");function Gr(t,e){if(!t||t.length===0)return"";let u=br(t),s=u.length;if(s===0)return"";let{stdoutColumns:n,columns:r}=Ar(e);if(r.length>s)throw new Error(`${r.length} columns defined, but only ${s} columns found`);let i=$r(n,r,u);return t.map(D=>Mr(i,D)).join(` +`)}a(Gr,"mD"),K();var Wr=["<",">","=",">=","<="];function jr(t){if(!Wr.includes(t))throw new TypeError(`Invalid breakpoint operator: ${t}`)}a(jr,"xD");function Ur(t){let e=Object.keys(t).map(u=>{let[s,n]=u.split(" ");jr(s);let r=Number.parseInt(n,10);if(Number.isNaN(r))throw new TypeError(`Invalid breakpoint value: ${n}`);let i=t[u];return{operator:s,breakpoint:r,value:i}}).sort((u,s)=>s.breakpoint-u.breakpoint);return u=>{var s;return(s=e.find(({operator:n,breakpoint:r})=>n==="="&&u===r||n===">"&&u>r||n==="<"&&u="&&u>=r||n==="<="&&u<=r))==null?void 0:s.value}}a(Ur,"wD");const Kr=a(t=>t.replace(/[\W_]([a-z\d])?/gi,(e,u)=>u?u.toUpperCase():""),"S"),Vr=a(t=>t.replace(/\B([A-Z])/g,"-$1").toLowerCase(),"q"),zr={"> 80":[{width:"content-width",paddingLeft:2,paddingRight:8},{width:"auto"}],"> 40":[{width:"auto",paddingLeft:2,paddingRight:8,preprocess:a(t=>t.trim(),"preprocess")},{width:"100%",paddingLeft:2,paddingBottom:1}],"> 0":{stdoutColumns:1e3,columns:[{width:"content-width",paddingLeft:2,paddingRight:8},{width:"content-width"}]}};function Yr(t){let e=!1;return{type:"table",data:{tableData:Object.keys(t).sort((u,s)=>u.localeCompare(s)).map(u=>{const s=t[u],n="alias"in s;return n&&(e=!0),{name:u,flag:s,flagFormatted:`--${Vr(u)}`,aliasesEnabled:e,aliasFormatted:n?`-${s.alias}`:void 0}}).map(u=>(u.aliasesEnabled=e,[{type:"flagName",data:u},{type:"flagDescription",data:u}])),tableBreakpoints:zr}}}a(Yr,"D");const Mu=a(t=>!t||(t.version??(t.help?t.help.version:void 0)),"A"),Gu=a(t=>{const e="parent"in t&&t.parent?.name;return(e?`${e} `:"")+t.name},"C");function qr(t){const e=[];t.name&&e.push(Gu(t));const u=Mu(t)??("parent"in t&&Mu(t.parent));if(u&&e.push(`v${u}`),e.length!==0)return{id:"name",type:"text",data:`${e.join(" ")} +`}}a(qr,"R");function Xr(t){const{help:e}=t;if(!(!e||!e.description))return{id:"description",type:"text",data:`${e.description} +`}}a(Xr,"L");function Qr(t){const e=t.help||{};if("usage"in e)return e.usage?{id:"usage",type:"section",data:{title:"Usage:",body:Array.isArray(e.usage)?e.usage.join(` +`):e.usage}}:void 0;if(t.name){const u=[],s=[Gu(t)];if(t.flags&&Object.keys(t.flags).length>0&&s.push("[flags...]"),t.parameters&&t.parameters.length>0){const{parameters:n}=t,r=n.indexOf("--"),i=r>-1&&n.slice(r+1).some(D=>D.startsWith("<"));s.push(n.map(D=>D!=="--"?D:i?"--":"[--]").join(" "))}if(s.length>1&&u.push(s.join(" ")),"commands"in t&&t.commands?.length&&u.push(`${t.name} `),u.length>0)return{id:"usage",type:"section",data:{title:"Usage:",body:u.join(` +`)}}}}a(Qr,"T");function Zr(t){return!("commands"in t)||!t.commands?.length?void 0:{id:"commands",type:"section",data:{title:"Commands:",body:{type:"table",data:{tableData:t.commands.map(e=>[e.options.name,e.options.help?e.options.help.description:""]),tableOptions:[{width:"content-width",paddingLeft:2,paddingRight:8}]}},indentBody:0}}}a(Zr,"_");function Jr(t){if(!(!t.flags||Object.keys(t.flags).length===0))return{id:"flags",type:"section",data:{title:"Flags:",body:Yr(t.flags),indentBody:0}}}a(Jr,"k");function ei(t){const{help:e}=t;if(!e||!e.examples||e.examples.length===0)return;let{examples:u}=e;if(Array.isArray(u)&&(u=u.join(` +`)),u)return{id:"examples",type:"section",data:{title:"Examples:",body:u}}}a(ei,"F");function ti(t){if(!("alias"in t)||!t.alias)return;const{alias:e}=t;return{id:"aliases",type:"section",data:{title:"Aliases:",body:Array.isArray(e)?e.join(", "):e}}}a(ti,"H");const ui=a(t=>[qr,Xr,Qr,Zr,Jr,ei,ti].map(e=>e(t)).filter(Boolean),"U"),si=bn.WriteStream.prototype.hasColors();class ni{static{a(this,"M")}text(e){return e}bold(e){return si?`\x1B[1m${e}\x1B[22m`:e.toLocaleUpperCase()}indentText({text:e,spaces:u}){return e.replace(/^/gm," ".repeat(u))}heading(e){return this.bold(e)}section({title:e,body:u,indentBody:s=2}){return`${(e?`${this.heading(e)} +`:"")+(u?this.indentText({text:this.render(u),spaces:s}):"")} +`}table({tableData:e,tableOptions:u,tableBreakpoints:s}){return Gr(e.map(n=>n.map(r=>this.render(r))),s?Ur(s):u)}flagParameter(e){return e===Boolean?"":e===String?"":e===Number?"":Array.isArray(e)?this.flagParameter(e[0]):""}flagOperator(e){return" "}flagName(e){const{flag:u,flagFormatted:s,aliasesEnabled:n,aliasFormatted:r}=e;let i="";if(r?i+=`${r}, `:n&&(i+=" "),i+=s,"placeholder"in u&&typeof u.placeholder=="string")i+=`${this.flagOperator(e)}${u.placeholder}`;else{const D=this.flagParameter("type"in u?u.type:u);D&&(i+=`${this.flagOperator(e)}${D}`)}return i}flagDefault(e){return JSON.stringify(e)}flagDescription({flag:e}){let u="description"in e?e.description??"":"";if("default"in e){let{default:s}=e;typeof s=="function"&&(s=s()),s&&(u+=` (default: ${this.flagDefault(s)})`)}return u}render(e){if(typeof e=="string")return e;if(Array.isArray(e))return e.map(u=>this.render(u)).join(` +`);if("type"in e&&this[e.type]){const u=this[e.type];if(typeof u=="function")return u.call(this,e.data)}throw new Error(`Invalid node type: ${JSON.stringify(e)}`)}}const Ct=/^[\w.-]+$/,{stringify:ee}=JSON,ri=/[|\\{}()[\]^$+*?.]/;function Ft(t){const e=[];let u,s;for(const n of t){if(s)throw new Error(`Invalid parameter: Spread parameter ${ee(s)} must be last`);const r=n[0],i=n[n.length-1];let D;if(r==="<"&&i===">"&&(D=!0,u))throw new Error(`Invalid parameter: Required parameter ${ee(n)} cannot come after optional parameter ${ee(u)}`);if(r==="["&&i==="]"&&(D=!1,u=n),D===void 0)throw new Error(`Invalid parameter: ${ee(n)}. Must be wrapped in <> (required parameter) or [] (optional parameter)`);let o=n.slice(1,-1);const c=o.slice(-3)==="...";c&&(s=n,o=o.slice(0,-3));const f=o.match(ri);if(f)throw new Error(`Invalid parameter: ${ee(n)}. Invalid character found ${ee(f[0])}`);e.push({name:o,required:D,spread:c})}return e}a(Ft,"w");function gt(t,e,u,s){for(let n=0;n{console.log(e.version)},"f");if(r&&o.flags.version===!0)return c(),process.exit(0);const f=new ni,h=D&&i?.render?i.render:C=>f.render(C),l=a(C=>{const g=ui({...e,...C?{help:C}:{},flags:n});console.log(h(g,f))},"u");if(D&&o.flags.help===!0)return l(),process.exit(0);if(e.parameters){let{parameters:C}=e,g=o._;const y=C.indexOf("--"),B=C.slice(y+1),H=Object.create(null);if(y>-1&&B.length>0){C=C.slice(0,y);const $=o._["--"];g=g.slice(0,-$.length||void 0),gt(H,Ft(C),g,l),gt(H,Ft(B),$,l)}else gt(H,Ft(C),g,l);Object.assign(o._,H)}const p={...o,showVersion:c,showHelp:l};return typeof u=="function"&&u(p),{command:t,...p}}a(Wu,"x");function Di(t,e){const u=new Map;for(const s of e){const n=[s.options.name],{alias:r}=s.options;r&&(Array.isArray(r)?n.push(...r):n.push(r));for(const i of n){if(u.has(i))throw new Error(`Duplicate command name found: ${ee(i)}`);u.set(i,s)}}return u.get(t)}a(Di,"z");function ju(t,e,u=process.argv.slice(2)){if(!t)throw new Error("Options is required");if("name"in t&&(!t.name||!Ct.test(t.name)))throw new Error(`Invalid script name: ${ee(t.name)}`);const s=u[0];if(t.commands&&Ct.test(s)){const n=Di(s,t.commands);if(n)return Wu(n.options.name,{...n.options,parent:t},n.callback,u.slice(1))}return Wu(void 0,t,e,u)}a(ju,"Z");function oi(t,e){if(!t)throw new Error("Command options are required");const{name:u}=t;if(t.name===void 0)throw new Error("Command name is required");if(!Ct.test(u))throw new Error(`Invalid command name ${JSON.stringify(u)}. Command names must be one word.`);return{options:t,callback:e}}a(oi,"G");var ai=typeof globalThis<"u"?globalThis:typeof window<"u"?window:typeof global<"u"?global:typeof self<"u"?self:{};function li(t){return t&&t.__esModule&&Object.prototype.hasOwnProperty.call(t,"default")?t.default:t}a(li,"getDefaultExportFromCjs");var fe={exports:{}},mt,Uu;function ci(){if(Uu)return mt;Uu=1,mt=s,s.sync=n;var t=De;function e(r,i){var D=i.pathExt!==void 0?i.pathExt:process.env.PATHEXT;if(!D||(D=D.split(";"),D.indexOf("")!==-1))return!0;for(var o=0;oObject.assign(new Error(`not found: ${t}`),{code:"ENOENT"}),"getNotFoundError"),qu=a((t,e)=>{const u=e.colon||Ei,s=t.match(/\//)||he&&t.match(/\\/)?[""]:[...he?[process.cwd()]:[],...(e.path||process.env.PATH||"").split(u)],n=he?e.pathExt||process.env.PATHEXT||".EXE;.CMD;.BAT;.COM":"",r=he?n.split(u):[""];return he&&t.indexOf(".")!==-1&&r[0]!==""&&r.unshift(""),{pathEnv:s,pathExt:r,pathExtExe:n}},"getPathInfo"),Xu=a((t,e,u)=>{typeof e=="function"&&(u=e,e={}),e||(e={});const{pathEnv:s,pathExt:n,pathExtExe:r}=qu(t,e),i=[],D=a(c=>new Promise((f,h)=>{if(c===s.length)return e.all&&i.length?f(i):h(Yu(t));const l=s[c],p=/^".*"$/.test(l)?l.slice(1,-1):l,C=Vu.join(p,t),g=!p&&/^\.[\\\/]/.test(t)?t.slice(0,2)+C:C;f(o(g,c,0))}),"step"),o=a((c,f,h)=>new Promise((l,p)=>{if(h===n.length)return l(D(f+1));const C=n[h];zu(c+C,{pathExt:r},(g,y)=>{if(!g&&y)if(e.all)i.push(c+C);else return l(c+C);return l(o(c,f,h+1))})}),"subStep");return u?D(0).then(c=>u(null,c),u):D(0)},"which$1"),pi=a((t,e)=>{e=e||{};const{pathEnv:u,pathExt:s,pathExtExe:n}=qu(t,e),r=[];for(let i=0;i{const e=t.env||process.env;return(t.platform||process.platform)!=="win32"?"PATH":Object.keys(e).reverse().find(s=>s.toUpperCase()==="PATH")||"Path"},"pathKey");yt.exports=Qu,yt.exports.default=Qu;var Fi=yt.exports;const Zu=z,gi=Ci,mi=Fi;function Ju(t,e){const u=t.options.env||process.env,s=process.cwd(),n=t.options.cwd!=null,r=n&&process.chdir!==void 0&&!process.chdir.disabled;if(r)try{process.chdir(t.options.cwd)}catch{}let i;try{i=gi.sync(t.command,{path:u[mi({env:u})],pathExt:e?Zu.delimiter:void 0})}catch{}finally{r&&process.chdir(s)}return i&&(i=Zu.resolve(n?t.options.cwd:"",i)),i}a(Ju,"resolveCommandAttempt");function _i(t){return Ju(t)||Ju(t,!0)}a(_i,"resolveCommand$1");var Ai=_i,wt={};const Rt=/([()\][%!^"`<>&|;, *?])/g;function yi(t){return t=t.replace(Rt,"^$1"),t}a(yi,"escapeCommand");function wi(t,e){return t=`${t}`,t=t.replace(/(\\*)"/g,'$1$1\\"'),t=t.replace(/(\\*)$/,"$1$1"),t=`"${t}"`,t=t.replace(Rt,"^$1"),e&&(t=t.replace(Rt,"^$1")),t}a(wi,"escapeArgument"),wt.command=yi,wt.argument=wi;var Ri=/^#!(.*)/;const bi=Ri;var vi=a((t="")=>{const e=t.match(bi);if(!e)return null;const[u,s]=e[0].replace(/#! ?/,"").split(" "),n=u.split("/").pop();return n==="env"?s:s?`${n} ${s}`:n},"shebangCommand$1");const bt=De,Si=vi;function Bi(t){const u=Buffer.alloc(150);let s;try{s=bt.openSync(t,"r"),bt.readSync(s,u,0,150,0),bt.closeSync(s)}catch{}return Si(u.toString())}a(Bi,"readShebang$1");var $i=Bi;const Ti=z,es=Ai,ts=wt,xi=$i,Oi=process.platform==="win32",Ni=/\.(?:com|exe)$/i,Hi=/node_modules[\\/].bin[\\/][^\\/]+\.cmd$/i;function Pi(t){t.file=es(t);const e=t.file&&xi(t.file);return e?(t.args.unshift(t.file),t.command=e,es(t)):t.file}a(Pi,"detectShebang");function Li(t){if(!Oi)return t;const e=Pi(t),u=!Ni.test(e);if(t.options.forceShell||u){const s=Hi.test(e);t.command=Ti.normalize(t.command),t.command=ts.command(t.command),t.args=t.args.map(r=>ts.argument(r,s));const n=[t.command].concat(t.args).join(" ");t.args=["/d","/s","/c",`"${n}"`],t.command=process.env.comspec||"cmd.exe",t.options.windowsVerbatimArguments=!0}return t}a(Li,"parseNonShell");function Ii(t,e,u){e&&!Array.isArray(e)&&(u=e,e=null),e=e?e.slice(0):[],u=Object.assign({},u);const s={command:t,args:e,options:u,file:void 0,original:{command:t,args:e}};return u.shell?s:Li(s)}a(Ii,"parse$5");var ki=Ii;const vt=process.platform==="win32";function St(t,e){return Object.assign(new Error(`${e} ${t.command} ENOENT`),{code:"ENOENT",errno:"ENOENT",syscall:`${e} ${t.command}`,path:t.command,spawnargs:t.args})}a(St,"notFoundError");function Mi(t,e){if(!vt)return;const u=t.emit;t.emit=function(s,n){if(s==="exit"){const r=us(n,e);if(r)return u.call(t,"error",r)}return u.apply(t,arguments)}}a(Mi,"hookChildProcess");function us(t,e){return vt&&t===1&&!e.file?St(e.original,"spawn"):null}a(us,"verifyENOENT");function Gi(t,e){return vt&&t===1&&!e.file?St(e.original,"spawnSync"):null}a(Gi,"verifyENOENTSync");var Wi={hookChildProcess:Mi,verifyENOENT:us,verifyENOENTSync:Gi,notFoundError:St};const ss=On,Bt=ki,$t=Wi;function ns(t,e,u){const s=Bt(t,e,u),n=ss.spawn(s.command,s.args,s.options);return $t.hookChildProcess(n,s),n}a(ns,"spawn");function ji(t,e,u){const s=Bt(t,e,u),n=ss.spawnSync(s.command,s.args,s.options);return n.error=n.error||$t.verifyENOENTSync(n.status,s),n}a(ji,"spawnSync"),fe.exports=ns,fe.exports.spawn=ns,fe.exports.sync=ji,fe.exports._parse=Bt,fe.exports._enoent=$t;var Ui=fe.exports,Ki=li(Ui);const rs=a((t,e)=>{const u={...process.env},s=["inherit","inherit","inherit"];process.send&&s.push("ipc"),e&&(e.noCache&&(u.TSX_DISABLE_CACHE="1"),e.tsconfigPath&&(u.TSX_TSCONFIG_PATH=e.tsconfigPath));const n=t.filter(r=>r!=="-i"&&r!=="--interactive").length===0;return Ki(process.execPath,["--require",Ie.resolve("./preflight.cjs"),...n?["--require",Ie.resolve("./patch-repl.cjs")]:[],mu(Nn)?"--import":"--loader",Tn(Ie.resolve("./loader.mjs")).toString(),...t],{stdio:s,env:u})},"run");var Ue={};const Vi=z,te="\\\\/",is=`[^${te}]`,ue="\\.",zi="\\+",Yi="\\?",Ke="\\/",qi="(?=.)",Ds="[^/]",Tt=`(?:${Ke}|$)`,os=`(?:^|${Ke})`,xt=`${ue}{1,2}${Tt}`,Xi=`(?!${ue})`,Qi=`(?!${os}${xt})`,Zi=`(?!${ue}{0,1}${Tt})`,Ji=`(?!${xt})`,eD=`[^.${Ke}]`,tD=`${Ds}*?`,as={DOT_LITERAL:ue,PLUS_LITERAL:zi,QMARK_LITERAL:Yi,SLASH_LITERAL:Ke,ONE_CHAR:qi,QMARK:Ds,END_ANCHOR:Tt,DOTS_SLASH:xt,NO_DOT:Xi,NO_DOTS:Qi,NO_DOT_SLASH:Zi,NO_DOTS_SLASH:Ji,QMARK_NO_DOT:eD,STAR:tD,START_ANCHOR:os},uD={...as,SLASH_LITERAL:`[${te}]`,QMARK:is,STAR:`${is}*?`,DOTS_SLASH:`${ue}{1,2}(?:[${te}]|$)`,NO_DOT:`(?!${ue})`,NO_DOTS:`(?!(?:^|[${te}])${ue}{1,2}(?:[${te}]|$))`,NO_DOT_SLASH:`(?!${ue}{0,1}(?:[${te}]|$))`,NO_DOTS_SLASH:`(?!${ue}{1,2}(?:[${te}]|$))`,QMARK_NO_DOT:`[^.${te}]`,START_ANCHOR:`(?:^|[${te}])`,END_ANCHOR:`(?:[${te}]|$)`},sD={alnum:"a-zA-Z0-9",alpha:"a-zA-Z",ascii:"\\x00-\\x7F",blank:" \\t",cntrl:"\\x00-\\x1F\\x7F",digit:"0-9",graph:"\\x21-\\x7E",lower:"a-z",print:"\\x20-\\x7E ",punct:"\\-!\"#$%&'()\\*+,./:;<=>?@[\\]^_`{|}~",space:" \\t\\r\\n\\v\\f",upper:"A-Z",word:"A-Za-z0-9_",xdigit:"A-Fa-f0-9"};var Ve={MAX_LENGTH:1024*64,POSIX_REGEX_SOURCE:sD,REGEX_BACKSLASH:/\\(?![*+?^${}(|)[\]])/g,REGEX_NON_SPECIAL_CHARS:/^[^@![\].,$*+?^{}()|\\/]+/,REGEX_SPECIAL_CHARS:/[-*+?.^${}(|)[\]]/,REGEX_SPECIAL_CHARS_BACKREF:/(\\?)((\W)(\3*))/g,REGEX_SPECIAL_CHARS_GLOBAL:/([-*+?.^${}(|)[\]])/g,REGEX_REMOVE_BACKSLASH:/(?:\[.*?[^\\]\]|\\(?=.))/g,REPLACEMENTS:{"***":"*","**/**":"**","**/**/**":"**"},CHAR_0:48,CHAR_9:57,CHAR_UPPERCASE_A:65,CHAR_LOWERCASE_A:97,CHAR_UPPERCASE_Z:90,CHAR_LOWERCASE_Z:122,CHAR_LEFT_PARENTHESES:40,CHAR_RIGHT_PARENTHESES:41,CHAR_ASTERISK:42,CHAR_AMPERSAND:38,CHAR_AT:64,CHAR_BACKWARD_SLASH:92,CHAR_CARRIAGE_RETURN:13,CHAR_CIRCUMFLEX_ACCENT:94,CHAR_COLON:58,CHAR_COMMA:44,CHAR_DOT:46,CHAR_DOUBLE_QUOTE:34,CHAR_EQUAL:61,CHAR_EXCLAMATION_MARK:33,CHAR_FORM_FEED:12,CHAR_FORWARD_SLASH:47,CHAR_GRAVE_ACCENT:96,CHAR_HASH:35,CHAR_HYPHEN_MINUS:45,CHAR_LEFT_ANGLE_BRACKET:60,CHAR_LEFT_CURLY_BRACE:123,CHAR_LEFT_SQUARE_BRACKET:91,CHAR_LINE_FEED:10,CHAR_NO_BREAK_SPACE:160,CHAR_PERCENT:37,CHAR_PLUS:43,CHAR_QUESTION_MARK:63,CHAR_RIGHT_ANGLE_BRACKET:62,CHAR_RIGHT_CURLY_BRACE:125,CHAR_RIGHT_SQUARE_BRACKET:93,CHAR_SEMICOLON:59,CHAR_SINGLE_QUOTE:39,CHAR_SPACE:32,CHAR_TAB:9,CHAR_UNDERSCORE:95,CHAR_VERTICAL_LINE:124,CHAR_ZERO_WIDTH_NOBREAK_SPACE:65279,SEP:Vi.sep,extglobChars(t){return{"!":{type:"negate",open:"(?:(?!(?:",close:`))${t.STAR})`},"?":{type:"qmark",open:"(?:",close:")?"},"+":{type:"plus",open:"(?:",close:")+"},"*":{type:"star",open:"(?:",close:")*"},"@":{type:"at",open:"(?:",close:")"}}},globChars(t){return t===!0?uD:as}};(function(t){const e=z,u=process.platform==="win32",{REGEX_BACKSLASH:s,REGEX_REMOVE_BACKSLASH:n,REGEX_SPECIAL_CHARS:r,REGEX_SPECIAL_CHARS_GLOBAL:i}=Ve;t.isObject=D=>D!==null&&typeof D=="object"&&!Array.isArray(D),t.hasRegexChars=D=>r.test(D),t.isRegexChar=D=>D.length===1&&t.hasRegexChars(D),t.escapeRegex=D=>D.replace(i,"\\$1"),t.toPosixSlashes=D=>D.replace(s,"/"),t.removeBackslashes=D=>D.replace(n,o=>o==="\\"?"":o),t.supportsLookbehinds=()=>{const D=process.version.slice(1).split(".").map(Number);return D.length===3&&D[0]>=9||D[0]===8&&D[1]>=10},t.isWindows=D=>D&&typeof D.windows=="boolean"?D.windows:u===!0||e.sep==="\\",t.escapeLast=(D,o,c)=>{const f=D.lastIndexOf(o,c);return f===-1?D:D[f-1]==="\\"?t.escapeLast(D,o,f-1):`${D.slice(0,f)}\\${D.slice(f)}`},t.removePrefix=(D,o={})=>{let c=D;return c.startsWith("./")&&(c=c.slice(2),o.prefix="./"),c},t.wrapOutput=(D,o={},c={})=>{const f=c.contains?"":"^",h=c.contains?"":"$";let l=`${f}(?:${D})${h}`;return o.negated===!0&&(l=`(?:^(?!${l}).*$)`),l}})(Ue);const ls=Ue,{CHAR_ASTERISK:Ot,CHAR_AT:nD,CHAR_BACKWARD_SLASH:ye,CHAR_COMMA:rD,CHAR_DOT:Nt,CHAR_EXCLAMATION_MARK:Ht,CHAR_FORWARD_SLASH:cs,CHAR_LEFT_CURLY_BRACE:Pt,CHAR_LEFT_PARENTHESES:Lt,CHAR_LEFT_SQUARE_BRACKET:iD,CHAR_PLUS:DD,CHAR_QUESTION_MARK:fs,CHAR_RIGHT_CURLY_BRACE:oD,CHAR_RIGHT_PARENTHESES:hs,CHAR_RIGHT_SQUARE_BRACKET:aD}=Ve,ds=a(t=>t===cs||t===ye,"isPathSeparator"),Es=a(t=>{t.isPrefix!==!0&&(t.depth=t.isGlobstar?1/0:1)},"depth"),lD=a((t,e)=>{const u=e||{},s=t.length-1,n=u.parts===!0||u.scanToEnd===!0,r=[],i=[],D=[];let o=t,c=-1,f=0,h=0,l=!1,p=!1,C=!1,g=!1,y=!1,B=!1,H=!1,$=!1,Q=!1,G=!1,ne=0,W,A,v={value:"",depth:0,isGlob:!1};const M=a(()=>c>=s,"eos"),F=a(()=>o.charCodeAt(c+1),"peek"),O=a(()=>(W=A,o.charCodeAt(++c)),"advance");for(;c0&&(re=o.slice(0,f),o=o.slice(f),h-=f),T&&C===!0&&h>0?(T=o.slice(0,h),d=o.slice(h)):C===!0?(T="",d=o):T=o,T&&T!==""&&T!=="/"&&T!==o&&ds(T.charCodeAt(T.length-1))&&(T=T.slice(0,-1)),u.unescape===!0&&(d&&(d=ls.removeBackslashes(d)),T&&H===!0&&(T=ls.removeBackslashes(T)));const E={prefix:re,input:t,start:f,base:T,glob:d,isBrace:l,isBracket:p,isGlob:C,isExtglob:g,isGlobstar:y,negated:$,negatedExtglob:Q};if(u.tokens===!0&&(E.maxDepth=0,ds(A)||i.push(v),E.tokens=i),u.parts===!0||u.tokens===!0){let j;for(let b=0;b{if(typeof e.expandRange=="function")return e.expandRange(...t,e);t.sort();const u=`[${t.join("-")}]`;try{new RegExp(u)}catch{return t.map(n=>Y.escapeRegex(n)).join("..")}return u},"expandRange"),de=a((t,e)=>`Missing ${t}: "${e}" - use "\\\\${e}" to match literal characters`,"syntaxError"),It=a((t,e)=>{if(typeof t!="string")throw new TypeError("Expected a string");t=ps[t]||t;const u={...e},s=typeof u.maxLength=="number"?Math.min(Ye,u.maxLength):Ye;let n=t.length;if(n>s)throw new SyntaxError(`Input length: ${n}, exceeds maximum allowed length: ${s}`);const r={type:"bos",value:"",output:u.prepend||""},i=[r],D=u.capture?"":"?:",o=Y.isWindows(e),c=ze.globChars(o),f=ze.extglobChars(c),{DOT_LITERAL:h,PLUS_LITERAL:l,SLASH_LITERAL:p,ONE_CHAR:C,DOTS_SLASH:g,NO_DOT:y,NO_DOT_SLASH:B,NO_DOTS_SLASH:H,QMARK:$,QMARK_NO_DOT:Q,STAR:G,START_ANCHOR:ne}=c,W=a(_=>`(${D}(?:(?!${ne}${_.dot?g:h}).)*?)`,"globstar"),A=u.dot?"":y,v=u.dot?$:Q;let M=u.bash===!0?W(u):G;u.capture&&(M=`(${M})`),typeof u.noext=="boolean"&&(u.noextglob=u.noext);const F={input:t,index:-1,start:0,dot:u.dot===!0,consumed:"",output:"",prefix:"",backtrack:!1,negated:!1,brackets:0,braces:0,parens:0,quotes:0,globstar:!1,tokens:i};t=Y.removePrefix(t,F),n=t.length;const O=[],T=[],re=[];let d=r,E;const j=a(()=>F.index===n-1,"eos"),b=F.peek=(_=1)=>t[F.index+_],Z=F.advance=()=>t[++F.index]||"",J=a(()=>t.slice(F.index+1),"remaining"),V=a((_="",x=0)=>{F.consumed+=_,F.index+=x},"consume"),Ne=a(_=>{F.output+=_.output!=null?_.output:_.value,V(_.value)},"append"),yn=a(()=>{let _=1;for(;b()==="!"&&(b(2)!=="("||b(3)==="?");)Z(),F.start++,_++;return _%2===0?!1:(F.negated=!0,F.start++,!0)},"negate"),He=a(_=>{F[_]++,re.push(_)},"increment"),ie=a(_=>{F[_]--,re.pop()},"decrement"),R=a(_=>{if(d.type==="globstar"){const x=F.braces>0&&(_.type==="comma"||_.type==="brace"),m=_.extglob===!0||O.length&&(_.type==="pipe"||_.type==="paren");_.type!=="slash"&&_.type!=="paren"&&!x&&!m&&(F.output=F.output.slice(0,-d.output.length),d.type="star",d.value="*",d.output=M,F.output+=d.output)}if(O.length&&_.type!=="paren"&&(O[O.length-1].inner+=_.value),(_.value||_.output)&&Ne(_),d&&d.type==="text"&&_.type==="text"){d.value+=_.value,d.output=(d.output||"")+_.value;return}_.prev=d,i.push(_),d=_},"push"),Pe=a((_,x)=>{const m={...f[x],conditions:1,inner:""};m.prev=d,m.parens=F.parens,m.output=F.output;const w=(u.capture?"(":"")+m.open;He("parens"),R({type:_,value:x,output:F.output?"":C}),R({type:"paren",extglob:!0,value:Z(),output:w}),O.push(m)},"extglobOpen"),wn=a(_=>{let x=_.close+(u.capture?")":""),m;if(_.type==="negate"){let w=M;if(_.inner&&_.inner.length>1&&_.inner.includes("/")&&(w=W(u)),(w!==M||j()||/^\)+$/.test(J()))&&(x=_.close=`)$))${w}`),_.inner.includes("*")&&(m=J())&&/^\.[^\\/.]+$/.test(m)){const N=It(m,{...e,fastpaths:!1}).output;x=_.close=`)${N})${w})`}_.prev.type==="bos"&&(F.negatedExtglob=!0)}R({type:"paren",extglob:!0,value:E,output:x}),ie("parens")},"extglobClose");if(u.fastpaths!==!1&&!/(^[*!]|[/()[\]{}"])/.test(t)){let _=!1,x=t.replace(dD,(m,w,N,U,L,at)=>U==="\\"?(_=!0,m):U==="?"?w?w+U+(L?$.repeat(L.length):""):at===0?v+(L?$.repeat(L.length):""):$.repeat(N.length):U==="."?h.repeat(N.length):U==="*"?w?w+U+(L?M:""):M:w?m:`\\${m}`);return _===!0&&(u.unescape===!0?x=x.replace(/\\/g,""):x=x.replace(/\\+/g,m=>m.length%2===0?"\\\\":m?"\\":"")),x===t&&u.contains===!0?(F.output=t,F):(F.output=Y.wrapOutput(x,F,e),F)}for(;!j();){if(E=Z(),E==="\0")continue;if(E==="\\"){const m=b();if(m==="/"&&u.bash!==!0||m==="."||m===";")continue;if(!m){E+="\\",R({type:"text",value:E});continue}const w=/^\\+/.exec(J());let N=0;if(w&&w[0].length>2&&(N=w[0].length,F.index+=N,N%2!==0&&(E+="\\")),u.unescape===!0?E=Z():E+=Z(),F.brackets===0){R({type:"text",value:E});continue}}if(F.brackets>0&&(E!=="]"||d.value==="["||d.value==="[^")){if(u.posix!==!1&&E===":"){const m=d.value.slice(1);if(m.includes("[")&&(d.posix=!0,m.includes(":"))){const w=d.value.lastIndexOf("["),N=d.value.slice(0,w),U=d.value.slice(w+2),L=fD[U];if(L){d.value=N+L,F.backtrack=!0,Z(),!r.output&&i.indexOf(d)===1&&(r.output=C);continue}}}(E==="["&&b()!==":"||E==="-"&&b()==="]")&&(E=`\\${E}`),E==="]"&&(d.value==="["||d.value==="[^")&&(E=`\\${E}`),u.posix===!0&&E==="!"&&d.value==="["&&(E="^"),d.value+=E,Ne({value:E});continue}if(F.quotes===1&&E!=='"'){E=Y.escapeRegex(E),d.value+=E,Ne({value:E});continue}if(E==='"'){F.quotes=F.quotes===1?0:1,u.keepQuotes===!0&&R({type:"text",value:E});continue}if(E==="("){He("parens"),R({type:"paren",value:E});continue}if(E===")"){if(F.parens===0&&u.strictBrackets===!0)throw new SyntaxError(de("opening","("));const m=O[O.length-1];if(m&&F.parens===m.parens+1){wn(O.pop());continue}R({type:"paren",value:E,output:F.parens?")":"\\)"}),ie("parens");continue}if(E==="["){if(u.nobracket===!0||!J().includes("]")){if(u.nobracket!==!0&&u.strictBrackets===!0)throw new SyntaxError(de("closing","]"));E=`\\${E}`}else He("brackets");R({type:"bracket",value:E});continue}if(E==="]"){if(u.nobracket===!0||d&&d.type==="bracket"&&d.value.length===1){R({type:"text",value:E,output:`\\${E}`});continue}if(F.brackets===0){if(u.strictBrackets===!0)throw new SyntaxError(de("opening","["));R({type:"text",value:E,output:`\\${E}`});continue}ie("brackets");const m=d.value.slice(1);if(d.posix!==!0&&m[0]==="^"&&!m.includes("/")&&(E=`/${E}`),d.value+=E,Ne({value:E}),u.literalBrackets===!1||Y.hasRegexChars(m))continue;const w=Y.escapeRegex(d.value);if(F.output=F.output.slice(0,-d.value.length),u.literalBrackets===!0){F.output+=w,d.value=w;continue}d.value=`(${D}${w}|${d.value})`,F.output+=d.value;continue}if(E==="{"&&u.nobrace!==!0){He("braces");const m={type:"brace",value:E,output:"(",outputIndex:F.output.length,tokensIndex:F.tokens.length};T.push(m),R(m);continue}if(E==="}"){const m=T[T.length-1];if(u.nobrace===!0||!m){R({type:"text",value:E,output:E});continue}let w=")";if(m.dots===!0){const N=i.slice(),U=[];for(let L=N.length-1;L>=0&&(i.pop(),N[L].type!=="brace");L--)N[L].type!=="dots"&&U.unshift(N[L].value);w=ED(U,u),F.backtrack=!0}if(m.comma!==!0&&m.dots!==!0){const N=F.output.slice(0,m.outputIndex),U=F.tokens.slice(m.tokensIndex);m.value=m.output="\\{",E=w="\\}",F.output=N;for(const L of U)F.output+=L.output||L.value}R({type:"brace",value:E,output:w}),ie("braces"),T.pop();continue}if(E==="|"){O.length>0&&O[O.length-1].conditions++,R({type:"text",value:E});continue}if(E===","){let m=E;const w=T[T.length-1];w&&re[re.length-1]==="braces"&&(w.comma=!0,m="|"),R({type:"comma",value:E,output:m});continue}if(E==="/"){if(d.type==="dot"&&F.index===F.start+1){F.start=F.index+1,F.consumed="",F.output="",i.pop(),d=r;continue}R({type:"slash",value:E,output:p});continue}if(E==="."){if(F.braces>0&&d.type==="dot"){d.value==="."&&(d.output=h);const m=T[T.length-1];d.type="dots",d.output+=E,d.value+=E,m.dots=!0;continue}if(F.braces+F.parens===0&&d.type!=="bos"&&d.type!=="slash"){R({type:"text",value:E,output:h});continue}R({type:"dot",value:E,output:h});continue}if(E==="?"){if(!(d&&d.value==="(")&&u.noextglob!==!0&&b()==="("&&b(2)!=="?"){Pe("qmark",E);continue}if(d&&d.type==="paren"){const w=b();let N=E;if(w==="<"&&!Y.supportsLookbehinds())throw new Error("Node.js v10 or higher is required for regex lookbehinds");(d.value==="("&&!/[!=<:]/.test(w)||w==="<"&&!/<([!=]|\w+>)/.test(J()))&&(N=`\\${E}`),R({type:"text",value:E,output:N});continue}if(u.dot!==!0&&(d.type==="slash"||d.type==="bos")){R({type:"qmark",value:E,output:Q});continue}R({type:"qmark",value:E,output:$});continue}if(E==="!"){if(u.noextglob!==!0&&b()==="("&&(b(2)!=="?"||!/[!=<:]/.test(b(3)))){Pe("negate",E);continue}if(u.nonegate!==!0&&F.index===0){yn();continue}}if(E==="+"){if(u.noextglob!==!0&&b()==="("&&b(2)!=="?"){Pe("plus",E);continue}if(d&&d.value==="("||u.regex===!1){R({type:"plus",value:E,output:l});continue}if(d&&(d.type==="bracket"||d.type==="paren"||d.type==="brace")||F.parens>0){R({type:"plus",value:E});continue}R({type:"plus",value:l});continue}if(E==="@"){if(u.noextglob!==!0&&b()==="("&&b(2)!=="?"){R({type:"at",extglob:!0,value:E,output:""});continue}R({type:"text",value:E});continue}if(E!=="*"){(E==="$"||E==="^")&&(E=`\\${E}`);const m=hD.exec(J());m&&(E+=m[0],F.index+=m[0].length),R({type:"text",value:E});continue}if(d&&(d.type==="globstar"||d.star===!0)){d.type="star",d.star=!0,d.value+=E,d.output=M,F.backtrack=!0,F.globstar=!0,V(E);continue}let _=J();if(u.noextglob!==!0&&/^\([^?]/.test(_)){Pe("star",E);continue}if(d.type==="star"){if(u.noglobstar===!0){V(E);continue}const m=d.prev,w=m.prev,N=m.type==="slash"||m.type==="bos",U=w&&(w.type==="star"||w.type==="globstar");if(u.bash===!0&&(!N||_[0]&&_[0]!=="/")){R({type:"star",value:E,output:""});continue}const L=F.braces>0&&(m.type==="comma"||m.type==="brace"),at=O.length&&(m.type==="pipe"||m.type==="paren");if(!N&&m.type!=="paren"&&!L&&!at){R({type:"star",value:E,output:""});continue}for(;_.slice(0,3)==="/**";){const Le=t[F.index+4];if(Le&&Le!=="/")break;_=_.slice(3),V("/**",3)}if(m.type==="bos"&&j()){d.type="globstar",d.value+=E,d.output=W(u),F.output=d.output,F.globstar=!0,V(E);continue}if(m.type==="slash"&&m.prev.type!=="bos"&&!U&&j()){F.output=F.output.slice(0,-(m.output+d.output).length),m.output=`(?:${m.output}`,d.type="globstar",d.output=W(u)+(u.strictSlashes?")":"|$)"),d.value+=E,F.globstar=!0,F.output+=m.output+d.output,V(E);continue}if(m.type==="slash"&&m.prev.type!=="bos"&&_[0]==="/"){const Le=_[1]!==void 0?"|$":"";F.output=F.output.slice(0,-(m.output+d.output).length),m.output=`(?:${m.output}`,d.type="globstar",d.output=`${W(u)}${p}|${p}${Le})`,d.value+=E,F.output+=m.output+d.output,F.globstar=!0,V(E+Z()),R({type:"slash",value:"/",output:""});continue}if(m.type==="bos"&&_[0]==="/"){d.type="globstar",d.value+=E,d.output=`(?:^|${p}|${W(u)}${p})`,F.output=d.output,F.globstar=!0,V(E+Z()),R({type:"slash",value:"/",output:""});continue}F.output=F.output.slice(0,-d.output.length),d.type="globstar",d.output=W(u),d.value+=E,F.output+=d.output,F.globstar=!0,V(E);continue}const x={type:"star",value:E,output:M};if(u.bash===!0){x.output=".*?",(d.type==="bos"||d.type==="slash")&&(x.output=A+x.output),R(x);continue}if(d&&(d.type==="bracket"||d.type==="paren")&&u.regex===!0){x.output=E,R(x);continue}(F.index===F.start||d.type==="slash"||d.type==="dot")&&(d.type==="dot"?(F.output+=B,d.output+=B):u.dot===!0?(F.output+=H,d.output+=H):(F.output+=A,d.output+=A),b()!=="*"&&(F.output+=C,d.output+=C)),R(x)}for(;F.brackets>0;){if(u.strictBrackets===!0)throw new SyntaxError(de("closing","]"));F.output=Y.escapeLast(F.output,"["),ie("brackets")}for(;F.parens>0;){if(u.strictBrackets===!0)throw new SyntaxError(de("closing",")"));F.output=Y.escapeLast(F.output,"("),ie("parens")}for(;F.braces>0;){if(u.strictBrackets===!0)throw new SyntaxError(de("closing","}"));F.output=Y.escapeLast(F.output,"{"),ie("braces")}if(u.strictSlashes!==!0&&(d.type==="star"||d.type==="bracket")&&R({type:"maybe_slash",value:"",output:`${p}?`}),F.backtrack===!0){F.output="";for(const _ of F.tokens)F.output+=_.output!=null?_.output:_.value,_.suffix&&(F.output+=_.suffix)}return F},"parse$3");It.fastpaths=(t,e)=>{const u={...e},s=typeof u.maxLength=="number"?Math.min(Ye,u.maxLength):Ye,n=t.length;if(n>s)throw new SyntaxError(`Input length: ${n}, exceeds maximum allowed length: ${s}`);t=ps[t]||t;const r=Y.isWindows(e),{DOT_LITERAL:i,SLASH_LITERAL:D,ONE_CHAR:o,DOTS_SLASH:c,NO_DOT:f,NO_DOTS:h,NO_DOTS_SLASH:l,STAR:p,START_ANCHOR:C}=ze.globChars(r),g=u.dot?h:f,y=u.dot?l:f,B=u.capture?"":"?:",H={negated:!1,prefix:""};let $=u.bash===!0?".*?":p;u.capture&&($=`(${$})`);const Q=a(A=>A.noglobstar===!0?$:`(${B}(?:(?!${C}${A.dot?c:i}).)*?)`,"globstar"),G=a(A=>{switch(A){case"*":return`${g}${o}${$}`;case".*":return`${i}${o}${$}`;case"*.*":return`${g}${$}${i}${o}${$}`;case"*/*":return`${g}${$}${D}${o}${y}${$}`;case"**":return g+Q(u);case"**/*":return`(?:${g}${Q(u)}${D})?${y}${o}${$}`;case"**/*.*":return`(?:${g}${Q(u)}${D})?${y}${$}${i}${o}${$}`;case"**/.*":return`(?:${g}${Q(u)}${D})?${i}${o}${$}`;default:{const v=/^(.*?)\.(\w+)$/.exec(A);if(!v)return;const M=G(v[1]);return M?M+i+v[2]:void 0}}},"create"),ne=Y.removePrefix(t,H);let W=G(ne);return W&&u.strictSlashes!==!0&&(W+=`${D}?`),W};var pD=It;const CD=z,FD=cD,kt=pD,Mt=Ue,gD=Ve,mD=a(t=>t&&typeof t=="object"&&!Array.isArray(t),"isObject$1"),P=a((t,e,u=!1)=>{if(Array.isArray(t)){const f=t.map(l=>P(l,e,u));return a(l=>{for(const p of f){const C=p(l);if(C)return C}return!1},"arrayMatcher")}const s=mD(t)&&t.tokens&&t.input;if(t===""||typeof t!="string"&&!s)throw new TypeError("Expected pattern to be a non-empty string");const n=e||{},r=Mt.isWindows(e),i=s?P.compileRe(t,e):P.makeRe(t,e,!1,!0),D=i.state;delete i.state;let o=a(()=>!1,"isIgnored");if(n.ignore){const f={...e,ignore:null,onMatch:null,onResult:null};o=P(n.ignore,f,u)}const c=a((f,h=!1)=>{const{isMatch:l,match:p,output:C}=P.test(f,i,e,{glob:t,posix:r}),g={glob:t,state:D,regex:i,posix:r,input:f,output:C,match:p,isMatch:l};return typeof n.onResult=="function"&&n.onResult(g),l===!1?(g.isMatch=!1,h?g:!1):o(f)?(typeof n.onIgnore=="function"&&n.onIgnore(g),g.isMatch=!1,h?g:!1):(typeof n.onMatch=="function"&&n.onMatch(g),h?g:!0)},"matcher");return u&&(c.state=D),c},"picomatch$3");P.test=(t,e,u,{glob:s,posix:n}={})=>{if(typeof t!="string")throw new TypeError("Expected input to be a string");if(t==="")return{isMatch:!1,output:""};const r=u||{},i=r.format||(n?Mt.toPosixSlashes:null);let D=t===s,o=D&&i?i(t):t;return D===!1&&(o=i?i(t):t,D=o===s),(D===!1||r.capture===!0)&&(r.matchBase===!0||r.basename===!0?D=P.matchBase(t,e,u,n):D=e.exec(o)),{isMatch:!!D,match:D,output:o}},P.matchBase=(t,e,u,s=Mt.isWindows(u))=>(e instanceof RegExp?e:P.makeRe(e,u)).test(CD.basename(t)),P.isMatch=(t,e,u)=>P(e,u)(t),P.parse=(t,e)=>Array.isArray(t)?t.map(u=>P.parse(u,e)):kt(t,{...e,fastpaths:!1}),P.scan=(t,e)=>FD(t,e),P.compileRe=(t,e,u=!1,s=!1)=>{if(u===!0)return t.output;const n=e||{},r=n.contains?"":"^",i=n.contains?"":"$";let D=`${r}(?:${t.output})${i}`;t&&t.negated===!0&&(D=`^(?!${D}).*$`);const o=P.toRegex(D,e);return s===!0&&(o.state=t),o},P.makeRe=(t,e={},u=!1,s=!1)=>{if(!t||typeof t!="string")throw new TypeError("Expected a non-empty string");let n={negated:!1,fastpaths:!0};return e.fastpaths!==!1&&(t[0]==="."||t[0]==="*")&&(n.output=kt.fastpaths(t,e)),n.output||(n=kt(t,e)),P.compileRe(n,e,u,s)},P.toRegex=(t,e)=>{try{const u=e||{};return new RegExp(t,u.flags||(u.nocase?"i":""))}catch(u){if(e&&e.debug===!0)throw u;return/$^/}},P.constants=gD;var _D=P,Cs=_D;const we=De,{Readable:AD}=In,Re=z,{promisify:qe}=ge,Gt=Cs,yD=qe(we.readdir),wD=qe(we.stat),Fs=qe(we.lstat),RD=qe(we.realpath),bD="!",gs="READDIRP_RECURSIVE_ERROR",vD=new Set(["ENOENT","EPERM","EACCES","ELOOP",gs]),Wt="files",ms="directories",Xe="files_directories",Qe="all",_s=[Wt,ms,Xe,Qe],SD=a(t=>vD.has(t.code),"isNormalFlowError"),[As,BD]=process.versions.node.split(".").slice(0,2).map(t=>Number.parseInt(t,10)),$D=process.platform==="win32"&&(As>10||As===10&&BD>=5),ys=a(t=>{if(t!==void 0){if(typeof t=="function")return t;if(typeof t=="string"){const e=Gt(t.trim());return u=>e(u.basename)}if(Array.isArray(t)){const e=[],u=[];for(const s of t){const n=s.trim();n.charAt(0)===bD?u.push(Gt(n.slice(1))):e.push(Gt(n))}return u.length>0?e.length>0?s=>e.some(n=>n(s.basename))&&!u.some(n=>n(s.basename)):s=>!u.some(n=>n(s.basename)):s=>e.some(n=>n(s.basename))}}},"normalizeFilter");class ot extends AD{static{a(this,"ReaddirpStream")}static get defaultOptions(){return{root:".",fileFilter:a(e=>!0,"fileFilter"),directoryFilter:a(e=>!0,"directoryFilter"),type:Wt,lstat:!1,depth:2147483648,alwaysStat:!1}}constructor(e={}){super({objectMode:!0,autoDestroy:!0,highWaterMark:e.highWaterMark||4096});const u={...ot.defaultOptions,...e},{root:s,type:n}=u;this._fileFilter=ys(u.fileFilter),this._directoryFilter=ys(u.directoryFilter);const r=u.lstat?Fs:wD;$D?this._stat=i=>r(i,{bigint:!0}):this._stat=r,this._maxDepth=u.depth,this._wantsDir=[ms,Xe,Qe].includes(n),this._wantsFile=[Wt,Xe,Qe].includes(n),this._wantsEverything=n===Qe,this._root=Re.resolve(s),this._isDirent="Dirent"in we&&!u.alwaysStat,this._statsProp=this._isDirent?"dirent":"stats",this._rdOptions={encoding:"utf8",withFileTypes:this._isDirent},this.parents=[this._exploreDir(s,1)],this.reading=!1,this.parent=void 0}async _read(e){if(!this.reading){this.reading=!0;try{for(;!this.destroyed&&e>0;){const{path:u,depth:s,files:n=[]}=this.parent||{};if(n.length>0){const r=n.splice(0,e).map(i=>this._formatEntry(i,u));for(const i of await Promise.all(r)){if(this.destroyed)return;const D=await this._getEntryType(i);D==="directory"&&this._directoryFilter(i)?(s<=this._maxDepth&&this.parents.push(this._exploreDir(i.fullPath,s+1)),this._wantsDir&&(this.push(i),e--)):(D==="file"||this._includeAsFile(i))&&this._fileFilter(i)&&this._wantsFile&&(this.push(i),e--)}}else{const r=this.parents.pop();if(!r){this.push(null);break}if(this.parent=await r,this.destroyed)return}}}catch(u){this.destroy(u)}finally{this.reading=!1}}}async _exploreDir(e,u){let s;try{s=await yD(e,this._rdOptions)}catch(n){this._onError(n)}return{files:s,depth:u,path:e}}async _formatEntry(e,u){let s;try{const n=this._isDirent?e.name:e,r=Re.resolve(Re.join(u,n));s={path:Re.relative(this._root,r),fullPath:r,basename:n},s[this._statsProp]=this._isDirent?e:await this._stat(r)}catch(n){this._onError(n)}return s}_onError(e){SD(e)&&!this.destroyed?this.emit("warn",e):this.destroy(e)}async _getEntryType(e){const u=e&&e[this._statsProp];if(u){if(u.isFile())return"file";if(u.isDirectory())return"directory";if(u&&u.isSymbolicLink()){const s=e.fullPath;try{const n=await RD(s),r=await Fs(n);if(r.isFile())return"file";if(r.isDirectory()){const i=n.length;if(s.startsWith(n)&&s.substr(i,1)===Re.sep){const D=new Error(`Circular symlink detected: "${s}" points to "${n}"`);return D.code=gs,this._onError(D)}return"directory"}}catch(n){this._onError(n)}}}}_includeAsFile(e){const u=e&&e[this._statsProp];return u&&this._wantsEverything&&!u.isDirectory()}}const Ee=a((t,e={})=>{let u=e.entryType||e.type;if(u==="both"&&(u=Xe),u&&(e.type=u),t){if(typeof t!="string")throw new TypeError("readdirp: root argument must be a string. Usage: readdirp(root, options)");if(u&&!_s.includes(u))throw new Error(`readdirp: Invalid type passed. Use one of ${_s.join(", ")}`)}else throw new Error("readdirp: root argument is required. Usage: readdirp(root, options)");return e.root=t,new ot(e)},"readdirp$1"),TD=a((t,e={})=>new Promise((u,s)=>{const n=[];Ee(t,e).on("data",r=>n.push(r)).on("end",()=>u(n)).on("error",r=>s(r))}),"readdirpPromise");Ee.promise=TD,Ee.ReaddirpStream=ot,Ee.default=Ee;var xD=Ee,jt={exports:{}};/*! + * normalize-path + * + * Copyright (c) 2014-2018, Jon Schlinkert. + * Released under the MIT License. + */var ws=a(function(t,e){if(typeof t!="string")throw new TypeError("expected path to be a string");if(t==="\\"||t==="/")return"/";var u=t.length;if(u<=1)return t;var s="";if(u>4&&t[3]==="\\"){var n=t[2];(n==="?"||n===".")&&t.slice(0,2)==="\\\\"&&(t=t.slice(2),s="//")}var r=t.split(/[/\\]+/);return e!==!1&&r[r.length-1]===""&&r.pop(),s+r.join("/")},"normalizePath$2"),OD=jt.exports;Object.defineProperty(OD,"__esModule",{value:!0});const Rs=Cs,ND=ws,bs="!",HD={returnIndex:!1},PD=a(t=>Array.isArray(t)?t:[t],"arrify$1"),LD=a((t,e)=>{if(typeof t=="function")return t;if(typeof t=="string"){const u=Rs(t,e);return s=>t===s||u(s)}return t instanceof RegExp?u=>t.test(u):u=>!1},"createPattern"),vs=a((t,e,u,s)=>{const n=Array.isArray(u),r=n?u[0]:u;if(!n&&typeof r!="string")throw new TypeError("anymatch: second argument must be a string: got "+Object.prototype.toString.call(r));const i=ND(r,!1);for(let o=0;o{if(t==null)throw new TypeError("anymatch: specify first argument");const s=typeof u=="boolean"?{returnIndex:u}:u,n=s.returnIndex||!1,r=PD(t),i=r.filter(o=>typeof o=="string"&&o.charAt(0)===bs).map(o=>o.slice(1)).map(o=>Rs(o,s)),D=r.filter(o=>typeof o!="string"||typeof o=="string"&&o.charAt(0)!==bs).map(o=>LD(o,s));return e==null?(o,c=!1)=>vs(D,i,o,typeof c=="boolean"?c:!1):vs(D,i,e,n)},"anymatch$1");Ut.default=Ut,jt.exports=Ut;var ID=jt.exports;/*! + * is-extglob + * + * Copyright (c) 2014-2016, Jon Schlinkert. + * Licensed under the MIT License. + */var kD=a(function(e){if(typeof e!="string"||e==="")return!1;for(var u;u=/(\\).|([@?!+*]\(.*\))/g.exec(e);){if(u[2])return!0;e=e.slice(u.index+u[0].length)}return!1},"isExtglob");/*! + * is-glob + * + * Copyright (c) 2014-2017, Jon Schlinkert. + * Released under the MIT License. + */var MD=kD,Ss={"{":"}","(":")","[":"]"},GD=a(function(t){if(t[0]==="!")return!0;for(var e=0,u=-2,s=-2,n=-2,r=-2,i=-2;ee&&(i===-1||i>s||(i=t.indexOf("\\",e),i===-1||i>s)))||n!==-1&&t[e]==="{"&&t[e+1]!=="}"&&(n=t.indexOf("}",e),n>e&&(i=t.indexOf("\\",e),i===-1||i>n))||r!==-1&&t[e]==="("&&t[e+1]==="?"&&/[:!=]/.test(t[e+2])&&t[e+3]!==")"&&(r=t.indexOf(")",e),r>e&&(i=t.indexOf("\\",e),i===-1||i>r))||u!==-1&&t[e]==="("&&t[e+1]!=="|"&&(uu&&(i=t.indexOf("\\",u),i===-1||i>r))))return!0;if(t[e]==="\\"){var D=t[e+1];e+=2;var o=Ss[D];if(o){var c=t.indexOf(o,e);c!==-1&&(e=c+1)}if(t[e]==="!")return!0}else e++}return!1},"strictCheck"),WD=a(function(t){if(t[0]==="!")return!0;for(var e=0;etypeof e=="number"?Number.isInteger(e):typeof e=="string"&&e.trim()!==""?Number.isInteger(Number(e)):!1,t.find=(e,u)=>e.nodes.find(s=>s.type===u),t.exceedsLimit=(e,u,s=1,n)=>n===!1||!t.isInteger(e)||!t.isInteger(u)?!1:(Number(u)-Number(e))/Number(s)>=n,t.escapeNode=(e,u=0,s)=>{let n=e.nodes[u];n&&(s&&n.type===s||n.type==="open"||n.type==="close")&&n.escaped!==!0&&(n.value="\\"+n.value,n.escaped=!0)},t.encloseBrace=e=>e.type!=="brace"||e.commas>>0+e.ranges>>0?!1:(e.invalid=!0,!0),t.isInvalidBrace=e=>e.type!=="brace"?!1:e.invalid===!0||e.dollar?!0:!(e.commas>>0+e.ranges>>0)||e.open!==!0||e.close!==!0?(e.invalid=!0,!0):!1,t.isOpenOrClose=e=>e.type==="open"||e.type==="close"?!0:e.open===!0||e.close===!0,t.reduce=e=>e.reduce((u,s)=>(s.type==="text"&&u.push(s.value),s.type==="range"&&(s.type="text"),u),[]),t.flatten=(...e)=>{const u=[],s=a(n=>{for(let r=0;r{let u=a((s,n={})=>{let r=e.escapeInvalid&&$s.isInvalidBrace(n),i=s.invalid===!0&&e.escapeInvalid===!0,D="";if(s.value)return(r||i)&&$s.isOpenOrClose(s)?"\\"+s.value:s.value;if(s.value)return s.value;if(s.nodes)for(let o of s.nodes)D+=u(o);return D},"stringify");return u(t)},"stringify$4");/*! + * is-number + * + * Copyright (c) 2014-present, Jon Schlinkert. + * Released under the MIT License. + */var QD=a(function(t){return typeof t=="number"?t-t===0:typeof t=="string"&&t.trim()!==""?Number.isFinite?Number.isFinite(+t):isFinite(+t):!1},"isNumber$2");/*! + * to-regex-range + * + * Copyright (c) 2015-present, Jon Schlinkert. + * Released under the MIT License. + */const Ts=QD,ae=a((t,e,u)=>{if(Ts(t)===!1)throw new TypeError("toRegexRange: expected the first argument to be a number");if(e===void 0||t===e)return String(t);if(Ts(e)===!1)throw new TypeError("toRegexRange: expected the second argument to be a number.");let s={relaxZeros:!0,...u};typeof s.strictZeros=="boolean"&&(s.relaxZeros=s.strictZeros===!1);let n=String(s.relaxZeros),r=String(s.shorthand),i=String(s.capture),D=String(s.wrap),o=t+":"+e+"="+n+r+i+D;if(ae.cache.hasOwnProperty(o))return ae.cache[o].result;let c=Math.min(t,e),f=Math.max(t,e);if(Math.abs(c-f)===1){let g=t+"|"+e;return s.capture?`(${g})`:s.wrap===!1?g:`(?:${g})`}let h=Ls(t)||Ls(e),l={min:t,max:e,a:c,b:f},p=[],C=[];if(h&&(l.isPadded=h,l.maxLen=String(l.max).length),c<0){let g=f<0?Math.abs(f):1;C=xs(g,Math.abs(c),l,s),c=l.a=0}return f>=0&&(p=xs(c,f,l,s)),l.negatives=C,l.positives=p,l.result=ZD(C,p),s.capture===!0?l.result=`(${l.result})`:s.wrap!==!1&&p.length+C.length>1&&(l.result=`(?:${l.result})`),ae.cache[o]=l,l.result},"toRegexRange$1");function ZD(t,e,u){let s=zt(t,e,"-",!1)||[],n=zt(e,t,"",!1)||[],r=zt(t,e,"-?",!0)||[];return s.concat(r).concat(n).join("|")}a(ZD,"collatePatterns");function JD(t,e){let u=1,s=1,n=Ns(t,u),r=new Set([e]);for(;t<=n&&n<=e;)r.add(n),u+=1,n=Ns(t,u);for(n=Hs(e+1,s)-1;t1&&D.count.pop(),D.count.push(f.count[0]),D.string=D.pattern+Ps(D.count),i=c+1;continue}u.isPadded&&(h=no(c,u,s)),f.string=h+f.pattern+Ps(f.count),r.push(f),i=c+1,D=f}return r}a(xs,"splitToPatterns");function zt(t,e,u,s,n){let r=[];for(let i of t){let{string:D}=i;!s&&!Os(e,"string",D)&&r.push(u+D),s&&Os(e,"string",D)&&r.push(u+D)}return r}a(zt,"filterPatterns");function to(t,e){let u=[];for(let s=0;se?1:e>t?-1:0}a(uo,"compare");function Os(t,e,u){return t.some(s=>s[e]===u)}a(Os,"contains");function Ns(t,e){return Number(String(t).slice(0,-e)+"9".repeat(e))}a(Ns,"countNines");function Hs(t,e){return t-t%Math.pow(10,e)}a(Hs,"countZeros");function Ps(t){let[e=0,u=""]=t;return u||e>1?`{${e+(u?","+u:"")}}`:""}a(Ps,"toQuantifier");function so(t,e,u){return`[${t}${e-t===1?"":"-"}${e}]`}a(so,"toCharacterClass");function Ls(t){return/^-?(0+)\d/.test(t)}a(Ls,"hasPadding");function no(t,e,u){if(!e.isPadded)return t;let s=Math.abs(e.maxLen-String(t).length),n=u.relaxZeros!==!1;switch(s){case 0:return"";case 1:return n?"0?":"0";case 2:return n?"0{0,2}":"00";default:return n?`0{0,${s}}`:`0{${s}}`}}a(no,"padZeros"),ae.cache={},ae.clearCache=()=>ae.cache={};var ro=ae;/*! + * fill-range + * + * Copyright (c) 2014-present, Jon Schlinkert. + * Licensed under the MIT License. + */const io=ge,Is=ro,ks=a(t=>t!==null&&typeof t=="object"&&!Array.isArray(t),"isObject"),Do=a(t=>e=>t===!0?Number(e):String(e),"transform"),Yt=a(t=>typeof t=="number"||typeof t=="string"&&t!=="","isValidValue"),be=a(t=>Number.isInteger(+t),"isNumber"),qt=a(t=>{let e=`${t}`,u=-1;if(e[0]==="-"&&(e=e.slice(1)),e==="0")return!1;for(;e[++u]==="0";);return u>0},"zeros"),oo=a((t,e,u)=>typeof t=="string"||typeof e=="string"?!0:u.stringify===!0,"stringify$3"),ao=a((t,e,u)=>{if(e>0){let s=t[0]==="-"?"-":"";s&&(t=t.slice(1)),t=s+t.padStart(s?e-1:e,"0")}return u===!1?String(t):t},"pad"),Ms=a((t,e)=>{let u=t[0]==="-"?"-":"";for(u&&(t=t.slice(1),e--);t.length{t.negatives.sort((i,D)=>iD?1:0),t.positives.sort((i,D)=>iD?1:0);let u=e.capture?"":"?:",s="",n="",r;return t.positives.length&&(s=t.positives.join("|")),t.negatives.length&&(n=`-(${u}${t.negatives.join("|")})`),s&&n?r=`${s}|${n}`:r=s||n,e.wrap?`(${u}${r})`:r},"toSequence"),Gs=a((t,e,u,s)=>{if(u)return Is(t,e,{wrap:!1,...s});let n=String.fromCharCode(t);if(t===e)return n;let r=String.fromCharCode(e);return`[${n}-${r}]`},"toRange"),Ws=a((t,e,u)=>{if(Array.isArray(t)){let s=u.wrap===!0,n=u.capture?"":"?:";return s?`(${n}${t.join("|")})`:t.join("|")}return Is(t,e,u)},"toRegex"),js=a((...t)=>new RangeError("Invalid range arguments: "+io.inspect(...t)),"rangeError"),Us=a((t,e,u)=>{if(u.strictRanges===!0)throw js([t,e]);return[]},"invalidRange"),co=a((t,e)=>{if(e.strictRanges===!0)throw new TypeError(`Expected step "${t}" to be a number`);return[]},"invalidStep"),fo=a((t,e,u=1,s={})=>{let n=Number(t),r=Number(e);if(!Number.isInteger(n)||!Number.isInteger(r)){if(s.strictRanges===!0)throw js([t,e]);return[]}n===0&&(n=0),r===0&&(r=0);let i=n>r,D=String(t),o=String(e),c=String(u);u=Math.max(Math.abs(u),1);let f=qt(D)||qt(o)||qt(c),h=f?Math.max(D.length,o.length,c.length):0,l=f===!1&&oo(t,e,s)===!1,p=s.transform||Do(l);if(s.toRegex&&u===1)return Gs(Ms(t,h),Ms(e,h),!0,s);let C={negatives:[],positives:[]},g=a(H=>C[H<0?"negatives":"positives"].push(Math.abs(H)),"push"),y=[],B=0;for(;i?n>=r:n<=r;)s.toRegex===!0&&u>1?g(n):y.push(ao(p(n,B),h,l)),n=i?n-u:n+u,B++;return s.toRegex===!0?u>1?lo(C,s):Ws(y,null,{wrap:!1,...s}):y},"fillNumbers"),ho=a((t,e,u=1,s={})=>{if(!be(t)&&t.length>1||!be(e)&&e.length>1)return Us(t,e,s);let n=s.transform||(l=>String.fromCharCode(l)),r=`${t}`.charCodeAt(0),i=`${e}`.charCodeAt(0),D=r>i,o=Math.min(r,i),c=Math.max(r,i);if(s.toRegex&&u===1)return Gs(o,c,!1,s);let f=[],h=0;for(;D?r>=i:r<=i;)f.push(n(r,h)),r=D?r-u:r+u,h++;return s.toRegex===!0?Ws(f,null,{wrap:!1,options:s}):f},"fillLetters"),Je=a((t,e,u,s={})=>{if(e==null&&Yt(t))return[t];if(!Yt(t)||!Yt(e))return Us(t,e,s);if(typeof u=="function")return Je(t,e,1,{transform:u});if(ks(u))return Je(t,e,0,u);let n={...s};return n.capture===!0&&(n.wrap=!0),u=u||n.step||1,be(u)?be(t)&&be(e)?fo(t,e,u,n):ho(t,e,Math.max(Math.abs(u),1),n):u!=null&&!ks(u)?co(u,n):Je(t,e,1,u)},"fill$2");var Ks=Je;const Eo=Ks,Vs=Ze,po=a((t,e={})=>{let u=a((s,n={})=>{let r=Vs.isInvalidBrace(n),i=s.invalid===!0&&e.escapeInvalid===!0,D=r===!0||i===!0,o=e.escapeInvalid===!0?"\\":"",c="";if(s.isOpen===!0||s.isClose===!0)return o+s.value;if(s.type==="open")return D?o+s.value:"(";if(s.type==="close")return D?o+s.value:")";if(s.type==="comma")return s.prev.type==="comma"?"":D?s.value:"|";if(s.value)return s.value;if(s.nodes&&s.ranges>0){let f=Vs.reduce(s.nodes),h=Eo(...f,{...e,wrap:!1,toRegex:!0});if(h.length!==0)return f.length>1&&h.length>1?`(${h})`:h}if(s.nodes)for(let f of s.nodes)c+=u(f,s);return c},"walk");return u(t)},"compile$1");var Co=po;const Fo=Ks,zs=Vt,pe=Ze,le=a((t="",e="",u=!1)=>{let s=[];if(t=[].concat(t),e=[].concat(e),!e.length)return t;if(!t.length)return u?pe.flatten(e).map(n=>`{${n}}`):e;for(let n of t)if(Array.isArray(n))for(let r of n)s.push(le(r,e,u));else for(let r of e)u===!0&&typeof r=="string"&&(r=`{${r}}`),s.push(Array.isArray(r)?le(n,r,u):n+r);return pe.flatten(s)},"append"),go=a((t,e={})=>{let u=e.rangeLimit===void 0?1e3:e.rangeLimit,s=a((n,r={})=>{n.queue=[];let i=r,D=r.queue;for(;i.type!=="brace"&&i.type!=="root"&&i.parent;)i=i.parent,D=i.queue;if(n.invalid||n.dollar){D.push(le(D.pop(),zs(n,e)));return}if(n.type==="brace"&&n.invalid!==!0&&n.nodes.length===2){D.push(le(D.pop(),["{}"]));return}if(n.nodes&&n.ranges>0){let h=pe.reduce(n.nodes);if(pe.exceedsLimit(...h,e.step,u))throw new RangeError("expanded array length exceeds range limit. Use options.rangeLimit to increase or disable the limit.");let l=Fo(...h,e);l.length===0&&(l=zs(n,e)),D.push(le(D.pop(),l)),n.nodes=[];return}let o=pe.encloseBrace(n),c=n.queue,f=n;for(;f.type!=="brace"&&f.type!=="root"&&f.parent;)f=f.parent,c=f.queue;for(let h=0;h",CHAR_RIGHT_CURLY_BRACE:"}",CHAR_RIGHT_SQUARE_BRACKET:"]",CHAR_SEMICOLON:";",CHAR_SINGLE_QUOTE:"'",CHAR_SPACE:" ",CHAR_TAB:" ",CHAR_UNDERSCORE:"_",CHAR_VERTICAL_LINE:"|",CHAR_ZERO_WIDTH_NOBREAK_SPACE:"\uFEFF"};const Ao=Vt,{MAX_LENGTH:Ys,CHAR_BACKSLASH:Xt,CHAR_BACKTICK:yo,CHAR_COMMA:wo,CHAR_DOT:Ro,CHAR_LEFT_PARENTHESES:bo,CHAR_RIGHT_PARENTHESES:vo,CHAR_LEFT_CURLY_BRACE:So,CHAR_RIGHT_CURLY_BRACE:Bo,CHAR_LEFT_SQUARE_BRACKET:qs,CHAR_RIGHT_SQUARE_BRACKET:Xs,CHAR_DOUBLE_QUOTE:$o,CHAR_SINGLE_QUOTE:To,CHAR_NO_BREAK_SPACE:xo,CHAR_ZERO_WIDTH_NOBREAK_SPACE:Oo}=_o,No=a((t,e={})=>{if(typeof t!="string")throw new TypeError("Expected a string");let u=e||{},s=typeof u.maxLength=="number"?Math.min(Ys,u.maxLength):Ys;if(t.length>s)throw new SyntaxError(`Input length (${t.length}), exceeds max characters (${s})`);let n={type:"root",input:t,nodes:[]},r=[n],i=n,D=n,o=0,c=t.length,f=0,h=0,l;const p=a(()=>t[f++],"advance"),C=a(g=>{if(g.type==="text"&&D.type==="dot"&&(D.type="text"),D&&D.type==="text"&&g.type==="text"){D.value+=g.value;return}return i.nodes.push(g),g.parent=i,g.prev=D,D=g,g},"push");for(C({type:"bos"});f0){if(i.ranges>0){i.ranges=0;let g=i.nodes.shift();i.nodes=[g,{type:"text",value:Ao(i)}]}C({type:"comma",value:l}),i.commas++;continue}if(l===Ro&&h>0&&i.commas===0){let g=i.nodes;if(h===0||g.length===0){C({type:"text",value:l});continue}if(D.type==="dot"){if(i.range=[],D.value+=l,D.type="range",i.nodes.length!==3&&i.nodes.length!==5){i.invalid=!0,i.ranges=0,D.type="text";continue}i.ranges++,i.args=[];continue}if(D.type==="range"){g.pop();let y=g[g.length-1];y.value+=D.value+l,D=y,i.ranges--;continue}C({type:"dot",value:l});continue}C({type:"text",value:l})}do if(i=r.pop(),i.type!=="root"){i.nodes.forEach(B=>{B.nodes||(B.type==="open"&&(B.isOpen=!0),B.type==="close"&&(B.isClose=!0),B.nodes||(B.type="text"),B.invalid=!0)});let g=r[r.length-1],y=g.nodes.indexOf(i);g.nodes.splice(y,1,...i.nodes)}while(r.length>0);return C({type:"eos"}),n},"parse$1");var Ho=No;const Qs=Vt,Po=Co,Lo=mo,Io=Ho,q=a((t,e={})=>{let u=[];if(Array.isArray(t))for(let s of t){let n=q.create(s,e);Array.isArray(n)?u.push(...n):u.push(n)}else u=[].concat(q.create(t,e));return e&&e.expand===!0&&e.nodupes===!0&&(u=[...new Set(u)]),u},"braces$1");q.parse=(t,e={})=>Io(t,e),q.stringify=(t,e={})=>Qs(typeof t=="string"?q.parse(t,e):t,e),q.compile=(t,e={})=>(typeof t=="string"&&(t=q.parse(t,e)),Po(t,e)),q.expand=(t,e={})=>{typeof t=="string"&&(t=q.parse(t,e));let u=Lo(t,e);return e.noempty===!0&&(u=u.filter(Boolean)),e.nodupes===!0&&(u=[...new Set(u)]),u},q.create=(t,e={})=>t===""||t.length<3?[t]:e.expand!==!0?q.compile(t,e):q.expand(t,e);var ko=q,Mo=["3dm","3ds","3g2","3gp","7z","a","aac","adp","afdesign","afphoto","afpub","ai","aif","aiff","alz","ape","apk","appimage","ar","arj","asf","au","avi","bak","baml","bh","bin","bk","bmp","btif","bz2","bzip2","cab","caf","cgm","class","cmx","cpio","cr2","cur","dat","dcm","deb","dex","djvu","dll","dmg","dng","doc","docm","docx","dot","dotm","dra","DS_Store","dsk","dts","dtshd","dvb","dwg","dxf","ecelp4800","ecelp7470","ecelp9600","egg","eol","eot","epub","exe","f4v","fbs","fh","fla","flac","flatpak","fli","flv","fpx","fst","fvt","g3","gh","gif","graffle","gz","gzip","h261","h263","h264","icns","ico","ief","img","ipa","iso","jar","jpeg","jpg","jpgv","jpm","jxr","key","ktx","lha","lib","lvp","lz","lzh","lzma","lzo","m3u","m4a","m4v","mar","mdi","mht","mid","midi","mj2","mka","mkv","mmr","mng","mobi","mov","movie","mp3","mp4","mp4a","mpeg","mpg","mpga","mxu","nef","npx","numbers","nupkg","o","odp","ods","odt","oga","ogg","ogv","otf","ott","pages","pbm","pcx","pdb","pdf","pea","pgm","pic","png","pnm","pot","potm","potx","ppa","ppam","ppm","pps","ppsm","ppsx","ppt","pptm","pptx","psd","pya","pyc","pyo","pyv","qt","rar","ras","raw","resources","rgb","rip","rlc","rmf","rmvb","rpm","rtf","rz","s3m","s7z","scpt","sgi","shar","snap","sil","sketch","slk","smv","snk","so","stl","suo","sub","swf","tar","tbz","tbz2","tga","tgz","thmx","tif","tiff","tlz","ttc","ttf","txz","udf","uvh","uvi","uvm","uvp","uvs","uvu","viv","vob","war","wav","wax","wbmp","wdp","weba","webm","webp","whl","wim","wm","wma","wmv","wmx","woff","woff2","wrm","wvx","xbm","xif","xla","xlam","xls","xlsb","xlsm","xlsx","xlt","xltm","xltx","xm","xmind","xpi","xpm","xwd","xz","z","zip","zipx"],Go=Mo;const Wo=z,jo=Go,Uo=new Set(jo);var Ko=a(t=>Uo.has(Wo.extname(t).slice(1).toLowerCase()),"isBinaryPath$1"),et={};(function(t){const{sep:e}=z,{platform:u}=process,s=_u;t.EV_ALL="all",t.EV_READY="ready",t.EV_ADD="add",t.EV_CHANGE="change",t.EV_ADD_DIR="addDir",t.EV_UNLINK="unlink",t.EV_UNLINK_DIR="unlinkDir",t.EV_RAW="raw",t.EV_ERROR="error",t.STR_DATA="data",t.STR_END="end",t.STR_CLOSE="close",t.FSEVENT_CREATED="created",t.FSEVENT_MODIFIED="modified",t.FSEVENT_DELETED="deleted",t.FSEVENT_MOVED="moved",t.FSEVENT_CLONED="cloned",t.FSEVENT_UNKNOWN="unknown",t.FSEVENT_FLAG_MUST_SCAN_SUBDIRS=1,t.FSEVENT_TYPE_FILE="file",t.FSEVENT_TYPE_DIRECTORY="directory",t.FSEVENT_TYPE_SYMLINK="symlink",t.KEY_LISTENERS="listeners",t.KEY_ERR="errHandlers",t.KEY_RAW="rawEmitters",t.HANDLER_KEYS=[t.KEY_LISTENERS,t.KEY_ERR,t.KEY_RAW],t.DOT_SLASH=`.${e}`,t.BACK_SLASH_RE=/\\/g,t.DOUBLE_SLASH_RE=/\/\//,t.SLASH_OR_BACK_SLASH_RE=/[/\\]/,t.DOT_RE=/\..*\.(sw[px])$|~$|\.subl.*\.tmp/,t.REPLACER_RE=/^\.[/\\]/,t.SLASH="/",t.SLASH_SLASH="//",t.BRACE_START="{",t.BANG="!",t.ONE_DOT=".",t.TWO_DOTS="..",t.STAR="*",t.GLOBSTAR="**",t.ROOT_GLOBSTAR="/**/*",t.SLASH_GLOBSTAR="/**",t.DIR_SUFFIX="Dir",t.ANYMATCH_OPTS={dot:!0},t.STRING_TYPE="string",t.FUNCTION_TYPE="function",t.EMPTY_STR="",t.EMPTY_FN=()=>{},t.IDENTITY_FN=n=>n,t.isWindows=u==="win32",t.isMacos=u==="darwin",t.isLinux=u==="linux",t.isIBMi=s.type()==="OS400"})(et);const se=De,I=z,{promisify:ve}=ge,Vo=Ko,{isWindows:zo,isLinux:Yo,EMPTY_FN:qo,EMPTY_STR:Xo,KEY_LISTENERS:Ce,KEY_ERR:Qt,KEY_RAW:Se,HANDLER_KEYS:Qo,EV_CHANGE:tt,EV_ADD:ut,EV_ADD_DIR:Zo,EV_ERROR:Zs,STR_DATA:Jo,STR_END:ea,BRACE_START:ta,STAR:ua}=et,sa="watch",na=ve(se.open),Js=ve(se.stat),ra=ve(se.lstat),ia=ve(se.close),Zt=ve(se.realpath),Da={lstat:ra,stat:Js},Jt=a((t,e)=>{t instanceof Set?t.forEach(e):e(t)},"foreach"),Be=a((t,e,u)=>{let s=t[e];s instanceof Set||(t[e]=s=new Set([s])),s.add(u)},"addAndConvert"),oa=a(t=>e=>{const u=t[e];u instanceof Set?u.clear():delete t[e]},"clearItem"),$e=a((t,e,u)=>{const s=t[e];s instanceof Set?s.delete(u):s===u&&delete t[e]},"delFromSet"),en=a(t=>t instanceof Set?t.size===0:!t,"isEmptySet"),st=new Map;function tn(t,e,u,s,n){const r=a((i,D)=>{u(t),n(i,D,{watchedPath:t}),D&&t!==D&&nt(I.resolve(t,D),Ce,I.join(t,D))},"handleEvent");try{return se.watch(t,e,r)}catch(i){s(i)}}a(tn,"createFsWatchInstance");const nt=a((t,e,u,s,n)=>{const r=st.get(t);r&&Jt(r[e],i=>{i(u,s,n)})},"fsWatchBroadcast"),aa=a((t,e,u,s)=>{const{listener:n,errHandler:r,rawEmitter:i}=s;let D=st.get(e),o;if(!u.persistent)return o=tn(t,u,n,r,i),o.close.bind(o);if(D)Be(D,Ce,n),Be(D,Qt,r),Be(D,Se,i);else{if(o=tn(t,u,nt.bind(null,e,Ce),r,nt.bind(null,e,Se)),!o)return;o.on(Zs,async c=>{const f=nt.bind(null,e,Qt);if(D.watcherUnusable=!0,zo&&c.code==="EPERM")try{const h=await na(t,"r");await ia(h),f(c)}catch{}else f(c)}),D={listeners:n,errHandlers:r,rawEmitters:i,watcher:o},st.set(e,D)}return()=>{$e(D,Ce,n),$e(D,Qt,r),$e(D,Se,i),en(D.listeners)&&(D.watcher.close(),st.delete(e),Qo.forEach(oa(D)),D.watcher=void 0,Object.freeze(D))}},"setFsWatchListener"),eu=new Map,la=a((t,e,u,s)=>{const{listener:n,rawEmitter:r}=s;let i=eu.get(e);const D=i&&i.options;return D&&(D.persistentu.interval)&&(i.listeners,i.rawEmitters,se.unwatchFile(e),i=void 0),i?(Be(i,Ce,n),Be(i,Se,r)):(i={listeners:n,rawEmitters:r,options:u,watcher:se.watchFile(e,u,(o,c)=>{Jt(i.rawEmitters,h=>{h(tt,e,{curr:o,prev:c})});const f=o.mtimeMs;(o.size!==c.size||f>c.mtimeMs||f===0)&&Jt(i.listeners,h=>h(t,o))})},eu.set(e,i)),()=>{$e(i,Ce,n),$e(i,Se,r),en(i.listeners)&&(eu.delete(e),se.unwatchFile(e),i.options=i.watcher=void 0,Object.freeze(i))}},"setFsWatchFileListener");let ca=class{static{a(this,"NodeFsHandler")}constructor(e){this.fsw=e,this._boundHandleError=u=>e._handleError(u)}_watchWithNodeFs(e,u){const s=this.fsw.options,n=I.dirname(e),r=I.basename(e);this.fsw._getWatchedDir(n).add(r);const D=I.resolve(e),o={persistent:s.persistent};u||(u=qo);let c;return s.usePolling?(o.interval=s.enableBinaryInterval&&Vo(r)?s.binaryInterval:s.interval,c=la(e,D,o,{listener:u,rawEmitter:this.fsw._emitRaw})):c=aa(e,D,o,{listener:u,errHandler:this._boundHandleError,rawEmitter:this.fsw._emitRaw}),c}_handleFile(e,u,s){if(this.fsw.closed)return;const n=I.dirname(e),r=I.basename(e),i=this.fsw._getWatchedDir(n);let D=u;if(i.has(r))return;const o=a(async(f,h)=>{if(this.fsw._throttle(sa,e,5)){if(!h||h.mtimeMs===0)try{const l=await Js(e);if(this.fsw.closed)return;const p=l.atimeMs,C=l.mtimeMs;(!p||p<=C||C!==D.mtimeMs)&&this.fsw._emit(tt,e,l),Yo&&D.ino!==l.ino?(this.fsw._closeFile(f),D=l,this.fsw._addPathCloser(f,this._watchWithNodeFs(e,o))):D=l}catch{this.fsw._remove(n,r)}else if(i.has(r)){const l=h.atimeMs,p=h.mtimeMs;(!l||l<=p||p!==D.mtimeMs)&&this.fsw._emit(tt,e,h),D=h}}},"listener"),c=this._watchWithNodeFs(e,o);if(!(s&&this.fsw.options.ignoreInitial)&&this.fsw._isntIgnored(e)){if(!this.fsw._throttle(ut,e,0))return;this.fsw._emit(ut,e,u)}return c}async _handleSymlink(e,u,s,n){if(this.fsw.closed)return;const r=e.fullPath,i=this.fsw._getWatchedDir(u);if(!this.fsw.options.followSymlinks){this.fsw._incrReadyCount();let D;try{D=await Zt(s)}catch{return this.fsw._emitReady(),!0}return this.fsw.closed?void 0:(i.has(n)?this.fsw._symlinkPaths.get(r)!==D&&(this.fsw._symlinkPaths.set(r,D),this.fsw._emit(tt,s,e.stats)):(i.add(n),this.fsw._symlinkPaths.set(r,D),this.fsw._emit(ut,s,e.stats)),this.fsw._emitReady(),!0)}if(this.fsw._symlinkPaths.has(r))return!0;this.fsw._symlinkPaths.set(r,!0)}_handleRead(e,u,s,n,r,i,D){if(e=I.join(e,Xo),!s.hasGlob&&(D=this.fsw._throttle("readdir",e,1e3),!D))return;const o=this.fsw._getWatchedDir(s.path),c=new Set;let f=this.fsw._readdirp(e,{fileFilter:a(h=>s.filterPath(h),"fileFilter"),directoryFilter:a(h=>s.filterDir(h),"directoryFilter"),depth:0}).on(Jo,async h=>{if(this.fsw.closed){f=void 0;return}const l=h.path;let p=I.join(e,l);if(c.add(l),!(h.stats.isSymbolicLink()&&await this._handleSymlink(h,e,p,l))){if(this.fsw.closed){f=void 0;return}(l===n||!n&&!o.has(l))&&(this.fsw._incrReadyCount(),p=I.join(r,I.relative(r,p)),this._addToNodeFs(p,u,s,i+1))}}).on(Zs,this._boundHandleError);return new Promise(h=>f.once(ea,()=>{if(this.fsw.closed){f=void 0;return}const l=D?D.clear():!1;h(),o.getChildren().filter(p=>p!==e&&!c.has(p)&&(!s.hasGlob||s.filterPath({fullPath:I.resolve(e,p)}))).forEach(p=>{this.fsw._remove(e,p)}),f=void 0,l&&this._handleRead(e,!1,s,n,r,i,D)}))}async _handleDir(e,u,s,n,r,i,D){const o=this.fsw._getWatchedDir(I.dirname(e)),c=o.has(I.basename(e));!(s&&this.fsw.options.ignoreInitial)&&!r&&!c&&(!i.hasGlob||i.globFilter(e))&&this.fsw._emit(Zo,e,u),o.add(I.basename(e)),this.fsw._getWatchedDir(e);let f,h;const l=this.fsw.options.depth;if((l==null||n<=l)&&!this.fsw._symlinkPaths.has(D)){if(!r&&(await this._handleRead(e,s,i,r,e,n,f),this.fsw.closed))return;h=this._watchWithNodeFs(e,(p,C)=>{C&&C.mtimeMs===0||this._handleRead(p,!1,i,r,e,n,f)})}return h}async _addToNodeFs(e,u,s,n,r){const i=this.fsw._emitReady;if(this.fsw._isIgnored(e)||this.fsw.closed)return i(),!1;const D=this.fsw._getWatchHelpers(e,n);!D.hasGlob&&s&&(D.hasGlob=s.hasGlob,D.globFilter=s.globFilter,D.filterPath=o=>s.filterPath(o),D.filterDir=o=>s.filterDir(o));try{const o=await Da[D.statMethod](D.watchPath);if(this.fsw.closed)return;if(this.fsw._isIgnored(D.watchPath,o))return i(),!1;const c=this.fsw.options.followSymlinks&&!e.includes(ua)&&!e.includes(ta);let f;if(o.isDirectory()){const h=I.resolve(e),l=c?await Zt(e):e;if(this.fsw.closed||(f=await this._handleDir(D.watchPath,o,u,n,r,D,l),this.fsw.closed))return;h!==l&&l!==void 0&&this.fsw._symlinkPaths.set(h,l)}else if(o.isSymbolicLink()){const h=c?await Zt(e):e;if(this.fsw.closed)return;const l=I.dirname(D.watchPath);if(this.fsw._getWatchedDir(l).add(D.watchPath),this.fsw._emit(ut,D.watchPath,o),f=await this._handleDir(l,o,u,n,e,D,h),this.fsw.closed)return;h!==void 0&&this.fsw._symlinkPaths.set(I.resolve(e),h)}else f=this._handleFile(D.watchPath,o,u);return i(),this.fsw._addPathCloser(e,f),!1}catch(o){if(this.fsw._handleError(o))return i(),e}}};var fa=ca,tu={exports:{}};const uu=De,k=z,{promisify:su}=ge;let Fe;try{Fe=Ie("fsevents")}catch(t){process.env.CHOKIDAR_PRINT_FSEVENTS_REQUIRE_ERROR&&console.error(t)}if(Fe){const t=process.version.match(/v(\d+)\.(\d+)/);if(t&&t[1]&&t[2]){const e=Number.parseInt(t[1],10),u=Number.parseInt(t[2],10);e===8&&u<16&&(Fe=void 0)}}const{EV_ADD:nu,EV_CHANGE:ha,EV_ADD_DIR:un,EV_UNLINK:rt,EV_ERROR:da,STR_DATA:Ea,STR_END:pa,FSEVENT_CREATED:Ca,FSEVENT_MODIFIED:Fa,FSEVENT_DELETED:ga,FSEVENT_MOVED:ma,FSEVENT_UNKNOWN:_a,FSEVENT_FLAG_MUST_SCAN_SUBDIRS:Aa,FSEVENT_TYPE_FILE:ya,FSEVENT_TYPE_DIRECTORY:Te,FSEVENT_TYPE_SYMLINK:sn,ROOT_GLOBSTAR:nn,DIR_SUFFIX:wa,DOT_SLASH:rn,FUNCTION_TYPE:ru,EMPTY_FN:Ra,IDENTITY_FN:ba}=et,va=a(t=>isNaN(t)?{}:{depth:t},"Depth"),iu=su(uu.stat),Sa=su(uu.lstat),Dn=su(uu.realpath),Ba={stat:iu,lstat:Sa},ce=new Map,$a=10,Ta=new Set([69888,70400,71424,72704,73472,131328,131840,262912]),xa=a((t,e)=>({stop:Fe.watch(t,e)}),"createFSEventsInstance");function Oa(t,e,u,s){let n=k.extname(e)?k.dirname(e):e;const r=k.dirname(n);let i=ce.get(n);Na(r)&&(n=r);const D=k.resolve(t),o=D!==e,c=a((h,l,p)=>{o&&(h=h.replace(e,D)),(h===D||!h.indexOf(D+k.sep))&&u(h,l,p)},"filteredListener");let f=!1;for(const h of ce.keys())if(e.indexOf(k.resolve(h)+k.sep)===0){n=h,i=ce.get(n),f=!0;break}return i||f?i.listeners.add(c):(i={listeners:new Set([c]),rawEmitter:s,watcher:xa(n,(h,l)=>{if(!i.listeners.size||l&Aa)return;const p=Fe.getInfo(h,l);i.listeners.forEach(C=>{C(h,l,p)}),i.rawEmitter(p.event,h,p)})},ce.set(n,i)),()=>{const h=i.listeners;if(h.delete(c),!h.size&&(ce.delete(n),i.watcher))return i.watcher.stop().then(()=>{i.rawEmitter=i.watcher=void 0,Object.freeze(i)})}}a(Oa,"setFSEventsListener");const Na=a(t=>{let e=0;for(const u of ce.keys())if(u.indexOf(t)===0&&(e++,e>=$a))return!0;return!1},"couldConsolidate"),Ha=a(()=>Fe&&ce.size<128,"canUse"),Du=a((t,e)=>{let u=0;for(;!t.indexOf(e)&&(t=k.dirname(t))!==e;)u++;return u},"calcDepth"),on=a((t,e)=>t.type===Te&&e.isDirectory()||t.type===sn&&e.isSymbolicLink()||t.type===ya&&e.isFile(),"sameTypes");let Pa=class{static{a(this,"FsEventsHandler")}constructor(e){this.fsw=e}checkIgnored(e,u){const s=this.fsw._ignoredPaths;if(this.fsw._isIgnored(e,u))return s.add(e),u&&u.isDirectory()&&s.add(e+nn),!0;s.delete(e),s.delete(e+nn)}addOrChange(e,u,s,n,r,i,D,o){const c=r.has(i)?ha:nu;this.handleEvent(c,e,u,s,n,r,i,D,o)}async checkExists(e,u,s,n,r,i,D,o){try{const c=await iu(e);if(this.fsw.closed)return;on(D,c)?this.addOrChange(e,u,s,n,r,i,D,o):this.handleEvent(rt,e,u,s,n,r,i,D,o)}catch(c){c.code==="EACCES"?this.addOrChange(e,u,s,n,r,i,D,o):this.handleEvent(rt,e,u,s,n,r,i,D,o)}}handleEvent(e,u,s,n,r,i,D,o,c){if(!(this.fsw.closed||this.checkIgnored(u)))if(e===rt){const f=o.type===Te;(f||i.has(D))&&this.fsw._remove(r,D,f)}else{if(e===nu){if(o.type===Te&&this.fsw._getWatchedDir(u),o.type===sn&&c.followSymlinks){const h=c.depth===void 0?void 0:Du(s,n)+1;return this._addToFsEvents(u,!1,!0,h)}this.fsw._getWatchedDir(r).add(D)}const f=o.type===Te?e+wa:e;this.fsw._emit(f,u),f===un&&this._addToFsEvents(u,!1,!0)}}_watchWithFsEvents(e,u,s,n){if(this.fsw.closed||this.fsw._isIgnored(e))return;const r=this.fsw.options,D=Oa(e,u,a(async(o,c,f)=>{if(this.fsw.closed||r.depth!==void 0&&Du(o,u)>r.depth)return;const h=s(k.join(e,k.relative(e,o)));if(n&&!n(h))return;const l=k.dirname(h),p=k.basename(h),C=this.fsw._getWatchedDir(f.type===Te?h:l);if(Ta.has(c)||f.event===_a)if(typeof r.ignored===ru){let g;try{g=await iu(h)}catch{}if(this.fsw.closed||this.checkIgnored(h,g))return;on(f,g)?this.addOrChange(h,o,u,l,C,p,f,r):this.handleEvent(rt,h,o,u,l,C,p,f,r)}else this.checkExists(h,o,u,l,C,p,f,r);else switch(f.event){case Ca:case Fa:return this.addOrChange(h,o,u,l,C,p,f,r);case ga:case ma:return this.checkExists(h,o,u,l,C,p,f,r)}},"watchCallback"),this.fsw._emitRaw);return this.fsw._emitReady(),D}async _handleFsEventsSymlink(e,u,s,n){if(!(this.fsw.closed||this.fsw._symlinkPaths.has(u))){this.fsw._symlinkPaths.set(u,!0),this.fsw._incrReadyCount();try{const r=await Dn(e);if(this.fsw.closed)return;if(this.fsw._isIgnored(r))return this.fsw._emitReady();this.fsw._incrReadyCount(),this._addToFsEvents(r||e,i=>{let D=e;return r&&r!==rn?D=i.replace(r,e):i!==rn&&(D=k.join(e,i)),s(D)},!1,n)}catch(r){if(this.fsw._handleError(r))return this.fsw._emitReady()}}}emitAdd(e,u,s,n,r){const i=s(e),D=u.isDirectory(),o=this.fsw._getWatchedDir(k.dirname(i)),c=k.basename(i);D&&this.fsw._getWatchedDir(i),!o.has(c)&&(o.add(c),(!n.ignoreInitial||r===!0)&&this.fsw._emit(D?un:nu,i,u))}initWatch(e,u,s,n){if(this.fsw.closed)return;const r=this._watchWithFsEvents(s.watchPath,k.resolve(e||s.watchPath),n,s.globFilter);this.fsw._addPathCloser(u,r)}async _addToFsEvents(e,u,s,n){if(this.fsw.closed)return;const r=this.fsw.options,i=typeof u===ru?u:ba,D=this.fsw._getWatchHelpers(e);try{const o=await Ba[D.statMethod](D.watchPath);if(this.fsw.closed)return;if(this.fsw._isIgnored(D.watchPath,o))throw null;if(o.isDirectory()){if(D.globFilter||this.emitAdd(i(e),o,i,r,s),n&&n>r.depth)return;this.fsw._readdirp(D.watchPath,{fileFilter:a(c=>D.filterPath(c),"fileFilter"),directoryFilter:a(c=>D.filterDir(c),"directoryFilter"),...va(r.depth-(n||0))}).on(Ea,c=>{if(this.fsw.closed||c.stats.isDirectory()&&!D.filterPath(c))return;const f=k.join(D.watchPath,c.path),{fullPath:h}=c;if(D.followSymlinks&&c.stats.isSymbolicLink()){const l=r.depth===void 0?void 0:Du(f,k.resolve(D.watchPath))+1;this._handleFsEventsSymlink(f,h,i,l)}else this.emitAdd(f,c.stats,i,r,s)}).on(da,Ra).on(pa,()=>{this.fsw._emitReady()})}else this.emitAdd(D.watchPath,o,i,r,s),this.fsw._emitReady()}catch(o){(!o||this.fsw._handleError(o))&&(this.fsw._emitReady(),this.fsw._emitReady())}if(r.persistent&&s!==!0)if(typeof u===ru)this.initWatch(void 0,e,D,i);else{let o;try{o=await Dn(D.watchPath)}catch{}this.initWatch(o,e,D,i)}}};tu.exports=Pa,tu.exports.canUse=Ha;var La=tu.exports;const{EventEmitter:Ia}=Ln,ou=De,S=z,{promisify:an}=ge,ka=xD,au=ID.default,Ma=XD,lu=Bs,Ga=ko,Wa=ws,ja=fa,ln=La,{EV_ALL:cu,EV_READY:Ua,EV_ADD:it,EV_CHANGE:xe,EV_UNLINK:cn,EV_ADD_DIR:Ka,EV_UNLINK_DIR:Va,EV_RAW:za,EV_ERROR:fu,STR_CLOSE:Ya,STR_END:qa,BACK_SLASH_RE:Xa,DOUBLE_SLASH_RE:fn,SLASH_OR_BACK_SLASH_RE:Qa,DOT_RE:Za,REPLACER_RE:Ja,SLASH:hu,SLASH_SLASH:el,BRACE_START:tl,BANG:du,ONE_DOT:hn,TWO_DOTS:ul,GLOBSTAR:sl,SLASH_GLOBSTAR:Eu,ANYMATCH_OPTS:pu,STRING_TYPE:Cu,FUNCTION_TYPE:nl,EMPTY_STR:Fu,EMPTY_FN:rl,isWindows:il,isMacos:Dl,isIBMi:ol}=et,al=an(ou.stat),ll=an(ou.readdir),gu=a((t=[])=>Array.isArray(t)?t:[t],"arrify"),dn=a((t,e=[])=>(t.forEach(u=>{Array.isArray(u)?dn(u,e):e.push(u)}),e),"flatten"),En=a(t=>{const e=dn(gu(t));if(!e.every(u=>typeof u===Cu))throw new TypeError(`Non-string provided as watch path: ${e}`);return e.map(Cn)},"unifyPaths"),pn=a(t=>{let e=t.replace(Xa,hu),u=!1;for(e.startsWith(el)&&(u=!0);e.match(fn);)e=e.replace(fn,hu);return u&&(e=hu+e),e},"toUnix"),Cn=a(t=>pn(S.normalize(pn(t))),"normalizePathToUnix"),Fn=a((t=Fu)=>e=>typeof e!==Cu?e:Cn(S.isAbsolute(e)?e:S.join(t,e)),"normalizeIgnored"),cl=a((t,e)=>S.isAbsolute(t)?t:t.startsWith(du)?du+S.join(e,t.slice(1)):S.join(e,t),"getAbsolutePath"),X=a((t,e)=>t[e]===void 0,"undef");class fl{static{a(this,"DirEntry")}constructor(e,u){this.path=e,this._removeWatcher=u,this.items=new Set}add(e){const{items:u}=this;u&&e!==hn&&e!==ul&&u.add(e)}async remove(e){const{items:u}=this;if(!u||(u.delete(e),u.size>0))return;const s=this.path;try{await ll(s)}catch{this._removeWatcher&&this._removeWatcher(S.dirname(s),S.basename(s))}}has(e){const{items:u}=this;if(u)return u.has(e)}getChildren(){const{items:e}=this;if(e)return[...e.values()]}dispose(){this.items.clear(),delete this.path,delete this._removeWatcher,delete this.items,Object.freeze(this)}}const hl="stat",dl="lstat";class El{static{a(this,"WatchHelper")}constructor(e,u,s,n){this.fsw=n,this.path=e=e.replace(Ja,Fu),this.watchPath=u,this.fullWatchPath=S.resolve(u),this.hasGlob=u!==e,e===Fu&&(this.hasGlob=!1),this.globSymlink=this.hasGlob&&s?void 0:!1,this.globFilter=this.hasGlob?au(e,void 0,pu):!1,this.dirParts=this.getDirParts(e),this.dirParts.forEach(r=>{r.length>1&&r.pop()}),this.followSymlinks=s,this.statMethod=s?hl:dl}checkGlobSymlink(e){return this.globSymlink===void 0&&(this.globSymlink=e.fullParentDir===this.fullWatchPath?!1:{realPath:e.fullParentDir,linkPath:this.fullWatchPath}),this.globSymlink?e.fullPath.replace(this.globSymlink.realPath,this.globSymlink.linkPath):e.fullPath}entryPath(e){return S.join(this.watchPath,S.relative(this.watchPath,this.checkGlobSymlink(e)))}filterPath(e){const{stats:u}=e;if(u&&u.isSymbolicLink())return this.filterDir(e);const s=this.entryPath(e);return(this.hasGlob&&typeof this.globFilter===nl?this.globFilter(s):!0)&&this.fsw._isntIgnored(s,u)&&this.fsw._hasReadPermissions(u)}getDirParts(e){if(!this.hasGlob)return[];const u=[];return(e.includes(tl)?Ga.expand(e):[e]).forEach(n=>{u.push(S.relative(this.watchPath,n).split(Qa))}),u}filterDir(e){if(this.hasGlob){const u=this.getDirParts(this.checkGlobSymlink(e));let s=!1;this.unmatchedGlob=!this.dirParts.some(n=>n.every((r,i)=>(r===sl&&(s=!0),s||!u[0][i]||au(r,u[0][i],pu))))}return!this.unmatchedGlob&&this.fsw._isntIgnored(this.entryPath(e),e.stats)}}class pl extends Ia{static{a(this,"FSWatcher")}constructor(e){super();const u={};e&&Object.assign(u,e),this._watched=new Map,this._closers=new Map,this._ignoredPaths=new Set,this._throttled=new Map,this._symlinkPaths=new Map,this._streams=new Set,this.closed=!1,X(u,"persistent")&&(u.persistent=!0),X(u,"ignoreInitial")&&(u.ignoreInitial=!1),X(u,"ignorePermissionErrors")&&(u.ignorePermissionErrors=!1),X(u,"interval")&&(u.interval=100),X(u,"binaryInterval")&&(u.binaryInterval=300),X(u,"disableGlobbing")&&(u.disableGlobbing=!1),u.enableBinaryInterval=u.binaryInterval!==u.interval,X(u,"useFsEvents")&&(u.useFsEvents=!u.usePolling),ln.canUse()||(u.useFsEvents=!1),X(u,"usePolling")&&!u.useFsEvents&&(u.usePolling=Dl),ol&&(u.usePolling=!0);const n=process.env.CHOKIDAR_USEPOLLING;if(n!==void 0){const o=n.toLowerCase();o==="false"||o==="0"?u.usePolling=!1:o==="true"||o==="1"?u.usePolling=!0:u.usePolling=!!o}const r=process.env.CHOKIDAR_INTERVAL;r&&(u.interval=Number.parseInt(r,10)),X(u,"atomic")&&(u.atomic=!u.usePolling&&!u.useFsEvents),u.atomic&&(this._pendingUnlinks=new Map),X(u,"followSymlinks")&&(u.followSymlinks=!0),X(u,"awaitWriteFinish")&&(u.awaitWriteFinish=!1),u.awaitWriteFinish===!0&&(u.awaitWriteFinish={});const i=u.awaitWriteFinish;i&&(i.stabilityThreshold||(i.stabilityThreshold=2e3),i.pollInterval||(i.pollInterval=100),this._pendingWrites=new Map),u.ignored&&(u.ignored=gu(u.ignored));let D=0;this._emitReady=()=>{D++,D>=this._readyCount&&(this._emitReady=rl,this._readyEmitted=!0,process.nextTick(()=>this.emit(Ua)))},this._emitRaw=(...o)=>this.emit(za,...o),this._readyEmitted=!1,this.options=u,u.useFsEvents?this._fsEventsHandler=new ln(this):this._nodeFsHandler=new ja(this),Object.freeze(u)}add(e,u,s){const{cwd:n,disableGlobbing:r}=this.options;this.closed=!1;let i=En(e);return n&&(i=i.map(D=>{const o=cl(D,n);return r||!lu(D)?o:Wa(o)})),i=i.filter(D=>D.startsWith(du)?(this._ignoredPaths.add(D.slice(1)),!1):(this._ignoredPaths.delete(D),this._ignoredPaths.delete(D+Eu),this._userIgnored=void 0,!0)),this.options.useFsEvents&&this._fsEventsHandler?(this._readyCount||(this._readyCount=i.length),this.options.persistent&&(this._readyCount+=i.length),i.forEach(D=>this._fsEventsHandler._addToFsEvents(D))):(this._readyCount||(this._readyCount=0),this._readyCount+=i.length,Promise.all(i.map(async D=>{const o=await this._nodeFsHandler._addToNodeFs(D,!s,0,0,u);return o&&this._emitReady(),o})).then(D=>{this.closed||D.filter(o=>o).forEach(o=>{this.add(S.dirname(o),S.basename(u||o))})})),this}unwatch(e){if(this.closed)return this;const u=En(e),{cwd:s}=this.options;return u.forEach(n=>{!S.isAbsolute(n)&&!this._closers.has(n)&&(s&&(n=S.join(s,n)),n=S.resolve(n)),this._closePath(n),this._ignoredPaths.add(n),this._watched.has(n)&&this._ignoredPaths.add(n+Eu),this._userIgnored=void 0}),this}close(){if(this.closed)return this._closePromise;this.closed=!0,this.removeAllListeners();const e=[];return this._closers.forEach(u=>u.forEach(s=>{const n=s();n instanceof Promise&&e.push(n)})),this._streams.forEach(u=>u.destroy()),this._userIgnored=void 0,this._readyCount=0,this._readyEmitted=!1,this._watched.forEach(u=>u.dispose()),["closers","watched","streams","symlinkPaths","throttled"].forEach(u=>{this[`_${u}`].clear()}),this._closePromise=e.length?Promise.all(e).then(()=>{}):Promise.resolve(),this._closePromise}getWatched(){const e={};return this._watched.forEach((u,s)=>{const n=this.options.cwd?S.relative(this.options.cwd,s):s;e[n||hn]=u.getChildren().sort()}),e}emitWithAll(e,u){this.emit(...u),e!==fu&&this.emit(cu,...u)}async _emit(e,u,s,n,r){if(this.closed)return;const i=this.options;il&&(u=S.normalize(u)),i.cwd&&(u=S.relative(i.cwd,u));const D=[e,u];r!==void 0?D.push(s,n,r):n!==void 0?D.push(s,n):s!==void 0&&D.push(s);const o=i.awaitWriteFinish;let c;if(o&&(c=this._pendingWrites.get(u)))return c.lastChange=new Date,this;if(i.atomic){if(e===cn)return this._pendingUnlinks.set(u,D),setTimeout(()=>{this._pendingUnlinks.forEach((f,h)=>{this.emit(...f),this.emit(cu,...f),this._pendingUnlinks.delete(h)})},typeof i.atomic=="number"?i.atomic:100),this;e===it&&this._pendingUnlinks.has(u)&&(e=D[0]=xe,this._pendingUnlinks.delete(u))}if(o&&(e===it||e===xe)&&this._readyEmitted){const f=a((h,l)=>{h?(e=D[0]=fu,D[1]=h,this.emitWithAll(e,D)):l&&(D.length>2?D[2]=l:D.push(l),this.emitWithAll(e,D))},"awfEmit");return this._awaitWriteFinish(u,o.stabilityThreshold,e,f),this}if(e===xe&&!this._throttle(xe,u,50))return this;if(i.alwaysStat&&s===void 0&&(e===it||e===Ka||e===xe)){const f=i.cwd?S.join(i.cwd,u):u;let h;try{h=await al(f)}catch{}if(!h||this.closed)return;D.push(h)}return this.emitWithAll(e,D),this}_handleError(e){const u=e&&e.code;return e&&u!=="ENOENT"&&u!=="ENOTDIR"&&(!this.options.ignorePermissionErrors||u!=="EPERM"&&u!=="EACCES")&&this.emit(fu,e),e||this.closed}_throttle(e,u,s){this._throttled.has(e)||this._throttled.set(e,new Map);const n=this._throttled.get(e),r=n.get(u);if(r)return r.count++,!1;let i;const D=a(()=>{const c=n.get(u),f=c?c.count:0;return n.delete(u),clearTimeout(i),c&&clearTimeout(c.timeoutObject),f},"clear");i=setTimeout(D,s);const o={timeoutObject:i,clear:D,count:0};return n.set(u,o),o}_incrReadyCount(){return this._readyCount++}_awaitWriteFinish(e,u,s,n){let r,i=e;this.options.cwd&&!S.isAbsolute(e)&&(i=S.join(this.options.cwd,e));const D=new Date,o=a(c=>{ou.stat(i,(f,h)=>{if(f||!this._pendingWrites.has(e)){f&&f.code!=="ENOENT"&&n(f);return}const l=Number(new Date);c&&h.size!==c.size&&(this._pendingWrites.get(e).lastChange=l);const p=this._pendingWrites.get(e);l-p.lastChange>=u?(this._pendingWrites.delete(e),n(void 0,h)):r=setTimeout(o,this.options.awaitWriteFinish.pollInterval,h)})},"awaitWriteFinish");this._pendingWrites.has(e)||(this._pendingWrites.set(e,{lastChange:D,cancelWait:a(()=>(this._pendingWrites.delete(e),clearTimeout(r),s),"cancelWait")}),r=setTimeout(o,this.options.awaitWriteFinish.pollInterval))}_getGlobIgnored(){return[...this._ignoredPaths.values()]}_isIgnored(e,u){if(this.options.atomic&&Za.test(e))return!0;if(!this._userIgnored){const{cwd:s}=this.options,n=this.options.ignored,r=n&&n.map(Fn(s)),i=gu(r).filter(o=>typeof o===Cu&&!lu(o)).map(o=>o+Eu),D=this._getGlobIgnored().map(Fn(s)).concat(r,i);this._userIgnored=au(D,void 0,pu)}return this._userIgnored([e,u])}_isntIgnored(e,u){return!this._isIgnored(e,u)}_getWatchHelpers(e,u){const s=u||this.options.disableGlobbing||!lu(e)?e:Ma(e),n=this.options.followSymlinks;return new El(e,s,n,this)}_getWatchedDir(e){this._boundRemove||(this._boundRemove=this._remove.bind(this));const u=S.resolve(e);return this._watched.has(u)||this._watched.set(u,new fl(u,this._boundRemove)),this._watched.get(u)}_hasReadPermissions(e){if(this.options.ignorePermissionErrors)return!0;const s=(e&&Number.parseInt(e.mode,10))&511;return!!(4&Number.parseInt(s.toString(8)[0],10))}_remove(e,u,s){const n=S.join(e,u),r=S.resolve(n);if(s=s??(this._watched.has(n)||this._watched.has(r)),!this._throttle("remove",n,100))return;!s&&!this.options.useFsEvents&&this._watched.size===1&&this.add(e,u,!0),this._getWatchedDir(n).getChildren().forEach(l=>this._remove(n,l));const o=this._getWatchedDir(e),c=o.has(u);o.remove(u),this._symlinkPaths.has(r)&&this._symlinkPaths.delete(r);let f=n;if(this.options.cwd&&(f=S.relative(this.options.cwd,n)),this.options.awaitWriteFinish&&this._pendingWrites.has(f)&&this._pendingWrites.get(f).cancelWait()===it)return;this._watched.delete(n),this._watched.delete(r);const h=s?Va:cn;c&&!this._isIgnored(n)&&this._emit(h,n),this.options.useFsEvents||this._closePath(n)}_closePath(e){this._closeFile(e);const u=S.dirname(e);this._getWatchedDir(u).remove(S.basename(e))}_closeFile(e){const u=this._closers.get(e);u&&(u.forEach(s=>s()),this._closers.delete(e))}_addPathCloser(e,u){if(!u)return;let s=this._closers.get(e);s||(s=[],this._closers.set(e,s)),s.push(u)}_readdirp(e,u){if(this.closed)return;const s={type:cu,alwaysStat:!0,lstat:!0,...u};let n=ka(e,s);return this._streams.add(n),n.once(Ya,()=>{n=void 0}),n.once(qa,()=>{n&&(this._streams.delete(n),n=void 0)}),n}}const Cl=a((t,e)=>{const u=new pl(e);return u.add(t),u},"watch");var Fl=Cl;const Dt=a((t=!0)=>{let e=!1;return u=>{if(e||u==="unknown-flag")return!0;if(u==="argument")return e=!0,t}},"ignoreAfterArgument"),gn=a((t,e=process.argv.slice(2))=>(yu(t,e,{ignore:Dt()}),e),"removeArgvFlags"),gl=a(t=>{let e=Buffer.alloc(0);return u=>{for(e=Buffer.concat([e,u]);e.length>4;){const s=e.readInt32BE(0);if(e.length>=4+s){const n=e.slice(4,4+s);t(n),e=e.slice(4+s)}else break}}},"bufferData"),mn=a(async()=>{const t=jn.createServer(u=>{u.on("data",gl(s=>{const n=JSON.parse(s.toString());t.emit("data",n)}))}),e=Bn(process.pid);return await ct.promises.mkdir(Un,{recursive:!0}),await ct.promises.rm(e,{force:!0}),await new Promise((u,s)=>{t.listen(e,u),t.on("error",s)}),t.unref(),process.on("exit",()=>{if(t.close(),!$n)try{ct.rmSync(e)}catch{}}),t},"createIpcServer"),ml=a(()=>new Date().toLocaleTimeString(),"currentTime"),Oe=a((...t)=>console.log(kn(ml()),Mn("[tsx]"),...t),"log"),_l="\x1Bc",Al=a((t,e)=>{let u;return function(){u&&clearTimeout(u),u=setTimeout(()=>Reflect.apply(t,this,arguments),e)}},"debounce"),_n={noCache:{type:Boolean,description:"Disable caching",default:!1},tsconfig:{type:String,description:"Custom tsconfig.json path"},clearScreen:{type:Boolean,description:"Clearing the screen on rerun",default:!0},ignore:{type:[String],description:"Paths & globs to exclude from being watched (Deprecated: use --exclude)"},include:{type:[String],description:"Additional paths & globs to watch"},exclude:{type:[String],description:"Paths & globs to exclude from being watched"}},yl=oi({name:"watch",parameters:[" +``` + +To include NWSAPI in a standard web page and automatically replace the native QSA: + +```html + +``` + +To use NWSAPI with Node.js: + +``` +$ npm install nwsapi +``` + +NWSAPI currently supports browsers (as a global, `NW.Dom`) and headless environments (as a CommonJS module). + + +## Supported Selectors + +Here is a list of all the CSS2/CSS3/CSS4 [Supported selectors](https://github.com/dperini/nwsapi/wiki/CSS-supported-selectors). + + +## Features and Compliance + +You can read more about NWSAPI [features and compliance](https://github.com/dperini/nwsapi/wiki/Features-and-compliance) on the wiki. + + +## API + +### DOM Selection + +#### `ancestor( selector, context, callback )` + +Returns a reference to the nearest ancestor element matching `selector`, starting at `context`. Returns `null` if no element is found. If `callback` is provided, it is invoked for the matched element. + +#### `first( selector, context, callback )` + +Returns a reference to the first element matching `selector`, starting at `context`. Returns `null` if no element matches. If `callback` is provided, it is invoked for the matched element. + +#### `match( selector, element, callback )` + +Returns `true` if `element` matches `selector`, starting at `context`; returns `false` otherwise. If `callback` is provided, it is invoked for the matched element. + +#### `select( selector, context, callback )` + +Returns an array of all the elements matching `selector`, starting at `context`; returns empty `Array` otherwise. If `callback` is provided, it is invoked for each matching element. + + +### DOM Helpers + +#### `byId( id, from )` + +Returns a reference to the first element with ID `id`, optionally filtered to descendants of the element `from`. + +#### `byTag( tag, from )` + +Returns an array of elements having the specified tag name `tag`, optionally filtered to descendants of the element `from`. + +#### `byClass( class, from )` + +Returns an array of elements having the specified class name `class`, optionally filtered to descendants of the element `from`. + + +### Engine Configuration + +#### `configure( options )` + +The following is the list of currently available configuration options, their default values and descriptions, they are boolean flags that can be set to `true` or `false`: + +* `IDS_DUPES`: true - true to allow using multiple elements having the same id, false to disallow +* `LIVECACHE`: true - true for caching both results and resolvers, false for caching only resolvers +* `MIXEDCASE`: true - true to match tag names case insensitive, false to match using case sensitive +* `LOGERRORS`: true - true to print errors and warnings to the console, false to mute both of them + + +### Examples on extending the basic functionalities + +#### `configure( { : [ true | false ] } )` + +Disable logging errors/warnings to console, disallow duplicate ids. Example: + +```js +NW.Dom.configure( { LOGERRORS: false, IDS_DUPES: false } ); +``` +NOTE: NW.Dom.configure() without parameters return the current configuration. + +#### `registerCombinator( symbol, resolver )` + +Registers a new symbol and its matching resolver in the combinators table. Example: + +```js +NW.Dom.registerCombinator( '^', 'e.parentElement' ); +``` + +#### `registerOperator( symbol, resolver )` + +Registers a new symbol and its matching resolver in the attribute operators table. Example: + +```js +NW.Dom.registerOperator( '!=', { p1: '^', p2: '$', p3: 'false' } ); +``` + +#### `registerSelector( name, rexp, func )` + +Registers a new selector, the matching RE and the resolver function, in the selectors table. Example: + +```js +NW.Dom.registerSelector('Controls', /^\:(control)(.*)/i, + (function(global) { + return function(match, source, mode, callback) { + var status = true; + source = 'if(/^(button|input|select|textarea)/i.test(e.nodeName)){' + source + '}'; + return { 'source': source, 'status': status }; + }; + })(this)); +``` diff --git a/node_modules/@asamuzakjp/nwsapi/package.json b/node_modules/@asamuzakjp/nwsapi/package.json new file mode 100644 index 0000000000000000000000000000000000000000..6a44b9c71265e8ea0e7eed53c9d7dde8042daf51 --- /dev/null +++ b/node_modules/@asamuzakjp/nwsapi/package.json @@ -0,0 +1,43 @@ +{ + "name": "@asamuzakjp/nwsapi", + "version": "2.3.9", + "description": "Fast CSS Selectors API Engine", + "homepage": "http://javascript.nwbox.com/nwsapi/", + "main": "./src/nwsapi", + "keywords": [ + "css", + "css3", + "css4", + "matcher", + "selector" + ], + "licenses": [ + { + "type": "MIT", + "url": "http://javascript.nwbox.com/nwsapi/MIT-LICENSE" + } + ], + "license": "MIT", + "author": { + "name": "Diego Perini", + "email": "diego.perini@gmail.com", + "web": "http://www.iport.it/" + }, + "maintainers": [ + { + "name": "Diego Perini", + "email": "diego.perini@gmail.com", + "web": "http://www.iport.it/" + } + ], + "bugs": { + "url": "http://github.com/dperini/nwsapi/issues" + }, + "repository": { + "type": "git", + "url": "git://github.com/dperini/nwsapi.git" + }, + "scripts": { + "lint": "eslint ./src/nwsapi.js" + } +} diff --git a/node_modules/@asamuzakjp/nwsapi/src/nwsapi.js b/node_modules/@asamuzakjp/nwsapi/src/nwsapi.js new file mode 100644 index 0000000000000000000000000000000000000000..e118fd5e5320224396a78ce6e64f7795836d24a0 --- /dev/null +++ b/node_modules/@asamuzakjp/nwsapi/src/nwsapi.js @@ -0,0 +1,1855 @@ +/** + * Forked and modified from nwsapi@2.2.2 + * - Export to cjs only + * - Remove ./modules directory + * - Remove unused exported properties + * - Remove unused pseudo-classes + * - Remove Snapshot.root and resolve document.documentElement on runtime + * - Use `let` and `const` as much as possible + * - Use `===` and `!==` + * - Fix `:nth-of-type()` + * - Fix function source for :root, :target and :indeterminate pseudo-classes + * - Fix + * - Support complex selectors within `:is()` and `:not()` + * - Add ::slotted() and ::part() to pseudo-elements list + * - Add isContentEditable() function + * - Add createMatchingParensRegex() function from upstream + * - Invalidate cache for :has() pseudo class + * - Optimize some regular expressions + */ +/* + * Copyright (C) 2007-2019 Diego Perini + * All rights reserved. + * + * nwsapi.js - Fast CSS Selectors API Engine + * + * Author: Diego Perini + * Version: 2.2.0 + * Created: 20070722 + * Release: 20220901 + * + * License: + * http://javascript.nwbox.com/nwsapi/MIT-LICENSE + * Download: + * http://javascript.nwbox.com/nwsapi/nwsapi.js + */ + +(function Export(global, factory) { + 'use strict'; + module.exports = factory; +})(this, function Factory(global, Export) { + const version = 'nwsapi-2.2.2'; + + let doc = global.document; + + /** + * Generate a regex that matches a balanced set of parentheses. + * Outermost parentheses are excluded so any amount of children can be handled. + * See https://stackoverflow.com/a/35271017 for reference + * + * @param {number} depth + * @return {string} + */ + function createMatchingParensRegex(depth = 1) { + const out = '\\([^)(]*?(?:'.repeat(depth) + '\\([^)(]*?\\)' + '[^)(]*?)*?\\)'.repeat(depth); + // remove outermost escaped parens + return out.slice(2, out.length - 2); + } + + const CFG = { + // extensions + operators: '[~*^$|]=|=', + combinators: '[\\s>+~](?=[^>+~])' + }; + + const NOT = { + // not enclosed in double/single/parens/square + doubleEnc: '(?=(?:[^"]*"[^"]*")*[^"]*$)', + singleEnc: "(?=(?:[^']*'[^']*')*[^']*$)", + parensEnc: '(?![^\\x28]*\\x29)', + squareEnc: '(?![^\\x5b]*\\x5d)' + }; + + const REX = { + // regular expressions + hasEscapes: /\\/, + hexNumbers: /^[0-9a-f]/i, + escOrQuote: /^\\|[\x22\x27]/, + regExpChar: /(?:(?!\\)[\\^$.*+?()[\]{}|/])/g, + trimSpaces: /[\r\n\f]|^\s+|\s+$/g, + commaGroup: RegExp('(\\s{0,255},\\s{0,255})' + NOT.squareEnc + NOT.parensEnc, 'g'), + splitGroup: /((?:\x28[^\x29]{0,255}\x29|\[[^\]]{0,255}\]|\\.|[^,])+)/g, + fixEscapes: /\\([0-9a-f]{1,6}\s?|.)|([\x22\x27])/gi, + combineWSP: RegExp('\\s{1,255}' + NOT.singleEnc + NOT.doubleEnc, 'g'), + tabCharWSP: RegExp('(\\s?\\t{1,255}\\s?)' + NOT.singleEnc + NOT.doubleEnc, 'g'), + pseudosWSP: RegExp('\\s{1,255}([-+])\\s{1,255}' + NOT.squareEnc, 'g') + }; + + const STD = { + combinator: /\s?([>+~])\s?/g, + apimethods: /^(?:[a-z]+|\*)\|/i, + namespaces: /(\*|[a-z]+)\|[-a-z]+/i + }; + + const GROUPS = { + // pseudo-classes requiring parameters + logicalsel: '(is|where|matches|not|has)(?:\\x28\\s?(' + createMatchingParensRegex(3) + ')\\s?\\x29)', + treestruct: '(nth(?:-last)?(?:-child|-of-type))(?:\\x28\\s?(even|odd|(?:[-+]?\\d*)(?:n\\s?[-+]?\\s?\\d*)?)\\s?(?:\\x29|$))', + // pseudo-classes not requiring parameters + locationpc: '(any-link|link|visited|target)\\b', + structural: '(root|empty|(?:(?:first|last|only)(?:-child|-of-type)))\\b', + inputstate: '(enabled|disabled|read-(?:only|write)|placeholder-shown|default)\\b', + inputvalue: '(checked|indeterminate)\\b', + // pseudo-classes for parsing only selectors + pseudoNop: '(autofill|-webkit-autofill)\\b', + // pseudo-elements starting with single colon (:) + pseudoSng: '(after|before|first-letter|first-line)\\b', + // pseudo-elements starting with double colon (::) + pseudoDbl: ':(after|before|first-letter|first-line|selection|part|placeholder|slotted|-webkit-[-a-z0-9]{2,})\\b' + }; + + const Patterns = { + // pseudo-classes + treestruct: RegExp('^:(?:' + GROUPS.treestruct + ')(.*)', 'i'), + structural: RegExp('^:(?:' + GROUPS.structural + ')(.*)', 'i'), + inputstate: RegExp('^:(?:' + GROUPS.inputstate + ')(.*)', 'i'), + inputvalue: RegExp('^:(?:' + GROUPS.inputvalue + ')(.*)', 'i'), + locationpc: RegExp('^:(?:' + GROUPS.locationpc + ')(.*)', 'i'), + logicalsel: RegExp('^:(?:' + GROUPS.logicalsel + ')(.*)', 'i'), + pseudoNop: RegExp('^:(?:' + GROUPS.pseudoNop + ')(.*)', 'i'), + pseudoSng: RegExp('^:(?:' + GROUPS.pseudoSng + ')(.*)', 'i'), + pseudoDbl: RegExp('^:(?:' + GROUPS.pseudoDbl + ')(.*)', 'i'), + // combinator symbols + children: /^\s?>\s?(.*)/, + adjacent: /^\s?\+\s?(.*)/, + relative: /^\s?~\s?(.*)/, + ancestor: /^\s+(.*)/, + // universal & namespace + universal: /^\*(.*)/, + namespace: /^(\w+|\*)?\|(.*)/ + }; + + // emulate firefox error strings + const qsNotArgs = 'Not enough arguments'; + const qsInvalid = ' is not a valid selector'; + + // detect structural pseudo-classes in selectors + const reNthElem = /(:nth(?:-last)?-child)/i; + const reNthType = /(:nth(?:-last)?-of-type)/i; + + // placeholder for global regexp + let reOptimizer; + let reValidator; + + // special handling configuration flags + const Config = { + IDS_DUPES: true, + MIXEDCASE: true, + LOGERRORS: true, + VERBOSITY: true + }; + + let NAMESPACE; + let QUIRKS_MODE; + let HTML_DOCUMENT; + + const ATTR_STD_OPS = { + '=': 1, + '^=': 1, + '$=': 1, + '|=': 1, + '*=': 1, + '~=': 1 + }; + + const HTML_TABLE = { + accept: 1, + 'accept-charset': 1, + align: 1, + alink: 1, + axis: 1, + bgcolor: 1, + charset: 1, + checked: 1, + clear: 1, + codetype: 1, + color: 1, + compact: 1, + declare: 1, + defer: 1, + dir: 1, + direction: 1, + disabled: 1, + enctype: 1, + face: 1, + frame: 1, + hreflang: 1, + 'http-equiv': 1, + lang: 1, + language: 1, + link: 1, + media: 1, + method: 1, + multiple: 1, + nohref: 1, + noresize: 1, + noshade: 1, + nowrap: 1, + readonly: 1, + rel: 1, + rev: 1, + rules: 1, + scope: 1, + scrolling: 1, + selected: 1, + shape: 1, + target: 1, + text: 1, + type: 1, + valign: 1, + valuetype: 1, + vlink: 1 + }; + + const Combinators = {}; + + const Selectors = {}; + + const Operators = { + '=': { + p1: '^', + p2: '$', + p3: 'true' + }, + '^=': { + p1: '^', + p2: '', + p3: 'true' + }, + '$=': { + p1: '', + p2: '$', + p3: 'true' + }, + '*=': { + p1: '', + p2: '', + p3: 'true' + }, + '|=': { + p1: '^', + p2: '(-|$)', + p3: 'true' + }, + '~=': { + p1: '(^|\\s)', + p2: '(\\s|$)', + p3: 'true' + } + }; + + const concatCall = function (nodes, callback) { + let i = 0; + const l = nodes.length; + const list = Array(l); + while (l > i) { + if (callback(list[i] = nodes[i]) === false) { + break; + } + ++i; + } + return list; + }; + + const concatList = function (list, nodes) { + let i = -1; + let l = nodes.length; + while (l--) { + list[list.length] = nodes[++i]; + } + return list; + }; + + let hasDupes = false; + + const documentOrder = function (a, b) { + if (!hasDupes && a === b) { + hasDupes = true; + return 0; + } + return a.compareDocumentPosition(b) & 4 ? -1 : 1; + }; + + const unique = function (nodes) { + let i = 0; + let j = -1; + let l = nodes.length + 1; + const list = []; + while (--l) { + if (nodes[i++] === nodes[i]) { + continue; + } + list[++j] = nodes[i - 1]; + } + hasDupes = false; + return list; + }; + + // check context for mixed content + const hasMixedCaseTagNames = function (context) { + const api = 'getElementsByTagNameNS'; + + // current host context (ownerDocument) + context = context.ownerDocument || context; + + // documentElement (root) element namespace or default html/xhtml namespace + const ns = context.documentElement && context.documentElement.namespaceURI + ? context.documentElement.namespaceURI + : 'http://www.w3.org/1999/xhtml'; + + // checking the number of non HTML nodes in the document + return (context[api]('*', '*').length - context[api](ns, '*').length) > 0; + }; + + // check if the document type is HTML + const isHTML = function (node) { + const doc = node.ownerDocument || node; + return doc.nodeType === 9 && doc.contentType === 'text/html'; + }; + + // convert single codepoint to UTF-16 encoding + const codePointToUTF16 = function (codePoint) { + // out of range, use replacement character + if (codePoint < 1 || codePoint > 0x10ffff || + (codePoint > 0xd7ff && codePoint < 0xe000)) { + return '\\ufffd'; + } + // javascript strings are UTF-16 encoded + if (codePoint < 0x10000) { + const lowHex = '000' + codePoint.toString(16); + return '\\u' + lowHex.substr(lowHex.length - 4); + } + // supplementary high + low surrogates + return '\\u' + (((codePoint - 0x10000) >> 0x0a) + 0xd800).toString(16) + + '\\u' + (((codePoint - 0x10000) % 0x400) + 0xdc00).toString(16); + }; + + // convert single codepoint to string + const stringFromCodePoint = function (codePoint) { + // out of range, use replacement character + if (codePoint < 1 || codePoint > 0x10ffff || + (codePoint > 0xd7ff && codePoint < 0xe000)) { + return '\ufffd'; + } + if (codePoint < 0x10000) { + return String.fromCharCode(codePoint); + } + return String.fromCodePoint(codePoint); + }; + + // convert escape sequence in a CSS string or identifier + // to javascript string with javascript escape sequences + const convertEscapes = function (str) { + return REX.hasEscapes.test(str) + ? str.replace(REX.fixEscapes, function (substring, p1, p2) { + // unescaped " or ' + return p2 + ? '\\' + p2 + // javascript strings are UTF-16 encoded + : REX.hexNumbers.test(p1) + ? codePointToUTF16(parseInt(p1, 16)) + // \' \" + : REX.escOrQuote.test(p1) + ? substring + // \g \h \. \# etc + : p1; + }) + : str; + }; + + // convert escape sequence in a CSS string or identifier + // to javascript string with characters representations + const unescapeIdentifier = function (str) { + return REX.hasEscapes.test(str) + ? str.replace(REX.fixEscapes, function (substring, p1, p2) { + // unescaped " or ' + return p2 || (REX.hexNumbers.test(p1) + ? stringFromCodePoint(parseInt(p1, 16)) + // \' \" + : REX.escOrQuote.test(p1) + ? substring + // \g \h \. \# etc + : p1); + }) + : str; + }; + + // empty set + const none = []; + + // cached lambdas + const matchLambdas = {}; + const selectLambdas = {}; + + // cached resolvers + let matchResolvers = {}; + let selectResolvers = {}; + + const method = { + '#': 'getElementById', + '*': 'getElementsByTagName', + '|': 'getElementsByTagNameNS', + '.': 'getElementsByClassName' + }; + + // find duplicate ids using iterative walk + const byIdRaw = function (id, context) { + let node = context; + const nodes = []; + let next = node.firstElementChild; + while ((node = next)) { + node.id === id && nodes.push(node); + if ((next = node.firstElementChild || node.nextElementSibling)) { + continue; + } + while (!next && (node = node.parentElement) && node !== context) { + next = node.nextElementSibling; + } + } + return nodes; + }; + + // context agnostic getElementById + const byId = function (id, context) { + let e; + const api = method['#']; + + // duplicates id allowed + if (Config.IDS_DUPES === false) { + if (api in context) { + e = context[api](id); + return e ? [e] : none; + } + } else if ('all' in context) { + if ((e = context.all[id])) { + if (e.nodeType === 1) { + return e.getAttribute('id') !== id ? [] : [e]; + } else if (id === 'length') { + e = context[api](id); + return e ? [e] : none; + } + const nodes = []; + for (let i = 0, l = e.length; l > i; ++i) { + if (e[i].id === id) { + nodes.push(e[i]); + } + } + return nodes.length ? nodes : none; + } else { + return none; + } + } + + return byIdRaw(id, context); + }; + + // context agnostic getElementsByTagName + const byTag = function (tag, context) { + let e; + let nodes; + const api = method['*']; + + // DOCUMENT_NODE (9) & ELEMENT_NODE (1) + if (api in context) { + return Array.prototype.slice.call(context[api](tag)); + } else { + tag = tag.toLowerCase(); + // DOCUMENT_FRAGMENT_NODE (11) + if ((e = context.firstElementChild)) { + if (!(e.nextElementSibling || tag === '*' || e.localName === tag)) { + return Array.prototype.slice.call(e[api](tag)); + } else { + nodes = []; + do { + if (tag === '*' || e.localName === tag) { + nodes.push(e); + } + concatList(nodes, e[api](tag)); + } while ((e = e.nextElementSibling)); + } + } else { + nodes = none; + } + } + return nodes; + }; + + // context agnostic getElementsByClassName + const byClass = function (cls, context) { + let e; + let nodes; + const api = method['.']; + let reCls; + // DOCUMENT_NODE (9) & ELEMENT_NODE (1) + if (api in context) { + return Array.prototype.slice.call(context[api](cls)); + } else { + // DOCUMENT_FRAGMENT_NODE (11) + if ((e = context.firstElementChild)) { + reCls = RegExp('(^|\\s)' + cls + '(\\s|$)', QUIRKS_MODE ? 'i' : ''); + if (!(e.nextElementSibling || reCls.test(e.className))) { + return Array.prototype.slice.call(e[api](cls)); + } else { + nodes = []; + do { + if (reCls.test(e.className)) { + nodes.push(e); + } + concatList(nodes, e[api](cls)); + } while ((e = e.nextElementSibling)); + } + } else nodes = none; + } + return nodes; + }; + + const compat = { + '#': function (c, n) { + REX.hasEscapes.test(n) && (n = unescapeIdentifier(n)); + return function (e, f) { + return byId(n, c); + }; + }, + '*': function (c, n) { + REX.hasEscapes.test(n) && (n = unescapeIdentifier(n)); + return function (e, f) { + return byTag(n, c); + }; + }, + '|': function (c, n) { + REX.hasEscapes.test(n) && (n = unescapeIdentifier(n)); + return function (e, f) { + return byTag(n, c); + }; + }, + '.': function (c, n) { + REX.hasEscapes.test(n) && (n = unescapeIdentifier(n)); + return function (e, f) { + return byClass(n, c); + }; + } + }; + + // namespace aware hasAttribute + // helper for XML/XHTML documents + const hasAttributeNS = function (e, name) { + let i; + let l; + const attr = e.getAttributeNames(); + name = RegExp(':?' + name + '$', HTML_DOCUMENT ? 'i' : ''); + for (i = 0, l = attr.length; l > i; ++i) { + if (name.test(attr[i])) { + return true; + } + } + return false; + }; + + // fast resolver for the :nth-child() and :nth-last-child() pseudo-classes + const nthElement = (function () { + let idx = 0; + let len = 0; + let set = 0; + let parent; + let parents = []; + let nodes = []; + return function (element, dir) { + // ensure caches are emptied after each run, invoking with dir = 2 + if (dir === 2) { + idx = 0; len = 0; set = 0; nodes = []; parents = []; parent = undefined; + return -1; + } + let e, i, j, k, l; + if (parent === element.parentElement) { + i = set; j = idx; l = len; + } else { + l = parents.length; + parent = element.parentElement; + for (i = -1, j = 0, k = l - 1; l > j; ++j, --k) { + if (parents[j] === parent) { + i = j; + break; + } + if (parents[k] === parent) { + i = k; + break; + } + } + if (i < 0) { + parents[i = l] = parent; + l = 0; nodes[i] = []; + e = (parent && parent.firstElementChild) || element; + while (e) { + nodes[i][l] = e; + if (e === element) { + j = l; + } + e = e.nextElementSibling; + ++l; + } + set = i; idx = 0; len = l; + if (l < 2) { + return l; + } + } else { + l = nodes[i].length; + set = i; + } + } + if (element !== nodes[i][j] && element !== nodes[i][j = 0]) { + for (j = 0, e = nodes[i], k = l - 1; l > j; ++j, --k) { + if (e[j] === element) { + break; + } + if (e[k] === element) { + j = k; + break; + } + } + } + idx = j + 1; len = l; + return dir ? l - j : idx; + }; + })(); + + // fast resolver for the :nth-of-type() and :nth-last-of-type() pseudo-classes + const nthOfType = (function () { + let idx = 0; + let len = 0; + let set = 0; + let parent; + let parents = []; + let nodes = []; + return function (element, dir) { + // ensure caches are emptied after each run, invoking with dir = 2 + if (dir === 2) { + idx = 0; len = 0; set = 0; nodes = []; parents = []; parent = undefined; + return -1; + } + const name = element.localName; + const nsURI = element.namespaceURI; + if (nsURI !== 'http://www.w3.org/1999/xhtml') { + idx = 0; len = 0; set = 0; nodes = []; parents = []; parent = undefined; + } + let e; + let i; + let j; + let k; + let l; + if (nodes[set] && nodes[set][name] && parent === element.parentElement) { + i = set; + j = idx; + l = len; + } else { + l = parents.length; + parent = element.parentElement; + for (i = -1, j = 0, k = l - 1; l > j; ++j, --k) { + if (parents[j] === parent) { + i = j; + break; + } + if (parents[k] === parent) { + i = k; + break; + } + } + if (i < 0 || !nodes[i][name]) { + parents[i = l] = parent; + nodes[i] || (nodes[i] = Object()); + l = 0; nodes[i][name] = []; + e = (parent && parent.firstElementChild) || element; + while (e) { + if (e === element) { + j = l; + } + if (e.localName === name && e.namespaceURI === nsURI) { + nodes[i][name][l] = e; + ++l; + } + e = e.nextElementSibling; + } + set = i; idx = j; len = l; + if (l < 2) { + return l; + } + } else { + l = nodes[i][name].length; + set = i; + } + } + if (element !== nodes[i][name][j] && element !== nodes[i][name][j = 0]) { + for (j = 0, e = nodes[i][name], k = l - 1; l > j; ++j, --k) { + if (e[j] === element) { + break; + } + if (e[k] === element) { + j = k; + break; + } + } + } + idx = j + 1; len = l; + return dir ? l - j : idx; + }; + })(); + + // check if the node is the target + const isTarget = function (node) { + const doc = node.ownerDocument || node; + const { hash } = new URL(doc.URL); + if (node.id && hash === `#${node.id}` && doc.contains(node)) { + return true; + } + return false; + }; + + // check if node is indeterminate + const isIndeterminate = function (node) { + if ((node.indeterminate && node.localName === 'input' && + node.type === 'checkbox') || + (node.localName === 'progress' && !node.hasAttribute('value'))) { + return true; + } + if (node.localName === 'input' && node.type === 'radio' && + !node.hasAttribute('checked')) { + const nodeName = node.name; + let parent = node.parentNode; + while (parent) { + if (parent.localName === 'form') { + break; + } + parent = parent.parentNode; + } + if (!parent) { + const doc = node.ownerDocument; + parent = doc.documentElement; + } + const items = parent.getElementsByTagName('input'); + const l = items.length; + let checked; + for (let i = 0; i < l; i++) { + const item = items[i]; + if (item.getAttribute('type') === 'radio') { + if (nodeName) { + if (item.getAttribute('name') === nodeName) { + checked = !!item.checked; + } + } else if (!item.hasAttribute('name')) { + checked = !!item.checked; + } + if (checked) { + break; + } + } + } + if (!checked) { + return true; + } + } + return false; + }; + + // check if node content is editable + const isContentEditable = function (node) { + let attrValue = 'inherit'; + if (node.hasAttribute('contenteditable')) { + attrValue = node.getAttribute('contenteditable'); + } + switch (attrValue) { + case '': + case 'plaintext-only': + case 'true': + return true; + case 'false': + return false; + default: + if (node.parentNode && node.parentNode.nodeType === 1) { + return isContentEditable(node.parentNode); + } + return false; + } + }; + + // build validation regexps used by the engine + const setIdentifierSyntax = function () { + // + // NOTE: SPECIAL CASES IN CSS SYNTAX PARSING RULES + // + // The https://drafts.csswg.org/css-syntax/#typedef-eof-token + // allow mangled|unclosed selector syntax at the end of selectors strings + // + // Literal equivalent hex representations of the characters: " ' ` ] ) + // + // \\x22 = " - double quotes \\x5b = [ - open square bracket + // \\x27 = ' - single quote \\x5d = ] - closed square bracket + // \\x60 = ` - back tick \\x28 = ( - open round parens + // \\x5c = \ - back slash \\x29 = ) - closed round parens + // + // using hex format prevents false matches of opened/closed instances + // pairs, coloring breakage and other editors highlightning problems. + // + + // @see https://drafts.csswg.org/css-syntax-3/#ident-token-diagram + const nonascii = '[^\\x00-\\x9f]'; + const esctoken = '\\\\(?:[^\\r\\n\\f\\da-f]|[\\da-f]{1,6}\\s{0,255})'; + const identifier = + '(?:--|-?(?:[a-z_]|' + nonascii + '|' + esctoken + '))' + + '(?:[\\w-]|' + nonascii + '|' + esctoken + ')*'; + + const pseudonames = '[-\\w]+'; + const pseudoparms = '(?:[-+]?\\d*)(?:n\\s?[-+]?\\s?\\d*)'; + const doublequote = '"[^"\\\\]*(?:\\\\.[^"\\\\]*)*(?:"|$)'; + const singlequote = "'[^'\\\\]*(?:\\\\.[^'\\\\]*)*(?:'|$)"; + + const attrparser = identifier + '|' + doublequote + '|' + singlequote; + + const attrvalues = '([\\x22\\x27]?)((?!\\3)*|(?:\\\\?.)*?)(?:\\3|$)'; + + const attributes = + '\\[' + + // attribute presence + '(?:\\*\\|)?\\s?(' + identifier + '(?::' + identifier + ')?)\\s?' + + '(?:(' + CFG.operators + ')\\s?(?:' + attrparser + '))?' + + // attribute case sensitivity + '(?:\\s?\\b(i))?\\s?' + + '(?:\\]|$)'; + + const attrmatcher = attributes.replace(attrparser, attrvalues); + + const pseudoclass = + '(?:\\x28\\s*' + + '(?:' + pseudoparms + '?)?|' + + // universal * & + // namespace *|* + '[*|]|' + + '(?:' + + '(?::' + pseudonames + '(?:\\x28' + pseudoparms + '?(?:\\x29|$))?)|' + + '(?:[.#]?' + identifier + ')|' + + '(?:' + attributes + ')' + + ')+|' + + '\\s?[>+~]\\s?|' + + '\\s?,\\s?|' + + '\\s|' + + '\\x29|$' + + ')*'; + + const standardValidator = + '(?=\\s?[^>+~(){}<])' + + '(?:' + + // universal * & + // namespace *|* + '\\*|\\||' + + '(?:[.#]?' + identifier + ')+|' + + '(?:' + attributes + ')+|' + + '(?:::?' + pseudonames + pseudoclass + ')|' + + '(?:\\s?' + CFG.combinators + '\\s?)|' + + '\\s?,\\s?|' + + '\\s?' + + ')+'; + + // the following global RE is used to return the + // deepest localName in selector strings and then + // use it to retrieve all possible matching nodes + // that will be filtered by compiled resolvers + reOptimizer = RegExp( + '(?:([.:#*]?)(' + identifier + ')' + + '(?::[-\\w]+|\\[[^\\]]+(?:\\]|$)|\\x28[^\\x29]+(?:\\x29|$))*' + + ')$', 'i'); + + // global + reValidator = RegExp(standardValidator, 'gi'); + + Patterns.id = RegExp('^#(' + identifier + ')(.*)', 'i'); + Patterns.tagName = RegExp('^(' + identifier + ')(.*)', 'i'); + Patterns.className = RegExp('^\\.(' + identifier + ')(.*)', 'i'); + Patterns.attribute = RegExp('^(?:' + attrmatcher + ')(.*)'); + }; + + // configure the engine to use special handling + const configure = function (option, clear) { + if (typeof option === 'string') { + return !!Config[option]; + } + if (typeof option !== 'object') { + return Config; + } + for (const i in option) { + Config[i] = !!option[i]; + } + // clear lambda cache + if (clear) { + matchResolvers = {}; + selectResolvers = {}; + } + setIdentifierSyntax(); + return true; + }; + + // centralized error and exceptions handling + const emit = function (message, proto) { + let err; + if (Config.VERBOSITY) { + if (global[proto]) { + err = new global[proto](message); + } else { + err = new global.DOMException(message, 'SyntaxError'); + } + throw err; + } + if (Config.LOGERRORS && console && console.log) { + console.log(message); + } + }; + + // passed to resolvers + const Snapshot = { + doc: null, + from: null, + byTag: null, + first: null, + match: null, + ancestor: null, + nthOfType: null, + nthElement: null, + hasAttributeNS: null, + isTarget: null, + isIndeterminate: null, + isContentEditable: null + }; + + // context + let lastContext; + + const switchContext = function (context, force) { + const oldDoc = doc; + doc = context.ownerDocument || context; + if (force || oldDoc !== doc) { + // force a new check for each document change + // performed before the next select operation + HTML_DOCUMENT = isHTML(doc); + QUIRKS_MODE = HTML_DOCUMENT && doc.compatMode.indexOf('CSS') < 0; + NAMESPACE = doc.documentElement && doc.documentElement.namespaceURI; + Snapshot.doc = doc; + } + Snapshot.from = context; + return context; + }; + + // selector + let lastMatched; + let lastSelected; + + const F_INIT = '"use strict";return function Resolver(c,f,x,r)'; + + const S_HEAD = 'var e,n,o,j=r.length-1,k=-1'; + const M_HEAD = 'var e,n,o'; + + const S_LOOP = 'main:while((e=c[++k]))'; + const N_LOOP = 'main:while((e=c.item(++k)))'; + const M_LOOP = 'e=c;'; + + const S_BODY = 'r[++j]=c[k];'; + const N_BODY = 'r[++j]=c.item(k);'; + const M_BODY = ''; + + const S_TAIL = 'continue main;'; + const M_TAIL = 'r=true;'; + + const S_TEST = 'if(f(c[k])){break main;}'; + const N_TEST = 'if(f(c.item(k))){break main;}'; + const M_TEST = 'f(c);'; + + let S_VARS = []; + let M_VARS = []; + + // build conditional code to check components of selector strings + const compileSelector = function (expression, source, mode, callback) { + // N is the negation pseudo-class flag + // D is the default inverted negation flag + let a; + let b; + let n; + let f; + let name; + let NS; + const N = ''; + const D = '!'; + let compat; + let expr; + let match; + let result; + let status; + let symbol; + let test; + let type; + let selector = expression; + let vars; + + // original 'select' or 'match' selector string before normalization + const selectorString = mode ? lastSelected : lastMatched; + + // isolate selector combinators/components and normalize whitespace + selector = selector.replace(STD.combinator, '$1'); // .replace(STD.whitespace, ' '); + + let selectorRecursion = true; + while (selector) { + // get namespace prefix if present or get first char of selector + symbol = STD.apimethods.test(selector) ? '|' : selector[0]; + + switch (symbol) { + // universal resolver + case '*': + match = selector.match(Patterns.universal); + if (N === '!') { + source = 'if(' + N + 'true' + '){' + source + '}'; + } + break; + // id resolver + case '#': + match = selector.match(Patterns.id); + source = 'if(' + N + '(/^' + match[1] + '$/.test(e.getAttribute("id"))' + + ')){' + source + '}'; + break; + // class name resolver + case '.': + match = selector.match(Patterns.className); + compat = (QUIRKS_MODE ? 'i' : '') + '.test(e.getAttribute("class"))'; + source = 'if(' + N + '(/(^|\\s)' + match[1] + '(\\s|$)/' + compat + + ')){' + source + '}'; + break; + // tag name resolver + case (/[_a-z]/i.test(symbol) ? symbol : undefined): + match = selector.match(Patterns.tagName); + source = 'if(' + N + '(e.localName' + + (Config.MIXEDCASE || hasMixedCaseTagNames(doc) + ? '=="' + match[1].toLowerCase() + '"' + : '=="' + match[1].toUpperCase() + '"') + + ')){' + source + '}'; + break; + // namespace resolver + case '|': + match = selector.match(Patterns.namespace); + if (match[1] === '*') { + source = 'if(' + N + 'true){' + source + '}'; + } else if (!match[1]) { + source = 'if(' + N + '(!e.namespaceURI)){' + source + '}'; + } else if (typeof match[1] === 'string' && doc.documentElement && + doc.documentElement.prefix === match[1]) { + source = 'if(' + N + '(e.namespaceURI=="' + NAMESPACE + '")){' + source + '}'; + } else { + emit('\'' + selectorString + '\'' + qsInvalid); + } + break; + // attributes resolver + case '[': + match = selector.match(Patterns.attribute); + NS = match[0].match(STD.namespaces); + name = match[1]; + expr = name.split(':'); + expr = expr.length === 2 ? expr[1] : expr[0]; + if (match[2] && !(test = Operators[match[2]])) { + emit('\'' + selectorString + '\'' + qsInvalid); + return ''; + } + if (match[4] === '') { + test = match[2] === '~=' + ? { p1: '^\\s', p2: '+$', p3: 'true' } + : match[2] in ATTR_STD_OPS && match[2] !== '~=' + ? { p1: '^', p2: '$', p3: 'true' } + : test; + } else if (match[2] === '~=' && match[4].includes(' ')) { + // whitespace separated list but value contains space + source = 'if(' + N + 'false){' + source + '}'; + break; + } else if (match[4]) { + match[4] = convertEscapes(match[4]).replace(REX.regExpChar, '\\$&'); + } + type = match[5] === 'i' || (HTML_DOCUMENT && HTML_TABLE[expr.toLowerCase()]) + ? 'i' + : ''; + source = + 'if(' + N + '(' + + (!match[2] + ? (NS ? 's.hasAttributeNS(e,"' + name + '")' : 'e.hasAttribute&&e.hasAttribute("' + name + '")') + : !match[4] && ATTR_STD_OPS[match[2]] && match[2] !== '~=' + ? 'e.getAttribute&&e.getAttribute("' + name + '")==""' + : '(/' + test.p1 + match[4] + test.p2 + '/' + type + ').test(e.getAttribute&&e.getAttribute("' + name + '"))==' + test.p3) + + ')){' + source + '}'; + break; + // *** General sibling combinator + // E ~ F (F relative sibling of E) + case '~': + match = selector.match(Patterns.relative); + source = 'n=e;while((e=e.previousElementSibling)){' + source + '}e=n;'; + break; + // *** Adjacent sibling combinator + // E + F (F adiacent sibling of E) + case '+': + match = selector.match(Patterns.adjacent); + source = 'n=e;if((e=e.previousElementSibling)){' + source + '}e=n;'; + break; + // *** Descendant combinator + // E F (E ancestor of F) + case '\x09': + case '\x20': + match = selector.match(Patterns.ancestor); + source = 'n=e;while((e=e.parentElement)){' + source + '}e=n;'; + break; + // *** Child combinator + // E > F (F children of E) + case '>': + match = selector.match(Patterns.children); + source = 'n=e;if((e=e.parentElement)){' + source + '}e=n;'; + break; + // *** user supplied combinators extensions + case (symbol in Combinators ? symbol : undefined): + // for other registered combinators extensions + match[match.length - 1] = '*'; + source = Combinators[symbol](match) + source; + break; + // *** tree-structural pseudo-classes + // :root, :empty, :first-child, :last-child, :only-child, :first-of-type, :last-of-type, :only-of-type + case ':': + if ((match = selector.match(Patterns.structural))) { + match[1] = match[1].toLowerCase(); + switch (match[1]) { + case 'root': + // there can only be one :root element, so exit the loop once found + source = 'if(' + N + '(e===s.doc.documentElement)){' + source + (mode ? 'break main;' : '') + '}'; + break; + case 'empty': + // matches elements that don't contain elements or text nodes + source = 'n=e.firstChild;while(n&&!(/1|3/).test(n.nodeType)){n=n.nextSibling}if(' + D + 'n){' + source + '}'; + break; + // *** child-indexed pseudo-classes + // :first-child, :last-child, :only-child + case 'only-child': + source = 'if(' + N + '(!e.nextElementSibling&&!e.previousElementSibling)){' + source + '}'; + break; + case 'last-child': + source = 'if(' + N + '(!e.nextElementSibling)){' + source + '}'; + break; + case 'first-child': + source = 'if(' + N + '(!e.previousElementSibling)){' + source + '}'; + break; + // *** typed child-indexed pseudo-classes + // :only-of-type, :last-of-type, :first-of-type + case 'only-of-type': + source = 'o=e.localName;' + + 'n=e;while((n=n.nextElementSibling)&&n.localName!=o);if(!n){' + + 'n=e;while((n=n.previousElementSibling)&&n.localName!=o);}if(' + D + 'n){' + source + '}'; + break; + case 'last-of-type': + source = 'n=e;o=e.localName;while((n=n.nextElementSibling)&&n.localName!=o);if(' + D + 'n){' + source + '}'; + break; + case 'first-of-type': + source = 'n=e;o=e.localName;while((n=n.previousElementSibling)&&n.localName!=o);if(' + D + 'n){' + source + '}'; + break; + default: + emit('\'' + selectorString + '\'' + qsInvalid); + } + // *** child-indexed & typed child-indexed pseudo-classes + // :nth-child, :nth-of-type, :nth-last-child, :nth-last-of-type + } else if ((match = selector.match(Patterns.treestruct))) { + match[1] = match[1].toLowerCase(); + switch (match[1]) { + case 'nth-child': + case 'nth-of-type': + case 'nth-last-child': + case 'nth-last-of-type': + expr = /-of-type/i.test(match[1]); + if (match[1] && match[2]) { + type = /last/i.test(match[1]); + if (match[2] === 'n') { + source = 'if(' + N + 'true){' + source + '}'; + break; + } else if (match[2] === '1') { + test = type ? 'next' : 'previous'; + source = expr + ? 'n=e;o=e.localName;' + + 'while((n=n.' + test + 'ElementSibling)&&n.localName!=o);if(' + D + 'n){' + source + '}' + : 'if(' + N + '!e.' + test + 'ElementSibling){' + source + '}'; + break; + } else if (match[2] === 'even' || match[2] === '2n0' || match[2] === '2n+0' || match[2] === '2n') { + test = 'n%2==0'; + } else if (match[2] === 'odd' || match[2] === '2n1' || match[2] === '2n+1') { + test = 'n%2==1'; + } else { + f = /n/i.test(match[2]); + n = match[2].split('n'); + a = parseInt(n[0], 10) || 0; + b = parseInt(n[1], 10) || 0; + if (n[0] === '-') { + a = -1; + } + if (n[0] === '+') { + a = +1; + } + test = (b ? '(n' + (b > 0 ? '-' : '+') + Math.abs(b) + ')' : 'n') + '%' + a + '==0'; + test = a >= +1 + ? (f + ? 'n>' + (b - 1) + (Math.abs(a) !== 1 + ? '&&' + test + : '') + : 'n==' + a) + : a <= -1 + ? (f + ? 'n<' + (b + 1) + (Math.abs(a) !== 1 + ? '&&' + test + : '') + : 'n==' + a) + : a === 0 + ? (n[0] + ? 'n==' + b + : 'n>' + (b - 1)) + : 'false'; + } + expr = expr ? 'OfType' : 'Element'; + type = type ? 'true' : 'false'; + source = 'n=s.nth' + expr + '(e,' + type + ');if(' + N + '(' + test + ')){' + source + '}'; + } else { + emit('\'' + selectorString + '\'' + qsInvalid); + } + break; + default: + emit('\'' + selectorString + '\'' + qsInvalid); + } + // *** logical combination pseudo-classes + // :is( s1, [ s2, ... ]), :not( s1, [ s2, ... ]) + } else if ((match = selector.match(Patterns.logicalsel))) { + match[1] = match[1].toLowerCase(); + expr = match[2].replace(REX.CommaGroup, ',').replace(REX.TrimSpaces, ''); + switch (match[1]) { + // FIXME: + case 'is': + case 'where': + case 'matches': + source = 'if(s.match("' + expr.replace(/\x22/g, '\\"') + '",e)){' + source + '}'; + break; + // FIXME: + case 'not': + source = 'if(!s.match("' + expr.replace(/\x22/g, '\\"') + '",e)){' + source + '}'; + break; + // FIXME: + case 'has': + // clear cache + matchResolvers = {}; + source = 'if(e.querySelector(":scope ' + expr.replace(/\x22/g, '\\"') + '")){' + source + '}'; + break; + default: + emit('\'' + selectorString + '\'' + qsInvalid); + } + // *** location pseudo-classes + // :any-link, :link, :visited, :target + } else if ((match = selector.match(Patterns.locationpc))) { + match[1] = match[1].toLowerCase(); + switch (match[1]) { + case 'any-link': + source = 'if(' + N + '(/^a|area$/i.test(e.localName)&&e.hasAttribute("href")||e.visited)){' + source + '}'; + break; + case 'link': + source = 'if(' + N + '(/^a|area$/i.test(e.localName)&&e.hasAttribute("href"))){' + source + '}'; + break; + // FIXME: + case 'visited': + source = 'if(' + N + '(/^a|area$/i.test(e.localName)&&e.hasAttribute("href")&&e.visited)){' + source + '}'; + break; + case 'target': + source = 'if(s.isTarget(e)){' + source + '}'; + break; + default: + emit('\'' + selectorString + '\'' + qsInvalid); + } + // *** user interface and form pseudo-classes + // :enabled, :disabled, :read-only, :read-write, :placeholder-shown, :default + } else if ((match = selector.match(Patterns.inputstate))) { + match[1] = match[1].toLowerCase(); + switch (match[1]) { + // FIXME: lacks custom element support + case 'enabled': + source = 'if((("form" in e||/^optgroup$/i.test(e.localName))&&"disabled" in e &&e.disabled===false' + + ')){' + source + '}'; + break; + // FIXME: lacks custom element support + case 'disabled': + // https://html.spec.whatwg.org/#enabling-and-disabling-form-controls:-the-disabled-attribute + source = 'if((("form" in e||/^optgroup$/i.test(e.localName))&&"disabled" in e)){' + + // F is true if any of the fieldset elements in the ancestry chain has the disabled attribute specified + // L is true if the first legend element of the fieldset contains the element + 'var x=0,N=[],F=false,L=false;' + + 'if(!(/^(optgroup|option)$/i.test(e.localName))){' + + 'n=e.parentElement;' + + 'while(n){' + + 'if(n.localName==="fieldset"){' + + 'N[x++]=n;' + + 'if(n.disabled===true){' + + 'F=true;' + + 'break;' + + '}' + + '}' + + 'n=n.parentElement;' + + '}' + + 'for(var x=0;x + // assert: e.type is in double-colon format, like ::after + } else if ((match = selector.match(Patterns.pseudoDbl))) { + source = 'if(e.element&&e.type.toLowerCase()=="' + + match[0].toLowerCase() + '"){e=e.element;' + source + '}'; + // placeholder for parsed only no-op selectors + } else if ((match = selector.match(Patterns.pseudoNop))) { + source = 'if(' + N + 'false' + '){' + source + '}'; + } else { + // reset + expr = false; + status = false; + // process registered selector extensions + for (expr in Selectors) { + if ((match = selector.match(Selectors[expr].Expression))) { + result = Selectors[expr].Callback(match, source, mode, callback); + if ('match' in result) { + match = result.match; + } + vars = result.modvar; + if (mode) { + // add extra select() vars + vars && !S_VARS.includes(vars) && S_VARS.push(vars); + } else { + // add extra match() vars + vars && M_VARS.includes(vars) && M_VARS.push(vars); + } + // extension source code + source = result.source; + // extension status code + status = result.status; + // break on status error + if (status) { break; } + } + } + if (!status) { + emit('unknown pseudo-class selector \'' + selector + '\''); + return ''; + } + if (!expr) { + emit('unknown token in selector \'' + selector + '\''); + return ''; + } + } + break; + default: + selectorRecursion = false; + emit('\'' + selectorString + '\'' + qsInvalid); + } + // end of switch symbol + if (!selectorRecursion) { + break; + } + if (!match) { + emit('\'' + selectorString + '\'' + qsInvalid); + return ''; + } + + // pop last component + selector = match.pop(); + } + // end of while selector + + return source; + }; + + // compile groups or single selector strings into + // executable functions for matching or selecting + const compile = function (selector, mode, callback) { + let head = ''; let loop = ''; let macro = ''; let source = ''; let vars = ''; + + // 'mode' can be boolean or null + // true = select / false = match + // null to use collection.item() + switch (mode) { + case true: + if (selectLambdas[selector]) { + return selectLambdas[selector]; + } + macro = S_BODY + (callback ? S_TEST : '') + S_TAIL; + head = S_HEAD; + loop = S_LOOP; + break; + case false: + if (matchLambdas[selector]) { + return matchLambdas[selector]; + } + macro = M_BODY + (callback ? M_TEST : '') + M_TAIL; + head = M_HEAD; + loop = M_LOOP; + break; + case null: + if (selectLambdas[selector]) { + return selectLambdas[selector]; + } + macro = N_BODY + (callback ? N_TEST : '') + S_TAIL; + head = S_HEAD; + loop = N_LOOP; + break; + default: + } + + source = compileSelector(selector, macro, mode, callback); + + loop += (mode || mode === null) ? '{' + source + '}' : source; + + if ((mode || mode === null) && selector.includes(':nth')) { + loop += reNthElem.test(selector) ? 's.nthElement(null, 2);' : ''; + loop += reNthType.test(selector) ? 's.nthOfType(null, 2);' : ''; + } + + if (S_VARS[0] || M_VARS[0]) { + vars = ',' + (S_VARS.join(',') || M_VARS.join(',')); + S_VARS = []; + M_VARS = []; + } + + const factory = Function('s', F_INIT + '{' + head + vars + ';' + loop + 'return r;}')(Snapshot); + + return mode || mode === null ? (selectLambdas[selector] = factory) : (matchLambdas[selector] = factory); + }; + + // optimize selectors avoiding duplicated checks + const optimize = function (selector, token) { + const index = token.index; + const length = token[1].length + token[2].length; + return selector.slice(0, index) + + (' >+~'.indexOf(selector.charAt(index - 1)) > -1 + ? (':['.indexOf(selector.charAt(index + length + 1)) > -1 + ? '*' + : '') + : '') + selector.slice(index + length - (token[1] === '*' ? 1 : 0)); + }; + + // prepare factory resolvers and closure collections + const collect = function (selectors, context, callback) { + let i; + let l; + const seen = { }; + let token = ['', '*', '*']; + const optimized = selectors; + const factory = []; + const htmlset = []; + const nodeset = []; + let results = []; + let type; + + for (i = 0, l = selectors.length; l > i; ++i) { + if (!seen[selectors[i]] && (seen[selectors[i]] = true)) { + type = selectors[i].match(reOptimizer); + if (type && type[1] !== ':' && (token = type)) { + token[1] || (token[1] = '*'); + optimized[i] = optimize(optimized[i], token); + } else { + token = ['', '*', '*']; + } + } + + nodeset[i] = token[1] + token[2]; + htmlset[i] = compat[token[1]](context, token[2]); + factory[i] = compile(optimized[i], true, null); + + factory[i] + ? factory[i](htmlset[i](), callback, context, results) + : results.concat(htmlset[i]()); + } + + if (l > 1) { + results.sort(documentOrder); + hasDupes && (results = unique(results)); + } + + return { + callback, + context, + factory, + htmlset, + nodeset, + results + }; + }; + + // replace ':scope' pseudo-class with element references + const makeref = function (selectors, element) { + // DOCUMENT_NODE (9) + if (element.nodeType === 9) { + element = element.documentElement; + } + + return selectors.replace(/:scope/gi, + element.localName + + (element.id ? '#' + element.id : '') + + (element.className ? '.' + element.classList[0] : '')); + }; + + const matchAssert = function (f, element, callback) { + let r = false; + for (let i = 0, l = f.length; l > i; ++i) { + f[i](element, callback, null, false) && (r = true); + } + return r; + }; + + const matchCollect = function (selectors, callback) { + const f = []; + for (let i = 0, l = selectors.length; l > i; ++i) { + f[i] = compile(selectors[i], false, callback); + } + return { factory: f }; + }; + + // equivalent of w3c 'matches' method + const match = function _matches(selectors, element, callback) { + let expressions; + + if (element && !/:has\(/.test(selectors) && matchResolvers[selectors]) { + return matchAssert(matchResolvers[selectors].factory, element, callback); + } + + lastMatched = selectors; + + // arguments validation + if (arguments.length === 0) { + emit(qsNotArgs, 'TypeError'); + return Config.VERBOSITY ? undefined : false; + } else if (arguments[0] === '') { + emit('\'\'' + qsInvalid); + return Config.VERBOSITY ? undefined : false; + } + + // input NULL or UNDEFINED + if (typeof selectors !== 'string') { + selectors = '' + selectors; + } + + if ((/:scope/i).test(selectors)) { + selectors = makeref(selectors, element); + } + + // normalize input string + const parsed = selectors + .replace(/\0|\\$/g, '\ufffd') + .replace(REX.combineWSP, '\x20') + .replace(REX.pseudosWSP, '$1') + .replace(REX.tabCharWSP, '\t') + .replace(REX.commaGroup, ',') + .replace(REX.trimSpaces, ''); + + // parse, validate and split possible compound selectors + if ((expressions = parsed.match(reValidator)) && expressions.join('') === parsed) { + expressions = parsed.match(REX.splitGroup); + if (parsed[parsed.length - 1] === ',') { + emit(qsInvalid); + return Config.VERBOSITY ? undefined : false; + } + } else { + emit('\'' + selectors + '\'' + qsInvalid); + return Config.VERBOSITY ? undefined : false; + } + + matchResolvers[selectors] = matchCollect(expressions, callback); + + return matchAssert(matchResolvers[selectors].factory, element, callback); + }; + + // equivalent of w3c 'closest' method + const ancestor = function _closest(selectors, element, callback) { + if ((/:scope/i).test(selectors)) { + selectors = makeref(selectors, element); + } + + while (element) { + if (match(selectors, element, callback)) break; + element = element.parentElement; + } + return element; + }; + + // equivalent of w3c 'querySelectorAll' method + const select = function _querySelectorAll(selectors, context, callback) { + let expressions; let nodes = []; let resolver; + + context || (context = doc); + + if (selectors) { + if ((resolver = selectResolvers[selectors])) { + if (resolver.context === context && resolver.callback === callback) { + const f = resolver.factory; + const h = resolver.htmlset; + const n = resolver.nodeset; + if (n.length > 1) { + const l = n.length; + for (let i = 0, l = n.length, list; l > i; ++i) { + list = compat[n[i][0]](context, n[i].slice(1))(); + if (f[i] !== null) { + f[i](list, callback, context, nodes); + } else { + nodes = nodes.concat(list); + } + } + if (l > 1 && nodes.length > 1) { + nodes.sort(documentOrder); + hasDupes && (nodes = unique(nodes)); + } + } else { + if (f[0]) { + nodes = f[0](h[0](), callback, context, nodes); + } else { + nodes = h[0](); + } + } + return typeof callback === 'function' + ? concatCall(nodes, callback) + : nodes; + } + } + } + + lastSelected = selectors; + + // arguments validation + if (arguments.length === 0) { + emit(qsNotArgs, 'TypeError'); + return Config.VERBOSITY ? undefined : none; + } else if (arguments[0] === '') { + emit('\'\'' + qsInvalid); + return Config.VERBOSITY ? undefined : none; + } else if (lastContext !== context) { + lastContext = switchContext(context); + } + + // input NULL or UNDEFINED + if (typeof selectors !== 'string') { + selectors = '' + selectors; + } + + if ((/:scope/i).test(selectors)) { + selectors = makeref(selectors, context); + } + + // normalize input string + const parsed = selectors + .replace(/\0|\\$/g, '\ufffd') + .replace(REX.combineWSP, '\x20') + .replace(REX.pseudosWSP, '$1') + .replace(REX.tabCharWSP, '\t') + .replace(REX.commaGroup, ',') + .replace(REX.trimSpaces, ''); + + // parse, validate and split possible compound selectors + if ((expressions = parsed.match(reValidator)) && expressions.join('') === parsed) { + expressions = parsed.match(REX.splitGroup); + if (parsed[parsed.length - 1] === ',') { + emit(qsInvalid); + return Config.VERBOSITY ? undefined : false; + } + } else { + emit('\'' + selectors + '\'' + qsInvalid); + return Config.VERBOSITY ? undefined : false; + } + + // save/reuse factory and closure collection + selectResolvers[selectors] = collect(expressions, context, callback); + + nodes = selectResolvers[selectors].results; + + return typeof callback === 'function' + ? concatCall(nodes, callback) + : nodes; + }; + + // equivalent of w3c 'querySelector' method + const first = function _querySelector(selectors, context, callback) { + if (arguments.length === 0) { + emit(qsNotArgs, 'TypeError'); + } + return select(selectors, context, typeof callback === 'function' + ? function firstMatch(element) { + callback(element); + return false; + } + : function firstMatch() { + return false; + } + )[0] || null; + }; + + // execute the engine initialization code + const initialize = function (d) { + setIdentifierSyntax(); + lastContext = switchContext(d, true); + Snapshot.doc = doc; + Snapshot.from = doc; + Snapshot.byTag = byTag; + Snapshot.first = first; + Snapshot.match = match; + Snapshot.ancestor = ancestor; + Snapshot.nthOfType = nthOfType; + Snapshot.nthElement = nthElement; + Snapshot.hasAttributeNS = hasAttributeNS; + Snapshot.isTarget = isTarget; + Snapshot.isIndeterminate = isIndeterminate; + Snapshot.isContentEditable = isContentEditable; + }; + + initialize(doc); + + // public exported methods/objects + const Dom = { + // exported engine methods + Version: version, + configure, + match, + closest: ancestor, + first, + select + }; + + return Dom; +}); diff --git a/node_modules/@babel/code-frame/LICENSE b/node_modules/@babel/code-frame/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..f31575ec773bb199aeb7c0d0f1612cfe1c7038f1 --- /dev/null +++ b/node_modules/@babel/code-frame/LICENSE @@ -0,0 +1,22 @@ +MIT License + +Copyright (c) 2014-present Sebastian McKenzie and other contributors + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/node_modules/@babel/code-frame/README.md b/node_modules/@babel/code-frame/README.md new file mode 100644 index 0000000000000000000000000000000000000000..7160755113aaa771161a0626a7a9c7b6a99ac8d8 --- /dev/null +++ b/node_modules/@babel/code-frame/README.md @@ -0,0 +1,19 @@ +# @babel/code-frame + +> Generate errors that contain a code frame that point to source locations. + +See our website [@babel/code-frame](https://babeljs.io/docs/babel-code-frame) for more information. + +## Install + +Using npm: + +```sh +npm install --save-dev @babel/code-frame +``` + +or using yarn: + +```sh +yarn add @babel/code-frame --dev +``` diff --git a/node_modules/@babel/code-frame/lib/index.js b/node_modules/@babel/code-frame/lib/index.js new file mode 100644 index 0000000000000000000000000000000000000000..9c5db40650ce1a02ad39ad125e1b32f01e4eea64 --- /dev/null +++ b/node_modules/@babel/code-frame/lib/index.js @@ -0,0 +1,217 @@ +'use strict'; + +Object.defineProperty(exports, '__esModule', { value: true }); + +var picocolors = require('picocolors'); +var jsTokens = require('js-tokens'); +var helperValidatorIdentifier = require('@babel/helper-validator-identifier'); + +function isColorSupported() { + return (typeof process === "object" && (process.env.FORCE_COLOR === "0" || process.env.FORCE_COLOR === "false") ? false : picocolors.isColorSupported + ); +} +const compose = (f, g) => v => f(g(v)); +function buildDefs(colors) { + return { + keyword: colors.cyan, + capitalized: colors.yellow, + jsxIdentifier: colors.yellow, + punctuator: colors.yellow, + number: colors.magenta, + string: colors.green, + regex: colors.magenta, + comment: colors.gray, + invalid: compose(compose(colors.white, colors.bgRed), colors.bold), + gutter: colors.gray, + marker: compose(colors.red, colors.bold), + message: compose(colors.red, colors.bold), + reset: colors.reset + }; +} +const defsOn = buildDefs(picocolors.createColors(true)); +const defsOff = buildDefs(picocolors.createColors(false)); +function getDefs(enabled) { + return enabled ? defsOn : defsOff; +} + +const sometimesKeywords = new Set(["as", "async", "from", "get", "of", "set"]); +const NEWLINE$1 = /\r\n|[\n\r\u2028\u2029]/; +const BRACKET = /^[()[\]{}]$/; +let tokenize; +const JSX_TAG = /^[a-z][\w-]*$/i; +const getTokenType = function (token, offset, text) { + if (token.type === "name") { + const tokenValue = token.value; + if (helperValidatorIdentifier.isKeyword(tokenValue) || helperValidatorIdentifier.isStrictReservedWord(tokenValue, true) || sometimesKeywords.has(tokenValue)) { + return "keyword"; + } + if (JSX_TAG.test(tokenValue) && (text[offset - 1] === "<" || text.slice(offset - 2, offset) === " defs[type](str)).join("\n"); + } else { + highlighted += value; + } + } + return highlighted; +} + +let deprecationWarningShown = false; +const NEWLINE = /\r\n|[\n\r\u2028\u2029]/; +function getMarkerLines(loc, source, opts, startLineBaseZero) { + const startLoc = Object.assign({ + column: 0, + line: -1 + }, loc.start); + const endLoc = Object.assign({}, startLoc, loc.end); + const { + linesAbove = 2, + linesBelow = 3 + } = opts || {}; + const startLine = startLoc.line - startLineBaseZero; + const startColumn = startLoc.column; + const endLine = endLoc.line - startLineBaseZero; + const endColumn = endLoc.column; + let start = Math.max(startLine - (linesAbove + 1), 0); + let end = Math.min(source.length, endLine + linesBelow); + if (startLine === -1) { + start = 0; + } + if (endLine === -1) { + end = source.length; + } + const lineDiff = endLine - startLine; + const markerLines = {}; + if (lineDiff) { + for (let i = 0; i <= lineDiff; i++) { + const lineNumber = i + startLine; + if (!startColumn) { + markerLines[lineNumber] = true; + } else if (i === 0) { + const sourceLength = source[lineNumber - 1].length; + markerLines[lineNumber] = [startColumn, sourceLength - startColumn + 1]; + } else if (i === lineDiff) { + markerLines[lineNumber] = [0, endColumn]; + } else { + const sourceLength = source[lineNumber - i].length; + markerLines[lineNumber] = [0, sourceLength]; + } + } + } else { + if (startColumn === endColumn) { + if (startColumn) { + markerLines[startLine] = [startColumn, 0]; + } else { + markerLines[startLine] = true; + } + } else { + markerLines[startLine] = [startColumn, endColumn - startColumn]; + } + } + return { + start, + end, + markerLines + }; +} +function codeFrameColumns(rawLines, loc, opts = {}) { + const shouldHighlight = opts.forceColor || isColorSupported() && opts.highlightCode; + const startLineBaseZero = (opts.startLine || 1) - 1; + const defs = getDefs(shouldHighlight); + const lines = rawLines.split(NEWLINE); + const { + start, + end, + markerLines + } = getMarkerLines(loc, lines, opts, startLineBaseZero); + const hasColumns = loc.start && typeof loc.start.column === "number"; + const numberMaxWidth = String(end + startLineBaseZero).length; + const highlightedLines = shouldHighlight ? highlight(rawLines) : rawLines; + let frame = highlightedLines.split(NEWLINE, end).slice(start, end).map((line, index) => { + const number = start + 1 + index; + const paddedNumber = ` ${number + startLineBaseZero}`.slice(-numberMaxWidth); + const gutter = ` ${paddedNumber} |`; + const hasMarker = markerLines[number]; + const lastMarkerLine = !markerLines[number + 1]; + if (hasMarker) { + let markerLine = ""; + if (Array.isArray(hasMarker)) { + const markerSpacing = line.slice(0, Math.max(hasMarker[0] - 1, 0)).replace(/[^\t]/g, " "); + const numberOfMarkers = hasMarker[1] || 1; + markerLine = ["\n ", defs.gutter(gutter.replace(/\d/g, " ")), " ", markerSpacing, defs.marker("^").repeat(numberOfMarkers)].join(""); + if (lastMarkerLine && opts.message) { + markerLine += " " + defs.message(opts.message); + } + } + return [defs.marker(">"), defs.gutter(gutter), line.length > 0 ? ` ${line}` : "", markerLine].join(""); + } else { + return ` ${defs.gutter(gutter)}${line.length > 0 ? ` ${line}` : ""}`; + } + }).join("\n"); + if (opts.message && !hasColumns) { + frame = `${" ".repeat(numberMaxWidth + 1)}${opts.message}\n${frame}`; + } + if (shouldHighlight) { + return defs.reset(frame); + } else { + return frame; + } +} +function index (rawLines, lineNumber, colNumber, opts = {}) { + if (!deprecationWarningShown) { + deprecationWarningShown = true; + const message = "Passing lineNumber and colNumber is deprecated to @babel/code-frame. Please use `codeFrameColumns`."; + if (process.emitWarning) { + process.emitWarning(message, "DeprecationWarning"); + } else { + const deprecationError = new Error(message); + deprecationError.name = "DeprecationWarning"; + console.warn(new Error(message)); + } + } + colNumber = Math.max(colNumber, 0); + const location = { + start: { + column: colNumber, + line: lineNumber + } + }; + return codeFrameColumns(rawLines, location, opts); +} + +exports.codeFrameColumns = codeFrameColumns; +exports.default = index; +exports.highlight = highlight; +//# sourceMappingURL=index.js.map diff --git a/node_modules/@babel/code-frame/lib/index.js.map b/node_modules/@babel/code-frame/lib/index.js.map new file mode 100644 index 0000000000000000000000000000000000000000..6b85ae4957d58fb4115f8713e9c66639dffb50d9 --- /dev/null +++ b/node_modules/@babel/code-frame/lib/index.js.map @@ -0,0 +1 @@ +{"version":3,"file":"index.js","sources":["../src/defs.ts","../src/highlight.ts","../src/index.ts"],"sourcesContent":["import picocolors, { createColors } from \"picocolors\";\nimport type { Colors, Formatter } from \"picocolors/types\";\n\nexport function isColorSupported() {\n return (\n // See https://github.com/alexeyraspopov/picocolors/issues/62\n typeof process === \"object\" &&\n (process.env.FORCE_COLOR === \"0\" || process.env.FORCE_COLOR === \"false\")\n ? false\n : picocolors.isColorSupported\n );\n}\n\nexport type InternalTokenType =\n | \"keyword\"\n | \"capitalized\"\n | \"jsxIdentifier\"\n | \"punctuator\"\n | \"number\"\n | \"string\"\n | \"regex\"\n | \"comment\"\n | \"invalid\";\n\ntype UITokens = \"gutter\" | \"marker\" | \"message\";\n\nexport type Defs = Record;\n\nconst compose: (f: (gv: U) => V, g: (v: T) => U) => (v: T) => V =\n (f, g) => v =>\n f(g(v));\n\n/**\n * Styles for token types.\n */\nfunction buildDefs(colors: Colors): Defs {\n return {\n keyword: colors.cyan,\n capitalized: colors.yellow,\n jsxIdentifier: colors.yellow,\n punctuator: colors.yellow,\n number: colors.magenta,\n string: colors.green,\n regex: colors.magenta,\n comment: colors.gray,\n invalid: compose(compose(colors.white, colors.bgRed), colors.bold),\n\n gutter: colors.gray,\n marker: compose(colors.red, colors.bold),\n message: compose(colors.red, colors.bold),\n\n reset: colors.reset,\n };\n}\n\nconst defsOn = buildDefs(createColors(true));\nconst defsOff = buildDefs(createColors(false));\n\nexport function getDefs(enabled: boolean): Defs {\n return enabled ? defsOn : defsOff;\n}\n","import type { Token as JSToken, JSXToken } from \"js-tokens\";\nimport jsTokens from \"js-tokens\";\n// We inline this package\n// eslint-disable-next-line import/no-extraneous-dependencies\nimport * as charCodes from \"charcodes\";\n\nimport {\n isStrictReservedWord,\n isKeyword,\n} from \"@babel/helper-validator-identifier\";\n\nimport { getDefs, type InternalTokenType } from \"./defs.ts\";\n\n/**\n * Names that are always allowed as identifiers, but also appear as keywords\n * within certain syntactic productions.\n *\n * https://tc39.es/ecma262/#sec-keywords-and-reserved-words\n *\n * `target` has been omitted since it is very likely going to be a false\n * positive.\n */\nconst sometimesKeywords = new Set([\"as\", \"async\", \"from\", \"get\", \"of\", \"set\"]);\n\ntype Token = {\n type: InternalTokenType | \"uncolored\";\n value: string;\n};\n\n/**\n * RegExp to test for newlines in terminal.\n */\nconst NEWLINE = /\\r\\n|[\\n\\r\\u2028\\u2029]/;\n\n/**\n * RegExp to test for the three types of brackets.\n */\nconst BRACKET = /^[()[\\]{}]$/;\n\nlet tokenize: (\n text: string,\n) => Generator<{ type: InternalTokenType | \"uncolored\"; value: string }>;\n\nif (process.env.BABEL_8_BREAKING) {\n /**\n * Get the type of token, specifying punctuator type.\n */\n const getTokenType = function (\n token: JSToken | JSXToken,\n ): InternalTokenType | \"uncolored\" {\n if (token.type === \"IdentifierName\") {\n const tokenValue = token.value;\n if (\n isKeyword(tokenValue) ||\n isStrictReservedWord(tokenValue, true) ||\n sometimesKeywords.has(tokenValue)\n ) {\n return \"keyword\";\n }\n\n const firstChar = tokenValue.charCodeAt(0);\n if (firstChar < 128) {\n // ASCII characters\n if (\n firstChar >= charCodes.uppercaseA &&\n firstChar <= charCodes.uppercaseZ\n ) {\n return \"capitalized\";\n }\n } else {\n const firstChar = String.fromCodePoint(tokenValue.codePointAt(0));\n if (firstChar !== firstChar.toLowerCase()) {\n return \"capitalized\";\n }\n }\n }\n\n if (token.type === \"Punctuator\" && BRACKET.test(token.value)) {\n return \"uncolored\";\n }\n\n if (token.type === \"Invalid\" && token.value === \"@\") {\n return \"punctuator\";\n }\n\n switch (token.type) {\n case \"NumericLiteral\":\n return \"number\";\n\n case \"StringLiteral\":\n case \"JSXString\":\n case \"NoSubstitutionTemplate\":\n return \"string\";\n\n case \"RegularExpressionLiteral\":\n return \"regex\";\n\n case \"Punctuator\":\n case \"JSXPunctuator\":\n return \"punctuator\";\n\n case \"MultiLineComment\":\n case \"SingleLineComment\":\n return \"comment\";\n\n case \"Invalid\":\n case \"JSXInvalid\":\n return \"invalid\";\n\n case \"JSXIdentifier\":\n return \"jsxIdentifier\";\n\n default:\n return \"uncolored\";\n }\n };\n\n /**\n * Turn a string of JS into an array of objects.\n */\n tokenize = function* (text: string): Generator {\n for (const token of jsTokens(text, { jsx: true })) {\n switch (token.type) {\n case \"TemplateHead\":\n yield { type: \"string\", value: token.value.slice(0, -2) };\n yield { type: \"punctuator\", value: \"${\" };\n break;\n\n case \"TemplateMiddle\":\n yield { type: \"punctuator\", value: \"}\" };\n yield { type: \"string\", value: token.value.slice(1, -2) };\n yield { type: \"punctuator\", value: \"${\" };\n break;\n\n case \"TemplateTail\":\n yield { type: \"punctuator\", value: \"}\" };\n yield { type: \"string\", value: token.value.slice(1) };\n break;\n\n default:\n yield {\n type: getTokenType(token),\n value: token.value,\n };\n }\n }\n };\n} else {\n /**\n * RegExp to test for what seems to be a JSX tag name.\n */\n const JSX_TAG = /^[a-z][\\w-]*$/i;\n\n // The token here is defined in js-tokens@4. However we don't bother\n // typing it since the whole block will be removed in Babel 8\n const getTokenType = function (token: any, offset: number, text: string) {\n if (token.type === \"name\") {\n const tokenValue = token.value;\n if (\n isKeyword(tokenValue) ||\n isStrictReservedWord(tokenValue, true) ||\n sometimesKeywords.has(tokenValue)\n ) {\n return \"keyword\";\n }\n\n if (\n JSX_TAG.test(tokenValue) &&\n (text[offset - 1] === \"<\" || text.slice(offset - 2, offset) === \" defs[type as InternalTokenType](str))\n .join(\"\\n\");\n } else {\n highlighted += value;\n }\n }\n\n return highlighted;\n}\n","import { getDefs, isColorSupported } from \"./defs.ts\";\nimport { highlight } from \"./highlight.ts\";\n\nexport { highlight };\n\nlet deprecationWarningShown = false;\n\ntype Location = {\n column: number;\n line: number;\n};\n\ntype NodeLocation = {\n end?: Location;\n start: Location;\n};\n\nexport interface Options {\n /** Syntax highlight the code as JavaScript for terminals. default: false */\n highlightCode?: boolean;\n /** The number of lines to show above the error. default: 2 */\n linesAbove?: number;\n /** The number of lines to show below the error. default: 3 */\n linesBelow?: number;\n /** The line number corresponding to the first line in `rawLines`. default: 1 */\n startLine?: number;\n /**\n * Forcibly syntax highlight the code as JavaScript (for non-terminals);\n * overrides highlightCode.\n * default: false\n */\n forceColor?: boolean;\n /**\n * Pass in a string to be displayed inline (if possible) next to the\n * highlighted location in the code. If it can't be positioned inline,\n * it will be placed above the code frame.\n * default: nothing\n */\n message?: string;\n}\n\n/**\n * RegExp to test for newlines in terminal.\n */\n\nconst NEWLINE = /\\r\\n|[\\n\\r\\u2028\\u2029]/;\n\n/**\n * Extract what lines should be marked and highlighted.\n */\n\ntype MarkerLines = Record;\n\nfunction getMarkerLines(\n loc: NodeLocation,\n source: string[],\n opts: Options,\n startLineBaseZero: number,\n): {\n start: number;\n end: number;\n markerLines: MarkerLines;\n} {\n const startLoc: Location = {\n column: 0,\n line: -1,\n ...loc.start,\n };\n const endLoc: Location = {\n ...startLoc,\n ...loc.end,\n };\n const { linesAbove = 2, linesBelow = 3 } = opts || {};\n const startLine = startLoc.line - startLineBaseZero;\n const startColumn = startLoc.column;\n const endLine = endLoc.line - startLineBaseZero;\n const endColumn = endLoc.column;\n\n let start = Math.max(startLine - (linesAbove + 1), 0);\n let end = Math.min(source.length, endLine + linesBelow);\n\n if (startLine === -1) {\n start = 0;\n }\n\n if (endLine === -1) {\n end = source.length;\n }\n\n const lineDiff = endLine - startLine;\n const markerLines: MarkerLines = {};\n\n if (lineDiff) {\n for (let i = 0; i <= lineDiff; i++) {\n const lineNumber = i + startLine;\n\n if (!startColumn) {\n markerLines[lineNumber] = true;\n } else if (i === 0) {\n const sourceLength = source[lineNumber - 1].length;\n\n markerLines[lineNumber] = [startColumn, sourceLength - startColumn + 1];\n } else if (i === lineDiff) {\n markerLines[lineNumber] = [0, endColumn];\n } else {\n const sourceLength = source[lineNumber - i].length;\n\n markerLines[lineNumber] = [0, sourceLength];\n }\n }\n } else {\n if (startColumn === endColumn) {\n if (startColumn) {\n markerLines[startLine] = [startColumn, 0];\n } else {\n markerLines[startLine] = true;\n }\n } else {\n markerLines[startLine] = [startColumn, endColumn - startColumn];\n }\n }\n\n return { start, end, markerLines };\n}\n\nexport function codeFrameColumns(\n rawLines: string,\n loc: NodeLocation,\n opts: Options = {},\n): string {\n const shouldHighlight =\n opts.forceColor || (isColorSupported() && opts.highlightCode);\n const startLineBaseZero = (opts.startLine || 1) - 1;\n const defs = getDefs(shouldHighlight);\n\n const lines = rawLines.split(NEWLINE);\n const { start, end, markerLines } = getMarkerLines(\n loc,\n lines,\n opts,\n startLineBaseZero,\n );\n const hasColumns = loc.start && typeof loc.start.column === \"number\";\n\n const numberMaxWidth = String(end + startLineBaseZero).length;\n\n const highlightedLines = shouldHighlight ? highlight(rawLines) : rawLines;\n\n let frame = highlightedLines\n .split(NEWLINE, end)\n .slice(start, end)\n .map((line, index) => {\n const number = start + 1 + index;\n const paddedNumber = ` ${number + startLineBaseZero}`.slice(\n -numberMaxWidth,\n );\n const gutter = ` ${paddedNumber} |`;\n const hasMarker = markerLines[number];\n const lastMarkerLine = !markerLines[number + 1];\n if (hasMarker) {\n let markerLine = \"\";\n if (Array.isArray(hasMarker)) {\n const markerSpacing = line\n .slice(0, Math.max(hasMarker[0] - 1, 0))\n .replace(/[^\\t]/g, \" \");\n const numberOfMarkers = hasMarker[1] || 1;\n\n markerLine = [\n \"\\n \",\n defs.gutter(gutter.replace(/\\d/g, \" \")),\n \" \",\n markerSpacing,\n defs.marker(\"^\").repeat(numberOfMarkers),\n ].join(\"\");\n\n if (lastMarkerLine && opts.message) {\n markerLine += \" \" + defs.message(opts.message);\n }\n }\n return [\n defs.marker(\">\"),\n defs.gutter(gutter),\n line.length > 0 ? ` ${line}` : \"\",\n markerLine,\n ].join(\"\");\n } else {\n return ` ${defs.gutter(gutter)}${line.length > 0 ? ` ${line}` : \"\"}`;\n }\n })\n .join(\"\\n\");\n\n if (opts.message && !hasColumns) {\n frame = `${\" \".repeat(numberMaxWidth + 1)}${opts.message}\\n${frame}`;\n }\n\n if (shouldHighlight) {\n return defs.reset(frame);\n } else {\n return frame;\n }\n}\n\n/**\n * Create a code frame, adding line numbers, code highlighting, and pointing to a given position.\n */\n\nexport default function (\n rawLines: string,\n lineNumber: number,\n colNumber?: number | null,\n opts: Options = {},\n): string {\n if (!deprecationWarningShown) {\n deprecationWarningShown = true;\n\n const message =\n \"Passing lineNumber and colNumber is deprecated to @babel/code-frame. Please use `codeFrameColumns`.\";\n\n if (process.emitWarning) {\n // A string is directly supplied to emitWarning, because when supplying an\n // Error object node throws in the tests because of different contexts\n process.emitWarning(message, \"DeprecationWarning\");\n } else {\n const deprecationError = new Error(message);\n deprecationError.name = \"DeprecationWarning\";\n console.warn(new Error(message));\n }\n }\n\n colNumber = Math.max(colNumber, 0);\n\n const location: NodeLocation = {\n start: { column: colNumber, line: lineNumber },\n };\n\n return codeFrameColumns(rawLines, location, opts);\n}\n"],"names":["isColorSupported","process","env","FORCE_COLOR","picocolors","compose","f","g","v","buildDefs","colors","keyword","cyan","capitalized","yellow","jsxIdentifier","punctuator","number","magenta","string","green","regex","comment","gray","invalid","white","bgRed","bold","gutter","marker","red","message","reset","defsOn","createColors","defsOff","getDefs","enabled","sometimesKeywords","Set","NEWLINE","BRACKET","tokenize","JSX_TAG","getTokenType","token","offset","text","type","tokenValue","value","isKeyword","isStrictReservedWord","has","test","slice","firstChar","String","fromCodePoint","codePointAt","toLowerCase","match","jsTokens","default","exec","matchToToken","index","highlight","defs","highlighted","split","map","str","join","deprecationWarningShown","getMarkerLines","loc","source","opts","startLineBaseZero","startLoc","Object","assign","column","line","start","endLoc","end","linesAbove","linesBelow","startLine","startColumn","endLine","endColumn","Math","max","min","length","lineDiff","markerLines","i","lineNumber","sourceLength","codeFrameColumns","rawLines","shouldHighlight","forceColor","highlightCode","lines","hasColumns","numberMaxWidth","highlightedLines","frame","paddedNumber","hasMarker","lastMarkerLine","markerLine","Array","isArray","markerSpacing","replace","numberOfMarkers","repeat","colNumber","emitWarning","deprecationError","Error","name","console","warn","location"],"mappings":"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"} \ No newline at end of file diff --git a/node_modules/@babel/code-frame/package.json b/node_modules/@babel/code-frame/package.json new file mode 100644 index 0000000000000000000000000000000000000000..1f21a379df39479bc38dedc221ef6d18927cf0df --- /dev/null +++ b/node_modules/@babel/code-frame/package.json @@ -0,0 +1,32 @@ +{ + "name": "@babel/code-frame", + "version": "7.29.7", + "description": "Generate errors that contain a code frame that point to source locations.", + "author": "The Babel Team (https://babel.dev/team)", + "homepage": "https://babel.dev/docs/en/next/babel-code-frame", + "bugs": "https://github.com/babel/babel/issues?utf8=%E2%9C%93&q=is%3Aissue+is%3Aopen", + "license": "MIT", + "publishConfig": { + "access": "public" + }, + "repository": { + "type": "git", + "url": "https://github.com/babel/babel.git", + "directory": "packages/babel-code-frame" + }, + "main": "./lib/index.js", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "devDependencies": { + "charcodes": "^0.2.0", + "import-meta-resolve": "^4.1.0", + "strip-ansi": "^4.0.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "type": "commonjs" +} \ No newline at end of file diff --git a/node_modules/@babel/compat-data/LICENSE b/node_modules/@babel/compat-data/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..f31575ec773bb199aeb7c0d0f1612cfe1c7038f1 --- /dev/null +++ b/node_modules/@babel/compat-data/LICENSE @@ -0,0 +1,22 @@ +MIT License + +Copyright (c) 2014-present Sebastian McKenzie and other contributors + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/node_modules/@babel/compat-data/README.md b/node_modules/@babel/compat-data/README.md new file mode 100644 index 0000000000000000000000000000000000000000..c19189872ec7139957f70c5f933ca0572db8d6a1 --- /dev/null +++ b/node_modules/@babel/compat-data/README.md @@ -0,0 +1,19 @@ +# @babel/compat-data + +> The compat-data to determine required Babel plugins + +See our website [@babel/compat-data](https://babeljs.io/docs/babel-compat-data) for more information. + +## Install + +Using npm: + +```sh +npm install --save @babel/compat-data +``` + +or using yarn: + +```sh +yarn add @babel/compat-data +``` diff --git a/node_modules/@babel/compat-data/corejs2-built-ins.js b/node_modules/@babel/compat-data/corejs2-built-ins.js new file mode 100644 index 0000000000000000000000000000000000000000..ed19e0b8a4ffd6bf191602c04d7c9308038f5562 --- /dev/null +++ b/node_modules/@babel/compat-data/corejs2-built-ins.js @@ -0,0 +1,2 @@ +// Todo (Babel 8): remove this file as Babel 8 drop support of core-js 2 +module.exports = require("./data/corejs2-built-ins.json"); diff --git a/node_modules/@babel/compat-data/corejs3-shipped-proposals.js b/node_modules/@babel/compat-data/corejs3-shipped-proposals.js new file mode 100644 index 0000000000000000000000000000000000000000..7909b8c46d3717def2168c9a1e8cc6a068b29541 --- /dev/null +++ b/node_modules/@babel/compat-data/corejs3-shipped-proposals.js @@ -0,0 +1,2 @@ +// Todo (Babel 8): remove this file now that it is included in babel-plugin-polyfill-corejs3 +module.exports = require("./data/corejs3-shipped-proposals.json"); diff --git a/node_modules/@babel/compat-data/data/corejs2-built-ins.json b/node_modules/@babel/compat-data/data/corejs2-built-ins.json new file mode 100644 index 0000000000000000000000000000000000000000..c7fd1889bddc30127e94be2bc035794866cd3871 --- /dev/null +++ b/node_modules/@babel/compat-data/data/corejs2-built-ins.json @@ -0,0 +1,2120 @@ +{ + "es6.array.copy-within": { + "chrome": "45", + "opera": "32", + "edge": "12", + "firefox": "32", + "safari": "9", + "node": "4", + "deno": "1", + "ios": "9", + "samsung": "5", + "rhino": "1.7.13", + "opera_mobile": "32", + "electron": "0.31" + }, + "es6.array.every": { + "chrome": "5", + "opera": "10.10", + "edge": "12", + "firefox": "2", + "safari": "3.1", + "node": "0.4", + "deno": "1", + "ie": "9", + "android": "4", + "ios": "6", + "phantom": "1.9", + "samsung": "1", + "rhino": "1.7.13", + "opera_mobile": "10.1", + "electron": "0.20" + }, + "es6.array.fill": { + "chrome": "45", + "opera": "32", + "edge": "12", + "firefox": "31", + "safari": "7.1", + "node": "4", + "deno": "1", + "ios": "8", + "samsung": "5", + "rhino": "1.7.13", + "opera_mobile": "32", + "electron": "0.31" + }, + "es6.array.filter": { + "chrome": "51", + "opera": "38", + "edge": "13", + "firefox": "48", + "safari": "10", + "node": "6.5", + "deno": "1", + "ios": "10", + "samsung": "5", + "rhino": "1.9", + "opera_mobile": "41", + "electron": "1.2" + }, + "es6.array.find": { + "chrome": "45", + "opera": "32", + "edge": "12", + "firefox": "25", + "safari": "7.1", + "node": "4", + "deno": "1", + "ios": "8", + "samsung": "5", + "rhino": "1.7.13", + "opera_mobile": "32", + "electron": "0.31" + }, + "es6.array.find-index": { + "chrome": "45", + "opera": "32", + "edge": "12", + "firefox": "25", + "safari": "7.1", + "node": "4", + "deno": "1", + "ios": "8", + "samsung": "5", + "rhino": "1.7.13", + "opera_mobile": "32", + "electron": "0.31" + }, + "es7.array.flat-map": { + "chrome": "69", + "opera": "56", + "edge": "79", + "firefox": "62", + "safari": "12", + "node": "11", + "deno": "1", + "ios": "12", + "samsung": "10", + "rhino": "1.7.15", + "opera_mobile": "48", + "electron": "4.0" + }, + "es6.array.for-each": { + "chrome": "5", + "opera": "10.10", + "edge": "12", + "firefox": "2", + "safari": "3.1", + "node": "0.4", + "deno": "1", + "ie": "9", + "android": "4", + "ios": "6", + "phantom": "1.9", + "samsung": "1", + "rhino": "1.7.13", + "opera_mobile": "10.1", + "electron": "0.20" + }, + "es6.array.from": { + "chrome": "51", + "opera": "38", + "edge": "15", + "firefox": "36", + "safari": "10", + "node": "6.5", + "deno": "1", + "ios": "10", + "samsung": "5", + "rhino": "1.7.15", + "opera_mobile": "41", + "electron": "1.2" + }, + "es7.array.includes": { + "chrome": "47", + "opera": "34", + "edge": "14", + "firefox": "102", + "safari": "10", + "node": "6", + "deno": "1", + "ios": "10", + "samsung": "5", + "rhino": "1.8", + "opera_mobile": "34", + "electron": "0.36" + }, + "es6.array.index-of": { + "chrome": "5", + "opera": "10.10", + "edge": "12", + "firefox": "2", + "safari": "3.1", + "node": "0.4", + "deno": "1", + "ie": "9", + "android": "4", + "ios": "6", + "phantom": "1.9", + "samsung": "1", + "rhino": "1.7.13", + "opera_mobile": "10.1", + "electron": "0.20" + }, + "es6.array.is-array": { + "chrome": "5", + "opera": "10.50", + "edge": "12", + "firefox": "4", + "safari": "4", + "node": "0.4", + "deno": "1", + "ie": "9", + "android": "4", + "ios": "6", + "phantom": "1.9", + "samsung": "1", + "rhino": "1.7.13", + "opera_mobile": "10.1", + "electron": "0.20" + }, + "es6.array.iterator": { + "chrome": "66", + "opera": "53", + "edge": "12", + "firefox": "60", + "safari": "9", + "node": "10", + "deno": "1", + "ios": "9", + "samsung": "9", + "rhino": "1.7.13", + "opera_mobile": "47", + "electron": "3.0" + }, + "es6.array.last-index-of": { + "chrome": "5", + "opera": "10.10", + "edge": "12", + "firefox": "2", + "safari": "3.1", + "node": "0.4", + "deno": "1", + "ie": "9", + "android": "4", + "ios": "6", + "phantom": "1.9", + "samsung": "1", + "rhino": "1.7.13", + "opera_mobile": "10.1", + "electron": "0.20" + }, + "es6.array.map": { + "chrome": "51", + "opera": "38", + "edge": "13", + "firefox": "48", + "safari": "10", + "node": "6.5", + "deno": "1", + "ios": "10", + "samsung": "5", + "rhino": "1.9", + "opera_mobile": "41", + "electron": "1.2" + }, + "es6.array.of": { + "chrome": "45", + "opera": "32", + "edge": "12", + "firefox": "25", + "safari": "9", + "node": "4", + "deno": "1", + "ios": "9", + "samsung": "5", + "rhino": "1.7.13", + "opera_mobile": "32", + "electron": "0.31" + }, + "es6.array.reduce": { + "chrome": "5", + "opera": "10.50", + "edge": "12", + "firefox": "3", + "safari": "4", + "node": "0.4", + "deno": "1", + "ie": "9", + "android": "4", + "ios": "6", + "phantom": "1.9", + "samsung": "1", + "rhino": "1.7.13", + "opera_mobile": "10.1", + "electron": "0.20" + }, + "es6.array.reduce-right": { + "chrome": "5", + "opera": "10.50", + "edge": "12", + "firefox": "3", + "safari": "4", + "node": "0.4", + "deno": "1", + "ie": "9", + "android": "4", + "ios": "6", + "phantom": "1.9", + "samsung": "1", + "rhino": "1.7.13", + "opera_mobile": "10.1", + "electron": "0.20" + }, + "es6.array.slice": { + "chrome": "51", + "opera": "38", + "edge": "13", + "firefox": "48", + "safari": "10", + "node": "6.5", + "deno": "1", + "ios": "10", + "samsung": "5", + "rhino": "1.9", + "opera_mobile": "41", + "electron": "1.2" + }, + "es6.array.some": { + "chrome": "5", + "opera": "10.10", + "edge": "12", + "firefox": "2", + "safari": "3.1", + "node": "0.4", + "deno": "1", + "ie": "9", + "android": "4", + "ios": "6", + "phantom": "1.9", + "samsung": "1", + "rhino": "1.7.13", + "opera_mobile": "10.1", + "electron": "0.20" + }, + "es6.array.sort": { + "chrome": "63", + "opera": "50", + "edge": "12", + "firefox": "5", + "safari": "12", + "node": "10", + "deno": "1", + "ie": "9", + "ios": "12", + "samsung": "8", + "rhino": "1.7.13", + "opera_mobile": "46", + "electron": "3.0" + }, + "es6.array.species": { + "chrome": "51", + "opera": "38", + "edge": "13", + "firefox": "48", + "safari": "10", + "node": "6.5", + "deno": "1", + "ios": "10", + "samsung": "5", + "rhino": "1.7.15", + "opera_mobile": "41", + "electron": "1.2" + }, + "es6.date.now": { + "chrome": "5", + "opera": "10.50", + "edge": "12", + "firefox": "2", + "safari": "4", + "node": "0.4", + "deno": "1", + "ie": "9", + "android": "4", + "ios": "6", + "phantom": "1.9", + "samsung": "1", + "rhino": "1.7.13", + "opera_mobile": "10.1", + "electron": "0.20" + }, + "es6.date.to-iso-string": { + "chrome": "5", + "opera": "10.50", + "edge": "12", + "firefox": "3.5", + "safari": "4", + "node": "0.4", + "deno": "1", + "ie": "9", + "android": "4", + "ios": "6", + "phantom": "1.9", + "samsung": "1", + "rhino": "1.7.13", + "opera_mobile": "10.1", + "electron": "0.20" + }, + "es6.date.to-json": { + "chrome": "5", + "opera": "12.10", + "edge": "12", + "firefox": "4", + "safari": "10", + "node": "0.4", + "deno": "1", + "ie": "9", + "android": "4", + "ios": "10", + "samsung": "1", + "rhino": "1.7.13", + "opera_mobile": "12.1", + "electron": "0.20" + }, + "es6.date.to-primitive": { + "chrome": "47", + "opera": "34", + "edge": "15", + "firefox": "44", + "safari": "10", + "node": "6", + "deno": "1", + "ios": "10", + "samsung": "5", + "rhino": "1.8", + "opera_mobile": "34", + "electron": "0.36" + }, + "es6.date.to-string": { + "chrome": "5", + "opera": "10.50", + "edge": "12", + "firefox": "2", + "safari": "3.1", + "node": "0.4", + "deno": "1", + "ie": "10", + "android": "4", + "ios": "6", + "phantom": "1.9", + "samsung": "1", + "rhino": "1.7.13", + "opera_mobile": "10.1", + "electron": "0.20" + }, + "es6.function.bind": { + "chrome": "7", + "opera": "12", + "edge": "12", + "firefox": "4", + "safari": "5.1", + "node": "0.4", + "deno": "1", + "ie": "9", + "android": "4", + "ios": "6", + "phantom": "1.9", + "samsung": "1", + "rhino": "1.7.13", + "opera_mobile": "12", + "electron": "0.20" + }, + "es6.function.has-instance": { + "chrome": "51", + "opera": "38", + "edge": "15", + "firefox": "50", + "safari": "10", + "node": "6.5", + "deno": "1", + "ios": "10", + "samsung": "5", + "opera_mobile": "41", + "electron": "1.2" + }, + "es6.function.name": { + "chrome": "5", + "opera": "10.50", + "edge": "14", + "firefox": "2", + "safari": "4", + "node": "0.4", + "deno": "1", + "android": "4", + "ios": "6", + "phantom": "1.9", + "samsung": "1", + "rhino": "1.7.13", + "opera_mobile": "10.1", + "electron": "0.20" + }, + "es6.map": { + "chrome": "51", + "opera": "38", + "edge": "15", + "firefox": "53", + "safari": "10", + "node": "6.5", + "deno": "1", + "ios": "10", + "samsung": "5", + "opera_mobile": "41", + "electron": "1.2" + }, + "es6.math.acosh": { + "chrome": "38", + "opera": "25", + "edge": "12", + "firefox": "25", + "safari": "7.1", + "node": "0.12", + "deno": "1", + "ios": "8", + "samsung": "3", + "rhino": "1.7.13", + "opera_mobile": "25", + "electron": "0.20" + }, + "es6.math.asinh": { + "chrome": "38", + "opera": "25", + "edge": "12", + "firefox": "25", + "safari": "7.1", + "node": "0.12", + "deno": "1", + "ios": "8", + "samsung": "3", + "rhino": "1.7.13", + "opera_mobile": "25", + "electron": "0.20" + }, + "es6.math.atanh": { + "chrome": "38", + "opera": "25", + "edge": "12", + "firefox": "25", + "safari": "7.1", + "node": "0.12", + "deno": "1", + "ios": "8", + "samsung": "3", + "rhino": "1.7.13", + "opera_mobile": "25", + "electron": "0.20" + }, + "es6.math.cbrt": { + "chrome": "38", + "opera": "25", + "edge": "12", + "firefox": "25", + "safari": "7.1", + "node": "0.12", + "deno": "1", + "ios": "8", + "samsung": "3", + "rhino": "1.7.13", + "opera_mobile": "25", + "electron": "0.20" + }, + "es6.math.clz32": { + "chrome": "38", + "opera": "25", + "edge": "12", + "firefox": "31", + "safari": "9", + "node": "0.12", + "deno": "1", + "ios": "9", + "samsung": "3", + "rhino": "1.7.13", + "opera_mobile": "25", + "electron": "0.20" + }, + "es6.math.cosh": { + "chrome": "38", + "opera": "25", + "edge": "12", + "firefox": "25", + "safari": "7.1", + "node": "0.12", + "deno": "1", + "ios": "8", + "samsung": "3", + "rhino": "1.7.13", + "opera_mobile": "25", + "electron": "0.20" + }, + "es6.math.expm1": { + "chrome": "38", + "opera": "25", + "edge": "12", + "firefox": "25", + "safari": "7.1", + "node": "0.12", + "deno": "1", + "ios": "8", + "samsung": "3", + "rhino": "1.7.13", + "opera_mobile": "25", + "electron": "0.20" + }, + "es6.math.fround": { + "chrome": "38", + "opera": "25", + "edge": "12", + "firefox": "26", + "safari": "7.1", + "node": "0.12", + "deno": "1", + "ios": "8", + "samsung": "3", + "rhino": "1.7.13", + "opera_mobile": "25", + "electron": "0.20" + }, + "es6.math.hypot": { + "chrome": "38", + "opera": "25", + "edge": "12", + "firefox": "27", + "safari": "7.1", + "node": "0.12", + "deno": "1", + "ios": "8", + "samsung": "3", + "rhino": "1.7.13", + "opera_mobile": "25", + "electron": "0.20" + }, + "es6.math.imul": { + "chrome": "30", + "opera": "17", + "edge": "12", + "firefox": "23", + "safari": "7", + "node": "0.12", + "deno": "1", + "android": "4.4", + "ios": "7", + "samsung": "2", + "rhino": "1.7.13", + "opera_mobile": "18", + "electron": "0.20" + }, + "es6.math.log1p": { + "chrome": "38", + "opera": "25", + "edge": "12", + "firefox": "25", + "safari": "7.1", + "node": "0.12", + "deno": "1", + "ios": "8", + "samsung": "3", + "rhino": "1.7.13", + "opera_mobile": "25", + "electron": "0.20" + }, + "es6.math.log10": { + "chrome": "38", + "opera": "25", + "edge": "12", + "firefox": "25", + "safari": "7.1", + "node": "0.12", + "deno": "1", + "ios": "8", + "samsung": "3", + "rhino": "1.7.13", + "opera_mobile": "25", + "electron": "0.20" + }, + "es6.math.log2": { + "chrome": "38", + "opera": "25", + "edge": "12", + "firefox": "25", + "safari": "7.1", + "node": "0.12", + "deno": "1", + "ios": "8", + "samsung": "3", + "rhino": "1.7.13", + "opera_mobile": "25", + "electron": "0.20" + }, + "es6.math.sign": { + "chrome": "38", + "opera": "25", + "edge": "12", + "firefox": "25", + "safari": "9", + "node": "0.12", + "deno": "1", + "ios": "9", + "samsung": "3", + "rhino": "1.7.13", + "opera_mobile": "25", + "electron": "0.20" + }, + "es6.math.sinh": { + "chrome": "38", + "opera": "25", + "edge": "12", + "firefox": "25", + "safari": "7.1", + "node": "0.12", + "deno": "1", + "ios": "8", + "samsung": "3", + "rhino": "1.7.13", + "opera_mobile": "25", + "electron": "0.20" + }, + "es6.math.tanh": { + "chrome": "38", + "opera": "25", + "edge": "12", + "firefox": "25", + "safari": "7.1", + "node": "0.12", + "deno": "1", + "ios": "8", + "samsung": "3", + "rhino": "1.7.13", + "opera_mobile": "25", + "electron": "0.20" + }, + "es6.math.trunc": { + "chrome": "38", + "opera": "25", + "edge": "12", + "firefox": "25", + "safari": "7.1", + "node": "0.12", + "deno": "1", + "ios": "8", + "samsung": "3", + "rhino": "1.7.13", + "opera_mobile": "25", + "electron": "0.20" + }, + "es6.number.constructor": { + "chrome": "41", + "opera": "28", + "edge": "12", + "firefox": "36", + "safari": "9", + "node": "4", + "deno": "1", + "ios": "9", + "samsung": "3.4", + "rhino": "1.7.13", + "opera_mobile": "28", + "electron": "0.21" + }, + "es6.number.epsilon": { + "chrome": "34", + "opera": "21", + "edge": "12", + "firefox": "25", + "safari": "9", + "node": "0.12", + "deno": "1", + "ios": "9", + "samsung": "2", + "rhino": "1.7.14", + "opera_mobile": "21", + "electron": "0.20" + }, + "es6.number.is-finite": { + "chrome": "19", + "opera": "15", + "edge": "12", + "firefox": "16", + "safari": "9", + "node": "0.8", + "deno": "1", + "android": "4.1", + "ios": "9", + "samsung": "1.5", + "rhino": "1.7.13", + "opera_mobile": "14", + "electron": "0.20" + }, + "es6.number.is-integer": { + "chrome": "34", + "opera": "21", + "edge": "12", + "firefox": "16", + "safari": "9", + "node": "0.12", + "deno": "1", + "ios": "9", + "samsung": "2", + "rhino": "1.7.13", + "opera_mobile": "21", + "electron": "0.20" + }, + "es6.number.is-nan": { + "chrome": "19", + "opera": "15", + "edge": "12", + "firefox": "15", + "safari": "9", + "node": "0.8", + "deno": "1", + "android": "4.1", + "ios": "9", + "samsung": "1.5", + "rhino": "1.7.13", + "opera_mobile": "14", + "electron": "0.20" + }, + "es6.number.is-safe-integer": { + "chrome": "34", + "opera": "21", + "edge": "12", + "firefox": "32", + "safari": "9", + "node": "0.12", + "deno": "1", + "ios": "9", + "samsung": "2", + "rhino": "1.7.13", + "opera_mobile": "21", + "electron": "0.20" + }, + "es6.number.max-safe-integer": { + "chrome": "34", + "opera": "21", + "edge": "12", + "firefox": "31", + "safari": "9", + "node": "0.12", + "deno": "1", + "ios": "9", + "samsung": "2", + "rhino": "1.7.13", + "opera_mobile": "21", + "electron": "0.20" + }, + "es6.number.min-safe-integer": { + "chrome": "34", + "opera": "21", + "edge": "12", + "firefox": "31", + "safari": "9", + "node": "0.12", + "deno": "1", + "ios": "9", + "samsung": "2", + "rhino": "1.7.13", + "opera_mobile": "21", + "electron": "0.20" + }, + "es6.number.parse-float": { + "chrome": "34", + "opera": "21", + "edge": "12", + "firefox": "25", + "safari": "9", + "node": "0.12", + "deno": "1", + "ios": "9", + "samsung": "2", + "rhino": "1.7.14", + "opera_mobile": "21", + "electron": "0.20" + }, + "es6.number.parse-int": { + "chrome": "34", + "opera": "21", + "edge": "12", + "firefox": "25", + "safari": "9", + "node": "0.12", + "deno": "1", + "ios": "9", + "samsung": "2", + "rhino": "1.7.14", + "opera_mobile": "21", + "electron": "0.20" + }, + "es6.object.assign": { + "chrome": "49", + "opera": "36", + "edge": "13", + "firefox": "36", + "safari": "10", + "node": "6", + "deno": "1", + "ios": "10", + "samsung": "5", + "rhino": "1.8", + "opera_mobile": "36", + "electron": "0.37" + }, + "es6.object.create": { + "chrome": "5", + "opera": "12", + "edge": "12", + "firefox": "4", + "safari": "4", + "node": "0.4", + "deno": "1", + "ie": "9", + "android": "4", + "ios": "6", + "phantom": "1.9", + "samsung": "1", + "rhino": "1.7.13", + "opera_mobile": "12", + "electron": "0.20" + }, + "es7.object.define-getter": { + "chrome": "62", + "opera": "49", + "edge": "16", + "firefox": "48", + "safari": "9", + "node": "8.10", + "deno": "1", + "ios": "9", + "samsung": "8", + "opera_mobile": "46", + "electron": "3.0" + }, + "es7.object.define-setter": { + "chrome": "62", + "opera": "49", + "edge": "16", + "firefox": "48", + "safari": "9", + "node": "8.10", + "deno": "1", + "ios": "9", + "samsung": "8", + "opera_mobile": "46", + "electron": "3.0" + }, + "es6.object.define-property": { + "chrome": "5", + "opera": "12", + "edge": "12", + "firefox": "4", + "safari": "5.1", + "node": "0.4", + "deno": "1", + "ie": "9", + "android": "4", + "ios": "6", + "phantom": "1.9", + "samsung": "1", + "rhino": "1.7.13", + "opera_mobile": "12", + "electron": "0.20" + }, + "es6.object.define-properties": { + "chrome": "5", + "opera": "12", + "edge": "12", + "firefox": "4", + "safari": "4", + "node": "0.4", + "deno": "1", + "ie": "9", + "android": "4", + "ios": "6", + "phantom": "1.9", + "samsung": "1", + "rhino": "1.7.13", + "opera_mobile": "12", + "electron": "0.20" + }, + "es7.object.entries": { + "chrome": "54", + "opera": "41", + "edge": "14", + "firefox": "47", + "safari": "10.1", + "node": "7", + "deno": "1", + "ios": "10.3", + "samsung": "6", + "rhino": "1.7.14", + "opera_mobile": "41", + "electron": "1.4" + }, + "es6.object.freeze": { + "chrome": "44", + "opera": "31", + "edge": "12", + "firefox": "35", + "safari": "9", + "node": "4", + "deno": "1", + "ios": "9", + "samsung": "4", + "rhino": "1.7.13", + "opera_mobile": "32", + "electron": "0.30" + }, + "es6.object.get-own-property-descriptor": { + "chrome": "44", + "opera": "31", + "edge": "12", + "firefox": "35", + "safari": "9", + "node": "4", + "deno": "1", + "ios": "9", + "samsung": "4", + "rhino": "1.7.13", + "opera_mobile": "32", + "electron": "0.30" + }, + "es7.object.get-own-property-descriptors": { + "chrome": "54", + "opera": "41", + "edge": "15", + "firefox": "50", + "safari": "10.1", + "node": "7", + "deno": "1", + "ios": "10.3", + "samsung": "6", + "rhino": "1.8", + "opera_mobile": "41", + "electron": "1.4" + }, + "es6.object.get-own-property-names": { + "chrome": "40", + "opera": "27", + "edge": "12", + "firefox": "33", + "safari": "9", + "node": "4", + "deno": "1", + "ios": "9", + "samsung": "3.4", + "rhino": "1.7.13", + "opera_mobile": "27", + "electron": "0.21" + }, + "es6.object.get-prototype-of": { + "chrome": "44", + "opera": "31", + "edge": "12", + "firefox": "35", + "safari": "9", + "node": "4", + "deno": "1", + "ios": "9", + "samsung": "4", + "rhino": "1.7.13", + "opera_mobile": "32", + "electron": "0.30" + }, + "es7.object.lookup-getter": { + "chrome": "62", + "opera": "49", + "edge": "79", + "firefox": "36", + "safari": "9", + "node": "8.10", + "deno": "1", + "ios": "9", + "samsung": "8", + "opera_mobile": "46", + "electron": "3.0" + }, + "es7.object.lookup-setter": { + "chrome": "62", + "opera": "49", + "edge": "79", + "firefox": "36", + "safari": "9", + "node": "8.10", + "deno": "1", + "ios": "9", + "samsung": "8", + "opera_mobile": "46", + "electron": "3.0" + }, + "es6.object.prevent-extensions": { + "chrome": "44", + "opera": "31", + "edge": "12", + "firefox": "35", + "safari": "9", + "node": "4", + "deno": "1", + "ios": "9", + "samsung": "4", + "rhino": "1.7.13", + "opera_mobile": "32", + "electron": "0.30" + }, + "es6.object.to-string": { + "chrome": "57", + "opera": "44", + "edge": "15", + "firefox": "51", + "safari": "10", + "node": "8", + "deno": "1", + "ios": "10", + "samsung": "7", + "rhino": "1.9", + "opera_mobile": "43", + "electron": "1.7" + }, + "es6.object.is": { + "chrome": "19", + "opera": "15", + "edge": "12", + "firefox": "22", + "safari": "9", + "node": "0.8", + "deno": "1", + "android": "4.1", + "ios": "9", + "samsung": "1.5", + "rhino": "1.7.13", + "opera_mobile": "14", + "electron": "0.20" + }, + "es6.object.is-frozen": { + "chrome": "44", + "opera": "31", + "edge": "12", + "firefox": "35", + "safari": "9", + "node": "4", + "deno": "1", + "ios": "9", + "samsung": "4", + "rhino": "1.7.13", + "opera_mobile": "32", + "electron": "0.30" + }, + "es6.object.is-sealed": { + "chrome": "44", + "opera": "31", + "edge": "12", + "firefox": "35", + "safari": "9", + "node": "4", + "deno": "1", + "ios": "9", + "samsung": "4", + "rhino": "1.7.13", + "opera_mobile": "32", + "electron": "0.30" + }, + "es6.object.is-extensible": { + "chrome": "44", + "opera": "31", + "edge": "12", + "firefox": "35", + "safari": "9", + "node": "4", + "deno": "1", + "ios": "9", + "samsung": "4", + "rhino": "1.7.13", + "opera_mobile": "32", + "electron": "0.30" + }, + "es6.object.keys": { + "chrome": "40", + "opera": "27", + "edge": "12", + "firefox": "35", + "safari": "9", + "node": "4", + "deno": "1", + "ios": "9", + "samsung": "3.4", + "rhino": "1.7.13", + "opera_mobile": "27", + "electron": "0.21" + }, + "es6.object.seal": { + "chrome": "44", + "opera": "31", + "edge": "12", + "firefox": "35", + "safari": "9", + "node": "4", + "deno": "1", + "ios": "9", + "samsung": "4", + "rhino": "1.7.13", + "opera_mobile": "32", + "electron": "0.30" + }, + "es6.object.set-prototype-of": { + "chrome": "34", + "opera": "21", + "edge": "12", + "firefox": "31", + "safari": "9", + "node": "0.12", + "deno": "1", + "ie": "11", + "ios": "9", + "samsung": "2", + "rhino": "1.7.13", + "opera_mobile": "21", + "electron": "0.20" + }, + "es7.object.values": { + "chrome": "54", + "opera": "41", + "edge": "14", + "firefox": "47", + "safari": "10.1", + "node": "7", + "deno": "1", + "ios": "10.3", + "samsung": "6", + "rhino": "1.7.14", + "opera_mobile": "41", + "electron": "1.4" + }, + "es6.promise": { + "chrome": "51", + "opera": "38", + "edge": "14", + "firefox": "45", + "safari": "10", + "node": "6.5", + "deno": "1", + "ios": "10", + "samsung": "5", + "rhino": "1.7.15", + "opera_mobile": "41", + "electron": "1.2" + }, + "es7.promise.finally": { + "chrome": "63", + "opera": "50", + "edge": "18", + "firefox": "58", + "safari": "11.1", + "node": "10", + "deno": "1", + "ios": "11.3", + "samsung": "8", + "opera_mobile": "46", + "electron": "3.0" + }, + "es6.reflect.apply": { + "chrome": "49", + "opera": "36", + "edge": "12", + "firefox": "42", + "safari": "10", + "node": "6", + "deno": "1", + "ios": "10", + "samsung": "5", + "rhino": "1.8", + "opera_mobile": "36", + "electron": "0.37" + }, + "es6.reflect.construct": { + "chrome": "49", + "opera": "36", + "edge": "13", + "firefox": "49", + "safari": "10", + "node": "6", + "deno": "1", + "ios": "10", + "samsung": "5", + "opera_mobile": "36", + "electron": "0.37" + }, + "es6.reflect.define-property": { + "chrome": "49", + "opera": "36", + "edge": "13", + "firefox": "42", + "safari": "10", + "node": "6", + "deno": "1", + "ios": "10", + "samsung": "5", + "rhino": "1.8", + "opera_mobile": "36", + "electron": "0.37" + }, + "es6.reflect.delete-property": { + "chrome": "49", + "opera": "36", + "edge": "12", + "firefox": "42", + "safari": "10", + "node": "6", + "deno": "1", + "ios": "10", + "samsung": "5", + "rhino": "1.8", + "opera_mobile": "36", + "electron": "0.37" + }, + "es6.reflect.get": { + "chrome": "49", + "opera": "36", + "edge": "12", + "firefox": "42", + "safari": "10", + "node": "6", + "deno": "1", + "ios": "10", + "samsung": "5", + "rhino": "1.8", + "opera_mobile": "36", + "electron": "0.37" + }, + "es6.reflect.get-own-property-descriptor": { + "chrome": "49", + "opera": "36", + "edge": "12", + "firefox": "42", + "safari": "10", + "node": "6", + "deno": "1", + "ios": "10", + "samsung": "5", + "rhino": "1.8", + "opera_mobile": "36", + "electron": "0.37" + }, + "es6.reflect.get-prototype-of": { + "chrome": "49", + "opera": "36", + "edge": "12", + "firefox": "42", + "safari": "10", + "node": "6", + "deno": "1", + "ios": "10", + "samsung": "5", + "rhino": "1.8", + "opera_mobile": "36", + "electron": "0.37" + }, + "es6.reflect.has": { + "chrome": "49", + "opera": "36", + "edge": "12", + "firefox": "42", + "safari": "10", + "node": "6", + "deno": "1", + "ios": "10", + "samsung": "5", + "rhino": "1.8", + "opera_mobile": "36", + "electron": "0.37" + }, + "es6.reflect.is-extensible": { + "chrome": "49", + "opera": "36", + "edge": "12", + "firefox": "42", + "safari": "10", + "node": "6", + "deno": "1", + "ios": "10", + "samsung": "5", + "rhino": "1.8", + "opera_mobile": "36", + "electron": "0.37" + }, + "es6.reflect.own-keys": { + "chrome": "49", + "opera": "36", + "edge": "12", + "firefox": "42", + "safari": "10", + "node": "6", + "deno": "1", + "ios": "10", + "samsung": "5", + "rhino": "1.8", + "opera_mobile": "36", + "electron": "0.37" + }, + "es6.reflect.prevent-extensions": { + "chrome": "49", + "opera": "36", + "edge": "12", + "firefox": "42", + "safari": "10", + "node": "6", + "deno": "1", + "ios": "10", + "samsung": "5", + "rhino": "1.8", + "opera_mobile": "36", + "electron": "0.37" + }, + "es6.reflect.set": { + "chrome": "49", + "opera": "36", + "edge": "12", + "firefox": "42", + "safari": "10", + "node": "6", + "deno": "1", + "ios": "10", + "samsung": "5", + "rhino": "1.8", + "opera_mobile": "36", + "electron": "0.37" + }, + "es6.reflect.set-prototype-of": { + "chrome": "49", + "opera": "36", + "edge": "12", + "firefox": "42", + "safari": "10", + "node": "6", + "deno": "1", + "ios": "10", + "samsung": "5", + "rhino": "1.8", + "opera_mobile": "36", + "electron": "0.37" + }, + "es6.regexp.constructor": { + "chrome": "50", + "opera": "37", + "edge": "79", + "firefox": "40", + "safari": "10", + "node": "6", + "deno": "1", + "ios": "10", + "samsung": "5", + "opera_mobile": "37", + "electron": "1.1" + }, + "es6.regexp.flags": { + "chrome": "49", + "opera": "36", + "edge": "79", + "firefox": "37", + "safari": "9", + "node": "6", + "deno": "1", + "ios": "9", + "samsung": "5", + "rhino": "1.7.15", + "opera_mobile": "36", + "electron": "0.37" + }, + "es6.regexp.match": { + "chrome": "50", + "opera": "37", + "edge": "79", + "firefox": "49", + "safari": "10", + "node": "6", + "deno": "1", + "ios": "10", + "samsung": "5", + "rhino": "1.7.13", + "opera_mobile": "37", + "electron": "1.1" + }, + "es6.regexp.replace": { + "chrome": "50", + "opera": "37", + "edge": "79", + "firefox": "49", + "safari": "10", + "node": "6", + "deno": "1", + "ios": "10", + "samsung": "5", + "rhino": "1.9", + "opera_mobile": "37", + "electron": "1.1" + }, + "es6.regexp.split": { + "chrome": "50", + "opera": "37", + "edge": "79", + "firefox": "49", + "safari": "10", + "node": "6", + "deno": "1", + "ios": "10", + "samsung": "5", + "rhino": "1.9", + "opera_mobile": "37", + "electron": "1.1" + }, + "es6.regexp.search": { + "chrome": "50", + "opera": "37", + "edge": "79", + "firefox": "49", + "safari": "10", + "node": "6", + "deno": "1", + "ios": "10", + "samsung": "5", + "rhino": "1.7.13", + "opera_mobile": "37", + "electron": "1.1" + }, + "es6.regexp.to-string": { + "chrome": "50", + "opera": "37", + "edge": "79", + "firefox": "39", + "safari": "10", + "node": "6", + "deno": "1", + "ios": "10", + "samsung": "5", + "rhino": "1.7.15", + "opera_mobile": "37", + "electron": "1.1" + }, + "es6.set": { + "chrome": "51", + "opera": "38", + "edge": "15", + "firefox": "53", + "safari": "10", + "node": "6.5", + "deno": "1", + "ios": "10", + "samsung": "5", + "opera_mobile": "41", + "electron": "1.2" + }, + "es6.symbol": { + "chrome": "51", + "opera": "38", + "edge": "79", + "firefox": "51", + "safari": "10", + "node": "6.5", + "deno": "1", + "ios": "10", + "samsung": "5", + "opera_mobile": "41", + "electron": "1.2" + }, + "es7.symbol.async-iterator": { + "chrome": "63", + "opera": "50", + "edge": "79", + "firefox": "57", + "safari": "12", + "node": "10", + "deno": "1", + "ios": "12", + "samsung": "8", + "opera_mobile": "46", + "electron": "3.0" + }, + "es6.string.anchor": { + "chrome": "5", + "opera": "15", + "edge": "12", + "firefox": "17", + "safari": "6", + "node": "0.4", + "deno": "1", + "android": "4", + "ios": "7", + "phantom": "1.9", + "samsung": "1", + "rhino": "1.7.14", + "opera_mobile": "14", + "electron": "0.20" + }, + "es6.string.big": { + "chrome": "5", + "opera": "15", + "edge": "12", + "firefox": "17", + "safari": "6", + "node": "0.4", + "deno": "1", + "android": "4", + "ios": "7", + "phantom": "1.9", + "samsung": "1", + "rhino": "1.7.14", + "opera_mobile": "14", + "electron": "0.20" + }, + "es6.string.blink": { + "chrome": "5", + "opera": "15", + "edge": "12", + "firefox": "17", + "safari": "6", + "node": "0.4", + "deno": "1", + "android": "4", + "ios": "7", + "phantom": "1.9", + "samsung": "1", + "rhino": "1.7.14", + "opera_mobile": "14", + "electron": "0.20" + }, + "es6.string.bold": { + "chrome": "5", + "opera": "15", + "edge": "12", + "firefox": "17", + "safari": "6", + "node": "0.4", + "deno": "1", + "android": "4", + "ios": "7", + "phantom": "1.9", + "samsung": "1", + "rhino": "1.7.14", + "opera_mobile": "14", + "electron": "0.20" + }, + "es6.string.code-point-at": { + "chrome": "41", + "opera": "28", + "edge": "12", + "firefox": "29", + "safari": "9", + "node": "4", + "deno": "1", + "ios": "9", + "samsung": "3.4", + "rhino": "1.7.13", + "opera_mobile": "28", + "electron": "0.21" + }, + "es6.string.ends-with": { + "chrome": "41", + "opera": "28", + "edge": "12", + "firefox": "29", + "safari": "9", + "node": "4", + "deno": "1", + "ios": "9", + "samsung": "3.4", + "rhino": "1.7.13", + "opera_mobile": "28", + "electron": "0.21" + }, + "es6.string.fixed": { + "chrome": "5", + "opera": "15", + "edge": "12", + "firefox": "17", + "safari": "6", + "node": "0.4", + "deno": "1", + "android": "4", + "ios": "7", + "phantom": "1.9", + "samsung": "1", + "rhino": "1.7.14", + "opera_mobile": "14", + "electron": "0.20" + }, + "es6.string.fontcolor": { + "chrome": "5", + "opera": "15", + "edge": "12", + "firefox": "17", + "safari": "6", + "node": "0.4", + "deno": "1", + "android": "4", + "ios": "7", + "phantom": "1.9", + "samsung": "1", + "rhino": "1.7.14", + "opera_mobile": "14", + "electron": "0.20" + }, + "es6.string.fontsize": { + "chrome": "5", + "opera": "15", + "edge": "12", + "firefox": "17", + "safari": "6", + "node": "0.4", + "deno": "1", + "android": "4", + "ios": "7", + "phantom": "1.9", + "samsung": "1", + "rhino": "1.7.14", + "opera_mobile": "14", + "electron": "0.20" + }, + "es6.string.from-code-point": { + "chrome": "41", + "opera": "28", + "edge": "12", + "firefox": "29", + "safari": "9", + "node": "4", + "deno": "1", + "ios": "9", + "samsung": "3.4", + "rhino": "1.7.13", + "opera_mobile": "28", + "electron": "0.21" + }, + "es6.string.includes": { + "chrome": "41", + "opera": "28", + "edge": "12", + "firefox": "40", + "safari": "9", + "node": "4", + "deno": "1", + "ios": "9", + "samsung": "3.4", + "rhino": "1.7.13", + "opera_mobile": "28", + "electron": "0.21" + }, + "es6.string.italics": { + "chrome": "5", + "opera": "15", + "edge": "12", + "firefox": "17", + "safari": "6", + "node": "0.4", + "deno": "1", + "android": "4", + "ios": "7", + "phantom": "1.9", + "samsung": "1", + "rhino": "1.7.14", + "opera_mobile": "14", + "electron": "0.20" + }, + "es6.string.iterator": { + "chrome": "38", + "opera": "25", + "edge": "12", + "firefox": "36", + "safari": "9", + "node": "0.12", + "deno": "1", + "ios": "9", + "samsung": "3", + "rhino": "1.7.13", + "opera_mobile": "25", + "electron": "0.20" + }, + "es6.string.link": { + "chrome": "5", + "opera": "15", + "edge": "12", + "firefox": "17", + "safari": "6", + "node": "0.4", + "deno": "1", + "android": "4", + "ios": "7", + "phantom": "1.9", + "samsung": "1", + "rhino": "1.7.14", + "opera_mobile": "14", + "electron": "0.20" + }, + "es7.string.pad-start": { + "chrome": "57", + "opera": "44", + "edge": "15", + "firefox": "48", + "safari": "10", + "node": "8", + "deno": "1", + "ios": "10", + "samsung": "7", + "rhino": "1.7.13", + "opera_mobile": "43", + "electron": "1.7" + }, + "es7.string.pad-end": { + "chrome": "57", + "opera": "44", + "edge": "15", + "firefox": "48", + "safari": "10", + "node": "8", + "deno": "1", + "ios": "10", + "samsung": "7", + "rhino": "1.7.13", + "opera_mobile": "43", + "electron": "1.7" + }, + "es6.string.raw": { + "chrome": "41", + "opera": "28", + "edge": "12", + "firefox": "34", + "safari": "9", + "node": "4", + "deno": "1", + "ios": "9", + "samsung": "3.4", + "rhino": "1.7.14", + "opera_mobile": "28", + "electron": "0.21" + }, + "es6.string.repeat": { + "chrome": "41", + "opera": "28", + "edge": "12", + "firefox": "24", + "safari": "9", + "node": "4", + "deno": "1", + "ios": "9", + "samsung": "3.4", + "rhino": "1.7.13", + "opera_mobile": "28", + "electron": "0.21" + }, + "es6.string.small": { + "chrome": "5", + "opera": "15", + "edge": "12", + "firefox": "17", + "safari": "6", + "node": "0.4", + "deno": "1", + "android": "4", + "ios": "7", + "phantom": "1.9", + "samsung": "1", + "rhino": "1.7.14", + "opera_mobile": "14", + "electron": "0.20" + }, + "es6.string.starts-with": { + "chrome": "41", + "opera": "28", + "edge": "12", + "firefox": "29", + "safari": "9", + "node": "4", + "deno": "1", + "ios": "9", + "samsung": "3.4", + "rhino": "1.7.13", + "opera_mobile": "28", + "electron": "0.21" + }, + "es6.string.strike": { + "chrome": "5", + "opera": "15", + "edge": "12", + "firefox": "17", + "safari": "6", + "node": "0.4", + "deno": "1", + "android": "4", + "ios": "7", + "phantom": "1.9", + "samsung": "1", + "rhino": "1.7.14", + "opera_mobile": "14", + "electron": "0.20" + }, + "es6.string.sub": { + "chrome": "5", + "opera": "15", + "edge": "12", + "firefox": "17", + "safari": "6", + "node": "0.4", + "deno": "1", + "android": "4", + "ios": "7", + "phantom": "1.9", + "samsung": "1", + "rhino": "1.7.14", + "opera_mobile": "14", + "electron": "0.20" + }, + "es6.string.sup": { + "chrome": "5", + "opera": "15", + "edge": "12", + "firefox": "17", + "safari": "6", + "node": "0.4", + "deno": "1", + "android": "4", + "ios": "7", + "phantom": "1.9", + "samsung": "1", + "rhino": "1.7.14", + "opera_mobile": "14", + "electron": "0.20" + }, + "es6.string.trim": { + "chrome": "5", + "opera": "10.50", + "edge": "12", + "firefox": "3.5", + "safari": "4", + "node": "0.4", + "deno": "1", + "ie": "9", + "android": "4", + "ios": "6", + "phantom": "1.9", + "samsung": "1", + "rhino": "1.7.13", + "opera_mobile": "10.1", + "electron": "0.20" + }, + "es7.string.trim-left": { + "chrome": "66", + "opera": "53", + "edge": "79", + "firefox": "61", + "safari": "12", + "node": "10", + "deno": "1", + "ios": "12", + "samsung": "9", + "rhino": "1.7.13", + "opera_mobile": "47", + "electron": "3.0" + }, + "es7.string.trim-right": { + "chrome": "66", + "opera": "53", + "edge": "79", + "firefox": "61", + "safari": "12", + "node": "10", + "deno": "1", + "ios": "12", + "samsung": "9", + "rhino": "1.7.13", + "opera_mobile": "47", + "electron": "3.0" + }, + "es6.typed.array-buffer": { + "chrome": "51", + "opera": "38", + "edge": "13", + "firefox": "48", + "safari": "10", + "node": "6.5", + "deno": "1", + "ios": "10", + "samsung": "5", + "opera_mobile": "41", + "electron": "1.2" + }, + "es6.typed.data-view": { + "chrome": "5", + "opera": "12", + "edge": "12", + "firefox": "15", + "safari": "5.1", + "node": "0.4", + "deno": "1", + "ie": "10", + "android": "4", + "ios": "6", + "phantom": "1.9", + "samsung": "1", + "rhino": "1.7.13", + "opera_mobile": "12", + "electron": "0.20" + }, + "es6.typed.int8-array": { + "chrome": "51", + "opera": "38", + "edge": "13", + "firefox": "48", + "safari": "10", + "node": "6.5", + "deno": "1", + "ios": "10", + "samsung": "5", + "rhino": "1.9", + "opera_mobile": "41", + "electron": "1.2" + }, + "es6.typed.uint8-array": { + "chrome": "51", + "opera": "38", + "edge": "13", + "firefox": "48", + "safari": "10", + "node": "6.5", + "deno": "1", + "ios": "10", + "samsung": "5", + "rhino": "1.9", + "opera_mobile": "41", + "electron": "1.2" + }, + "es6.typed.uint8-clamped-array": { + "chrome": "51", + "opera": "38", + "edge": "13", + "firefox": "48", + "safari": "10", + "node": "6.5", + "deno": "1", + "ios": "10", + "samsung": "5", + "rhino": "1.9", + "opera_mobile": "41", + "electron": "1.2" + }, + "es6.typed.int16-array": { + "chrome": "51", + "opera": "38", + "edge": "13", + "firefox": "48", + "safari": "10", + "node": "6.5", + "deno": "1", + "ios": "10", + "samsung": "5", + "rhino": "1.9", + "opera_mobile": "41", + "electron": "1.2" + }, + "es6.typed.uint16-array": { + "chrome": "51", + "opera": "38", + "edge": "13", + "firefox": "48", + "safari": "10", + "node": "6.5", + "deno": "1", + "ios": "10", + "samsung": "5", + "rhino": "1.9", + "opera_mobile": "41", + "electron": "1.2" + }, + "es6.typed.int32-array": { + "chrome": "51", + "opera": "38", + "edge": "13", + "firefox": "48", + "safari": "10", + "node": "6.5", + "deno": "1", + "ios": "10", + "samsung": "5", + "rhino": "1.9", + "opera_mobile": "41", + "electron": "1.2" + }, + "es6.typed.uint32-array": { + "chrome": "51", + "opera": "38", + "edge": "13", + "firefox": "48", + "safari": "10", + "node": "6.5", + "deno": "1", + "ios": "10", + "samsung": "5", + "rhino": "1.9", + "opera_mobile": "41", + "electron": "1.2" + }, + "es6.typed.float32-array": { + "chrome": "51", + "opera": "38", + "edge": "13", + "firefox": "48", + "safari": "10", + "node": "6.5", + "deno": "1", + "ios": "10", + "samsung": "5", + "rhino": "1.9", + "opera_mobile": "41", + "electron": "1.2" + }, + "es6.typed.float64-array": { + "chrome": "51", + "opera": "38", + "edge": "13", + "firefox": "48", + "safari": "10", + "node": "6.5", + "deno": "1", + "ios": "10", + "samsung": "5", + "rhino": "1.9", + "opera_mobile": "41", + "electron": "1.2" + }, + "es6.weak-map": { + "chrome": "51", + "opera": "38", + "edge": "15", + "firefox": "53", + "safari": "9", + "node": "6.5", + "deno": "1", + "ios": "9", + "samsung": "5", + "rhino": "1.7.15", + "opera_mobile": "41", + "electron": "1.2" + }, + "es6.weak-set": { + "chrome": "51", + "opera": "38", + "edge": "15", + "firefox": "53", + "safari": "9", + "node": "6.5", + "deno": "1", + "ios": "9", + "samsung": "5", + "rhino": "1.7.15", + "opera_mobile": "41", + "electron": "1.2" + } +} diff --git a/node_modules/@babel/compat-data/data/corejs3-shipped-proposals.json b/node_modules/@babel/compat-data/data/corejs3-shipped-proposals.json new file mode 100644 index 0000000000000000000000000000000000000000..d03b698ff0c18a2e55ff3df2414e4bc366962515 --- /dev/null +++ b/node_modules/@babel/compat-data/data/corejs3-shipped-proposals.json @@ -0,0 +1,5 @@ +[ + "esnext.promise.all-settled", + "esnext.string.match-all", + "esnext.global-this" +] diff --git a/node_modules/@babel/compat-data/data/native-modules.json b/node_modules/@babel/compat-data/data/native-modules.json new file mode 100644 index 0000000000000000000000000000000000000000..2328d2138bd513cde3d1fe009ef41f969226f023 --- /dev/null +++ b/node_modules/@babel/compat-data/data/native-modules.json @@ -0,0 +1,18 @@ +{ + "es6.module": { + "chrome": "61", + "and_chr": "61", + "edge": "16", + "firefox": "60", + "and_ff": "60", + "node": "13.2.0", + "opera": "48", + "op_mob": "45", + "safari": "10.1", + "ios": "10.3", + "samsung": "8.2", + "android": "61", + "electron": "2.0", + "ios_saf": "10.3" + } +} diff --git a/node_modules/@babel/compat-data/data/overlapping-plugins.json b/node_modules/@babel/compat-data/data/overlapping-plugins.json new file mode 100644 index 0000000000000000000000000000000000000000..c0df4bd445ed52b2138b0b7c228cda5ebe713609 --- /dev/null +++ b/node_modules/@babel/compat-data/data/overlapping-plugins.json @@ -0,0 +1,38 @@ +{ + "transform-async-to-generator": [ + "bugfix/transform-async-arrows-in-class" + ], + "transform-parameters": [ + "bugfix/transform-edge-default-parameters", + "bugfix/transform-safari-id-destructuring-collision-in-function-expression" + ], + "transform-function-name": [ + "bugfix/transform-edge-function-name" + ], + "transform-block-scoping": [ + "bugfix/transform-safari-block-shadowing", + "bugfix/transform-safari-for-shadowing" + ], + "transform-destructuring": [ + "bugfix/transform-safari-rest-destructuring-rhs-array" + ], + "transform-template-literals": [ + "bugfix/transform-tagged-template-caching" + ], + "transform-optional-chaining": [ + "bugfix/transform-v8-spread-parameters-in-optional-chaining" + ], + "proposal-optional-chaining": [ + "bugfix/transform-v8-spread-parameters-in-optional-chaining" + ], + "transform-class-properties": [ + "bugfix/transform-v8-static-class-fields-redefine-readonly", + "bugfix/transform-firefox-class-in-computed-class-key", + "bugfix/transform-safari-class-field-initializer-scope" + ], + "proposal-class-properties": [ + "bugfix/transform-v8-static-class-fields-redefine-readonly", + "bugfix/transform-firefox-class-in-computed-class-key", + "bugfix/transform-safari-class-field-initializer-scope" + ] +} diff --git a/node_modules/@babel/compat-data/data/plugin-bugfixes.json b/node_modules/@babel/compat-data/data/plugin-bugfixes.json new file mode 100644 index 0000000000000000000000000000000000000000..7b6589ae7f8936a45c1b6efbd370cf4d502253d8 --- /dev/null +++ b/node_modules/@babel/compat-data/data/plugin-bugfixes.json @@ -0,0 +1,231 @@ +{ + "bugfix/transform-async-arrows-in-class": { + "chrome": "55", + "opera": "42", + "edge": "15", + "firefox": "52", + "safari": "11", + "node": "7.6", + "deno": "1", + "ios": "11", + "samsung": "6", + "opera_mobile": "42", + "electron": "1.6" + }, + "bugfix/transform-edge-default-parameters": { + "chrome": "49", + "opera": "36", + "edge": "18", + "firefox": "52", + "safari": "10", + "node": "6", + "deno": "1", + "ios": "10", + "samsung": "5", + "opera_mobile": "36", + "electron": "0.37" + }, + "bugfix/transform-edge-function-name": { + "chrome": "51", + "opera": "38", + "edge": "79", + "firefox": "53", + "safari": "10", + "node": "6.5", + "deno": "1", + "ios": "10", + "samsung": "5", + "rhino": "1.9", + "opera_mobile": "41", + "electron": "1.2" + }, + "bugfix/transform-safari-block-shadowing": { + "chrome": "49", + "opera": "36", + "edge": "12", + "firefox": "44", + "safari": "11", + "node": "6", + "deno": "1", + "ie": "11", + "ios": "11", + "samsung": "5", + "opera_mobile": "36", + "electron": "0.37" + }, + "bugfix/transform-safari-for-shadowing": { + "chrome": "49", + "opera": "36", + "edge": "12", + "firefox": "4", + "safari": "11", + "node": "6", + "deno": "1", + "ie": "11", + "ios": "11", + "samsung": "5", + "rhino": "1.7.13", + "opera_mobile": "36", + "electron": "0.37" + }, + "bugfix/transform-safari-id-destructuring-collision-in-function-expression": { + "chrome": "49", + "opera": "36", + "edge": "14", + "firefox": "2", + "safari": "16.3", + "node": "6", + "deno": "1", + "ios": "16.3", + "samsung": "5", + "opera_mobile": "36", + "electron": "0.37" + }, + "bugfix/transform-safari-rest-destructuring-rhs-array": { + "chrome": "49", + "opera": "36", + "edge": "14", + "firefox": "34", + "safari": "14.1", + "node": "6", + "deno": "1", + "ios": "14.5", + "samsung": "5", + "opera_mobile": "36", + "electron": "0.37" + }, + "bugfix/transform-tagged-template-caching": { + "chrome": "41", + "opera": "28", + "edge": "12", + "firefox": "34", + "safari": "13", + "node": "4", + "deno": "1", + "ios": "13", + "samsung": "3.4", + "rhino": "1.7.14", + "opera_mobile": "28", + "electron": "0.21" + }, + "bugfix/transform-v8-spread-parameters-in-optional-chaining": { + "chrome": "91", + "opera": "77", + "edge": "91", + "firefox": "74", + "safari": "13.1", + "node": "16.9", + "deno": "1.9", + "ios": "13.4", + "samsung": "16", + "opera_mobile": "64", + "electron": "13.0" + }, + "transform-optional-chaining": { + "chrome": "80", + "opera": "67", + "edge": "80", + "firefox": "74", + "safari": "13.1", + "node": "14", + "deno": "1", + "ios": "13.4", + "samsung": "13", + "rhino": "1.8", + "opera_mobile": "57", + "electron": "8.0" + }, + "proposal-optional-chaining": { + "chrome": "80", + "opera": "67", + "edge": "80", + "firefox": "74", + "safari": "13.1", + "node": "14", + "deno": "1", + "ios": "13.4", + "samsung": "13", + "rhino": "1.8", + "opera_mobile": "57", + "electron": "8.0" + }, + "transform-parameters": { + "chrome": "49", + "opera": "36", + "edge": "15", + "firefox": "52", + "safari": "10", + "node": "6", + "deno": "1", + "ios": "10", + "samsung": "5", + "opera_mobile": "36", + "electron": "0.37" + }, + "transform-async-to-generator": { + "chrome": "55", + "opera": "42", + "edge": "15", + "firefox": "52", + "safari": "10.1", + "node": "7.6", + "deno": "1", + "ios": "10.3", + "samsung": "6", + "opera_mobile": "42", + "electron": "1.6" + }, + "transform-template-literals": { + "chrome": "41", + "opera": "28", + "edge": "13", + "firefox": "34", + "safari": "9", + "node": "4", + "deno": "1", + "ios": "9", + "samsung": "3.4", + "rhino": "1.9", + "opera_mobile": "28", + "electron": "0.21" + }, + "transform-function-name": { + "chrome": "51", + "opera": "38", + "edge": "14", + "firefox": "53", + "safari": "10", + "node": "6.5", + "deno": "1", + "ios": "10", + "samsung": "5", + "opera_mobile": "41", + "electron": "1.2" + }, + "transform-destructuring": { + "chrome": "51", + "opera": "38", + "edge": "15", + "firefox": "53", + "safari": "10", + "node": "6.5", + "deno": "1", + "ios": "10", + "samsung": "5", + "opera_mobile": "41", + "electron": "1.2" + }, + "transform-block-scoping": { + "chrome": "50", + "opera": "37", + "edge": "14", + "firefox": "53", + "safari": "10", + "node": "6", + "deno": "1", + "ios": "10", + "samsung": "5", + "opera_mobile": "37", + "electron": "1.1" + } +} diff --git a/node_modules/@babel/compat-data/data/plugins.json b/node_modules/@babel/compat-data/data/plugins.json new file mode 100644 index 0000000000000000000000000000000000000000..81e6de4ef58a38bccac0bc6628392f8d41a08edb --- /dev/null +++ b/node_modules/@babel/compat-data/data/plugins.json @@ -0,0 +1,843 @@ +{ + "transform-explicit-resource-management": { + "chrome": "141", + "edge": "141", + "firefox": "141", + "node": "25", + "electron": "39.0" + }, + "transform-duplicate-named-capturing-groups-regex": { + "chrome": "126", + "opera": "112", + "edge": "126", + "firefox": "129", + "safari": "17.4", + "node": "23", + "ios": "17.4", + "rhino": "1.9", + "electron": "31.0" + }, + "transform-regexp-modifiers": { + "chrome": "125", + "opera": "111", + "edge": "125", + "firefox": "132", + "node": "23", + "samsung": "27", + "electron": "31.0" + }, + "transform-unicode-sets-regex": { + "chrome": "112", + "opera": "98", + "edge": "112", + "firefox": "116", + "safari": "17", + "node": "20", + "deno": "1.32", + "ios": "17", + "samsung": "23", + "opera_mobile": "75", + "electron": "24.0" + }, + "bugfix/transform-v8-static-class-fields-redefine-readonly": { + "chrome": "98", + "opera": "84", + "edge": "98", + "firefox": "75", + "safari": "15", + "node": "12", + "deno": "1.18", + "ios": "15", + "samsung": "11", + "opera_mobile": "52", + "electron": "17.0" + }, + "bugfix/transform-firefox-class-in-computed-class-key": { + "chrome": "74", + "opera": "62", + "edge": "79", + "firefox": "126", + "safari": "16", + "node": "12", + "deno": "1", + "ios": "16", + "samsung": "11", + "opera_mobile": "53", + "electron": "6.0" + }, + "bugfix/transform-safari-class-field-initializer-scope": { + "chrome": "74", + "opera": "62", + "edge": "79", + "firefox": "69", + "safari": "16", + "node": "12", + "deno": "1", + "ios": "16", + "samsung": "11", + "opera_mobile": "53", + "electron": "6.0" + }, + "transform-class-static-block": { + "chrome": "94", + "opera": "80", + "edge": "94", + "firefox": "93", + "safari": "16.4", + "node": "16.11", + "deno": "1.14", + "ios": "16.4", + "samsung": "17", + "opera_mobile": "66", + "electron": "15.0" + }, + "proposal-class-static-block": { + "chrome": "94", + "opera": "80", + "edge": "94", + "firefox": "93", + "safari": "16.4", + "node": "16.11", + "deno": "1.14", + "ios": "16.4", + "samsung": "17", + "opera_mobile": "66", + "electron": "15.0" + }, + "transform-private-property-in-object": { + "chrome": "91", + "opera": "77", + "edge": "91", + "firefox": "90", + "safari": "15", + "node": "16.9", + "deno": "1.9", + "ios": "15", + "samsung": "16", + "opera_mobile": "64", + "electron": "13.0" + }, + "proposal-private-property-in-object": { + "chrome": "91", + "opera": "77", + "edge": "91", + "firefox": "90", + "safari": "15", + "node": "16.9", + "deno": "1.9", + "ios": "15", + "samsung": "16", + "opera_mobile": "64", + "electron": "13.0" + }, + "transform-class-properties": { + "chrome": "74", + "opera": "62", + "edge": "79", + "firefox": "90", + "safari": "14.1", + "node": "12", + "deno": "1", + "ios": "14.5", + "samsung": "11", + "opera_mobile": "53", + "electron": "6.0" + }, + "proposal-class-properties": { + "chrome": "74", + "opera": "62", + "edge": "79", + "firefox": "90", + "safari": "14.1", + "node": "12", + "deno": "1", + "ios": "14.5", + "samsung": "11", + "opera_mobile": "53", + "electron": "6.0" + }, + "transform-private-methods": { + "chrome": "84", + "opera": "70", + "edge": "84", + "firefox": "90", + "safari": "15", + "node": "14.6", + "deno": "1", + "ios": "15", + "samsung": "14", + "opera_mobile": "60", + "electron": "10.0" + }, + "proposal-private-methods": { + "chrome": "84", + "opera": "70", + "edge": "84", + "firefox": "90", + "safari": "15", + "node": "14.6", + "deno": "1", + "ios": "15", + "samsung": "14", + "opera_mobile": "60", + "electron": "10.0" + }, + "transform-numeric-separator": { + "chrome": "75", + "opera": "62", + "edge": "79", + "firefox": "70", + "safari": "13", + "node": "12.5", + "deno": "1", + "ios": "13", + "samsung": "11", + "rhino": "1.7.14", + "opera_mobile": "54", + "electron": "6.0" + }, + "proposal-numeric-separator": { + "chrome": "75", + "opera": "62", + "edge": "79", + "firefox": "70", + "safari": "13", + "node": "12.5", + "deno": "1", + "ios": "13", + "samsung": "11", + "rhino": "1.7.14", + "opera_mobile": "54", + "electron": "6.0" + }, + "transform-logical-assignment-operators": { + "chrome": "85", + "opera": "71", + "edge": "85", + "firefox": "79", + "safari": "14", + "node": "15", + "deno": "1.2", + "ios": "14", + "samsung": "14", + "opera_mobile": "60", + "electron": "10.0" + }, + "proposal-logical-assignment-operators": { + "chrome": "85", + "opera": "71", + "edge": "85", + "firefox": "79", + "safari": "14", + "node": "15", + "deno": "1.2", + "ios": "14", + "samsung": "14", + "opera_mobile": "60", + "electron": "10.0" + }, + "transform-nullish-coalescing-operator": { + "chrome": "80", + "opera": "67", + "edge": "80", + "firefox": "72", + "safari": "13.1", + "node": "14", + "deno": "1", + "ios": "13.4", + "samsung": "13", + "rhino": "1.8", + "opera_mobile": "57", + "electron": "8.0" + }, + "proposal-nullish-coalescing-operator": { + "chrome": "80", + "opera": "67", + "edge": "80", + "firefox": "72", + "safari": "13.1", + "node": "14", + "deno": "1", + "ios": "13.4", + "samsung": "13", + "rhino": "1.8", + "opera_mobile": "57", + "electron": "8.0" + }, + "transform-optional-chaining": { + "chrome": "91", + "opera": "77", + "edge": "91", + "firefox": "74", + "safari": "13.1", + "node": "16.9", + "deno": "1.9", + "ios": "13.4", + "samsung": "16", + "opera_mobile": "64", + "electron": "13.0" + }, + "proposal-optional-chaining": { + "chrome": "91", + "opera": "77", + "edge": "91", + "firefox": "74", + "safari": "13.1", + "node": "16.9", + "deno": "1.9", + "ios": "13.4", + "samsung": "16", + "opera_mobile": "64", + "electron": "13.0" + }, + "transform-json-strings": { + "chrome": "66", + "opera": "53", + "edge": "79", + "firefox": "62", + "safari": "12", + "node": "10", + "deno": "1", + "ios": "12", + "samsung": "9", + "rhino": "1.7.14", + "opera_mobile": "47", + "electron": "3.0" + }, + "proposal-json-strings": { + "chrome": "66", + "opera": "53", + "edge": "79", + "firefox": "62", + "safari": "12", + "node": "10", + "deno": "1", + "ios": "12", + "samsung": "9", + "rhino": "1.7.14", + "opera_mobile": "47", + "electron": "3.0" + }, + "transform-optional-catch-binding": { + "chrome": "66", + "opera": "53", + "edge": "79", + "firefox": "58", + "safari": "11.1", + "node": "10", + "deno": "1", + "ios": "11.3", + "samsung": "9", + "opera_mobile": "47", + "electron": "3.0" + }, + "proposal-optional-catch-binding": { + "chrome": "66", + "opera": "53", + "edge": "79", + "firefox": "58", + "safari": "11.1", + "node": "10", + "deno": "1", + "ios": "11.3", + "samsung": "9", + "opera_mobile": "47", + "electron": "3.0" + }, + "transform-parameters": { + "chrome": "49", + "opera": "36", + "edge": "18", + "firefox": "52", + "safari": "16.3", + "node": "6", + "deno": "1", + "ios": "16.3", + "samsung": "5", + "opera_mobile": "36", + "electron": "0.37" + }, + "transform-async-generator-functions": { + "chrome": "63", + "opera": "50", + "edge": "79", + "firefox": "57", + "safari": "12", + "node": "10", + "deno": "1", + "ios": "12", + "samsung": "8", + "opera_mobile": "46", + "electron": "3.0" + }, + "proposal-async-generator-functions": { + "chrome": "63", + "opera": "50", + "edge": "79", + "firefox": "57", + "safari": "12", + "node": "10", + "deno": "1", + "ios": "12", + "samsung": "8", + "opera_mobile": "46", + "electron": "3.0" + }, + "transform-object-rest-spread": { + "chrome": "60", + "opera": "47", + "edge": "79", + "firefox": "55", + "safari": "11.1", + "node": "8.3", + "deno": "1", + "ios": "11.3", + "samsung": "8", + "opera_mobile": "44", + "electron": "2.0" + }, + "proposal-object-rest-spread": { + "chrome": "60", + "opera": "47", + "edge": "79", + "firefox": "55", + "safari": "11.1", + "node": "8.3", + "deno": "1", + "ios": "11.3", + "samsung": "8", + "opera_mobile": "44", + "electron": "2.0" + }, + "transform-dotall-regex": { + "chrome": "62", + "opera": "49", + "edge": "79", + "firefox": "78", + "safari": "11.1", + "node": "8.10", + "deno": "1", + "ios": "11.3", + "samsung": "8", + "rhino": "1.7.15", + "opera_mobile": "46", + "electron": "3.0" + }, + "transform-unicode-property-regex": { + "chrome": "64", + "opera": "51", + "edge": "79", + "firefox": "78", + "safari": "11.1", + "node": "10", + "deno": "1", + "ios": "11.3", + "samsung": "9", + "rhino": "1.9", + "opera_mobile": "47", + "electron": "3.0" + }, + "proposal-unicode-property-regex": { + "chrome": "64", + "opera": "51", + "edge": "79", + "firefox": "78", + "safari": "11.1", + "node": "10", + "deno": "1", + "ios": "11.3", + "samsung": "9", + "rhino": "1.9", + "opera_mobile": "47", + "electron": "3.0" + }, + "transform-named-capturing-groups-regex": { + "chrome": "64", + "opera": "51", + "edge": "79", + "firefox": "78", + "safari": "11.1", + "node": "10", + "deno": "1", + "ios": "11.3", + "samsung": "9", + "rhino": "1.9", + "opera_mobile": "47", + "electron": "3.0" + }, + "transform-async-to-generator": { + "chrome": "55", + "opera": "42", + "edge": "15", + "firefox": "52", + "safari": "11", + "node": "7.6", + "deno": "1", + "ios": "11", + "samsung": "6", + "opera_mobile": "42", + "electron": "1.6" + }, + "transform-exponentiation-operator": { + "chrome": "52", + "opera": "39", + "edge": "14", + "firefox": "52", + "safari": "10.1", + "node": "7", + "deno": "1", + "ios": "10.3", + "samsung": "6", + "rhino": "1.7.14", + "opera_mobile": "41", + "electron": "1.3" + }, + "transform-template-literals": { + "chrome": "41", + "opera": "28", + "edge": "13", + "firefox": "34", + "safari": "13", + "node": "4", + "deno": "1", + "ios": "13", + "samsung": "3.4", + "rhino": "1.9", + "opera_mobile": "28", + "electron": "0.21" + }, + "transform-literals": { + "chrome": "44", + "opera": "31", + "edge": "12", + "firefox": "53", + "safari": "9", + "node": "4", + "deno": "1", + "ios": "9", + "samsung": "4", + "rhino": "1.7.15", + "opera_mobile": "32", + "electron": "0.30" + }, + "transform-function-name": { + "chrome": "51", + "opera": "38", + "edge": "79", + "firefox": "53", + "safari": "10", + "node": "6.5", + "deno": "1", + "ios": "10", + "samsung": "5", + "opera_mobile": "41", + "electron": "1.2" + }, + "transform-arrow-functions": { + "chrome": "47", + "opera": "34", + "edge": "13", + "firefox": "43", + "safari": "10", + "node": "6", + "deno": "1", + "ios": "10", + "samsung": "5", + "rhino": "1.7.13", + "opera_mobile": "34", + "electron": "0.36" + }, + "transform-block-scoped-functions": { + "chrome": "41", + "opera": "28", + "edge": "12", + "firefox": "46", + "safari": "10", + "node": "4", + "deno": "1", + "ie": "11", + "ios": "10", + "samsung": "3.4", + "opera_mobile": "28", + "electron": "0.21" + }, + "transform-classes": { + "chrome": "46", + "opera": "33", + "edge": "13", + "firefox": "45", + "safari": "10", + "node": "5", + "deno": "1", + "ios": "10", + "samsung": "5", + "opera_mobile": "33", + "electron": "0.36" + }, + "transform-object-super": { + "chrome": "46", + "opera": "33", + "edge": "13", + "firefox": "45", + "safari": "10", + "node": "5", + "deno": "1", + "ios": "10", + "samsung": "5", + "opera_mobile": "33", + "electron": "0.36" + }, + "transform-shorthand-properties": { + "chrome": "43", + "opera": "30", + "edge": "12", + "firefox": "33", + "safari": "9", + "node": "4", + "deno": "1", + "ios": "9", + "samsung": "4", + "rhino": "1.7.14", + "opera_mobile": "30", + "electron": "0.27" + }, + "transform-duplicate-keys": { + "chrome": "42", + "opera": "29", + "edge": "12", + "firefox": "34", + "safari": "9", + "node": "4", + "deno": "1", + "ios": "9", + "samsung": "3.4", + "opera_mobile": "29", + "electron": "0.25" + }, + "transform-computed-properties": { + "chrome": "44", + "opera": "31", + "edge": "12", + "firefox": "34", + "safari": "7.1", + "node": "4", + "deno": "1", + "ios": "8", + "samsung": "4", + "rhino": "1.8", + "opera_mobile": "32", + "electron": "0.30" + }, + "transform-for-of": { + "chrome": "51", + "opera": "38", + "edge": "15", + "firefox": "53", + "safari": "10", + "node": "6.5", + "deno": "1", + "ios": "10", + "samsung": "5", + "opera_mobile": "41", + "electron": "1.2" + }, + "transform-sticky-regex": { + "chrome": "49", + "opera": "36", + "edge": "13", + "firefox": "3", + "safari": "10", + "node": "6", + "deno": "1", + "ios": "10", + "samsung": "5", + "rhino": "1.7.15", + "opera_mobile": "36", + "electron": "0.37" + }, + "transform-unicode-escapes": { + "chrome": "44", + "opera": "31", + "edge": "12", + "firefox": "53", + "safari": "9", + "node": "4", + "deno": "1", + "ios": "9", + "samsung": "4", + "rhino": "1.7.15", + "opera_mobile": "32", + "electron": "0.30" + }, + "transform-unicode-regex": { + "chrome": "50", + "opera": "37", + "edge": "13", + "firefox": "46", + "safari": "12", + "node": "6", + "deno": "1", + "ios": "12", + "samsung": "5", + "opera_mobile": "37", + "electron": "1.1" + }, + "transform-spread": { + "chrome": "46", + "opera": "33", + "edge": "13", + "firefox": "45", + "safari": "10", + "node": "5", + "deno": "1", + "ios": "10", + "samsung": "5", + "opera_mobile": "33", + "electron": "0.36" + }, + "transform-destructuring": { + "chrome": "51", + "opera": "38", + "edge": "15", + "firefox": "53", + "safari": "14.1", + "node": "6.5", + "deno": "1", + "ios": "14.5", + "samsung": "5", + "opera_mobile": "41", + "electron": "1.2" + }, + "transform-block-scoping": { + "chrome": "50", + "opera": "37", + "edge": "14", + "firefox": "53", + "safari": "11", + "node": "6", + "deno": "1", + "ios": "11", + "samsung": "5", + "opera_mobile": "37", + "electron": "1.1" + }, + "transform-typeof-symbol": { + "chrome": "48", + "opera": "35", + "edge": "12", + "firefox": "36", + "safari": "9", + "node": "6", + "deno": "1", + "ios": "9", + "samsung": "5", + "rhino": "1.8", + "opera_mobile": "35", + "electron": "0.37" + }, + "transform-new-target": { + "chrome": "46", + "opera": "33", + "edge": "14", + "firefox": "41", + "safari": "10", + "node": "5", + "deno": "1", + "ios": "10", + "samsung": "5", + "opera_mobile": "33", + "electron": "0.36" + }, + "transform-regenerator": { + "chrome": "50", + "opera": "37", + "edge": "13", + "firefox": "53", + "safari": "10", + "node": "6", + "deno": "1", + "ios": "10", + "samsung": "5", + "opera_mobile": "37", + "electron": "1.1" + }, + "transform-member-expression-literals": { + "chrome": "7", + "opera": "12", + "edge": "12", + "firefox": "2", + "safari": "5.1", + "node": "0.4", + "deno": "1", + "ie": "9", + "android": "4", + "ios": "6", + "phantom": "1.9", + "samsung": "1", + "rhino": "1.7.13", + "opera_mobile": "12", + "electron": "0.20" + }, + "transform-property-literals": { + "chrome": "7", + "opera": "12", + "edge": "12", + "firefox": "2", + "safari": "5.1", + "node": "0.4", + "deno": "1", + "ie": "9", + "android": "4", + "ios": "6", + "phantom": "1.9", + "samsung": "1", + "rhino": "1.7.13", + "opera_mobile": "12", + "electron": "0.20" + }, + "transform-reserved-words": { + "chrome": "13", + "opera": "10.50", + "edge": "12", + "firefox": "2", + "safari": "3.1", + "node": "0.6", + "deno": "1", + "ie": "9", + "android": "4.4", + "ios": "6", + "phantom": "1.9", + "samsung": "1", + "rhino": "1.7.13", + "opera_mobile": "10.1", + "electron": "0.20" + }, + "transform-export-namespace-from": { + "chrome": "72", + "deno": "1.0", + "edge": "79", + "firefox": "80", + "node": "13.2.0", + "opera": "60", + "opera_mobile": "51", + "safari": "14.1", + "ios": "14.5", + "samsung": "11.0", + "android": "72", + "electron": "5.0" + }, + "proposal-export-namespace-from": { + "chrome": "72", + "deno": "1.0", + "edge": "79", + "firefox": "80", + "node": "13.2.0", + "opera": "60", + "opera_mobile": "51", + "safari": "14.1", + "ios": "14.5", + "samsung": "11.0", + "android": "72", + "electron": "5.0" + } +} diff --git a/node_modules/@babel/compat-data/native-modules.js b/node_modules/@babel/compat-data/native-modules.js new file mode 100644 index 0000000000000000000000000000000000000000..f8c25fa37c725bf198ca05821ec0515e377fe137 --- /dev/null +++ b/node_modules/@babel/compat-data/native-modules.js @@ -0,0 +1,2 @@ +// Todo (Babel 8): remove this file, in Babel 8 users import the .json directly +module.exports = require("./data/native-modules.json"); diff --git a/node_modules/@babel/compat-data/overlapping-plugins.js b/node_modules/@babel/compat-data/overlapping-plugins.js new file mode 100644 index 0000000000000000000000000000000000000000..0dd35f1573f98ceb7d2c947e658b8bd0fcf639bb --- /dev/null +++ b/node_modules/@babel/compat-data/overlapping-plugins.js @@ -0,0 +1,2 @@ +// Todo (Babel 8): remove this file, in Babel 8 users import the .json directly +module.exports = require("./data/overlapping-plugins.json"); diff --git a/node_modules/@babel/compat-data/package.json b/node_modules/@babel/compat-data/package.json new file mode 100644 index 0000000000000000000000000000000000000000..9a59bd07b6c4f9557ad3d9bd74fd9398e87a096e --- /dev/null +++ b/node_modules/@babel/compat-data/package.json @@ -0,0 +1,40 @@ +{ + "name": "@babel/compat-data", + "version": "7.29.7", + "author": "The Babel Team (https://babel.dev/team)", + "license": "MIT", + "description": "The compat-data to determine required Babel plugins", + "repository": { + "type": "git", + "url": "https://github.com/babel/babel.git", + "directory": "packages/babel-compat-data" + }, + "publishConfig": { + "access": "public" + }, + "exports": { + "./plugins": "./plugins.js", + "./native-modules": "./native-modules.js", + "./corejs2-built-ins": "./corejs2-built-ins.js", + "./corejs3-shipped-proposals": "./corejs3-shipped-proposals.js", + "./overlapping-plugins": "./overlapping-plugins.js", + "./plugin-bugfixes": "./plugin-bugfixes.js" + }, + "scripts": { + "build-data": "./scripts/download-compat-table.sh && node ./scripts/build-data.mjs && node ./scripts/build-modules-support.mjs && node ./scripts/build-bugfixes-targets.mjs" + }, + "keywords": [ + "babel", + "compat-table", + "compat-data" + ], + "devDependencies": { + "@mdn/browser-compat-data": "^6.0.8", + "core-js-compat": "^3.48.0", + "electron-to-chromium": "^1.5.278" + }, + "engines": { + "node": ">=6.9.0" + }, + "type": "commonjs" +} \ No newline at end of file diff --git a/node_modules/@babel/compat-data/plugin-bugfixes.js b/node_modules/@babel/compat-data/plugin-bugfixes.js new file mode 100644 index 0000000000000000000000000000000000000000..9aaf3641701370ff57b1a2866a0e5c811cf7ac04 --- /dev/null +++ b/node_modules/@babel/compat-data/plugin-bugfixes.js @@ -0,0 +1,2 @@ +// Todo (Babel 8): remove this file, in Babel 8 users import the .json directly +module.exports = require("./data/plugin-bugfixes.json"); diff --git a/node_modules/@babel/compat-data/plugins.js b/node_modules/@babel/compat-data/plugins.js new file mode 100644 index 0000000000000000000000000000000000000000..b191017be6a865bb2760ca351d9f1e610693468c --- /dev/null +++ b/node_modules/@babel/compat-data/plugins.js @@ -0,0 +1,2 @@ +// Todo (Babel 8): remove this file, in Babel 8 users import the .json directly +module.exports = require("./data/plugins.json"); diff --git a/node_modules/@babel/core/LICENSE b/node_modules/@babel/core/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..f31575ec773bb199aeb7c0d0f1612cfe1c7038f1 --- /dev/null +++ b/node_modules/@babel/core/LICENSE @@ -0,0 +1,22 @@ +MIT License + +Copyright (c) 2014-present Sebastian McKenzie and other contributors + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/node_modules/@babel/core/README.md b/node_modules/@babel/core/README.md new file mode 100644 index 0000000000000000000000000000000000000000..2903543469699795279ec0c977ee56114bd21e01 --- /dev/null +++ b/node_modules/@babel/core/README.md @@ -0,0 +1,19 @@ +# @babel/core + +> Babel compiler core. + +See our website [@babel/core](https://babeljs.io/docs/babel-core) for more information or the [issues](https://github.com/babel/babel/issues?utf8=%E2%9C%93&q=is%3Aissue+label%3A%22pkg%3A%20core%22+is%3Aopen) associated with this package. + +## Install + +Using npm: + +```sh +npm install --save-dev @babel/core +``` + +or using yarn: + +```sh +yarn add @babel/core --dev +``` diff --git a/node_modules/@babel/core/lib/config/cache-contexts.js b/node_modules/@babel/core/lib/config/cache-contexts.js new file mode 100644 index 0000000000000000000000000000000000000000..f2ececdae59d09ca55a8961edd77db7baf7b8bd5 --- /dev/null +++ b/node_modules/@babel/core/lib/config/cache-contexts.js @@ -0,0 +1,5 @@ +"use strict"; + +0 && 0; + +//# sourceMappingURL=cache-contexts.js.map diff --git a/node_modules/@babel/core/lib/config/cache-contexts.js.map b/node_modules/@babel/core/lib/config/cache-contexts.js.map new file mode 100644 index 0000000000000000000000000000000000000000..39b1898d30e8184fd5d489488be517e50f0a75bf --- /dev/null +++ b/node_modules/@babel/core/lib/config/cache-contexts.js.map @@ -0,0 +1 @@ +{"version":3,"names":[],"sources":["../../src/config/cache-contexts.ts"],"sourcesContent":["import type { ConfigContext } from \"./config-chain.ts\";\nimport type {\n CallerMetadata,\n TargetsListOrObject,\n} from \"./validation/options.ts\";\n\nexport type { ConfigContext as FullConfig };\n\nexport type FullPreset = {\n targets: TargetsListOrObject;\n} & ConfigContext;\nexport type FullPlugin = {\n assumptions: Record;\n} & FullPreset;\n\n// Context not including filename since it is used in places that cannot\n// process 'ignore'/'only' and other filename-based logic.\nexport type SimpleConfig = {\n envName: string;\n caller: CallerMetadata | undefined;\n};\nexport type SimplePreset = {\n targets: TargetsListOrObject;\n} & SimpleConfig;\nexport type SimplePlugin = {\n assumptions: Record;\n} & SimplePreset;\n"],"mappings":"","ignoreList":[]} \ No newline at end of file diff --git a/node_modules/@babel/core/lib/config/caching.js b/node_modules/@babel/core/lib/config/caching.js new file mode 100644 index 0000000000000000000000000000000000000000..344c8390e3d054513e7c797fef4cfa98f0ba30f7 --- /dev/null +++ b/node_modules/@babel/core/lib/config/caching.js @@ -0,0 +1,261 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.assertSimpleType = assertSimpleType; +exports.makeStrongCache = makeStrongCache; +exports.makeStrongCacheSync = makeStrongCacheSync; +exports.makeWeakCache = makeWeakCache; +exports.makeWeakCacheSync = makeWeakCacheSync; +function _gensync() { + const data = require("gensync"); + _gensync = function () { + return data; + }; + return data; +} +var _async = require("../gensync-utils/async.js"); +var _util = require("./util.js"); +const synchronize = gen => { + return _gensync()(gen).sync; +}; +function* genTrue() { + return true; +} +function makeWeakCache(handler) { + return makeCachedFunction(WeakMap, handler); +} +function makeWeakCacheSync(handler) { + return synchronize(makeWeakCache(handler)); +} +function makeStrongCache(handler) { + return makeCachedFunction(Map, handler); +} +function makeStrongCacheSync(handler) { + return synchronize(makeStrongCache(handler)); +} +function makeCachedFunction(CallCache, handler) { + const callCacheSync = new CallCache(); + const callCacheAsync = new CallCache(); + const futureCache = new CallCache(); + return function* cachedFunction(arg, data) { + const asyncContext = yield* (0, _async.isAsync)(); + const callCache = asyncContext ? callCacheAsync : callCacheSync; + const cached = yield* getCachedValueOrWait(asyncContext, callCache, futureCache, arg, data); + if (cached.valid) return cached.value; + const cache = new CacheConfigurator(data); + const handlerResult = handler(arg, cache); + let finishLock; + let value; + if ((0, _util.isIterableIterator)(handlerResult)) { + value = yield* (0, _async.onFirstPause)(handlerResult, () => { + finishLock = setupAsyncLocks(cache, futureCache, arg); + }); + } else { + value = handlerResult; + } + updateFunctionCache(callCache, cache, arg, value); + if (finishLock) { + futureCache.delete(arg); + finishLock.release(value); + } + return value; + }; +} +function* getCachedValue(cache, arg, data) { + const cachedValue = cache.get(arg); + if (cachedValue) { + for (const { + value, + valid + } of cachedValue) { + if (yield* valid(data)) return { + valid: true, + value + }; + } + } + return { + valid: false, + value: null + }; +} +function* getCachedValueOrWait(asyncContext, callCache, futureCache, arg, data) { + const cached = yield* getCachedValue(callCache, arg, data); + if (cached.valid) { + return cached; + } + if (asyncContext) { + const cached = yield* getCachedValue(futureCache, arg, data); + if (cached.valid) { + const value = yield* (0, _async.waitFor)(cached.value.promise); + return { + valid: true, + value + }; + } + } + return { + valid: false, + value: null + }; +} +function setupAsyncLocks(config, futureCache, arg) { + const finishLock = new Lock(); + updateFunctionCache(futureCache, config, arg, finishLock); + return finishLock; +} +function updateFunctionCache(cache, config, arg, value) { + if (!config.configured()) config.forever(); + let cachedValue = cache.get(arg); + config.deactivate(); + switch (config.mode()) { + case "forever": + cachedValue = [{ + value, + valid: genTrue + }]; + cache.set(arg, cachedValue); + break; + case "invalidate": + cachedValue = [{ + value, + valid: config.validator() + }]; + cache.set(arg, cachedValue); + break; + case "valid": + if (cachedValue) { + cachedValue.push({ + value, + valid: config.validator() + }); + } else { + cachedValue = [{ + value, + valid: config.validator() + }]; + cache.set(arg, cachedValue); + } + } +} +class CacheConfigurator { + constructor(data) { + this._active = true; + this._never = false; + this._forever = false; + this._invalidate = false; + this._configured = false; + this._pairs = []; + this._data = void 0; + this._data = data; + } + simple() { + return makeSimpleConfigurator(this); + } + mode() { + if (this._never) return "never"; + if (this._forever) return "forever"; + if (this._invalidate) return "invalidate"; + return "valid"; + } + forever() { + if (!this._active) { + throw new Error("Cannot change caching after evaluation has completed."); + } + if (this._never) { + throw new Error("Caching has already been configured with .never()"); + } + this._forever = true; + this._configured = true; + } + never() { + if (!this._active) { + throw new Error("Cannot change caching after evaluation has completed."); + } + if (this._forever) { + throw new Error("Caching has already been configured with .forever()"); + } + this._never = true; + this._configured = true; + } + using(handler) { + if (!this._active) { + throw new Error("Cannot change caching after evaluation has completed."); + } + if (this._never || this._forever) { + throw new Error("Caching has already been configured with .never or .forever()"); + } + this._configured = true; + const key = handler(this._data); + const fn = (0, _async.maybeAsync)(handler, `You appear to be using an async cache handler, but Babel has been called synchronously`); + if ((0, _async.isThenable)(key)) { + return key.then(key => { + this._pairs.push([key, fn]); + return key; + }); + } + this._pairs.push([key, fn]); + return key; + } + invalidate(handler) { + this._invalidate = true; + return this.using(handler); + } + validator() { + const pairs = this._pairs; + return function* (data) { + for (const [key, fn] of pairs) { + if (key !== (yield* fn(data))) return false; + } + return true; + }; + } + deactivate() { + this._active = false; + } + configured() { + return this._configured; + } +} +function makeSimpleConfigurator(cache) { + function cacheFn(val) { + if (typeof val === "boolean") { + if (val) cache.forever();else cache.never(); + return; + } + return cache.using(() => assertSimpleType(val())); + } + cacheFn.forever = () => cache.forever(); + cacheFn.never = () => cache.never(); + cacheFn.using = cb => cache.using(() => assertSimpleType(cb())); + cacheFn.invalidate = cb => cache.invalidate(() => assertSimpleType(cb())); + return cacheFn; +} +function assertSimpleType(value) { + if ((0, _async.isThenable)(value)) { + throw new Error(`You appear to be using an async cache handler, ` + `which your current version of Babel does not support. ` + `We may add support for this in the future, ` + `but if you're on the most recent version of @babel/core and still ` + `seeing this error, then you'll need to synchronously handle your caching logic.`); + } + if (value != null && typeof value !== "string" && typeof value !== "boolean" && typeof value !== "number") { + throw new Error("Cache keys must be either string, boolean, number, null, or undefined."); + } + return value; +} +class Lock { + constructor() { + this.released = false; + this.promise = void 0; + this._resolve = void 0; + this.promise = new Promise(resolve => { + this._resolve = resolve; + }); + } + release(value) { + this.released = true; + this._resolve(value); + } +} +0 && 0; + +//# sourceMappingURL=caching.js.map diff --git a/node_modules/@babel/core/lib/config/caching.js.map b/node_modules/@babel/core/lib/config/caching.js.map new file mode 100644 index 0000000000000000000000000000000000000000..c9a69fd65d4727620da46808e2112a1e76eb7a38 --- /dev/null +++ b/node_modules/@babel/core/lib/config/caching.js.map @@ -0,0 +1 @@ +{"version":3,"names":["_gensync","data","require","_async","_util","synchronize","gen","gensync","sync","genTrue","makeWeakCache","handler","makeCachedFunction","WeakMap","makeWeakCacheSync","makeStrongCache","Map","makeStrongCacheSync","CallCache","callCacheSync","callCacheAsync","futureCache","cachedFunction","arg","asyncContext","isAsync","callCache","cached","getCachedValueOrWait","valid","value","cache","CacheConfigurator","handlerResult","finishLock","isIterableIterator","onFirstPause","setupAsyncLocks","updateFunctionCache","delete","release","getCachedValue","cachedValue","get","waitFor","promise","config","Lock","configured","forever","deactivate","mode","set","validator","push","constructor","_active","_never","_forever","_invalidate","_configured","_pairs","_data","simple","makeSimpleConfigurator","Error","never","using","key","fn","maybeAsync","isThenable","then","invalidate","pairs","cacheFn","val","assertSimpleType","cb","released","_resolve","Promise","resolve"],"sources":["../../src/config/caching.ts"],"sourcesContent":["import gensync from \"gensync\";\nimport type { Handler } from \"gensync\";\nimport {\n maybeAsync,\n isAsync,\n onFirstPause,\n waitFor,\n isThenable,\n} from \"../gensync-utils/async.ts\";\nimport { isIterableIterator } from \"./util.ts\";\n\nexport type { CacheConfigurator };\n\nexport type SimpleCacheConfigurator = {\n (forever: boolean): void;\n (handler: () => T): T;\n\n forever: () => void;\n never: () => void;\n using: (handler: () => T) => T;\n invalidate: (handler: () => T) => T;\n};\n\nexport type CacheEntry = {\n value: ResultT;\n valid: (channel: SideChannel) => Handler;\n}[];\n\nconst synchronize = (\n gen: (...args: ArgsT) => Handler,\n): ((...args: ArgsT) => ResultT) => {\n return gensync(gen).sync;\n};\n\n// eslint-disable-next-line require-yield\nfunction* genTrue() {\n return true;\n}\n\nexport function makeWeakCache(\n handler: (\n arg: ArgT,\n cache: CacheConfigurator,\n ) => Handler | ResultT,\n): (arg: ArgT, data: SideChannel) => Handler {\n return makeCachedFunction(WeakMap, handler);\n}\n\nexport function makeWeakCacheSync(\n handler: (arg: ArgT, cache?: CacheConfigurator) => ResultT,\n): (arg: ArgT, data?: SideChannel) => ResultT {\n return synchronize<[ArgT, SideChannel], ResultT>(\n makeWeakCache(handler),\n );\n}\n\nexport function makeStrongCache(\n handler: (\n arg: ArgT,\n cache: CacheConfigurator,\n ) => Handler | ResultT,\n): (arg: ArgT, data: SideChannel) => Handler {\n return makeCachedFunction(Map, handler);\n}\n\nexport function makeStrongCacheSync(\n handler: (arg: ArgT, cache?: CacheConfigurator) => ResultT,\n): (arg: ArgT, data?: SideChannel) => ResultT {\n return synchronize<[ArgT, SideChannel], ResultT>(\n makeStrongCache(handler),\n );\n}\n\n/* NOTE: Part of the logic explained in this comment is explained in the\n * getCachedValueOrWait and setupAsyncLocks functions.\n *\n * > There are only two hard things in Computer Science: cache invalidation and naming things.\n * > -- Phil Karlton\n *\n * I don't know if Phil was also thinking about handling a cache whose invalidation function is\n * defined asynchronously is considered, but it is REALLY hard to do correctly.\n *\n * The implemented logic (only when gensync is run asynchronously) is the following:\n * 1. If there is a valid cache associated to the current \"arg\" parameter,\n * a. RETURN the cached value\n * 3. If there is a FinishLock associated to the current \"arg\" parameter representing a valid cache,\n * a. Wait for that lock to be released\n * b. RETURN the value associated with that lock\n * 5. Start executing the function to be cached\n * a. If it pauses on a promise, then\n * i. Let FinishLock be a new lock\n * ii. Store FinishLock as associated to the current \"arg\" parameter\n * iii. Wait for the function to finish executing\n * iv. Release FinishLock\n * v. Send the function result to anyone waiting on FinishLock\n * 6. Store the result in the cache\n * 7. RETURN the result\n */\nfunction makeCachedFunction(\n CallCache: new () => CacheMap,\n handler: (\n arg: ArgT,\n cache: CacheConfigurator,\n ) => Handler | ResultT,\n): (arg: ArgT, data: SideChannel) => Handler {\n const callCacheSync = new CallCache();\n const callCacheAsync = new CallCache();\n const futureCache = new CallCache>();\n\n return function* cachedFunction(arg: ArgT, data: SideChannel) {\n const asyncContext = yield* isAsync();\n const callCache = asyncContext ? callCacheAsync : callCacheSync;\n\n const cached = yield* getCachedValueOrWait(\n asyncContext,\n callCache,\n futureCache,\n arg,\n data,\n );\n if (cached.valid) return cached.value;\n\n const cache = new CacheConfigurator(data);\n\n const handlerResult: Handler | ResultT = handler(arg, cache);\n\n let finishLock: Lock;\n let value: ResultT;\n\n if (isIterableIterator(handlerResult)) {\n value = yield* onFirstPause(handlerResult, () => {\n finishLock = setupAsyncLocks(cache, futureCache, arg);\n });\n } else {\n value = handlerResult;\n }\n\n updateFunctionCache(callCache, cache, arg, value);\n\n if (finishLock) {\n futureCache.delete(arg);\n finishLock.release(value);\n }\n\n return value;\n };\n}\n\ntype CacheMap =\n | Map>\n // @ts-expect-error todo(flow->ts): add `extends object` constraint to ArgT\n | WeakMap>;\n\nfunction* getCachedValue(\n cache: CacheMap,\n arg: ArgT,\n data: SideChannel,\n): Handler<{ valid: true; value: ResultT } | { valid: false; value: null }> {\n const cachedValue: CacheEntry | void = cache.get(arg);\n\n if (cachedValue) {\n for (const { value, valid } of cachedValue) {\n if (yield* valid(data)) return { valid: true, value };\n }\n }\n\n return { valid: false, value: null };\n}\n\nfunction* getCachedValueOrWait(\n asyncContext: boolean,\n callCache: CacheMap,\n futureCache: CacheMap, SideChannel>,\n arg: ArgT,\n data: SideChannel,\n): Handler<{ valid: true; value: ResultT } | { valid: false; value: null }> {\n const cached = yield* getCachedValue(callCache, arg, data);\n if (cached.valid) {\n return cached;\n }\n\n if (asyncContext) {\n const cached = yield* getCachedValue(futureCache, arg, data);\n if (cached.valid) {\n const value = yield* waitFor(cached.value.promise);\n return { valid: true, value };\n }\n }\n\n return { valid: false, value: null };\n}\n\nfunction setupAsyncLocks(\n config: CacheConfigurator,\n futureCache: CacheMap, SideChannel>,\n arg: ArgT,\n): Lock {\n const finishLock = new Lock();\n\n updateFunctionCache(futureCache, config, arg, finishLock);\n\n return finishLock;\n}\n\nfunction updateFunctionCache<\n ArgT,\n ResultT,\n SideChannel,\n Cache extends CacheMap,\n>(\n cache: Cache,\n config: CacheConfigurator,\n arg: ArgT,\n value: ResultT,\n) {\n if (!config.configured()) config.forever();\n\n let cachedValue: CacheEntry | void = cache.get(arg);\n\n config.deactivate();\n\n switch (config.mode()) {\n case \"forever\":\n cachedValue = [{ value, valid: genTrue }];\n cache.set(arg, cachedValue);\n break;\n case \"invalidate\":\n cachedValue = [{ value, valid: config.validator() }];\n cache.set(arg, cachedValue);\n break;\n case \"valid\":\n if (cachedValue) {\n cachedValue.push({ value, valid: config.validator() });\n } else {\n cachedValue = [{ value, valid: config.validator() }];\n cache.set(arg, cachedValue);\n }\n }\n}\n\nclass CacheConfigurator {\n _active: boolean = true;\n _never: boolean = false;\n _forever: boolean = false;\n _invalidate: boolean = false;\n\n _configured: boolean = false;\n\n _pairs: [\n cachedValue: unknown,\n handler: (data: SideChannel) => Handler,\n ][] = [];\n\n _data: SideChannel;\n\n constructor(data: SideChannel) {\n this._data = data;\n }\n\n simple() {\n return makeSimpleConfigurator(this);\n }\n\n mode() {\n if (this._never) return \"never\";\n if (this._forever) return \"forever\";\n if (this._invalidate) return \"invalidate\";\n return \"valid\";\n }\n\n forever() {\n if (!this._active) {\n throw new Error(\"Cannot change caching after evaluation has completed.\");\n }\n if (this._never) {\n throw new Error(\"Caching has already been configured with .never()\");\n }\n this._forever = true;\n this._configured = true;\n }\n\n never() {\n if (!this._active) {\n throw new Error(\"Cannot change caching after evaluation has completed.\");\n }\n if (this._forever) {\n throw new Error(\"Caching has already been configured with .forever()\");\n }\n this._never = true;\n this._configured = true;\n }\n\n using(handler: (data: SideChannel) => T): T {\n if (!this._active) {\n throw new Error(\"Cannot change caching after evaluation has completed.\");\n }\n if (this._never || this._forever) {\n throw new Error(\n \"Caching has already been configured with .never or .forever()\",\n );\n }\n this._configured = true;\n\n const key = handler(this._data);\n\n const fn = maybeAsync(\n handler,\n `You appear to be using an async cache handler, but Babel has been called synchronously`,\n );\n\n if (isThenable(key)) {\n // @ts-expect-error todo(flow->ts): improve function return type annotation\n return key.then((key: unknown) => {\n this._pairs.push([key, fn]);\n return key;\n });\n }\n\n this._pairs.push([key, fn]);\n return key;\n }\n\n invalidate(handler: (data: SideChannel) => T): T {\n this._invalidate = true;\n return this.using(handler);\n }\n\n validator(): (data: SideChannel) => Handler {\n const pairs = this._pairs;\n return function* (data: SideChannel) {\n for (const [key, fn] of pairs) {\n if (key !== (yield* fn(data))) return false;\n }\n return true;\n };\n }\n\n deactivate() {\n this._active = false;\n }\n\n configured() {\n return this._configured;\n }\n}\n\nfunction makeSimpleConfigurator(\n cache: CacheConfigurator,\n): SimpleCacheConfigurator {\n function cacheFn(val: any) {\n if (typeof val === \"boolean\") {\n if (val) cache.forever();\n else cache.never();\n return;\n }\n\n return cache.using(() => assertSimpleType(val()));\n }\n cacheFn.forever = () => cache.forever();\n cacheFn.never = () => cache.never();\n cacheFn.using = (cb: () => SimpleType) =>\n cache.using(() => assertSimpleType(cb()));\n cacheFn.invalidate = (cb: () => SimpleType) =>\n cache.invalidate(() => assertSimpleType(cb()));\n\n return cacheFn as any;\n}\n\n// Types are limited here so that in the future these values can be used\n// as part of Babel's caching logic.\nexport type SimpleType =\n | string\n | boolean\n | number\n | null\n | void\n | Promise;\nexport function assertSimpleType(value: unknown): SimpleType {\n if (isThenable(value)) {\n throw new Error(\n `You appear to be using an async cache handler, ` +\n `which your current version of Babel does not support. ` +\n `We may add support for this in the future, ` +\n `but if you're on the most recent version of @babel/core and still ` +\n `seeing this error, then you'll need to synchronously handle your caching logic.`,\n );\n }\n\n if (\n value != null &&\n typeof value !== \"string\" &&\n typeof value !== \"boolean\" &&\n typeof value !== \"number\"\n ) {\n throw new Error(\n \"Cache keys must be either string, boolean, number, null, or undefined.\",\n );\n }\n // @ts-expect-error Type 'unknown' is not assignable to type 'SimpleType'. This can be removed\n // when strictNullCheck is enabled\n return value;\n}\n\nclass Lock {\n released: boolean = false;\n promise: Promise;\n _resolve: (value: T) => void;\n\n constructor() {\n this.promise = new Promise(resolve => {\n this._resolve = resolve;\n });\n }\n\n release(value: T) {\n this.released = true;\n this._resolve(value);\n }\n}\n"],"mappings":"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","ignoreList":[]} \ No newline at end of file diff --git a/node_modules/@babel/core/lib/config/config-chain.js b/node_modules/@babel/core/lib/config/config-chain.js new file mode 100644 index 0000000000000000000000000000000000000000..5fded8e6b84cbc073667b8788e880f7b07025c6e --- /dev/null +++ b/node_modules/@babel/core/lib/config/config-chain.js @@ -0,0 +1,469 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.buildPresetChain = buildPresetChain; +exports.buildPresetChainWalker = void 0; +exports.buildRootChain = buildRootChain; +function _path() { + const data = require("path"); + _path = function () { + return data; + }; + return data; +} +function _debug() { + const data = require("debug"); + _debug = function () { + return data; + }; + return data; +} +var _options = require("./validation/options.js"); +var _patternToRegex = require("./pattern-to-regex.js"); +var _printer = require("./printer.js"); +var _rewriteStackTrace = require("../errors/rewrite-stack-trace.js"); +var _configError = require("../errors/config-error.js"); +var _index = require("./files/index.js"); +var _caching = require("./caching.js"); +var _configDescriptors = require("./config-descriptors.js"); +const debug = _debug()("babel:config:config-chain"); +function* buildPresetChain(arg, context) { + const chain = yield* buildPresetChainWalker(arg, context); + if (!chain) return null; + return { + plugins: dedupDescriptors(chain.plugins), + presets: dedupDescriptors(chain.presets), + options: chain.options.map(o => createConfigChainOptions(o)), + files: new Set() + }; +} +const buildPresetChainWalker = exports.buildPresetChainWalker = makeChainWalker({ + root: preset => loadPresetDescriptors(preset), + env: (preset, envName) => loadPresetEnvDescriptors(preset)(envName), + overrides: (preset, index) => loadPresetOverridesDescriptors(preset)(index), + overridesEnv: (preset, index, envName) => loadPresetOverridesEnvDescriptors(preset)(index)(envName), + createLogger: () => () => {} +}); +const loadPresetDescriptors = (0, _caching.makeWeakCacheSync)(preset => buildRootDescriptors(preset, preset.alias, _configDescriptors.createUncachedDescriptors)); +const loadPresetEnvDescriptors = (0, _caching.makeWeakCacheSync)(preset => (0, _caching.makeStrongCacheSync)(envName => buildEnvDescriptors(preset, preset.alias, _configDescriptors.createUncachedDescriptors, envName))); +const loadPresetOverridesDescriptors = (0, _caching.makeWeakCacheSync)(preset => (0, _caching.makeStrongCacheSync)(index => buildOverrideDescriptors(preset, preset.alias, _configDescriptors.createUncachedDescriptors, index))); +const loadPresetOverridesEnvDescriptors = (0, _caching.makeWeakCacheSync)(preset => (0, _caching.makeStrongCacheSync)(index => (0, _caching.makeStrongCacheSync)(envName => buildOverrideEnvDescriptors(preset, preset.alias, _configDescriptors.createUncachedDescriptors, index, envName)))); +function* buildRootChain(opts, context) { + let configReport, babelRcReport; + const programmaticLogger = new _printer.ConfigPrinter(); + const programmaticChain = yield* loadProgrammaticChain({ + options: opts, + dirname: context.cwd + }, context, undefined, programmaticLogger); + if (!programmaticChain) return null; + const programmaticReport = yield* programmaticLogger.output(); + let configFile; + if (typeof opts.configFile === "string") { + configFile = yield* (0, _index.loadConfig)(opts.configFile, context.cwd, context.envName, context.caller); + } else if (opts.configFile !== false) { + configFile = yield* (0, _index.findRootConfig)(context.root, context.envName, context.caller); + } + let { + babelrc, + babelrcRoots + } = opts; + let babelrcRootsDirectory = context.cwd; + const configFileChain = emptyChain(); + const configFileLogger = new _printer.ConfigPrinter(); + if (configFile) { + const validatedFile = validateConfigFile(configFile); + const result = yield* loadFileChain(validatedFile, context, undefined, configFileLogger); + if (!result) return null; + configReport = yield* configFileLogger.output(); + if (babelrc === undefined) { + babelrc = validatedFile.options.babelrc; + } + if (babelrcRoots === undefined) { + babelrcRootsDirectory = validatedFile.dirname; + babelrcRoots = validatedFile.options.babelrcRoots; + } + mergeChain(configFileChain, result); + } + let ignoreFile, babelrcFile; + let isIgnored = false; + const fileChain = emptyChain(); + if ((babelrc === true || babelrc === undefined) && typeof context.filename === "string") { + const pkgData = yield* (0, _index.findPackageData)(context.filename); + if (pkgData && babelrcLoadEnabled(context, pkgData, babelrcRoots, babelrcRootsDirectory)) { + ({ + ignore: ignoreFile, + config: babelrcFile + } = yield* (0, _index.findRelativeConfig)(pkgData, context.envName, context.caller)); + if (ignoreFile) { + fileChain.files.add(ignoreFile.filepath); + } + if (ignoreFile && shouldIgnore(context, ignoreFile.ignore, null, ignoreFile.dirname)) { + isIgnored = true; + } + if (babelrcFile && !isIgnored) { + const validatedFile = validateBabelrcFile(babelrcFile); + const babelrcLogger = new _printer.ConfigPrinter(); + const result = yield* loadFileChain(validatedFile, context, undefined, babelrcLogger); + if (!result) { + isIgnored = true; + } else { + babelRcReport = yield* babelrcLogger.output(); + mergeChain(fileChain, result); + } + } + if (babelrcFile && isIgnored) { + fileChain.files.add(babelrcFile.filepath); + } + } + } + if (context.showConfig) { + console.log(`Babel configs on "${context.filename}" (ascending priority):\n` + [configReport, babelRcReport, programmaticReport].filter(x => !!x).join("\n\n") + "\n-----End Babel configs-----"); + } + const chain = mergeChain(mergeChain(mergeChain(emptyChain(), configFileChain), fileChain), programmaticChain); + return { + plugins: isIgnored ? [] : dedupDescriptors(chain.plugins), + presets: isIgnored ? [] : dedupDescriptors(chain.presets), + options: isIgnored ? [] : chain.options.map(o => createConfigChainOptions(o)), + fileHandling: isIgnored ? "ignored" : "transpile", + ignore: ignoreFile || undefined, + babelrc: babelrcFile || undefined, + config: configFile || undefined, + files: chain.files + }; +} +function babelrcLoadEnabled(context, pkgData, babelrcRoots, babelrcRootsDirectory) { + if (typeof babelrcRoots === "boolean") return babelrcRoots; + const absoluteRoot = context.root; + if (babelrcRoots === undefined) { + return pkgData.directories.includes(absoluteRoot); + } + let babelrcPatterns = babelrcRoots; + if (!Array.isArray(babelrcPatterns)) { + babelrcPatterns = [babelrcPatterns]; + } + babelrcPatterns = babelrcPatterns.map(pat => { + return typeof pat === "string" ? _path().resolve(babelrcRootsDirectory, pat) : pat; + }); + if (babelrcPatterns.length === 1 && babelrcPatterns[0] === absoluteRoot) { + return pkgData.directories.includes(absoluteRoot); + } + return babelrcPatterns.some(pat => { + if (typeof pat === "string") { + pat = (0, _patternToRegex.default)(pat, babelrcRootsDirectory); + } + return pkgData.directories.some(directory => { + return matchPattern(pat, babelrcRootsDirectory, directory, context); + }); + }); +} +const validateConfigFile = (0, _caching.makeWeakCacheSync)(file => ({ + filepath: file.filepath, + dirname: file.dirname, + options: (0, _options.validate)("configfile", file.options, file.filepath) +})); +const validateBabelrcFile = (0, _caching.makeWeakCacheSync)(file => ({ + filepath: file.filepath, + dirname: file.dirname, + options: (0, _options.validate)("babelrcfile", file.options, file.filepath) +})); +const validateExtendFile = (0, _caching.makeWeakCacheSync)(file => ({ + filepath: file.filepath, + dirname: file.dirname, + options: (0, _options.validate)("extendsfile", file.options, file.filepath) +})); +const loadProgrammaticChain = makeChainWalker({ + root: input => buildRootDescriptors(input, "base", _configDescriptors.createCachedDescriptors), + env: (input, envName) => buildEnvDescriptors(input, "base", _configDescriptors.createCachedDescriptors, envName), + overrides: (input, index) => buildOverrideDescriptors(input, "base", _configDescriptors.createCachedDescriptors, index), + overridesEnv: (input, index, envName) => buildOverrideEnvDescriptors(input, "base", _configDescriptors.createCachedDescriptors, index, envName), + createLogger: (input, context, baseLogger) => buildProgrammaticLogger(input, context, baseLogger) +}); +const loadFileChainWalker = makeChainWalker({ + root: file => loadFileDescriptors(file), + env: (file, envName) => loadFileEnvDescriptors(file)(envName), + overrides: (file, index) => loadFileOverridesDescriptors(file)(index), + overridesEnv: (file, index, envName) => loadFileOverridesEnvDescriptors(file)(index)(envName), + createLogger: (file, context, baseLogger) => buildFileLogger(file.filepath, context, baseLogger) +}); +function* loadFileChain(input, context, files, baseLogger) { + const chain = yield* loadFileChainWalker(input, context, files, baseLogger); + chain == null || chain.files.add(input.filepath); + return chain; +} +const loadFileDescriptors = (0, _caching.makeWeakCacheSync)(file => buildRootDescriptors(file, file.filepath, _configDescriptors.createUncachedDescriptors)); +const loadFileEnvDescriptors = (0, _caching.makeWeakCacheSync)(file => (0, _caching.makeStrongCacheSync)(envName => buildEnvDescriptors(file, file.filepath, _configDescriptors.createUncachedDescriptors, envName))); +const loadFileOverridesDescriptors = (0, _caching.makeWeakCacheSync)(file => (0, _caching.makeStrongCacheSync)(index => buildOverrideDescriptors(file, file.filepath, _configDescriptors.createUncachedDescriptors, index))); +const loadFileOverridesEnvDescriptors = (0, _caching.makeWeakCacheSync)(file => (0, _caching.makeStrongCacheSync)(index => (0, _caching.makeStrongCacheSync)(envName => buildOverrideEnvDescriptors(file, file.filepath, _configDescriptors.createUncachedDescriptors, index, envName)))); +function buildFileLogger(filepath, context, baseLogger) { + if (!baseLogger) { + return () => {}; + } + return baseLogger.configure(context.showConfig, _printer.ChainFormatter.Config, { + filepath + }); +} +function buildRootDescriptors({ + dirname, + options +}, alias, descriptors) { + return descriptors(dirname, options, alias); +} +function buildProgrammaticLogger(_, context, baseLogger) { + var _context$caller; + if (!baseLogger) { + return () => {}; + } + return baseLogger.configure(context.showConfig, _printer.ChainFormatter.Programmatic, { + callerName: (_context$caller = context.caller) == null ? void 0 : _context$caller.name + }); +} +function buildEnvDescriptors({ + dirname, + options +}, alias, descriptors, envName) { + var _options$env; + const opts = (_options$env = options.env) == null ? void 0 : _options$env[envName]; + return opts ? descriptors(dirname, opts, `${alias}.env["${envName}"]`) : null; +} +function buildOverrideDescriptors({ + dirname, + options +}, alias, descriptors, index) { + var _options$overrides; + const opts = (_options$overrides = options.overrides) == null ? void 0 : _options$overrides[index]; + if (!opts) throw new Error("Assertion failure - missing override"); + return descriptors(dirname, opts, `${alias}.overrides[${index}]`); +} +function buildOverrideEnvDescriptors({ + dirname, + options +}, alias, descriptors, index, envName) { + var _options$overrides2, _override$env; + const override = (_options$overrides2 = options.overrides) == null ? void 0 : _options$overrides2[index]; + if (!override) throw new Error("Assertion failure - missing override"); + const opts = (_override$env = override.env) == null ? void 0 : _override$env[envName]; + return opts ? descriptors(dirname, opts, `${alias}.overrides[${index}].env["${envName}"]`) : null; +} +function makeChainWalker({ + root, + env, + overrides, + overridesEnv, + createLogger +}) { + return function* chainWalker(input, context, files = new Set(), baseLogger) { + const { + dirname + } = input; + const flattenedConfigs = []; + const rootOpts = root(input); + if (configIsApplicable(rootOpts, dirname, context, input.filepath)) { + flattenedConfigs.push({ + config: rootOpts, + envName: undefined, + index: undefined + }); + const envOpts = env(input, context.envName); + if (envOpts && configIsApplicable(envOpts, dirname, context, input.filepath)) { + flattenedConfigs.push({ + config: envOpts, + envName: context.envName, + index: undefined + }); + } + (rootOpts.options.overrides || []).forEach((_, index) => { + const overrideOps = overrides(input, index); + if (configIsApplicable(overrideOps, dirname, context, input.filepath)) { + flattenedConfigs.push({ + config: overrideOps, + index, + envName: undefined + }); + const overrideEnvOpts = overridesEnv(input, index, context.envName); + if (overrideEnvOpts && configIsApplicable(overrideEnvOpts, dirname, context, input.filepath)) { + flattenedConfigs.push({ + config: overrideEnvOpts, + index, + envName: context.envName + }); + } + } + }); + } + if (flattenedConfigs.some(({ + config: { + options: { + ignore, + only + } + } + }) => shouldIgnore(context, ignore, only, dirname))) { + return null; + } + const chain = emptyChain(); + const logger = createLogger(input, context, baseLogger); + for (const { + config, + index, + envName + } of flattenedConfigs) { + if (!(yield* mergeExtendsChain(chain, config.options, dirname, context, files, baseLogger))) { + return null; + } + logger(config, index, envName); + yield* mergeChainOpts(chain, config); + } + return chain; + }; +} +function* mergeExtendsChain(chain, opts, dirname, context, files, baseLogger) { + if (opts.extends === undefined) return true; + const file = yield* (0, _index.loadConfig)(opts.extends, dirname, context.envName, context.caller); + if (files.has(file)) { + throw new Error(`Configuration cycle detected loading ${file.filepath}.\n` + `File already loaded following the config chain:\n` + Array.from(files, file => ` - ${file.filepath}`).join("\n")); + } + files.add(file); + const fileChain = yield* loadFileChain(validateExtendFile(file), context, files, baseLogger); + files.delete(file); + if (!fileChain) return false; + mergeChain(chain, fileChain); + return true; +} +function mergeChain(target, source) { + target.options.push(...source.options); + target.plugins.push(...source.plugins); + target.presets.push(...source.presets); + for (const file of source.files) { + target.files.add(file); + } + return target; +} +function* mergeChainOpts(target, { + options, + plugins, + presets +}) { + target.options.push(options); + target.plugins.push(...(yield* plugins())); + target.presets.push(...(yield* presets())); + return target; +} +function emptyChain() { + return { + options: [], + presets: [], + plugins: [], + files: new Set() + }; +} +function createConfigChainOptions(opts) { + const options = Object.assign({}, opts); + delete options.extends; + delete options.env; + delete options.overrides; + delete options.plugins; + delete options.presets; + delete options.passPerPreset; + delete options.ignore; + delete options.only; + delete options.test; + delete options.include; + delete options.exclude; + if (hasOwnProperty.call(options, "sourceMap")) { + options.sourceMaps = options.sourceMap; + delete options.sourceMap; + } + return options; +} +function dedupDescriptors(items) { + const map = new Map(); + const descriptors = []; + for (const item of items) { + if (typeof item.value === "function") { + const fnKey = item.value; + let nameMap = map.get(fnKey); + if (!nameMap) { + nameMap = new Map(); + map.set(fnKey, nameMap); + } + let desc = nameMap.get(item.name); + if (!desc) { + desc = { + value: item + }; + descriptors.push(desc); + if (!item.ownPass) nameMap.set(item.name, desc); + } else { + desc.value = item; + } + } else { + descriptors.push({ + value: item + }); + } + } + return descriptors.reduce((acc, desc) => { + acc.push(desc.value); + return acc; + }, []); +} +function configIsApplicable({ + options +}, dirname, context, configName) { + return (options.test === undefined || configFieldIsApplicable(context, options.test, dirname, configName)) && (options.include === undefined || configFieldIsApplicable(context, options.include, dirname, configName)) && (options.exclude === undefined || !configFieldIsApplicable(context, options.exclude, dirname, configName)); +} +function configFieldIsApplicable(context, test, dirname, configName) { + const patterns = Array.isArray(test) ? test : [test]; + return matchesPatterns(context, patterns, dirname, configName); +} +function ignoreListReplacer(_key, value) { + if (value instanceof RegExp) { + return String(value); + } + return value; +} +function shouldIgnore(context, ignore, only, dirname) { + if (ignore && matchesPatterns(context, ignore, dirname)) { + var _context$filename; + const message = `No config is applied to "${(_context$filename = context.filename) != null ? _context$filename : "(unknown)"}" because it matches one of \`ignore: ${JSON.stringify(ignore, ignoreListReplacer)}\` from "${dirname}"`; + debug(message); + if (context.showConfig) { + console.log(message); + } + return true; + } + if (only && !matchesPatterns(context, only, dirname)) { + var _context$filename2; + const message = `No config is applied to "${(_context$filename2 = context.filename) != null ? _context$filename2 : "(unknown)"}" because it fails to match one of \`only: ${JSON.stringify(only, ignoreListReplacer)}\` from "${dirname}"`; + debug(message); + if (context.showConfig) { + console.log(message); + } + return true; + } + return false; +} +function matchesPatterns(context, patterns, dirname, configName) { + return patterns.some(pattern => matchPattern(pattern, dirname, context.filename, context, configName)); +} +function matchPattern(pattern, dirname, pathToTest, context, configName) { + if (typeof pattern === "function") { + return !!(0, _rewriteStackTrace.endHiddenCallStack)(pattern)(pathToTest, { + dirname, + envName: context.envName, + caller: context.caller + }); + } + if (typeof pathToTest !== "string") { + throw new _configError.default(`Configuration contains string/RegExp pattern, but no filename was passed to Babel`, configName); + } + if (typeof pattern === "string") { + pattern = (0, _patternToRegex.default)(pattern, dirname); + } + return pattern.test(pathToTest); +} +0 && 0; + +//# sourceMappingURL=config-chain.js.map diff --git a/node_modules/@babel/core/lib/config/config-chain.js.map b/node_modules/@babel/core/lib/config/config-chain.js.map new file mode 100644 index 0000000000000000000000000000000000000000..92414e53a24b3b0cf89092347a6dbf16a1a8ee48 --- /dev/null +++ b/node_modules/@babel/core/lib/config/config-chain.js.map @@ -0,0 +1 @@ +{"version":3,"names":["_path","data","require","_debug","_options","_patternToRegex","_printer","_rewriteStackTrace","_configError","_index","_caching","_configDescriptors","debug","buildDebug","buildPresetChain","arg","context","chain","buildPresetChainWalker","plugins","dedupDescriptors","presets","options","map","o","createConfigChainOptions","files","Set","exports","makeChainWalker","root","preset","loadPresetDescriptors","env","envName","loadPresetEnvDescriptors","overrides","index","loadPresetOverridesDescriptors","overridesEnv","loadPresetOverridesEnvDescriptors","createLogger","makeWeakCacheSync","buildRootDescriptors","alias","createUncachedDescriptors","makeStrongCacheSync","buildEnvDescriptors","buildOverrideDescriptors","buildOverrideEnvDescriptors","buildRootChain","opts","configReport","babelRcReport","programmaticLogger","ConfigPrinter","programmaticChain","loadProgrammaticChain","dirname","cwd","undefined","programmaticReport","output","configFile","loadConfig","caller","findRootConfig","babelrc","babelrcRoots","babelrcRootsDirectory","configFileChain","emptyChain","configFileLogger","validatedFile","validateConfigFile","result","loadFileChain","mergeChain","ignoreFile","babelrcFile","isIgnored","fileChain","filename","pkgData","findPackageData","babelrcLoadEnabled","ignore","config","findRelativeConfig","add","filepath","shouldIgnore","validateBabelrcFile","babelrcLogger","showConfig","console","log","filter","x","join","fileHandling","absoluteRoot","directories","includes","babelrcPatterns","Array","isArray","pat","path","resolve","length","some","pathPatternToRegex","directory","matchPattern","file","validate","validateExtendFile","input","createCachedDescriptors","baseLogger","buildProgrammaticLogger","loadFileChainWalker","loadFileDescriptors","loadFileEnvDescriptors","loadFileOverridesDescriptors","loadFileOverridesEnvDescriptors","buildFileLogger","configure","ChainFormatter","Config","descriptors","_","_context$caller","Programmatic","callerName","name","_options$env","_options$overrides","Error","_options$overrides2","_override$env","override","chainWalker","flattenedConfigs","rootOpts","configIsApplicable","push","envOpts","forEach","overrideOps","overrideEnvOpts","only","logger","mergeExtendsChain","mergeChainOpts","extends","has","from","delete","target","source","Object","assign","passPerPreset","test","include","exclude","hasOwnProperty","call","sourceMaps","sourceMap","items","Map","item","value","fnKey","nameMap","get","set","desc","ownPass","reduce","acc","configName","configFieldIsApplicable","patterns","matchesPatterns","ignoreListReplacer","_key","RegExp","String","_context$filename","message","JSON","stringify","_context$filename2","pattern","pathToTest","endHiddenCallStack","ConfigError"],"sources":["../../src/config/config-chain.ts"],"sourcesContent":["/* eslint-disable @typescript-eslint/no-use-before-define */\n\nimport path from \"node:path\";\nimport buildDebug from \"debug\";\nimport type { Handler } from \"gensync\";\nimport { validate } from \"./validation/options.ts\";\nimport type {\n ConfigApplicableTest,\n BabelrcSearch,\n CallerMetadata,\n MatchItem,\n InputOptions,\n ConfigChainOptions,\n} from \"./validation/options.ts\";\nimport pathPatternToRegex from \"./pattern-to-regex.ts\";\nimport { ConfigPrinter, ChainFormatter } from \"./printer.ts\";\nimport type { ReadonlyDeepArray } from \"./helpers/deep-array.ts\";\n\nimport { endHiddenCallStack } from \"../errors/rewrite-stack-trace.ts\";\nimport ConfigError from \"../errors/config-error.ts\";\nimport type { PluginAPI, PresetAPI } from \"./helpers/config-api.ts\";\n\nconst debug = buildDebug(\"babel:config:config-chain\");\n\nimport {\n findPackageData,\n findRelativeConfig,\n findRootConfig,\n loadConfig,\n} from \"./files/index.ts\";\nimport type { ConfigFile, IgnoreFile, FilePackageData } from \"./files/index.ts\";\n\nimport { makeWeakCacheSync, makeStrongCacheSync } from \"./caching.ts\";\n\nimport {\n createCachedDescriptors,\n createUncachedDescriptors,\n} from \"./config-descriptors.ts\";\nimport type {\n UnloadedDescriptor,\n OptionsAndDescriptors,\n ValidatedFile,\n} from \"./config-descriptors.ts\";\n\nexport type ConfigChain = {\n plugins: UnloadedDescriptor[];\n presets: UnloadedDescriptor[];\n options: ConfigChainOptions[];\n files: Set;\n};\n\nexport type PresetInstance = {\n options: InputOptions;\n alias: string;\n dirname: string;\n externalDependencies: ReadonlyDeepArray;\n};\n\nexport type ConfigContext = {\n filename: string | undefined;\n cwd: string;\n root: string;\n envName: string;\n caller: CallerMetadata | undefined;\n showConfig: boolean;\n};\n\n/**\n * Build a config chain for a given preset.\n */\nexport function* buildPresetChain(\n arg: PresetInstance,\n context: any,\n): Handler {\n const chain = yield* buildPresetChainWalker(arg, context);\n if (!chain) return null;\n\n return {\n plugins: dedupDescriptors(chain.plugins),\n presets: dedupDescriptors(chain.presets),\n options: chain.options.map(o => createConfigChainOptions(o)),\n files: new Set(),\n };\n}\n\nexport const buildPresetChainWalker = makeChainWalker({\n root: preset => loadPresetDescriptors(preset),\n env: (preset, envName) => loadPresetEnvDescriptors(preset)(envName),\n overrides: (preset, index) => loadPresetOverridesDescriptors(preset)(index),\n overridesEnv: (preset, index, envName) =>\n loadPresetOverridesEnvDescriptors(preset)(index)(envName),\n createLogger: () => () => {}, // Currently we don't support logging how preset is expanded\n});\nconst loadPresetDescriptors = makeWeakCacheSync((preset: PresetInstance) =>\n buildRootDescriptors(preset, preset.alias, createUncachedDescriptors),\n);\nconst loadPresetEnvDescriptors = makeWeakCacheSync((preset: PresetInstance) =>\n makeStrongCacheSync((envName: string) =>\n buildEnvDescriptors(\n preset,\n preset.alias,\n createUncachedDescriptors,\n envName,\n ),\n ),\n);\nconst loadPresetOverridesDescriptors = makeWeakCacheSync(\n (preset: PresetInstance) =>\n makeStrongCacheSync((index: number) =>\n buildOverrideDescriptors(\n preset,\n preset.alias,\n createUncachedDescriptors,\n index,\n ),\n ),\n);\nconst loadPresetOverridesEnvDescriptors = makeWeakCacheSync(\n (preset: PresetInstance) =>\n makeStrongCacheSync((index: number) =>\n makeStrongCacheSync((envName: string) =>\n buildOverrideEnvDescriptors(\n preset,\n preset.alias,\n createUncachedDescriptors,\n index,\n envName,\n ),\n ),\n ),\n);\n\nexport type FileHandling = \"transpile\" | \"ignored\" | \"unsupported\";\nexport type RootConfigChain = ConfigChain & {\n babelrc: ConfigFile | undefined;\n config: ConfigFile | undefined;\n ignore: IgnoreFile | undefined;\n fileHandling: FileHandling;\n files: Set;\n};\n\n/**\n * Build a config chain for Babel's full root configuration.\n */\nexport function* buildRootChain(\n opts: InputOptions,\n context: ConfigContext,\n): Handler {\n let configReport, babelRcReport;\n const programmaticLogger = new ConfigPrinter();\n const programmaticChain = yield* loadProgrammaticChain(\n {\n options: opts,\n dirname: context.cwd,\n },\n context,\n undefined,\n programmaticLogger,\n );\n if (!programmaticChain) return null;\n const programmaticReport = yield* programmaticLogger.output();\n\n let configFile;\n if (typeof opts.configFile === \"string\") {\n configFile = yield* loadConfig(\n opts.configFile,\n context.cwd,\n context.envName,\n context.caller,\n );\n } else if (opts.configFile !== false) {\n configFile = yield* findRootConfig(\n context.root,\n context.envName,\n context.caller,\n );\n }\n\n let { babelrc, babelrcRoots } = opts;\n let babelrcRootsDirectory = context.cwd;\n\n const configFileChain = emptyChain();\n const configFileLogger = new ConfigPrinter();\n if (configFile) {\n const validatedFile = validateConfigFile(configFile);\n const result = yield* loadFileChain(\n validatedFile,\n context,\n undefined,\n configFileLogger,\n );\n if (!result) return null;\n configReport = yield* configFileLogger.output();\n\n // Allow config files to toggle `.babelrc` resolution on and off and\n // specify where the roots are.\n if (babelrc === undefined) {\n babelrc = validatedFile.options.babelrc;\n }\n if (babelrcRoots === undefined) {\n babelrcRootsDirectory = validatedFile.dirname;\n babelrcRoots = validatedFile.options.babelrcRoots;\n }\n\n mergeChain(configFileChain, result);\n }\n\n let ignoreFile, babelrcFile;\n let isIgnored = false;\n const fileChain = emptyChain();\n // resolve all .babelrc files\n if (\n (babelrc === true || babelrc === undefined) &&\n typeof context.filename === \"string\"\n ) {\n const pkgData = yield* findPackageData(context.filename);\n\n if (\n pkgData &&\n babelrcLoadEnabled(context, pkgData, babelrcRoots, babelrcRootsDirectory)\n ) {\n ({ ignore: ignoreFile, config: babelrcFile } = yield* findRelativeConfig(\n pkgData,\n context.envName,\n context.caller,\n ));\n\n if (ignoreFile) {\n fileChain.files.add(ignoreFile.filepath);\n }\n\n if (\n ignoreFile &&\n shouldIgnore(context, ignoreFile.ignore, null, ignoreFile.dirname)\n ) {\n isIgnored = true;\n }\n\n if (babelrcFile && !isIgnored) {\n const validatedFile = validateBabelrcFile(babelrcFile);\n const babelrcLogger = new ConfigPrinter();\n const result = yield* loadFileChain(\n validatedFile,\n context,\n undefined,\n babelrcLogger,\n );\n if (!result) {\n isIgnored = true;\n } else {\n babelRcReport = yield* babelrcLogger.output();\n mergeChain(fileChain, result);\n }\n }\n\n if (babelrcFile && isIgnored) {\n fileChain.files.add(babelrcFile.filepath);\n }\n }\n }\n\n if (context.showConfig) {\n console.log(\n `Babel configs on \"${context.filename}\" (ascending priority):\\n` +\n // print config by the order of ascending priority\n [configReport, babelRcReport, programmaticReport]\n .filter(x => !!x)\n .join(\"\\n\\n\") +\n \"\\n-----End Babel configs-----\",\n );\n }\n // Insert file chain in front so programmatic options have priority\n // over configuration file chain items.\n const chain = mergeChain(\n mergeChain(mergeChain(emptyChain(), configFileChain), fileChain),\n programmaticChain,\n );\n\n return {\n plugins: isIgnored ? [] : dedupDescriptors(chain.plugins),\n presets: isIgnored ? [] : dedupDescriptors(chain.presets),\n options: isIgnored\n ? []\n : chain.options.map(o => createConfigChainOptions(o)),\n fileHandling: isIgnored ? \"ignored\" : \"transpile\",\n ignore: ignoreFile || undefined,\n babelrc: babelrcFile || undefined,\n config: configFile || undefined,\n files: chain.files,\n };\n}\n\nfunction babelrcLoadEnabled(\n context: ConfigContext,\n pkgData: FilePackageData,\n babelrcRoots: BabelrcSearch | undefined,\n babelrcRootsDirectory: string,\n): boolean {\n if (typeof babelrcRoots === \"boolean\") return babelrcRoots;\n\n const absoluteRoot = context.root;\n\n // Fast path to avoid having to match patterns if the babelrc is just\n // loading in the standard root directory.\n if (babelrcRoots === undefined) {\n return pkgData.directories.includes(absoluteRoot);\n }\n\n let babelrcPatterns = babelrcRoots;\n if (!Array.isArray(babelrcPatterns)) {\n babelrcPatterns = [babelrcPatterns];\n }\n babelrcPatterns = babelrcPatterns.map(pat => {\n return typeof pat === \"string\"\n ? path.resolve(babelrcRootsDirectory, pat)\n : pat;\n });\n\n // Fast path to avoid having to match patterns if the babelrc is just\n // loading in the standard root directory.\n if (babelrcPatterns.length === 1 && babelrcPatterns[0] === absoluteRoot) {\n return pkgData.directories.includes(absoluteRoot);\n }\n\n return babelrcPatterns.some(pat => {\n if (typeof pat === \"string\") {\n pat = pathPatternToRegex(pat, babelrcRootsDirectory);\n }\n\n return pkgData.directories.some(directory => {\n return matchPattern(pat, babelrcRootsDirectory, directory, context);\n });\n });\n}\n\nconst validateConfigFile = makeWeakCacheSync(\n (file: ConfigFile): ValidatedFile => ({\n filepath: file.filepath,\n dirname: file.dirname,\n options: validate(\"configfile\", file.options, file.filepath),\n }),\n);\n\nconst validateBabelrcFile = makeWeakCacheSync(\n (file: ConfigFile): ValidatedFile => ({\n filepath: file.filepath,\n dirname: file.dirname,\n options: validate(\"babelrcfile\", file.options, file.filepath),\n }),\n);\n\nconst validateExtendFile = makeWeakCacheSync(\n (file: ConfigFile): ValidatedFile => ({\n filepath: file.filepath,\n dirname: file.dirname,\n options: validate(\"extendsfile\", file.options, file.filepath),\n }),\n);\n\n/**\n * Build a config chain for just the programmatic options passed into Babel.\n */\nconst loadProgrammaticChain = makeChainWalker({\n root: input => buildRootDescriptors(input, \"base\", createCachedDescriptors),\n env: (input, envName) =>\n buildEnvDescriptors(input, \"base\", createCachedDescriptors, envName),\n overrides: (input, index) =>\n buildOverrideDescriptors(input, \"base\", createCachedDescriptors, index),\n overridesEnv: (input, index, envName) =>\n buildOverrideEnvDescriptors(\n input,\n \"base\",\n createCachedDescriptors,\n index,\n envName,\n ),\n createLogger: (input, context, baseLogger) =>\n buildProgrammaticLogger(input, context, baseLogger),\n});\n\n/**\n * Build a config chain for a given file.\n */\nconst loadFileChainWalker = makeChainWalker({\n root: file => loadFileDescriptors(file),\n env: (file, envName) => loadFileEnvDescriptors(file)(envName),\n overrides: (file, index) => loadFileOverridesDescriptors(file)(index),\n overridesEnv: (file, index, envName) =>\n loadFileOverridesEnvDescriptors(file)(index)(envName),\n createLogger: (file, context, baseLogger) =>\n buildFileLogger(file.filepath, context, baseLogger),\n});\n\nfunction* loadFileChain(\n input: ValidatedFile,\n context: ConfigContext,\n files: Set,\n baseLogger: ConfigPrinter,\n) {\n const chain = yield* loadFileChainWalker(input, context, files, baseLogger);\n chain?.files.add(input.filepath);\n\n return chain;\n}\n\nconst loadFileDescriptors = makeWeakCacheSync((file: ValidatedFile) =>\n buildRootDescriptors(file, file.filepath, createUncachedDescriptors),\n);\nconst loadFileEnvDescriptors = makeWeakCacheSync((file: ValidatedFile) =>\n makeStrongCacheSync((envName: string) =>\n buildEnvDescriptors(\n file,\n file.filepath,\n createUncachedDescriptors,\n envName,\n ),\n ),\n);\nconst loadFileOverridesDescriptors = makeWeakCacheSync((file: ValidatedFile) =>\n makeStrongCacheSync((index: number) =>\n buildOverrideDescriptors(\n file,\n file.filepath,\n createUncachedDescriptors,\n index,\n ),\n ),\n);\nconst loadFileOverridesEnvDescriptors = makeWeakCacheSync(\n (file: ValidatedFile) =>\n makeStrongCacheSync((index: number) =>\n makeStrongCacheSync((envName: string) =>\n buildOverrideEnvDescriptors(\n file,\n file.filepath,\n createUncachedDescriptors,\n index,\n envName,\n ),\n ),\n ),\n);\n\nfunction buildFileLogger(\n filepath: string,\n context: ConfigContext,\n baseLogger: ConfigPrinter | void,\n) {\n if (!baseLogger) {\n return () => {};\n }\n return baseLogger.configure(context.showConfig, ChainFormatter.Config, {\n filepath,\n });\n}\n\nfunction buildRootDescriptors(\n { dirname, options }: Partial,\n alias: string,\n descriptors: (\n dirname: string,\n options: InputOptions,\n alias: string,\n ) => OptionsAndDescriptors,\n) {\n return descriptors(dirname, options, alias);\n}\n\nfunction buildProgrammaticLogger(\n _: unknown,\n context: ConfigContext,\n baseLogger: ConfigPrinter | void,\n) {\n if (!baseLogger) {\n return () => {};\n }\n return baseLogger.configure(context.showConfig, ChainFormatter.Programmatic, {\n callerName: context.caller?.name,\n });\n}\n\nfunction buildEnvDescriptors(\n { dirname, options }: Partial,\n alias: string,\n descriptors: (\n dirname: string,\n options: InputOptions,\n alias: string,\n ) => OptionsAndDescriptors,\n envName: string,\n) {\n const opts = options.env?.[envName];\n return opts ? descriptors(dirname, opts, `${alias}.env[\"${envName}\"]`) : null;\n}\n\nfunction buildOverrideDescriptors(\n { dirname, options }: Partial,\n alias: string,\n descriptors: (\n dirname: string,\n options: InputOptions,\n alias: string,\n ) => OptionsAndDescriptors,\n index: number,\n) {\n const opts = options.overrides?.[index];\n if (!opts) throw new Error(\"Assertion failure - missing override\");\n\n return descriptors(dirname, opts, `${alias}.overrides[${index}]`);\n}\n\nfunction buildOverrideEnvDescriptors(\n { dirname, options }: Partial,\n alias: string,\n descriptors: (\n dirname: string,\n options: InputOptions,\n alias: string,\n ) => OptionsAndDescriptors,\n index: number,\n envName: string,\n) {\n const override = options.overrides?.[index];\n if (!override) throw new Error(\"Assertion failure - missing override\");\n\n const opts = override.env?.[envName];\n return opts\n ? descriptors(\n dirname,\n opts,\n `${alias}.overrides[${index}].env[\"${envName}\"]`,\n )\n : null;\n}\n\nfunction makeChainWalker<\n ArgT extends {\n options: InputOptions;\n dirname: string;\n filepath?: string;\n },\n>({\n root,\n env,\n overrides,\n overridesEnv,\n createLogger,\n}: {\n root: (configEntry: ArgT) => OptionsAndDescriptors;\n env: (configEntry: ArgT, env: string) => OptionsAndDescriptors | null;\n overrides: (configEntry: ArgT, index: number) => OptionsAndDescriptors;\n overridesEnv: (\n configEntry: ArgT,\n index: number,\n env: string,\n ) => OptionsAndDescriptors | null;\n createLogger: (\n configEntry: ArgT,\n context: ConfigContext,\n printer: ConfigPrinter | void,\n ) => (\n opts: OptionsAndDescriptors,\n index?: number | null,\n env?: string | null,\n ) => void;\n}): (\n configEntry: ArgT,\n context: ConfigContext,\n files?: Set,\n baseLogger?: ConfigPrinter,\n) => Handler {\n return function* chainWalker(input, context, files = new Set(), baseLogger) {\n const { dirname } = input;\n\n const flattenedConfigs: {\n config: OptionsAndDescriptors;\n index: number | undefined | null;\n envName: string | undefined | null;\n }[] = [];\n\n const rootOpts = root(input);\n if (configIsApplicable(rootOpts, dirname, context, input.filepath)) {\n flattenedConfigs.push({\n config: rootOpts,\n envName: undefined,\n index: undefined,\n });\n\n const envOpts = env(input, context.envName);\n if (\n envOpts &&\n configIsApplicable(envOpts, dirname, context, input.filepath)\n ) {\n flattenedConfigs.push({\n config: envOpts,\n envName: context.envName,\n index: undefined,\n });\n }\n\n (rootOpts.options.overrides || []).forEach((_, index) => {\n const overrideOps = overrides(input, index);\n if (configIsApplicable(overrideOps, dirname, context, input.filepath)) {\n flattenedConfigs.push({\n config: overrideOps,\n index,\n envName: undefined,\n });\n\n const overrideEnvOpts = overridesEnv(input, index, context.envName);\n if (\n overrideEnvOpts &&\n configIsApplicable(\n overrideEnvOpts,\n dirname,\n context,\n input.filepath,\n )\n ) {\n flattenedConfigs.push({\n config: overrideEnvOpts,\n index,\n envName: context.envName,\n });\n }\n }\n });\n }\n\n // Process 'ignore' and 'only' before 'extends' items are processed so\n // that we don't do extra work loading extended configs if a file is\n // ignored.\n if (\n flattenedConfigs.some(\n ({\n config: {\n options: { ignore, only },\n },\n }) => shouldIgnore(context, ignore, only, dirname),\n )\n ) {\n return null;\n }\n\n const chain = emptyChain();\n const logger = createLogger(input, context, baseLogger);\n\n for (const { config, index, envName } of flattenedConfigs) {\n if (\n !(yield* mergeExtendsChain(\n chain,\n config.options,\n dirname,\n context,\n files,\n baseLogger,\n ))\n ) {\n return null;\n }\n\n logger(config, index, envName);\n yield* mergeChainOpts(chain, config);\n }\n return chain;\n };\n}\n\nfunction* mergeExtendsChain(\n chain: ConfigChain,\n opts: InputOptions,\n dirname: string,\n context: ConfigContext,\n files: Set,\n baseLogger?: ConfigPrinter,\n): Handler {\n if (opts.extends === undefined) return true;\n\n const file = yield* loadConfig(\n opts.extends,\n dirname,\n context.envName,\n context.caller,\n );\n\n if (files.has(file)) {\n throw new Error(\n `Configuration cycle detected loading ${file.filepath}.\\n` +\n `File already loaded following the config chain:\\n` +\n Array.from(files, file => ` - ${file.filepath}`).join(\"\\n\"),\n );\n }\n\n files.add(file);\n const fileChain = yield* loadFileChain(\n validateExtendFile(file),\n context,\n files,\n baseLogger,\n );\n files.delete(file);\n\n if (!fileChain) return false;\n\n mergeChain(chain, fileChain);\n\n return true;\n}\n\nfunction mergeChain(target: ConfigChain, source: ConfigChain): ConfigChain {\n target.options.push(...source.options);\n target.plugins.push(...source.plugins);\n target.presets.push(...source.presets);\n for (const file of source.files) {\n target.files.add(file);\n }\n\n return target;\n}\n\nfunction* mergeChainOpts(\n target: ConfigChain,\n { options, plugins, presets }: OptionsAndDescriptors,\n): Handler {\n target.options.push(options);\n target.plugins.push(...(yield* plugins()));\n target.presets.push(...(yield* presets()));\n\n return target;\n}\n\nfunction emptyChain(): ConfigChain {\n return {\n options: [],\n presets: [],\n plugins: [],\n files: new Set(),\n };\n}\n\nfunction createConfigChainOptions(opts: InputOptions): ConfigChainOptions {\n const options = {\n ...opts,\n };\n delete options.extends;\n delete options.env;\n delete options.overrides;\n delete options.plugins;\n delete options.presets;\n delete options.passPerPreset;\n delete options.ignore;\n delete options.only;\n delete options.test;\n delete options.include;\n delete options.exclude;\n\n // \"sourceMap\" is just aliased to sourceMap, so copy it over as\n // we merge the options together.\n if (Object.hasOwn(options, \"sourceMap\")) {\n options.sourceMaps = options.sourceMap;\n delete options.sourceMap;\n }\n return options;\n}\n\nfunction dedupDescriptors(\n items: UnloadedDescriptor[],\n): UnloadedDescriptor[] {\n const map = new Map<\n Function,\n Map }>\n >();\n\n const descriptors = [];\n\n for (const item of items) {\n if (typeof item.value === \"function\") {\n const fnKey = item.value;\n let nameMap = map.get(fnKey);\n if (!nameMap) {\n nameMap = new Map();\n map.set(fnKey, nameMap);\n }\n let desc = nameMap.get(item.name);\n if (!desc) {\n desc = { value: item };\n descriptors.push(desc);\n\n // Treat passPerPreset presets as unique, skipping them\n // in the merge processing steps.\n if (!item.ownPass) nameMap.set(item.name, desc);\n } else {\n desc.value = item;\n }\n } else {\n descriptors.push({ value: item });\n }\n }\n\n return descriptors.reduce((acc, desc) => {\n acc.push(desc.value);\n return acc;\n }, []);\n}\n\nfunction configIsApplicable(\n { options }: OptionsAndDescriptors,\n dirname: string,\n context: ConfigContext,\n configName: string,\n): boolean {\n return (\n (options.test === undefined ||\n configFieldIsApplicable(context, options.test, dirname, configName)) &&\n (options.include === undefined ||\n configFieldIsApplicable(context, options.include, dirname, configName)) &&\n (options.exclude === undefined ||\n !configFieldIsApplicable(context, options.exclude, dirname, configName))\n );\n}\n\nfunction configFieldIsApplicable(\n context: ConfigContext,\n test: ConfigApplicableTest,\n dirname: string,\n configName: string,\n): boolean {\n const patterns = Array.isArray(test) ? test : [test];\n\n return matchesPatterns(context, patterns, dirname, configName);\n}\n\n/**\n * Print the ignoreList-values in a more helpful way than the default.\n */\nfunction ignoreListReplacer(\n _key: string,\n value: MatchItem[] | MatchItem,\n): MatchItem[] | MatchItem | string {\n if (value instanceof RegExp) {\n return String(value);\n }\n\n return value;\n}\n\n/**\n * Tests if a filename should be ignored based on \"ignore\" and \"only\" options.\n */\nfunction shouldIgnore(\n context: ConfigContext,\n ignore: MatchItem[] | undefined | null,\n only: MatchItem[] | undefined | null,\n dirname: string,\n): boolean {\n if (ignore && matchesPatterns(context, ignore, dirname)) {\n const message = `No config is applied to \"${\n context.filename ?? \"(unknown)\"\n }\" because it matches one of \\`ignore: ${JSON.stringify(\n ignore,\n ignoreListReplacer,\n )}\\` from \"${dirname}\"`;\n debug(message);\n if (context.showConfig) {\n console.log(message);\n }\n return true;\n }\n\n if (only && !matchesPatterns(context, only, dirname)) {\n const message = `No config is applied to \"${\n context.filename ?? \"(unknown)\"\n }\" because it fails to match one of \\`only: ${JSON.stringify(\n only,\n ignoreListReplacer,\n )}\\` from \"${dirname}\"`;\n debug(message);\n if (context.showConfig) {\n console.log(message);\n }\n return true;\n }\n\n return false;\n}\n\n/**\n * Returns result of calling function with filename if pattern is a function.\n * Otherwise returns result of matching pattern Regex with filename.\n */\nfunction matchesPatterns(\n context: ConfigContext,\n patterns: MatchItem[],\n dirname: string,\n configName?: string,\n): boolean {\n return patterns.some(pattern =>\n matchPattern(pattern, dirname, context.filename, context, configName),\n );\n}\n\nfunction matchPattern(\n pattern: MatchItem,\n dirname: string,\n pathToTest: string | undefined,\n context: ConfigContext,\n configName?: string,\n): boolean {\n if (typeof pattern === \"function\") {\n return !!endHiddenCallStack(pattern)(pathToTest, {\n dirname,\n envName: context.envName,\n caller: context.caller,\n });\n }\n\n if (typeof pathToTest !== \"string\") {\n throw new ConfigError(\n `Configuration contains string/RegExp pattern, but no filename was passed to Babel`,\n configName,\n );\n }\n\n if (typeof pattern === \"string\") {\n pattern = pathPatternToRegex(pattern, dirname);\n }\n return pattern.test(pathToTest);\n}\n"],"mappings":"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","ignoreList":[]} \ No newline at end of file diff --git a/node_modules/@babel/core/lib/config/config-descriptors.js b/node_modules/@babel/core/lib/config/config-descriptors.js new file mode 100644 index 0000000000000000000000000000000000000000..21fb4146b7f3dcb802756eaf3141d6e424e8a399 --- /dev/null +++ b/node_modules/@babel/core/lib/config/config-descriptors.js @@ -0,0 +1,190 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.createCachedDescriptors = createCachedDescriptors; +exports.createDescriptor = createDescriptor; +exports.createUncachedDescriptors = createUncachedDescriptors; +function _gensync() { + const data = require("gensync"); + _gensync = function () { + return data; + }; + return data; +} +var _functional = require("../gensync-utils/functional.js"); +var _index = require("./files/index.js"); +var _item = require("./item.js"); +var _caching = require("./caching.js"); +var _resolveTargets = require("./resolve-targets.js"); +function isEqualDescriptor(a, b) { + var _a$file, _b$file, _a$file2, _b$file2; + return a.name === b.name && a.value === b.value && a.options === b.options && a.dirname === b.dirname && a.alias === b.alias && a.ownPass === b.ownPass && ((_a$file = a.file) == null ? void 0 : _a$file.request) === ((_b$file = b.file) == null ? void 0 : _b$file.request) && ((_a$file2 = a.file) == null ? void 0 : _a$file2.resolved) === ((_b$file2 = b.file) == null ? void 0 : _b$file2.resolved); +} +function* handlerOf(value) { + return value; +} +function optionsWithResolvedBrowserslistConfigFile(options, dirname) { + if (typeof options.browserslistConfigFile === "string") { + options.browserslistConfigFile = (0, _resolveTargets.resolveBrowserslistConfigFile)(options.browserslistConfigFile, dirname); + } + return options; +} +function createCachedDescriptors(dirname, options, alias) { + const { + plugins, + presets, + passPerPreset + } = options; + return { + options: optionsWithResolvedBrowserslistConfigFile(options, dirname), + plugins: plugins ? () => createCachedPluginDescriptors(plugins, dirname)(alias) : () => handlerOf([]), + presets: presets ? () => createCachedPresetDescriptors(presets, dirname)(alias)(!!passPerPreset) : () => handlerOf([]) + }; +} +function createUncachedDescriptors(dirname, options, alias) { + return { + options: optionsWithResolvedBrowserslistConfigFile(options, dirname), + plugins: (0, _functional.once)(() => createPluginDescriptors(options.plugins || [], dirname, alias)), + presets: (0, _functional.once)(() => createPresetDescriptors(options.presets || [], dirname, alias, !!options.passPerPreset)) + }; +} +const PRESET_DESCRIPTOR_CACHE = new WeakMap(); +const createCachedPresetDescriptors = (0, _caching.makeWeakCacheSync)((items, cache) => { + const dirname = cache.using(dir => dir); + return (0, _caching.makeStrongCacheSync)(alias => (0, _caching.makeStrongCache)(function* (passPerPreset) { + const descriptors = yield* createPresetDescriptors(items, dirname, alias, passPerPreset); + return descriptors.map(desc => loadCachedDescriptor(PRESET_DESCRIPTOR_CACHE, desc)); + })); +}); +const PLUGIN_DESCRIPTOR_CACHE = new WeakMap(); +const createCachedPluginDescriptors = (0, _caching.makeWeakCacheSync)((items, cache) => { + const dirname = cache.using(dir => dir); + return (0, _caching.makeStrongCache)(function* (alias) { + const descriptors = yield* createPluginDescriptors(items, dirname, alias); + return descriptors.map(desc => loadCachedDescriptor(PLUGIN_DESCRIPTOR_CACHE, desc)); + }); +}); +const DEFAULT_OPTIONS = {}; +function loadCachedDescriptor(cache, desc) { + const { + value, + options = DEFAULT_OPTIONS + } = desc; + if (options === false) return desc; + let cacheByOptions = cache.get(value); + if (!cacheByOptions) { + cacheByOptions = new WeakMap(); + cache.set(value, cacheByOptions); + } + let possibilities = cacheByOptions.get(options); + if (!possibilities) { + possibilities = []; + cacheByOptions.set(options, possibilities); + } + if (!possibilities.includes(desc)) { + const matches = possibilities.filter(possibility => isEqualDescriptor(possibility, desc)); + if (matches.length > 0) { + return matches[0]; + } + possibilities.push(desc); + } + return desc; +} +function* createPresetDescriptors(items, dirname, alias, passPerPreset) { + return yield* createDescriptors("preset", items, dirname, alias, passPerPreset); +} +function* createPluginDescriptors(items, dirname, alias) { + return yield* createDescriptors("plugin", items, dirname, alias); +} +function* createDescriptors(type, items, dirname, alias, ownPass) { + const descriptors = yield* _gensync().all(items.map((item, index) => createDescriptor(item, dirname, { + type, + alias: `${alias}$${index}`, + ownPass: !!ownPass + }))); + assertNoDuplicates(descriptors); + return descriptors; +} +function* createDescriptor(pair, dirname, { + type, + alias, + ownPass +}) { + const desc = (0, _item.getItemDescriptor)(pair); + if (desc) { + return desc; + } + let name; + let options; + let value = pair; + if (Array.isArray(value)) { + if (value.length === 3) { + [value, options, name] = value; + } else { + [value, options] = value; + } + } + let file = undefined; + let filepath = null; + if (typeof value === "string") { + if (typeof type !== "string") { + throw new Error("To resolve a string-based item, the type of item must be given"); + } + const resolver = type === "plugin" ? _index.loadPlugin : _index.loadPreset; + const request = value; + ({ + filepath, + value + } = yield* resolver(value, dirname)); + file = { + request, + resolved: filepath + }; + } + if (!value) { + throw new Error(`Unexpected falsy value: ${String(value)}`); + } + if (typeof value === "object" && value.__esModule) { + if (value.default) { + value = value.default; + } else { + throw new Error("Must export a default export when using ES6 modules."); + } + } + if (typeof value !== "object" && typeof value !== "function") { + throw new Error(`Unsupported format: ${typeof value}. Expected an object or a function.`); + } + if (filepath !== null && typeof value === "object" && value) { + throw new Error(`Plugin/Preset files are not allowed to export objects, only functions. In ${filepath}`); + } + return { + name, + alias: filepath || alias, + value, + options, + dirname, + ownPass, + file + }; +} +function assertNoDuplicates(items) { + const map = new Map(); + for (const item of items) { + if (typeof item.value !== "function") continue; + let nameMap = map.get(item.value); + if (!nameMap) { + nameMap = new Set(); + map.set(item.value, nameMap); + } + if (nameMap.has(item.name)) { + const conflicts = items.filter(i => i.value === item.value); + throw new Error([`Duplicate plugin/preset detected.`, `If you'd like to use two separate instances of a plugin,`, `they need separate names, e.g.`, ``, ` plugins: [`, ` ['some-plugin', {}],`, ` ['some-plugin', {}, 'some unique name'],`, ` ]`, ``, `Duplicates detected are:`, `${JSON.stringify(conflicts, null, 2)}`].join("\n")); + } + nameMap.add(item.name); + } +} +0 && 0; + +//# sourceMappingURL=config-descriptors.js.map diff --git a/node_modules/@babel/core/lib/config/config-descriptors.js.map b/node_modules/@babel/core/lib/config/config-descriptors.js.map new file mode 100644 index 0000000000000000000000000000000000000000..b51b2cbd362407cf3f2ce7e210977eaf4e16ecab --- /dev/null +++ b/node_modules/@babel/core/lib/config/config-descriptors.js.map @@ -0,0 +1 @@ +{"version":3,"names":["_gensync","data","require","_functional","_index","_item","_caching","_resolveTargets","isEqualDescriptor","a","b","_a$file","_b$file","_a$file2","_b$file2","name","value","options","dirname","alias","ownPass","file","request","resolved","handlerOf","optionsWithResolvedBrowserslistConfigFile","browserslistConfigFile","resolveBrowserslistConfigFile","createCachedDescriptors","plugins","presets","passPerPreset","createCachedPluginDescriptors","createCachedPresetDescriptors","createUncachedDescriptors","once","createPluginDescriptors","createPresetDescriptors","PRESET_DESCRIPTOR_CACHE","WeakMap","makeWeakCacheSync","items","cache","using","dir","makeStrongCacheSync","makeStrongCache","descriptors","map","desc","loadCachedDescriptor","PLUGIN_DESCRIPTOR_CACHE","DEFAULT_OPTIONS","cacheByOptions","get","set","possibilities","includes","matches","filter","possibility","length","push","createDescriptors","type","gensync","all","item","index","createDescriptor","assertNoDuplicates","pair","getItemDescriptor","Array","isArray","undefined","filepath","Error","resolver","loadPlugin","loadPreset","String","__esModule","default","Map","nameMap","Set","has","conflicts","i","JSON","stringify","join","add"],"sources":["../../src/config/config-descriptors.ts"],"sourcesContent":["import gensync, { type Handler } from \"gensync\";\nimport { once } from \"../gensync-utils/functional.ts\";\n\nimport { loadPlugin, loadPreset } from \"./files/index.ts\";\n\nimport { getItemDescriptor } from \"./item.ts\";\n\nimport {\n makeWeakCacheSync,\n makeStrongCacheSync,\n makeStrongCache,\n} from \"./caching.ts\";\nimport type { CacheConfigurator } from \"./caching.ts\";\n\nimport type {\n PluginItem,\n InputOptions,\n PresetItem,\n} from \"./validation/options.ts\";\n\nimport { resolveBrowserslistConfigFile } from \"./resolve-targets.ts\";\nimport type { PluginAPI, PresetAPI } from \"./helpers/config-api.ts\";\n\n// Represents a config object and functions to lazily load the descriptors\n// for the plugins and presets so we don't load the plugins/presets unless\n// the options object actually ends up being applicable.\nexport type OptionsAndDescriptors = {\n options: InputOptions;\n plugins: () => Handler[]>;\n presets: () => Handler[]>;\n};\n\n// Represents a plugin or presets at a given location in a config object.\n// At this point these have been resolved to a specific object or function,\n// but have not yet been executed to call functions with options.\nexport interface UnloadedDescriptor {\n name: string | undefined;\n value: object | ((api: API, options: Options, dirname: string) => unknown);\n options: Options;\n dirname: string;\n alias: string;\n ownPass?: boolean;\n file?: {\n request: string;\n resolved: string;\n };\n}\n\nfunction isEqualDescriptor(\n a: UnloadedDescriptor,\n b: UnloadedDescriptor,\n): boolean {\n return (\n a.name === b.name &&\n a.value === b.value &&\n a.options === b.options &&\n a.dirname === b.dirname &&\n a.alias === b.alias &&\n a.ownPass === b.ownPass &&\n a.file?.request === b.file?.request &&\n a.file?.resolved === b.file?.resolved\n );\n}\n\nexport type ValidatedFile = {\n filepath: string;\n dirname: string;\n options: InputOptions;\n};\n\n// eslint-disable-next-line require-yield\nfunction* handlerOf(value: T): Handler {\n return value;\n}\n\nfunction optionsWithResolvedBrowserslistConfigFile(\n options: InputOptions,\n dirname: string,\n): InputOptions {\n if (typeof options.browserslistConfigFile === \"string\") {\n options.browserslistConfigFile = resolveBrowserslistConfigFile(\n options.browserslistConfigFile,\n dirname,\n );\n }\n return options;\n}\n\n/**\n * Create a set of descriptors from a given options object, preserving\n * descriptor identity based on the identity of the plugin/preset arrays\n * themselves, and potentially on the identity of the plugins/presets + options.\n */\nexport function createCachedDescriptors(\n dirname: string,\n options: InputOptions,\n alias: string,\n): OptionsAndDescriptors {\n const { plugins, presets, passPerPreset } = options;\n return {\n options: optionsWithResolvedBrowserslistConfigFile(options, dirname),\n plugins: plugins\n ? () =>\n // @ts-expect-error todo(flow->ts) ts complains about incorrect arguments\n // eslint-disable-next-line @typescript-eslint/no-use-before-define\n createCachedPluginDescriptors(plugins, dirname)(alias)\n : () => handlerOf([]),\n presets: presets\n ? () =>\n // @ts-expect-error todo(flow->ts) ts complains about incorrect arguments\n // eslint-disable-next-line @typescript-eslint/no-use-before-define\n createCachedPresetDescriptors(presets, dirname)(alias)(\n !!passPerPreset,\n )\n : () => handlerOf([]),\n };\n}\n\n/**\n * Create a set of descriptors from a given options object, with consistent\n * identity for the descriptors, but not caching based on any specific identity.\n */\nexport function createUncachedDescriptors(\n dirname: string,\n options: InputOptions,\n alias: string,\n): OptionsAndDescriptors {\n return {\n options: optionsWithResolvedBrowserslistConfigFile(options, dirname),\n // The returned result here is cached to represent a config object in\n // memory, so we build and memoize the descriptors to ensure the same\n // values are returned consistently.\n plugins: once(() =>\n createPluginDescriptors(options.plugins || [], dirname, alias),\n ),\n presets: once(() =>\n createPresetDescriptors(\n options.presets || [],\n dirname,\n alias,\n !!options.passPerPreset,\n ),\n ),\n };\n}\n\nconst PRESET_DESCRIPTOR_CACHE = new WeakMap();\nconst createCachedPresetDescriptors = makeWeakCacheSync(\n (items: PresetItem[], cache: CacheConfigurator) => {\n const dirname = cache.using(dir => dir);\n return makeStrongCacheSync((alias: string) =>\n makeStrongCache(function* (\n passPerPreset: boolean,\n ): Handler[]> {\n const descriptors = yield* createPresetDescriptors(\n items,\n dirname,\n alias,\n passPerPreset,\n );\n return descriptors.map(\n // Items are cached using the overall preset array identity when\n // possibly, but individual descriptors are also cached if a match\n // can be found in the previously-used descriptor lists.\n desc => loadCachedDescriptor(PRESET_DESCRIPTOR_CACHE, desc),\n );\n }),\n );\n },\n);\n\nconst PLUGIN_DESCRIPTOR_CACHE = new WeakMap();\nconst createCachedPluginDescriptors = makeWeakCacheSync(\n (items: PluginItem[], cache: CacheConfigurator) => {\n const dirname = cache.using(dir => dir);\n return makeStrongCache(function* (\n alias: string,\n ): Handler[]> {\n const descriptors = yield* createPluginDescriptors(items, dirname, alias);\n return descriptors.map(\n // Items are cached using the overall plugin array identity when\n // possibly, but individual descriptors are also cached if a match\n // can be found in the previously-used descriptor lists.\n desc => loadCachedDescriptor(PLUGIN_DESCRIPTOR_CACHE, desc),\n );\n });\n },\n);\n\n/**\n * When no options object is given in a descriptor, this object is used\n * as a WeakMap key in order to have consistent identity.\n */\nconst DEFAULT_OPTIONS = {};\n\n/**\n * Given the cache and a descriptor, returns a matching descriptor from the\n * cache, or else returns the input descriptor and adds it to the cache for\n * next time.\n */\nfunction loadCachedDescriptor(\n cache: WeakMap[]>>,\n desc: UnloadedDescriptor,\n) {\n const { value, options = DEFAULT_OPTIONS } = desc;\n if (options === false) return desc;\n\n let cacheByOptions = cache.get(value);\n if (!cacheByOptions) {\n cacheByOptions = new WeakMap();\n cache.set(value, cacheByOptions);\n }\n\n let possibilities = cacheByOptions.get(options);\n if (!possibilities) {\n possibilities = [];\n cacheByOptions.set(options, possibilities);\n }\n\n if (!possibilities.includes(desc)) {\n const matches = possibilities.filter(possibility =>\n isEqualDescriptor(possibility, desc),\n );\n if (matches.length > 0) {\n return matches[0];\n }\n\n possibilities.push(desc);\n }\n\n return desc;\n}\n\nfunction* createPresetDescriptors(\n items: PresetItem[],\n dirname: string,\n alias: string,\n passPerPreset: boolean,\n): Handler[]> {\n return yield* createDescriptors(\n \"preset\",\n items,\n dirname,\n alias,\n passPerPreset,\n );\n}\n\nfunction* createPluginDescriptors(\n items: PluginItem[],\n dirname: string,\n alias: string,\n): Handler[]> {\n return yield* createDescriptors(\"plugin\", items, dirname, alias);\n}\n\nfunction* createDescriptors(\n type: Type,\n items: Type extends \"plugin\" ? PluginItem[] : PresetItem[],\n dirname: string,\n alias: string,\n ownPass?: boolean,\n): Handler<\n UnloadedDescriptor[]\n> {\n const descriptors = yield* gensync.all(\n items.map((item, index) =>\n createDescriptor(item, dirname, {\n type,\n alias: `${alias}$${index}`,\n ownPass: !!ownPass,\n }),\n ),\n );\n\n assertNoDuplicates(descriptors);\n\n return descriptors;\n}\n\n/**\n * Given a plugin/preset item, resolve it into a standard format.\n */\nexport function* createDescriptor(\n pair: PluginItem | PresetItem,\n dirname: string,\n {\n type,\n alias,\n ownPass,\n }: {\n type?: \"plugin\" | \"preset\";\n alias: string;\n ownPass?: boolean;\n },\n): Handler> {\n const desc = getItemDescriptor(pair);\n if (desc) {\n return desc;\n }\n\n let name;\n let options;\n let value = pair;\n if (Array.isArray(value)) {\n if (value.length === 3) {\n [value, options, name] = value;\n } else {\n [value, options] = value;\n }\n }\n\n let file = undefined;\n let filepath = null;\n if (typeof value === \"string\") {\n if (typeof type !== \"string\") {\n throw new Error(\n \"To resolve a string-based item, the type of item must be given\",\n );\n }\n const resolver = type === \"plugin\" ? loadPlugin : loadPreset;\n const request = value;\n\n // @ts-expect-error value must be a PluginItem\n ({ filepath, value } = yield* resolver(value, dirname));\n\n file = {\n request,\n resolved: filepath,\n };\n }\n\n if (!value) {\n // eslint-disable-next-line @typescript-eslint/no-base-to-string\n throw new Error(`Unexpected falsy value: ${String(value)}`);\n }\n\n // @ts-expect-error Handle transpiled ES6 modules.\n if (typeof value === \"object\" && value.__esModule) {\n // @ts-expect-error Handle transpiled ES6 modules.\n if (value.default) {\n // @ts-expect-error Handle transpiled ES6 modules.\n value = value.default;\n } else {\n throw new Error(\"Must export a default export when using ES6 modules.\");\n }\n }\n\n if (typeof value !== \"object\" && typeof value !== \"function\") {\n throw new Error(\n `Unsupported format: ${typeof value}. Expected an object or a function.`,\n );\n }\n\n if (filepath !== null && typeof value === \"object\" && value) {\n // We allow object values for plugins/presets nested directly within a\n // config object, because it can be useful to define them in nested\n // configuration contexts.\n throw new Error(\n `Plugin/Preset files are not allowed to export objects, only functions. In ${filepath}`,\n );\n }\n\n return {\n name,\n alias: filepath || alias,\n value,\n options,\n dirname,\n ownPass,\n file,\n };\n}\n\nfunction assertNoDuplicates(items: UnloadedDescriptor[]): void {\n const map = new Map();\n\n for (const item of items) {\n if (typeof item.value !== \"function\") continue;\n\n let nameMap = map.get(item.value);\n if (!nameMap) {\n nameMap = new Set();\n map.set(item.value, nameMap);\n }\n\n if (nameMap.has(item.name)) {\n const conflicts = items.filter(i => i.value === item.value);\n throw new Error(\n [\n `Duplicate plugin/preset detected.`,\n `If you'd like to use two separate instances of a plugin,`,\n `they need separate names, e.g.`,\n ``,\n ` plugins: [`,\n ` ['some-plugin', {}],`,\n ` ['some-plugin', {}, 'some unique name'],`,\n ` ]`,\n ``,\n `Duplicates detected are:`,\n `${JSON.stringify(conflicts, null, 2)}`,\n ].join(\"\\n\"),\n );\n }\n\n nameMap.add(item.name);\n }\n}\n"],"mappings":"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","ignoreList":[]} \ No newline at end of file diff --git a/node_modules/@babel/core/lib/config/files/configuration.js b/node_modules/@babel/core/lib/config/files/configuration.js new file mode 100644 index 0000000000000000000000000000000000000000..582fc327e173fefad09fa07c9c1866eb41067282 --- /dev/null +++ b/node_modules/@babel/core/lib/config/files/configuration.js @@ -0,0 +1,290 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.ROOT_CONFIG_FILENAMES = void 0; +exports.findConfigUpwards = findConfigUpwards; +exports.findRelativeConfig = findRelativeConfig; +exports.findRootConfig = findRootConfig; +exports.loadConfig = loadConfig; +exports.resolveShowConfigPath = resolveShowConfigPath; +function _debug() { + const data = require("debug"); + _debug = function () { + return data; + }; + return data; +} +function _fs() { + const data = require("fs"); + _fs = function () { + return data; + }; + return data; +} +function _path() { + const data = require("path"); + _path = function () { + return data; + }; + return data; +} +function _json() { + const data = require("json5"); + _json = function () { + return data; + }; + return data; +} +function _gensync() { + const data = require("gensync"); + _gensync = function () { + return data; + }; + return data; +} +var _caching = require("../caching.js"); +var _configApi = require("../helpers/config-api.js"); +var _utils = require("./utils.js"); +var _moduleTypes = require("./module-types.js"); +var _patternToRegex = require("../pattern-to-regex.js"); +var _configError = require("../../errors/config-error.js"); +var fs = require("../../gensync-utils/fs.js"); +require("module"); +var _rewriteStackTrace = require("../../errors/rewrite-stack-trace.js"); +var _async = require("../../gensync-utils/async.js"); +const debug = _debug()("babel:config:loading:files:configuration"); +const ROOT_CONFIG_FILENAMES = exports.ROOT_CONFIG_FILENAMES = ["babel.config.js", "babel.config.cjs", "babel.config.mjs", "babel.config.json", "babel.config.cts", "babel.config.ts", "babel.config.mts"]; +const RELATIVE_CONFIG_FILENAMES = [".babelrc", ".babelrc.js", ".babelrc.cjs", ".babelrc.mjs", ".babelrc.json", ".babelrc.cts"]; +const BABELIGNORE_FILENAME = ".babelignore"; +const runConfig = (0, _caching.makeWeakCache)(function* runConfig(options, cache) { + yield* []; + return { + options: (0, _rewriteStackTrace.endHiddenCallStack)(options)((0, _configApi.makeConfigAPI)(cache)), + cacheNeedsConfiguration: !cache.configured() + }; +}); +function* readConfigCode(filepath, data) { + if (!_fs().existsSync(filepath)) return null; + let options = yield* (0, _moduleTypes.default)(filepath, (yield* (0, _async.isAsync)()) ? "auto" : "require", "You appear to be using a native ECMAScript module configuration " + "file, which is only supported when running Babel asynchronously " + "or when using the Node.js `--experimental-require-module` flag.", "You appear to be using a configuration file that contains top-level " + "await, which is only supported when running Babel asynchronously."); + let cacheNeedsConfiguration = false; + if (typeof options === "function") { + ({ + options, + cacheNeedsConfiguration + } = yield* runConfig(options, data)); + } + if (!options || typeof options !== "object" || Array.isArray(options)) { + throw new _configError.default(`Configuration should be an exported JavaScript object.`, filepath); + } + if (typeof options.then === "function") { + options.catch == null || options.catch(() => {}); + throw new _configError.default(`You appear to be using an async configuration, ` + `which your current version of Babel does not support. ` + `We may add support for this in the future, ` + `but if you're on the most recent version of @babel/core and still ` + `seeing this error, then you'll need to synchronously return your config.`, filepath); + } + if (cacheNeedsConfiguration) throwConfigError(filepath); + return buildConfigFileObject(options, filepath); +} +const cfboaf = new WeakMap(); +function buildConfigFileObject(options, filepath) { + let configFilesByFilepath = cfboaf.get(options); + if (!configFilesByFilepath) { + cfboaf.set(options, configFilesByFilepath = new Map()); + } + let configFile = configFilesByFilepath.get(filepath); + if (!configFile) { + configFile = { + filepath, + dirname: _path().dirname(filepath), + options + }; + configFilesByFilepath.set(filepath, configFile); + } + return configFile; +} +const packageToBabelConfig = (0, _caching.makeWeakCacheSync)(file => { + const babel = file.options.babel; + if (babel === undefined) return null; + if (typeof babel !== "object" || Array.isArray(babel) || babel === null) { + throw new _configError.default(`.babel property must be an object`, file.filepath); + } + return { + filepath: file.filepath, + dirname: file.dirname, + options: babel + }; +}); +const readConfigJSON5 = (0, _utils.makeStaticFileCache)((filepath, content) => { + let options; + try { + options = _json().parse(content); + } catch (err) { + throw new _configError.default(`Error while parsing config - ${err.message}`, filepath); + } + if (!options) throw new _configError.default(`No config detected`, filepath); + if (typeof options !== "object") { + throw new _configError.default(`Config returned typeof ${typeof options}`, filepath); + } + if (Array.isArray(options)) { + throw new _configError.default(`Expected config object but found array`, filepath); + } + delete options.$schema; + return { + filepath, + dirname: _path().dirname(filepath), + options + }; +}); +const readIgnoreConfig = (0, _utils.makeStaticFileCache)((filepath, content) => { + const ignoreDir = _path().dirname(filepath); + const ignorePatterns = content.split("\n").map(line => line.replace(/#.*$/, "").trim()).filter(Boolean); + for (const pattern of ignorePatterns) { + if (pattern.startsWith("!")) { + throw new _configError.default(`Negation of file paths is not supported.`, filepath); + } + } + return { + filepath, + dirname: _path().dirname(filepath), + ignore: ignorePatterns.map(pattern => (0, _patternToRegex.default)(pattern, ignoreDir)) + }; +}); +function findConfigUpwards(rootDir) { + let dirname = rootDir; + for (;;) { + for (const filename of ROOT_CONFIG_FILENAMES) { + if (_fs().existsSync(_path().join(dirname, filename))) { + return dirname; + } + } + const nextDir = _path().dirname(dirname); + if (dirname === nextDir) break; + dirname = nextDir; + } + return null; +} +function* findRelativeConfig(packageData, envName, caller) { + let config = null; + let ignore = null; + const dirname = _path().dirname(packageData.filepath); + for (const loc of packageData.directories) { + if (!config) { + var _packageData$pkg; + config = yield* loadOneConfig(RELATIVE_CONFIG_FILENAMES, loc, envName, caller, ((_packageData$pkg = packageData.pkg) == null ? void 0 : _packageData$pkg.dirname) === loc ? packageToBabelConfig(packageData.pkg) : null); + } + if (!ignore) { + const ignoreLoc = _path().join(loc, BABELIGNORE_FILENAME); + ignore = yield* readIgnoreConfig(ignoreLoc); + if (ignore) { + debug("Found ignore %o from %o.", ignore.filepath, dirname); + } + } + } + return { + config, + ignore + }; +} +function findRootConfig(dirname, envName, caller) { + return loadOneConfig(ROOT_CONFIG_FILENAMES, dirname, envName, caller); +} +function* loadOneConfig(names, dirname, envName, caller, previousConfig = null) { + const configs = yield* _gensync().all(names.map(filename => readConfig(_path().join(dirname, filename), envName, caller))); + const config = configs.reduce((previousConfig, config) => { + if (config && previousConfig) { + throw new _configError.default(`Multiple configuration files found. Please remove one:\n` + ` - ${_path().basename(previousConfig.filepath)}\n` + ` - ${config.filepath}\n` + `from ${dirname}`); + } + return config || previousConfig; + }, previousConfig); + if (config) { + debug("Found configuration %o from %o.", config.filepath, dirname); + } + return config; +} +function* loadConfig(name, dirname, envName, caller) { + const filepath = (((v, w) => (v = v.split("."), w = w.split("."), +v[0] > +w[0] || v[0] == w[0] && +v[1] >= +w[1]))(process.versions.node, "8.9") ? require.resolve : (r, { + paths: [b] + }, M = require("module")) => { + let f = M._findPath(r, M._nodeModulePaths(b).concat(b)); + if (f) return f; + f = new Error(`Cannot resolve module '${r}'`); + f.code = "MODULE_NOT_FOUND"; + throw f; + })(name, { + paths: [dirname] + }); + const conf = yield* readConfig(filepath, envName, caller); + if (!conf) { + throw new _configError.default(`Config file contains no configuration data`, filepath); + } + debug("Loaded config %o from %o.", name, dirname); + return conf; +} +function readConfig(filepath, envName, caller) { + const ext = _path().extname(filepath); + switch (ext) { + case ".js": + case ".cjs": + case ".mjs": + case ".ts": + case ".cts": + case ".mts": + return readConfigCode(filepath, { + envName, + caller + }); + default: + return readConfigJSON5(filepath); + } +} +function* resolveShowConfigPath(dirname) { + const targetPath = process.env.BABEL_SHOW_CONFIG_FOR; + if (targetPath != null) { + const absolutePath = _path().resolve(dirname, targetPath); + const stats = yield* fs.stat(absolutePath); + if (!stats.isFile()) { + throw new Error(`${absolutePath}: BABEL_SHOW_CONFIG_FOR must refer to a regular file, directories are not supported.`); + } + return absolutePath; + } + return null; +} +function throwConfigError(filepath) { + throw new _configError.default(`\ +Caching was left unconfigured. Babel's plugins, presets, and .babelrc.js files can be configured +for various types of caching, using the first param of their handler functions: + +module.exports = function(api) { + // The API exposes the following: + + // Cache the returned value forever and don't call this function again. + api.cache(true); + + // Don't cache at all. Not recommended because it will be very slow. + api.cache(false); + + // Cached based on the value of some function. If this function returns a value different from + // a previously-encountered value, the plugins will re-evaluate. + var env = api.cache(() => process.env.NODE_ENV); + + // If testing for a specific env, we recommend specifics to avoid instantiating a plugin for + // any possible NODE_ENV value that might come up during plugin execution. + var isProd = api.cache(() => process.env.NODE_ENV === "production"); + + // .cache(fn) will perform a linear search though instances to find the matching plugin based + // based on previous instantiated plugins. If you want to recreate the plugin and discard the + // previous instance whenever something changes, you may use: + var isProd = api.cache.invalidate(() => process.env.NODE_ENV === "production"); + + // Note, we also expose the following more-verbose versions of the above examples: + api.cache.forever(); // api.cache(true) + api.cache.never(); // api.cache(false) + api.cache.using(fn); // api.cache(fn) + + // Return the value that will be cached. + return { }; +};`, filepath); +} +0 && 0; + +//# sourceMappingURL=configuration.js.map diff --git a/node_modules/@babel/core/lib/config/files/configuration.js.map b/node_modules/@babel/core/lib/config/files/configuration.js.map new file mode 100644 index 0000000000000000000000000000000000000000..07c99ef74203c2857c1b9aeeab02b86c3a8b5691 --- /dev/null +++ b/node_modules/@babel/core/lib/config/files/configuration.js.map @@ -0,0 +1 @@ +{"version":3,"names":["_debug","data","require","_fs","_path","_json","_gensync","_caching","_configApi","_utils","_moduleTypes","_patternToRegex","_configError","fs","_rewriteStackTrace","_async","debug","buildDebug","ROOT_CONFIG_FILENAMES","exports","RELATIVE_CONFIG_FILENAMES","BABELIGNORE_FILENAME","runConfig","makeWeakCache","options","cache","endHiddenCallStack","makeConfigAPI","cacheNeedsConfiguration","configured","readConfigCode","filepath","nodeFs","existsSync","loadCodeDefault","isAsync","Array","isArray","ConfigError","then","catch","throwConfigError","buildConfigFileObject","cfboaf","WeakMap","configFilesByFilepath","get","set","Map","configFile","dirname","path","packageToBabelConfig","makeWeakCacheSync","file","babel","undefined","readConfigJSON5","makeStaticFileCache","content","json5","parse","err","message","$schema","readIgnoreConfig","ignoreDir","ignorePatterns","split","map","line","replace","trim","filter","Boolean","pattern","startsWith","ignore","pathPatternToRegex","findConfigUpwards","rootDir","filename","join","nextDir","findRelativeConfig","packageData","envName","caller","config","loc","directories","_packageData$pkg","loadOneConfig","pkg","ignoreLoc","findRootConfig","names","previousConfig","configs","gensync","all","readConfig","reduce","basename","loadConfig","name","v","w","process","versions","node","resolve","r","paths","b","M","f","_findPath","_nodeModulePaths","concat","Error","code","conf","ext","extname","resolveShowConfigPath","targetPath","env","BABEL_SHOW_CONFIG_FOR","absolutePath","stats","stat","isFile"],"sources":["../../../src/config/files/configuration.ts"],"sourcesContent":["import buildDebug from \"debug\";\nimport nodeFs from \"node:fs\";\nimport path from \"node:path\";\nimport json5 from \"json5\";\nimport gensync from \"gensync\";\nimport type { Handler } from \"gensync\";\nimport { makeWeakCache, makeWeakCacheSync } from \"../caching.ts\";\nimport type { CacheConfigurator } from \"../caching.ts\";\nimport { makeConfigAPI } from \"../helpers/config-api.ts\";\nimport type { ConfigAPI } from \"../helpers/config-api.ts\";\nimport { makeStaticFileCache } from \"./utils.ts\";\nimport loadCodeDefault from \"./module-types.ts\";\nimport pathPatternToRegex from \"../pattern-to-regex.ts\";\nimport type { FilePackageData, RelativeConfig, ConfigFile } from \"./types.ts\";\nimport type { CallerMetadata, InputOptions } from \"../validation/options.ts\";\nimport ConfigError from \"../../errors/config-error.ts\";\n\nimport * as fs from \"../../gensync-utils/fs.ts\";\n\nimport { createRequire } from \"node:module\";\nimport { endHiddenCallStack } from \"../../errors/rewrite-stack-trace.ts\";\nimport { isAsync } from \"../../gensync-utils/async.ts\";\nconst require = createRequire(import.meta.url);\n\nconst debug = buildDebug(\"babel:config:loading:files:configuration\");\n\nexport const ROOT_CONFIG_FILENAMES = [\n \"babel.config.js\",\n \"babel.config.cjs\",\n \"babel.config.mjs\",\n \"babel.config.json\",\n \"babel.config.cts\",\n \"babel.config.ts\",\n \"babel.config.mts\",\n];\nconst RELATIVE_CONFIG_FILENAMES = [\n \".babelrc\",\n \".babelrc.js\",\n \".babelrc.cjs\",\n \".babelrc.mjs\",\n \".babelrc.json\",\n \".babelrc.cts\",\n];\n\nconst BABELIGNORE_FILENAME = \".babelignore\";\n\ntype ConfigCacheData = {\n envName: string;\n caller: CallerMetadata | undefined;\n};\n\nconst runConfig = makeWeakCache(function* runConfig(\n options: Function,\n cache: CacheConfigurator,\n): Handler<{\n options: InputOptions | null;\n cacheNeedsConfiguration: boolean;\n}> {\n // if we want to make it possible to use async configs\n yield* [];\n\n return {\n options: endHiddenCallStack(options as any as (api: ConfigAPI) => unknown)(\n makeConfigAPI(cache),\n ),\n cacheNeedsConfiguration: !cache.configured(),\n };\n});\n\nfunction* readConfigCode(\n filepath: string,\n data: ConfigCacheData,\n): Handler {\n if (!nodeFs.existsSync(filepath)) return null;\n\n let options = yield* loadCodeDefault(\n filepath,\n (yield* isAsync()) ? \"auto\" : \"require\",\n \"You appear to be using a native ECMAScript module configuration \" +\n \"file, which is only supported when running Babel asynchronously \" +\n \"or when using the Node.js `--experimental-require-module` flag.\",\n \"You appear to be using a configuration file that contains top-level \" +\n \"await, which is only supported when running Babel asynchronously.\",\n );\n\n let cacheNeedsConfiguration = false;\n if (typeof options === \"function\") {\n ({ options, cacheNeedsConfiguration } = yield* runConfig(options, data));\n }\n\n if (!options || typeof options !== \"object\" || Array.isArray(options)) {\n throw new ConfigError(\n `Configuration should be an exported JavaScript object.`,\n filepath,\n );\n }\n\n // @ts-expect-error todo(flow->ts)\n if (typeof options.then === \"function\") {\n // @ts-expect-error We use ?. in case options is a thenable but not a promise\n options.catch?.(() => {});\n throw new ConfigError(\n `You appear to be using an async configuration, ` +\n `which your current version of Babel does not support. ` +\n `We may add support for this in the future, ` +\n `but if you're on the most recent version of @babel/core and still ` +\n `seeing this error, then you'll need to synchronously return your config.`,\n filepath,\n );\n }\n\n if (cacheNeedsConfiguration) throwConfigError(filepath);\n\n return buildConfigFileObject(options, filepath);\n}\n\n// We cache the generated ConfigFile object rather than creating a new one\n// every time, so that it can be used as a cache key in other functions.\nconst cfboaf /* configFilesByOptionsAndFilepath */ = new WeakMap<\n InputOptions,\n Map\n>();\nfunction buildConfigFileObject(\n options: InputOptions,\n filepath: string,\n): ConfigFile {\n let configFilesByFilepath = cfboaf.get(options);\n if (!configFilesByFilepath) {\n cfboaf.set(options, (configFilesByFilepath = new Map()));\n }\n\n let configFile = configFilesByFilepath.get(filepath);\n if (!configFile) {\n configFile = {\n filepath,\n dirname: path.dirname(filepath),\n options,\n };\n configFilesByFilepath.set(filepath, configFile);\n }\n\n return configFile;\n}\n\nconst packageToBabelConfig = makeWeakCacheSync(\n (file: ConfigFile): ConfigFile | null => {\n const babel: unknown = file.options.babel;\n\n if (babel === undefined) return null;\n\n if (typeof babel !== \"object\" || Array.isArray(babel) || babel === null) {\n throw new ConfigError(`.babel property must be an object`, file.filepath);\n }\n\n return {\n filepath: file.filepath,\n dirname: file.dirname,\n options: babel,\n };\n },\n);\n\nconst readConfigJSON5 = makeStaticFileCache((filepath, content): ConfigFile => {\n let options;\n try {\n options = json5.parse(content);\n } catch (err) {\n throw new ConfigError(\n `Error while parsing config - ${err.message}`,\n filepath,\n );\n }\n\n if (!options) throw new ConfigError(`No config detected`, filepath);\n\n if (typeof options !== \"object\") {\n throw new ConfigError(`Config returned typeof ${typeof options}`, filepath);\n }\n if (Array.isArray(options)) {\n throw new ConfigError(`Expected config object but found array`, filepath);\n }\n\n delete options.$schema;\n\n return {\n filepath,\n dirname: path.dirname(filepath),\n options,\n };\n});\n\nconst readIgnoreConfig = makeStaticFileCache((filepath, content) => {\n const ignoreDir = path.dirname(filepath);\n const ignorePatterns = content\n .split(\"\\n\")\n .map(line =>\n line.replace(process.env.BABEL_8_BREAKING ? /^#.*$/ : /#.*$/, \"\").trim(),\n )\n .filter(Boolean);\n\n for (const pattern of ignorePatterns) {\n if (pattern.startsWith(\"!\")) {\n throw new ConfigError(\n `Negation of file paths is not supported.`,\n filepath,\n );\n }\n }\n\n return {\n filepath,\n dirname: path.dirname(filepath),\n ignore: ignorePatterns.map(pattern =>\n pathPatternToRegex(pattern, ignoreDir),\n ),\n };\n});\n\nexport function findConfigUpwards(rootDir: string): string | null {\n let dirname = rootDir;\n for (;;) {\n for (const filename of ROOT_CONFIG_FILENAMES) {\n if (nodeFs.existsSync(path.join(dirname, filename))) {\n return dirname;\n }\n }\n\n const nextDir = path.dirname(dirname);\n if (dirname === nextDir) break;\n dirname = nextDir;\n }\n\n return null;\n}\n\nexport function* findRelativeConfig(\n packageData: FilePackageData,\n envName: string,\n caller: CallerMetadata | undefined,\n): Handler {\n let config = null;\n let ignore = null;\n\n const dirname = path.dirname(packageData.filepath);\n\n for (const loc of packageData.directories) {\n if (!config) {\n config = yield* loadOneConfig(\n RELATIVE_CONFIG_FILENAMES,\n loc,\n envName,\n caller,\n packageData.pkg?.dirname === loc\n ? packageToBabelConfig(packageData.pkg)\n : null,\n );\n }\n\n if (!ignore) {\n const ignoreLoc = path.join(loc, BABELIGNORE_FILENAME);\n ignore = yield* readIgnoreConfig(ignoreLoc);\n\n if (ignore) {\n debug(\"Found ignore %o from %o.\", ignore.filepath, dirname);\n }\n }\n }\n\n return { config, ignore };\n}\n\nexport function findRootConfig(\n dirname: string,\n envName: string,\n caller: CallerMetadata | undefined,\n): Handler {\n return loadOneConfig(ROOT_CONFIG_FILENAMES, dirname, envName, caller);\n}\n\nfunction* loadOneConfig(\n names: string[],\n dirname: string,\n envName: string,\n caller: CallerMetadata | undefined,\n previousConfig: ConfigFile | null = null,\n): Handler {\n const configs = yield* gensync.all(\n names.map(filename =>\n readConfig(path.join(dirname, filename), envName, caller),\n ),\n );\n const config = configs.reduce((previousConfig: ConfigFile | null, config) => {\n if (config && previousConfig) {\n throw new ConfigError(\n `Multiple configuration files found. Please remove one:\\n` +\n ` - ${path.basename(previousConfig.filepath)}\\n` +\n ` - ${config.filepath}\\n` +\n `from ${dirname}`,\n );\n }\n\n return config || previousConfig;\n }, previousConfig);\n\n if (config) {\n debug(\"Found configuration %o from %o.\", config.filepath, dirname);\n }\n return config;\n}\n\nexport function* loadConfig(\n name: string,\n dirname: string,\n envName: string,\n caller: CallerMetadata | undefined,\n): Handler {\n const filepath = require.resolve(name, { paths: [dirname] });\n\n const conf = yield* readConfig(filepath, envName, caller);\n if (!conf) {\n throw new ConfigError(\n `Config file contains no configuration data`,\n filepath,\n );\n }\n\n debug(\"Loaded config %o from %o.\", name, dirname);\n return conf;\n}\n\n/**\n * Read the given config file, returning the result. Returns null if no config was found, but will\n * throw if there are parsing errors while loading a config.\n */\nfunction readConfig(\n filepath: string,\n envName: string,\n caller: CallerMetadata | undefined,\n): Handler {\n const ext = path.extname(filepath);\n switch (ext) {\n case \".js\":\n case \".cjs\":\n case \".mjs\":\n case \".ts\":\n case \".cts\":\n case \".mts\":\n return readConfigCode(filepath, { envName, caller });\n default:\n return readConfigJSON5(filepath);\n }\n}\n\nexport function* resolveShowConfigPath(\n dirname: string,\n): Handler {\n const targetPath = process.env.BABEL_SHOW_CONFIG_FOR;\n if (targetPath != null) {\n const absolutePath = path.resolve(dirname, targetPath);\n const stats = yield* fs.stat(absolutePath);\n if (!stats.isFile()) {\n throw new Error(\n `${absolutePath}: BABEL_SHOW_CONFIG_FOR must refer to a regular file, directories are not supported.`,\n );\n }\n return absolutePath;\n }\n return null;\n}\n\nfunction throwConfigError(filepath: string): never {\n throw new ConfigError(\n `\\\nCaching was left unconfigured. Babel's plugins, presets, and .babelrc.js files can be configured\nfor various types of caching, using the first param of their handler functions:\n\nmodule.exports = function(api) {\n // The API exposes the following:\n\n // Cache the returned value forever and don't call this function again.\n api.cache(true);\n\n // Don't cache at all. Not recommended because it will be very slow.\n api.cache(false);\n\n // Cached based on the value of some function. If this function returns a value different from\n // a previously-encountered value, the plugins will re-evaluate.\n var env = api.cache(() => process.env.NODE_ENV);\n\n // If testing for a specific env, we recommend specifics to avoid instantiating a plugin for\n // any possible NODE_ENV value that might come up during plugin execution.\n var isProd = api.cache(() => process.env.NODE_ENV === \"production\");\n\n // .cache(fn) will perform a linear search though instances to find the matching plugin based\n // based on previous instantiated plugins. If you want to recreate the plugin and discard the\n // previous instance whenever something changes, you may use:\n var isProd = api.cache.invalidate(() => process.env.NODE_ENV === \"production\");\n\n // Note, we also expose the following more-verbose versions of the above examples:\n api.cache.forever(); // api.cache(true)\n api.cache.never(); // api.cache(false)\n api.cache.using(fn); // api.cache(fn)\n\n // Return the value that will be cached.\n return { };\n};`,\n filepath,\n );\n}\n"],"mappings":"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","ignoreList":[]} \ No newline at end of file diff --git a/node_modules/@babel/core/lib/config/files/import.cjs b/node_modules/@babel/core/lib/config/files/import.cjs new file mode 100644 index 0000000000000000000000000000000000000000..46fa5d5cfc41c42efc8b8be53aa993e8a9e3c651 --- /dev/null +++ b/node_modules/@babel/core/lib/config/files/import.cjs @@ -0,0 +1,6 @@ +module.exports = function import_(filepath) { + return import(filepath); +}; +0 && 0; + +//# sourceMappingURL=import.cjs.map diff --git a/node_modules/@babel/core/lib/config/files/import.cjs.map b/node_modules/@babel/core/lib/config/files/import.cjs.map new file mode 100644 index 0000000000000000000000000000000000000000..2200da80f43f57e23145c3ce719b16743188fd66 --- /dev/null +++ b/node_modules/@babel/core/lib/config/files/import.cjs.map @@ -0,0 +1 @@ +{"version":3,"names":["module","exports","import_","filepath"],"sources":["../../../src/config/files/import.cjs"],"sourcesContent":["// We keep this in a separate file so that in older node versions, where\n// import() isn't supported, we can try/catch around the require() call\n// when loading this file.\n\nmodule.exports = function import_(filepath) {\n return import(filepath);\n};\n"],"mappings":"AAIAA,MAAM,CAACC,OAAO,GAAG,SAASC,OAAOA,CAACC,QAAQ,EAAE;EAC1C,OAAO,OAAOA,QAAQ,CAAC;AACzB,CAAC;AAAC","ignoreList":[]} \ No newline at end of file diff --git a/node_modules/@babel/core/lib/config/files/index-browser.js b/node_modules/@babel/core/lib/config/files/index-browser.js new file mode 100644 index 0000000000000000000000000000000000000000..d8ba7dbc8d7e96837481886ae35931428acca238 --- /dev/null +++ b/node_modules/@babel/core/lib/config/files/index-browser.js @@ -0,0 +1,58 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.ROOT_CONFIG_FILENAMES = void 0; +exports.findConfigUpwards = findConfigUpwards; +exports.findPackageData = findPackageData; +exports.findRelativeConfig = findRelativeConfig; +exports.findRootConfig = findRootConfig; +exports.loadConfig = loadConfig; +exports.loadPlugin = loadPlugin; +exports.loadPreset = loadPreset; +exports.resolvePlugin = resolvePlugin; +exports.resolvePreset = resolvePreset; +exports.resolveShowConfigPath = resolveShowConfigPath; +function findConfigUpwards(rootDir) { + return null; +} +function* findPackageData(filepath) { + return { + filepath, + directories: [], + pkg: null, + isPackage: false + }; +} +function* findRelativeConfig(pkgData, envName, caller) { + return { + config: null, + ignore: null + }; +} +function* findRootConfig(dirname, envName, caller) { + return null; +} +function* loadConfig(name, dirname, envName, caller) { + throw new Error(`Cannot load ${name} relative to ${dirname} in a browser`); +} +function* resolveShowConfigPath(dirname) { + return null; +} +const ROOT_CONFIG_FILENAMES = exports.ROOT_CONFIG_FILENAMES = []; +function resolvePlugin(name, dirname) { + return null; +} +function resolvePreset(name, dirname) { + return null; +} +function loadPlugin(name, dirname) { + throw new Error(`Cannot load plugin ${name} relative to ${dirname} in a browser`); +} +function loadPreset(name, dirname) { + throw new Error(`Cannot load preset ${name} relative to ${dirname} in a browser`); +} +0 && 0; + +//# sourceMappingURL=index-browser.js.map diff --git a/node_modules/@babel/core/lib/config/files/index-browser.js.map b/node_modules/@babel/core/lib/config/files/index-browser.js.map new file mode 100644 index 0000000000000000000000000000000000000000..e10ddeeef8bece1675f04692609e1f8fcb4aadb1 --- /dev/null +++ b/node_modules/@babel/core/lib/config/files/index-browser.js.map @@ -0,0 +1 @@ +{"version":3,"names":["findConfigUpwards","rootDir","findPackageData","filepath","directories","pkg","isPackage","findRelativeConfig","pkgData","envName","caller","config","ignore","findRootConfig","dirname","loadConfig","name","Error","resolveShowConfigPath","ROOT_CONFIG_FILENAMES","exports","resolvePlugin","resolvePreset","loadPlugin","loadPreset"],"sources":["../../../src/config/files/index-browser.ts"],"sourcesContent":["/* c8 ignore start */\n\nimport type { Handler } from \"gensync\";\n\nimport type {\n ConfigFile,\n IgnoreFile,\n RelativeConfig,\n FilePackageData,\n} from \"./types.ts\";\n\nimport type { CallerMetadata } from \"../validation/options.ts\";\n\nexport type { ConfigFile, IgnoreFile, RelativeConfig, FilePackageData };\n\nexport function findConfigUpwards(\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n rootDir: string,\n): string | null {\n return null;\n}\n\n// eslint-disable-next-line require-yield\nexport function* findPackageData(filepath: string): Handler {\n return {\n filepath,\n directories: [],\n pkg: null,\n isPackage: false,\n };\n}\n\n// eslint-disable-next-line require-yield\nexport function* findRelativeConfig(\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n pkgData: FilePackageData,\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n envName: string,\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n caller: CallerMetadata | undefined,\n): Handler {\n return { config: null, ignore: null };\n}\n\n// eslint-disable-next-line require-yield\nexport function* findRootConfig(\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n dirname: string,\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n envName: string,\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n caller: CallerMetadata | undefined,\n): Handler {\n return null;\n}\n\n// eslint-disable-next-line require-yield\nexport function* loadConfig(\n name: string,\n dirname: string,\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n envName: string,\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n caller: CallerMetadata | undefined,\n): Handler {\n throw new Error(`Cannot load ${name} relative to ${dirname} in a browser`);\n}\n\n// eslint-disable-next-line require-yield\nexport function* resolveShowConfigPath(\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n dirname: string,\n): Handler {\n return null;\n}\n\nexport const ROOT_CONFIG_FILENAMES: string[] = [];\n\ntype Resolved =\n | { loader: \"require\"; filepath: string }\n | { loader: \"import\"; filepath: string };\n\n// eslint-disable-next-line @typescript-eslint/no-unused-vars\nexport function resolvePlugin(name: string, dirname: string): Resolved | null {\n return null;\n}\n\n// eslint-disable-next-line @typescript-eslint/no-unused-vars\nexport function resolvePreset(name: string, dirname: string): Resolved | null {\n return null;\n}\n\nexport function loadPlugin(\n name: string,\n dirname: string,\n): Handler<{\n filepath: string;\n value: unknown;\n}> {\n throw new Error(\n `Cannot load plugin ${name} relative to ${dirname} in a browser`,\n );\n}\n\nexport function loadPreset(\n name: string,\n dirname: string,\n): Handler<{\n filepath: string;\n value: unknown;\n}> {\n throw new Error(\n `Cannot load preset ${name} relative to ${dirname} in a browser`,\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAeO,SAASA,iBAAiBA,CAE/BC,OAAe,EACA;EACf,OAAO,IAAI;AACb;AAGO,UAAUC,eAAeA,CAACC,QAAgB,EAA4B;EAC3E,OAAO;IACLA,QAAQ;IACRC,WAAW,EAAE,EAAE;IACfC,GAAG,EAAE,IAAI;IACTC,SAAS,EAAE;EACb,CAAC;AACH;AAGO,UAAUC,kBAAkBA,CAEjCC,OAAwB,EAExBC,OAAe,EAEfC,MAAkC,EACT;EACzB,OAAO;IAAEC,MAAM,EAAE,IAAI;IAAEC,MAAM,EAAE;EAAK,CAAC;AACvC;AAGO,UAAUC,cAAcA,CAE7BC,OAAe,EAEfL,OAAe,EAEfC,MAAkC,EACN;EAC5B,OAAO,IAAI;AACb;AAGO,UAAUK,UAAUA,CACzBC,IAAY,EACZF,OAAe,EAEfL,OAAe,EAEfC,MAAkC,EACb;EACrB,MAAM,IAAIO,KAAK,CAAC,eAAeD,IAAI,gBAAgBF,OAAO,eAAe,CAAC;AAC5E;AAGO,UAAUI,qBAAqBA,CAEpCJ,OAAe,EACS;EACxB,OAAO,IAAI;AACb;AAEO,MAAMK,qBAA+B,GAAAC,OAAA,CAAAD,qBAAA,GAAG,EAAE;AAO1C,SAASE,aAAaA,CAACL,IAAY,EAAEF,OAAe,EAAmB;EAC5E,OAAO,IAAI;AACb;AAGO,SAASQ,aAAaA,CAACN,IAAY,EAAEF,OAAe,EAAmB;EAC5E,OAAO,IAAI;AACb;AAEO,SAASS,UAAUA,CACxBP,IAAY,EACZF,OAAe,EAId;EACD,MAAM,IAAIG,KAAK,CACb,sBAAsBD,IAAI,gBAAgBF,OAAO,eACnD,CAAC;AACH;AAEO,SAASU,UAAUA,CACxBR,IAAY,EACZF,OAAe,EAId;EACD,MAAM,IAAIG,KAAK,CACb,sBAAsBD,IAAI,gBAAgBF,OAAO,eACnD,CAAC;AACH;AAAC","ignoreList":[]} \ No newline at end of file diff --git a/node_modules/@babel/core/lib/config/files/index.js b/node_modules/@babel/core/lib/config/files/index.js new file mode 100644 index 0000000000000000000000000000000000000000..8750f40a9c80b2ef6cb5d980f4125092b9b2c031 --- /dev/null +++ b/node_modules/@babel/core/lib/config/files/index.js @@ -0,0 +1,78 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +Object.defineProperty(exports, "ROOT_CONFIG_FILENAMES", { + enumerable: true, + get: function () { + return _configuration.ROOT_CONFIG_FILENAMES; + } +}); +Object.defineProperty(exports, "findConfigUpwards", { + enumerable: true, + get: function () { + return _configuration.findConfigUpwards; + } +}); +Object.defineProperty(exports, "findPackageData", { + enumerable: true, + get: function () { + return _package.findPackageData; + } +}); +Object.defineProperty(exports, "findRelativeConfig", { + enumerable: true, + get: function () { + return _configuration.findRelativeConfig; + } +}); +Object.defineProperty(exports, "findRootConfig", { + enumerable: true, + get: function () { + return _configuration.findRootConfig; + } +}); +Object.defineProperty(exports, "loadConfig", { + enumerable: true, + get: function () { + return _configuration.loadConfig; + } +}); +Object.defineProperty(exports, "loadPlugin", { + enumerable: true, + get: function () { + return _plugins.loadPlugin; + } +}); +Object.defineProperty(exports, "loadPreset", { + enumerable: true, + get: function () { + return _plugins.loadPreset; + } +}); +Object.defineProperty(exports, "resolvePlugin", { + enumerable: true, + get: function () { + return _plugins.resolvePlugin; + } +}); +Object.defineProperty(exports, "resolvePreset", { + enumerable: true, + get: function () { + return _plugins.resolvePreset; + } +}); +Object.defineProperty(exports, "resolveShowConfigPath", { + enumerable: true, + get: function () { + return _configuration.resolveShowConfigPath; + } +}); +var _package = require("./package.js"); +var _configuration = require("./configuration.js"); +var _plugins = require("./plugins.js"); +({}); +0 && 0; + +//# sourceMappingURL=index.js.map