summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.claude/commands/add-livehouse.md66
-rw-r--r--.gitignore8
-rw-r--r--README.md147
-rw-r--r--app/components/EventCard.tsx89
-rw-r--r--app/components/FilterBar.tsx85
-rw-r--r--app/lib/db.server.ts192
-rw-r--r--app/lib/markdown-writer.server.ts80
-rw-r--r--app/lib/scraper-runner.server.ts77
-rw-r--r--app/lib/venue-meta.server.ts14
-rw-r--r--app/root.tsx5
-rw-r--r--app/routes.ts12
-rw-r--r--app/routes/api.scrape.ts37
-rw-r--r--app/routes/events.$id.tsx124
-rw-r--r--app/routes/events._index.tsx94
-rw-r--r--app/routes/index.tsx5
-rw-r--r--app/routes/venues.tsx68
-rw-r--r--app/scrapers/base.ts14
-rw-r--r--app/scrapers/club-quattro.ts78
-rw-r--r--app/scrapers/index.ts20
-rw-r--r--app/scrapers/liquid-room.ts87
-rw-r--r--app/scrapers/shibuya-o.ts82
-rw-r--r--app/scrapers/shinjuku-loft.ts80
-rw-r--r--app/scrapers/www-shibuya.ts79
-rw-r--r--package-lock.json2094
-rw-r--r--package.json20
-rw-r--r--vite.config.ts6
26 files changed, 3100 insertions, 563 deletions
diff --git a/.claude/commands/add-livehouse.md b/.claude/commands/add-livehouse.md
new file mode 100644
index 0000000..5aff417
--- /dev/null
+++ b/.claude/commands/add-livehouse.md
@@ -0,0 +1,66 @@
+# add-livehouse
+
+新しいライブハウスのスクレイパーを追加するスキル。
+
+## 使い方
+
+```
+/add-livehouse <会場名> <URL>
+```
+
+例:
+```
+/add-livehouse "渋谷 WWW X" "https://www-shibuya.jp/schedule/"
+```
+
+## 実行手順
+
+以下の手順を順番に実行する。
+
+### 1. スクレイパーファイルの作成
+
+`$ARGUMENTS` から会場名とURLを取得して、`app/scrapers/<venue-id>.ts` を作成する。
+
+- `venue-id` はURLや会場名から小文字ハイフン区切りで生成(例: `shibuya-www-x`)
+- 既存スクレイパー(例: `app/scrapers/liquid-room.ts`)を参考にして同じ構造で作成
+- `VenueMeta` に `id`, `name`, `url`, `area` を設定
+- `scrape()` メソッドで対象URLのHTMLを fetch → cheerio でパース → `EventInput[]` を返す
+- 最低限 `title`, `date`, `artist`, `start_time`, `open_time`, `ticket_url`, `price` を取得する
+- 日付パース関数 `parseJapaneseDate` を実装(既存コードからコピー可)
+
+### 2. インデックスへの登録
+
+`app/scrapers/index.ts` を編集して新しいスクレイパーを追加:
+
+```ts
+import { scraper as <camelCaseName> } from "./<venue-id>";
+
+export const ALL_SCRAPERS: Scraper[] = [
+ // 既存エントリ...
+ <camelCaseName>, // ← 追加
+];
+```
+
+### 3. 動作確認
+
+```bash
+# 開発サーバーを起動した状態で
+curl "http://localhost:5173/api/scrape?venue_id=<venue-id>"
+```
+
+レスポンス例:
+```json
+{ "results": [{ "venue_id": "...", "events_saved": 12 }] }
+```
+
+### 4. Markdown ファイルの確認
+
+スクレイプ成功後、`events/<venue-id>.md` が生成される。
+内容を確認して日付・出演者・料金が正しくパースされているか確認する。
+
+## 注意事項
+
+- `scrape()` 内で `fetch()` を使う(Node.js 標準 fetch で OK)
+- HTML構造はサイトによって異なるので、実際のページを確認してセレクタを調整する
+- サイトによってはJavaScriptレンダリングが必要な場合がある(その場合は公式RSSやAPIを探すこと)
+- ファイル名は必ず `.ts`(サーバー専用コードだが `.server.ts` は不要)
diff --git a/.gitignore b/.gitignore
index 039ee62..7c4fad0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,3 +5,11 @@
# React Router
/.react-router/
/build/
+
+# SQLite DB (runtime data)
+*.db
+*.db-shm
+*.db-wal
+
+# Generated Markdown event files (dev reference, auto-generated)
+/events/
diff --git a/README.md b/README.md
index 5c4780a..7347bfc 100644
--- a/README.md
+++ b/README.md
@@ -1,87 +1,130 @@
-# Welcome to React Router!
+# 東京ライブハウス イベント情報
-A modern, production-ready template for building full-stack React applications using React Router.
+東京の主要ライブハウスのイベント情報を自動収集・集約するフルスタック Web サービス。
-[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/remix-run/react-router-templates/tree/main/default)
+React Router v7 (SSR) + SQLite + Tailwind CSS で構築。
-## Features
+## 機能
-- 🚀 Server-side rendering
-- ⚡️ Hot Module Replacement (HMR)
-- 📦 Asset bundling and optimization
-- 🔄 Data loading and mutations
-- 🔒 TypeScript by default
-- 🎉 TailwindCSS for styling
-- 📖 [React Router docs](https://reactrouter.com/)
+- 複数ライブハウスのイベントをスクレイピングで自動取得
+- キーワード・会場・期間によるフィルタリング
+- 出演者・日時・場所・チケットURL・料金を一覧表示
+- 会場ごとのイベント Markdown ファイルを自動生成(開発参照用)
+- スクレイパーをモジュールとして追加可能な設計
-## Getting Started
+## 対応ライブハウス(初期)
-### Installation
+| 会場 | エリア |
+|------|--------|
+| LIQUID ROOM | 恵比寿 |
+| WWW / WWW X | 渋谷 |
+| 渋谷 O-EAST / O-WEST | 渋谷 |
+| 新宿 LOFT | 新宿 |
+| CLUB QUATTRO | 渋谷 |
-Install the dependencies:
+## セットアップ
-```bash
-npm install
-```
-
-### Development
+### 必要環境
-Start the development server with HMR:
+- Node.js 20.12 以上(`styleText` API が必要)
+- npm
```bash
+npm install
npm run dev
```
-Your application will be available at `http://localhost:5173`.
-
-## Building for Production
+`http://localhost:5173` にアクセス。
-Create a production build:
+### 本番ビルド
```bash
npm run build
+npm start
```
-## Deployment
+## 画面構成
-### Docker Deployment
+| パス | 内容 |
+|------|------|
+| `/events` | イベント一覧(フィルタ付き) |
+| `/events/:id` | イベント詳細 |
+| `/venues` | 会場一覧 |
+| `GET /api/scrape` | 全会場スクレイプ実行 |
+| `GET /api/scrape?venue_id=<id>` | 特定会場のみ実行 |
-To build and run using Docker:
+## 新しい会場を追加する
-```bash
-docker build -t my-app .
+### 1. スクレイパーファイルを作成
-# Run the container
-docker run -p 3000:3000 my-app
-```
+`app/scrapers/` に新しいファイルを追加し、`Scraper` インターフェースを実装する。
+既存のファイル(例: [`app/scrapers/liquid-room.ts`](app/scrapers/liquid-room.ts))を参考にする。
-The containerized application can be deployed to any platform that supports Docker, including:
+```ts
+import type { Scraper, VenueMeta } from "./base";
-- AWS ECS
-- Google Cloud Run
-- Azure Container Apps
-- Digital Ocean App Platform
-- Fly.io
-- Railway
+export const venue: VenueMeta = {
+ id: "my-venue", // URL 等から小文字ハイフン区切りで
+ name: "MY VENUE",
+ url: "https://example.com",
+ area: "渋谷",
+};
-### DIY Deployment
+export const scraper: Scraper = {
+ venue,
+ async scrape() {
+ // fetch → cheerio でパース → EventInput[] を返す
+ },
+};
+```
-If you're familiar with deploying Node applications, the built-in app server is production-ready.
+### 2. インデックスに登録
-Make sure to deploy the output of `npm run build`
+[`app/scrapers/index.ts`](app/scrapers/index.ts) に追記するだけで自動的に全機能に反映される。
+```ts
+import { scraper as myVenue } from "./my-venue";
+
+export const ALL_SCRAPERS: Scraper[] = [
+ // 既存...
+ myVenue,
+];
```
-├── package.json
-├── package-lock.json (or pnpm-lock.yaml, or bun.lockb)
-├── build/
-│ ├── client/ # Static assets
-│ └── server/ # Server-side code
-```
-## Styling
+### Claude Code スキル
+
+プロジェクト内に `/add-livehouse` スキルを定義済み。
+Claude Code から `/add-livehouse <会場名> <URL>` を実行するとスクレイパー追加を自動的にガイドする。
-This template comes with [Tailwind CSS](https://tailwindcss.com/) already configured for a simple default starting experience. You can use whatever CSS framework you prefer.
+## プロジェクト構成
+
+```
+app/
+├── lib/
+│ ├── db.server.ts # SQLite 操作(better-sqlite3)
+│ ├── scraper-runner.server.ts # スクレイプ実行 + Markdown 生成
+│ ├── markdown-writer.server.ts # events/<venue-id>.md 生成(開発参照用)
+│ └── venue-meta.server.ts # サーバー専用スクレイパーメタデータ
+├── scrapers/
+│ ├── base.ts # Scraper インターフェース定義
+│ ├── index.ts # スクレイパーレジストリ(ここに追加)
+│ ├── liquid-room.ts
+│ ├── www-shibuya.ts
+│ ├── shibuya-o.ts
+│ ├── shinjuku-loft.ts
+│ └── club-quattro.ts
+├── components/
+│ ├── EventCard.tsx # イベントカード(出演者・日時・会場・料金・URL)
+│ └── FilterBar.tsx # 検索フィルタ
+└── routes/
+ ├── events._index.tsx
+ ├── events.$id.tsx
+ ├── venues.tsx
+ └── api.scrape.ts
+events/ # スクレイプ後に自動生成される Markdown(開発参照用)
+```
----
+## 注意事項
-Built with ❤️ using React Router.
+- 各ライブハウスの HTML 構造はサイトによって異なるため、スクレイパーのセレクタは実際のページを確認しながら調整が必要
+- JavaScript レンダリングが必要なサイトは `fetch` ではなく公式 RSS / API の利用を検討する
diff --git a/app/components/EventCard.tsx b/app/components/EventCard.tsx
new file mode 100644
index 0000000..6651ff9
--- /dev/null
+++ b/app/components/EventCard.tsx
@@ -0,0 +1,89 @@
+import { Link } from "react-router";
+import type { Event } from "~/lib/db.server";
+
+interface Props {
+ event: Event;
+}
+
+export default function EventCard({ event }: Props) {
+ const formattedDate = formatDate(event.date);
+ const timeLabel = buildTimeLabel(event.open_time, event.start_time);
+
+ return (
+ <Link
+ to={`/events/${event.id}`}
+ className="group flex flex-col rounded-xl bg-gray-800/60 border border-gray-700/40 overflow-hidden hover:border-indigo-500/60 hover:bg-gray-800 transition-all"
+ >
+ {event.image_url ? (
+ <img
+ src={event.image_url}
+ alt={event.title}
+ className="h-36 w-full object-cover"
+ />
+ ) : (
+ <div className="h-36 w-full bg-gradient-to-br from-gray-800 to-gray-900 flex items-center justify-center">
+ <span className="text-4xl opacity-20">🎸</span>
+ </div>
+ )}
+
+ <div className="flex-1 p-4 flex flex-col gap-2">
+ {/* Title */}
+ <h2 className="font-semibold text-sm leading-snug group-hover:text-indigo-300 transition-colors line-clamp-2">
+ {event.title}
+ </h2>
+
+ {/* Artist — required */}
+ <p className="text-xs text-indigo-300 font-medium line-clamp-1">
+ {event.artist ?? "出演者未定"}
+ </p>
+
+ {/* Date + time */}
+ <div className="flex items-center gap-2 text-xs text-gray-300">
+ <span>📅 {formattedDate}</span>
+ {timeLabel && <span className="text-gray-500">| {timeLabel}</span>}
+ </div>
+
+ {/* Venue */}
+ <div className="flex items-center gap-1 text-xs text-gray-400">
+ <span>📍</span>
+ <span className="rounded-full bg-gray-700/60 px-2 py-0.5">
+ {event.venue_name}
+ {event.venue_area ? `(${event.venue_area})` : ""}
+ </span>
+ </div>
+
+ {/* Fee */}
+ {event.price && (
+ <p className="text-xs text-emerald-400">¥ {event.price}</p>
+ )}
+
+ {/* Ticket URL */}
+ {event.ticket_url && (
+ <a
+ href={event.ticket_url}
+ target="_blank"
+ rel="noopener noreferrer"
+ onClick={(e) => e.stopPropagation()}
+ className="mt-auto inline-flex items-center gap-1 text-xs text-indigo-400 hover:underline"
+ >
+ 🎟 チケット
+ </a>
+ )}
+ </div>
+ </Link>
+ );
+}
+
+function formatDate(iso: string): string {
+ const [y, m, d] = iso.split("-");
+ const days = ["日", "月", "火", "水", "木", "金", "土"];
+ const dayIdx = new Date(`${iso}T00:00:00`).getDay();
+ return `${y}/${m}/${d}(${days[dayIdx]})`;
+}
+
+function buildTimeLabel(open: string | null, start: string | null): string {
+ const parts: string[] = [];
+ if (open) parts.push(`OPEN ${open}`);
+ if (start) parts.push(`START ${start}`);
+ return parts.join(" / ");
+}
diff --git a/app/components/FilterBar.tsx b/app/components/FilterBar.tsx
new file mode 100644
index 0000000..97a3c02
--- /dev/null
+++ b/app/components/FilterBar.tsx
@@ -0,0 +1,85 @@
+import { Form, useSearchParams } from "react-router";
+import type { Venue } from "~/lib/db.server";
+
+interface Props {
+ venues: Venue[];
+}
+
+export default function FilterBar({ venues }: Props) {
+ const [searchParams] = useSearchParams();
+
+ return (
+ <Form method="get" className="flex flex-wrap gap-3 items-end">
+ {/* Keyword */}
+ <div className="flex flex-col gap-1">
+ <label className="text-xs text-gray-400">キーワード</label>
+ <input
+ name="keyword"
+ type="text"
+ defaultValue={searchParams.get("keyword") ?? ""}
+ placeholder="アーティスト名、イベント名..."
+ className="rounded-md bg-gray-800 border border-gray-700 px-3 py-1.5 text-sm focus:outline-none focus:ring-1 focus:ring-indigo-500 w-52"
+ />
+ </div>
+
+ {/* Venue */}
+ <div className="flex flex-col gap-1">
+ <label className="text-xs text-gray-400">会場</label>
+ <select
+ name="venue_id"
+ defaultValue={searchParams.get("venue_id") ?? ""}
+ className="rounded-md bg-gray-800 border border-gray-700 px-3 py-1.5 text-sm focus:outline-none focus:ring-1 focus:ring-indigo-500"
+ >
+ <option value="">すべて</option>
+ {venues.map((v) => (
+ <option key={v.id} value={v.id}>
+ {v.name}
+ </option>
+ ))}
+ </select>
+ </div>
+
+ {/* Date from */}
+ <div className="flex flex-col gap-1">
+ <label className="text-xs text-gray-400">開始日</label>
+ <input
+ name="date_from"
+ type="date"
+ defaultValue={searchParams.get("date_from") ?? ""}
+ className="rounded-md bg-gray-800 border border-gray-700 px-3 py-1.5 text-sm focus:outline-none focus:ring-1 focus:ring-indigo-500"
+ />
+ </div>
+
+ {/* Date to */}
+ <div className="flex flex-col gap-1">
+ <label className="text-xs text-gray-400">終了日</label>
+ <input
+ name="date_to"
+ type="date"
+ defaultValue={searchParams.get("date_to") ?? ""}
+ className="rounded-md bg-gray-800 border border-gray-700 px-3 py-1.5 text-sm focus:outline-none focus:ring-1 focus:ring-indigo-500"
+ />
+ </div>
+
+ <button
+ type="submit"
+ className="rounded-md bg-gray-700 px-4 py-1.5 text-sm font-medium hover:bg-gray-600 transition-colors"
+ >
+ 絞り込む
+ </button>
+
+ {hasFilters(searchParams) && (
+ <a
+ href="/events"
+ className="rounded-md border border-gray-700 px-4 py-1.5 text-sm text-gray-400 hover:text-white transition-colors"
+ >
+ クリア
+ </a>
+ )}
+ </Form>
+ );
+}
+
+function hasFilters(params: URLSearchParams): boolean {
+ return ["keyword", "venue_id", "date_from", "date_to"].some((k) => params.get(k));
+}
diff --git a/app/lib/db.server.ts b/app/lib/db.server.ts
new file mode 100644
index 0000000..0c55991
--- /dev/null
+++ b/app/lib/db.server.ts
@@ -0,0 +1,192 @@
+import Database from "better-sqlite3";
+import path from "path";
+import { fileURLToPath } from "url";
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url));
+const DB_PATH = path.join(__dirname, "../../events.db");
+
+let _db: Database.Database | null = null;
+
+function getDb(): Database.Database {
+ if (!_db) {
+ _db = new Database(DB_PATH);
+ _db.pragma("journal_mode = WAL");
+ _db.pragma("foreign_keys = ON");
+ initSchema(_db);
+ }
+ return _db;
+}
+
+function initSchema(db: Database.Database) {
+ db.exec(`
+ CREATE TABLE IF NOT EXISTS venues (
+ id TEXT PRIMARY KEY,
+ name TEXT NOT NULL,
+ url TEXT NOT NULL,
+ area TEXT
+ );
+
+ CREATE TABLE IF NOT EXISTS events (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ venue_id TEXT NOT NULL REFERENCES venues(id),
+ title TEXT NOT NULL,
+ artist TEXT,
+ date TEXT NOT NULL,
+ start_time TEXT,
+ open_time TEXT,
+ ticket_url TEXT,
+ price TEXT,
+ image_url TEXT,
+ description TEXT,
+ source_url TEXT,
+ fetched_at TEXT NOT NULL DEFAULT (datetime('now')),
+ UNIQUE(venue_id, title, date)
+ );
+
+ CREATE INDEX IF NOT EXISTS idx_events_date ON events(date);
+ CREATE INDEX IF NOT EXISTS idx_events_venue_id ON events(venue_id);
+ `);
+}
+
+export interface Venue {
+ id: string;
+ name: string;
+ url: string;
+ area: string | null;
+ event_count?: number;
+}
+
+export interface Event {
+ id: number;
+ venue_id: string;
+ venue_name: string;
+ venue_area: string | null;
+ venue_url?: string;
+ title: string;
+ artist: string | null;
+ date: string;
+ start_time: string | null;
+ open_time: string | null;
+ ticket_url: string | null;
+ price: string | null;
+ image_url: string | null;
+ description: string | null;
+ source_url: string | null;
+ fetched_at: string;
+}
+
+export interface EventInput {
+ venue_id: string;
+ title: string;
+ artist?: string | null;
+ date: string;
+ start_time?: string | null;
+ open_time?: string | null;
+ ticket_url?: string | null;
+ price?: string | null;
+ image_url?: string | null;
+ description?: string | null;
+ source_url?: string | null;
+}
+
+export function upsertVenue(
+ id: string,
+ name: string,
+ url: string,
+ area?: string
+) {
+ getDb()
+ .prepare(
+ "INSERT OR REPLACE INTO venues (id, name, url, area) VALUES (?, ?, ?, ?)"
+ )
+ .run(id, name, url, area ?? null);
+}
+
+export function upsertEvent(event: EventInput) {
+ getDb()
+ .prepare(
+ `INSERT INTO events
+ (venue_id, title, artist, date, start_time, open_time,
+ ticket_url, price, image_url, description, source_url, fetched_at)
+ VALUES
+ (@venue_id, @title, @artist, @date, @start_time, @open_time,
+ @ticket_url, @price, @image_url, @description, @source_url, datetime('now'))
+ ON CONFLICT(venue_id, title, date) DO UPDATE SET
+ artist = excluded.artist,
+ start_time = excluded.start_time,
+ open_time = excluded.open_time,
+ ticket_url = excluded.ticket_url,
+ price = excluded.price,
+ image_url = excluded.image_url,
+ description = excluded.description,
+ source_url = excluded.source_url,
+ fetched_at = excluded.fetched_at`
+ )
+ .run(event);
+}
+
+export interface QueryEventsParams {
+ date_from?: string;
+ date_to?: string;
+ venue_id?: string;
+ keyword?: string;
+ limit?: number;
+ offset?: number;
+}
+
+export function queryEvents(params: QueryEventsParams = {}): Event[] {
+ const { date_from, date_to, venue_id, keyword, limit = 60, offset = 0 } =
+ params;
+
+ const clauses: string[] = [];
+ const args: unknown[] = [];
+
+ if (date_from) {
+ clauses.push("e.date >= ?");
+ args.push(date_from);
+ }
+ if (date_to) {
+ clauses.push("e.date <= ?");
+ args.push(date_to);
+ }
+ if (venue_id) {
+ clauses.push("e.venue_id = ?");
+ args.push(venue_id);
+ }
+ if (keyword) {
+ clauses.push("(e.title LIKE ? OR e.artist LIKE ?)");
+ args.push(`%${keyword}%`, `%${keyword}%`);
+ }
+
+ const where = clauses.length ? `WHERE ${clauses.join(" AND ")}` : "";
+
+ return getDb()
+ .prepare(
+ `SELECT e.*, v.name AS venue_name, v.area AS venue_area
+ FROM events e JOIN venues v ON e.venue_id = v.id
+ ${where}
+ ORDER BY e.date ASC, e.start_time ASC
+ LIMIT ? OFFSET ?`
+ )
+ .all(...args, limit, offset) as Event[];
+}
+
+export function getEvent(id: number): Event | undefined {
+ return getDb()
+ .prepare(
+ `SELECT e.*, v.name AS venue_name, v.url AS venue_url, v.area AS venue_area
+ FROM events e JOIN venues v ON e.venue_id = v.id
+ WHERE e.id = ?`
+ )
+ .get(id) as Event | undefined;
+}
+
+export function getVenues(): Venue[] {
+ return getDb()
+ .prepare(
+ `SELECT v.*, COUNT(e.id) AS event_count
+ FROM venues v LEFT JOIN events e ON v.id = e.venue_id
+ GROUP BY v.id ORDER BY v.name`
+ )
+ .all() as Venue[];
+}
diff --git a/app/lib/markdown-writer.server.ts b/app/lib/markdown-writer.server.ts
new file mode 100644
index 0000000..cfef315
--- /dev/null
+++ b/app/lib/markdown-writer.server.ts
@@ -0,0 +1,80 @@
+/**
+ * Generates a Markdown summary file per venue after scraping.
+ * Files are written to events/<venue-id>.md in the project root.
+ */
+import fs from "fs";
+import path from "path";
+import { fileURLToPath } from "url";
+import { queryEvents } from "./db.server";
+import type { Event } from "./db.server";
+
+const ROOT = path.join(path.dirname(fileURLToPath(import.meta.url)), "../../");
+const EVENTS_DIR = path.join(ROOT, "events");
+
+export function generateVenueMarkdown(venueId: string): void {
+ const events = queryEvents({ venue_id: venueId, limit: 200 });
+ if (events.length === 0) return;
+
+ fs.mkdirSync(EVENTS_DIR, { recursive: true });
+
+ const venueName = events[0].venue_name;
+ const venueArea = events[0].venue_area ?? "";
+ const now = new Date().toISOString().slice(0, 10);
+
+ const lines: string[] = [
+ `# ${venueName}(${venueArea})イベント情報`,
+ ``,
+ `> 最終更新: ${now} `,
+ `> データソース: スクレイパー自動取得`,
+ ``,
+ `| 日付 | 出演者 | タイトル | 時間 | 料金 | URL |`,
+ `| ---- | ------ | -------- | ---- | ---- | --- |`,
+ ];
+
+ for (const ev of events) {
+ const date = formatDate(ev.date);
+ const artist = escape(ev.artist ?? "未定");
+ const title = escape(ev.title);
+ const time = buildTime(ev.open_time, ev.start_time);
+ const fee = escape(ev.price ?? "");
+ const url = ev.ticket_url
+ ? `[チケット](${ev.ticket_url})`
+ : ev.source_url
+ ? `[詳細](${ev.source_url})`
+ : "";
+
+ lines.push(`| ${date} | ${artist} | ${title} | ${time} | ${fee} | ${url} |`);
+ }
+
+ lines.push(``);
+ lines.push(`---`);
+ lines.push(`*このファイルは自動生成されます。手動編集は次回更新時に上書きされます。*`);
+ lines.push(``);
+
+ const filePath = path.join(EVENTS_DIR, `${venueId}.md`);
+ fs.writeFileSync(filePath, lines.join("\n"), "utf-8");
+}
+
+export function generateAllVenueMarkdown(venueIds: string[]): void {
+ for (const id of venueIds) {
+ generateVenueMarkdown(id);
+ }
+}
+
+function formatDate(iso: string): string {
+ const [y, m, d] = iso.split("-");
+ const days = ["日", "月", "火", "水", "木", "金", "土"];
+ const dayIdx = new Date(`${iso}T00:00:00`).getDay();
+ return `${y}/${m}/${d}(${days[dayIdx]})`;
+}
+
+function buildTime(open: string | null, start: string | null): string {
+ const parts: string[] = [];
+ if (open) parts.push(`OPEN ${open}`);
+ if (start) parts.push(`START ${start}`);
+ return parts.join(" / ") || "";
+}
+
+function escape(s: string): string {
+ return s.replace(/\|/g, "\\|").replace(/\n/g, " ");
+}
diff --git a/app/lib/scraper-runner.server.ts b/app/lib/scraper-runner.server.ts
new file mode 100644
index 0000000..070a568
--- /dev/null
+++ b/app/lib/scraper-runner.server.ts
@@ -0,0 +1,77 @@
+import { upsertVenue, upsertEvent } from "./db.server";
+import { generateVenueMarkdown, generateAllVenueMarkdown } from "./markdown-writer.server";
+import { ALL_SCRAPERS } from "~/scrapers/index";
+
+export interface ScrapeResult {
+ venue_id: string;
+ venue_name: string;
+ events_saved: number;
+ markdown_path?: string;
+ error?: string;
+}
+
+export async function runAllScrapers(): Promise<ScrapeResult[]> {
+ const results: ScrapeResult[] = [];
+ const successIds: string[] = [];
+
+ for (const scraper of ALL_SCRAPERS) {
+ const { venue } = scraper;
+ upsertVenue(venue.id, venue.name, venue.url, venue.area);
+
+ try {
+ const events = await scraper.scrape();
+ for (const event of events) {
+ upsertEvent(event);
+ }
+ successIds.push(venue.id);
+ results.push({
+ venue_id: venue.id,
+ venue_name: venue.name,
+ events_saved: events.length,
+ });
+ } catch (err) {
+ results.push({
+ venue_id: venue.id,
+ venue_name: venue.name,
+ events_saved: 0,
+ error: err instanceof Error ? err.message : String(err),
+ });
+ }
+ }
+
+ // Generate Markdown files for all venues that scraped successfully
+ generateAllVenueMarkdown(successIds);
+
+ return results;
+}
+
+export async function runScraper(venueId: string): Promise<ScrapeResult> {
+ const scraper = ALL_SCRAPERS.find((s) => s.venue.id === venueId);
+ if (!scraper) {
+ return { venue_id: venueId, venue_name: venueId, events_saved: 0, error: "Scraper not found" };
+ }
+
+ const { venue } = scraper;
+ upsertVenue(venue.id, venue.name, venue.url, venue.area);
+
+ try {
+ const events = await scraper.scrape();
+ for (const event of events) {
+ upsertEvent(event);
+ }
+ generateVenueMarkdown(venue.id);
+ return {
+ venue_id: venue.id,
+ venue_name: venue.name,
+ events_saved: events.length,
+ markdown_path: `events/${venue.id}.md`,
+ };
+ } catch (err) {
+ return {
+ venue_id: venue.id,
+ venue_name: venue.name,
+ events_saved: 0,
+ error: err instanceof Error ? err.message : String(err),
+ };
+ }
+}
diff --git a/app/lib/venue-meta.server.ts b/app/lib/venue-meta.server.ts
new file mode 100644
index 0000000..58743c8
--- /dev/null
+++ b/app/lib/venue-meta.server.ts
@@ -0,0 +1,14 @@
+/**
+ * Server-only module: exposes venue metadata from all registered scrapers.
+ * Importing this in a route loader ensures scraper code never reaches the client bundle.
+ */
+import { ALL_SCRAPERS } from "~/scrapers/index";
+import type { VenueMeta } from "~/scrapers/base";
+
+export function getScraperVenues(): VenueMeta[] {
+ return ALL_SCRAPERS.map((s) => s.venue);
+}
+
+export function getScraperIds(): string[] {
+ return ALL_SCRAPERS.map((s) => s.venue.id);
+}
diff --git a/app/root.tsx b/app/root.tsx
index 9fc6636..dd55df0 100644
--- a/app/root.tsx
+++ b/app/root.tsx
@@ -25,14 +25,15 @@ export const links: Route.LinksFunction = () => [
export function Layout({ children }: { children: React.ReactNode }) {
return (
- <html lang="en">
+ <html lang="ja" className="dark">
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
+ <title>東京ライブハウス</title>
<Meta />
<Links />
</head>
- <body>
+ <body className="bg-gray-950 text-gray-100 antialiased">
{children}
<ScrollRestoration />
<Scripts />
diff --git a/app/routes.ts b/app/routes.ts
index 102b402..028da16 100644
--- a/app/routes.ts
+++ b/app/routes.ts
@@ -1,3 +1,11 @@
-import { type RouteConfig, index } from "@react-router/dev/routes";
+import { type RouteConfig, index, route, prefix } from "@react-router/dev/routes";
-export default [index("routes/home.tsx")] satisfies RouteConfig;
+export default [
+ index("routes/index.tsx"),
+ ...prefix("events", [
+ index("routes/events._index.tsx"),
+ route(":id", "routes/events.$id.tsx"),
+ ]),
+ route("venues", "routes/venues.tsx"),
+ route("api/scrape", "routes/api.scrape.ts"),
+] satisfies RouteConfig;
diff --git a/app/routes/api.scrape.ts b/app/routes/api.scrape.ts
new file mode 100644
index 0000000..4071985
--- /dev/null
+++ b/app/routes/api.scrape.ts
@@ -0,0 +1,37 @@
+/**
+ * Resource route: POST /api/scrape
+ * Triggers scraping for all venues (or a specific one via ?venue_id=xxx).
+ * Returns JSON results and redirects back if called from a form.
+ */
+import { redirect } from "react-router";
+import type { Route } from "./+types/api.scrape";
+import { runAllScrapers, runScraper } from "~/lib/scraper-runner.server";
+
+export async function action({ request }: Route.ActionArgs) {
+ const formData = await request.formData();
+ const venueId = formData.get("venue_id");
+
+ const results = venueId
+ ? [await runScraper(String(venueId))]
+ : await runAllScrapers();
+
+ // If called from a browser form, redirect back
+ const referer = request.headers.get("Referer");
+ if (referer) {
+ return redirect(referer);
+ }
+
+ return Response.json({ results });
+}
+
+// Allow GET for quick testing in the browser
+export async function loader({ request }: Route.LoaderArgs) {
+ const url = new URL(request.url);
+ const venueId = url.searchParams.get("venue_id");
+
+ const results = venueId
+ ? [await runScraper(venueId)]
+ : await runAllScrapers();
+
+ return Response.json({ results });
+}
diff --git a/app/routes/events.$id.tsx b/app/routes/events.$id.tsx
new file mode 100644
index 0000000..cecb282
--- /dev/null
+++ b/app/routes/events.$id.tsx
@@ -0,0 +1,124 @@
+import { useLoaderData, Link } from "react-router";
+import type { Route } from "./+types/events.$id";
+import { getEvent } from "~/lib/db.server";
+
+export async function loader({ params }: Route.LoaderArgs) {
+ const id = parseInt(params.id, 10);
+ if (isNaN(id)) throw new Response("Not Found", { status: 404 });
+ const event = getEvent(id);
+ if (!event) throw new Response("Not Found", { status: 404 });
+ return { event };
+}
+
+export default function EventDetail() {
+ const { event } = useLoaderData<typeof loader>();
+
+ return (
+ <div className="min-h-screen bg-gray-950 text-gray-100">
+ <header className="border-b border-gray-800 px-6 py-4 flex items-center justify-between">
+ <Link to="/" className="text-xl font-bold tracking-tight text-white">
+ 🎸 東京ライブハウス
+ </Link>
+ <nav className="flex gap-6 text-sm text-gray-400">
+ <Link to="/events" className="hover:text-white transition-colors">イベント</Link>
+ <Link to="/venues" className="hover:text-white transition-colors">会場一覧</Link>
+ </nav>
+ </header>
+
+ <main className="max-w-3xl mx-auto px-4 py-10">
+ <Link to="/events" className="text-sm text-indigo-400 hover:underline">
+ ← イベント一覧に戻る
+ </Link>
+
+ <div className="mt-6">
+ {event.image_url && (
+ <img
+ src={event.image_url}
+ alt={event.title}
+ className="w-full max-h-72 object-cover rounded-xl mb-6"
+ />
+ )}
+
+ <div className="flex items-start justify-between gap-4 flex-wrap">
+ <div>
+ <h1 className="text-3xl font-bold leading-tight">{event.title}</h1>
+ {event.artist && (
+ <p className="mt-1 text-lg text-gray-300">{event.artist}</p>
+ )}
+ </div>
+ <span className="rounded-full bg-indigo-700/60 px-3 py-1 text-sm font-medium whitespace-nowrap">
+ {event.venue_name}
+ </span>
+ </div>
+
+ <dl className="mt-8 grid grid-cols-2 gap-4 text-sm">
+ <Detail label="日付" value={formatDate(event.date)} />
+ {event.open_time && <Detail label="OPEN" value={event.open_time} />}
+ {event.start_time && <Detail label="START" value={event.start_time} />}
+ {event.price && <Detail label="料金" value={event.price} />}
+ {event.venue_area && <Detail label="エリア" value={event.venue_area} />}
+ </dl>
+
+ {event.description && (
+ <p className="mt-8 text-gray-300 leading-relaxed whitespace-pre-line">
+ {event.description}
+ </p>
+ )}
+
+ <div className="mt-8 flex gap-4 flex-wrap">
+ {event.ticket_url && (
+ <a
+ href={event.ticket_url}
+ target="_blank"
+ rel="noopener noreferrer"
+ className="rounded-md bg-indigo-600 px-5 py-2 text-sm font-medium hover:bg-indigo-500 transition-colors"
+ >
+ チケット購入
+ </a>
+ )}
+ {event.source_url && (
+ <a
+ href={event.source_url}
+ target="_blank"
+ rel="noopener noreferrer"
+ className="rounded-md bg-gray-700 px-5 py-2 text-sm font-medium hover:bg-gray-600 transition-colors"
+ >
+ 詳細ページ
+ </a>
+ )}
+ {event.venue_url && (
+ <a
+ href={event.venue_url}
+ target="_blank"
+ rel="noopener noreferrer"
+ className="rounded-md bg-gray-700 px-5 py-2 text-sm font-medium hover:bg-gray-600 transition-colors"
+ >
+ 会場サイト
+ </a>
+ )}
+ </div>
+
+ <p className="mt-10 text-xs text-gray-600">
+ 最終取得: {event.fetched_at}
+ </p>
+ </div>
+ </main>
+ </div>
+ );
+}
+
+function Detail({ label, value }: { label: string; value: string }) {
+ return (
+ <div className="rounded-lg bg-gray-800/60 p-3">
+ <dt className="text-xs text-gray-500 mb-1">{label}</dt>
+ <dd className="font-medium">{value}</dd>
+ </div>
+ );
+}
+
+function formatDate(iso: string): string {
+ const [y, m, d] = iso.split("-");
+ const days = ["日", "月", "火", "水", "木", "金", "土"];
+ const day = days[new Date(iso).getDay()];
+ return `${y}年${m}月${d}日(${day})`;
+}
diff --git a/app/routes/events._index.tsx b/app/routes/events._index.tsx
new file mode 100644
index 0000000..3883d37
--- /dev/null
+++ b/app/routes/events._index.tsx
@@ -0,0 +1,94 @@
+import { useLoaderData, useSearchParams, Form, Link } from "react-router";
+import type { Route } from "./+types/events._index";
+import { queryEvents, getVenues } from "~/lib/db.server";
+import EventCard from "~/components/EventCard";
+import FilterBar from "~/components/FilterBar";
+
+export async function loader({ request }: Route.LoaderArgs) {
+ const url = new URL(request.url);
+ const date_from = url.searchParams.get("date_from") ?? undefined;
+ const date_to = url.searchParams.get("date_to") ?? undefined;
+ const venue_id = url.searchParams.get("venue_id") ?? undefined;
+ const keyword = url.searchParams.get("keyword") ?? undefined;
+ const page = Math.max(1, parseInt(url.searchParams.get("page") ?? "1", 10));
+ const limit = 30;
+ const offset = (page - 1) * limit;
+
+ const events = queryEvents({ date_from, date_to, venue_id, keyword, limit, offset });
+ const venues = getVenues();
+
+ return { events, venues, page, hasMore: events.length === limit };
+}
+
+export default function EventsIndex() {
+ const { events, venues, page, hasMore } = useLoaderData<typeof loader>();
+ const [searchParams] = useSearchParams();
+
+ return (
+ <div className="min-h-screen bg-gray-950 text-gray-100">
+ <header className="border-b border-gray-800 px-6 py-4 flex items-center justify-between">
+ <Link to="/" className="text-xl font-bold tracking-tight text-white">
+ 🎸 東京ライブハウス
+ </Link>
+ <nav className="flex gap-6 text-sm text-gray-400">
+ <Link to="/events" className="hover:text-white transition-colors">イベント</Link>
+ <Link to="/venues" className="hover:text-white transition-colors">会場一覧</Link>
+ </nav>
+ </header>
+
+ <main className="max-w-6xl mx-auto px-4 py-8">
+ <div className="mb-6 flex items-center justify-between">
+ <h1 className="text-2xl font-bold">イベント一覧</h1>
+ <Form method="post" action="/api/scrape">
+ <button
+ type="submit"
+ className="rounded-md bg-indigo-600 px-4 py-2 text-sm font-medium hover:bg-indigo-500 transition-colors"
+ >
+ 情報を更新
+ </button>
+ </Form>
+ </div>
+
+ <FilterBar venues={venues} />
+
+ {events.length === 0 ? (
+ <div className="mt-16 text-center text-gray-500">
+ <p className="text-lg">イベントが見つかりません</p>
+ <p className="mt-2 text-sm">「情報を更新」ボタンでデータを取得してください。</p>
+ </div>
+ ) : (
+ <div className="mt-6 grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
+ {events.map((event) => (
+ <EventCard key={event.id} event={event} />
+ ))}
+ </div>
+ )}
+
+ <div className="mt-8 flex justify-center gap-4">
+ {page > 1 && (
+ <Link
+ to={`?${buildPageParams(searchParams, page - 1)}`}
+ className="rounded bg-gray-800 px-4 py-2 text-sm hover:bg-gray-700"
+ >
+ ← 前のページ
+ </Link>
+ )}
+ {hasMore && (
+ <Link
+ to={`?${buildPageParams(searchParams, page + 1)}`}
+ className="rounded bg-gray-800 px-4 py-2 text-sm hover:bg-gray-700"
+ >
+ 次のページ →
+ </Link>
+ )}
+ </div>
+ </main>
+ </div>
+ );
+}
+
+function buildPageParams(params: URLSearchParams, page: number): string {
+ const next = new URLSearchParams(params);
+ next.set("page", String(page));
+ return next.toString();
+}
diff --git a/app/routes/index.tsx b/app/routes/index.tsx
new file mode 100644
index 0000000..1cdb9a4
--- /dev/null
+++ b/app/routes/index.tsx
@@ -0,0 +1,5 @@
+import { redirect } from "react-router";
+
+export function loader() {
+ return redirect("/events");
+}
diff --git a/app/routes/venues.tsx b/app/routes/venues.tsx
new file mode 100644
index 0000000..23b052f
--- /dev/null
+++ b/app/routes/venues.tsx
@@ -0,0 +1,68 @@
+import { useLoaderData, Link } from "react-router";
+import type { Route } from "./+types/venues";
+import { getVenues } from "~/lib/db.server";
+import { getScraperIds } from "~/lib/venue-meta.server";
+
+export async function loader(_: Route.LoaderArgs) {
+ const venues = getVenues();
+ const scraperIds = getScraperIds();
+ return { venues, scraperIds };
+}
+
+export default function Venues() {
+ const { venues, scraperIds: scraperIdList } = useLoaderData<typeof loader>();
+ const scraperIds = new Set(scraperIdList);
+
+ return (
+ <div className="min-h-screen bg-gray-950 text-gray-100">
+ <header className="border-b border-gray-800 px-6 py-4 flex items-center justify-between">
+ <Link to="/" className="text-xl font-bold tracking-tight text-white">
+ 🎸 東京ライブハウス
+ </Link>
+ <nav className="flex gap-6 text-sm text-gray-400">
+ <Link to="/events" className="hover:text-white transition-colors">イベント</Link>
+ <Link to="/venues" className="text-white font-medium">会場一覧</Link>
+ </nav>
+ </header>
+
+ <main className="max-w-4xl mx-auto px-4 py-10">
+ <div className="mb-8">
+ <h1 className="text-2xl font-bold">会場一覧</h1>
+ <p className="mt-1 text-sm text-gray-400">
+ 現在 {scraperIdList.length} 会場のスクレイパーが登録されています。
+ 新しい会場を追加するには <code className="bg-gray-800 px-1 rounded">app/scrapers/</code> に
+ モジュールを追加して <code className="bg-gray-800 px-1 rounded">index.ts</code> に登録してください。
+ </p>
+ </div>
+
+ {venues.length === 0 ? (
+ <p className="text-gray-500">まだ会場データがありません。「情報を更新」してください。</p>
+ ) : (
+ <div className="grid gap-4 sm:grid-cols-2">
+ {venues.map((v) => (
+ <Link
+ key={v.id}
+ to={`/events?venue_id=${v.id}`}
+ className="flex items-center justify-between rounded-xl bg-gray-800/60 p-4 hover:bg-gray-800 transition-colors border border-gray-700/50"
+ >
+ <div>
+ <p className="font-semibold">{v.name}</p>
+ {v.area && <p className="text-sm text-gray-400">{v.area}</p>}
+ {scraperIds.has(v.id) && (
+ <span className="mt-1 inline-block rounded-full bg-emerald-700/40 px-2 py-0.5 text-xs text-emerald-300">
+ スクレイパー登録済
+ </span>
+ )}
+ </div>
+ <span className="text-2xl font-bold text-gray-500">
+ {v.event_count ?? 0}
+ <span className="text-sm font-normal ml-1">件</span>
+ </span>
+ </Link>
+ ))}
+ </div>
+ )}
+ </main>
+ </div>
+ );
+}
diff --git a/app/scrapers/base.ts b/app/scrapers/base.ts
new file mode 100644
index 0000000..512fcbb
--- /dev/null
+++ b/app/scrapers/base.ts
@@ -0,0 +1,14 @@
+import type { EventInput } from "~/lib/db.server";
+
+export interface VenueMeta {
+ id: string;
+ name: string;
+ url: string;
+ area: string;
+}
+
+export interface Scraper {
+ venue: VenueMeta;
+ /** Fetch events from the venue's website. Returns event inputs ready for upsert. */
+ scrape(): Promise<EventInput[]>;
+}
diff --git a/app/scrapers/club-quattro.ts b/app/scrapers/club-quattro.ts
new file mode 100644
index 0000000..ae903bc
--- /dev/null
+++ b/app/scrapers/club-quattro.ts
@@ -0,0 +1,78 @@
+/**
+ * Club Quattro 渋谷 — https://www.club-quattro.com/shibuya/schedule/
+ */
+import * as cheerio from "cheerio";
+import type { Scraper, VenueMeta } from "./base";
+import type { EventInput } from "~/lib/db.server";
+
+export const venue: VenueMeta = {
+ id: "club-quattro",
+ name: "CLUB QUATTRO",
+ url: "https://www.club-quattro.com",
+ area: "渋谷",
+};
+
+export const scraper: Scraper = {
+ venue,
+ async scrape(): Promise<EventInput[]> {
+ const res = await fetch("https://www.club-quattro.com/shibuya/schedule/");
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
+ const html = await res.text();
+ const $ = cheerio.load(html);
+ const events: EventInput[] = [];
+
+ $(".schedule-list__item, .c-event, li.event").each((_, el) => {
+ const $el = $(el);
+
+ const title = $el.find(".schedule-list__title, .event-name, h3, h2").first().text().trim();
+ if (!title) return;
+
+ const rawDate =
+ $el.find(".schedule-list__date, .event-date, time").first().text().trim() ||
+ $el.find("time").attr("datetime") ||
+ "";
+ const date = parseJapaneseDate(rawDate);
+ if (!date) return;
+
+ const timeText = $el.find(".schedule-list__time, .time-info").first().text();
+ const openMatch = timeText.match(/OPEN[:: ]*(\d{2}:\d{2})/i);
+ const startMatch = timeText.match(/START[:: ]*(\d{2}:\d{2})/i);
+
+ const detailHref = $el.find("a[href]").first().attr("href") ?? null;
+
+ events.push({
+ venue_id: venue.id,
+ title,
+ artist: $el.find(".schedule-list__artist, .artist-name").first().text().trim() || null,
+ date,
+ open_time: openMatch?.[1] ?? null,
+ start_time: startMatch?.[1] ?? null,
+ ticket_url:
+ $el.find("a[href*='eplus'], a[href*='pia'], a[href*='ticket']").first().attr("href") ?? null,
+ image_url: $el.find("img").first().attr("src")
+ ? absoluteUrl($el.find("img").first().attr("src")!, venue.url)
+ : null,
+ source_url: detailHref ? absoluteUrl(detailHref, venue.url) : null,
+ });
+ });
+
+ return events;
+ },
+};
+
+function parseJapaneseDate(raw: string): string | null {
+ const m =
+ raw.match(/(\d{4})[./年](\d{1,2})[./月](\d{1,2})/) ||
+ raw.match(/(\d{1,2})[./月](\d{1,2})/);
+ if (!m) return null;
+ if (m.length === 4) {
+ return `${m[1]}-${m[2].padStart(2, "0")}-${m[3].padStart(2, "0")}`;
+ }
+ const year = new Date().getFullYear();
+ return `${year}-${m[1].padStart(2, "0")}-${m[2].padStart(2, "0")}`;
+}
+
+function absoluteUrl(url: string, base: string): string {
+ if (url.startsWith("http")) return url;
+ return url.startsWith("/") ? base + url : `${base}/${url}`;
+}
diff --git a/app/scrapers/index.ts b/app/scrapers/index.ts
new file mode 100644
index 0000000..97d2586
--- /dev/null
+++ b/app/scrapers/index.ts
@@ -0,0 +1,20 @@
+/**
+ * Registry of all venue scrapers.
+ * To add a new venue: create a new file implementing Scraper, then add it here.
+ */
+import type { Scraper } from "./base";
+import { scraper as liquidRoom } from "./liquid-room";
+import { scraper as wwwShibuya } from "./www-shibuya";
+import { scraper as shibuyaO } from "./shibuya-o";
+import { scraper as shinjukuLoft } from "./shinjuku-loft";
+import { scraper as clubQuattro } from "./club-quattro";
+
+export const ALL_SCRAPERS: Scraper[] = [
+ liquidRoom,
+ wwwShibuya,
+ shibuyaO,
+ shinjukuLoft,
+ clubQuattro,
+];
+
+export type { Scraper } from "./base";
diff --git a/app/scrapers/liquid-room.ts b/app/scrapers/liquid-room.ts
new file mode 100644
index 0000000..b497759
--- /dev/null
+++ b/app/scrapers/liquid-room.ts
@@ -0,0 +1,87 @@
+/**
+ * Liquid Room (恵比寿) — https://www.liquidroom.net/schedule
+ *
+ * The schedule page lists events with JSON-LD or HTML data.
+ * Structure: <div class="p-schedule__item"> contains date, title, etc.
+ */
+import * as cheerio from "cheerio";
+import type { Scraper, VenueMeta } from "./base";
+import type { EventInput } from "~/lib/db.server";
+
+export const venue: VenueMeta = {
+ id: "liquid-room",
+ name: "LIQUID ROOM",
+ url: "https://www.liquidroom.net",
+ area: "恵比寿",
+};
+
+export const scraper: Scraper = {
+ venue,
+ async scrape(): Promise<EventInput[]> {
+ const res = await fetch("https://www.liquidroom.net/schedule");
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
+ const html = await res.text();
+ const $ = cheerio.load(html);
+ const events: EventInput[] = [];
+
+ $("article.p-schedule__item, .schedule-list__item, .c-event-item").each(
+ (_, el) => {
+ const $el = $(el);
+
+ const title =
+ $el.find(".p-schedule__title, .event-title, h3, h2").first().text().trim();
+ if (!title) return;
+
+ const dateStr =
+ $el.find(".p-schedule__date, .event-date, time").first().text().trim() ||
+ $el.find("time").attr("datetime") ||
+ "";
+ const date = parseJapaneseDate(dateStr);
+ if (!date) return;
+
+ const artist =
+ $el.find(".p-schedule__artist, .artist").first().text().trim() || null;
+ const startTime =
+ $el.find(".p-schedule__time, .open-time").first().text().trim().match(/\d{2}:\d{2}/)?.[0] ?? null;
+ const ticketUrl =
+ $el.find("a[href*='ticket'], a[href*='eplus'], a[href*='pia']").first().attr("href") ?? null;
+ const imageUrl =
+ $el.find("img").first().attr("src") ?? null;
+ const sourceUrl =
+ $el.find("a").first().attr("href") ?? null;
+
+ events.push({
+ venue_id: venue.id,
+ title,
+ artist,
+ date,
+ start_time: startTime,
+ ticket_url: ticketUrl,
+ image_url: imageUrl ? absoluteUrl(imageUrl, venue.url) : null,
+ source_url: sourceUrl ? absoluteUrl(sourceUrl, venue.url) : null,
+ });
+ }
+ );
+
+ return events;
+ },
+};
+
+function parseJapaneseDate(raw: string): string | null {
+ // Handles "2025.06.15" "2025/06/15" "2025年06月15日" "06.15" formats
+ const m =
+ raw.match(/(\d{4})[./年](\d{1,2})[./月](\d{1,2})/) ||
+ raw.match(/(\d{1,2})[./月](\d{1,2})/);
+ if (!m) return null;
+ if (m.length === 4) {
+ return `${m[1]}-${m[2].padStart(2, "0")}-${m[3].padStart(2, "0")}`;
+ }
+ const year = new Date().getFullYear();
+ return `${year}-${m[1].padStart(2, "0")}-${m[2].padStart(2, "0")}`;
+}
+
+function absoluteUrl(url: string, base: string): string {
+ if (url.startsWith("http")) return url;
+ if (url.startsWith("/")) return base + url;
+ return base + "/" + url;
+}
diff --git a/app/scrapers/shibuya-o.ts b/app/scrapers/shibuya-o.ts
new file mode 100644
index 0000000..1ad8d8c
--- /dev/null
+++ b/app/scrapers/shibuya-o.ts
@@ -0,0 +1,82 @@
+/**
+ * Shibuya O-East / O-West / O-Crest / O-Nest (渋谷)
+ * https://www.shibuya-o.com/schedule/
+ *
+ * The page uses a unified schedule listing for all O venues.
+ */
+import * as cheerio from "cheerio";
+import type { Scraper, VenueMeta } from "./base";
+import type { EventInput } from "~/lib/db.server";
+
+export const venue: VenueMeta = {
+ id: "shibuya-o",
+ name: "渋谷 O-EAST / O-WEST",
+ url: "https://www.shibuya-o.com",
+ area: "渋谷",
+};
+
+export const scraper: Scraper = {
+ venue,
+ async scrape(): Promise<EventInput[]> {
+ const res = await fetch("https://www.shibuya-o.com/schedule/");
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
+ const html = await res.text();
+ const $ = cheerio.load(html);
+ const events: EventInput[] = [];
+
+ $(".schedule_list li, .c-schedule__item, .event-item").each((_, el) => {
+ const $el = $(el);
+
+ const title = $el.find(".schedule_title, .title, h3").first().text().trim();
+ if (!title) return;
+
+ const rawDate =
+ $el.find(".schedule_date, .date, time").first().text().trim() ||
+ $el.find("time").attr("datetime") ||
+ "";
+ const date = parseJapaneseDate(rawDate);
+ if (!date) return;
+
+ const hall = $el.find(".schedule_hall, .hall, .venue-name").first().text().trim() || null;
+ const timeText = $el.find(".schedule_time, .time").first().text();
+ const openMatch = timeText.match(/OPEN[:: ]*(\d{2}:\d{2})/i);
+ const startMatch = timeText.match(/START[:: ]*(\d{2}:\d{2})/i);
+
+ const detailHref = $el.find("a[href]").first().attr("href") ?? null;
+
+ events.push({
+ venue_id: venue.id,
+ title,
+ artist: hall,
+ date,
+ open_time: openMatch?.[1] ?? null,
+ start_time: startMatch?.[1] ?? null,
+ ticket_url:
+ $el.find("a[href*='eplus'], a[href*='lawson'], a[href*='ticket']").first().attr("href") ?? null,
+ image_url: $el.find("img").first().attr("src")
+ ? absoluteUrl($el.find("img").first().attr("src")!, venue.url)
+ : null,
+ source_url: detailHref ? absoluteUrl(detailHref, venue.url) : null,
+ });
+ });
+
+ return events;
+ },
+};
+
+function parseJapaneseDate(raw: string): string | null {
+ const m =
+ raw.match(/(\d{4})[./年](\d{1,2})[./月](\d{1,2})/) ||
+ raw.match(/(\d{1,2})[./月](\d{1,2})/);
+ if (!m) return null;
+ if (m.length === 4) {
+ return `${m[1]}-${m[2].padStart(2, "0")}-${m[3].padStart(2, "0")}`;
+ }
+ const year = new Date().getFullYear();
+ return `${year}-${m[1].padStart(2, "0")}-${m[2].padStart(2, "0")}`;
+}
+
+function absoluteUrl(url: string, base: string): string {
+ if (url.startsWith("http")) return url;
+ return url.startsWith("/") ? base + url : `${base}/${url}`;
+}
diff --git a/app/scrapers/shinjuku-loft.ts b/app/scrapers/shinjuku-loft.ts
new file mode 100644
index 0000000..8a64761
--- /dev/null
+++ b/app/scrapers/shinjuku-loft.ts
@@ -0,0 +1,80 @@
+/**
+ * 新宿 LOFT — https://www.loft-prj.co.jp/schedule/loft
+ *
+ * The schedule page renders events inside `.eventlist` items.
+ */
+import * as cheerio from "cheerio";
+import type { Scraper, VenueMeta } from "./base";
+import type { EventInput } from "~/lib/db.server";
+
+export const venue: VenueMeta = {
+ id: "shinjuku-loft",
+ name: "新宿 LOFT",
+ url: "https://www.loft-prj.co.jp",
+ area: "新宿",
+};
+
+export const scraper: Scraper = {
+ venue,
+ async scrape(): Promise<EventInput[]> {
+ const res = await fetch("https://www.loft-prj.co.jp/schedule/loft");
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
+ const html = await res.text();
+ const $ = cheerio.load(html);
+ const events: EventInput[] = [];
+
+ $(".eventlist__item, .schedule-item, .event_list li").each((_, el) => {
+ const $el = $(el);
+
+ const title = $el.find(".eventlist__title, .event-title, h3, h2").first().text().trim();
+ if (!title) return;
+
+ const rawDate =
+ $el.find(".eventlist__date, .event-date, time").first().text().trim() ||
+ $el.find("time").attr("datetime") ||
+ "";
+ const date = parseJapaneseDate(rawDate);
+ if (!date) return;
+
+ const timeText = $el.find(".eventlist__time, .time").first().text();
+ const openMatch = timeText.match(/OPEN[:: ]*(\d{2}:\d{2})/i);
+ const startMatch = timeText.match(/START[:: ]*(\d{2}:\d{2})/i);
+
+ const detailHref = $el.find("a[href]").first().attr("href") ?? null;
+
+ events.push({
+ venue_id: venue.id,
+ title,
+ artist: $el.find(".eventlist__artist, .artist").first().text().trim() || null,
+ date,
+ open_time: openMatch?.[1] ?? null,
+ start_time: startMatch?.[1] ?? null,
+ ticket_url:
+ $el.find("a[href*='eplus'], a[href*='pia'], a[href*='ticket']").first().attr("href") ?? null,
+ image_url: $el.find("img").first().attr("src")
+ ? absoluteUrl($el.find("img").first().attr("src")!, venue.url)
+ : null,
+ source_url: detailHref ? absoluteUrl(detailHref, venue.url) : null,
+ });
+ });
+
+ return events;
+ },
+};
+
+function parseJapaneseDate(raw: string): string | null {
+ const m =
+ raw.match(/(\d{4})[./年](\d{1,2})[./月](\d{1,2})/) ||
+ raw.match(/(\d{1,2})[./月](\d{1,2})/);
+ if (!m) return null;
+ if (m.length === 4) {
+ return `${m[1]}-${m[2].padStart(2, "0")}-${m[3].padStart(2, "0")}`;
+ }
+ const year = new Date().getFullYear();
+ return `${year}-${m[1].padStart(2, "0")}-${m[2].padStart(2, "0")}`;
+}
+
+function absoluteUrl(url: string, base: string): string {
+ if (url.startsWith("http")) return url;
+ return url.startsWith("/") ? base + url : `${base}/${url}`;
+}
diff --git a/app/scrapers/www-shibuya.ts b/app/scrapers/www-shibuya.ts
new file mode 100644
index 0000000..905fc61
--- /dev/null
+++ b/app/scrapers/www-shibuya.ts
@@ -0,0 +1,79 @@
+/**
+ * WWW / WWW X (渋谷) — https://www-shibuya.jp/schedule/
+ */
+import * as cheerio from "cheerio";
+import type { Scraper, VenueMeta } from "./base";
+import type { EventInput } from "~/lib/db.server";
+
+export const venue: VenueMeta = {
+ id: "www-shibuya",
+ name: "WWW / WWW X",
+ url: "https://www-shibuya.jp",
+ area: "渋谷",
+};
+
+export const scraper: Scraper = {
+ venue,
+ async scrape(): Promise<EventInput[]> {
+ const res = await fetch("https://www-shibuya.jp/schedule/");
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
+ const html = await res.text();
+ const $ = cheerio.load(html);
+ const events: EventInput[] = [];
+
+ $(".schedule-list li, .p-schedule-item, article").each((_, el) => {
+ const $el = $(el);
+
+ const title = $el.find(".schedule-title, .title, h3, h2").first().text().trim();
+ if (!title) return;
+
+ const rawDate =
+ $el.find(".schedule-date, .date, time").first().text().trim() ||
+ $el.find("time").attr("datetime") ||
+ "";
+ const date = parseJapaneseDate(rawDate);
+ if (!date) return;
+
+ const timeText = $el.find(".schedule-time, .time").first().text();
+ const openMatch = timeText.match(/OPEN\s*(\d{2}:\d{2})/i);
+ const startMatch = timeText.match(/START\s*(\d{2}:\d{2})/i);
+
+ const detailHref = $el.find("a").first().attr("href") ?? null;
+
+ events.push({
+ venue_id: venue.id,
+ title,
+ artist: $el.find(".artist").first().text().trim() || null,
+ date,
+ open_time: openMatch?.[1] ?? null,
+ start_time: startMatch?.[1] ?? null,
+ ticket_url:
+ $el.find("a[href*='eplus'], a[href*='pia'], a[href*='ticket']").first().attr("href") ?? null,
+ image_url:
+ $el.find("img").first().attr("src")
+ ? absoluteUrl($el.find("img").first().attr("src")!, venue.url)
+ : null,
+ source_url: detailHref ? absoluteUrl(detailHref, venue.url) : null,
+ });
+ });
+
+ return events;
+ },
+};
+
+function parseJapaneseDate(raw: string): string | null {
+ const m =
+ raw.match(/(\d{4})[./年](\d{1,2})[./月](\d{1,2})/) ||
+ raw.match(/(\d{1,2})[./月](\d{1,2})/);
+ if (!m) return null;
+ if (m.length === 4) {
+ return `${m[1]}-${m[2].padStart(2, "0")}-${m[3].padStart(2, "0")}`;
+ }
+ const year = new Date().getFullYear();
+ return `${year}-${m[1].padStart(2, "0")}-${m[2].padStart(2, "0")}`;
+}
+
+function absoluteUrl(url: string, base: string): string {
+ if (url.startsWith("http")) return url;
+ return url.startsWith("/") ? base + url : `${base}/${url}`;
+}
diff --git a/package-lock.json b/package-lock.json
index f1e9498..daa9bae 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -6,22 +6,26 @@
"": {
"name": "tokyo-livehouse-events",
"dependencies": {
- "@react-router/node": "7.15.0",
- "@react-router/serve": "7.15.0",
+ "@react-router/node": "^7.3.0",
+ "@react-router/serve": "^7.3.0",
+ "better-sqlite3": "^12.9.0",
+ "cheerio": "^1.2.0",
"isbot": "^5.1.36",
"react": "^19.2.5",
"react-dom": "^19.2.5",
- "react-router": "7.15.0"
+ "react-router": "^7.3.0"
},
"devDependencies": {
- "@react-router/dev": "7.15.0",
- "@tailwindcss/vite": "^4.2.2",
+ "@react-router/dev": "^7.3.0",
+ "@tailwindcss/vite": "^4.2.4",
+ "@types/better-sqlite3": "^7.6.13",
"@types/node": "^22",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
- "tailwindcss": "^4.2.2",
+ "tailwindcss": "^4.2.4",
"typescript": "^5.9.3",
- "vite": "^8.0.3"
+ "vite": "^6.3.5",
+ "vite-tsconfig-paths": "^6.1.1"
}
},
"node_modules/@babel/code-frame": {
@@ -327,6 +331,21 @@
"node": ">=6.0.0"
}
},
+ "node_modules/@babel/plugin-syntax-decorators": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.28.6.tgz",
+ "integrity": "sha512-71EYI0ONURHJBL4rSFXnITXqXrrY8q4P0q006DPfN+Rk+ASM+++IBXem/ruokgBZR8YNEWZ8R6B+rCb8VcUTqA==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
"node_modules/@babel/plugin-syntax-jsx": {
"version": "7.28.6",
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.28.6.tgz",
@@ -903,6 +922,23 @@
"node": ">=18"
}
},
+ "node_modules/@isaacs/cliui": {
+ "version": "8.0.2",
+ "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
+ "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
+ "dev": true,
+ "dependencies": {
+ "string-width": "^5.1.2",
+ "string-width-cjs": "npm:string-width@^4.2.0",
+ "strip-ansi": "^7.0.1",
+ "strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
+ "wrap-ansi": "^8.1.0",
+ "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/@jridgewell/gen-mapping": {
"version": "0.3.13",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
@@ -971,49 +1007,107 @@
"@emnapi/runtime": "^1.7.1"
}
},
- "node_modules/@oxc-project/types": {
- "version": "0.127.0",
- "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.127.0.tgz",
- "integrity": "sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ==",
+ "node_modules/@npmcli/git": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-4.1.0.tgz",
+ "integrity": "sha512-9hwoB3gStVfa0N31ymBmrX+GuDGdVA/QWShZVqE0HK2Af+7QGGrCTbZia/SW0ImUTjTne7SP91qxDmtXvDHRPQ==",
"dev": true,
- "funding": {
- "url": "https://github.com/sponsors/Boshen"
+ "dependencies": {
+ "@npmcli/promise-spawn": "^6.0.0",
+ "lru-cache": "^7.4.4",
+ "npm-pick-manifest": "^8.0.0",
+ "proc-log": "^3.0.0",
+ "promise-inflight": "^1.0.1",
+ "promise-retry": "^2.0.1",
+ "semver": "^7.3.5",
+ "which": "^3.0.0"
+ },
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@npmcli/git/node_modules/lru-cache": {
+ "version": "7.18.3",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz",
+ "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@npmcli/package-json": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/@npmcli/package-json/-/package-json-4.0.1.tgz",
+ "integrity": "sha512-lRCEGdHZomFsURroh522YvA/2cVb9oPIJrjHanCJZkiasz1BzcnLr3tBJhlV7S86MBJBuAQ33is2D60YitZL2Q==",
+ "dev": true,
+ "dependencies": {
+ "@npmcli/git": "^4.1.0",
+ "glob": "^10.2.2",
+ "hosted-git-info": "^6.1.1",
+ "json-parse-even-better-errors": "^3.0.0",
+ "normalize-package-data": "^5.0.0",
+ "proc-log": "^3.0.0",
+ "semver": "^7.5.3"
+ },
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@npmcli/promise-spawn": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-6.0.2.tgz",
+ "integrity": "sha512-gGq0NJkIGSwdbUt4yhdF8ZrmkGKVz9vAdVzpOfnom+V8PLSmSOVhZwbNvZZS1EYcJN5hzzKBxmmVVAInM6HQLg==",
+ "dev": true,
+ "dependencies": {
+ "which": "^3.0.0"
+ },
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@pkgjs/parseargs": {
+ "version": "0.11.0",
+ "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
+ "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
+ "dev": true,
+ "optional": true,
+ "engines": {
+ "node": ">=14"
}
},
"node_modules/@react-router/dev": {
- "version": "7.15.0",
- "resolved": "https://registry.npmjs.org/@react-router/dev/-/dev-7.15.0.tgz",
- "integrity": "sha512-ZwUQu4KNZrViFqdeFWqh00Bk/QbLNvoWRDfjsqOp3oyuG3jSRLYnqRD3VAMK/FYMpL+s37ByT7XqqLXaF7Nw1g==",
+ "version": "7.3.0",
+ "resolved": "https://registry.npmjs.org/@react-router/dev/-/dev-7.3.0.tgz",
+ "integrity": "sha512-k9eWulu3FyJ2swN/RftgFAQviaRT4vMwE4COQ6WUCVQryQsru/ZK/dhw5YjZLPp1V+QieijigPxK4e7Pz4cOrg==",
"dev": true,
"dependencies": {
- "@babel/core": "^7.27.7",
- "@babel/generator": "^7.27.5",
- "@babel/parser": "^7.27.7",
- "@babel/plugin-syntax-jsx": "^7.27.1",
- "@babel/preset-typescript": "^7.27.1",
- "@babel/traverse": "^7.27.7",
- "@babel/types": "^7.27.7",
- "@react-router/node": "7.15.0",
- "@remix-run/node-fetch-server": "^0.13.0",
+ "@babel/core": "^7.21.8",
+ "@babel/generator": "^7.21.5",
+ "@babel/parser": "^7.21.8",
+ "@babel/plugin-syntax-decorators": "^7.22.10",
+ "@babel/plugin-syntax-jsx": "^7.21.4",
+ "@babel/preset-typescript": "^7.21.5",
+ "@babel/traverse": "^7.23.2",
+ "@babel/types": "^7.22.5",
+ "@npmcli/package-json": "^4.0.1",
+ "@react-router/node": "7.3.0",
"arg": "^5.0.1",
"babel-dead-code-elimination": "^1.0.6",
"chokidar": "^4.0.0",
"dedent": "^1.5.3",
"es-module-lexer": "^1.3.1",
"exit-hook": "2.2.1",
- "isbot": "^5.1.11",
+ "fs-extra": "^10.0.0",
"jsesc": "3.0.2",
"lodash": "^4.17.21",
- "p-map": "^7.0.3",
"pathe": "^1.1.2",
"picocolors": "^1.1.1",
- "pkg-types": "^2.3.0",
- "prettier": "^3.6.2",
+ "prettier": "^2.7.1",
"react-refresh": "^0.14.0",
"semver": "^7.3.7",
- "tinyglobby": "^0.2.14",
- "valibot": "^1.2.0",
- "vite-node": "^3.2.2"
+ "set-cookie-parser": "^2.6.0",
+ "valibot": "^0.41.0",
+ "vite-node": "3.0.0-beta.2"
},
"bin": {
"react-router": "bin.js"
@@ -1022,24 +1116,16 @@
"node": ">=20.0.0"
},
"peerDependencies": {
- "@react-router/serve": "^7.15.0",
- "@vitejs/plugin-rsc": "~0.5.21",
- "react-router": "^7.15.0",
- "react-server-dom-webpack": "^19.2.3",
- "typescript": "^5.1.0 || ^6.0.0",
- "vite": "^5.1.0 || ^6.0.0 || ^7.0.0 || ^8.0.0",
- "wrangler": "^3.28.2 || ^4.0.0"
+ "@react-router/serve": "^7.3.0",
+ "react-router": "^7.3.0",
+ "typescript": "^5.1.0",
+ "vite": "^5.1.0 || ^6.0.0",
+ "wrangler": "^3.28.2"
},
"peerDependenciesMeta": {
"@react-router/serve": {
"optional": true
},
- "@vitejs/plugin-rsc": {
- "optional": true
- },
- "react-server-dom-webpack": {
- "optional": true
- },
"typescript": {
"optional": true
},
@@ -1049,19 +1135,19 @@
}
},
"node_modules/@react-router/express": {
- "version": "7.15.0",
- "resolved": "https://registry.npmjs.org/@react-router/express/-/express-7.15.0.tgz",
- "integrity": "sha512-WldQrI5FxbsTLztOqx0+c7248m8IDUchCUUX177I+qz9OMuLR9ueIqz53zBKCVQ+Zf7k6FyatwpoLmr/Wovalg==",
+ "version": "7.3.0",
+ "resolved": "https://registry.npmjs.org/@react-router/express/-/express-7.3.0.tgz",
+ "integrity": "sha512-wAv020Wfwj6mW62NC8JQ6rlY1cJgeg+hRCFV2gcx5vxUfjH/tCFBRN4Rujo9WLFfRCwCEeilFqiuwBKeODDBsw==",
"dependencies": {
- "@react-router/node": "7.15.0"
+ "@react-router/node": "7.3.0"
},
"engines": {
"node": ">=20.0.0"
},
"peerDependencies": {
"express": "^4.17.1 || ^5",
- "react-router": "7.15.0",
- "typescript": "^5.1.0 || ^6.0.0"
+ "react-router": "7.3.0",
+ "typescript": "^5.1.0"
},
"peerDependenciesMeta": {
"typescript": {
@@ -1070,18 +1156,21 @@
}
},
"node_modules/@react-router/node": {
- "version": "7.15.0",
- "resolved": "https://registry.npmjs.org/@react-router/node/-/node-7.15.0.tgz",
- "integrity": "sha512-SgvWaWF1n3u+bpXXZUW9BSd2p/NwkIYLz4SSeDYqoX5RkYX5rcI4cHHuNJXszPu+Dm9QIri4J9g/4EV3KfgiXQ==",
+ "version": "7.3.0",
+ "resolved": "https://registry.npmjs.org/@react-router/node/-/node-7.3.0.tgz",
+ "integrity": "sha512-Vhww6DH0cVusO2yGhZuKmboGvFHuYOeIYEW0gpf0gFshbU0tR7MNAnOZS2Cud48hxVUSrEtgl0Kbs5BN+RQKJg==",
"dependencies": {
- "@mjackson/node-fetch-server": "^0.2.0"
+ "@mjackson/node-fetch-server": "^0.2.0",
+ "source-map-support": "^0.5.21",
+ "stream-slice": "^0.1.2",
+ "undici": "^6.19.2"
},
"engines": {
"node": ">=20.0.0"
},
"peerDependencies": {
- "react-router": "7.15.0",
- "typescript": "^5.1.0 || ^6.0.0"
+ "react-router": "7.3.0",
+ "typescript": "^5.1.0"
},
"peerDependenciesMeta": {
"typescript": {
@@ -1089,18 +1178,25 @@
}
}
},
+ "node_modules/@react-router/node/node_modules/undici": {
+ "version": "6.25.0",
+ "resolved": "https://registry.npmjs.org/undici/-/undici-6.25.0.tgz",
+ "integrity": "sha512-ZgpWDC5gmNiuY9CnLVXEH8rl50xhRCuLNA97fAUnKi8RRuV4E6KG31pDTsLVUKnohJE0I3XDrTeEydAXRw47xg==",
+ "engines": {
+ "node": ">=18.17"
+ }
+ },
"node_modules/@react-router/serve": {
- "version": "7.15.0",
- "resolved": "https://registry.npmjs.org/@react-router/serve/-/serve-7.15.0.tgz",
- "integrity": "sha512-0XtYmwc11vWdYn2zeEXx9E3u0I6TH3bm4uDaMdsyI09S6hl6uc98vBkTSXg7Znm3qR82R/jjtn3LvV2QEZ193w==",
+ "version": "7.3.0",
+ "resolved": "https://registry.npmjs.org/@react-router/serve/-/serve-7.3.0.tgz",
+ "integrity": "sha512-NGW8FRDghV6F04nki4dnL+sS8NHgbWkeQTtnGTIdTtPw2CaHOP7nsfJNJcetoSTw8xQjZWMDCbJfWYzSYeTF/g==",
"dependencies": {
- "@mjackson/node-fetch-server": "^0.2.0",
- "@react-router/express": "7.15.0",
- "@react-router/node": "7.15.0",
- "compression": "^1.8.1",
+ "@react-router/express": "7.3.0",
+ "@react-router/node": "7.3.0",
+ "compression": "^1.7.4",
"express": "^4.19.2",
"get-port": "5.1.1",
- "morgan": "^1.10.1",
+ "morgan": "^1.10.0",
"source-map-support": "^0.5.21"
},
"bin": {
@@ -1110,263 +1206,9 @@
"node": ">=20.0.0"
},
"peerDependencies": {
- "react-router": "7.15.0"
- }
- },
- "node_modules/@remix-run/node-fetch-server": {
- "version": "0.13.1",
- "resolved": "https://registry.npmjs.org/@remix-run/node-fetch-server/-/node-fetch-server-0.13.1.tgz",
- "integrity": "sha512-dOL+A/C84EA47gO/ps52KGrVSiYy96512rwtbXmJfWKYFm1FbrbjA3jao1hcIfao+jwVNEaZ1kTMwFjiino+HQ==",
- "dev": true
- },
- "node_modules/@rolldown/binding-android-arm64": {
- "version": "1.0.0-rc.17",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.17.tgz",
- "integrity": "sha512-s70pVGhw4zqGeFnXWvAzJDlvxhlRollagdCCKRgOsgUOH3N1l0LIxf83AtGzmb5SiVM4Hjl5HyarMRfdfj3DaQ==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/binding-darwin-arm64": {
- "version": "1.0.0-rc.17",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.17.tgz",
- "integrity": "sha512-4ksWc9n0mhlZpZ9PMZgTGjeOPRu8MB1Z3Tz0Mo02eWfWCHMW1zN82Qz/pL/rC+yQa+8ZnutMF0JjJe7PjwasYw==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/binding-darwin-x64": {
- "version": "1.0.0-rc.17",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.17.tgz",
- "integrity": "sha512-SUSDOI6WwUVNcWxd02QEBjLdY1VPHvlEkw6T/8nYG322iYWCTxRb1vzk4E+mWWYehTp7ERibq54LSJGjmouOsw==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/binding-freebsd-x64": {
- "version": "1.0.0-rc.17",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.17.tgz",
- "integrity": "sha512-hwnz3nw9dbJ05EDO/PvcjaaewqqDy7Y1rn1UO81l8iIK1GjenME75dl16ajbvSSMfv66WXSRCYKIqfgq2KCfxw==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "optional": true,
- "os": [
- "freebsd"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/binding-linux-arm-gnueabihf": {
- "version": "1.0.0-rc.17",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.17.tgz",
- "integrity": "sha512-IS+W7epTcwANmFSQFrS1SivEXHtl1JtuQA9wlxrZTcNi6mx+FDOYrakGevvvTwgj2JvWiK8B29/qD9BELZPyXQ==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/binding-linux-arm64-gnu": {
- "version": "1.0.0-rc.17",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.17.tgz",
- "integrity": "sha512-e6usGaHKW5BMNZOymS1UcEYGowQMWcgZ71Z17Sl/h2+ZziNJ1a9n3Zvcz6LdRyIW5572wBCTH/Z+bKuZouGk9Q==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/binding-linux-arm64-musl": {
- "version": "1.0.0-rc.17",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.17.tgz",
- "integrity": "sha512-b/CgbwAJpmrRLp02RPfhbudf5tZnN9nsPWK82znefso832etkem8H7FSZwxrOI9djcdTP7U6YfNhbRnh7djErg==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/binding-linux-ppc64-gnu": {
- "version": "1.0.0-rc.17",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.17.tgz",
- "integrity": "sha512-4EII1iNGRUN5WwGbF/kOh/EIkoDN9HsupgLQoXfY+D1oyJm7/F4t5PYU5n8SWZgG0FEwakyM8pGgwcBYruGTlA==",
- "cpu": [
- "ppc64"
- ],
- "dev": true,
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/binding-linux-s390x-gnu": {
- "version": "1.0.0-rc.17",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.17.tgz",
- "integrity": "sha512-AH8oq3XqQo4IibpVXvPeLDI5pzkpYn0WiZAfT05kFzoJ6tQNzwRdDYQ45M8I/gslbodRZwW8uxLhbSBbkv96rA==",
- "cpu": [
- "s390x"
- ],
- "dev": true,
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/binding-linux-x64-gnu": {
- "version": "1.0.0-rc.17",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.17.tgz",
- "integrity": "sha512-cLnjV3xfo7KslbU41Z7z8BH/E1y5mzUYzAqih1d1MDaIGZRCMqTijqLv76/P7fyHuvUcfGsIpqCdddbxLLK9rA==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/binding-linux-x64-musl": {
- "version": "1.0.0-rc.17",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.17.tgz",
- "integrity": "sha512-0phclDw1spsL7dUB37sIARuis2tAgomCJXAHZlpt8PXZ4Ba0dRP1e+66lsRqrfhISeN9bEGNjQs+T/Fbd7oYGw==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/binding-openharmony-arm64": {
- "version": "1.0.0-rc.17",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.17.tgz",
- "integrity": "sha512-0ag/hEgXOwgw4t8QyQvUCxvEg+V0KBcA6YuOx9g0r02MprutRF5dyljgm3EmR02O292UX7UeS6HzWHAl6KgyhA==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "optional": true,
- "os": [
- "openharmony"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
+ "react-router": "7.3.0"
}
},
- "node_modules/@rolldown/binding-wasm32-wasi": {
- "version": "1.0.0-rc.17",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.17.tgz",
- "integrity": "sha512-LEXei6vo0E5wTGwpkJ4KoT3OZJRnglwldt5ziLzOlc6qqb55z4tWNq2A+PFqCJuvWWdP53CVhG1Z9NtToDPJrA==",
- "cpu": [
- "wasm32"
- ],
- "dev": true,
- "optional": true,
- "dependencies": {
- "@emnapi/core": "1.10.0",
- "@emnapi/runtime": "1.10.0",
- "@napi-rs/wasm-runtime": "^1.1.4"
- },
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/binding-win32-arm64-msvc": {
- "version": "1.0.0-rc.17",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.17.tgz",
- "integrity": "sha512-gUmyzBl3SPMa6hrqFUth9sVfcLBlYsbMzBx5PlexMroZStgzGqlZ26pYG89rBb45Mnia+oil6YAIFeEWGWhoZA==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/binding-win32-x64-msvc": {
- "version": "1.0.0-rc.17",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.17.tgz",
- "integrity": "sha512-3hkiolcUAvPB9FLb3UZdfjVVNWherN1f/skkGWJP/fgSQhYUZpSIRr0/I8ZK9TkF3F7kxvJAk0+IcKvPHk9qQg==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/pluginutils": {
- "version": "1.0.0-rc.17",
- "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.17.tgz",
- "integrity": "sha512-n8iosDOt6Ig1UhJ2AYqoIhHWh/isz0xpicHTzpKBeotdVsTEcxsSA/i3EVM7gQAj0rU27OLAxCjzlj15IWY7bg==",
- "dev": true
- },
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.60.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.3.tgz",
@@ -1959,6 +1801,20 @@
"tslib": "^2.4.0"
}
},
+ "node_modules/@types/better-sqlite3": {
+ "version": "7.6.13",
+ "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz",
+ "integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==",
+ "dev": true,
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/cookie": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz",
+ "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA=="
+ },
"node_modules/@types/estree": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
@@ -2012,6 +1868,30 @@
"node": ">= 0.6"
}
},
+ "node_modules/ansi-regex": {
+ "version": "6.2.2",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
+ "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+ }
+ },
+ "node_modules/ansi-styles": {
+ "version": "6.2.3",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
+ "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
"node_modules/arg": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
@@ -2035,6 +1915,31 @@
"@babel/types": "^7.23.6"
}
},
+ "node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "dev": true
+ },
+ "node_modules/base64-js": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
+ "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ]
+ },
"node_modules/baseline-browser-mapping": {
"version": "2.10.27",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.27.tgz",
@@ -2063,6 +1968,37 @@
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
},
+ "node_modules/better-sqlite3": {
+ "version": "12.9.0",
+ "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.9.0.tgz",
+ "integrity": "sha512-wqUv4Gm3toFpHDQmaKD4QhZm3g1DjUBI0yzS4UBl6lElUmXFYdTQmmEDpAFa5o8FiFiymURypEnfVHzILKaxqQ==",
+ "hasInstallScript": true,
+ "dependencies": {
+ "bindings": "^1.5.0",
+ "prebuild-install": "^7.1.1"
+ },
+ "engines": {
+ "node": "20.x || 22.x || 23.x || 24.x || 25.x"
+ }
+ },
+ "node_modules/bindings": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
+ "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
+ "dependencies": {
+ "file-uri-to-path": "1.0.0"
+ }
+ },
+ "node_modules/bl": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
+ "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
+ "dependencies": {
+ "buffer": "^5.5.0",
+ "inherits": "^2.0.4",
+ "readable-stream": "^3.4.0"
+ }
+ },
"node_modules/body-parser": {
"version": "1.20.5",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.5.tgz",
@@ -2113,6 +2049,20 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/boolbase": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
+ "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="
+ },
+ "node_modules/brace-expansion": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz",
+ "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==",
+ "dev": true,
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
"node_modules/browserslist": {
"version": "4.28.2",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz",
@@ -2146,6 +2096,29 @@
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
}
},
+ "node_modules/buffer": {
+ "version": "5.7.1",
+ "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
+ "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "dependencies": {
+ "base64-js": "^1.3.1",
+ "ieee754": "^1.1.13"
+ }
+ },
"node_modules/buffer-from": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
@@ -2215,6 +2188,46 @@
}
]
},
+ "node_modules/cheerio": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.2.0.tgz",
+ "integrity": "sha512-WDrybc/gKFpTYQutKIK6UvfcuxijIZfMfXaYm8NMsPQxSYvf+13fXUJ4rztGGbJcBQ/GF55gvrZ0Bc0bj/mqvg==",
+ "dependencies": {
+ "cheerio-select": "^2.1.0",
+ "dom-serializer": "^2.0.0",
+ "domhandler": "^5.0.3",
+ "domutils": "^3.2.2",
+ "encoding-sniffer": "^0.2.1",
+ "htmlparser2": "^10.1.0",
+ "parse5": "^7.3.0",
+ "parse5-htmlparser2-tree-adapter": "^7.1.0",
+ "parse5-parser-stream": "^7.1.2",
+ "undici": "^7.19.0",
+ "whatwg-mimetype": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=20.18.1"
+ },
+ "funding": {
+ "url": "https://github.com/cheeriojs/cheerio?sponsor=1"
+ }
+ },
+ "node_modules/cheerio-select": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz",
+ "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==",
+ "dependencies": {
+ "boolbase": "^1.0.0",
+ "css-select": "^5.1.0",
+ "css-what": "^6.1.0",
+ "domelementtype": "^2.3.0",
+ "domhandler": "^5.0.3",
+ "domutils": "^3.0.1"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/fb55"
+ }
+ },
"node_modules/chokidar": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
@@ -2230,6 +2243,29 @@
"url": "https://paulmillr.com/funding/"
}
},
+ "node_modules/chownr": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
+ "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="
+ },
+ "node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true
+ },
"node_modules/compressible": {
"version": "2.0.18",
"resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz",
@@ -2271,12 +2307,6 @@
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
},
- "node_modules/confbox": {
- "version": "0.2.4",
- "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz",
- "integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==",
- "dev": true
- },
"node_modules/content-disposition": {
"version": "0.5.4",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
@@ -2315,6 +2345,61 @@
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz",
"integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA=="
},
+ "node_modules/cross-spawn": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+ "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
+ "dev": true,
+ "dependencies": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/cross-spawn/node_modules/which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "dev": true,
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/node-which"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/css-select": {
+ "version": "5.2.2",
+ "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz",
+ "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==",
+ "dependencies": {
+ "boolbase": "^1.0.0",
+ "css-what": "^6.1.0",
+ "domhandler": "^5.0.2",
+ "domutils": "^3.0.1",
+ "nth-check": "^2.0.1"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/fb55"
+ }
+ },
+ "node_modules/css-what": {
+ "version": "6.2.2",
+ "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz",
+ "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==",
+ "engines": {
+ "node": ">= 6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/fb55"
+ }
+ },
"node_modules/csstype": {
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
@@ -2338,6 +2423,20 @@
}
}
},
+ "node_modules/decompress-response": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
+ "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
+ "dependencies": {
+ "mimic-response": "^3.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/dedent": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.2.tgz",
@@ -2352,6 +2451,14 @@
}
}
},
+ "node_modules/deep-extend": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
+ "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
+ "engines": {
+ "node": ">=4.0.0"
+ }
+ },
"node_modules/depd": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
@@ -2373,11 +2480,61 @@
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
- "dev": true,
"engines": {
"node": ">=8"
}
},
+ "node_modules/dom-serializer": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
+ "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
+ "dependencies": {
+ "domelementtype": "^2.3.0",
+ "domhandler": "^5.0.2",
+ "entities": "^4.2.0"
+ },
+ "funding": {
+ "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
+ }
+ },
+ "node_modules/domelementtype": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
+ "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fb55"
+ }
+ ]
+ },
+ "node_modules/domhandler": {
+ "version": "5.0.3",
+ "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
+ "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
+ "dependencies": {
+ "domelementtype": "^2.3.0"
+ },
+ "engines": {
+ "node": ">= 4"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/domhandler?sponsor=1"
+ }
+ },
+ "node_modules/domutils": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz",
+ "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==",
+ "dependencies": {
+ "dom-serializer": "^2.0.0",
+ "domelementtype": "^2.3.0",
+ "domhandler": "^5.0.3"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/domutils?sponsor=1"
+ }
+ },
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
@@ -2391,6 +2548,12 @@
"node": ">= 0.4"
}
},
+ "node_modules/eastasianwidth": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
+ "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
+ "dev": true
+ },
"node_modules/ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
@@ -2402,6 +2565,12 @@
"integrity": "sha512-9D7Iqx8RImSvCnOsj86rCH6eQjZFQoM04Jn6HnZVM0Nu/G58/gmKYQ1d12MZTbjQbQSTGI8nwEy07ErsA2slLA==",
"dev": true
},
+ "node_modules/emoji-regex": {
+ "version": "9.2.2",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
+ "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
+ "dev": true
+ },
"node_modules/encodeurl": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
@@ -2410,6 +2579,37 @@
"node": ">= 0.8"
}
},
+ "node_modules/encoding-sniffer": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.1.tgz",
+ "integrity": "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==",
+ "dependencies": {
+ "iconv-lite": "^0.6.3",
+ "whatwg-encoding": "^3.1.1"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/encoding-sniffer?sponsor=1"
+ }
+ },
+ "node_modules/encoding-sniffer/node_modules/iconv-lite": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
+ "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/end-of-stream": {
+ "version": "1.4.5",
+ "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
+ "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
+ "dependencies": {
+ "once": "^1.4.0"
+ }
+ },
"node_modules/enhanced-resolve": {
"version": "5.21.0",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.0.tgz",
@@ -2423,6 +2623,23 @@
"node": ">=10.13.0"
}
},
+ "node_modules/entities": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
+ "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
+ "node_modules/err-code": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz",
+ "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==",
+ "dev": true
+ },
"node_modules/es-define-property": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
@@ -2531,6 +2748,14 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/expand-template": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz",
+ "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/express": {
"version": "4.22.1",
"resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz",
@@ -2589,12 +2814,6 @@
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
},
- "node_modules/exsolve": {
- "version": "1.0.8",
- "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz",
- "integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==",
- "dev": true
- },
"node_modules/fdir": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
@@ -2612,6 +2831,11 @@
}
}
},
+ "node_modules/file-uri-to-path": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
+ "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw=="
+ },
"node_modules/finalhandler": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz",
@@ -2642,6 +2866,22 @@
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
},
+ "node_modules/foreground-child": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
+ "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==",
+ "dev": true,
+ "dependencies": {
+ "cross-spawn": "^7.0.6",
+ "signal-exit": "^4.0.1"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
"node_modules/forwarded": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
@@ -2658,6 +2898,25 @@
"node": ">= 0.6"
}
},
+ "node_modules/fs-constants": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
+ "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="
+ },
+ "node_modules/fs-extra": {
+ "version": "10.1.0",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
+ "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==",
+ "dev": true,
+ "dependencies": {
+ "graceful-fs": "^4.2.0",
+ "jsonfile": "^6.0.1",
+ "universalify": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@@ -2735,6 +2994,39 @@
"node": ">= 0.4"
}
},
+ "node_modules/github-from-package": {
+ "version": "0.0.0",
+ "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz",
+ "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw=="
+ },
+ "node_modules/glob": {
+ "version": "10.5.0",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
+ "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==",
+ "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me",
+ "dev": true,
+ "dependencies": {
+ "foreground-child": "^3.1.0",
+ "jackspeak": "^3.1.2",
+ "minimatch": "^9.0.4",
+ "minipass": "^7.1.2",
+ "package-json-from-dist": "^1.0.0",
+ "path-scurry": "^1.11.1"
+ },
+ "bin": {
+ "glob": "dist/esm/bin.mjs"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/globrex": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz",
+ "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/gopd": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
@@ -2774,6 +3066,56 @@
"node": ">= 0.4"
}
},
+ "node_modules/hosted-git-info": {
+ "version": "6.1.3",
+ "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-6.1.3.tgz",
+ "integrity": "sha512-HVJyzUrLIL1c0QmviVh5E8VGyUS7xCFPS6yydaVd1UegW+ibV/CohqTH9MkOLDp5o+rb82DMo77PTuc9F/8GKw==",
+ "dev": true,
+ "dependencies": {
+ "lru-cache": "^7.5.1"
+ },
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/hosted-git-info/node_modules/lru-cache": {
+ "version": "7.18.3",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz",
+ "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/htmlparser2": {
+ "version": "10.1.0",
+ "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz",
+ "integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==",
+ "funding": [
+ "https://github.com/fb55/htmlparser2?sponsor=1",
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fb55"
+ }
+ ],
+ "dependencies": {
+ "domelementtype": "^2.3.0",
+ "domhandler": "^5.0.3",
+ "domutils": "^3.2.2",
+ "entities": "^7.0.1"
+ }
+ },
+ "node_modules/htmlparser2/node_modules/entities": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz",
+ "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==",
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
"node_modules/http-errors": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
@@ -2804,11 +3146,35 @@
"node": ">=0.10.0"
}
},
+ "node_modules/ieee754": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
+ "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ]
+ },
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
},
+ "node_modules/ini": {
+ "version": "1.3.8",
+ "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
+ "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="
+ },
"node_modules/ipaddr.js": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
@@ -2817,6 +3183,30 @@
"node": ">= 0.10"
}
},
+ "node_modules/is-core-module": {
+ "version": "2.16.2",
+ "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz",
+ "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==",
+ "dev": true,
+ "dependencies": {
+ "hasown": "^2.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/isbot": {
"version": "5.1.40",
"resolved": "https://registry.npmjs.org/isbot/-/isbot-5.1.40.tgz",
@@ -2825,6 +3215,27 @@
"node": ">=18"
}
},
+ "node_modules/isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+ "dev": true
+ },
+ "node_modules/jackspeak": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
+ "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==",
+ "dev": true,
+ "dependencies": {
+ "@isaacs/cliui": "^8.0.2"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ },
+ "optionalDependencies": {
+ "@pkgjs/parseargs": "^0.11.0"
+ }
+ },
"node_modules/jiti": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz",
@@ -2852,6 +3263,15 @@
"node": ">=6"
}
},
+ "node_modules/json-parse-even-better-errors": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-3.0.2.tgz",
+ "integrity": "sha512-fi0NG4bPjCHunUJffmLd0gxssIgkNmArMvis4iNah6Owg1MCJjWhEcDLmsK6iGkJq3tHwbDkTlce70/tmXN4cQ==",
+ "dev": true,
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
"node_modules/json5": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
@@ -2864,6 +3284,18 @@
"node": ">=6"
}
},
+ "node_modules/jsonfile": {
+ "version": "6.2.1",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz",
+ "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==",
+ "dev": true,
+ "dependencies": {
+ "universalify": "^2.0.0"
+ },
+ "optionalDependencies": {
+ "graceful-fs": "^4.1.6"
+ }
+ },
"node_modules/lightningcss": {
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
@@ -3207,6 +3639,54 @@
"node": ">= 0.6"
}
},
+ "node_modules/mimic-response": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
+ "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/minimatch": {
+ "version": "9.0.9",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz",
+ "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==",
+ "dev": true,
+ "dependencies": {
+ "brace-expansion": "^2.0.2"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/minimist": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
+ "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/minipass": {
+ "version": "7.1.3",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz",
+ "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==",
+ "dev": true,
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ }
+ },
+ "node_modules/mkdirp-classic": {
+ "version": "0.5.3",
+ "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
+ "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="
+ },
"node_modules/morgan": {
"version": "1.10.1",
"resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.1.tgz",
@@ -3269,6 +3749,11 @@
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
+ "node_modules/napi-build-utils": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz",
+ "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA=="
+ },
"node_modules/negotiator": {
"version": "0.6.4",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz",
@@ -3277,12 +3762,100 @@
"node": ">= 0.6"
}
},
+ "node_modules/node-abi": {
+ "version": "3.91.0",
+ "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.91.0.tgz",
+ "integrity": "sha512-B+S7X/GS3Un6wMICtnsNjQD7oSpVBQrZftHE6GZ1Fe9/k3XOOoqbM5DZZ0GO4x3YiSCQfrM28yj1ppplwgIsfg==",
+ "dependencies": {
+ "semver": "^7.3.5"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/node-releases": {
"version": "2.0.38",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.38.tgz",
"integrity": "sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==",
"dev": true
},
+ "node_modules/normalize-package-data": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-5.0.0.tgz",
+ "integrity": "sha512-h9iPVIfrVZ9wVYQnxFgtw1ugSvGEMOlyPWWtm8BMJhnwyEL/FLbYbTY3V3PpjI/BUK67n9PEWDu6eHzu1fB15Q==",
+ "dev": true,
+ "dependencies": {
+ "hosted-git-info": "^6.0.0",
+ "is-core-module": "^2.8.1",
+ "semver": "^7.3.5",
+ "validate-npm-package-license": "^3.0.4"
+ },
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/npm-install-checks": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/npm-install-checks/-/npm-install-checks-6.3.0.tgz",
+ "integrity": "sha512-W29RiK/xtpCGqn6f3ixfRYGk+zRyr+Ew9F2E20BfXxT5/euLdA/Nm7fO7OeTGuAmTs30cpgInyJ0cYe708YTZw==",
+ "dev": true,
+ "dependencies": {
+ "semver": "^7.1.1"
+ },
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/npm-normalize-package-bin": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-3.0.1.tgz",
+ "integrity": "sha512-dMxCf+zZ+3zeQZXKxmyuCKlIDPGuv8EF940xbkC4kQVDTtqoh6rJFO+JTKSA6/Rwi0getWmtuy4Itup0AMcaDQ==",
+ "dev": true,
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/npm-package-arg": {
+ "version": "10.1.0",
+ "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-10.1.0.tgz",
+ "integrity": "sha512-uFyyCEmgBfZTtrKk/5xDfHp6+MdrqGotX/VoOyEEl3mBwiEE5FlBaePanazJSVMPT7vKepcjYBY2ztg9A3yPIA==",
+ "dev": true,
+ "dependencies": {
+ "hosted-git-info": "^6.0.0",
+ "proc-log": "^3.0.0",
+ "semver": "^7.3.5",
+ "validate-npm-package-name": "^5.0.0"
+ },
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/npm-pick-manifest": {
+ "version": "8.0.2",
+ "resolved": "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-8.0.2.tgz",
+ "integrity": "sha512-1dKY+86/AIiq1tkKVD3l0WI+Gd3vkknVGAggsFeBkTvbhMQ1OND/LKkYv4JtXPKUJ8bOTCyLiqEg2P6QNdK+Gg==",
+ "dev": true,
+ "dependencies": {
+ "npm-install-checks": "^6.0.0",
+ "npm-normalize-package-bin": "^3.0.0",
+ "npm-package-arg": "^10.0.0",
+ "semver": "^7.3.5"
+ },
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/nth-check": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
+ "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==",
+ "dependencies": {
+ "boolbase": "^1.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/nth-check?sponsor=1"
+ }
+ },
"node_modules/object-inspect": {
"version": "1.13.4",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
@@ -3313,16 +3886,63 @@
"node": ">= 0.8"
}
},
- "node_modules/p-map": {
- "version": "7.0.4",
- "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.4.tgz",
- "integrity": "sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ==",
- "dev": true,
+ "node_modules/once": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+ "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+ "dependencies": {
+ "wrappy": "1"
+ }
+ },
+ "node_modules/package-json-from-dist": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
+ "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
+ "dev": true
+ },
+ "node_modules/parse5": {
+ "version": "7.3.0",
+ "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
+ "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==",
+ "dependencies": {
+ "entities": "^6.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/inikulin/parse5?sponsor=1"
+ }
+ },
+ "node_modules/parse5-htmlparser2-tree-adapter": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz",
+ "integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==",
+ "dependencies": {
+ "domhandler": "^5.0.3",
+ "parse5": "^7.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/inikulin/parse5?sponsor=1"
+ }
+ },
+ "node_modules/parse5-parser-stream": {
+ "version": "7.1.2",
+ "resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz",
+ "integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==",
+ "dependencies": {
+ "parse5": "^7.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/inikulin/parse5?sponsor=1"
+ }
+ },
+ "node_modules/parse5/node_modules/entities": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
+ "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",
"engines": {
- "node": ">=18"
+ "node": ">=0.12"
},
"funding": {
- "url": "https://github.com/sponsors/sindresorhus"
+ "url": "https://github.com/fb55/entities?sponsor=1"
}
},
"node_modules/parseurl": {
@@ -3333,6 +3953,37 @@
"node": ">= 0.8"
}
},
+ "node_modules/path-key": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-scurry": {
+ "version": "1.11.1",
+ "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
+ "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
+ "dev": true,
+ "dependencies": {
+ "lru-cache": "^10.2.0",
+ "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/path-scurry/node_modules/lru-cache": {
+ "version": "10.4.3",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
+ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
+ "dev": true
+ },
"node_modules/path-to-regexp": {
"version": "0.1.13",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz",
@@ -3362,23 +4013,6 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
- "node_modules/pkg-types": {
- "version": "2.3.1",
- "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.1.tgz",
- "integrity": "sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg==",
- "dev": true,
- "dependencies": {
- "confbox": "^0.2.4",
- "exsolve": "^1.0.8",
- "pathe": "^2.0.3"
- }
- },
- "node_modules/pkg-types/node_modules/pathe": {
- "version": "2.0.3",
- "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
- "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
- "dev": true
- },
"node_modules/postcss": {
"version": "8.5.14",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz",
@@ -3407,21 +4041,75 @@
"node": "^10 || ^12 || >=14"
}
},
+ "node_modules/prebuild-install": {
+ "version": "7.1.3",
+ "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz",
+ "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==",
+ "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.",
+ "dependencies": {
+ "detect-libc": "^2.0.0",
+ "expand-template": "^2.0.3",
+ "github-from-package": "0.0.0",
+ "minimist": "^1.2.3",
+ "mkdirp-classic": "^0.5.3",
+ "napi-build-utils": "^2.0.0",
+ "node-abi": "^3.3.0",
+ "pump": "^3.0.0",
+ "rc": "^1.2.7",
+ "simple-get": "^4.0.0",
+ "tar-fs": "^2.0.0",
+ "tunnel-agent": "^0.6.0"
+ },
+ "bin": {
+ "prebuild-install": "bin.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/prettier": {
- "version": "3.8.3",
- "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.3.tgz",
- "integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==",
+ "version": "2.8.8",
+ "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz",
+ "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==",
"dev": true,
"bin": {
- "prettier": "bin/prettier.cjs"
+ "prettier": "bin-prettier.js"
},
"engines": {
- "node": ">=14"
+ "node": ">=10.13.0"
},
"funding": {
"url": "https://github.com/prettier/prettier?sponsor=1"
}
},
+ "node_modules/proc-log": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-3.0.0.tgz",
+ "integrity": "sha512-++Vn7NS4Xf9NacaU9Xq3URUuqZETPsf8L4j5/ckhaRYsfPeRyzGw+iDjFhV/Jr3uNmTvvddEJFWh5R1gRgUH8A==",
+ "dev": true,
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/promise-inflight": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz",
+ "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==",
+ "dev": true
+ },
+ "node_modules/promise-retry": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz",
+ "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==",
+ "dev": true,
+ "dependencies": {
+ "err-code": "^2.0.2",
+ "retry": "^0.12.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/proxy-addr": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
@@ -3434,6 +4122,15 @@
"node": ">= 0.10"
}
},
+ "node_modules/pump": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz",
+ "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==",
+ "dependencies": {
+ "end-of-stream": "^1.1.0",
+ "once": "^1.3.1"
+ }
+ },
"node_modules/qs": {
"version": "6.14.2",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz",
@@ -3470,6 +4167,20 @@
"node": ">= 0.8"
}
},
+ "node_modules/rc": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
+ "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
+ "dependencies": {
+ "deep-extend": "^0.6.0",
+ "ini": "~1.3.0",
+ "minimist": "^1.2.0",
+ "strip-json-comments": "~2.0.1"
+ },
+ "bin": {
+ "rc": "cli.js"
+ }
+ },
"node_modules/react": {
"version": "19.2.5",
"resolved": "https://registry.npmjs.org/react/-/react-19.2.5.tgz",
@@ -3499,12 +4210,14 @@
}
},
"node_modules/react-router": {
- "version": "7.15.0",
- "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.15.0.tgz",
- "integrity": "sha512-HW9vYwuM8f4yx66Izy8xfrzCM+SBJluoZcCbww9A1TySax11S5Vgw6fi3ZjMONw9J4gQwngL7PzkyIpJJpJ7RQ==",
+ "version": "7.3.0",
+ "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.3.0.tgz",
+ "integrity": "sha512-466f2W7HIWaNXTKM5nHTqNxLrHTyXybm7R0eBlVSt0k/u55tTCDO194OIx/NrYD4TS5SXKTNekXfT37kMKUjgw==",
"dependencies": {
+ "@types/cookie": "^0.6.0",
"cookie": "^1.0.1",
- "set-cookie-parser": "^2.6.0"
+ "set-cookie-parser": "^2.6.0",
+ "turbo-stream": "2.4.0"
},
"engines": {
"node": ">=20.0.0"
@@ -3531,6 +4244,19 @@
"url": "https://opencollective.com/express"
}
},
+ "node_modules/readable-stream": {
+ "version": "3.6.2",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
+ "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
+ "dependencies": {
+ "inherits": "^2.0.3",
+ "string_decoder": "^1.1.1",
+ "util-deprecate": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
"node_modules/readdirp": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
@@ -3544,37 +4270,13 @@
"url": "https://paulmillr.com/funding/"
}
},
- "node_modules/rolldown": {
- "version": "1.0.0-rc.17",
- "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.17.tgz",
- "integrity": "sha512-ZrT53oAKrtA4+YtBWPQbtPOxIbVDbxT0orcYERKd63VJTF13zPcgXTvD4843L8pcsI7M6MErt8QtON6lrB9tyA==",
+ "node_modules/retry": {
+ "version": "0.12.0",
+ "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz",
+ "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==",
"dev": true,
- "dependencies": {
- "@oxc-project/types": "=0.127.0",
- "@rolldown/pluginutils": "1.0.0-rc.17"
- },
- "bin": {
- "rolldown": "bin/cli.mjs"
- },
"engines": {
- "node": "^20.19.0 || >=22.12.0"
- },
- "optionalDependencies": {
- "@rolldown/binding-android-arm64": "1.0.0-rc.17",
- "@rolldown/binding-darwin-arm64": "1.0.0-rc.17",
- "@rolldown/binding-darwin-x64": "1.0.0-rc.17",
- "@rolldown/binding-freebsd-x64": "1.0.0-rc.17",
- "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.17",
- "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.17",
- "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.17",
- "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.17",
- "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.17",
- "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.17",
- "@rolldown/binding-linux-x64-musl": "1.0.0-rc.17",
- "@rolldown/binding-openharmony-arm64": "1.0.0-rc.17",
- "@rolldown/binding-wasm32-wasi": "1.0.0-rc.17",
- "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.17",
- "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.17"
+ "node": ">= 4"
}
},
"node_modules/rollup": {
@@ -3654,7 +4356,6 @@
"version": "7.7.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
- "dev": true,
"bin": {
"semver": "bin/semver.js"
},
@@ -3722,6 +4423,27 @@
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="
},
+ "node_modules/shebang-command": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+ "dev": true,
+ "dependencies": {
+ "shebang-regex": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shebang-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/side-channel": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
@@ -3790,6 +4512,61 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/signal-exit": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
+ "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
+ "dev": true,
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/simple-concat": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz",
+ "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ]
+ },
+ "node_modules/simple-get": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz",
+ "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "dependencies": {
+ "decompress-response": "^6.0.0",
+ "once": "^1.3.1",
+ "simple-concat": "^1.0.0"
+ }
+ },
"node_modules/source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
@@ -3816,6 +4593,38 @@
"source-map": "^0.6.0"
}
},
+ "node_modules/spdx-correct": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz",
+ "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==",
+ "dev": true,
+ "dependencies": {
+ "spdx-expression-parse": "^3.0.0",
+ "spdx-license-ids": "^3.0.0"
+ }
+ },
+ "node_modules/spdx-exceptions": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz",
+ "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==",
+ "dev": true
+ },
+ "node_modules/spdx-expression-parse": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz",
+ "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==",
+ "dev": true,
+ "dependencies": {
+ "spdx-exceptions": "^2.1.0",
+ "spdx-license-ids": "^3.0.0"
+ }
+ },
+ "node_modules/spdx-license-ids": {
+ "version": "3.0.23",
+ "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.23.tgz",
+ "integrity": "sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw==",
+ "dev": true
+ },
"node_modules/statuses": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
@@ -3824,6 +4633,123 @@
"node": ">= 0.8"
}
},
+ "node_modules/stream-slice": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/stream-slice/-/stream-slice-0.1.2.tgz",
+ "integrity": "sha512-QzQxpoacatkreL6jsxnVb7X5R/pGw9OUv2qWTYWnmLpg4NdN31snPy/f3TdQE1ZUXaThRvj1Zw4/OGg0ZkaLMA=="
+ },
+ "node_modules/string_decoder": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
+ "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
+ "dependencies": {
+ "safe-buffer": "~5.2.0"
+ }
+ },
+ "node_modules/string-width": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
+ "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
+ "dev": true,
+ "dependencies": {
+ "eastasianwidth": "^0.2.0",
+ "emoji-regex": "^9.2.2",
+ "strip-ansi": "^7.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/string-width-cjs": {
+ "name": "string-width",
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dev": true,
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/string-width-cjs/node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/string-width-cjs/node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "dev": true
+ },
+ "node_modules/string-width-cjs/node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-ansi": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz",
+ "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==",
+ "dev": true,
+ "dependencies": {
+ "ansi-regex": "^6.2.2"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+ }
+ },
+ "node_modules/strip-ansi-cjs": {
+ "name": "strip-ansi",
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-ansi-cjs/node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-json-comments": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
+ "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/tailwindcss": {
"version": "4.2.4",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.4.tgz",
@@ -3843,6 +4769,32 @@
"url": "https://opencollective.com/webpack"
}
},
+ "node_modules/tar-fs": {
+ "version": "2.1.4",
+ "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz",
+ "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==",
+ "dependencies": {
+ "chownr": "^1.1.1",
+ "mkdirp-classic": "^0.5.2",
+ "pump": "^3.0.0",
+ "tar-stream": "^2.1.4"
+ }
+ },
+ "node_modules/tar-stream": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
+ "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
+ "dependencies": {
+ "bl": "^4.0.3",
+ "end-of-stream": "^1.4.1",
+ "fs-constants": "^1.0.0",
+ "inherits": "^2.0.3",
+ "readable-stream": "^3.1.1"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/tinyglobby": {
"version": "0.2.16",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz",
@@ -3867,6 +4819,27 @@
"node": ">=0.6"
}
},
+ "node_modules/tsconfck": {
+ "version": "3.1.6",
+ "resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-3.1.6.tgz",
+ "integrity": "sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "tsconfck": "bin/tsconfck.js"
+ },
+ "engines": {
+ "node": "^18 || >=20"
+ },
+ "peerDependencies": {
+ "typescript": "^5.0.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
@@ -3874,6 +4847,22 @@
"dev": true,
"optional": true
},
+ "node_modules/tunnel-agent": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
+ "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==",
+ "dependencies": {
+ "safe-buffer": "^5.0.1"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/turbo-stream": {
+ "version": "2.4.0",
+ "resolved": "https://registry.npmjs.org/turbo-stream/-/turbo-stream-2.4.0.tgz",
+ "integrity": "sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g=="
+ },
"node_modules/type-is": {
"version": "1.6.18",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
@@ -3899,12 +4888,29 @@
"node": ">=14.17"
}
},
+ "node_modules/undici": {
+ "version": "7.25.0",
+ "resolved": "https://registry.npmjs.org/undici/-/undici-7.25.0.tgz",
+ "integrity": "sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ==",
+ "engines": {
+ "node": ">=20.18.1"
+ }
+ },
"node_modules/undici-types": {
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"dev": true
},
+ "node_modules/universalify": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
+ "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
+ "dev": true,
+ "engines": {
+ "node": ">= 10.0.0"
+ }
+ },
"node_modules/unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
@@ -3943,6 +4949,11 @@
"browserslist": ">= 4.21.0"
}
},
+ "node_modules/util-deprecate": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
+ },
"node_modules/utils-merge": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
@@ -3952,9 +4963,9 @@
}
},
"node_modules/valibot": {
- "version": "1.4.0",
- "resolved": "https://registry.npmjs.org/valibot/-/valibot-1.4.0.tgz",
- "integrity": "sha512-iC/x7fVcSyOwlm/VSt7RlHnzNGLGvR9GnxdifUeWoCJo0q4ZZvrVkIHC6faTlkxG47I2Y4UrFquPuVHCrOnrLg==",
+ "version": "0.41.0",
+ "resolved": "https://registry.npmjs.org/valibot/-/valibot-0.41.0.tgz",
+ "integrity": "sha512-igDBb8CTYr8YTQlOKgaN9nSS0Be7z+WRuaeYqGf3Cjz3aKmSnqEmYnkfVjzIuumGqfHpa3fLIvMEAfhrpqN8ng==",
"dev": true,
"peerDependencies": {
"typescript": ">=5"
@@ -3965,6 +4976,25 @@
}
}
},
+ "node_modules/validate-npm-package-license": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz",
+ "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==",
+ "dev": true,
+ "dependencies": {
+ "spdx-correct": "^3.0.0",
+ "spdx-expression-parse": "^3.0.0"
+ }
+ },
+ "node_modules/validate-npm-package-name": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-5.0.1.tgz",
+ "integrity": "sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ==",
+ "dev": true,
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
"node_modules/vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
@@ -3974,22 +5004,23 @@
}
},
"node_modules/vite": {
- "version": "8.0.10",
- "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.10.tgz",
- "integrity": "sha512-rZuUu9j6J5uotLDs+cAA4O5H4K1SfPliUlQwqa6YEwSrWDZzP4rhm00oJR5snMewjxF5V/K3D4kctsUTsIU9Mw==",
+ "version": "6.3.5",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz",
+ "integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==",
"dev": true,
"dependencies": {
- "lightningcss": "^1.32.0",
- "picomatch": "^4.0.4",
- "postcss": "^8.5.10",
- "rolldown": "1.0.0-rc.17",
- "tinyglobby": "^0.2.16"
+ "esbuild": "^0.25.0",
+ "fdir": "^6.4.4",
+ "picomatch": "^4.0.2",
+ "postcss": "^8.5.3",
+ "rollup": "^4.34.9",
+ "tinyglobby": "^0.2.13"
},
"bin": {
"vite": "bin/vite.js"
},
"engines": {
- "node": "^20.19.0 || >=22.12.0"
+ "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
},
"funding": {
"url": "https://github.com/vitejs/vite?sponsor=1"
@@ -3998,15 +5029,14 @@
"fsevents": "~2.3.3"
},
"peerDependencies": {
- "@types/node": "^20.19.0 || >=22.12.0",
- "@vitejs/devtools": "^0.1.0",
- "esbuild": "^0.27.0 || ^0.28.0",
+ "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
"jiti": ">=1.21.0",
- "less": "^4.0.0",
- "sass": "^1.70.0",
- "sass-embedded": "^1.70.0",
- "stylus": ">=0.54.8",
- "sugarss": "^5.0.0",
+ "less": "*",
+ "lightningcss": "^1.21.0",
+ "sass": "*",
+ "sass-embedded": "*",
+ "stylus": "*",
+ "sugarss": "*",
"terser": "^5.16.0",
"tsx": "^4.8.1",
"yaml": "^2.4.2"
@@ -4015,18 +5045,15 @@
"@types/node": {
"optional": true
},
- "@vitejs/devtools": {
- "optional": true
- },
- "esbuild": {
- "optional": true
- },
"jiti": {
"optional": true
},
"less": {
"optional": true
},
+ "lightningcss": {
+ "optional": true
+ },
"sass": {
"optional": true
},
@@ -4051,16 +5078,16 @@
}
},
"node_modules/vite-node": {
- "version": "3.2.4",
- "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz",
- "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==",
+ "version": "3.0.0-beta.2",
+ "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.0.0-beta.2.tgz",
+ "integrity": "sha512-ofTf6cfRdL30Wbl9n/BX81EyIR5s4PReLmSurrxQ+koLaWUNOEo8E0lCM53OJkb8vpa2URM2nSrxZsIFyvY1rg==",
"dev": true,
"dependencies": {
"cac": "^6.7.14",
- "debug": "^4.4.1",
- "es-module-lexer": "^1.7.0",
- "pathe": "^2.0.3",
- "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0"
+ "debug": "^4.4.0",
+ "es-module-lexer": "^1.5.4",
+ "pathe": "^1.1.2",
+ "vite": "^5.0.0 || ^6.0.0"
},
"bin": {
"vite-node": "vite-node.mjs"
@@ -4072,86 +5099,163 @@
"url": "https://opencollective.com/vitest"
}
},
- "node_modules/vite-node/node_modules/pathe": {
- "version": "2.0.3",
- "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
- "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
- "dev": true
+ "node_modules/vite-tsconfig-paths": {
+ "version": "6.1.1",
+ "resolved": "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-6.1.1.tgz",
+ "integrity": "sha512-2cihq7zliibCCZ8P9cKJrQBkfgdvcFkOOc3Y02o3GWUDLgqjWsZudaoiuOwO/gzTzy17cS5F7ZPo4bsnS4DGkg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.1.1",
+ "globrex": "^0.1.2",
+ "tsconfck": "^3.0.3"
+ },
+ "peerDependencies": {
+ "vite": "*"
+ }
+ },
+ "node_modules/whatwg-encoding": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz",
+ "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==",
+ "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation",
+ "dependencies": {
+ "iconv-lite": "0.6.3"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/whatwg-encoding/node_modules/iconv-lite": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
+ "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/whatwg-mimetype": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz",
+ "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==",
+ "engines": {
+ "node": ">=18"
+ }
},
- "node_modules/vite-node/node_modules/vite": {
- "version": "6.4.2",
- "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.2.tgz",
- "integrity": "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==",
+ "node_modules/which": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/which/-/which-3.0.1.tgz",
+ "integrity": "sha512-XA1b62dzQzLfaEOSQFTCOd5KFf/1VSzZo7/7TUjnya6u0vGGKzU96UQBZTAThCb2j4/xjBAyii1OhRLJEivHvg==",
"dev": true,
"dependencies": {
- "esbuild": "^0.25.0",
- "fdir": "^6.4.4",
- "picomatch": "^4.0.2",
- "postcss": "^8.5.3",
- "rollup": "^4.34.9",
- "tinyglobby": "^0.2.13"
+ "isexe": "^2.0.0"
},
"bin": {
- "vite": "bin/vite.js"
+ "node-which": "bin/which.js"
},
"engines": {
- "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/wrap-ansi": {
+ "version": "8.1.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
+ "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^6.1.0",
+ "string-width": "^5.0.1",
+ "strip-ansi": "^7.0.1"
+ },
+ "engines": {
+ "node": ">=12"
},
"funding": {
- "url": "https://github.com/vitejs/vite?sponsor=1"
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/wrap-ansi-cjs": {
+ "name": "wrap-ansi",
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+ "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
},
- "optionalDependencies": {
- "fsevents": "~2.3.3"
+ "engines": {
+ "node": ">=10"
},
- "peerDependencies": {
- "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
- "jiti": ">=1.21.0",
- "less": "*",
- "lightningcss": "^1.21.0",
- "sass": "*",
- "sass-embedded": "*",
- "stylus": "*",
- "sugarss": "*",
- "terser": "^5.16.0",
- "tsx": "^4.8.1",
- "yaml": "^2.4.2"
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "dependencies": {
+ "color-convert": "^2.0.1"
},
- "peerDependenciesMeta": {
- "@types/node": {
- "optional": true
- },
- "jiti": {
- "optional": true
- },
- "less": {
- "optional": true
- },
- "lightningcss": {
- "optional": true
- },
- "sass": {
- "optional": true
- },
- "sass-embedded": {
- "optional": true
- },
- "stylus": {
- "optional": true
- },
- "sugarss": {
- "optional": true
- },
- "terser": {
- "optional": true
- },
- "tsx": {
- "optional": true
- },
- "yaml": {
- "optional": true
- }
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
+ "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "dev": true
+ },
+ "node_modules/wrap-ansi-cjs/node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dev": true,
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/wrappy": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
+ },
"node_modules/yallist": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
diff --git a/package.json b/package.json
index 0286409..16f2708 100644
--- a/package.json
+++ b/package.json
@@ -9,21 +9,25 @@
"typecheck": "react-router typegen && tsc"
},
"dependencies": {
- "@react-router/node": "7.15.0",
- "@react-router/serve": "7.15.0",
+ "@react-router/node": "^7.3.0",
+ "@react-router/serve": "^7.3.0",
+ "better-sqlite3": "^12.9.0",
+ "cheerio": "^1.2.0",
"isbot": "^5.1.36",
"react": "^19.2.5",
"react-dom": "^19.2.5",
- "react-router": "7.15.0"
+ "react-router": "^7.3.0"
},
"devDependencies": {
- "@react-router/dev": "7.15.0",
- "@tailwindcss/vite": "^4.2.2",
+ "@react-router/dev": "^7.3.0",
+ "@tailwindcss/vite": "^4.2.4",
+ "@types/better-sqlite3": "^7.6.13",
"@types/node": "^22",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
- "tailwindcss": "^4.2.2",
+ "tailwindcss": "^4.2.4",
"typescript": "^5.9.3",
- "vite": "^8.0.3"
+ "vite": "^6.3.5",
+ "vite-tsconfig-paths": "^6.1.1"
}
-} \ No newline at end of file
+}
diff --git a/vite.config.ts b/vite.config.ts
index 7a686e1..0afac80 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -1,10 +1,8 @@
import { reactRouter } from "@react-router/dev/vite";
import tailwindcss from "@tailwindcss/vite";
+import tsconfigPaths from "vite-tsconfig-paths";
import { defineConfig } from "vite";
export default defineConfig({
- plugins: [tailwindcss(), reactRouter()],
- resolve: {
- tsconfigPaths: true,
- },
+ plugins: [tailwindcss(), tsconfigPaths(), reactRouter()],
});